├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── api-docs.yml │ └── test.yml ├── .gitignore ├── .spi.yml ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources └── SQLiteKit │ ├── Docs.docc │ ├── Documentation.md │ ├── images │ │ └── vapor-sqlitekit-logo.svg │ └── theme-settings.json │ ├── Exports.swift │ ├── SQLiteConfiguration.swift │ ├── SQLiteConnection+SQLKit.swift │ ├── SQLiteConnectionSource.swift │ ├── SQLiteDataDecoder.swift │ ├── SQLiteDataEncoder.swift │ ├── SQLiteDialect.swift │ └── SQLiteRow+SQLRow.swift └── Tests └── SQLiteKitTests └── SQLiteKitTests.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gwynne 2 | /.github/CONTRIBUTING.md @gwynne @0xTim 3 | /.github/workflows/*.yml @gwynne @0xTim 4 | /.github/workflows/test.yml @gwynne 5 | /.spi.yml @gwynne @0xTim 6 | /.gitignore @gwynne @0xTim 7 | /LICENSE @gwynne @0xTim 8 | /README.md @gwynne @0xTim 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | dependencies: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy-api-docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main 10 | secrets: inherit 11 | with: 12 | package_name: sqlite-kit 13 | modules: SQLiteKit 14 | pathsToInvalidate: /sqlitekit/* 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | pull_request: { types: [opened, reopened, synchronize, ready_for_review] } 7 | push: { branches: [ main ] } 8 | 9 | env: 10 | LOG_LEVEL: info 11 | SWIFT_DETERMINISTIC_HASHING: 1 12 | 13 | jobs: 14 | 15 | # also serves as code coverage baseline update 16 | unit-tests: 17 | uses: vapor/ci/.github/workflows/run-unit-tests.yml@main 18 | secrets: inherit 19 | 20 | # Make sure downstream dependents still work 21 | dependents-check: 22 | if: ${{ !(github.event.pull_request.draft || false) }} 23 | runs-on: ubuntu-latest 24 | container: swift:5.10-jammy 25 | steps: 26 | - name: Check out package 27 | uses: actions/checkout@v4 28 | with: 29 | path: sqlite-kit 30 | - name: Check out FluentKit driver 31 | uses: actions/checkout@v4 32 | with: 33 | repository: vapor/fluent-sqlite-driver 34 | path: fluent-sqlite-driver 35 | - name: Tell dependents to use local checkout 36 | run: swift package --package-path fluent-sqlite-driver edit sqlite-kit --path sqlite-kit 37 | - name: Run FluentSQLiteDriver tests with Thread Sanitizer 38 | run: swift test --package-path fluent-sqlite-driver --sanitize=thread 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Sources/main.swift 3 | .build 4 | Packages 5 | *.xcodeproj 6 | Package.pins 7 | Package.resolved 8 | DerivedData 9 | .swiftpm 10 | Tests/LinuxMain.swift 11 | 12 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | metadata: 3 | authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community." 4 | external_links: 5 | documentation: "https://api.vapor.codes/sqlitekit/documentation/sqlitekit/" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.8 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "sqlite-kit", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | .iOS(.v13), 9 | .watchOS(.v6), 10 | .tvOS(.v13), 11 | ], 12 | products: [ 13 | .library(name: "SQLiteKit", targets: ["SQLiteKit"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), 17 | .package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.9.0"), 18 | .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"), 19 | .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "SQLiteKit", 24 | dependencies: [ 25 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 26 | .product(name: "AsyncKit", package: "async-kit"), 27 | .product(name: "SQLiteNIO", package: "sqlite-nio"), 28 | .product(name: "SQLKit", package: "sql-kit"), 29 | ], 30 | swiftSettings: swiftSettings 31 | ), 32 | .testTarget( 33 | name: "SQLiteKitTests", 34 | dependencies: [ 35 | .product(name: "SQLKitBenchmark", package: "sql-kit"), 36 | .target(name: "SQLiteKit"), 37 | ], 38 | swiftSettings: swiftSettings 39 | ), 40 | ] 41 | ) 42 | 43 | var swiftSettings: [SwiftSetting] { [ 44 | .enableUpcomingFeature("ConciseMagicFile"), 45 | .enableUpcomingFeature("ForwardTrailingClosures"), 46 | ] } 47 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "sqlite-kit", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | .iOS(.v13), 9 | .watchOS(.v6), 10 | .tvOS(.v13), 11 | ], 12 | products: [ 13 | .library(name: "SQLiteKit", targets: ["SQLiteKit"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), 17 | .package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.9.0"), 18 | .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"), 19 | .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "SQLiteKit", 24 | dependencies: [ 25 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 26 | .product(name: "AsyncKit", package: "async-kit"), 27 | .product(name: "SQLiteNIO", package: "sqlite-nio"), 28 | .product(name: "SQLKit", package: "sql-kit"), 29 | ], 30 | swiftSettings: swiftSettings 31 | ), 32 | .testTarget( 33 | name: "SQLiteKitTests", 34 | dependencies: [ 35 | .product(name: "SQLKitBenchmark", package: "sql-kit"), 36 | .target(name: "SQLiteKit"), 37 | ], 38 | swiftSettings: swiftSettings 39 | ), 40 | ] 41 | ) 42 | 43 | var swiftSettings: [SwiftSetting] { [ 44 | .enableUpcomingFeature("ExistentialAny"), 45 | .enableUpcomingFeature("ConciseMagicFile"), 46 | .enableUpcomingFeature("ForwardTrailingClosures"), 47 | .enableUpcomingFeature("DisableOutwardActorInference"), 48 | .enableExperimentalFeature("StrictConcurrency=complete"), 49 | ] } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | SQLiteKit 6 | 7 |
8 |
9 | Documentation 10 | Team Chat 11 | MIT License 12 | Continuous Integration 13 | 14 | Swift 5.8+ 15 | SSWG Incubation Level: Graduated 16 |

17 | 18 |
19 | 20 | SQLiteKit is an [SQLKit] driver for SQLite clients. It supports building and serializing SQLite-dialect SQL queries. SQLiteKit uses [SQLiteNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. 21 | 22 | [SQLKit]: https://github.com/vapor/sql-kit 23 | [SQLiteNIO]: https://github.com/vapor/sqlite-nio 24 | [AsyncKit]: https://github.com/vapor/async-kit 25 | 26 | ### Usage 27 | 28 | Use the SPM string to easily include the dependendency in your `Package.swift` file. 29 | 30 | ```swift 31 | .package(url: "https://github.com/vapor/sqlite-kit.git", from: "4.0.0") 32 | ``` 33 | 34 | ### Supported Platforms 35 | 36 | SQLiteKit supports the following platforms: 37 | 38 | - Ubuntu 20.04+ 39 | - macOS 10.15+ 40 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/Docs.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``SQLiteKit`` 2 | 3 | @Metadata { 4 | @TitleHeading(Package) 5 | } 6 | 7 | SQLiteKit is a library providing an SQLKit driver for SQLiteNIO. 8 | 9 | ## Overview 10 | 11 | This package provides the "foundational" level of support for using [Fluent] with SQLite by implementing the requirements of an [SQLKit] driver. It is responsible for: 12 | 13 | - Managing the underlying SQLite library ([SQLiteNIO]), 14 | - Providing a two-way bridge between SQLiteNIO and SQLKit's generic data and metadata formats, 15 | - Presenting an interface for establishing, managing, and interacting with database connections via [AsyncKit]. 16 | 17 | > Tip: The FluentKit driver for SQLite is provided by the [FluentSQLiteDriver] package. 18 | 19 | ## Version Support 20 | 21 | This package uses [SQLiteNIO] for all underlying database interactions. The version of SQLite embedded by that package is always the one used. 22 | 23 | [SQLKit]: https://swiftpackageindex.com/vapor/sql-kit 24 | [SQLiteNIO]: https://swiftpackageindex.com/vapor/sqlite-nio 25 | [Fluent]: https://swiftpackageindex.com/vapor/fluent-kit 26 | [FluentSQLiteDriver]: https://swiftpackageindex.com/vapor/fluent-sqlite-driver 27 | [AsyncKit]: https://swiftpackageindex.com/vapor/async-kit 28 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/Docs.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, 4 | "border-radius": "0", 5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 7 | "color": { 8 | "sqlitekit": "hsl(215, 45%, 58%)", 9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-sqlitekit) 30%, #000 100%)", 10 | "documentation-intro-accent": "var(--color-sqlitekit)", 11 | "logo-base": { "dark": "#fff", "light": "#000" }, 12 | "logo-shape": { "dark": "#000", "light": "#fff" }, 13 | "fill": { "dark": "#000", "light": "#fff" } 14 | }, 15 | "icons": { "technology": "/sqlitekit/images/vapor-sqlitekit-logo.svg" } 16 | }, 17 | "features": { 18 | "quickNavigation": { "enable": true }, 19 | "i18n": { "enable": true } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/Exports.swift: -------------------------------------------------------------------------------- 1 | @_documentation(visibility: internal) @_exported import SQLKit 2 | @_documentation(visibility: internal) @_exported import SQLiteNIO 3 | @_documentation(visibility: internal) @_exported import AsyncKit 4 | @_documentation(visibility: internal) @_exported import struct Logging.Logger 5 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteConfiguration.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.UUID 2 | 3 | /// Describes a configuration for an SQLite database connection. 4 | public struct SQLiteConfiguration: Sendable { 5 | /// The possible storage types for an SQLite database. 6 | public enum Storage: Sendable { 7 | /// Specify an SQLite database stored in memory, using a randomly generated identifier. 8 | /// 9 | /// See ``memory(identifier:)``. 10 | public static var memory: Self { 11 | .memory(identifier: UUID().uuidString) 12 | } 13 | 14 | /// Specify an SQLite database stored in memory, using a given identifier string. 15 | /// 16 | /// An in-memory database persists only until the last connection to it is closed. If a new connection is 17 | /// opened after that point, even using the same identifier, a new, empty database is created. 18 | /// 19 | /// - Parameter identifier: Uniquely identifies the in-memory storage. Multiple connections may use this 20 | /// identifier to connect to the same in-memory storage for the duration of its lifetime. The identifer 21 | /// has no predefined format or restrictions on its content. 22 | case memory(identifier: String) 23 | 24 | /// Specify an SQLite database stored in a file at the specified path. 25 | /// 26 | /// If a relative path is specified, it is interpreted relative to the current working directory of the 27 | /// current process (e.g. `NIOFileSystem.shared.currentWorkingDirectory`). It is recommended to always use 28 | /// absolute paths whenever possible. 29 | /// 30 | /// - Parameter path: The filesystem path at which to store the database. 31 | case file(path: String) 32 | } 33 | 34 | /// The storage type for the database. 35 | /// 36 | /// See ``Storage-swift.enum`` for the available storage types. 37 | public var storage: Storage 38 | 39 | /// When `true`, foreign key support is automatically enabled on all connections using this configuration. 40 | /// 41 | /// Internally issues a `PRAGMA foreign_keys = ON` query when enabled. 42 | public var enableForeignKeys: Bool 43 | 44 | /// Create a new ``SQLiteConfiguration``. 45 | /// 46 | /// - Parameters: 47 | /// - storage: The storage type to use for the database. See ``Storage-swift.enum``. 48 | /// - enableForeignKeys: Whether to enable foreign key support by default for all connections. 49 | /// Defaults to `true`. 50 | public init(storage: Storage, enableForeignKeys: Bool = true) { 51 | self.storage = storage 52 | self.enableForeignKeys = enableForeignKeys 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteConnection+SQLKit.swift: -------------------------------------------------------------------------------- 1 | import SQLKit 2 | import SQLiteNIO 3 | import Logging 4 | 5 | // Hint: Yes, I know what default arguments are. This ridiculous spelling out of each alternative avoids public API 6 | // breakage from adding the defaults. 7 | 8 | extension SQLiteDatabase { 9 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 10 | @inlinable 11 | public func sql() -> any SQLDatabase { 12 | self.sql(encoder: .init(), decoder: .init(), queryLogLevel: .debug) 13 | } 14 | 15 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 16 | @inlinable 17 | public func sql(encoder: SQLiteDataEncoder) -> any SQLDatabase { 18 | self.sql(encoder: encoder, decoder: .init(), queryLogLevel: .debug) 19 | } 20 | 21 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 22 | @inlinable 23 | public func sql(decoder: SQLiteDataDecoder) -> any SQLDatabase { 24 | self.sql(encoder: .init(), decoder: decoder, queryLogLevel: .debug) 25 | } 26 | 27 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 28 | @inlinable 29 | public func sql(encoder: SQLiteDataEncoder, decoder: SQLiteDataDecoder) -> any SQLDatabase { 30 | self.sql(encoder: encoder, decoder: decoder, queryLogLevel: .debug) 31 | } 32 | 33 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 34 | @inlinable 35 | public func sql(queryLogLevel: Logger.Level?) -> any SQLDatabase { 36 | self.sql(encoder: .init(), decoder: .init(), queryLogLevel: queryLogLevel) 37 | } 38 | 39 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 40 | @inlinable 41 | public func sql(encoder: SQLiteDataEncoder, queryLogLevel: Logger.Level?) -> any SQLDatabase { 42 | self.sql(encoder: encoder, decoder: .init(), queryLogLevel: queryLogLevel) 43 | } 44 | 45 | /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. 46 | @inlinable 47 | public func sql(decoder: SQLiteDataDecoder, queryLogLevel: Logger.Level?) -> any SQLDatabase { 48 | self.sql(encoder: .init(), decoder: decoder, queryLogLevel: queryLogLevel) 49 | } 50 | 51 | /// Return an object allowing access to this database via the `SQLDatabase` interface. 52 | /// 53 | /// - Parameters: 54 | /// - encoder: An ``SQLiteDataEncoder`` used to translate bound query parameters into `SQLiteData` values. 55 | /// - decoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. 56 | /// - queryLogLevel: The level at which SQL queries issued through the SQLKit interface will be logged. 57 | /// - Returns: An instance of `SQLDatabase` which accesses the same database as `self`. 58 | @inlinable 59 | public func sql( 60 | encoder: SQLiteDataEncoder, 61 | decoder: SQLiteDataDecoder, 62 | queryLogLevel: Logger.Level? 63 | ) -> any SQLDatabase { 64 | SQLiteSQLDatabase(database: self, encoder: encoder, decoder: decoder, queryLogLevel: queryLogLevel) 65 | } 66 | } 67 | 68 | struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { 69 | /// The numeric value of the version. 70 | /// 71 | /// The value is laid out identicallly to [the `SQLITE_VERSION_NUMBER` constant](c_source_id). 72 | /// 73 | /// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html 74 | let intValue: Int 75 | 76 | /// The string representation of the version. 77 | /// 78 | /// The string is formatted identically to [the `SQLITE_VERSION` constant](c_source_id). 79 | /// 80 | /// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html 81 | /// 82 | /// This value is not used for equality or ordering comparisons; it is really only useful for display. We 83 | /// maintain a stored property for it rather than generating it as-needed from the numeric value in order to 84 | /// preserve any additional information the original value may contain. 85 | /// 86 | /// > Note: The string value should always represent the same version as the numeric value. This requirement is 87 | /// > asserted in debug builds, but is not otherwise enforced. 88 | let stringValue: String 89 | 90 | /// Separates an appropriately formatted numeric value into its individual components. 91 | static func components(of intValue: Int) -> (major: Int, minor: Int, patch: Int) { 92 | ( 93 | major: intValue / 1_000_000, 94 | minor: intValue % 1_000_000 / 1_000, 95 | patch: intValue % 1_000 96 | ) 97 | } 98 | 99 | /// Get the runtime version of the SQLite3 library in use. 100 | static var runtimeVersion: Self { 101 | self.init( 102 | intValue: Int(SQLiteConnection.libraryVersion()), 103 | stringValue: SQLiteConnection.libraryVersionString() 104 | ) 105 | } 106 | 107 | /// Build a version value from individual components and synthesize the approiate string value. 108 | init(major: Int, minor: Int, patch: Int) { 109 | self.init(intValue: major * 1_000_000 + minor * 1_000 + patch) 110 | } 111 | 112 | /// Designated initializer. Build a version value from the combined numeric value and a corresponding string value. 113 | /// If the string value is omitted, it is synthesized 114 | init(intValue: Int, stringValue: String? = nil) { 115 | let components = Self.components(of: intValue) 116 | 117 | self.intValue = intValue 118 | if let stringValue { 119 | assert( 120 | stringValue.hasPrefix("\(components.major).\(components.minor).\(components.patch)"), 121 | "SQLite version string '\(stringValue)' must prefix-match numeric version '\(intValue)'" 122 | ) 123 | self.stringValue = stringValue 124 | } else { 125 | self.stringValue = "\(components.major).\(components.major).\(components.patch)" 126 | } 127 | } 128 | 129 | /// The major version number. 130 | /// 131 | /// This is likely to be 3 for a long time to come yet. 132 | var majorVersion: Int { 133 | Self.components(of: self.intValue).major 134 | } 135 | 136 | /// The minor version number. 137 | var minorVersion: Int { 138 | Self.components(of: self.intValue).minor 139 | } 140 | 141 | /// The patch version number. 142 | var patchVersion: Int { 143 | Self.components(of: self.intValue).patch 144 | } 145 | 146 | // See `SQLDatabaseReportedVersion.isEqual(to:)`. 147 | func isEqual(to otherVersion: any SQLDatabaseReportedVersion) -> Bool { 148 | (otherVersion as? Self).map { $0.intValue == self.intValue } ?? false 149 | } 150 | 151 | // See `SQLDatabaseReportedVersion.isOlder(than:)`. 152 | func isOlder(than otherVersion: any SQLDatabaseReportedVersion) -> Bool { 153 | (otherVersion as? Self).map { 154 | (self.majorVersion != $0.majorVersion ? self.majorVersion < $0.majorVersion : 155 | (self.minorVersion != $0.minorVersion ? self.minorVersion < $0.minorVersion : 156 | (self.patchVersion < $0.patchVersion))) 157 | } ?? false 158 | } 159 | } 160 | 161 | /// Wraps a `SQLiteDatabase` with the `SQLDatabase` protocol. 162 | @usableFromInline 163 | /*private*/ struct SQLiteSQLDatabase: SQLDatabase { 164 | /// The underlying database. 165 | @usableFromInline 166 | let database: D 167 | 168 | /// An ``SQLiteDataEncoder`` used to translate bindings into `SQLiteData` values. 169 | @usableFromInline 170 | let encoder: SQLiteDataEncoder 171 | 172 | /// An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. 173 | @usableFromInline 174 | let decoder: SQLiteDataDecoder 175 | 176 | // See `SQLDatabase.eventLoop`. 177 | @usableFromInline 178 | var eventLoop: any EventLoop { 179 | self.database.eventLoop 180 | } 181 | 182 | // See `SQLDatabase.version`. 183 | @usableFromInline 184 | var version: (any SQLDatabaseReportedVersion)? { 185 | SQLiteDatabaseVersion.runtimeVersion 186 | } 187 | 188 | // See `SQLDatabase.logger`. 189 | @usableFromInline 190 | var logger: Logger { 191 | self.database.logger 192 | } 193 | 194 | // See `SQLDatabase.dialect`. 195 | @usableFromInline 196 | var dialect: any SQLDialect { 197 | SQLiteDialect() 198 | } 199 | 200 | // See `SQLDatabase.queryLogLevel`. 201 | @usableFromInline 202 | let queryLogLevel: Logger.Level? 203 | 204 | @inlinable 205 | init(database: D, encoder: SQLiteDataEncoder, decoder: SQLiteDataDecoder, queryLogLevel: Logger.Level?) { 206 | self.database = database 207 | self.encoder = encoder 208 | self.decoder = decoder 209 | self.queryLogLevel = queryLogLevel 210 | } 211 | 212 | // See `SQLDatabase.execute(sql:_:)`. 213 | @usableFromInline 214 | func execute( 215 | sql query: any SQLExpression, 216 | _ onRow: @escaping @Sendable (any SQLRow) -> () 217 | ) -> EventLoopFuture { 218 | let (sql, rawBinds) = self.serialize(query) 219 | 220 | if let queryLogLevel = self.queryLogLevel { 221 | self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(rawBinds.map { .string("\($0)") })]) 222 | } 223 | 224 | let binds: [SQLiteData] 225 | do { 226 | binds = try rawBinds.map { try self.encoder.encode($0) } 227 | } catch { 228 | return self.eventLoop.makeFailedFuture(error) 229 | } 230 | 231 | return self.database.query( 232 | sql, 233 | binds, 234 | { onRow($0.sql(decoder: self.decoder)) } 235 | ) 236 | } 237 | 238 | // See `SQLDatabase.execute(sql:_:)`. 239 | @usableFromInline 240 | func execute( 241 | sql query: any SQLExpression, 242 | _ onRow: @escaping @Sendable (any SQLRow) -> () 243 | ) async throws { 244 | let (sql, rawBinds) = self.serialize(query) 245 | 246 | if let queryLogLevel = self.queryLogLevel { 247 | self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(rawBinds.map { .string("\($0)") })]) 248 | } 249 | 250 | try await self.database.query( 251 | sql, 252 | rawBinds.map { try self.encoder.encode($0) }, 253 | { onRow($0.sql(decoder: self.decoder)) } 254 | ) 255 | } 256 | 257 | // See `SQLDatabase.withSession(_:)`. 258 | @usableFromInline 259 | func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { 260 | try await self.database.withConnection { 261 | try await closure($0.sql(encoder: self.encoder, decoder: self.decoder, queryLogLevel: self.queryLogLevel)) 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteConnectionSource.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Darwin) 2 | import Foundation 3 | #else 4 | @preconcurrency import Foundation 5 | #endif 6 | import Logging 7 | import AsyncKit 8 | import NIOPosix 9 | import SQLiteNIO 10 | import NIOCore 11 | 12 | /// A `ConnectionPoolSource` providing SQLite database connections for a given ``SQLiteConfiguration``. 13 | public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { 14 | private let configuration: SQLiteConfiguration 15 | private let actualURL: URL 16 | private let threadPool: NIOThreadPool 17 | 18 | private var connectionStorage: SQLiteConnection.Storage { 19 | .file(path: self.actualURL.absoluteString) 20 | } 21 | 22 | /// Create a new ``SQLiteConnectionSource``. 23 | /// 24 | /// > Important: If the caller provides a thread pool other than the default, they are responsible for starting 25 | /// > the pool before any connections are made and shutting it down only after all connections are closed. It is 26 | /// > strongly recommended that all callers use the default. 27 | /// 28 | /// - Parameters: 29 | /// - configuration: The configuration for new connections. 30 | /// - threadPool: The thread pool used by connections. Defaults to the global singleton. 31 | public init( 32 | configuration: SQLiteConfiguration, 33 | threadPool: NIOThreadPool = .singleton 34 | ) { 35 | self.configuration = configuration 36 | self.actualURL = configuration.storage.urlForSQLite 37 | self.threadPool = threadPool 38 | } 39 | 40 | // See `ConnectionPoolSource.makeConnection(logger:on:)`. 41 | public func makeConnection( 42 | logger: Logger, 43 | on eventLoop: any EventLoop 44 | ) -> EventLoopFuture { 45 | SQLiteConnection.open( 46 | storage: self.connectionStorage, 47 | threadPool: self.threadPool, 48 | logger: logger, 49 | on: eventLoop 50 | ).flatMap { conn in 51 | if self.configuration.enableForeignKeys { 52 | return conn.query("PRAGMA foreign_keys = ON").map { _ in conn } 53 | } else { 54 | return eventLoop.makeSucceededFuture(conn) 55 | } 56 | } 57 | } 58 | } 59 | 60 | extension SQLiteNIO.SQLiteConnection: AsyncKit.ConnectionPoolItem {} 61 | 62 | fileprivate extension String { 63 | /// Attempt to "sanitize" a string for use as a filename. This is quick and dirty and probably not 100% correct. 64 | var asSafeFilename: String { 65 | #if os(Windows) 66 | self.replacingOccurrences(of: ":", with: "_").replacingOccurrences(of: "\\", with: "-") 67 | #else 68 | self.replacingOccurrences(of: "/", with: "-") 69 | #endif 70 | } 71 | } 72 | 73 | fileprivate extension SQLiteConfiguration.Storage { 74 | /// Because SQLiteNIO specifies the recommended `SQLITE_OMIT_SHARED_CACHE` build flag, we cannot implement our 75 | /// claimed support for multiple connections to in-memory databases using actual in-memory databases. 76 | /// Unfortunately, Fluent relies on having this support, and it had been public API since long before the change 77 | /// in build flags. Therefore, we work around it by using temporary files to fake in-memory databases. 78 | /// 79 | /// This has the unfortunate side effect of violating the "when the last connection to an in-memory database is 80 | /// closed, it is immediately deleted" semantics, but fortunately no one seems to have relied on that behavior. 81 | /// 82 | /// We include both the user-provided identifer and the current process ID in the filename for the temporary 83 | /// file because in-memory databases are expected to be process-specific. 84 | var urlForSQLite: URL { 85 | switch self { 86 | case .memory(identifier: let identifier): 87 | let tempFilename = "sqlite-kit_memorydb-\(ProcessInfo.processInfo.processIdentifier)-\(identifier).sqlite3" 88 | 89 | return FileManager.default.temporaryDirectory 90 | .appendingPathComponent(tempFilename.asSafeFilename, isDirectory: false) 91 | case .file(path: let path): 92 | if path.starts(with: "file:"), let url = URL(string: path) { 93 | return url 94 | } else { 95 | return URL(fileURLWithPath: path, isDirectory: false) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteDataDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SQLiteNIO 3 | @_spi(CodableUtilities) import SQLKit 4 | import NIOFoundationCompat 5 | 6 | /// Translates `SQLiteData` values received from the database into `Decodable` values. 7 | /// 8 | /// Types which conform to `SQLiteDataConvertible` are converted directly to the requested type. For other types, 9 | /// an attempt is made to interpret the database value as JSON and decode the type from it. 10 | public struct SQLiteDataDecoder: Sendable { 11 | /// A wrapper to silence `Sendable` warnings for `JSONDecoder` when not on macOS. 12 | struct FakeSendable: @unchecked Sendable { let value: T } 13 | 14 | /// The `JSONDecoder` used for decoding values that can't be directly converted. 15 | let json: FakeSendable 16 | 17 | /// Initialize a ``SQLiteDataDecoder`` with an unconfigured JSON decoder. 18 | public init() { 19 | self.init(json: .init()) 20 | } 21 | 22 | /// Initialize a ``SQLiteDataDecoder`` with a JSON decoder. 23 | /// 24 | /// - Parameter json: A `JSONDecoder` to use for decoding types that can't be directly converted. 25 | public init(json: JSONDecoder) { 26 | self.json = .init(value: json) 27 | } 28 | 29 | /// Convert the given `SQLiteData` into a value of type `T`, if possible. 30 | /// 31 | /// - Parameters: 32 | /// - type: The desired result type. 33 | /// - data: The data to decode. 34 | /// - Returns: The decoded value, if successful. 35 | public func decode(_ type: T.Type, from data: SQLiteData) throws -> T { 36 | // If `T` can be converted directly, just do so. 37 | if let type = type as? any SQLiteDataConvertible.Type { 38 | guard let value = type.init(sqliteData: data) else { 39 | throw DecodingError.typeMismatch(T.self, .init( 40 | codingPath: [], 41 | debugDescription: "Could not convert SQLite data to \(T.self): \(data)." 42 | )) 43 | } 44 | return value as! T 45 | } else { 46 | do { 47 | return try T.init(from: NestedSingleValueUnwrappingDecoder(decoder: self, data: data)) 48 | } catch is SQLCodingError { 49 | // Couldn't unwrap it either. Fall back to attempting a JSON decode. 50 | let buf: Data 51 | 52 | switch data { 53 | case .text(let str): buf = .init(str.utf8) 54 | case .blob(let blob): buf = .init(buffer: blob, byteTransferStrategy: .noCopy) 55 | // The remaining cases should never happen, but we implement them anyway just in case. 56 | case .integer(let n): buf = .init(String(n).utf8) 57 | case .float(let n): buf = .init(String(n).utf8) 58 | case .null: buf = .init() 59 | } 60 | return try self.json.value.decode(T.self, from: buf) 61 | } 62 | } 63 | } 64 | 65 | /// A trivial decoder for unwrapping types which decode as trivial single-value containers. This allows for 66 | /// correct handling of types such as `Optional` when they do not conform to `SQLiteDataCovnertible`. 67 | private final class NestedSingleValueUnwrappingDecoder: Decoder, SingleValueDecodingContainer { 68 | // See `Decoder.codingPath` and `SingleValueDecodingContainer.codingPath`. 69 | var codingPath: [any CodingKey] { [] } 70 | 71 | // See `Decoder.userInfo`. 72 | var userInfo: [CodingUserInfoKey: Any] { [:] } 73 | 74 | /// The parent ``SQLiteDataDecoder``. 75 | let dataDecoder: SQLiteDataDecoder 76 | 77 | /// The data to decode. 78 | let data: SQLiteData 79 | 80 | /// Create a new decoder with an ``SQLiteDataDecoder`` and the data to decode. 81 | init(decoder: SQLiteDataDecoder, data: SQLiteData) { 82 | self.dataDecoder = decoder 83 | self.data = data 84 | } 85 | 86 | // See `Decoder.container(keyedBy:)`. 87 | func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer { 88 | throw .invalid(at: self.codingPath) 89 | } 90 | 91 | // See `Decoder.unkeyedContainer()`. 92 | func unkeyedContainer() throws -> any UnkeyedDecodingContainer { 93 | throw .invalid(at: self.codingPath) 94 | } 95 | 96 | // See `Decoder.singleValueContainer()`. 97 | func singleValueContainer() throws -> any SingleValueDecodingContainer { 98 | self 99 | } 100 | 101 | // See `SingleValueDecodingContainer.decodeNil()`. 102 | func decodeNil() -> Bool { 103 | self.data.isNull 104 | } 105 | 106 | // See `SingleValueDecodingContainer.decode(_:)`. 107 | func decode(_: T.Type) throws -> T { 108 | try self.dataDecoder.decode(T.self, from: self.data) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteDataEncoder.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import Foundation 3 | @_spi(CodableUtilities) import SQLKit 4 | import SQLiteNIO 5 | 6 | /// Translates `Encodable` values into `SQLiteData` values suitable for use with an `SQLiteDatabase`. 7 | /// 8 | /// Types which conform to `SQLiteDataConvertible` are converted directly to `SQLiteData`. Other types are 9 | /// encoded as JSON and sent to the database as text. 10 | public struct SQLiteDataEncoder: Sendable { 11 | /// A wrapper to silence `Sendable` warnings for `JSONEncoder` when not on macOS. 12 | struct FakeSendable: @unchecked Sendable { let value: T } 13 | 14 | /// The `JSONEncoder` used for encoding values that can't be directly converted. 15 | let json: FakeSendable 16 | 17 | /// Initialize a ``SQLiteDataEncoder`` with an unconfigured JSON encoder. 18 | public init() { 19 | self.init(json: .init()) 20 | } 21 | 22 | /// Initialize a ``SQLiteDataEncoder`` with a JSON encoder. 23 | /// 24 | /// - Parameter json: A `JSONEncoder` to use for encoding types that can't be directly converted. 25 | public init(json: JSONEncoder) { 26 | self.json = .init(value: json) 27 | } 28 | 29 | /// Convert the given `Encodable` value to an `SQLiteData` value, if possible. 30 | /// 31 | /// - Parameter value: The value to convert. 32 | /// - Returns: A converted `SQLiteData` value, if successful. 33 | public func encode(_ value: any Encodable) throws -> SQLiteData { 34 | if let data = (value as? any SQLiteDataConvertible)?.sqliteData { 35 | return data 36 | } else { 37 | let encoder = NestedSingleValueUnwrappingEncoder(dataEncoder: self) 38 | 39 | do { 40 | try value.encode(to: encoder) 41 | guard let value = encoder.value else { 42 | throw SQLCodingError.unsupportedOperation("missing value", codingPath: []) 43 | } 44 | return value 45 | } catch is SQLCodingError { 46 | // Starting with SQLite 3.45.0 (2024-01-15), sending textual JSON as a blob will cause inexplicable 47 | // errors due to the data being interpreted as JSONB (arguably not the best behavior for SQLite's API, 48 | // but not technically a compatibility break). As there is no good way to get at the underlying SQLite 49 | // version from the data encoder, and extending `SQLiteData` would make a rather epic mess, we now just 50 | // always send JSON as text instead. This is technically what we should have been doing all along 51 | // anyway, meaning this change is a bugfix. Good thing, too - otherwise we'd be stuck trying to retain 52 | // bug-for-bug compatibility, starting with reverse-engineering SQLite's JSONB format (which is not the 53 | // same as PostgreSQL's, of course). 54 | // 55 | // Update: SQLite 3.45.1 (2024-01-30) fixed the JSON-blob behavior, but as noted above, we prefer 56 | // sending JSON as text anyway, so we've left it as-is. 57 | return .text(.init(decoding: try self.json.value.encode(value), as: UTF8.self)) 58 | } 59 | } 60 | } 61 | 62 | /// A trivial encoder for unwrapping types which encode as trivial single-value containers. This allows for 63 | /// correct handling of types such as `Optional` when they do not conform to `SQLiteDataConvertible`. 64 | private final class NestedSingleValueUnwrappingEncoder: Encoder, SingleValueEncodingContainer { 65 | // See `Encoder.userInfo`. 66 | var userInfo: [CodingUserInfoKey: Any] { [:] } 67 | 68 | // See `Encoder.codingPath` and `SingleValueEncodingContainer.codingPath`. 69 | var codingPath: [any CodingKey] { [] } 70 | 71 | /// The parent ``SQLiteDataEncoder``. 72 | let dataEncoder: SQLiteDataEncoder 73 | 74 | /// Storage for the resulting converted value. 75 | var value: SQLiteData? = nil 76 | 77 | /// Create a new encoder with an ``SQLiteDataEncoder``. 78 | init(dataEncoder: SQLiteDataEncoder) { 79 | self.dataEncoder = dataEncoder 80 | } 81 | 82 | // See `Encoder.container(keyedBy:)`. 83 | func container(keyedBy: K.Type) -> KeyedEncodingContainer { 84 | .invalid(at: self.codingPath) 85 | } 86 | 87 | // See `Encoder.unkeyedContainer`. 88 | func unkeyedContainer() -> any UnkeyedEncodingContainer { 89 | .invalid(at: self.codingPath) 90 | } 91 | 92 | // See `Encoder.singleValueContainer`. 93 | func singleValueContainer() -> any SingleValueEncodingContainer { 94 | self 95 | } 96 | 97 | // See `SingleValueEncodingContainer.encodeNil()`. 98 | func encodeNil() throws { 99 | self.value = .null 100 | } 101 | 102 | // See `SingleValueEncodingContainer.encode(_:)`. 103 | func encode(_ value: Bool) throws { 104 | self.value = .integer(value ? 1 : 0) 105 | } 106 | 107 | // See `SingleValueEncodingContainer.encode(_:)`. 108 | func encode(_ value: String) throws { 109 | self.value = .text(value) 110 | } 111 | 112 | // See `SingleValueEncodingContainer.encode(_:)`. 113 | func encode(_ value: Float) throws { 114 | self.value = .float(Double(value)) 115 | } 116 | 117 | // See `SingleValueEncodingContainer.encode(_:)`. 118 | func encode(_ value: Double) throws { 119 | self.value = .float(value) 120 | } 121 | 122 | // See `SingleValueEncodingContainer.encode(_:)`. 123 | func encode(_ value: Int8) throws { 124 | self.value = .integer(numericCast(value)) 125 | } 126 | 127 | // See `SingleValueEncodingContainer.encode(_:)`. 128 | func encode(_ value: Int16) throws { 129 | self.value = .integer(numericCast(value)) 130 | } 131 | 132 | // See `SingleValueEncodingContainer.encode(_:)`. 133 | func encode(_ value: Int32) throws { 134 | self.value = .integer(numericCast(value)) 135 | } 136 | 137 | // See `SingleValueEncodingContainer.encode(_:)`. 138 | func encode(_ value: Int64) throws { 139 | self.value = .integer(numericCast(value)) 140 | } 141 | 142 | // See `SingleValueEncodingContainer.encode(_:)`. 143 | func encode(_ value: Int) throws { 144 | self.value = .integer(numericCast(value)) 145 | } 146 | 147 | // See `SingleValueEncodingContainer.encode(_:)`. 148 | func encode(_ value: UInt8) throws { 149 | self.value = .integer(numericCast(value)) 150 | } 151 | 152 | // See `SingleValueEncodingContainer.encode(_:)`. 153 | func encode(_ value: UInt16) throws { 154 | self.value = .integer(numericCast(value)) 155 | } 156 | 157 | // See `SingleValueEncodingContainer.encode(_:)`. 158 | func encode(_ value: UInt32) throws { 159 | self.value = .integer(numericCast(value)) 160 | } 161 | 162 | // See `SingleValueEncodingContainer.encode(_:)`. 163 | func encode(_ value: UInt64) throws { 164 | self.value = .integer(numericCast(value)) 165 | } 166 | 167 | // See `SingleValueEncodingContainer.encode(_:)`. 168 | func encode(_ value: UInt) throws { 169 | self.value = .integer(numericCast(value)) 170 | } 171 | 172 | // See `SingleValueEncodingContainer.encode(_:)`. 173 | func encode(_ value: some Encodable) throws { 174 | self.value = try self.dataEncoder.encode(value) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteDialect.swift: -------------------------------------------------------------------------------- 1 | import SQLKit 2 | 3 | /// The `SQLDialect` defintions for SQLite. 4 | /// 5 | /// > Note: There is only ever one SQLite library in use by SQLiteNIO in any given process (even if there are 6 | /// > other versions of the library being used by other things). As such, there is no need for the dialect to 7 | /// > concern itself with what version the connection using it "connected" to - it can always just look up the 8 | /// > global "runtime version". 9 | public struct SQLiteDialect: SQLDialect { 10 | /// Create a new ``SQLiteDialect``. 11 | public init() {} 12 | 13 | // See `SQLDialect.name`. 14 | public var name: String { 15 | "sqlite" 16 | } 17 | 18 | // See `SQLDialect.identifierQuote`. 19 | public var identifierQuote: any SQLExpression { 20 | SQLRaw(#"""#) 21 | } 22 | 23 | // See `SQLDialect.literalStringQuote`. 24 | public var literalStringQuote: any SQLExpression { 25 | SQLRaw("'") 26 | } 27 | 28 | // See `SQLDialect.bindPlaceholder(at:)`. 29 | public func bindPlaceholder(at position: Int) -> any SQLExpression { 30 | SQLRaw("?\(position)") 31 | } 32 | 33 | // See `SQLDialect.literalBoolean(_:)`. 34 | public func literalBoolean(_ value: Bool) -> any SQLExpression { 35 | SQLRaw(value ? "TRUE" : "FALSE") 36 | } 37 | 38 | // See `SQLDialect.literalDefault`. 39 | public var literalDefault: any SQLExpression { 40 | SQLLiteral.null 41 | } 42 | 43 | // See `SQLDialect.supportsIfExists`. 44 | public var supportsIfExists: Bool { 45 | true 46 | } 47 | 48 | // See `SQLDialect.supportsDropBehavior`. 49 | public var supportsDropBehavior: Bool { 50 | false 51 | } 52 | 53 | // See `SQLDialect.supportsAutoIncrement`. 54 | public var supportsAutoIncrement: Bool { 55 | false 56 | } 57 | 58 | // See `SQLDialect.autoIncrementClause`. 59 | public var autoIncrementClause: any SQLExpression { 60 | SQLRaw("AUTOINCREMENT") 61 | } 62 | 63 | // See `SQLDialect.enumSyntax`. 64 | public var enumSyntax: SQLEnumSyntax { 65 | .unsupported 66 | } 67 | 68 | // See `SQLDialect.triggerSyntax`. 69 | public var triggerSyntax: SQLTriggerSyntax { 70 | .init(create: [.supportsBody, .supportsCondition, .supportsUpdateColumns], drop: []) 71 | } 72 | 73 | // See `SQLDialect.alterTableSyntax`. 74 | public var alterTableSyntax: SQLAlterTableSyntax { 75 | .init(allowsBatch: false) 76 | } 77 | 78 | // See `SQLDialect.upsertSyntax`. 79 | public var upsertSyntax: SQLUpsertSyntax { 80 | self.isAtLeastVersion(3, 24, 0) ? .standard : .unsupported // `UPSERT` was added to SQLite in 3.24.0. 81 | } 82 | 83 | // See `SQLDialect.supportsReturning`. 84 | public var supportsReturning: Bool { 85 | self.isAtLeastVersion(3, 35, 0) // `RETURNING` was added to SQLite in 3.35.0. 86 | } 87 | 88 | // See `SQLDialect.unionFeatures`. 89 | public var unionFeatures: SQLUnionFeatures { 90 | [.union, .unionAll, .intersect, .except] 91 | } 92 | 93 | // See `SQLDialect.customDataType(for:)`. 94 | public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? { 95 | if case .bigint = dataType { 96 | // Translate requests for bigint to requests for SQLite's plain integer type. This yields the autoincrement 97 | // primary key behavior when a 64-bit integer is requested from a higher layer. 98 | return SQLDataType.int 99 | } 100 | return nil 101 | } 102 | 103 | // See `SQLDialect.nestedSubpathExpression(in:for:)`. 104 | public func nestedSubpathExpression(in column: any SQLExpression, for path: [String]) -> (any SQLExpression)? { 105 | guard !path.isEmpty else { return nil } 106 | 107 | return SQLFunction("json_extract", args: [ 108 | column, 109 | SQLLiteral.string("$.\(path.joined(separator: "."))") 110 | ]) 111 | } 112 | 113 | /// Convenience utility for checking current SQLite version. 114 | private func isAtLeastVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool { 115 | SQLiteDatabaseVersion.runtimeVersion >= SQLiteDatabaseVersion(major: major, minor: minor, patch: patch) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/SQLiteKit/SQLiteRow+SQLRow.swift: -------------------------------------------------------------------------------- 1 | import SQLKit 2 | import SQLiteNIO 3 | 4 | extension SQLiteRow { 5 | /// Return an `SQLRow` interface to this row. 6 | /// 7 | /// - Parameter decoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in 8 | /// `SQLRow`s. 9 | /// - Returns: An instance of `SQLRow` which accesses the same data as `self`. 10 | public func sql(decoder: SQLiteDataDecoder = .init()) -> any SQLRow { 11 | SQLiteSQLRow(row: self, decoder: decoder) 12 | } 13 | } 14 | 15 | /// An error used to signal that a column requested from a `MySQLRow` using the `SQLRow` interface is not present. 16 | struct MissingColumn: Error { 17 | let column: String 18 | } 19 | 20 | /// Wraps an `SQLiteRow` with the `SQLRow` protocol. 21 | private struct SQLiteSQLRow: SQLRow { 22 | /// The underlying `SQLiteRow`. 23 | let row: SQLiteRow 24 | 25 | /// A ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values. 26 | let decoder: SQLiteDataDecoder 27 | 28 | // See `SQLRow.allColumns`. 29 | var allColumns: [String] { 30 | self.row.columns.map { $0.name } 31 | } 32 | 33 | // See `SQLRow.contains(column:)`. 34 | func contains(column: String) -> Bool { 35 | self.row.column(column) != nil 36 | } 37 | 38 | // See `SQLRow.decodeNil(column:)`. 39 | func decodeNil(column: String) throws -> Bool { 40 | guard let data = self.row.column(column) else { 41 | return true 42 | } 43 | return data == .null 44 | } 45 | 46 | // See `SQLRow.decode(column:as:)`. 47 | func decode(column: String, as: D.Type) throws -> D { 48 | guard let data = self.row.column(column) else { 49 | throw MissingColumn(column: column) 50 | } 51 | return try self.decoder.decode(D.self, from: data) 52 | } 53 | } 54 | 55 | /// A legacy deprecated conformance of `SQLiteRow` directly to `SQLRow`. This interface exists solely 56 | /// because its absence would be a public API break. 57 | /// 58 | /// Do not use these methods directly. Call `sql(decoder:)` instead to access `SQLiteRow`s through 59 | /// an `SQLKit` interface. 60 | @available(*, deprecated, message: "Use SQLiteRow.sql(decoder:) to access an SQLiteRow as an SQLRow.") 61 | extension SQLiteNIO.SQLiteRow: SQLKit.SQLRow { 62 | // See `SQLRow.allColumns`. 63 | public var allColumns: [String] { self.columns.map { $0.name } } 64 | 65 | // See `SQLRow.contains(column:)`. 66 | public func contains(column: String) -> Bool { self.column(column) != nil } 67 | 68 | // See `SQLRow.decodeNil(column:)`. 69 | public func decodeNil(column: String) throws -> Bool { (self.column(column) ?? .null) == .null } 70 | 71 | // See `SQLRow.decode(column:as:)`. 72 | public func decode(column: String, as: D.Type) throws -> D { 73 | guard let data = self.column(column) else { throw MissingColumn(column: column) } 74 | return try SQLiteDataDecoder().decode(D.self, from: data) 75 | } 76 | 77 | // See `SQLRow.decode(column:inferringAs:)`. 78 | public func decode(column c: String, inferringAs: D.Type = D.self) throws -> D { try self.decode(column: c, as: D.self) } 79 | 80 | // See `SQLRow.decode(model:prefix:keyDecodingStrategy:userInfo:)`. 81 | public func decode( 82 | model: D.Type, prefix: String? = nil, keyDecodingStrategy: SQLRowDecoder.KeyDecodingStrategy = .useDefaultKeys, 83 | userInfo: [CodingUserInfoKey: any Sendable] = [:] 84 | ) throws -> D { 85 | try self.decode(model: D.self, with: .init(prefix: prefix, keyDecodingStrategy: keyDecodingStrategy, userInfo: userInfo)) 86 | } 87 | 88 | // See `SQLRow.decode(model:with:)`. 89 | public func decode(model: D.Type, with: SQLRowDecoder) throws -> D { try with.decode(D.self, from: self) } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/SQLiteKitTests/SQLiteKitTests.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | import SQLiteKit 3 | import SQLKitBenchmark 4 | import XCTest 5 | import SQLiteNIO 6 | import SQLKit 7 | 8 | final class SQLiteKitTests: XCTestCase { 9 | func testSQLKitBenchmark() async throws { 10 | let benchmark = SQLBenchmarker(on: self.db) 11 | 12 | try await benchmark.runAllTests() 13 | } 14 | 15 | func testPlanets() async throws { 16 | try await self.db.drop(table: "planets") 17 | .ifExists() 18 | .run() 19 | try await self.db.drop(table: "galaxies") 20 | .ifExists() 21 | .run() 22 | 23 | try await self.db.create(table: "galaxies") 24 | .column("id", type: .int, .primaryKey) 25 | .column("name", type: .text) 26 | .run() 27 | try await self.db.create(table: "planets").ifNotExists() 28 | .column("id", type: .int, .primaryKey) 29 | .column("galaxyID", type: .int, .references("galaxies", "id")) 30 | .run() 31 | 32 | try await self.db.alter(table: "planets") 33 | .column("name", type: .text, .default(SQLLiteral.string("Unamed Planet"))) 34 | .run() 35 | try await self.db.create(index: "test_index") 36 | .on("planets") 37 | .column("id") 38 | .unique() 39 | .run() 40 | 41 | // INSERT INTO "galaxies" ("id", "name") VALUES (DEFAULT, $1) 42 | try await self.db.insert(into: "galaxies") 43 | .columns("id", "name") 44 | .values(SQLLiteral.null, SQLBind("Milky Way")) 45 | .values(SQLLiteral.null, SQLBind("Andromeda")) 46 | .run() 47 | 48 | // SELECT * FROM galaxies WHERE name != NULL AND (name == ? OR name == ?) 49 | _ = try await self.db.select() 50 | .column("*") 51 | .from("galaxies") 52 | .where("name", .notEqual, SQLLiteral.null) 53 | .where { $0 54 | .orWhere("name", .equal, SQLBind("Milky Way")) 55 | .orWhere("name", .equal, SQLBind("Andromeda")) 56 | } 57 | .all() 58 | 59 | _ = try await self.db.select() 60 | .column("*") 61 | .from("galaxies") 62 | .where(SQLColumn("name"), .equal, SQLBind("Milky Way")) 63 | .groupBy("id") 64 | .orderBy("name", .descending) 65 | .all() 66 | 67 | try await self.db.insert(into: "planets") 68 | .columns("id", "name") 69 | .values(SQLLiteral.null, SQLBind("Earth")) 70 | .run() 71 | 72 | try await self.db.insert(into: "planets") 73 | .columns("id", "name") 74 | .values(SQLLiteral.null, SQLBind("Mercury")) 75 | .values(SQLLiteral.null, SQLBind("Venus")) 76 | .values(SQLLiteral.null, SQLBind("Mars")) 77 | .values(SQLLiteral.null, SQLBind("Jpuiter")) 78 | .values(SQLLiteral.null, SQLBind("Pluto")) 79 | .run() 80 | 81 | try await self.db.select() 82 | .column(SQLFunction("count", args: "name")) 83 | .from("planets") 84 | .where("galaxyID", .equal, SQLBind(5)) 85 | .run() 86 | 87 | try await self.db.select() 88 | .column(SQLFunction("count", args: SQLLiteral.all)) 89 | .from("planets") 90 | .where("galaxyID", .equal, SQLBind(5)) 91 | .run() 92 | } 93 | 94 | func testForeignKeysEnabledOnlyWhenRequested() async throws { 95 | let res = try await self.connection.query("PRAGMA foreign_keys").get() 96 | 97 | XCTAssertEqual(res[0].column("foreign_keys"), .integer(1)) 98 | 99 | // Using `.file` storage here is a quick and dirty nod to increasing test coverage. 100 | let source = SQLiteConnectionSource( 101 | configuration: .init(storage: .file( 102 | path: FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID()).sqlite3", isDirectory: false).path 103 | ), enableForeignKeys: false), 104 | threadPool: .singleton 105 | ) 106 | 107 | let conn2 = try await source.makeConnection(logger: self.connection.logger, on: MultiThreadedEventLoopGroup.singleton.any()).get() 108 | defer { try! conn2.close().wait() } 109 | 110 | let res2 = try await conn2.query("PRAGMA foreign_keys").get() 111 | XCTAssertEqual(res2[0].column("foreign_keys"), .integer(0)) 112 | } 113 | 114 | func testJSONStringColumn() async throws { 115 | _ = try await self.connection.query("CREATE TABLE foo (bar TEXT)").get() 116 | _ = try await self.connection.query(#"INSERT INTO foo (bar) VALUES ('{"baz": "qux"}')"#).get() 117 | let rows = try await self.connection.query("SELECT * FROM foo").get() 118 | 119 | struct Bar: Codable { 120 | var baz: String 121 | } 122 | let bar = try SQLiteDataDecoder().decode(Bar.self, from: rows[0].column("bar")!) 123 | XCTAssertEqual(bar.baz, "qux") 124 | } 125 | 126 | func testMultipleInMemoryDatabases() async throws { 127 | let a = SQLiteConnectionSource( 128 | configuration: .init(storage: .memory, enableForeignKeys: true), 129 | threadPool: .singleton 130 | ) 131 | let b = SQLiteConnectionSource( 132 | configuration: .init(storage: .memory, enableForeignKeys: true), 133 | threadPool: .singleton 134 | ) 135 | 136 | let a1 = try await a.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() 137 | defer { try! a1.close().wait() } 138 | let a2 = try await a.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() 139 | defer { try! a2.close().wait() } 140 | let b1 = try await b.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() 141 | defer { try! b1.close().wait() } 142 | let b2 = try await b.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() 143 | defer { try! b2.close().wait() } 144 | 145 | _ = try await a1.query("CREATE TABLE foo (bar INTEGER)").get() 146 | _ = try await a2.query("SELECT * FROM foo").get() 147 | _ = try await b1.query("CREATE TABLE foo (bar INTEGER)").get() 148 | _ = try await b2.query("SELECT * FROM foo").get() 149 | } 150 | 151 | // https://github.com/vapor/sqlite-kit/issues/56 152 | func testDoubleConstraintError() async throws { 153 | try await self.db.create(table: "foo") 154 | .ifNotExists() 155 | .column("id", type: .text, .primaryKey(autoIncrement: false), .notNull) 156 | .run() 157 | } 158 | 159 | func testRowDecoding() async throws { 160 | try await self.db.create(table: "foo") 161 | .column("id", type: .int, .primaryKey(autoIncrement: false), .notNull) 162 | .column("value", type: .text) 163 | .run() 164 | try await self.db.insert(into: "foo") 165 | .columns("id", "value") 166 | .values(SQLLiteral.numeric("1"), SQLBind("abc")) 167 | .values(SQLLiteral.numeric("2"), SQLBind(String?.none)) 168 | .run() 169 | let rows = try await self.db.select() 170 | .column("value") 171 | .from("foo") 172 | .orderBy("id") 173 | .all() 174 | 175 | XCTAssertEqual(rows.count, 2) 176 | let row1 = try XCTUnwrap(rows.dropFirst(0).first), 177 | row2 = try XCTUnwrap(rows.dropFirst(1).first) 178 | XCTAssertTrue(row1.contains(column: "value")) 179 | XCTAssertFalse(try row1.decodeNil(column: "value")) 180 | XCTAssertEqual(try row1.decode(column: "value", as: String?.self), "abc") 181 | XCTAssertThrowsError(try row1.decode(column: "value", as: Int.self)) 182 | XCTAssertThrowsError(try row1.decode(column: "nonexistent", as: String?.self)) 183 | XCTAssertTrue(try row1.decodeNil(column: "nonexistent")) 184 | XCTAssertTrue(row2.contains(column: "value")) 185 | XCTAssertTrue(try row2.decodeNil(column: "value")) 186 | XCTAssertEqual(try row2.decode(column: "value", as: String?.self), nil) 187 | XCTAssertThrowsError(try row2.decode(column: "value", as: Int.self)) 188 | XCTAssertThrowsError(try row2.decode(column: "nonexistent", as: String?.self)) 189 | XCTAssertTrue(try row2.decodeNil(column: "nonexistent")) 190 | } 191 | 192 | func testRowEncoding() async throws { 193 | struct SubFoo: Codable { 194 | struct NestFoo: Codable { let x: Double } 195 | let arr: [Int] 196 | let val: String 197 | let nest: NestFoo 198 | } 199 | await XCTAssertNoThrowAsync( 200 | try await self.db.create(table: "foo") 201 | .column("id", type: .int, .primaryKey(autoIncrement: false), .notNull) 202 | .column("value", type: .custom(SQLRaw("json"))) 203 | .run() 204 | ) 205 | await XCTAssertNoThrowAsync( 206 | try await self.db.insert(into: "foo") 207 | .columns("id", "value") 208 | .values(SQLLiteral.numeric("1"), SQLBind(SubFoo(arr: [1,2,3], val: "a", nest: .init(x: 1.1)))) 209 | .values(SQLLiteral.numeric("2"), SQLBind(SubFoo?.none)) 210 | .run() 211 | ) 212 | let rows = try await XCTUnwrapAsync( 213 | try await self.db.select() 214 | .column(self.db.dialect.nestedSubpathExpression(in: SQLColumn("value"), for: ["nest", "x"])!, as: "x") 215 | .from("foo") 216 | .orderBy("id") 217 | .all() 218 | ) 219 | 220 | XCTAssertEqual(rows.count, 2) 221 | let row1 = try XCTUnwrap(rows.dropFirst(0).first), 222 | row2 = try XCTUnwrap(rows.dropFirst(1).first) 223 | XCTAssertTrue(row1.contains(column: "x")) 224 | XCTAssertTrue(row2.contains(column: "x")) 225 | XCTAssertEqual(try row1.decode(column: "x", as: Double.self), 1.1) 226 | XCTAssertTrue(try row2.decodeNil(column: "x")) 227 | } 228 | 229 | // https://github.com/vapor/sqlite-kit/issues/62 230 | func testEncodeNestedArray() throws { 231 | struct Foo: Encodable { 232 | var bar: [String] 233 | } 234 | let foo = Foo(bar: ["a", "b", "c"]) 235 | _ = try SQLiteDataEncoder().encode(foo) 236 | } 237 | 238 | var db: any SQLDatabase { self.connection.sql() } 239 | var connection: SQLiteConnection! 240 | 241 | override func setUp() async throws { 242 | XCTAssertTrue(isLoggingConfigured) 243 | 244 | self.connection = try await SQLiteConnectionSource( 245 | configuration: .init(storage: .memory, enableForeignKeys: true), 246 | threadPool: .singleton 247 | ).makeConnection( 248 | logger: .init(label: "test"), 249 | on: MultiThreadedEventLoopGroup.singleton.any() 250 | ).get() 251 | } 252 | 253 | override func tearDown() async throws { 254 | try await self.connection.close().get() 255 | self.connection = nil 256 | } 257 | } 258 | 259 | func env(_ name: String) -> String? { 260 | getenv(name).flatMap { String(cString: $0) } 261 | } 262 | 263 | let isLoggingConfigured: Bool = { 264 | LoggingSystem.bootstrap { label in 265 | var handler = StreamLogHandler.standardOutput(label: label) 266 | handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .debug 267 | return handler 268 | } 269 | return true 270 | }() 271 | 272 | func XCTAssertNoThrowAsync( 273 | _ expression: @autoclosure () async throws -> T, 274 | _ message: @autoclosure () -> String = "", 275 | file: StaticString = #filePath, line: UInt = #line 276 | ) async { 277 | do { 278 | _ = try await expression() 279 | } catch { 280 | XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line) 281 | } 282 | } 283 | 284 | func XCTUnwrapAsync( 285 | _ expression: @autoclosure () async throws -> T?, 286 | _ message: @autoclosure () -> String = "", 287 | file: StaticString = #filePath, line: UInt = #line 288 | ) async throws -> T { 289 | let result: T? 290 | 291 | do { 292 | result = try await expression() 293 | } catch { 294 | return try XCTUnwrap(try { throw error }(), message(), file: file, line: line) 295 | } 296 | return try XCTUnwrap(result, message(), file: file, line: line) 297 | } 298 | --------------------------------------------------------------------------------