Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- iOS: add data to a table from the Shortcuts app. Two new shortcuts, Add Row to Table and Add Rows to Table, pick a saved connection, database or schema, and table, then insert from JSON or CSV. They run without opening the app. (#1788)
- Refresh button in Settings > Plugins > Browse to reload the plugin list on demand. (#1799)
- SQL Favorites: put ;; in a saved query to set where the cursor lands after keyword expansion in the editor. (#1795)
- SELECT queries without their own LIMIT now run with the row cap applied as a real LIMIT on the statement sent to the server, so forgetting a LIMIT no longer pulls millions of rows. The query text in the editor never changes, a query with its own LIMIT is sent as written and not capped, and Execute Query Without Limit (Option+Cmd+Enter, also in the Query menu and the Execute button menu) skips the cap for one run. (#1794)

### Fixed

- Restored table tabs no longer reload all at once or flood failure dialogs on launch. Only the frontmost tab loads immediately; other restored tabs load when you switch to them, and a load failure now shows inline in the tab instead of a dialog. (#1796)
- Execute All Statements now applies the query result row cap to each SELECT in the batch instead of returning every row, and each result tab tracks its own truncation state so Fetch All loads the rest of the result you are viewing. (#1794)
- Queries starting with a comment or with a newline right after the first keyword are now classified correctly: the row cap applies to them, and Safe Mode recognizes such writes and destructive statements instead of letting them through unprompted.
- Enum and set value pickers now populate when the driver reports the values only inside the column type instead of as a separate list.
- The plugin list no longer goes stale. The app now checks the plugin registry for changes at launch, when the plugin browser opens, and before reporting a plugin as missing, so newly published plugins show up and install right away. A registry that cannot be reached now reports a connection problem instead of "Plugin not found". (#1799)
- Dropping a materialized view or foreign table from the sidebar now generates the correct DROP statement instead of DROP TABLE, and drop and truncate statements are schema-qualified. ClickHouse now lists materialized views as their own sidebar section and drops them with the DROP VIEW syntax it requires. (#1800)

Expand Down
29 changes: 19 additions & 10 deletions Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
func mysqlTypeToString(_ fieldPtr: UnsafePointer<MYSQL_FIELD>) -> String {
let field = fieldPtr.pointee
let flags = UInt(field.flags)
let length = field.length

Check warning on line 70 in Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

initialization of immutable value 'length' was never used; consider replacing with assignment to '_' or removing it

// MariaDB extended metadata: detect JSON stored as LONGTEXT.
// `MARIADB_CONST_STRING` is length-prefixed (not null-terminated), so we must read
Expand Down Expand Up @@ -414,26 +414,30 @@

// MARK: - Query Execution

func executeQuery(_ query: String) async throws -> MariaDBPluginQueryResult {
func executeQuery(_ query: String, rowCap: Int? = nil) async throws -> MariaDBPluginQueryResult {
let queryToRun = String(query)

return try await pluginDispatchAsync(on: queue) { [self] in
guard !isShuttingDown else { throw MariaDBPluginError.notConnected }
return try executeQuerySync(queryToRun)
return try executeQuerySync(queryToRun, rowCap: rowCap)
}
}

func executeParameterizedQuery(_ query: String, parameters: [PluginCellValue]) async throws -> MariaDBPluginQueryResult {
func executeParameterizedQuery(
_ query: String,
parameters: [PluginCellValue],
rowCap: Int? = nil
) async throws -> MariaDBPluginQueryResult {
let queryToRun = String(query)
let params = parameters

return try await pluginDispatchAsync(on: queue) { [self] in
guard !isShuttingDown else { throw MariaDBPluginError.notConnected }
return try executeParameterizedQuerySync(queryToRun, parameters: params)
return try executeParameterizedQuerySync(queryToRun, parameters: params, rowCap: rowCap)
}
}

private func executeQuerySync(_ query: String) throws -> MariaDBPluginQueryResult {
private func executeQuerySync(_ query: String, rowCap: Int? = nil) throws -> MariaDBPluginQueryResult {
guard !isShuttingDown, let mysql = self.mysql else {
throw MariaDBPluginError.notConnected
}
Expand Down Expand Up @@ -500,7 +504,7 @@
var rows: [[PluginCellValue]] = []
rows.reserveCapacity(min(1_000, PluginRowLimits.emergencyMax))

let maxRows = PluginRowLimits.emergencyMax
let maxRows = rowCap.map { min(max($0, 1), PluginRowLimits.emergencyMax) } ?? PluginRowLimits.emergencyMax
var truncated = false

while let rowPtr = mysql_fetch_row(resultPtr) {
Expand Down Expand Up @@ -659,7 +663,8 @@
columns: [String],
columnTypes: [UInt32],
columnTypeNames: [String],
columnIsBinary: [Bool]
columnIsBinary: [Bool],
rowCap: Int? = nil
) throws -> (rows: [[PluginCellValue]], isTruncated: Bool) {
let numFields = columns.count
var resultBinds: [MYSQL_BIND] = Array(repeating: MYSQL_BIND(), count: numFields)
Expand Down Expand Up @@ -694,7 +699,7 @@
}

var rows: [[PluginCellValue]] = []
let maxRows = PluginRowLimits.emergencyMax
let maxRows = rowCap.map { min(max($0, 1), PluginRowLimits.emergencyMax) } ?? PluginRowLimits.emergencyMax
var truncated = false

while true {
Expand Down Expand Up @@ -762,7 +767,11 @@
return (rows: rows, isTruncated: truncated)
}

private func executeParameterizedQuerySync(_ query: String, parameters: [PluginCellValue]) throws -> MariaDBPluginQueryResult {
private func executeParameterizedQuerySync(
_ query: String,
parameters: [PluginCellValue],
rowCap: Int? = nil
) throws -> MariaDBPluginQueryResult {
guard !isShuttingDown, let mysql = self.mysql else {
throw MariaDBPluginError.notConnected
}
Expand Down Expand Up @@ -857,7 +866,7 @@
let fetchResult = try fetchResultSet(
from: stmt, metadata: metadata,
columns: columns, columnTypes: columnTypes, columnTypeNames: columnTypeNames,
columnIsBinary: columnIsBinary
columnIsBinary: columnIsBinary, rowCap: rowCap
)

return MariaDBPluginQueryResult(
Expand Down
27 changes: 24 additions & 3 deletions Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
try await executeWithReconnect(query: query, isRetry: false)
}

func executeUserQuery(query: String, rowCap: Int?, parameters: [PluginCellValue]?) async throws -> PluginQueryResult {
let cap = rowCap.flatMap { $0 > 0 ? $0 : nil }
guard let parameters else {
return try await executeWithReconnect(query: query, isRetry: false, rowCap: cap)
}
guard let conn = mariadbConnection else {
throw MariaDBPluginError.notConnected
}
let startTime = Date()
let result = try await conn.executeParameterizedQuery(query, parameters: parameters, rowCap: cap)
return PluginQueryResult(
columns: result.columns,
columnTypeNames: result.columnTypeNames,
rows: result.rows,
rowsAffected: Int(result.affectedRows),
executionTime: Date().timeIntervalSince(startTime),
isTruncated: result.isTruncated,
columnMeta: result.columnMeta
)
}

func executeParameterized(query: String, parameters: [PluginCellValue]) async throws -> PluginQueryResult {
guard let conn = mariadbConnection else {
throw MariaDBPluginError.notConnected
Expand All @@ -134,15 +155,15 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
mariadbConnection?.cancelCurrentQuery()
}

private func executeWithReconnect(query: String, isRetry: Bool) async throws -> PluginQueryResult {
private func executeWithReconnect(query: String, isRetry: Bool, rowCap: Int? = nil) async throws -> PluginQueryResult {
let startTime = Date()

guard let conn = mariadbConnection else {
throw MariaDBPluginError.notConnected
}

do {
let result = try await conn.executeQuery(query)
let result = try await conn.executeQuery(query, rowCap: rowCap)

if result.columns.isEmpty && result.rows.isEmpty {
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down Expand Up @@ -171,7 +192,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
)
} catch let error as MariaDBPluginError where !isRetry && isConnectionLostError(error) {
try await reconnect()
return try await executeWithReconnect(query: query, isRetry: true)
return try await executeWithReconnect(query: query, isRetry: true, rowCap: rowCap)
}
}

Expand Down
5 changes: 5 additions & 0 deletions Plugins/TableProPluginKit/PluginDatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable {
// EXPLAIN query building (optional)
func buildExplainQuery(_ sql: String) -> String?

// Row limit injection for executed queries (optional, return nil to use app-level fallback)
func injectRowLimit(_ sql: String, limit: Int) -> String?

func quoteIdentifier(_ name: String) -> String

func escapeStringLiteral(_ value: String) -> String
Expand Down Expand Up @@ -347,6 +350,8 @@ public extension PluginDatabaseDriver {

func buildExplainQuery(_ sql: String) -> String? { nil }

func injectRowLimit(_ sql: String, limit: Int) -> String? { nil }

func createViewTemplate() -> String? { nil }
func editViewFallbackTemplate(viewName: String) -> String? { nil }
func castColumnToText(_ column: String) -> String { column }
Expand Down
4 changes: 4 additions & 0 deletions Plugins/TableProPluginKit/SqlDialect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public enum SqlDialect: String, Sendable, CaseIterable {
self == .postgres
}

public var supportsHashLineComments: Bool {
self == .mysql
}

public var supportsEscapeStringPrefix: Bool {
self == .postgres
}
Expand Down
1 change: 1 addition & 0 deletions TablePro/Core/Coordinators/PaginationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ final class PaginationCoordinator {
tab.execution.executionTime = result.executionTime
tab.schemaVersion += 1
tab.pagination.resetLoadMore()
tab.display.activeResultSet?.isTruncated = false
}
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(replaceDelta)
parent.toolbarState.setExecuting(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ extension QueryExecutionCoordinator {
sql: String,
connection conn: DatabaseConnection,
isTruncated: Bool = false,
queryParameterValues: [QueryParameter]? = nil
queryParameterValues: [QueryParameter]? = nil,
historySQL: String? = nil
) {
guard let idx = parent.tabManager.tabs.firstIndex(where: { $0.id == tabId }) else { return }

Expand Down Expand Up @@ -148,6 +149,8 @@ extension QueryExecutionCoordinator {
rs.tableName = tab.tableContext.tableName
rs.isEditable = tab.tableContext.isEditable
rs.metadataVersion = tab.metadataVersion
rs.isTruncated = isTruncated
rs.baseQuery = sql

let pinned = tab.display.resultSets.filter(\.isPinned)
tab.display.resultSets = pinned + [rs]
Expand Down Expand Up @@ -191,7 +194,7 @@ extension QueryExecutionCoordinator {
}

QueryHistoryManager.shared.recordQuery(
query: sql,
query: historySQL ?? sql,
connectionId: conn.id,
databaseName: parent.activeDatabaseName,
executionTime: executionTime,
Expand Down
Loading
Loading