From 573ed393324a65d4d7e8e0782ff8fb3c5400508d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Sep 2025 13:08:36 -0700 Subject: [PATCH 1/4] Add explicit `Bool` case to `QueryBinding` --- .../StructuredQueriesCore/QueryBinding.swift | 5 ++++ .../Triggers.swift | 2 ++ .../_StructuredQueriesSQLite/Database.swift | 2 ++ .../DatabaseFunction.swift | 26 ++++++++++--------- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Sources/StructuredQueriesCore/QueryBinding.swift b/Sources/StructuredQueriesCore/QueryBinding.swift index cfd2befd..2aa9a0cb 100644 --- a/Sources/StructuredQueriesCore/QueryBinding.swift +++ b/Sources/StructuredQueriesCore/QueryBinding.swift @@ -5,6 +5,9 @@ public enum QueryBinding: Hashable, Sendable { /// A value that should be bound to a statement as bytes. case blob([UInt8]) + /// A value that should be bound to a statement as a Boolean. + case bool(Bool) + /// A value that should be bound to a statement as a double. case double(Double) @@ -51,6 +54,8 @@ extension QueryBinding: CustomDebugStringConvertible { .dropLast() .dropFirst() .quoted(.text) + case .bool(let bool): + return bool ? "1" : "0" case .date(let date): return date.iso8601String.quoted(.text) case .double(let value): diff --git a/Sources/StructuredQueriesSQLiteCore/Triggers.swift b/Sources/StructuredQueriesSQLiteCore/Triggers.swift index f786841b..fbbdf9c0 100644 --- a/Sources/StructuredQueriesSQLiteCore/Triggers.swift +++ b/Sources/StructuredQueriesSQLiteCore/Triggers.swift @@ -442,6 +442,8 @@ public struct TemporaryTrigger: Sendable, Statement { $0.append(hex) } $0.append("unhex(\(quote: hex, delimiter: .text))") + case .bool(let bool): + $0.append("\(raw: bool ? 1 : 0)") case .double(let double): $0.append("\(raw: double)") case .date(let date): diff --git a/Sources/_StructuredQueriesSQLite/Database.swift b/Sources/_StructuredQueriesSQLite/Database.swift index 7a519b7c..aefc050a 100644 --- a/Sources/_StructuredQueriesSQLite/Database.swift +++ b/Sources/_StructuredQueriesSQLite/Database.swift @@ -140,6 +140,8 @@ public struct Database { switch binding { case .blob(let blob): sqlite3_bind_blob(statement, index, Array(blob), Int32(blob.count), SQLITE_TRANSIENT) + case .bool(let bool): + sqlite3_bind_int64(statement, index, bool ? 1 : 0) case .date(let date): sqlite3_bind_text(statement, index, date.iso8601String, -1, SQLITE_TRANSIENT) case .double(let double): diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 65a18caa..6b553df2 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -67,20 +67,22 @@ extension [QueryBinding] { extension QueryBinding { fileprivate func result(db: OpaquePointer?) { switch self { - case .blob(let value): - sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT) - case .double(let value): - sqlite3_result_double(db, value) - case .date(let value): - sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT) - case .int(let value): - sqlite3_result_int64(db, value) + case .blob(let blob): + sqlite3_result_blob(db, Array(blob), Int32(blob.count), SQLITE_TRANSIENT) + case .bool(let bool): + sqlite3_result_int64(db, bool ? 1 : 0) + case .double(let double): + sqlite3_result_double(db, double) + case .date(let date): + sqlite3_result_text(db, date.iso8601String, -1, SQLITE_TRANSIENT) + case .int(let int): + sqlite3_result_int64(db, int) case .null: sqlite3_result_null(db) - case .text(let value): - sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT) - case .uuid(let value): - sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT) + case .text(let text): + sqlite3_result_text(db, text, -1, SQLITE_TRANSIENT) + case .uuid(let uuid): + sqlite3_result_text(db, uuid.uuidString.lowercased(), -1, SQLITE_TRANSIENT) case .invalid(let error): sqlite3_result_error(db, error.underlyingError.localizedDescription, -1) } From c1be9b9c317592002d41bd978d34580b650517cb Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Sep 2025 13:42:58 -0700 Subject: [PATCH 2/4] wip --- .../StructuredQueriesCore/QueryBindable.swift | 8 ++----- .../StructuredQueriesCore/QueryBinding.swift | 21 ++++++++++++------- .../QueryDecodable.swift | 20 +++++++++--------- .../StructuredQueriesCore/QueryDecoder.swift | 6 ++++++ .../Triggers.swift | 2 ++ .../_StructuredQueriesSQLite/Database.swift | 6 +++++- .../DatabaseFunction.swift | 4 ++++ .../SQLiteQueryDecoder.swift | 17 +++++++++++++++ 8 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Sources/StructuredQueriesCore/QueryBindable.swift b/Sources/StructuredQueriesCore/QueryBindable.swift index 558a31ab..1e77b61d 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -132,14 +132,10 @@ extension UInt32: QueryBindable { extension UInt64: QueryBindable { public var queryBinding: QueryBinding { - if self > UInt64(Int64.max) { - return .invalid(OverflowError()) - } else { - return .int(Int64(self)) - } + return .uint(self) } public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding, value >= UInt64.min else { return nil } + guard case .uint(let value) = queryBinding else { return nil } self.init(value) } } diff --git a/Sources/StructuredQueriesCore/QueryBinding.swift b/Sources/StructuredQueriesCore/QueryBinding.swift index 2aa9a0cb..6f7780e2 100644 --- a/Sources/StructuredQueriesCore/QueryBinding.swift +++ b/Sources/StructuredQueriesCore/QueryBinding.swift @@ -23,6 +23,9 @@ public enum QueryBinding: Hashable, Sendable { /// A value that should be bound to a statement as a string. case text(String) + /// A value that should be bound to a statement as an unsigned integer. + case uint(UInt64) + /// A value that should be bound to a statement as a unique identifier. case uuid(UUID) @@ -48,8 +51,8 @@ public struct QueryBindingError: Error, Hashable { extension QueryBinding: CustomDebugStringConvertible { public var debugDescription: String { switch self { - case .blob(let data): - return String(decoding: data, as: UTF8.self) + case .blob(let blob): + return String(decoding: blob, as: UTF8.self) .debugDescription .dropLast() .dropFirst() @@ -58,14 +61,16 @@ extension QueryBinding: CustomDebugStringConvertible { return bool ? "1" : "0" case .date(let date): return date.iso8601String.quoted(.text) - case .double(let value): - return "\(value)" - case .int(let value): - return "\(value)" + case .double(let double): + return "\(double)" + case .int(let int): + return "\(int)" case .null: return "NULL" - case .text(let string): - return string.quoted(.text) + case .text(let text): + return text.quoted(.text) + case .uint(let uint): + return "\(uint)" case .uuid(let uuid): return uuid.uuidString.lowercased().quoted(.text) case .invalid(let error): diff --git a/Sources/StructuredQueriesCore/QueryDecodable.swift b/Sources/StructuredQueriesCore/QueryDecodable.swift index b8c1da7f..b9ab40b2 100644 --- a/Sources/StructuredQueriesCore/QueryDecodable.swift +++ b/Sources/StructuredQueriesCore/QueryDecodable.swift @@ -109,17 +109,15 @@ extension Int32: QueryDecodable { extension UInt: QueryDecodable { @inlinable public init(decoder: inout some QueryDecoder) throws { - let n = try Int64(decoder: &decoder) - guard n >= 0 else { throw OverflowError() } - self.init(n) + try self.init(UInt64(decoder: &decoder)) } } extension UInt8: QueryDecodable { @inlinable public init(decoder: inout some QueryDecoder) throws { - let n = try Int64(decoder: &decoder) - guard (Int64(UInt8.min)...Int64(UInt8.max)).contains(n) else { throw OverflowError() } + let n = try UInt64(decoder: &decoder) + guard (UInt64(UInt8.min)...UInt64(UInt8.max)).contains(n) else { throw OverflowError() } self.init(n) } } @@ -127,8 +125,8 @@ extension UInt8: QueryDecodable { extension UInt16: QueryDecodable { @inlinable public init(decoder: inout some QueryDecoder) throws { - let n = try Int64(decoder: &decoder) - guard (Int64(UInt16.min)...Int64(UInt16.max)).contains(n) else { throw OverflowError() } + let n = try UInt64(decoder: &decoder) + guard (UInt64(UInt16.min)...UInt64(UInt16.max)).contains(n) else { throw OverflowError() } self.init(n) } } @@ -136,8 +134,8 @@ extension UInt16: QueryDecodable { extension UInt32: QueryDecodable { @inlinable public init(decoder: inout some QueryDecoder) throws { - let n = try Int64(decoder: &decoder) - guard (Int64(UInt32.min)...Int64(UInt32.max)).contains(n) else { throw OverflowError() } + let n = try UInt64(decoder: &decoder) + guard (UInt64(UInt32.min)...UInt64(UInt32.max)).contains(n) else { throw OverflowError() } self.init(n) } } @@ -145,7 +143,9 @@ extension UInt32: QueryDecodable { extension UInt64: QueryDecodable { @inlinable public init(decoder: inout some QueryDecoder) throws { - try self.init(Int64(decoder: &decoder)) + guard let result = try decoder.decode(UInt64.self) + else { throw QueryDecodingError.missingRequiredColumn } + self = result } } diff --git a/Sources/StructuredQueriesCore/QueryDecoder.swift b/Sources/StructuredQueriesCore/QueryDecoder.swift index 4d5ba3fd..ee2ffa55 100644 --- a/Sources/StructuredQueriesCore/QueryDecoder.swift +++ b/Sources/StructuredQueriesCore/QueryDecoder.swift @@ -20,6 +20,12 @@ public protocol QueryDecoder { /// - Returns: A value of the requested type, or `nil` if the column is `NULL`. mutating func decode(_ columnType: Int64.Type) throws -> Int64? + /// Decodes a single value of the given type from the current column. + /// + /// - Parameter columnType: The type to decode as. + /// - Returns: A value of the requested type, or `nil` if the column is `NULL`. + mutating func decode(_ columnType: UInt64.Type) throws -> UInt64? + /// Decodes a single value of the given type from the current column. /// /// - Parameter columnType: The type to decode as. diff --git a/Sources/StructuredQueriesSQLiteCore/Triggers.swift b/Sources/StructuredQueriesSQLiteCore/Triggers.swift index fbbdf9c0..ce9bfcfc 100644 --- a/Sources/StructuredQueriesSQLiteCore/Triggers.swift +++ b/Sources/StructuredQueriesSQLiteCore/Triggers.swift @@ -466,6 +466,8 @@ public struct TemporaryTrigger: Sendable, Statement { $0.append("NULL") case .text(let string): $0.append("\(quote: string, delimiter: .text)") + case .uint(let uint): + $0.append("\(raw: uint)") case .uuid(let uuid): reportIssue( """ diff --git a/Sources/_StructuredQueriesSQLite/Database.swift b/Sources/_StructuredQueriesSQLite/Database.swift index aefc050a..14dd59b1 100644 --- a/Sources/_StructuredQueriesSQLite/Database.swift +++ b/Sources/_StructuredQueriesSQLite/Database.swift @@ -152,6 +152,10 @@ public struct Database { sqlite3_bind_null(statement, index) case .text(let text): sqlite3_bind_text(statement, index, text, -1, SQLITE_TRANSIENT) + case .uint(let uint) where uint <= UInt64(Int64.max): + sqlite3_bind_int64(statement, index, Int64(uint)) + case .uint(let uint): + throw Int64OverflowError(unsignedInteger: uint) case .uuid(let uuid): sqlite3_bind_text(statement, index, uuid.uuidString.lowercased(), -1, SQLITE_TRANSIENT) case .invalid(let error): @@ -192,7 +196,7 @@ public struct Database { } } -private struct InvalidBindingError: Error {} +private struct Int64OverflowError: Error { let unsignedInteger: UInt64 } let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 6b553df2..8e005c4c 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -81,6 +81,10 @@ extension QueryBinding { sqlite3_result_null(db) case .text(let text): sqlite3_result_text(db, text, -1, SQLITE_TRANSIENT) + case .uint(let uint) where uint <= UInt64(Int64.max): + sqlite3_result_int64(db, Int64(uint)) + case .uint(let uint): + sqlite3_result_error(db, "Unsigned integer \(uint) overflows Int64.max", -1) case .uuid(let uuid): sqlite3_result_text(db, uuid.uuidString.lowercased(), -1, SQLITE_TRANSIENT) case .invalid(let error): diff --git a/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift index 027ce9f8..09b47511 100644 --- a/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift +++ b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift @@ -71,9 +71,26 @@ struct SQLiteQueryDecoder: QueryDecoder { return String(cString: sqlite3_column_text(statement, currentIndex)) } + @inlinable + mutating func decode(_ columnType: UInt64.Type) throws -> UInt64? { + guard let n = try decode(Int64.self) else { return nil } + guard n >= 0 else { throw UInt64OverflowError(signedInteger: n) } + return UInt64(n) + } + @usableFromInline mutating func decode(_ columnType: UUID.Type) throws -> UUID? { guard let uuidString = try decode(String.self) else { return nil } return UUID(uuidString: uuidString) } } + +@usableFromInline +struct UInt64OverflowError: Error { + let signedInteger: Int64 + + @usableFromInline + init(signedInteger: Int64) { + self.signedInteger = signedInteger + } +} From 952c1c3efd208556934dabd02bbfab212806eb8f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Sep 2025 13:45:18 -0700 Subject: [PATCH 3/4] wip --- Tests/StructuredQueriesTests/BindingTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/StructuredQueriesTests/BindingTests.swift b/Tests/StructuredQueriesTests/BindingTests.swift index f8944901..7df134cf 100644 --- a/Tests/StructuredQueriesTests/BindingTests.swift +++ b/Tests/StructuredQueriesTests/BindingTests.swift @@ -61,12 +61,12 @@ extension SnapshotTests { INSERT INTO "records" ("id", "name", "duration") VALUES - ('\u{07AD}��ޭ��ޭ��ޭ��', '', ) + ('\u{07AD}��ޭ��ޭ��ޭ��', '', 18446744073709551615) RETURNING "id", "name", "duration" """# } results: { """ - The operation couldn’t be completed. (StructuredQueriesCore.OverflowError error 1.) + The operation couldn’t be completed. (_StructuredQueriesSQLite.(unknown context at $10a41b6f8).Int64OverflowError error 1.) """ } } From cb6e417e9a58194cbedecaa553f9306a593f0db8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Sep 2025 14:02:36 -0700 Subject: [PATCH 4/4] wip --- Sources/_StructuredQueriesSQLite/Database.swift | 2 +- Tests/StructuredQueriesTests/BindingTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/_StructuredQueriesSQLite/Database.swift b/Sources/_StructuredQueriesSQLite/Database.swift index 14dd59b1..2d6158c0 100644 --- a/Sources/_StructuredQueriesSQLite/Database.swift +++ b/Sources/_StructuredQueriesSQLite/Database.swift @@ -196,7 +196,7 @@ public struct Database { } } -private struct Int64OverflowError: Error { let unsignedInteger: UInt64 } +struct Int64OverflowError: Error { let unsignedInteger: UInt64 } let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) diff --git a/Tests/StructuredQueriesTests/BindingTests.swift b/Tests/StructuredQueriesTests/BindingTests.swift index 7df134cf..b87505f0 100644 --- a/Tests/StructuredQueriesTests/BindingTests.swift +++ b/Tests/StructuredQueriesTests/BindingTests.swift @@ -66,7 +66,7 @@ extension SnapshotTests { """# } results: { """ - The operation couldn’t be completed. (_StructuredQueriesSQLite.(unknown context at $10a41b6f8).Int64OverflowError error 1.) + The operation couldn’t be completed. (_StructuredQueriesSQLite.Int64OverflowError error 1.) """ } }