├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------