From eb47155b76011b4f1c77637e9efb51c69ef0083e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 8 Jul 2026 11:55:45 -0700 Subject: [PATCH] Strict decoding --- .../AggregateFunctions.swift | 4 +- .../StructuredQueriesCore/QueryDecoder.swift | 4 ++ .../QueryRepresentable/Date+UnixTime.swift | 2 +- .../SQLiteFunctionDecoder.swift | 62 ++++++++++++++----- .../SQLiteQueryDecoder.swift | 60 +++++++++++++----- .../AggregateFunctionsTests.swift | 6 +- 6 files changed, 99 insertions(+), 39 deletions(-) diff --git a/Sources/StructuredQueriesCore/AggregateFunctions.swift b/Sources/StructuredQueriesCore/AggregateFunctions.swift index 96e8b420..fef35469 100644 --- a/Sources/StructuredQueriesCore/AggregateFunctions.swift +++ b/Sources/StructuredQueriesCore/AggregateFunctions.swift @@ -182,8 +182,8 @@ where QueryValue: _OptionalPromotable, QueryValue._Optionalized.Wrapped: Numeric public func total( distinct isDistinct: Bool = false, filter: (some QueryExpression)? = Bool?.none - ) -> some QueryExpression { - AggregateFunctionExpression( + ) -> some QueryExpression { + AggregateFunctionExpression( "total", isDistinct: isDistinct, [queryFragment], diff --git a/Sources/StructuredQueriesCore/QueryDecoder.swift b/Sources/StructuredQueriesCore/QueryDecoder.swift index cf3462ec..eb6dd55b 100644 --- a/Sources/StructuredQueriesCore/QueryDecoder.swift +++ b/Sources/StructuredQueriesCore/QueryDecoder.swift @@ -106,5 +106,9 @@ extension QueryDecoder { } public enum QueryDecodingError: Error { + /// A required column was `NULL`. case missingRequiredColumn + + /// A column's value could not be decoded as the given type. + case typeMismatch(Any.Type) } diff --git a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift index b7798fcf..aacc9725 100644 --- a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift +++ b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift @@ -35,7 +35,7 @@ extension Date.UnixTimeRepresentation: QueryBindable { extension Date.UnixTimeRepresentation: QueryDecodable { public init(decoder: inout some QueryDecoder) throws { - try self.init(queryOutput: Date(timeIntervalSince1970: Double(decoder: &decoder))) + try self.init(queryOutput: Date(timeIntervalSince1970: Double(Int64(decoder: &decoder)))) } } diff --git a/Sources/_StructuredQueriesSQLite/SQLiteFunctionDecoder.swift b/Sources/_StructuredQueriesSQLite/SQLiteFunctionDecoder.swift index 3e1b364c..4fbc6852 100644 --- a/Sources/_StructuredQueriesSQLite/SQLiteFunctionDecoder.swift +++ b/Sources/_StructuredQueriesSQLite/SQLiteFunctionDecoder.swift @@ -31,16 +31,23 @@ struct SQLiteFunctionDecoder: QueryDecoder { @inlinable mutating func decode(_ columnType: [UInt8].Type) throws -> [UInt8]? { - defer { currentIndex += 1 } precondition(argumentCount > currentIndex) let value = arguments?[Int(currentIndex)] - guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } - if let blob = sqlite3_value_blob(value) { - let count = Int(sqlite3_value_bytes(value)) - let buffer = UnsafeRawBufferPointer(start: blob, count: count) - return [UInt8](buffer) - } else { - return [] + switch sqlite3_value_type(value) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_BLOB: + defer { currentIndex += 1 } + if let blob = sqlite3_value_blob(value) { + let count = Int(sqlite3_value_bytes(value)) + let buffer = UnsafeRawBufferPointer(start: blob, count: count) + return [UInt8](buffer) + } else { + return [] + } + default: + throw QueryDecodingError.typeMismatch([UInt8].self) } } @@ -57,11 +64,18 @@ struct SQLiteFunctionDecoder: QueryDecoder { @inlinable mutating func decode(_ columnType: Double.Type) throws -> Double? { - defer { currentIndex += 1 } precondition(argumentCount > currentIndex) let value = arguments?[Int(currentIndex)] - guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } - return sqlite3_value_double(value) + switch sqlite3_value_type(value) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_FLOAT: + defer { currentIndex += 1 } + return sqlite3_value_double(value) + default: + throw QueryDecodingError.typeMismatch(Double.self) + } } @inlinable @@ -71,20 +85,34 @@ struct SQLiteFunctionDecoder: QueryDecoder { @inlinable mutating func decode(_ columnType: Int64.Type) throws -> Int64? { - defer { currentIndex += 1 } precondition(argumentCount > currentIndex) let value = arguments?[Int(currentIndex)] - guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } - return sqlite3_value_int64(value) + switch sqlite3_value_type(value) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_INTEGER: + defer { currentIndex += 1 } + return sqlite3_value_int64(value) + default: + throw QueryDecodingError.typeMismatch(Int64.self) + } } @inlinable mutating func decode(_ columnType: String.Type) throws -> String? { - defer { currentIndex += 1 } precondition(argumentCount > currentIndex) let value = arguments?[Int(currentIndex)] - guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } - return String(cString: sqlite3_value_text(value)) + switch sqlite3_value_type(value) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_TEXT: + defer { currentIndex += 1 } + return String(cString: sqlite3_value_text(value)) + default: + throw QueryDecodingError.typeMismatch(String.self) + } } @inlinable diff --git a/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift index 54b83abb..e98f5adb 100644 --- a/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift +++ b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift @@ -27,15 +27,22 @@ struct SQLiteQueryDecoder: QueryDecoder { @inlinable mutating func decode(_ columnType: [UInt8].Type) throws -> [UInt8]? { - defer { currentIndex += 1 } precondition(sqlite3_column_count(statement) > currentIndex) - guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } - return [UInt8]( - UnsafeRawBufferPointer( - start: sqlite3_column_blob(statement, currentIndex), - count: Int(sqlite3_column_bytes(statement, currentIndex)) + switch sqlite3_column_type(statement, currentIndex) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_BLOB: + defer { currentIndex += 1 } + return [UInt8]( + UnsafeRawBufferPointer( + start: sqlite3_column_blob(statement, currentIndex), + count: Int(sqlite3_column_bytes(statement, currentIndex)) + ) ) - ) + default: + throw QueryDecodingError.typeMismatch([UInt8].self) + } } @inlinable @@ -51,10 +58,17 @@ struct SQLiteQueryDecoder: QueryDecoder { @inlinable mutating func decode(_ columnType: Double.Type) throws -> Double? { - defer { currentIndex += 1 } precondition(sqlite3_column_count(statement) > currentIndex) - guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } - return sqlite3_column_double(statement, currentIndex) + switch sqlite3_column_type(statement, currentIndex) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_FLOAT: + defer { currentIndex += 1 } + return sqlite3_column_double(statement, currentIndex) + default: + throw QueryDecodingError.typeMismatch(Double.self) + } } @inlinable @@ -64,18 +78,32 @@ struct SQLiteQueryDecoder: QueryDecoder { @inlinable mutating func decode(_ columnType: Int64.Type) throws -> Int64? { - defer { currentIndex += 1 } precondition(sqlite3_column_count(statement) > currentIndex) - guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } - return sqlite3_column_int64(statement, currentIndex) + switch sqlite3_column_type(statement, currentIndex) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_INTEGER: + defer { currentIndex += 1 } + return sqlite3_column_int64(statement, currentIndex) + default: + throw QueryDecodingError.typeMismatch(Int64.self) + } } @inlinable mutating func decode(_ columnType: String.Type) throws -> String? { - defer { currentIndex += 1 } precondition(sqlite3_column_count(statement) > currentIndex) - guard sqlite3_column_type(statement, currentIndex) != SQLITE_NULL else { return nil } - return String(cString: sqlite3_column_text(statement, currentIndex)) + switch sqlite3_column_type(statement, currentIndex) { + case SQLITE_NULL: + currentIndex += 1 + return nil + case SQLITE_TEXT: + defer { currentIndex += 1 } + return String(cString: sqlite3_column_text(statement, currentIndex)) + default: + throw QueryDecodingError.typeMismatch(String.self) + } } @inlinable diff --git a/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift b/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift index f37cc169..a9b8dc5a 100644 --- a/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift @@ -190,9 +190,9 @@ extension SnapshotTests { """ } results: { """ - ┌────┐ - │ 55 │ - └────┘ + ┌──────┐ + │ 55.0 │ + └──────┘ """ } }