Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Explain Analyze running write statements on Read-Only connections and skipping the Alert and Safe Mode confirmation.
- Remote deletions of connections, groups, tags, SSH profiles and table favorites applied with their sync category off.
- **Local only** connections taking edits and deletions made on another device.
- Column header sort on Redis, etcd and Kafka re-running the last command with `ORDER BY` appended.
- Import sheet ignoring a CSV or Excel option change until the next edit, then resetting the column mapping.
- Import sheet showing an earlier table's columns after switching tables while the first was still loading.
- CSV and JSON imports failing on every row, or writing a skipped field, when two fields differ only by case.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ extension PluginMetadataRegistry {
supportsDropSchema: false,
defaultSSLMode: .verifyIdentity,
supportsOpportunisticTLS: false,
verifiesServerWithSystemTrust: true
verifiesServerWithSystemTrust: true,
supportsColumnSort: false
),
schema: PluginMetadataSnapshot.SchemaInfo(
defaultSchemaName: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ extension PluginMetadataRegistry {
supportsQueryProgress: false,
requiresReconnectForDatabaseSwitch: false,
supportsDropDatabase: false,
supportsOpportunisticTLS: false
supportsOpportunisticTLS: false,
supportsColumnSort: false
),
schema: PluginMetadataSnapshot.SchemaInfo(
defaultSchemaName: "public",
Expand Down Expand Up @@ -908,7 +909,8 @@ extension PluginMetadataRegistry {
supportsQueryProgress: false,
requiresReconnectForDatabaseSwitch: false,
supportsDropDatabase: false,
supportsOpportunisticTLS: false
supportsOpportunisticTLS: false,
supportsColumnSort: false
),
schema: PluginMetadataSnapshot.SchemaInfo(
defaultSchemaName: "public",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ struct PluginMetadataRegistryCuratedCapabilityTests {
#expect(built.capabilities.pagination == .leadingRowsOnly(maximumRows: nil))
}

@Test("Redis, etcd and Kafka offer no column sort, since their commands take no ORDER BY", arguments: [
"Redis", "etcd", "Kafka"
])
func commandLanguagesWithoutOrderByOfferNoColumnSort(typeId: String) throws {
let snapshot = try #require(PluginMetadataRegistry.shared.snapshot(forRegisteredTypeId: typeId))

#expect(snapshot.capabilities.supportsColumnSort == false)
}

@Test("ScyllaDB declares the same query limits as Cassandra on its own curated entry")
func scyllaDBDeclaresTheCassandraQueryLimits() {
let registry = PluginMetadataRegistry.shared
Expand Down
64 changes: 64 additions & 0 deletions TableProTests/Views/Main/MainContentCoordinatorSortTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,56 @@ struct MainContentCoordinatorSortTests {
#expect(sorted == ["a", "b", "c"])
}

@Test(
"A Redis, etcd or Kafka result is sorted in place, never re-run with an ORDER BY its language lacks",
arguments: CommandWithoutOrderBy.all
)
func commandWithoutOrderBySortsInPlace(command: CommandWithoutOrderBy) async throws {
let connection = TestFixtures.makeConnection(type: command.type)
let driver = StatementRecordingDriver(connection: connection)
var session = ConnectionSession(connection: connection, driver: driver)
session.status = .connected
DatabaseManager.shared.injectSession(session, for: connection.id)
let tabManager = QueryTabManager()
let coordinator = MainContentCoordinator(
connection: connection,
tabManager: tabManager,
changeManager: DataChangeManager(),
toolbarState: ConnectionToolbarState()
)
defer {
coordinator.cancelAllQueryTasks()
coordinator.teardown()
DatabaseManager.shared.removeSession(for: connection.id)
}
var tab = QueryTab(title: "Query", query: command.text, tabType: .query)
tab.execution.lastExecutedAt = Date()
tabManager.tabs.append(tab)
tabManager.selectedTabId = tab.id
let rows = TableRows.from(
queryRows: [["b"], ["c"], ["a"]].map { row in row.map(PluginCellValue.fromOptional) },
columns: [command.column],
columnTypes: [.text(rawType: nil)]
)
let shown = ResultSet(label: "Result", tableRows: rows)
shown.baseQuery = command.text
tabManager.mutate(tabId: tab.id) { $0.display.replaceUnpinnedResults(with: [shown]) }
coordinator.setActiveTableRows(rows, for: tab.id)

coordinator.handleSortStateChanged(sortState([(0, .ascending)]))
for _ in 0 ..< 500 {
if !coordinator.isShowingSafeModePrompt, !coordinator.tabExecution.isExecuting(tab.id) { break }
try await Task.sleep(for: .milliseconds(10))
}

#expect(driver.sentSQL.isEmpty)
let idx = try #require(tabManager.tabs.firstIndex { $0.id == tab.id })
#expect(tabManager.tabs[idx].content.query == command.text)
#expect(tabManager.tabs[idx].sortState.columns == [SortColumn(columnIndex: 0, direction: .ascending)])
let sorted = coordinator.tabSessionRegistry.tableRows(for: tab.id).rows.map { $0[0].asText }
#expect(sorted == ["a", "b", "c"])
}

@Test("Table tab keeps the rows-per-page LIMIT through ascending, descending, and cleared sort")
func tableTabSortPreservesPageSize() {
let (coordinator, tabManager, tabId) = makeTableCoordinator(pageSize: 10)
Expand Down Expand Up @@ -509,6 +559,20 @@ enum InFlightRunEnding: Sendable {
}
}

struct CommandWithoutOrderBy: Sendable, CustomTestStringConvertible {
let type: DatabaseType
let text: String
let column: String

var testDescription: String { type.rawValue }

static let all = [
CommandWithoutOrderBy(type: .redis, text: "LPUSH mylist a", column: "length"),
CommandWithoutOrderBy(type: .etcd, text: "put k v", column: "Key"),
CommandWithoutOrderBy(type: .kafka, text: "CONSUME \"orders\" FROM NEWEST LIMIT 1", column: "Offset")
]
}

/// One statement as it reached the driver, with the values bound to it.
private struct SentStatement: Equatable {
let sql: String
Expand Down
Loading