From e428430918155654027eace5d9c4fd4e77482075 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 21 Jan 2026 18:50:00 -0500 Subject: [PATCH 1/5] Add traits for SQLite compile options --- Package.swift | 320 ++++++++++++++++-- Package@swift-5.9.swift | 78 +++++ Sources/SQLiteDB/Core/Blob.swift | 2 +- Sources/SQLiteDB/Core/SQLiteVersion.swift | 4 +- Sources/SQLiteDB/Core/Value.swift | 6 +- Sources/SQLiteDB/Extensions/FTS4.swift | 2 +- Sources/SQLiteDB/Foundation.swift | 2 +- .../SQLiteDB/Schema/SchemaDefinitions.swift | 16 +- Sources/SQLiteDB/Schema/SchemaReader.swift | 2 +- Sources/SQLiteDB/Typed/Expression.swift | 2 +- Sources/SQLiteDB/Typed/Query+with.swift | 6 +- Sources/SQLiteDB/Typed/Query.swift | 8 +- .../SQLiteDBTests/Core/ConnectionTests.swift | 12 +- Tests/SQLiteDBTests/TestHelpers.swift | 2 +- .../Typed/CustomAggregationTests.swift | 8 +- 15 files changed, 397 insertions(+), 73 deletions(-) create mode 100644 Package@swift-5.9.swift diff --git a/Package.swift b/Package.swift index 4ccc111..c934070 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,113 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.1 import PackageDescription +/// Compile-time options +/// - seealso: [Recommended Compile-time Options](https://sqlite.org/compile.html#recommended_compile_time_options) +let compileTimeOptions: [CSetting] = [ + // https://sqlite.org/compile.html#dqs + .define("SQLITE_DQS", to: "0", .when(traits: ["DQS_0"])), + .define("SQLITE_DQS", to: "1", .when(traits: ["DQS_1"])), + .define("SQLITE_DQS", to: "2", .when(traits: ["DQS_2"])), + .define("SQLITE_DQS", to: "3", .when(traits: ["DQS_3"])), + // https://sqlite.org/compile.html#threadsafe + .define("SQLITE_THREADSAFE", to: "0", .when(traits: ["THREADSAFE_0"])), + .define("SQLITE_THREADSAFE", to: "1", .when(traits: ["THREADSAFE_1"])), + .define("SQLITE_THREADSAFE", to: "2", .when(traits: ["THREADSAFE_2"])), + // https://sqlite.org/compile.html#default_memstatus + .define("SQLITE_DEFAULT_MEMSTATUS", to: "0", .when(traits: ["DEFAULT_MEMSTATUS_0"])), + // https://sqlite.org/compile.html#default_wal_synchronous + .define("SQLITE_DEFAULT_WAL_SYNCHRONOUS", to: "1", .when(traits: ["DEFAULT_WAL_SYNCHRONOUS_1"])), + // https://sqlite.org/compile.html#like_doesnt_match_blobs + .define("SQLITE_LIKE_DOESNT_MATCH_BLOBS", .when(traits: ["LIKE_DOESNT_MATCH_BLOBS"])), + // https://sqlite.org/limits.html#max_expr_depth + .define("SQLITE_MAX_EXPR_DEPTH", to: "0", .when(traits: ["MAX_EXPR_DEPTH_0"])), + // https://sqlite.org/compile.html#omit_decltype + .define("SQLITE_OMIT_DECLTYPE", .when(traits: ["OMIT_DECLTYPE"])), + // https://sqlite.org/compile.html#omit_deprecated + .define("SQLITE_OMIT_DEPRECATED", .when(traits: ["OMIT_DEPRECATED"])), + // https://sqlite.org/compile.html#omit_progress_callback + .define("SQLITE_OMIT_PROGRESS_CALLBACK", .when(traits: ["OMIT_PROGRESS_CALLBACK"])), + // https://sqlite.org/compile.html#omit_shared_cache + .define("SQLITE_OMIT_SHARED_CACHE", .when(traits: ["OMIT_SHARED_CACHE"])), + // https://sqlite.org/compile.html#use_alloca + .define("SQLITE_USE_ALLOCA", .when(traits: ["USE_ALLOCA"])), + // https://sqlite.org/compile.html#omit_autoinit + .define("SQLITE_OMIT_AUTOINIT", .when(traits: ["OMIT_AUTOINIT"])), + // https://sqlite.org/compile.html#strict_subtype + .define("SQLITE_STRICT_SUBTYPE", to: "1", .when(traits: ["STRICT_SUBTYPE_1"])), +] + +/// Platform configuration +/// - seealso: [Platform Configuration](https://sqlite.org/compile.html#_platform_configuration) +let platformConfiguration: [CSetting] = [ + .define("HAVE_ISNAN", to: "1"), + // 'strchrnul' is available on linux since glibc 2.1.1 (1999-05-24) + .define("HAVE_STRCHRNUL", to: "1", .when(platforms: [.linux])), + // 'strchrnul' is only available on macOS 15.4, iOS 18.4, tvOS 18.4, watchOS 11.4 + // and there is no way to specify a version in the build settings condition platform + .define("HAVE_UTIME", to: "1"), +] + +/// Features +/// - seealso: [Options To Enable Features Normally Turned Off](https://sqlite.org/compile.html#_options_to_enable_features_normally_turned_off) +let features: [CSetting] = [ + // https://sqlite.org/bytecodevtab.html + .define("SQLITE_ENABLE_BYTECODE_VTAB", .when(traits: ["ENABLE_BYTECODE_VTAB"])), + // https://sqlite.org/carray.html + .define("SQLITE_ENABLE_CARRAY", .when(traits: ["ENABLE_CARRAY"])), + // https://sqlite.org/c3ref/column_database_name.html + .define("SQLITE_ENABLE_COLUMN_METADATA", .when(traits: ["ENABLE_COLUMN_METADATA"])), + // https://sqlite.org/dbpage.html + .define("SQLITE_ENABLE_DBPAGE_VTAB", .when(traits: ["ENABLE_DBPAGE_VTAB"])), + // https://sqlite.org/dbstat.html + .define("SQLITE_ENABLE_DBSTAT_VTAB", .when(traits: ["ENABLE_DBSTAT_VTAB"])), + // https://sqlite.org/fts3.html + .define("SQLITE_ENABLE_FTS4", .when(traits: ["ENABLE_FTS4"])), + // https://sqlite.org/fts5.html + .define("SQLITE_ENABLE_FTS5", .when(traits: ["ENABLE_FTS5"])), + // https://sqlite.org/geopoly.html + .define("SQLITE_ENABLE_GEOPOLY", .when(traits: ["ENABLE_GEOPOLY"])), + //.define("SQLITE_ENABLE_ICU", .when(traits: ["ENABLE_ICU"])), + // https://sqlite.org/lang_mathfunc.html + .define("SQLITE_ENABLE_MATH_FUNCTIONS", .when(traits: ["ENABLE_MATH_FUNCTIONS"])), + // https://sqlite.org/c3ref/expanded_sql.html + .define("SQLITE_ENABLE_NORMALIZE", .when(traits: ["ENABLE_NORMALIZE"])), + // https://sqlite.org/percentile.html + .define("SQLITE_ENABLE_PERCENTILE", .when(traits: ["ENABLE_PERCENTILE"])), + // https://sqlite.org/c3ref/preupdate_blobwrite.html + .define("SQLITE_ENABLE_PREUPDATE_HOOK", .when(traits: ["ENABLE_PREUPDATE_HOOK"])), + // https://sqlite.org/rtree.html + .define("SQLITE_ENABLE_RTREE", .when(traits: ["ENABLE_RTREE"])), + // https://sqlite.org/sessionintro.html + .define("SQLITE_ENABLE_SESSION", .when(traits: ["ENABLE_SESSION"])), + // https://sqlite.org/c3ref/snapshot.html + .define("SQLITE_ENABLE_SNAPSHOT", .when(traits: ["ENABLE_SNAPSHOT"])), + // https://sqlite.org/stmt.html + .define("SQLITE_ENABLE_STMTVTAB", .when(traits: ["ENABLE_STMTVTAB"])), + // https://sqlite.org/fileformat2.html#stat4tab + .define("SQLITE_ENABLE_STAT4", .when(traits: ["ENABLE_STAT4"])), +] + +let sqlcipherConfiguration: [CSetting] = [ + .headerSearchPath("libtomcrypt/headers"), + .define("SQLITE_ENABLE_API_ARMOR"), + .define("SQLITE_ENABLE_MEMORY_MANAGEMENT"), + .define("SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION"), + .define("SQLITE_ENABLE_UNLOCK_NOTIFY"), + .define("SQLITE_MAX_VARIABLE_NUMBER", to: "250000"), + .define("SQLITE_SECURE_DELETE"), + .define("SQLITE_USE_URI"), + .define("SQLITE_HAS_CODEC"), + .define("SQLITE_HOMEGROWN_RECURSIVE_MUTEX"), // needed or we see hangs in test cases + .define("SQLITE_TEMP_STORE", to: "2"), + .define("SQLITE_EXTRA_INIT", to: "sqlcipher_extra_init"), + .define("SQLITE_EXTRA_SHUTDOWN", to: "sqlcipher_extra_shutdown"), + .define("HAVE_GETHOSTUUID", to: "0"), + .define("HAVE_STDINT_H"), + .define("SQLCIPHER_CRYPTO_LIBTOMCRYPT"), + .define("SQLCIPHER_CRYPTO_CUSTOM", to: "sqlcipher_ltc_setup"), +] + let package = Package( name: "swift-sqlcipher", platforms: [ @@ -20,6 +127,179 @@ let package = Package( targets: ["SQLCipher"] ) ], + traits: [ + // Compile-time options + .trait( + name: "DQS_0", + description: "Disallow double-quoted string literals in DDL and DML" + ), + .trait( + name: "DQS_1", + description: "Disallow double-quoted strings in DDL, allow in DML" + ), + .trait( + name: "DQS_2", + description: "Allow double-quoted strings in DDL, disallow in DML" + ), + .trait( + name: "DQS_3", + description: "Allow double-quoted strings in DDL and DML" + ), + .trait( + name: "THREADSAFE_0", + description: "Omit all mutex and thread-safety logic" + ), + .trait( + name: "THREADSAFE_1", + description: "The library may be safely used from multiple threads (serialized mode)" + ), + .trait( + name: "THREADSAFE_2", + description: "The library may be safely used from multiple threads but individual database connections can only be used by a single thread at a time (multi-thread mode)" + ), + .trait( + name: "DEFAULT_MEMSTATUS_0", + description: "Disable memory allocation statistics by default" + ), + .trait( + name: "DEFAULT_WAL_SYNCHRONOUS_1", + description: "Use synchronous=NORMAL in WAL mode by default" + ), + .trait( + name: "LIKE_DOESNT_MATCH_BLOBS", + description: "Don't allow BLOB operands to LIKE and GLOB operators" + ), + .trait( + name: "MAX_EXPR_DEPTH_0", + description: "Disable all checking of the expression parse-tree depth" + ), + .trait( + name: "OMIT_DECLTYPE", + description: "Omit the ability to return the declared type of columns" + ), + .trait( + name: "OMIT_DEPRECATED", + description: "Omit deprecated interfaces and features" + ), + .trait( + name: "OMIT_PROGRESS_CALLBACK", + description: "Omit progress callback" + ), + .trait( + name: "OMIT_SHARED_CACHE", + description: "Omit shared cache support" + ), + .trait( + name: "USE_ALLOCA", + description: "Use alloca to dynamically allocate temporary stack space" + ), + .trait( + name: "OMIT_AUTOINIT", + description: "Omit automatic library initialization" + ), + .trait( + name: "STRICT_SUBTYPE_1", + description: "Cause application-defined SQL functions not registered with the SQLITE_RESULT_SUBTYPE property to raise an error when invoking sqlite3_result_subtype" + ), + // Features + .trait( + name: "ENABLE_BYTECODE_VTAB", + description: "Enables bytecode and tables_used table-valued functions" + ), + .trait( + name: "ENABLE_CARRAY", + description: "Enables the carray extension" + ), + .trait( + name: "ENABLE_COLUMN_METADATA", + description: "Enables column and table metadata functions" + ), + .trait( + name: "ENABLE_DBPAGE_VTAB", + description: "Enables the sqlite_dbpage virtual table" + ), + .trait( + name: "ENABLE_DBSTAT_VTAB", + description: "Enables the dbstat virtual table" + ), + .trait( + name: "ENABLE_FTS4", + description: "Enables versions 3 and 4 of the full-text search engine (fts3 and fts4)" + ), + .trait( + name: "ENABLE_FTS5", + description: "Enables version 5 of the full-text search engine (fts5)" + ), + .trait( + name: "ENABLE_GEOPOLY", + description: "Enables the Geopoly extension" + ), + .trait( + name: "ENABLE_MATH_FUNCTIONS", + description: "Enables the built-in SQL math functions" + ), + .trait( + name: "ENABLE_NORMALIZE", + description: "Enables the sqlite3_normalized_sql function" + ), + .trait( + name: "ENABLE_PERCENTILE", + description: "Enables the percentile extension" + ), + .trait( + name: "ENABLE_PREUPDATE_HOOK", + description: "Enables the pre-update hook" + ), + .trait( + name: "ENABLE_RTREE", + description: "Enables the R*Tree index extension" + ), + .trait( + name: "ENABLE_SESSION", + description: "Enables the pre-update hook and session extension", + enabledTraits: [ + "ENABLE_PREUPDATE_HOOK", + ] + ), + .trait( + name: "ENABLE_SNAPSHOT", + description: "Enables support for database snapshots" + ), + .trait( + name: "ENABLE_STMTVTAB", + description: "Enables the sqlite_stmt virtual table" + ), + .trait( + name: "ENABLE_STAT4", + description: "Enables the sqlite_stat4 table" + ), + // Default traits + .default(enabledTraits: [ + "DQS_0", + "ENABLE_COLUMN_METADATA", + "ENABLE_DBSTAT_VTAB", + "ENABLE_SESSION", + "ENABLE_PREUPDATE_HOOK", + "THREADSAFE_2", + "DEFAULT_MEMSTATUS_0", + "DEFAULT_WAL_SYNCHRONOUS_1", + "LIKE_DOESNT_MATCH_BLOBS", + "MAX_EXPR_DEPTH_0", + "OMIT_DEPRECATED", + "OMIT_PROGRESS_CALLBACK", + "OMIT_SHARED_CACHE", + "USE_ALLOCA", + "STRICT_SUBTYPE_1", + "ENABLE_CARRAY", + "ENABLE_FTS5", + "ENABLE_MATH_FUNCTIONS", + "ENABLE_PERCENTILE", + "ENABLE_RTREE", + "ENABLE_SNAPSHOT", + "ENABLE_STMTVTAB", + "ENABLE_STAT4", + ]), + ], targets: [ .target( name: "SQLiteDB", @@ -31,42 +311,8 @@ let package = Package( name: "SQLCipher", sources: ["sqlite", "libtomcrypt"], publicHeadersPath: "sqlite", - cSettings: [ - .headerSearchPath("libtomcrypt/headers"), - .define("SQLITE_DQS", to: "0"), - .define("SQLITE_ENABLE_API_ARMOR"), - .define("SQLITE_ENABLE_COLUMN_METADATA"), - .define("SQLITE_ENABLE_DBSTAT_VTAB"), - .define("SQLITE_ENABLE_FTS3"), - .define("SQLITE_ENABLE_FTS3_PARENTHESIS"), - .define("SQLITE_ENABLE_FTS3_TOKENIZER"), - .define("SQLITE_ENABLE_FTS4"), - .define("SQLITE_ENABLE_FTS5"), - .define("SQLITE_ENABLE_MEMORY_MANAGEMENT"), - .define("SQLITE_ENABLE_PREUPDATE_HOOK"), - .define("SQLITE_ENABLE_RTREE"), - .define("SQLITE_ENABLE_SESSION"), - .define("SQLITE_ENABLE_STMTVTAB"), - .define("SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION"), - .define("SQLITE_ENABLE_UNLOCK_NOTIFY"), - .define("SQLITE_MAX_VARIABLE_NUMBER", to: "250000"), - .define("SQLITE_LIKE_DOESNT_MATCH_BLOBS"), - .define("SQLITE_OMIT_DEPRECATED"), - .define("SQLITE_OMIT_SHARED_CACHE"), - .define("SQLITE_SECURE_DELETE"), - .define("SQLITE_THREADSAFE", to: "2"), - .define("SQLITE_USE_URI"), - .define("SQLITE_ENABLE_SNAPSHOT"), - .define("SQLITE_HAS_CODEC"), - .define("SQLITE_HOMEGROWN_RECURSIVE_MUTEX"), // needed or we see hangs in test cases - .define("SQLITE_TEMP_STORE", to: "2"), - .define("SQLITE_EXTRA_INIT", to: "sqlcipher_extra_init"), - .define("SQLITE_EXTRA_SHUTDOWN", to: "sqlcipher_extra_shutdown"), - .define("HAVE_GETHOSTUUID", to: "0"), - .define("HAVE_STDINT_H"), - .define("SQLCIPHER_CRYPTO_LIBTOMCRYPT"), - .define("SQLCIPHER_CRYPTO_CUSTOM", to: "sqlcipher_ltc_setup"), - ], + cSettings: + compileTimeOptions + platformConfiguration + features + sqlcipherConfiguration, linkerSettings: [.linkedLibrary("log", .when(platforms: [.android]))]), .testTarget( name: "SQLiteDBTests", diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000..4ccc111 --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,78 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "swift-sqlcipher", + platforms: [ + .iOS(.v13), + .macOS(.v10_13), + .watchOS(.v4), + .tvOS(.v12), + .visionOS(.v1) + ], + products: [ + .library( + name: "SQLiteDB", + targets: ["SQLiteDB"] + ), + .library( + name: "SQLCipher", + targets: ["SQLCipher"] + ) + ], + targets: [ + .target( + name: "SQLiteDB", + dependencies: [.target(name: "SQLCipher")], + cSettings: [.define("SQLITE_HAS_CODEC")], + swiftSettings: [.define("SQLITE_SWIFT_SQLCIPHER")] + ), + .target( + name: "SQLCipher", + sources: ["sqlite", "libtomcrypt"], + publicHeadersPath: "sqlite", + cSettings: [ + .headerSearchPath("libtomcrypt/headers"), + .define("SQLITE_DQS", to: "0"), + .define("SQLITE_ENABLE_API_ARMOR"), + .define("SQLITE_ENABLE_COLUMN_METADATA"), + .define("SQLITE_ENABLE_DBSTAT_VTAB"), + .define("SQLITE_ENABLE_FTS3"), + .define("SQLITE_ENABLE_FTS3_PARENTHESIS"), + .define("SQLITE_ENABLE_FTS3_TOKENIZER"), + .define("SQLITE_ENABLE_FTS4"), + .define("SQLITE_ENABLE_FTS5"), + .define("SQLITE_ENABLE_MEMORY_MANAGEMENT"), + .define("SQLITE_ENABLE_PREUPDATE_HOOK"), + .define("SQLITE_ENABLE_RTREE"), + .define("SQLITE_ENABLE_SESSION"), + .define("SQLITE_ENABLE_STMTVTAB"), + .define("SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION"), + .define("SQLITE_ENABLE_UNLOCK_NOTIFY"), + .define("SQLITE_MAX_VARIABLE_NUMBER", to: "250000"), + .define("SQLITE_LIKE_DOESNT_MATCH_BLOBS"), + .define("SQLITE_OMIT_DEPRECATED"), + .define("SQLITE_OMIT_SHARED_CACHE"), + .define("SQLITE_SECURE_DELETE"), + .define("SQLITE_THREADSAFE", to: "2"), + .define("SQLITE_USE_URI"), + .define("SQLITE_ENABLE_SNAPSHOT"), + .define("SQLITE_HAS_CODEC"), + .define("SQLITE_HOMEGROWN_RECURSIVE_MUTEX"), // needed or we see hangs in test cases + .define("SQLITE_TEMP_STORE", to: "2"), + .define("SQLITE_EXTRA_INIT", to: "sqlcipher_extra_init"), + .define("SQLITE_EXTRA_SHUTDOWN", to: "sqlcipher_extra_shutdown"), + .define("HAVE_GETHOSTUUID", to: "0"), + .define("HAVE_STDINT_H"), + .define("SQLCIPHER_CRYPTO_LIBTOMCRYPT"), + .define("SQLCIPHER_CRYPTO_CUSTOM", to: "sqlcipher_ltc_setup"), + ], + linkerSettings: [.linkedLibrary("log", .when(platforms: [.android]))]), + .testTarget( + name: "SQLiteDBTests", + dependencies: ["SQLiteDB"], + resources: [.process("Resources")], + swiftSettings: [.define("SQLITE_SWIFT_SQLCIPHER")] + ) + ] +) diff --git a/Sources/SQLiteDB/Core/Blob.swift b/Sources/SQLiteDB/Core/Blob.swift index a709fb4..6bf8f3a 100644 --- a/Sources/SQLiteDB/Core/Blob.swift +++ b/Sources/SQLiteDB/Core/Blob.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public struct Blob { +public struct Blob: Sendable { public let bytes: [UInt8] diff --git a/Sources/SQLiteDB/Core/SQLiteVersion.swift b/Sources/SQLiteDB/Core/SQLiteVersion.swift index fa7358d..7ae4aa5 100644 --- a/Sources/SQLiteDB/Core/SQLiteVersion.swift +++ b/Sources/SQLiteDB/Core/SQLiteVersion.swift @@ -1,6 +1,6 @@ import Foundation -public struct SQLiteVersion: Comparable, CustomStringConvertible { +public struct SQLiteVersion: Comparable, CustomStringConvertible, Sendable { public let major: Int public let minor: Int public var point: Int = 0 @@ -17,6 +17,6 @@ public struct SQLiteVersion: Comparable, CustomStringConvertible { lhs.tuple == rhs.tuple } - static var zero: SQLiteVersion = .init(major: 0, minor: 0) + static let zero: SQLiteVersion = .init(major: 0, minor: 0) private var tuple: (Int, Int, Int) { (major, minor, point) } } diff --git a/Sources/SQLiteDB/Core/Value.swift b/Sources/SQLiteDB/Core/Value.swift index 249a772..f0a77aa 100644 --- a/Sources/SQLiteDB/Core/Value.swift +++ b/Sources/SQLiteDB/Core/Value.swift @@ -27,7 +27,7 @@ /// /// Do not conform custom types to the Binding protocol. See the `Value` /// protocol, instead. -public protocol Binding {} +public protocol Binding: Sendable {} public protocol Number: Binding {} @@ -105,7 +105,7 @@ extension Blob: Binding, Value { extension Bool: Binding, Value { - public static var declaredDatatype = Int64.declaredDatatype + public static let declaredDatatype = Int64.declaredDatatype public static func fromDatatypeValue(_ datatypeValue: Int64) -> Bool { datatypeValue != 0 @@ -119,7 +119,7 @@ extension Bool: Binding, Value { extension Int: Number, Value { - public static var declaredDatatype = Int64.declaredDatatype + public static let declaredDatatype = Int64.declaredDatatype public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int { Int(datatypeValue) diff --git a/Sources/SQLiteDB/Extensions/FTS4.swift b/Sources/SQLiteDB/Extensions/FTS4.swift index 3fbe454..e651cc9 100644 --- a/Sources/SQLiteDB/Extensions/FTS4.swift +++ b/Sources/SQLiteDB/Extensions/FTS4.swift @@ -88,7 +88,7 @@ extension VirtualTable { } // swiftlint:disable identifier_name -public struct Tokenizer { +public struct Tokenizer: Sendable { public static let Simple = Tokenizer("simple") public static let Porter = Tokenizer("porter") diff --git a/Sources/SQLiteDB/Foundation.swift b/Sources/SQLiteDB/Foundation.swift index 44a3173..eb2946b 100644 --- a/Sources/SQLiteDB/Foundation.swift +++ b/Sources/SQLiteDB/Foundation.swift @@ -61,7 +61,7 @@ extension Date: Value { /// A global date formatter used to serialize and deserialize `NSDate` objects. /// If multiple date formats are used in an application’s database(s), use a /// custom `Value` type per additional format. -public var dateFormatter: DateFormatter = { +public let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" formatter.locale = Locale(identifier: "en_US_POSIX") diff --git a/Sources/SQLiteDB/Schema/SchemaDefinitions.swift b/Sources/SQLiteDB/Schema/SchemaDefinitions.swift index 80f9e19..9ff5b48 100644 --- a/Sources/SQLiteDB/Schema/SchemaDefinitions.swift +++ b/Sources/SQLiteDB/Schema/SchemaDefinitions.swift @@ -39,13 +39,13 @@ public struct ObjectDefinition: Equatable { // https://sqlite.org/syntax/column-def.html // column-name -> type-name -> column-constraint* -public struct ColumnDefinition: Equatable { +public struct ColumnDefinition: Equatable, Sendable { // The type affinity of a column is the recommended type for data stored in that column. // The important idea here is that the type is recommended, not required. Any column can still // store any type of data. It is just that some columns, given the choice, will prefer to use one // storage class over another. The preferred storage class for a column is called its "affinity". - public enum Affinity: String, CustomStringConvertible, CaseIterable { + public enum Affinity: String, CustomStringConvertible, CaseIterable, Sendable { case INTEGER case NUMERIC case REAL @@ -73,7 +73,7 @@ public struct ColumnDefinition: Equatable { } } - public enum OnConflict: String, CaseIterable { + public enum OnConflict: String, CaseIterable, Sendable { case ROLLBACK case ABORT case FAIL @@ -86,7 +86,7 @@ public struct ColumnDefinition: Equatable { } } - public struct PrimaryKey: Equatable { + public struct PrimaryKey: Equatable, Sendable { let autoIncrement: Bool let onConflict: OnConflict? @@ -116,7 +116,7 @@ public struct ColumnDefinition: Equatable { } } - public struct ForeignKey: Equatable { + public struct ForeignKey: Equatable, Sendable { let table: String let column: String let primaryKey: String? @@ -151,7 +151,7 @@ public struct ColumnDefinition: Equatable { } } -public enum LiteralValue: Equatable, CustomStringConvertible { +public enum LiteralValue: Equatable, CustomStringConvertible, Sendable { // swiftlint:disable force_try private static let singleQuote = try! NSRegularExpression(pattern: "^'(.*)'$") private static let doubleQuote = try! NSRegularExpression(pattern: "^\"(.*)\"$") @@ -233,7 +233,7 @@ public enum LiteralValue: Equatable, CustomStringConvertible { // https://sqlite.org/lang_createindex.html // schema-name.index-name ON table-name ( indexed-column+ ) WHERE expr -public struct IndexDefinition: Equatable { +public struct IndexDefinition: Equatable, Sendable { // SQLite supports index names up to 64 characters. static let maxIndexLength = 64 @@ -242,7 +242,7 @@ public struct IndexDefinition: Equatable { static let orderRe = try! NSRegularExpression(pattern: "\"?(\\w+)\"? DESC") // swiftlint:enable force_try - public enum Order: String { case ASC, DESC } + public enum Order: String, Sendable { case ASC, DESC } public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil, orders: [String: Order]? = nil) { self.table = table diff --git a/Sources/SQLiteDB/Schema/SchemaReader.swift b/Sources/SQLiteDB/Schema/SchemaReader.swift index f14704c..b858176 100644 --- a/Sources/SQLiteDB/Schema/SchemaReader.swift +++ b/Sources/SQLiteDB/Schema/SchemaReader.swift @@ -125,7 +125,7 @@ public class SchemaReader { } } -private enum SchemaTable { +private enum SchemaTable: Sendable { private static let name = Table("sqlite_schema", database: "main") private static let tempName = Table("sqlite_schema", database: "temp") // legacy names (< 3.33.0) diff --git a/Sources/SQLiteDB/Typed/Expression.swift b/Sources/SQLiteDB/Typed/Expression.swift index e599d6b..26f477c 100644 --- a/Sources/SQLiteDB/Typed/Expression.swift +++ b/Sources/SQLiteDB/Typed/Expression.swift @@ -70,7 +70,7 @@ public struct SQLExpression: ExpressionType { } -public protocol Expressible { +public protocol Expressible: Sendable { var expression: SQLExpression { get } diff --git a/Sources/SQLiteDB/Typed/Query+with.swift b/Sources/SQLiteDB/Typed/Query+with.swift index a87ce59..e4ec46b 100644 --- a/Sources/SQLiteDB/Typed/Query+with.swift +++ b/Sources/SQLiteDB/Typed/Query+with.swift @@ -95,15 +95,15 @@ extension QueryType { } /// Materialization hints for `WITH` clause -public enum MaterializationHint: String { +public enum MaterializationHint: String, Sendable { case materialized = "MATERIALIZED" case notMaterialized = "NOT MATERIALIZED" } -struct WithClauses { - struct Clause { +struct WithClauses: Sendable { + struct Clause: Sendable { var alias: Table var columns: [Expressible]? var hint: MaterializationHint? diff --git a/Sources/SQLiteDB/Typed/Query.swift b/Sources/SQLiteDB/Typed/Query.swift index 3d50ca9..79c95af 100644 --- a/Sources/SQLiteDB/Typed/Query.swift +++ b/Sources/SQLiteDB/Typed/Query.swift @@ -1168,7 +1168,7 @@ extension Connection { } -public struct Row { +public struct Row: Sendable { let columnNames: [String: Int] @@ -1242,7 +1242,7 @@ public struct Row { } /// Determines the join operator for a query’s `JOIN` clause. -public enum JoinType: String { +public enum JoinType: String, Sendable { /// A `CROSS` join. case cross = "CROSS" @@ -1256,7 +1256,7 @@ public enum JoinType: String { } /// ON CONFLICT resolutions. -public enum OnConflict: String { +public enum OnConflict: String, Sendable { case replace = "REPLACE" @@ -1272,7 +1272,7 @@ public enum OnConflict: String { // MARK: - Private -public struct QueryClauses { +public struct QueryClauses: Sendable { var select = (distinct: false, columns: [SQLExpression(literal: "*") as Expressible]) diff --git a/Tests/SQLiteDBTests/Core/ConnectionTests.swift b/Tests/SQLiteDBTests/Core/ConnectionTests.swift index c15bf79..688c01c 100644 --- a/Tests/SQLiteDBTests/Core/ConnectionTests.swift +++ b/Tests/SQLiteDBTests/Core/ConnectionTests.swift @@ -292,7 +292,7 @@ class ConnectionTests: SQLiteTestCase { assertSQL("RELEASE SAVEPOINT '1'", 0) } - func test_updateHook_setsUpdateHook_withInsert() throws { + @MainActor func test_updateHook_setsUpdateHook_withInsert() throws { try async { done in db.updateHook { operation, db, table, rowid in XCTAssertEqual(Connection.Operation.insert, operation) @@ -305,7 +305,7 @@ class ConnectionTests: SQLiteTestCase { } } - func test_updateHook_setsUpdateHook_withUpdate() throws { + @MainActor func test_updateHook_setsUpdateHook_withUpdate() throws { try insertUser("alice") try async { done in db.updateHook { operation, db, table, rowid in @@ -319,7 +319,7 @@ class ConnectionTests: SQLiteTestCase { } } - func test_updateHook_setsUpdateHook_withDelete() throws { + @MainActor func test_updateHook_setsUpdateHook_withDelete() throws { try insertUser("alice") try async { done in db.updateHook { operation, db, table, rowid in @@ -333,7 +333,7 @@ class ConnectionTests: SQLiteTestCase { } } - func test_commitHook_setsCommitHook() throws { + @MainActor func test_commitHook_setsCommitHook() throws { try async { done in db.commitHook { done() @@ -345,7 +345,7 @@ class ConnectionTests: SQLiteTestCase { } } - func test_rollbackHook_setsRollbackHook() throws { + @MainActor func test_rollbackHook_setsRollbackHook() throws { try async { done in db.rollbackHook(done) do { @@ -359,7 +359,7 @@ class ConnectionTests: SQLiteTestCase { } } - func test_commitHook_withRollback_rollsBack() throws { + @MainActor func test_commitHook_withRollback_rollsBack() throws { try async { done in db.commitHook { throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) diff --git a/Tests/SQLiteDBTests/TestHelpers.swift b/Tests/SQLiteDBTests/TestHelpers.swift index 4989a37..a6f48e3 100644 --- a/Tests/SQLiteDBTests/TestHelpers.swift +++ b/Tests/SQLiteDBTests/TestHelpers.swift @@ -66,7 +66,7 @@ class SQLiteTestCase: XCTestCase { // if let count = trace[SQL] { trace[SQL] = count - 1 } // } - func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) throws -> Void) throws { + @MainActor func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) throws -> Void) throws { let expectation = self.expectation(description: description) try block({ expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) diff --git a/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift b/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift index dee2ced..e299f60 100644 --- a/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift +++ b/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift @@ -147,11 +147,11 @@ class CustomAggregationTests: SQLiteTestCase { /// This class is used to test that aggregation state variables /// can be reference types and are properly memory managed when /// crossing the Swift<->C boundary multiple times. -class TestObject { - static var inits = 0 - static var deinits = 0 +final class TestObject: @unsafe Sendable { + nonisolated(unsafe) static var inits = 0 + nonisolated(unsafe) static var deinits = 0 - var value: Int64 + nonisolated(unsafe) var value: Int64 init(value: Int64) { self.value = value TestObject.inits += 1 From e25108c2c85e4c0d7a1c1a038bd8fe972a44c44f Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 21 Jan 2026 20:45:48 -0500 Subject: [PATCH 2/5] Make more types Sendable --- Sources/SQLiteDB/Core/URIQueryParameter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SQLiteDB/Core/URIQueryParameter.swift b/Sources/SQLiteDB/Core/URIQueryParameter.swift index abbab2e..25f303e 100644 --- a/Sources/SQLiteDB/Core/URIQueryParameter.swift +++ b/Sources/SQLiteDB/Core/URIQueryParameter.swift @@ -1,12 +1,12 @@ import Foundation /// See https://www.sqlite.org/uri.html -public enum URIQueryParameter: CustomStringConvertible { - public enum FileMode: String { +public enum URIQueryParameter: CustomStringConvertible, Sendable { + public enum FileMode: String, Sendable { case readOnly = "ro", readWrite = "rw", readWriteCreate = "rwc", memory } - public enum CacheMode: String { + public enum CacheMode: String, Sendable { case shared, `private` } From 7e6a06de36c40e99e8b219bfd0e439a5cb1cc841 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 22 Jan 2026 14:17:14 -0500 Subject: [PATCH 3/5] Wrap Connection in an @unchecked Sendable for test_concurrent_access_single_connection --- Tests/SQLiteDBTests/Core/ConnectionTests.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Tests/SQLiteDBTests/Core/ConnectionTests.swift b/Tests/SQLiteDBTests/Core/ConnectionTests.swift index 688c01c..dbaa6be 100644 --- a/Tests/SQLiteDBTests/Core/ConnectionTests.swift +++ b/Tests/SQLiteDBTests/Core/ConnectionTests.swift @@ -427,20 +427,26 @@ class ConnectionTests: SQLiteTestCase { #endif func test_concurrent_access_single_connection() throws { - // test can fail on iOS/tvOS 9.x: SQLite compile-time differences? - guard #available(iOS 10.0, OSX 10.10, tvOS 10.0, watchOS 2.2, *) else { return } - let conn = try Connection("\(NSTemporaryDirectory())/\(UUID().uuidString)") try conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") try conn.run("INSERT INTO test(value) VALUES(?)", 0) let queue = DispatchQueue(label: "Readers", attributes: [.concurrent]) + struct ConnectionWrapper: @unchecked Sendable { + let conn: Connection + + func callScalar() { + _ = try! conn.scalar("SELECT value FROM test") + } + } + + let wrapper = ConnectionWrapper(conn: conn) let nReaders = 5 let semaphores = Array(repeating: DispatchSemaphore(value: 100), count: nReaders) for index in 0.. Date: Thu, 22 Jan 2026 14:22:36 -0500 Subject: [PATCH 4/5] --- Package.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index c934070..ef48fbf 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.1 +// swift-tools-version:6.2 import PackageDescription /// Compile-time options @@ -305,7 +305,14 @@ let package = Package( name: "SQLiteDB", dependencies: [.target(name: "SQLCipher")], cSettings: [.define("SQLITE_HAS_CODEC")], - swiftSettings: [.define("SQLITE_SWIFT_SQLCIPHER")] + swiftSettings: [ + .define("SQLITE_SWIFT_SQLCIPHER"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("InferSendableFromCaptures"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault") + ] ), .target( name: "SQLCipher", From b65b610489c8c295b4a7f2515b1d767ffe4864ca Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 22 Jan 2026 14:26:46 -0500 Subject: [PATCH 5/5] Remove @unsafe Sendable from TestObject --- Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift b/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift index e299f60..aafba05 100644 --- a/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift +++ b/Tests/SQLiteDBTests/Typed/CustomAggregationTests.swift @@ -147,7 +147,7 @@ class CustomAggregationTests: SQLiteTestCase { /// This class is used to test that aggregation state variables /// can be reference types and are properly memory managed when /// crossing the Swift<->C boundary multiple times. -final class TestObject: @unsafe Sendable { +final class TestObject: Sendable { nonisolated(unsafe) static var inits = 0 nonisolated(unsafe) static var deinits = 0