Skip to content

Commit c07d530

Browse files
authored
Modernize SQLite feature support (#100)
* Drop support for Swift <5.5 * Same heavy CI update SQLiteNIO got * Do a mostly visual update to SQLiteDialect, removing quite a lot of confusing whitespace and simplifying most of the content. Adds a `customDataType(for:)` override to handling requests for bigint types and switches from plain `?` for placeholders to SQLite's numbered `?NNNN` representation per SQLite docs recommendation. * Tiny bit of tests cleanup * Provide a type implementing SQLDatabaseReportedVersion, vend it from SQLiteDatabase, and use it to enable version-dependent RETURNING and UPSERT support for SQLite. * Require updated SQLiteNIO that has the new APIs
1 parent 55ed1fc commit c07d530

File tree

5 files changed

+211
-100
lines changed

5 files changed

+211
-100
lines changed

.github/workflows/test.yml

Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,118 @@
11
name: test
22
on:
3-
pull_request:
3+
pull_request: { branches: ['*'] }
44
push: { branches: [ main ] }
5-
defaults:
6-
run:
7-
shell: bash
5+
6+
env:
7+
LOG_LEVEL: debug
8+
SWIFT_DETERMINISTIC_HASHING: 1
9+
810
jobs:
9-
dependents:
11+
12+
codecov:
1013
runs-on: ubuntu-latest
11-
container: swift:5.5-focal
12-
strategy:
13-
fail-fast: false
14-
matrix:
15-
dependent:
16-
- fluent-sqlite-driver
14+
container: swift:5.7-jammy
15+
steps:
16+
# N.B.: When we switch to embedded SQLite, these first two steps should be removed,
17+
# and the version saved to the environment should come from the checked-out package.
18+
- name: Install libsqlite3 dependency
19+
run: apt-get -q update && apt-get -q install -y libsqlite3-dev
20+
- name: Save SQLite version to env
21+
run: |
22+
echo SQLITE_VERSION="$(pkg-config --modversion sqlite3)" >> $GITHUB_ENV
23+
- name: Check out package
24+
uses: actions/checkout@v3
25+
- name: Run local tests with coverage
26+
run: swift test --enable-code-coverage
27+
- name: Submit coverage report to Codecov.io
28+
uses: vapor/swift-codecov-action@v0.2
29+
with:
30+
cc_flags: 'unittests'
31+
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,SQLITE_VERSION'
32+
cc_fail_ci_if_error: true
33+
cc_verbose: true
34+
cc_dry_run: false
35+
36+
# Check for API breakage versus main
37+
api-breakage:
38+
if: github.event_name == 'pull_request'
39+
runs-on: ubuntu-latest
40+
container: swift:5.7-jammy
1741
steps:
18-
- name: Install dependencies
42+
- name: Install libsqlite3 dependency
1943
run: apt-get -q update && apt-get -q install -y libsqlite3-dev
2044
- name: Check out package
21-
uses: actions/checkout@v2
45+
uses: actions/checkout@v3
2246
with:
23-
path: package
24-
- name: Check out dependent
25-
uses: actions/checkout@v2
47+
fetch-depth: 0
48+
# https://github.com/actions/checkout/issues/766
49+
- name: Mark the workspace as safe
50+
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
51+
- name: Check for API breaking changes
52+
run: swift package diagnose-api-breaking-changes origin/main
53+
54+
# Make sure downstream dependents still work
55+
dependents-check:
56+
if: github.event_name == 'pull_request'
57+
runs-on: ubuntu-latest
58+
container: swift:5.7-jammy
59+
steps:
60+
- name: Install libsqlite3 dependency
61+
run: apt-get -q update && apt-get -q install -y libsqlite3-dev
62+
- name: Check out package
63+
uses: actions/checkout@v3
2664
with:
27-
repository: vapor/${{ matrix.dependent }}
28-
path: dependent
29-
ref: main
30-
- name: Use local package
31-
run: swift package edit sqlite-kit --path ../package
32-
working-directory: dependent
33-
- name: Run tests with Thread Sanitizer
34-
run: swift test --enable-test-discovery --sanitize=thread
35-
working-directory: dependent
36-
linux:
65+
path: sqlite-kit
66+
- name: Check out FluentKit driver
67+
uses: actions/checkout@v3
68+
with:
69+
repository: vapor/fluent-sqlite-driver
70+
path: fluent-sqlite-driver
71+
- name: Tell dependents to use local checkout
72+
run: 'swift package --package-path fluent-sqlite-driver edit sqlite-kit --path sqlite-kit'
73+
- name: Run FluentSQLiteDriver tests with Thread Sanitizer
74+
run: swift test --package-path fluent-sqlite-driver --sanitize=thread
75+
76+
# Run unit tests (Linux)
77+
linux-unit:
78+
if: github.event_name == 'pull_request'
3779
strategy:
3880
fail-fast: false
3981
matrix:
4082
runner:
41-
- swift:5.2-focal
42-
- swift:5.5-focal
43-
- swiftlang/swift:nightly-main-focal
83+
- swift:5.5-bionic
84+
- swift:5.6-focal
85+
- swift:5.7-jammy
86+
- swiftlang/swift:nightly-main-jammy
4487
container: ${{ matrix.runner }}
4588
runs-on: ubuntu-latest
4689
steps:
47-
- name: Install dependencies
90+
- name: Install libsqlite3 dependency
4891
run: apt-get -q update && apt-get -q install -y libsqlite3-dev
4992
- name: Check out code
50-
uses: actions/checkout@v2
93+
uses: actions/checkout@v3
5194
- name: Run tests with Thread Sanitizer
52-
run: swift test --enable-test-discovery --sanitize=thread
53-
macOS:
95+
run: swift test --sanitize=thread
96+
97+
98+
# Run unit tests (macOS).
99+
macos-unit:
100+
if: github.event_name == 'pull_request'
54101
strategy:
55102
fail-fast: false
56103
matrix:
57-
version:
58-
- latest
104+
macos:
105+
- macos-11
106+
- macos-12
107+
xcode:
59108
- latest-stable
60-
runs-on: macos-11
109+
runs-on: ${{ matrix.macos }}
61110
steps:
62111
- name: Select latest available Xcode
63112
uses: maxim-lobanov/setup-xcode@v1
64113
with:
65-
xcode-version: ${{ matrix.version }}
114+
xcode-version: ${{ matrix.xcode }}
66115
- name: Check out code
67-
uses: actions/checkout@v2
116+
uses: actions/checkout@v3
68117
- name: Run tests with Thread Sanitizer
69-
run: swift test --enable-test-discovery --sanitize=thread
118+
run: swift test --sanitize=thread

Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.5
22
import PackageDescription
33

44
let package = Package(
@@ -11,9 +11,9 @@ let package = Package(
1111
.library(name: "SQLiteKit", targets: ["SQLiteKit"]),
1212
],
1313
dependencies: [
14-
.package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.0.0"),
15-
.package(url: "https://github.com/vapor/sql-kit.git", from: "3.16.0"),
16-
.package(url: "https://github.com/vapor/async-kit.git", from: "1.0.0"),
14+
.package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.2.0"),
15+
.package(url: "https://github.com/vapor/sql-kit.git", from: "3.19.0"),
16+
.package(url: "https://github.com/vapor/async-kit.git", from: "1.14.0"),
1717
],
1818
targets: [
1919
.target(name: "SQLiteKit", dependencies: [

Sources/SQLiteKit/SQLiteConnection+SQLKit.swift

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,93 @@ extension SQLiteDatabase {
44
}
55
}
66

7+
internal struct _SQLiteDatabaseVersion: SQLDatabaseReportedVersion {
8+
/// The numeric value of the version. The format of the value is the one described in
9+
/// https://sqlite.org/c3ref/c_source_id.html for the `SQLITE_VERSION_NUMBER` constant.
10+
let intValue: Int
11+
12+
/// The string representation of the version. The string is formatted according to the description in
13+
/// https://sqlite.org/c3ref/c_source_id.html for the `SQLITE_VERSION` constant.
14+
///
15+
/// This value is not used for equality or ordering comparisons; it is really only useful as a display value. We
16+
/// maintain a stored property for it here rather than always generating it as-needed from the numeric value so
17+
/// that we don't accidentally drop any additional information a particular library version might contain.
18+
///
19+
/// - Note: The string value should always represent the same version as the numeric value. This requirement is
20+
/// asserted in debug builds, but not otherwise enforced.
21+
let stringValue: String
22+
23+
/// Separates a numeric value into individual components and returns them.
24+
static func components(of intValue: Int) -> (major: Int, minor: Int, patch: Int) {
25+
let major = intValue / 1_000_000,
26+
minor = (intValue - major * 1_000_000) / 1_000,
27+
patch = intValue - major * 1_000_000 - minor * 1_000
28+
return (major: major, minor: minor, patch: patch)
29+
}
30+
31+
/// Get the version value representing the runtime version of the SQLite3 library in use.
32+
static var runtimeVersion: _SQLiteDatabaseVersion {
33+
self.init(intValue: Int(SQLiteConnection.libraryVersion()), stringValue: SQLiteConnection.libraryVersionString())
34+
}
35+
36+
/// Build a version value from individual components and synthesize the approiate string value.
37+
init(major: Int, minor: Int, patch: Int) {
38+
self.init(intValue: major * 1_000_000 + minor * 1_000 + patch)
39+
}
40+
41+
/// Designated initializer. Build a version value from the combined numeric value and a corresponding string value.
42+
/// If the string value is omitted, it is synthesized
43+
init(intValue: Int, stringValue: String? = nil) {
44+
let components = Self.components(of: intValue)
45+
46+
self.intValue = intValue
47+
if let stringValue = stringValue {
48+
assert(stringValue.hasPrefix("\(components.major).\(components.minor).\(components.patch)"), "SQLite version string '\(stringValue)' must match numeric version '\(intValue)'")
49+
self.stringValue = stringValue
50+
} else {
51+
self.stringValue = "\(components.major).\(components.major).\(components.patch)"
52+
}
53+
}
54+
55+
/// The major version number. This is likely to be 3 for a long time to come yet.
56+
var majorVersion: Int { Self.components(of: self.intValue).major }
57+
58+
/// The minor version number.
59+
var minorVersion: Int { Self.components(of: self.intValue).minor }
60+
61+
/// The patch version number.
62+
var patchVersion: Int { Self.components(of: self.intValue).patch }
63+
64+
/// See ``SQLDatabaseReportedVersion/isEqual(to:)``.
65+
func isEqual(to otherVersion: SQLDatabaseReportedVersion) -> Bool {
66+
(otherVersion as? _SQLiteDatabaseVersion).map { $0.intValue == self.intValue } ?? false
67+
}
68+
69+
/// See ``SQLDatabaseReportedVersion/isOlder(than:)``.
70+
func isOlder(than otherVersion: SQLDatabaseReportedVersion) -> Bool {
71+
(otherVersion as? _SQLiteDatabaseVersion).map {
72+
(self.majorVersion < $0.majorVersion ? true :
73+
(self.majorVersion > $0.majorVersion ? false :
74+
(self.minorVersion < $0.minorVersion ? true :
75+
(self.minorVersion > $0.minorVersion ? false :
76+
(self.patchVersion < $0.patchVersion ? true : false)))))
77+
} ?? false
78+
}
79+
}
80+
781
private struct _SQLiteSQLDatabase: SQLDatabase {
882
let database: SQLiteDatabase
983

1084
var eventLoop: EventLoop {
11-
return self.database.eventLoop
85+
self.database.eventLoop
86+
}
87+
88+
var version: SQLDatabaseReportedVersion? {
89+
_SQLiteDatabaseVersion.runtimeVersion
1290
}
1391

1492
var logger: Logger {
15-
return self.database.logger
93+
self.database.logger
1694
}
1795

1896
var dialect: SQLDialect {

Sources/SQLiteKit/SQLiteDialect.swift

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,42 @@
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".
19
public struct SQLiteDialect: SQLDialect {
2-
public var name: String {
3-
"sqlite"
4-
}
10+
public var name: String { "sqlite" }
511

6-
public var identifierQuote: SQLExpression {
7-
return SQLRaw("\"")
8-
}
9-
10-
public var literalStringQuote: SQLExpression {
11-
return SQLRaw("'")
12-
}
13-
14-
public var autoIncrementClause: SQLExpression {
15-
return SQLRaw("AUTOINCREMENT")
16-
}
17-
18-
public func bindPlaceholder(at position: Int) -> SQLExpression {
19-
return SQLRaw("?")
20-
}
21-
22-
public func literalBoolean(_ value: Bool) -> SQLExpression {
23-
switch value {
24-
case true: return SQLRaw("TRUE")
25-
case false: return SQLRaw("FALSE")
12+
public var identifierQuote: SQLExpression { SQLRaw("\"") }
13+
public var literalStringQuote: SQLExpression { SQLRaw("'") }
14+
public func bindPlaceholder(at position: Int) -> SQLExpression { SQLRaw("?\(position)") }
15+
public func literalBoolean(_ value: Bool) -> SQLExpression { SQLRaw(value ? "TRUE" : "FALSE") }
16+
public var literalDefault: SQLExpression { SQLLiteral.null }
17+
18+
public var supportsAutoIncrement: Bool { false }
19+
public var autoIncrementClause: SQLExpression { SQLRaw("AUTOINCREMENT") }
20+
21+
public var enumSyntax: SQLEnumSyntax { .unsupported }
22+
public var triggerSyntax: SQLTriggerSyntax { .init(create: [.supportsBody, .supportsCondition]) }
23+
public var alterTableSyntax: SQLAlterTableSyntax { .init(allowsBatch: false) }
24+
public var upsertSyntax: SQLUpsertSyntax { self.isAtLeastVersion(3, 24, 0) ? .standard : .unsupported } // `UPSERT` was added to SQLite in 3.24.0.
25+
public var supportsReturning: Bool { self.isAtLeastVersion(3, 35, 0) } // `RETURNING` was added to SQLite in 3.35.0.
26+
public var unionFeatures: SQLUnionFeatures { [.union, .unionAll, .intersect, .except] }
27+
28+
public func customDataType(for dataType: SQLDataType) -> SQLExpression? {
29+
if case .bigint = dataType {
30+
// Translate requests for bigint to requests for SQLite's plain integer type. This yields the autoincrement
31+
// primary key behavior when a 64-bit integer is requested from a higher layer.
32+
return SQLDataType.int
2633
}
34+
return nil
2735
}
2836

29-
public var literalDefault: SQLExpression {
30-
return SQLLiteral.null
31-
}
32-
33-
public var enumSyntax: SQLEnumSyntax {
34-
.unsupported
35-
}
36-
37-
public var supportsAutoIncrement: Bool {
38-
false
39-
}
40-
41-
public var alterTableSyntax: SQLAlterTableSyntax {
42-
.init(
43-
alterColumnDefinitionClause: nil,
44-
alterColumnDefinitionTypeKeyword: nil,
45-
allowsBatch: false
46-
)
47-
}
48-
49-
public var triggerSyntax: SQLTriggerSyntax {
50-
return .init(create: [.supportsBody, .supportsCondition])
51-
}
37+
public init() { }
5238

53-
public var unionFeatures: SQLUnionFeatures {
54-
[.union, .unionAll, .intersect, .except]
39+
private func isAtLeastVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
40+
_SQLiteDatabaseVersion.runtimeVersion.isNotOlder(than: _SQLiteDatabaseVersion(major: major, minor: minor, patch: patch))
5541
}
56-
57-
public init() { }
5842
}

Tests/SQLiteKitTests/SQLiteKitTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ class SQLiteKitTests: XCTestCase {
117117
threadPool: self.threadPool
118118
)
119119

120-
let a1 = try a.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.next()).wait()
120+
let a1 = try a.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).wait()
121121
defer { try! a1.close().wait() }
122-
let a2 = try a.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.next()).wait()
122+
let a2 = try a.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).wait()
123123
defer { try! a2.close().wait() }
124-
let b1 = try b.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.next()).wait()
124+
let b1 = try b.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).wait()
125125
defer { try! b1.close().wait() }
126-
let b2 = try b.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.next()).wait()
126+
let b2 = try b.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).wait()
127127
defer { try! b2.close().wait() }
128128

129129
_ = try a1.query("CREATE TABLE foo (bar INTEGER)").wait()
@@ -169,7 +169,7 @@ class SQLiteKitTests: XCTestCase {
169169
self.connection = try! SQLiteConnectionSource(
170170
configuration: .init(storage: .memory, enableForeignKeys: true),
171171
threadPool: self.threadPool
172-
).makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.next()).wait()
172+
).makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).wait()
173173
}
174174

175175
override func tearDown() {

0 commit comments

Comments
 (0)