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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a crash on macOS 26 and later when the editor redrew a diagnostic underline or search highlight whose text had been edited away.
- Fixed a crash when an input method, dictation or Look Up asked the editor about text that had already been edited away. (#2339)
- Fixed crashes when the editor's layout, syntax highlighting or accessibility read text that a newer edit had already removed. (#2340)
- A SQLite query that stops early, because the database is locked or the volume returns an error, now says so instead of showing an empty table.
- The XLSX, MQL and SQL Import plugins linked to a documentation page that did not exist. They now point at Import & Export.

## [0.67.0] - 2026-08-21
Expand Down
30 changes: 27 additions & 3 deletions Plugins/SQLiteDriverPlugin/SQLitePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ private actor SQLiteConnectionActor {
var rowsAffected = 0
var truncated = false

while sqlite3_step(statement) == SQLITE_ROW {
var stepResult = sqlite3_step(statement)
while stepResult == SQLITE_ROW {
if rows.count >= PluginRowLimits.emergencyMax {
truncated = true
break
Expand All @@ -200,6 +201,11 @@ private actor SQLiteConnectionActor {
}

rows.append(row)
stepResult = sqlite3_step(statement)
}

if !truncated, stepResult != SQLITE_DONE {
throw SQLitePluginError.queryFailed(String(cString: sqlite3_errmsg(db)))
}

if columns.isEmpty {
Expand Down Expand Up @@ -259,7 +265,8 @@ private actor SQLiteConnectionActor {
var batch: [PluginRow] = []
batch.reserveCapacity(batchSize)

while sqlite3_step(statement) == SQLITE_ROW {
var stepResult = sqlite3_step(statement)
while stepResult == SQLITE_ROW {
if Task.isCancelled {
if !batch.isEmpty {
continuation.yield(.rows(batch))
Expand Down Expand Up @@ -294,12 +301,23 @@ private actor SQLiteConnectionActor {
continuation.yield(.rows(batch))
batch.removeAll(keepingCapacity: true)
}
stepResult = sqlite3_step(statement)
}

if !batch.isEmpty {
continuation.yield(.rows(batch))
}

// A step that ends on anything but `SQLITE_DONE` stopped early: a locked database, an I/O
// error on the volume, a corrupt page. Finishing the stream normally would report the rows
// read so far as the whole table, which is indistinguishable from an empty one.
guard stepResult == SQLITE_DONE else {
let message = String(cString: sqlite3_errmsg(db))
sqlite3_finalize(statement)
continuation.finish(throwing: SQLitePluginError.queryFailed(message))
return
}

sqlite3_finalize(statement)
continuation.finish()
}
Expand Down Expand Up @@ -371,7 +389,8 @@ private actor SQLiteConnectionActor {
var rowsAffected = 0
var truncated = false

while sqlite3_step(statement) == SQLITE_ROW {
var stepResult = sqlite3_step(statement)
while stepResult == SQLITE_ROW {
if rows.count >= PluginRowLimits.emergencyMax {
truncated = true
break
Expand All @@ -398,6 +417,11 @@ private actor SQLiteConnectionActor {
}

rows.append(row)
stepResult = sqlite3_step(statement)
}

if !truncated, stepResult != SQLITE_DONE {
throw SQLitePluginError.queryFailed(String(cString: sqlite3_errmsg(db)))
}

if columns.isEmpty {
Expand Down
38 changes: 38 additions & 0 deletions TableProUITests/SQLiteFirstTableLoadUITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import XCTest

/// Issue #2342. The first table clicked after opening a SQLite database showed "Executing…"
/// forever. Cancelling it and clicking any table afterwards, including the same one, loaded
/// immediately, and an empty table stalled exactly like a large one, so the stall was never the
/// query itself.
final class SQLiteFirstTableLoadUITests: UITestCase {
func testTheFirstTableClickAfterOpeningADatabaseLoadsItsRows() throws {
let app = try launchWithSampleDatabase()
let window = app.windows.firstMatch
XCTAssertTrue(window.waitToExist(timeout: 30))
XCTAssertTrue(
waitForPredicate(timeout: 30) { window.outlines.firstMatch.outlineRows.count > 1 },
"The object browser must list the sample database's tables"
)

clickAtCenter(try tableRow(named: "Album", in: window))

let grid = window.tables.matching(identifier: "data-grid").firstMatch
XCTAssertTrue(grid.waitToExist(timeout: 30), "The first table click must open the data grid")
XCTAssertTrue(
waitForPredicate(timeout: 30) { !grid.tableRows.allElementsBoundByIndex.isEmpty },
"The first table click must load its rows rather than stall"
)
XCTAssertTrue(
waitForPredicate(timeout: 15) { !window.staticTexts["Executing…"].exists },
"The executing indicator must clear once the rows are in"
)
}

private func tableRow(named name: String, in window: XCUIElement) throws -> XCUIElement {
let match = window.outlines.firstMatch.staticTexts
.matching(NSPredicate(format: "value == %@", "Table: \(name)"))
.firstMatch
XCTAssertTrue(match.waitToExist(timeout: 20), "The object browser must list \(name)")
return match
}
}
Loading