From a08216b79805d35ab6c1178f65e4f75a9f226326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 13:51:20 +0800 Subject: [PATCH 1/9] Added support for casting when Codable. --- Sources/SQLite/Core/Statement.swift | 100 ++++++++++-------- Sources/SQLite/Schema/Connection+Schema.swift | 10 +- Sources/SQLite/Typed/Query.swift | 22 ++-- .../Core/Connection+AttachTests.swift | 4 +- Tests/SQLiteTests/Core/ConnectionTests.swift | 2 +- Tests/SQLiteTests/Core/StatementTests.swift | 2 +- .../Extensions/FTSIntegrationTests.swift | 4 +- .../Typed/CustomAggregationTests.swift | 12 +-- .../Typed/QueryIntegrationTests.swift | 34 ++++-- Tests/SQLiteTests/Typed/RowTests.swift | 46 +++++++- 10 files changed, 157 insertions(+), 79 deletions(-) diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index 82d535b9..0780f9a1 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -166,7 +166,7 @@ public final class Statement { reset(clearBindings: false) _ = try step() - return row[0] + return try row.getValue(0) } /// - Parameter bindings: A list of parameters to bind to the statement. @@ -229,9 +229,9 @@ extension Array { } extension Statement: FailableIterator { - public typealias Element = [Binding?] - public func failableNext() throws -> [Binding?]? { - try step() ? Array(row) : nil + public typealias Element = Cursor + public func failableNext() throws -> Cursor? { + try step() ? row : nil } } @@ -258,30 +258,68 @@ extension Statement: CustomStringConvertible { } -public struct Cursor { +public protocol CursorProtocol { + func getValue(_ idx: Int) throws -> Binding? + func getValue(_ idx: Int) throws -> Double + func getValue(_ idx: Int) throws -> Int64 + func getValue(_ idx: Int) throws -> String + func getValue(_ idx: Int) throws -> Blob +} + +extension CursorProtocol { + public func getValue(_ idx: Int) -> T? { + switch T.self { + case is Double.Type: + return try? getValue(idx) as Double as? T + case is Int64.Type: + return try? getValue(idx) as Int64 as? T + case is String.Type: + return try? getValue(idx) as String as? T + case is Blob.Type: + return try? getValue(idx) as Blob as? T + default: + return nil + } + } + + public func getValue(_ idx: Int) throws -> Bool { + try Bool.fromDatatypeValue(getValue(idx)) + } + + public func getValue(_ idx: Int) throws -> Int { + try Int.fromDatatypeValue(getValue(idx)) + } +} - fileprivate let handle: OpaquePointer +public struct Cursor: CursorProtocol { + fileprivate let statement: Statement + fileprivate var handle: OpaquePointer { + statement.handle! + } fileprivate let columnCount: Int fileprivate init(_ statement: Statement) { - handle = statement.handle! + self.statement = statement columnCount = statement.columnCount } - public subscript(idx: Int) -> Double { + public func getValue(_ idx: Int) throws -> Double { sqlite3_column_double(handle, Int32(idx)) } - public subscript(idx: Int) -> Int64 { + public func getValue(_ idx: Int) throws -> Int64 { sqlite3_column_int64(handle, Int32(idx)) } - public subscript(idx: Int) -> String { - String(cString: UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) + public func getValue(_ idx: Int) throws -> String { + guard let text = sqlite3_column_text(handle, Int32(idx)) else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return String(cString: UnsafePointer(text)) } - public subscript(idx: Int) -> Blob { + public func getValue(_ idx: Int) throws -> Blob { if let pointer = sqlite3_column_blob(handle, Int32(idx)) { let length = Int(sqlite3_column_bytes(handle, Int32(idx))) return Blob(bytes: pointer, length: length) @@ -292,48 +330,20 @@ public struct Cursor { } } - // MARK: - - - public subscript(idx: Int) -> Bool { - Bool.fromDatatypeValue(self[idx]) - } - - public subscript(idx: Int) -> Int { - Int.fromDatatypeValue(self[idx]) - } - -} - -/// Cursors provide direct access to a statement’s current row. -extension Cursor: Sequence { - - public subscript(idx: Int) -> Binding? { + public func getValue(_ idx: Int) throws -> Binding? { switch sqlite3_column_type(handle, Int32(idx)) { case SQLITE_BLOB: - return self[idx] as Blob + return try getValue(idx) as Blob case SQLITE_FLOAT: - return self[idx] as Double + return try getValue(idx) as Double case SQLITE_INTEGER: - return self[idx] as Int64 + return try getValue(idx) as Int64 case SQLITE_NULL: return nil case SQLITE_TEXT: - return self[idx] as String + return try getValue(idx) as String case let type: fatalError("unsupported column type: \(type)") } } - - public func makeIterator() -> AnyIterator { - var idx = 0 - return AnyIterator { - if idx >= columnCount { - return .none - } else { - idx += 1 - return self[idx - 1] - } - } - } - } diff --git a/Sources/SQLite/Schema/Connection+Schema.swift b/Sources/SQLite/Schema/Connection+Schema.swift index 2977af17..4c6e2a3b 100644 --- a/Sources/SQLite/Schema/Connection+Schema.swift +++ b/Sources/SQLite/Schema/Connection+Schema.swift @@ -14,10 +14,10 @@ public extension Connection { // https://sqlite.org/pragma.html#pragma_foreign_key_check func foreignKeyCheck(table: String? = nil) throws -> [ForeignKeyError] { try run("PRAGMA foreign_key_check" + (table.map { "(\($0.quote()))" } ?? "")) - .compactMap { (row: [Binding?]) -> ForeignKeyError? in - guard let table = row[0] as? String, - let rowId = row[1] as? Int64, - let target = row[2] as? String else { return nil } + .compactMap { (row: Cursor) -> ForeignKeyError? in + guard let table = row.getValue(0) as String?, + let rowId = row.getValue(1) as Int64?, + let target = row.getValue(2) as String? else { return nil } return ForeignKeyError(from: table, rowId: rowId, to: target) } @@ -29,7 +29,7 @@ public extension Connection { precondition(table == nil || supports(.partialIntegrityCheck), "partial integrity check not supported") return try run("PRAGMA integrity_check" + (table.map { "(\($0.quote()))" } ?? "")) - .compactMap { $0[0] as? String } + .compactMap { $0.getValue(0) as String? } .filter { $0 != "ok" } } } diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index 6162fcc7..f18d5e1b 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -1004,15 +1004,17 @@ public struct RowIterator: FailableIterator { extension Connection { - public func prepare(_ query: QueryType) throws -> AnySequence { + public func prepare(_ query: QueryType) throws -> LazySequence> { let expression = query.expression let statement = try prepare(expression.template, expression.bindings) let columnNames = try columnNamesForQuery(query) - return AnySequence { - AnyIterator { statement.next().map { Row(columnNames, $0) } } - } + return AnyIterator { + statement.next().map { + Row(columnNames, $0) + } + }.lazy } public func prepareRowIterator(_ query: QueryType) throws -> RowIterator { @@ -1172,9 +1174,9 @@ public struct Row { let columnNames: [String: Int] - fileprivate let values: [Binding?] + fileprivate let values: any CursorProtocol - internal init(_ columnNames: [String: Int], _ values: [Binding?]) { + init(_ columnNames: [String: Int], _ values: some CursorProtocol) { self.columnNames = columnNames self.values = values } @@ -1183,7 +1185,7 @@ public struct Row { guard let idx = columnNames[column.quote()] else { return false } - return values[idx] != nil + return (try? values.getValue(idx)) != nil } /// Returns a row’s value for the given column. @@ -1201,7 +1203,9 @@ public struct Row { public func get(_ column: Expression) throws -> V? { func valueAtIndex(_ idx: Int) throws -> V? { - guard let value = values[idx] as? V.Datatype else { return nil } + guard + let value = try (values.getValue(idx) as V.Datatype?) ?? (values.getValue(idx) as Binding? as? V.Datatype) + else { return nil } return try V.fromDatatypeValue(value) as? V } @@ -1237,7 +1241,7 @@ public struct Row { public subscript(column: Expression) -> T? { // swiftlint:disable:next force_try - try! get(column) + try? get(column) } } diff --git a/Tests/SQLiteTests/Core/Connection+AttachTests.swift b/Tests/SQLiteTests/Core/Connection+AttachTests.swift index 0e185da5..83c2292f 100644 --- a/Tests/SQLiteTests/Core/Connection+AttachTests.swift +++ b/Tests/SQLiteTests/Core/Connection+AttachTests.swift @@ -29,7 +29,7 @@ class ConnectionAttachTests: SQLiteTestCase { // query data let rows = try db.prepare(table.select(name)).map { $0[name] } - XCTAssertEqual(["test"], rows) + XCTAssertEqual(["test"], Array(rows)) try db.detach(schemaName) } @@ -44,7 +44,7 @@ class ConnectionAttachTests: SQLiteTestCase { let email = SQLite.Expression("email") let rows = try db.prepare(table.select(email)).map { $0[email] } - XCTAssertEqual(["foo@bar.com"], rows) + XCTAssertEqual(["foo@bar.com"], Array(rows)) try db.detach(schemaName) } diff --git a/Tests/SQLiteTests/Core/ConnectionTests.swift b/Tests/SQLiteTests/Core/ConnectionTests.swift index da50974a..82867fad 100644 --- a/Tests/SQLiteTests/Core/ConnectionTests.swift +++ b/Tests/SQLiteTests/Core/ConnectionTests.swift @@ -175,7 +175,7 @@ class ConnectionTests: SQLiteTestCase { try backup.step() let users = try target.prepare("SELECT email FROM users ORDER BY email") - XCTAssertEqual(users.map { $0[0] as? String }, ["alice@example.com", "betsy@example.com"]) + XCTAssertEqual(users.map { $0.getValue(0) as String? }, ["alice@example.com", "betsy@example.com"]) } func test_transaction_beginsAndCommitsTransactions() throws { diff --git a/Tests/SQLiteTests/Core/StatementTests.swift b/Tests/SQLiteTests/Core/StatementTests.swift index 3c90d941..bbf10105 100644 --- a/Tests/SQLiteTests/Core/StatementTests.swift +++ b/Tests/SQLiteTests/Core/StatementTests.swift @@ -21,7 +21,7 @@ class StatementTests: SQLiteTestCase { try insertUsers("alice") let statement = try db.prepare("SELECT email FROM users") XCTAssert(try statement.step()) - let blob = statement.row[0] as Blob + let blob = try statement.row.getValue(0) as Blob XCTAssertEqual("alice@example.com", String(bytes: blob.bytes, encoding: .utf8)!) } diff --git a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift index b3b9e617..f43a583a 100644 --- a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift +++ b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift @@ -50,14 +50,14 @@ class FTSIntegrationTests: SQLiteTestCase { func testMatch() throws { try createIndex() - let matches = Array(try db.prepare(index.match("Paul"))) + let matches = try db.prepare(index.match("Paul")) XCTAssertEqual(matches.map { $0[email ]}, ["Paul@example.com"]) } func testMatchPartial() throws { try insertUsers("Paula") try createIndex() - let matches = Array(try db.prepare(index.match("Pa*"))) + let matches = try db.prepare(index.match("Pa*")) XCTAssertEqual(matches.map { $0[email ]}, ["Paul@example.com", "Paula@example.com"]) } diff --git a/Tests/SQLiteTests/Typed/CustomAggregationTests.swift b/Tests/SQLiteTests/Typed/CustomAggregationTests.swift index 73a0767f..3198ab7b 100644 --- a/Tests/SQLiteTests/Typed/CustomAggregationTests.swift +++ b/Tests/SQLiteTests/Typed/CustomAggregationTests.swift @@ -46,7 +46,7 @@ class CustomAggregationTests: SQLiteTestCase { let result = try db.prepare("SELECT mySUM1(age) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { - let value = row[i] as? Int64 + let value = row.getValue(i) as Int64? XCTAssertEqual(83, value) } } @@ -70,7 +70,7 @@ class CustomAggregationTests: SQLiteTestCase { } let result = try db.prepare("SELECT mySUM2(age) AS s FROM users GROUP BY admin ORDER BY s") let i = result.columnNames.firstIndex(of: "s")! - let values = result.compactMap { $0[i] as? Int64 } + let values = result.compactMap { $0.getValue(i) as Int64? } XCTAssertTrue(values.elementsEqual([28, 55])) } @@ -83,7 +83,7 @@ class CustomAggregationTests: SQLiteTestCase { let result = try db.prepare("SELECT myReduceSUM1(age) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { - let value = row[i] as? Int64 + let value = row.getValue(i) as Int64? XCTAssertEqual(2083, value) } } @@ -96,7 +96,7 @@ class CustomAggregationTests: SQLiteTestCase { db.createAggregation("myReduceSUM2", initialValue: Int64(3000), reduce: reduce, result: { $0 }) let result = try db.prepare("SELECT myReduceSUM2(age) AS s FROM users GROUP BY admin ORDER BY s") let i = result.columnNames.firstIndex(of: "s")! - let values = result.compactMap { $0[i] as? Int64 } + let values = result.compactMap { $0.getValue(i) as Int64? } XCTAssertTrue(values.elementsEqual([3028, 3055])) } @@ -111,7 +111,7 @@ class CustomAggregationTests: SQLiteTestCase { let i = result.columnNames.firstIndex(of: "s")! for row in result { - let value = row[i] as? String + let value = row.getValue(i) as String? XCTAssertEqual("\(initial)Alice@example.comBob@example.comEve@example.com", value) } } @@ -134,7 +134,7 @@ class CustomAggregationTests: SQLiteTestCase { let result = try! db.prepare("SELECT myReduceSUMX(age) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { - let value = row[i] as? Int64 + let value = row.getValue(i) as Int64? XCTAssertEqual(1083, value) } }() diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index cde5a0c3..9ed26cbc 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -49,7 +49,7 @@ class QueryIntegrationTests: SQLiteTestCase { let names = ["a", "b", "c"] try insertUsers(names) - let emails = try db.prepare("select email from users", []).map { $0[0] as! String } + let emails = try db.prepare("select email from users", []).compactMap { try? $0.getValue(0) as String } XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) } @@ -213,8 +213,8 @@ class QueryIntegrationTests: SQLiteTestCase { let query1 = users.filter(email == "alice@example.com") let query2 = users.filter(email == "sally@example.com") - let actualIDs = try db.prepare(query1.union(query2)).map { $0[id] } - XCTAssertEqual(expectedIDs, actualIDs) + let actualIDs = try db.prepare(query1.union(query2)).map { $0[self.id] } + XCTAssertEqual(expectedIDs, Array(actualIDs)) let query3 = users.select(users[*], SQLite.Expression(literal: "1 AS weight")).filter(email == "sally@example.com") let query4 = users.select(users[*], SQLite.Expression(literal: "2 AS weight")).filter(email == "alice@example.com") @@ -226,8 +226,8 @@ class QueryIntegrationTests: SQLiteTestCase { SELECT "users".*, 2 AS weight FROM "users" WHERE ("email" = 'alice@example.com') ORDER BY weight """) - let orderedIDs = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[id] } - XCTAssertEqual(Array(expectedIDs.reversed()), orderedIDs) + let orderedIDs = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[self.id] } + XCTAssertEqual(Array(expectedIDs.reversed()), Array(orderedIDs)) } func test_no_such_column() throws { @@ -376,7 +376,7 @@ class QueryIntegrationTests: SQLiteTestCase { let results = try db.prepare(users.select(id, cumeDist)).map { $0[cumeDist] } - XCTAssertEqual([0.25, 0.5, 0.75, 1], results) + XCTAssertEqual([0.25, 0.5, 0.75, 1], Array(results)) } func test_select_window_row_number() throws { @@ -444,6 +444,28 @@ class QueryIntegrationTests: SQLiteTestCase { row = try db.pluck(users.select(id, nthValue))! XCTAssertEqual(row[nthValue], "Billy@example.com") } + + func test_codable_cast() throws { + let table = Table("test_codable_cast") + try db.run( + table.create { + $0.column(SQLite.Expression("int")) + $0.column(SQLite.Expression("string")) + $0.column(SQLite.Expression("bool")) + $0.column(SQLite.Expression("float")) + $0.column(SQLite.Expression("double")) + $0.column(SQLite.Expression("date")) + $0.column(SQLite.Expression("uuid")) + $0.column(SQLite.Expression("optional")) + $0.column(SQLite.Expression("sub")) + }) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + try db.run(table.insert(value)) + + let fetchValue: TestCodable = try db.prepare(table).map { try $0.decode() }[0] + XCTAssertEqual(fetchValue, value) + } } extension Connection { diff --git a/Tests/SQLiteTests/Typed/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift index 1aed00a7..75a70ca2 100644 --- a/Tests/SQLiteTests/Typed/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -1,8 +1,50 @@ import XCTest @testable import SQLite +struct CursorWithStringArray: CursorProtocol { + let elements: [Binding?] + init(elements: [Binding?]) { + self.elements = elements + } + + func getValue(_ idx: Int) throws -> Binding? { + elements[idx] + } + func getValue(_ idx: Int) throws -> Double { + guard let value = elements[idx] as? Double else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> Int64 { + guard let value = elements[idx] as? Int64 else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> String { + guard let value = elements[idx] as? String else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> Blob { + guard let value = elements[idx] as? Blob else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } +} + +extension Row { + init(_ columnNames: [String: Int], _ values: [Binding?]) { + self.init(columnNames, CursorWithStringArray(elements: values)) + } +} + class RowTests: XCTestCase { + public func test_get_value() throws { let row = Row(["\"foo\"": 0], ["value"]) let result = try row.get(SQLite.Expression("foo")) @@ -32,14 +74,14 @@ class RowTests: XCTestCase { } public func test_get_value_optional_nil() throws { - let row = Row(["\"foo\"": 0], [nil]) + let row = Row(["\"foo\"": 0], [String?.none]) let result = try row.get(SQLite.Expression("foo")) XCTAssertNil(result) } public func test_get_value_optional_nil_subscript() { - let row = Row(["\"foo\"": 0], [nil]) + let row = Row(["\"foo\"": 0], [String?.none]) let result = row[SQLite.Expression("foo")] XCTAssertNil(result) From e17fc4d77a01499afec7c0f5237bd980309f3b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 13:59:22 +0800 Subject: [PATCH 2/9] lint --- Sources/SQLite/Typed/Query.swift | 12 +++--------- Tests/SQLiteTests/Typed/RowTests.swift | 2 -- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index f18d5e1b..84b111ae 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -1010,11 +1010,7 @@ extension Connection { let columnNames = try columnNamesForQuery(query) - return AnyIterator { - statement.next().map { - Row(columnNames, $0) - } - }.lazy + return AnyIterator { statement.next().map { Row(columnNames, $0) } }.lazy } public func prepareRowIterator(_ query: QueryType) throws -> RowIterator { @@ -1203,9 +1199,7 @@ public struct Row { public func get(_ column: Expression) throws -> V? { func valueAtIndex(_ idx: Int) throws -> V? { - guard - let value = try (values.getValue(idx) as V.Datatype?) ?? (values.getValue(idx) as Binding? as? V.Datatype) - else { return nil } + guard let value = try (values.getValue(idx) as V.Datatype?) ?? (values.getValue(idx) as Binding? as? V.Datatype) else { return nil } return try V.fromDatatypeValue(value) as? V } @@ -1241,7 +1235,7 @@ public struct Row { public subscript(column: Expression) -> T? { // swiftlint:disable:next force_try - try? get(column) + try! get(column) } } diff --git a/Tests/SQLiteTests/Typed/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift index 75a70ca2..b4d08b5e 100644 --- a/Tests/SQLiteTests/Typed/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -43,8 +43,6 @@ extension Row { } class RowTests: XCTestCase { - - public func test_get_value() throws { let row = Row(["\"foo\"": 0], ["value"]) let result = try row.get(SQLite.Expression("foo")) From dcd8dd7647ab281ba9c79459ea19d0c83914a9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 16:49:16 +0800 Subject: [PATCH 3/9] Reuse CursorWithBindingArray to fix step issue --- Sources/SQLite/Core/Statement.swift | 35 ++++++++++++ Sources/SQLite/Typed/Query+Lazy.swift | 57 +++++++++++++++++++ Sources/SQLite/Typed/Query.swift | 16 ++---- .../Extensions/FTSIntegrationTests.swift | 10 +++- Tests/SQLiteTests/Typed/RowTests.swift | 41 ------------- 5 files changed, 105 insertions(+), 54 deletions(-) create mode 100644 Sources/SQLite/Typed/Query+Lazy.swift diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index 0780f9a1..bf0782b1 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -291,6 +291,41 @@ extension CursorProtocol { } } +struct CursorWithBindingArray: CursorProtocol { + let elements: [Binding?] + init(elements: [Binding?]) { + self.elements = elements + } + + func getValue(_ idx: Int) throws -> Binding? { + elements[idx] + } + func getValue(_ idx: Int) throws -> Double { + guard let value = elements[idx] as? Double else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> Int64 { + guard let value = elements[idx] as? Int64 else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> String { + guard let value = elements[idx] as? String else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> Blob { + guard let value = elements[idx] as? Blob else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } +} + public struct Cursor: CursorProtocol { fileprivate let statement: Statement fileprivate var handle: OpaquePointer { diff --git a/Sources/SQLite/Typed/Query+Lazy.swift b/Sources/SQLite/Typed/Query+Lazy.swift new file mode 100644 index 00000000..028d2a0f --- /dev/null +++ b/Sources/SQLite/Typed/Query+Lazy.swift @@ -0,0 +1,57 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +extension Array where Element == LazySequence> { + @available(swift, deprecated: 1, message: "Please use return value of prepare as Array directly") + public init(_ values: Element) { + preconditionFailure("Please use return value of prepare as Array directly") + } +} + +extension Connection { + @_disfavoredOverload + public func prepare(_ query: QueryType) throws -> [Row] { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) + + let columnNames = try columnNamesForQuery(query) + + return Array(AnyIterator { + statement.next().map { cursor in + Row(columnNames, (0.. LazySequence> { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) + + let columnNames = try columnNamesForQuery(query) + + return AnyIterator { statement.next().map { Row(columnNames, $0) } }.lazy + } +} diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index 84b111ae..5ebeeb57 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -1003,16 +1003,6 @@ public struct RowIterator: FailableIterator { } extension Connection { - - public func prepare(_ query: QueryType) throws -> LazySequence> { - let expression = query.expression - let statement = try prepare(expression.template, expression.bindings) - - let columnNames = try columnNamesForQuery(query) - - return AnyIterator { statement.next().map { Row(columnNames, $0) } }.lazy - } - public func prepareRowIterator(_ query: QueryType) throws -> RowIterator { let expression = query.expression let statement = try prepare(expression.template, expression.bindings) @@ -1027,7 +1017,7 @@ extension Connection { try prepare(statement, bindings).prepareRowIterator() } - private func columnNamesForQuery(_ query: QueryType) throws -> [String: Int] { + func columnNamesForQuery(_ query: QueryType) throws -> [String: Int] { var (columnNames, idx) = ([String: Int](), 0) column: for each in query.clauses.select.columns { var names = each.expression.template.split { $0 == "." }.map(String.init) @@ -1177,6 +1167,10 @@ public struct Row { self.values = values } + init(_ columnNames: [String: Int], _ values: [Binding?]) { + self.init(columnNames, CursorWithBindingArray(elements: values)) + } + func hasValue(for column: String) -> Bool { guard let idx = columnNames[column.quote()] else { return false diff --git a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift index f43a583a..41379c6d 100644 --- a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift +++ b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift @@ -54,6 +54,12 @@ class FTSIntegrationTests: SQLiteTestCase { XCTAssertEqual(matches.map { $0[email ]}, ["Paul@example.com"]) } + func testAny() throws { + try createIndex() + let match = try db.prepare(index.match("Paul")).last + XCTAssertEqual(match?[email], "Paul@example.com") + } + func testMatchPartial() throws { try insertUsers("Paula") try createIndex() @@ -63,8 +69,8 @@ class FTSIntegrationTests: SQLiteTestCase { func testTrigramIndex() throws { try createTrigramIndex() - let matches = Array(try db.prepare(index.match("Paul"))) - XCTAssertEqual(1, matches.count) + let matcheCount = try db.prepare(index.match("Paul")).count + XCTAssertEqual(1, matcheCount) } private func createOrSkip(_ createIndex: (Connection) throws -> Void) throws { diff --git a/Tests/SQLiteTests/Typed/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift index b4d08b5e..a75127b3 100644 --- a/Tests/SQLiteTests/Typed/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -1,47 +1,6 @@ import XCTest @testable import SQLite -struct CursorWithStringArray: CursorProtocol { - let elements: [Binding?] - init(elements: [Binding?]) { - self.elements = elements - } - - func getValue(_ idx: Int) throws -> Binding? { - elements[idx] - } - func getValue(_ idx: Int) throws -> Double { - guard let value = elements[idx] as? Double else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } - func getValue(_ idx: Int) throws -> Int64 { - guard let value = elements[idx] as? Int64 else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } - func getValue(_ idx: Int) throws -> String { - guard let value = elements[idx] as? String else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } - func getValue(_ idx: Int) throws -> Blob { - guard let value = elements[idx] as? Blob else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } -} - -extension Row { - init(_ columnNames: [String: Int], _ values: [Binding?]) { - self.init(columnNames, CursorWithStringArray(elements: values)) - } -} - class RowTests: XCTestCase { public func test_get_value() throws { let row = Row(["\"foo\"": 0], ["value"]) From 0246cbd240c11f358f0688da056509b5c96ce846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 17:20:24 +0800 Subject: [PATCH 4/9] fix test --- Tests/SQLiteTests/Typed/QueryIntegrationTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index 9ed26cbc..3c0c57e5 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -272,8 +272,8 @@ class QueryIntegrationTests: SQLiteTestCase { // https://github.com/stephencelis/SQLite.swift/issues/285 func test_order_by_random() throws { try insertUsers(["a", "b", "c'"]) - let result = Array(try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1))) - XCTAssertEqual(1, result.count) + let resultCount = try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1)).count + XCTAssertEqual(1, resultCount) } func test_with_recursive() throws { From 25e938516be42fca32ea66579b10f3f0ce9869be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 17:25:23 +0800 Subject: [PATCH 5/9] Update test --- .../Core/Connection+AttachTests.swift | 8 ++++---- .../Extensions/FTSIntegrationTests.swift | 4 ++-- .../Typed/QueryIntegrationTests.swift | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/SQLiteTests/Core/Connection+AttachTests.swift b/Tests/SQLiteTests/Core/Connection+AttachTests.swift index 83c2292f..8d3a1832 100644 --- a/Tests/SQLiteTests/Core/Connection+AttachTests.swift +++ b/Tests/SQLiteTests/Core/Connection+AttachTests.swift @@ -28,8 +28,8 @@ class ConnectionAttachTests: SQLiteTestCase { _ = try db.run(table.insert(name <- "test")) // query data - let rows = try db.prepare(table.select(name)).map { $0[name] } - XCTAssertEqual(["test"], Array(rows)) + let rows: [_] = try db.prepare(table.select(name)).map { $0[name] } + XCTAssertEqual(["test"], rows) try db.detach(schemaName) } @@ -43,8 +43,8 @@ class ConnectionAttachTests: SQLiteTestCase { let table = Table("tests", database: schemaName) let email = SQLite.Expression("email") - let rows = try db.prepare(table.select(email)).map { $0[email] } - XCTAssertEqual(["foo@bar.com"], Array(rows)) + let rows: [_] = try db.prepare(table.select(email)).map { $0[email] } + XCTAssertEqual(["foo@bar.com"], rows) try db.detach(schemaName) } diff --git a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift index 41379c6d..88ae7aca 100644 --- a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift +++ b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift @@ -69,8 +69,8 @@ class FTSIntegrationTests: SQLiteTestCase { func testTrigramIndex() throws { try createTrigramIndex() - let matcheCount = try db.prepare(index.match("Paul")).count - XCTAssertEqual(1, matcheCount) + let matches: [_] = try db.prepare(index.match("Paul")) + XCTAssertEqual(1, matches.count) } private func createOrSkip(_ createIndex: (Connection) throws -> Void) throws { diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index 3c0c57e5..dfee43ad 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -213,8 +213,8 @@ class QueryIntegrationTests: SQLiteTestCase { let query1 = users.filter(email == "alice@example.com") let query2 = users.filter(email == "sally@example.com") - let actualIDs = try db.prepare(query1.union(query2)).map { $0[self.id] } - XCTAssertEqual(expectedIDs, Array(actualIDs)) + let actualIDs: [_] = try db.prepare(query1.union(query2)).map { $0[self.id] } + XCTAssertEqual(expectedIDs,actualIDs) let query3 = users.select(users[*], SQLite.Expression(literal: "1 AS weight")).filter(email == "sally@example.com") let query4 = users.select(users[*], SQLite.Expression(literal: "2 AS weight")).filter(email == "alice@example.com") @@ -226,8 +226,8 @@ class QueryIntegrationTests: SQLiteTestCase { SELECT "users".*, 2 AS weight FROM "users" WHERE ("email" = 'alice@example.com') ORDER BY weight """) - let orderedIDs = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[self.id] } - XCTAssertEqual(Array(expectedIDs.reversed()), Array(orderedIDs)) + let orderedIDs: [_] = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[self.id] } + XCTAssertEqual(expectedIDs.reversed(), orderedIDs) } func test_no_such_column() throws { @@ -272,8 +272,8 @@ class QueryIntegrationTests: SQLiteTestCase { // https://github.com/stephencelis/SQLite.swift/issues/285 func test_order_by_random() throws { try insertUsers(["a", "b", "c'"]) - let resultCount = try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1)).count - XCTAssertEqual(1, resultCount) + let result: [_] = try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1)) + XCTAssertEqual(1, result.count) } func test_with_recursive() throws { @@ -373,10 +373,10 @@ class QueryIntegrationTests: SQLiteTestCase { try insertUser("Billy") let cumeDist = cumeDist(email) - let results = try db.prepare(users.select(id, cumeDist)).map { + let results: [_] = try db.prepare(users.select(id, cumeDist)).map { $0[cumeDist] } - XCTAssertEqual([0.25, 0.5, 0.75, 1], Array(results)) + XCTAssertEqual([0.25, 0.5, 0.75, 1], results) } func test_select_window_row_number() throws { From 91c30afef9204312cb99dc935b1405b9f66ffcc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 17:27:15 +0800 Subject: [PATCH 6/9] format --- Sources/SQLite/Typed/Query.swift | 10 ++-- .../Core/Connection+AttachTests.swift | 2 +- Tests/SQLiteTests/Core/ConnectionTests.swift | 2 +- .../Extensions/FTSIntegrationTests.swift | 12 ++--- .../Typed/QueryIntegrationTests.swift | 54 +++++++++---------- Tests/SQLiteTests/Typed/RowTests.swift | 2 +- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index 5ebeeb57..25482589 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -1160,22 +1160,22 @@ public struct Row { let columnNames: [String: Int] - fileprivate let values: any CursorProtocol + fileprivate let values: any CursorProtocol init(_ columnNames: [String: Int], _ values: some CursorProtocol) { self.columnNames = columnNames self.values = values } - init(_ columnNames: [String: Int], _ values: [Binding?]) { - self.init(columnNames, CursorWithBindingArray(elements: values)) - } + init(_ columnNames: [String: Int], _ values: [Binding?]) { + self.init(columnNames, CursorWithBindingArray(elements: values)) + } func hasValue(for column: String) -> Bool { guard let idx = columnNames[column.quote()] else { return false } - return (try? values.getValue(idx)) != nil + return (try? values.getValue(idx)) != nil } /// Returns a row’s value for the given column. diff --git a/Tests/SQLiteTests/Core/Connection+AttachTests.swift b/Tests/SQLiteTests/Core/Connection+AttachTests.swift index 8d3a1832..4e01db23 100644 --- a/Tests/SQLiteTests/Core/Connection+AttachTests.swift +++ b/Tests/SQLiteTests/Core/Connection+AttachTests.swift @@ -28,7 +28,7 @@ class ConnectionAttachTests: SQLiteTestCase { _ = try db.run(table.insert(name <- "test")) // query data - let rows: [_] = try db.prepare(table.select(name)).map { $0[name] } + let rows: [_] = try db.prepare(table.select(name)).map { $0[name] } XCTAssertEqual(["test"], rows) try db.detach(schemaName) diff --git a/Tests/SQLiteTests/Core/ConnectionTests.swift b/Tests/SQLiteTests/Core/ConnectionTests.swift index 82867fad..1b960264 100644 --- a/Tests/SQLiteTests/Core/ConnectionTests.swift +++ b/Tests/SQLiteTests/Core/ConnectionTests.swift @@ -175,7 +175,7 @@ class ConnectionTests: SQLiteTestCase { try backup.step() let users = try target.prepare("SELECT email FROM users ORDER BY email") - XCTAssertEqual(users.map { $0.getValue(0) as String? }, ["alice@example.com", "betsy@example.com"]) + XCTAssertEqual(users.map { $0.getValue(0) as String? }, ["alice@example.com", "betsy@example.com"]) } func test_transaction_beginsAndCommitsTransactions() throws { diff --git a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift index 88ae7aca..4cf8b08b 100644 --- a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift +++ b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift @@ -54,11 +54,11 @@ class FTSIntegrationTests: SQLiteTestCase { XCTAssertEqual(matches.map { $0[email ]}, ["Paul@example.com"]) } - func testAny() throws { - try createIndex() - let match = try db.prepare(index.match("Paul")).last - XCTAssertEqual(match?[email], "Paul@example.com") - } + func testAny() throws { + try createIndex() + let match = try db.prepare(index.match("Paul")).last + XCTAssertEqual(match?[email], "Paul@example.com") + } func testMatchPartial() throws { try insertUsers("Paula") @@ -69,7 +69,7 @@ class FTSIntegrationTests: SQLiteTestCase { func testTrigramIndex() throws { try createTrigramIndex() - let matches: [_] = try db.prepare(index.match("Paul")) + let matches: [_] = try db.prepare(index.match("Paul")) XCTAssertEqual(1, matches.count) } diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index dfee43ad..7fe7be8a 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -49,7 +49,7 @@ class QueryIntegrationTests: SQLiteTestCase { let names = ["a", "b", "c"] try insertUsers(names) - let emails = try db.prepare("select email from users", []).compactMap { try? $0.getValue(0) as String } + let emails = try db.prepare("select email from users", []).compactMap { try? $0.getValue(0) as String } XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) } @@ -213,7 +213,7 @@ class QueryIntegrationTests: SQLiteTestCase { let query1 = users.filter(email == "alice@example.com") let query2 = users.filter(email == "sally@example.com") - let actualIDs: [_] = try db.prepare(query1.union(query2)).map { $0[self.id] } + let actualIDs: [_] = try db.prepare(query1.union(query2)).map { $0[self.id] } XCTAssertEqual(expectedIDs,actualIDs) let query3 = users.select(users[*], SQLite.Expression(literal: "1 AS weight")).filter(email == "sally@example.com") @@ -226,7 +226,7 @@ class QueryIntegrationTests: SQLiteTestCase { SELECT "users".*, 2 AS weight FROM "users" WHERE ("email" = 'alice@example.com') ORDER BY weight """) - let orderedIDs: [_] = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[self.id] } + let orderedIDs: [_] = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[self.id] } XCTAssertEqual(expectedIDs.reversed(), orderedIDs) } @@ -272,8 +272,8 @@ class QueryIntegrationTests: SQLiteTestCase { // https://github.com/stephencelis/SQLite.swift/issues/285 func test_order_by_random() throws { try insertUsers(["a", "b", "c'"]) - let result: [_] = try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1)) - XCTAssertEqual(1, result.count) + let result: [_] = try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1)) + XCTAssertEqual(1, result.count) } func test_with_recursive() throws { @@ -373,7 +373,7 @@ class QueryIntegrationTests: SQLiteTestCase { try insertUser("Billy") let cumeDist = cumeDist(email) - let results: [_] = try db.prepare(users.select(id, cumeDist)).map { + let results: [_] = try db.prepare(users.select(id, cumeDist)).map { $0[cumeDist] } XCTAssertEqual([0.25, 0.5, 0.75, 1], results) @@ -445,27 +445,27 @@ class QueryIntegrationTests: SQLiteTestCase { XCTAssertEqual(row[nthValue], "Billy@example.com") } - func test_codable_cast() throws { - let table = Table("test_codable_cast") - try db.run( - table.create { - $0.column(SQLite.Expression("int")) - $0.column(SQLite.Expression("string")) - $0.column(SQLite.Expression("bool")) - $0.column(SQLite.Expression("float")) - $0.column(SQLite.Expression("double")) - $0.column(SQLite.Expression("date")) - $0.column(SQLite.Expression("uuid")) - $0.column(SQLite.Expression("optional")) - $0.column(SQLite.Expression("sub")) - }) - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, - date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) - try db.run(table.insert(value)) - - let fetchValue: TestCodable = try db.prepare(table).map { try $0.decode() }[0] - XCTAssertEqual(fetchValue, value) - } + func test_codable_cast() throws { + let table = Table("test_codable_cast") + try db.run( + table.create { + $0.column(SQLite.Expression("int")) + $0.column(SQLite.Expression("string")) + $0.column(SQLite.Expression("bool")) + $0.column(SQLite.Expression("float")) + $0.column(SQLite.Expression("double")) + $0.column(SQLite.Expression("date")) + $0.column(SQLite.Expression("uuid")) + $0.column(SQLite.Expression("optional")) + $0.column(SQLite.Expression("sub")) + }) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + try db.run(table.insert(value)) + + let fetchValue: TestCodable = try db.prepare(table).map { try $0.decode() }[0] + XCTAssertEqual(fetchValue, value) + } } extension Connection { diff --git a/Tests/SQLiteTests/Typed/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift index a75127b3..ed54e576 100644 --- a/Tests/SQLiteTests/Typed/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -31,7 +31,7 @@ class RowTests: XCTestCase { } public func test_get_value_optional_nil() throws { - let row = Row(["\"foo\"": 0], [String?.none]) + let row = Row(["\"foo\"": 0], [String?.none]) let result = try row.get(SQLite.Expression("foo")) XCTAssertNil(result) From a8bab56976d5cba573eaf5a4164743dde376a815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 17:28:33 +0800 Subject: [PATCH 7/9] format --- Sources/SQLite/Core/Statement.swift | 134 +++++++++--------- Sources/SQLite/Schema/Connection+Schema.swift | 2 +- Sources/SQLite/Typed/Query+Lazy.swift | 46 +++--- 3 files changed, 91 insertions(+), 91 deletions(-) diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index bf0782b1..c3da9155 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -166,7 +166,7 @@ public final class Statement { reset(clearBindings: false) _ = try step() - return try row.getValue(0) + return try row.getValue(0) } /// - Parameter bindings: A list of parameters to bind to the statement. @@ -259,83 +259,83 @@ extension Statement: CustomStringConvertible { } public protocol CursorProtocol { - func getValue(_ idx: Int) throws -> Binding? - func getValue(_ idx: Int) throws -> Double - func getValue(_ idx: Int) throws -> Int64 - func getValue(_ idx: Int) throws -> String - func getValue(_ idx: Int) throws -> Blob + func getValue(_ idx: Int) throws -> Binding? + func getValue(_ idx: Int) throws -> Double + func getValue(_ idx: Int) throws -> Int64 + func getValue(_ idx: Int) throws -> String + func getValue(_ idx: Int) throws -> Blob } extension CursorProtocol { - public func getValue(_ idx: Int) -> T? { - switch T.self { - case is Double.Type: - return try? getValue(idx) as Double as? T - case is Int64.Type: - return try? getValue(idx) as Int64 as? T - case is String.Type: - return try? getValue(idx) as String as? T - case is Blob.Type: - return try? getValue(idx) as Blob as? T - default: - return nil - } - } - - public func getValue(_ idx: Int) throws -> Bool { - try Bool.fromDatatypeValue(getValue(idx)) - } - - public func getValue(_ idx: Int) throws -> Int { - try Int.fromDatatypeValue(getValue(idx)) - } + public func getValue(_ idx: Int) -> T? { + switch T.self { + case is Double.Type: + return try? getValue(idx) as Double as? T + case is Int64.Type: + return try? getValue(idx) as Int64 as? T + case is String.Type: + return try? getValue(idx) as String as? T + case is Blob.Type: + return try? getValue(idx) as Blob as? T + default: + return nil + } + } + + public func getValue(_ idx: Int) throws -> Bool { + try Bool.fromDatatypeValue(getValue(idx)) + } + + public func getValue(_ idx: Int) throws -> Int { + try Int.fromDatatypeValue(getValue(idx)) + } } struct CursorWithBindingArray: CursorProtocol { - let elements: [Binding?] - init(elements: [Binding?]) { - self.elements = elements - } - - func getValue(_ idx: Int) throws -> Binding? { - elements[idx] - } - func getValue(_ idx: Int) throws -> Double { - guard let value = elements[idx] as? Double else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } - func getValue(_ idx: Int) throws -> Int64 { - guard let value = elements[idx] as? Int64 else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } - func getValue(_ idx: Int) throws -> String { - guard let value = elements[idx] as? String else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } - func getValue(_ idx: Int) throws -> Blob { - guard let value = elements[idx] as? Blob else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } - return value - } + let elements: [Binding?] + init(elements: [Binding?]) { + self.elements = elements + } + + func getValue(_ idx: Int) throws -> Binding? { + elements[idx] + } + func getValue(_ idx: Int) throws -> Double { + guard let value = elements[idx] as? Double else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> Int64 { + guard let value = elements[idx] as? Int64 else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> String { + guard let value = elements[idx] as? String else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } + func getValue(_ idx: Int) throws -> Blob { + guard let value = elements[idx] as? Blob else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } + return value + } } public struct Cursor: CursorProtocol { fileprivate let statement: Statement - fileprivate var handle: OpaquePointer { - statement.handle! - } + fileprivate var handle: OpaquePointer { + statement.handle! + } fileprivate let columnCount: Int fileprivate init(_ statement: Statement) { - self.statement = statement + self.statement = statement columnCount = statement.columnCount } @@ -348,9 +348,9 @@ public struct Cursor: CursorProtocol { } public func getValue(_ idx: Int) throws -> String { - guard let text = sqlite3_column_text(handle, Int32(idx)) else { - throw QueryError.unexpectedNullValue(name: "column at index \(idx)") - } + guard let text = sqlite3_column_text(handle, Int32(idx)) else { + throw QueryError.unexpectedNullValue(name: "column at index \(idx)") + } return String(cString: UnsafePointer(text)) } diff --git a/Sources/SQLite/Schema/Connection+Schema.swift b/Sources/SQLite/Schema/Connection+Schema.swift index 4c6e2a3b..137ac74d 100644 --- a/Sources/SQLite/Schema/Connection+Schema.swift +++ b/Sources/SQLite/Schema/Connection+Schema.swift @@ -15,7 +15,7 @@ public extension Connection { func foreignKeyCheck(table: String? = nil) throws -> [ForeignKeyError] { try run("PRAGMA foreign_key_check" + (table.map { "(\($0.quote()))" } ?? "")) .compactMap { (row: Cursor) -> ForeignKeyError? in - guard let table = row.getValue(0) as String?, + guard let table = row.getValue(0) as String?, let rowId = row.getValue(1) as Int64?, let target = row.getValue(2) as String? else { return nil } diff --git a/Sources/SQLite/Typed/Query+Lazy.swift b/Sources/SQLite/Typed/Query+Lazy.swift index 028d2a0f..f1ba2b63 100644 --- a/Sources/SQLite/Typed/Query+Lazy.swift +++ b/Sources/SQLite/Typed/Query+Lazy.swift @@ -23,35 +23,35 @@ // extension Array where Element == LazySequence> { - @available(swift, deprecated: 1, message: "Please use return value of prepare as Array directly") - public init(_ values: Element) { - preconditionFailure("Please use return value of prepare as Array directly") - } + @available(swift, deprecated: 1, message: "Please use return value of prepare as Array directly") + public init(_ values: Element) { + preconditionFailure("Please use return value of prepare as Array directly") + } } extension Connection { - @_disfavoredOverload - public func prepare(_ query: QueryType) throws -> [Row] { - let expression = query.expression - let statement = try prepare(expression.template, expression.bindings) + @_disfavoredOverload + public func prepare(_ query: QueryType) throws -> [Row] { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) - let columnNames = try columnNamesForQuery(query) + let columnNames = try columnNamesForQuery(query) - return Array(AnyIterator { - statement.next().map { cursor in - Row(columnNames, (0.. LazySequence> { - let expression = query.expression - let statement = try prepare(expression.template, expression.bindings) + public func prepare(_ query: QueryType) throws -> LazySequence> { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) - let columnNames = try columnNamesForQuery(query) + let columnNames = try columnNamesForQuery(query) - return AnyIterator { statement.next().map { Row(columnNames, $0) } }.lazy - } + return AnyIterator { statement.next().map { Row(columnNames, $0) } }.lazy + } } From ba0d63b5f98c7cab760d3c6f3d5bbf68ef9fcc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 18:27:12 +0800 Subject: [PATCH 8/9] fix lint --- Tests/SQLiteTests/Typed/QueryIntegrationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index 7fe7be8a..1f18f739 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -214,7 +214,7 @@ class QueryIntegrationTests: SQLiteTestCase { let query2 = users.filter(email == "sally@example.com") let actualIDs: [_] = try db.prepare(query1.union(query2)).map { $0[self.id] } - XCTAssertEqual(expectedIDs,actualIDs) + XCTAssertEqual(expectedIDs, actualIDs) let query3 = users.select(users[*], SQLite.Expression(literal: "1 AS weight")).filter(email == "sally@example.com") let query4 = users.select(users[*], SQLite.Expression(literal: "2 AS weight")).filter(email == "alice@example.com") From 86f3c775b987db6e2f4095fc25afd6b5e6177c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=BB=9B=E6=B7=B3=E5=8D=8E?= Date: Sat, 19 Jul 2025 18:53:14 +0800 Subject: [PATCH 9/9] fix build --- SQLite.xcodeproj/project.pbxproj | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index ec0423e9..fd786173 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -202,6 +202,11 @@ 64B8E1702B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; 64B8E1712B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; 64B8E1722B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 915BE7892E2BB05300360423 /* Query+Lazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915BE7882E2BB05300360423 /* Query+Lazy.swift */; }; + 915BE78A2E2BB05300360423 /* Query+Lazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915BE7882E2BB05300360423 /* Query+Lazy.swift */; }; + 915BE78B2E2BB05300360423 /* Query+Lazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915BE7882E2BB05300360423 /* Query+Lazy.swift */; }; + 915BE78C2E2BB05300360423 /* Query+Lazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915BE7882E2BB05300360423 /* Query+Lazy.swift */; }; + 915BE78D2E2BB05300360423 /* Query+Lazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915BE7882E2BB05300360423 /* Query+Lazy.swift */; }; 997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; 997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; 997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; @@ -428,6 +433,7 @@ 49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFunctions.swift; sourceTree = ""; }; 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFunctionsTests.swift; sourceTree = ""; }; + 915BE7882E2BB05300360423 /* Query+Lazy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+Lazy.swift"; sourceTree = ""; }; 997DF2AD287FC06D00F8DF95 /* Query+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+with.swift"; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB58B21028FB864300F8EEA4 /* SchemaReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaReader.swift; sourceTree = ""; }; @@ -733,6 +739,7 @@ EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */, EE247AFE1C3F06E900AE3E12 /* Expression.swift */, EE247AFF1C3F06E900AE3E12 /* Operators.swift */, + 915BE7882E2BB05300360423 /* Query+Lazy.swift */, EE247B001C3F06E900AE3E12 /* Query.swift */, 997DF2AD287FC06D00F8DF95 /* Query+with.swift */, EE247B011C3F06E900AE3E12 /* Schema.swift */, @@ -1133,6 +1140,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 915BE78D2E2BB05300360423 /* Query+Lazy.swift in Sources */, 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */, 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, @@ -1220,6 +1228,7 @@ 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */, 997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */, 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */, + 915BE78C2E2BB05300360423 /* Query+Lazy.swift in Sources */, 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */, DB58B21428FB864300F8EEA4 /* SchemaReader.swift in Sources */, 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */, @@ -1262,6 +1271,7 @@ DEB306BD2B61CEF500F9D46B /* Coding.swift in Sources */, DEB306BE2B61CEF500F9D46B /* RTree.swift in Sources */, DEB306BF2B61CEF500F9D46B /* Blob.swift in Sources */, + 915BE7892E2BB05300360423 /* Query+Lazy.swift in Sources */, DEB306C02B61CEF500F9D46B /* URIQueryParameter.swift in Sources */, DEB306C12B61CEF500F9D46B /* Foundation.swift in Sources */, DEB306C22B61CEF500F9D46B /* Connection.swift in Sources */, @@ -1338,6 +1348,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 915BE78A2E2BB05300360423 /* Query+Lazy.swift in Sources */, EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */, 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, @@ -1421,6 +1432,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 915BE78B2E2BB05300360423 /* Query+Lazy.swift in Sources */, EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */, 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */,