Skip to content

Adding public method to reset a prepared statement #1145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 23, 2022
10 changes: 9 additions & 1 deletion Documentation/Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ let query = try db.prepare(users)
for user in query {
// 💥 can throw an error here
}
````
```

#### Failable iteration

Expand Down Expand Up @@ -1872,6 +1872,14 @@ let stmt = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?
for row in stmt.bind(kUTTypeImage) { /* ... */ }
```

> _Note:_ Prepared queries can be reused, and long lived prepared queries should be `reset()` after each use. Otherwise, the transaction (either [implicit or explicit](https://www.sqlite.org/lang_transaction.html#implicit_versus_explicit_transactions)) will be held open until the query is reset or finalized. This can affect performance. Statements are reset automatically during `deinit`.
>
> ```swift
> someObj.statement = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)")
> for row in someObj.statement.bind(kUTTypeImage) { /* ... */ }
> someObj.statement.reset()
> ```

[UTTypeConformsTo]: https://developer.apple.com/documentation/coreservices/1444079-uttypeconformsto

## Custom Aggregations
Expand Down
6 changes: 5 additions & 1 deletion Sources/SQLite/Core/Statement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ public final class Statement {
try connection.sync { try connection.check(sqlite3_step(handle)) == SQLITE_ROW }
}

fileprivate func reset(clearBindings shouldClear: Bool = true) {
public func reset() {
reset(clearBindings: true)
}

fileprivate func reset(clearBindings shouldClear: Bool) {
sqlite3_reset(handle)
if shouldClear { sqlite3_clear_bindings(handle) }
}
Expand Down
38 changes: 38 additions & 0 deletions Tests/SQLiteTests/StatementTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import XCTest
import SQLite

#if SQLITE_SWIFT_STANDALONE
import sqlite3
#elseif SQLITE_SWIFT_SQLCIPHER
import SQLCipher
#elseif os(Linux)
import CSQLite
#else
import SQLite3
#endif

class StatementTests: SQLiteTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
Expand Down Expand Up @@ -34,4 +44,32 @@ class StatementTests: SQLiteTestCase {

XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted())
}

/// Check that a statement reset will close the implicit transaction, allowing wal file to checkpoint
func test_reset_statement() throws {
// insert single row
try insertUsers("bob")

// prepare a statement and read a single row. This will increment the cursor which
// prevents the implicit transaction from closing.
// https://www.sqlite.org/lang_transaction.html#implicit_versus_explicit_transactions
let statement = try db.prepare("SELECT email FROM users")
_ = try statement.step()

// verify implicit transaction is not closed, and the users table is still locked
XCTAssertThrowsError(try db.run("DROP TABLE users")) { error in
if case let Result.error(_, code, _) = error {
XCTAssertEqual(code, SQLITE_LOCKED)
} else {
XCTFail("unexpected error")
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XCTAssertThrowsError


// reset the prepared statement, unlocking the table and allowing the implicit transaction to close
statement.reset()

// truncate succeeds
try db.run("DROP TABLE users")
}

}