diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..119ccc65 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [stephencelis, jberkel, NathanFallet] diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..f9622b9b --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,20 @@ +> Issues are used to track bugs and feature requests. +> Need help or have a general question? Ask on Stack Overflow (tag sqlite.swift). + +## Build Information + +- Include the SQLite.swift version, commit or branch experiencing the issue. +- Mention Xcode and OS X versions affected. +- How do do you integrate SQLite.swift in your project? + - manual + - CocoaPods + - Carthage + - Swift Package manager + +## General guidelines + +- Be as descriptive as possible. +- Provide as much information needed to _reliably reproduce_ the issue. +- Attach screenshots if possible. +- Better yet: attach GIFs or link to video. +- Even better: link to a sample project exhibiting the issue. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..b74ffd8d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +Thanks for taking the time to submit a pull request. + +Before submitting, please do the following: + +- Run `make lint` to check if there are any format errors (install [swiftlint](https://github.com/realm/SwiftLint#installation) first) +- Run `swift test` to see if the tests pass. +- Write new tests for new functionality. +- Update documentation comments where applicable. + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3c6970f5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,84 @@ +name: Build and test +on: [push, pull_request] +env: + IOS_SIMULATOR: "iPhone 16" + IOS_VERSION: "18.4" +jobs: + build: + runs-on: macos-15 + steps: + - uses: actions/checkout@v2 + - name: "Lint" + run: make lint + - name: "Run tests (PACKAGE_MANAGER_COMMAND: test)" + env: + PACKAGE_MANAGER_COMMAND: test -Xswiftc -warnings-as-errors + run: ./run-tests.sh + - name: "Run tests (SPM integration test)" + env: + SPM: run + run: ./run-tests.sh + - name: "Run tests (BUILD_SCHEME: SQLite iOS)" + env: + BUILD_SCHEME: SQLite iOS + run: ./run-tests.sh + - name: "Run tests (BUILD_SCHEME: SQLite Mac)" + env: + BUILD_SCHEME: SQLite Mac + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: none)" + env: + VALIDATOR_SUBSPEC: none + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: standard)" + env: + VALIDATOR_SUBSPEC: standard + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: standalone)" + env: + VALIDATOR_SUBSPEC: standalone + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: SQLCipher)" + env: + VALIDATOR_SUBSPEC: SQLCipher + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: iOS)" + env: + CARTHAGE_PLATFORM: iOS + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: Mac)" + env: + CARTHAGE_PLATFORM: Mac + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: watchOS)" + env: + CARTHAGE_PLATFORM: watchOS + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: tvOS)" + env: + CARTHAGE_PLATFORM: tvOS + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: visionOS)" + env: + CARTHAGE_PLATFORM: visionOS + run: ./run-tests.sh + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: | + sudo apt-get update -qq + sudo apt-get install -y libsqlite3-dev + - name: Test + run: swift test + - name: "Run tests (SPM integration test)" + env: + SPM: run + run: ./run-tests.sh + build-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run tests + uses: skiptools/swift-android-action@v2 diff --git a/.gitignore b/.gitignore index 99e4f5e2..e7b2ad4d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,12 @@ DerivedData *.xcuserstate # Carthage -Carthage +/Carthage/ + +# Makefile +bin/ + +# Swift Package Manager +.build +Packages/ +.swiftpm/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29b..00000000 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..1bb21cd5 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,47 @@ +opt_in_rules: + - shorthand_optional_binding +disabled_rules: # rule identifiers to exclude from running + - todo + - operator_whitespace + - large_tuple + - closure_parameter_position + - inclusive_language # sqlite_master etc. + - blanket_disable_command +included: # paths to include during linting. `--path` is ignored if present. takes precendence over `excluded`. + - Sources + - Tests +excluded: # paths to ignore during linting. overridden by `included`. + +identifier_name: + excluded: + - db + - in + - to + - by + - or + - eq + - gt + - lt + - fn + - a + - b + - q + - SQLITE_TRANSIENT + +type_body_length: + warning: 350 + error: 350 + +function_body_length: + warning: 60 + error: 60 + +line_length: + warning: 150 + error: 150 + ignores_comments: true + +file_length: + warning: 500 + error: 500 + ignore_comment_only_lines: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b33f9af3..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: objective-c -matrix: - include: - - env: BUILD_SCHEME="SQLite iOS" - - env: BUILD_SCHEME="SQLite Mac" - - env: VALIDATOR_SUBSPEC="none" - - env: VALIDATOR_SUBSPEC="standard" - - env: VALIDATOR_SUBSPEC="standalone" -before_install: - - gem install xcpretty --no-document -script: - - ./run-tests.sh -osx_image: xcode7.3 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b77a8b07 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,274 @@ +0.15.4 (13-06-2025), [diff][diff-0.15.4] +======================================== +* Fix cross compilation for linux on macOS fails ([#1317][]) +* Support creating tables in schema changer ([#1315][]) +* Update oldest supported platform versions ([#1280][]) +* Add CustomStringConvertible for Setter ([#1279][]) + +0.15.3 (19-04-2024), [diff][diff-0.15.3] +======================================== +* Update `podspec` to include privacy manifest ([#1265][]) + +0.15.2 (16-04-2024), [diff][diff-0.15.2] +======================================== +* fix: visionos to cocoapods ([#1260][]) + +0.15.1 (14-04-2024), [diff][diff-0.15.1] +======================================== + +* Update CoreFunctions.swift fix typo ([#1249][]) +* Fix #1247 support nil case when decoding optionals ([#1248][]) +* Change deployment targets for Xcode 15 and add dependency on custom Cocoapods fork ([#1255][]) +* Add VisionOS support ([#1237][]) + +0.15.0 (24-02-2024), [diff][diff-0.15.0] +======================================== + +* Fix incorrect behavior when preparing `SELECT *` preceded by a `WITH` ([#1179][]) +* Adds support for returning extended error codes ([#1178][]) +* Fix typos ([#1182][]) +* fix Xcode build error ([#1192][]) +* Make the IndexDefinition properties public ([#1196][]) +* Fix GitHub Actions build badge ([#1200][]) +* Run CI on macOS 13 ([#1206][]) +* SchemaReader: return the correct column definition for a composite primary key ([#1217][]) +* Add optional support for decoding ([#1224][]) +* make fromDatatypeValue throw ([#1242][]) +* Implements built-in window functions ([#1228][]) +* Fix column affinity parsing to match how SQLite determines affinity ([#1218][]) +* Handle FK definitions w/o key references ([#1210][]) +* Add privacy manifest ([#1245][]) +* New minimum deployment targets: iOS/tvOS 11.0, watchOS 4.0 + +0.14.1 (01-11-2022), [diff][diff-0.14.1] +======================================== + +* Reverted `Blob` changes (See [#1167][] for rationale). + +0.14.0 (27-10-2022), [diff][diff-0.14.0] +======================================== +For breaking changes, see [Upgrading.md](Documentation/Upgrading.md). + +* Support more complex schema changes and queries ([#1073][], [#1146][] [#1148][]) +* Support `ATTACH`/`DETACH` ([#30][], [#1142][]) +* Expose connection flags (via `URIQueryParameter`) to open db ([#1074][]) +* Support `WITH` clause ([#1139][]) +* Add `Value` conformance for `NSURL` ([#1110][], [#1141][]) +* Add decoding for `UUID` ([#1137][]) +* SQLCipher: improve documentation ([#1098][]), add `sqlcipher_export` ([#1101][]) +* Fix `insertMany([Encodable])` ([#1130][], [#1138][]) +* Fix incorrect spelling of `remove_diacritics` ([#1128][]) +* Fix project build order ([#1131][]) +* Blob performance improvements ([#416][], [#1167][]) +* Various performance improvements ([#1109][], [#1115][], [#1132][]) +* Removed FTS3/4 tokenizer integration (`registerTokenizer`, [#1104][], [#1144][]) + +0.13.3 (27-03-2022), [diff][diff-0.13.3] +======================================== + +* UUID Fix ([#1112][]) +* Add prepareRowIterator method to an extension of Statement. ([#1119][]) +* Adding primary key support to column with references ([#1121][]) + +0.13.2 (25-01-2022), [diff][diff-0.13.2] +======================================== + +* Closing bracket position ([#1100][]) +* Native user_version support in Connection ([#1105][]) + +0.13.1 (17-11-2021), [diff][diff-0.13.1] +======================================== + +* Support for database backup ([#919][]) +* Support for custom SQL aggregates ([#881][]) +* Restore previous behavior in `FailableIterator` ([#1075][]) +* Fix compilation on Linux ([#1077][]) +* Align platform versions in SPM manifest and Xcode ([#1094][]) +* Revert OSX deployment target back to 10.10 ([#1095][]) + +0.13.0 (22-08-2021), [diff][diff-0.13.0] +======================================== + +* Swift 5.3 support +* Xcode 12.5 support +* Bumps minimum deployment versions +* Fixes up Package.swift to build SQLiteObjc module + +0.12.1, 0.12.2 (21-06-2019) [diff][diff-0.12.2] +======================================== + +* CocoaPods modular headers support + +0.12.0 (24-04-2019) [diff][diff-0.12.0] +======================================== + +* Version with Swift 5 Support + +0.11.6 (19-04-2019), [diff][diff-0.11.6] +======================================== + +* Swift 4.2, SQLCipher 4.x ([#866][]) + +0.11.5 (04-14-2018), [diff][diff-0.11.5] +======================================== + +* Swift 4.1 ([#797][]) + +0.11.4 (30-09-2017), [diff][diff-0.11.4] +======================================== + +* Collate `.nocase` strictly enforces `NOT NULL` even when using Optional ([#697][]) +* Fix transactions not being rolled back when committing fails ([#426][]) +* Add possibility to have expression on right hand side of like ([#591][]) +* Added Date and Time functions ([#142][]) +* Add Swift4 Coding support ([#733][]) +* Preliminary Linux support ([#315][], [#681][]) +* Add `RowIterator` for more safety ([#647][], [#726][]) +* Make `Row.get` throw instead of crash ([#649][]) +* Fix create/drop index functions ([#666][]) +* Revert deployment target to 8.0 ([#625][], [#671][], [#717][]) +* Added support for the union query clause ([#723][]) +* Add support for `ORDER` and `LIMIT` on `UPDATE` and `DELETE` ([#657][], [#722][]) +* Swift 4 support ([#668][]) + +0.11.3 (30-03-2017), [diff][diff-0.11.3] +======================================== + +* Fix compilation problems when using Carthage ([#615][]) +* Add `WITHOUT ROWID` table option ([#541][]) +* Argument count fixed for binary custom functions ([#481][]) +* Documentation updates +* Tested with Xcode 8.3 / iOS 10.3 + +0.11.2 (25-12-2016), [diff][diff-0.11.2] +======================================== + +* Fixed SQLCipher integration with read-only databases ([#559][]) +* Preliminary Swift Package Manager support ([#548][], [#560][]) +* Fixed null pointer when fetching an empty BLOB ([#561][]) +* Allow `where` as alias for `filter` ([#571][]) + +0.11.1 (06-12-2016), [diff][diff-0.11.1] +======================================== + +* Integrate SQLCipher via CocoaPods ([#546][], [#553][]) +* Made lastInsertRowid consistent with other SQLite wrappers ([#532][]) +* Fix for `~=` operator used with Double ranges +* Various documentation updates + +0.11.0 (19-10-2016) +=================== + +* Swift3 migration ([diff][diff-0.11.0]) + + +[diff-0.11.0]: https://github.com/stephencelis/SQLite.swift/compare/0.10.1...0.11.0 +[diff-0.11.1]: https://github.com/stephencelis/SQLite.swift/compare/0.11.0...0.11.1 +[diff-0.11.2]: https://github.com/stephencelis/SQLite.swift/compare/0.11.1...0.11.2 +[diff-0.11.3]: https://github.com/stephencelis/SQLite.swift/compare/0.11.2...0.11.3 +[diff-0.11.4]: https://github.com/stephencelis/SQLite.swift/compare/0.11.3...0.11.4 +[diff-0.11.5]: https://github.com/stephencelis/SQLite.swift/compare/0.11.4...0.11.5 +[diff-0.11.6]: https://github.com/stephencelis/SQLite.swift/compare/0.11.5...0.11.6 +[diff-0.12.0]: https://github.com/stephencelis/SQLite.swift/compare/0.11.6...0.12.0 +[diff-0.12.2]: https://github.com/stephencelis/SQLite.swift/compare/0.12.0...0.12.2 +[diff-0.13.0]: https://github.com/stephencelis/SQLite.swift/compare/0.12.2...0.13.0 +[diff-0.13.1]: https://github.com/stephencelis/SQLite.swift/compare/0.13.0...0.13.1 +[diff-0.13.2]: https://github.com/stephencelis/SQLite.swift/compare/0.13.1...0.13.2 +[diff-0.13.3]: https://github.com/stephencelis/SQLite.swift/compare/0.13.2...0.13.3 +[diff-0.14.0]: https://github.com/stephencelis/SQLite.swift/compare/0.13.3...0.14.0 +[diff-0.14.1]: https://github.com/stephencelis/SQLite.swift/compare/0.14.0...0.14.1 +[diff-0.15.0]: https://github.com/stephencelis/SQLite.swift/compare/0.14.0...0.15.0 +[diff-0.15.1]: https://github.com/stephencelis/SQLite.swift/compare/0.15.0...0.15.1 +[diff-0.15.2]: https://github.com/stephencelis/SQLite.swift/compare/0.15.1...0.15.2 +[diff-0.15.3]: https://github.com/stephencelis/SQLite.swift/compare/0.15.2...0.15.3 +[diff-0.15.4]: https://github.com/stephencelis/SQLite.swift/compare/0.15.3...0.15.4 + +[#30]: https://github.com/stephencelis/SQLite.swift/issues/30 +[#142]: https://github.com/stephencelis/SQLite.swift/issues/142 +[#315]: https://github.com/stephencelis/SQLite.swift/issues/315 +[#416]: https://github.com/stephencelis/SQLite.swift/pull/416 +[#426]: https://github.com/stephencelis/SQLite.swift/pull/426 +[#481]: https://github.com/stephencelis/SQLite.swift/pull/481 +[#532]: https://github.com/stephencelis/SQLite.swift/issues/532 +[#541]: https://github.com/stephencelis/SQLite.swift/issues/541 +[#546]: https://github.com/stephencelis/SQLite.swift/issues/546 +[#548]: https://github.com/stephencelis/SQLite.swift/pull/548 +[#553]: https://github.com/stephencelis/SQLite.swift/pull/553 +[#559]: https://github.com/stephencelis/SQLite.swift/pull/559 +[#560]: https://github.com/stephencelis/SQLite.swift/pull/560 +[#561]: https://github.com/stephencelis/SQLite.swift/issues/561 +[#571]: https://github.com/stephencelis/SQLite.swift/issues/571 +[#591]: https://github.com/stephencelis/SQLite.swift/pull/591 +[#615]: https://github.com/stephencelis/SQLite.swift/pull/615 +[#625]: https://github.com/stephencelis/SQLite.swift/issues/625 +[#647]: https://github.com/stephencelis/SQLite.swift/pull/647 +[#649]: https://github.com/stephencelis/SQLite.swift/pull/649 +[#657]: https://github.com/stephencelis/SQLite.swift/issues/657 +[#666]: https://github.com/stephencelis/SQLite.swift/pull/666 +[#668]: https://github.com/stephencelis/SQLite.swift/pull/668 +[#671]: https://github.com/stephencelis/SQLite.swift/issues/671 +[#681]: https://github.com/stephencelis/SQLite.swift/issues/681 +[#697]: https://github.com/stephencelis/SQLite.swift/issues/697 +[#717]: https://github.com/stephencelis/SQLite.swift/issues/717 +[#722]: https://github.com/stephencelis/SQLite.swift/pull/722 +[#723]: https://github.com/stephencelis/SQLite.swift/pull/723 +[#733]: https://github.com/stephencelis/SQLite.swift/pull/733 +[#726]: https://github.com/stephencelis/SQLite.swift/pull/726 +[#797]: https://github.com/stephencelis/SQLite.swift/pull/797 +[#866]: https://github.com/stephencelis/SQLite.swift/pull/866 +[#881]: https://github.com/stephencelis/SQLite.swift/pull/881 +[#919]: https://github.com/stephencelis/SQLite.swift/pull/919 +[#1073]: https://github.com/stephencelis/SQLite.swift/issues/1073 +[#1074]: https://github.com/stephencelis/SQLite.swift/issues/1074 +[#1075]: https://github.com/stephencelis/SQLite.swift/pull/1075 +[#1077]: https://github.com/stephencelis/SQLite.swift/issues/1077 +[#1094]: https://github.com/stephencelis/SQLite.swift/pull/1094 +[#1095]: https://github.com/stephencelis/SQLite.swift/pull/1095 +[#1098]: https://github.com/stephencelis/SQLite.swift/issues/1098 +[#1100]: https://github.com/stephencelis/SQLite.swift/pull/1100 +[#1101]: https://github.com/stephencelis/SQLite.swift/issues/1101 +[#1104]: https://github.com/stephencelis/SQLite.swift/issues/1104 +[#1105]: https://github.com/stephencelis/SQLite.swift/pull/1105 +[#1109]: https://github.com/stephencelis/SQLite.swift/issues/1109 +[#1110]: https://github.com/stephencelis/SQLite.swift/pull/1110 +[#1112]: https://github.com/stephencelis/SQLite.swift/pull/1112 +[#1115]: https://github.com/stephencelis/SQLite.swift/pull/1115 +[#1119]: https://github.com/stephencelis/SQLite.swift/pull/1119 +[#1121]: https://github.com/stephencelis/SQLite.swift/pull/1121 +[#1128]: https://github.com/stephencelis/SQLite.swift/issues/1128 +[#1130]: https://github.com/stephencelis/SQLite.swift/issues/1130 +[#1131]: https://github.com/stephencelis/SQLite.swift/pull/1131 +[#1132]: https://github.com/stephencelis/SQLite.swift/pull/1132 +[#1137]: https://github.com/stephencelis/SQLite.swift/pull/1137 +[#1138]: https://github.com/stephencelis/SQLite.swift/pull/1138 +[#1139]: https://github.com/stephencelis/SQLite.swift/pull/1139 +[#1141]: https://github.com/stephencelis/SQLite.swift/pull/1141 +[#1142]: https://github.com/stephencelis/SQLite.swift/pull/1142 +[#1144]: https://github.com/stephencelis/SQLite.swift/pull/1144 +[#1146]: https://github.com/stephencelis/SQLite.swift/pull/1146 +[#1148]: https://github.com/stephencelis/SQLite.swift/pull/1148 +[#1167]: https://github.com/stephencelis/SQLite.swift/pull/1167 +[#1179]: https://github.com/stephencelis/SQLite.swift/pull/1179 +[#1178]: https://github.com/stephencelis/SQLite.swift/pull/1178 +[#1182]: https://github.com/stephencelis/SQLite.swift/pull/1182 +[#1192]: https://github.com/stephencelis/SQLite.swift/pull/1192 +[#1196]: https://github.com/stephencelis/SQLite.swift/pull/1196 +[#1200]: https://github.com/stephencelis/SQLite.swift/pull/1200 +[#1206]: https://github.com/stephencelis/SQLite.swift/pull/1206 +[#1217]: https://github.com/stephencelis/SQLite.swift/pull/1217 +[#1224]: https://github.com/stephencelis/SQLite.swift/pull/1224 +[#1242]: https://github.com/stephencelis/SQLite.swift/pull/1242 +[#1228]: https://github.com/stephencelis/SQLite.swift/pull/1228 +[#1218]: https://github.com/stephencelis/SQLite.swift/pull/1218 +[#1210]: https://github.com/stephencelis/SQLite.swift/pull/1210 +[#1245]: https://github.com/stephencelis/SQLite.swift/pull/1245 +[#1249]: https://github.com/stephencelis/SQLite.swift/pull/1249 +[#1248]: https://github.com/stephencelis/SQLite.swift/pull/1248 +[#1255]: https://github.com/stephencelis/SQLite.swift/pull/1255 +[#1237]: https://github.com/stephencelis/SQLite.swift/pull/1237 +[#1260]: https://github.com/stephencelis/SQLite.swift/pull/1260 +[#1265]: https://github.com/stephencelis/SQLite.swift/pull/1265 +[#1279]: https://github.com/stephencelis/SQLite.swift/pull/1279 +[#1280]: https://github.com/stephencelis/SQLite.swift/pull/1280 +[#1315]: https://github.com/stephencelis/SQLite.swift/pull/1315 +[#1317]: https://github.com/stephencelis/SQLite.swift/pull/1317 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60c18370..6969a705 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ addresses everything. If it doesn’t, continue the conversation there. If your searches return empty, see the [bug](#bugs) or [feature request](#feature-requests) guidelines below. -[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Ask on Stack Overflow]: https://stackoverflow.com/questions/tagged/sqlite.swift [Search]: https://github.com/stephencelis/SQLite.swift/search?type=Issues @@ -74,7 +74,7 @@ Made it through everything above and still having trouble? Sorry! - Even better: link to a sample project exhibiting the issue. - Include the SQLite.swift commit or branch experiencing the issue. - Include devices and operating systems affected. - - Include build information: the Xcode and OS X versions affected. + - Include build information: the Xcode and macOS versions affected. [installation instructions]: Documentation/Index.md#installation [See Documentation]: Documentation/Index.md#sqliteswift-documentation diff --git a/CocoaPods/appletvos/module.modulemap b/CocoaPods/appletvos/module.modulemap deleted file mode 100644 index 637d9935..00000000 --- a/CocoaPods/appletvos/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/appletvsimulator/module.modulemap b/CocoaPods/appletvsimulator/module.modulemap deleted file mode 100644 index f8b9b671..00000000 --- a/CocoaPods/appletvsimulator/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/iphoneos/module.modulemap b/CocoaPods/iphoneos/module.modulemap deleted file mode 100644 index 043db6c4..00000000 --- a/CocoaPods/iphoneos/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/iphonesimulator/module.modulemap b/CocoaPods/iphonesimulator/module.modulemap deleted file mode 100644 index a7b14cbb..00000000 --- a/CocoaPods/iphonesimulator/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/macosx/module.modulemap b/CocoaPods/macosx/module.modulemap deleted file mode 100644 index 9e091297..00000000 --- a/CocoaPods/macosx/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/watchos/module.modulemap b/CocoaPods/watchos/module.modulemap deleted file mode 100644 index 62a6c4ee..00000000 --- a/CocoaPods/watchos/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/watchsimulator/module.modulemap b/CocoaPods/watchsimulator/module.modulemap deleted file mode 100644 index 086fbab2..00000000 --- a/CocoaPods/watchsimulator/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPodsTests/.gitignore b/CocoaPodsTests/.gitignore deleted file mode 100644 index 4cf24de2..00000000 --- a/CocoaPodsTests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -gems/ diff --git a/CocoaPodsTests/Gemfile b/CocoaPodsTests/Gemfile deleted file mode 100644 index 0a6af5c8..00000000 --- a/CocoaPodsTests/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source 'https://rubygems.org' - -gem 'cocoapods' -gem 'minitest' diff --git a/CocoaPodsTests/Gemfile.lock b/CocoaPodsTests/Gemfile.lock deleted file mode 100644 index 7173e2c4..00000000 --- a/CocoaPodsTests/Gemfile.lock +++ /dev/null @@ -1,65 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (4.2.6) - i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - claide (1.0.0) - cocoapods (1.0.0) - activesupport (>= 4.0.2) - claide (>= 1.0.0, < 2.0) - cocoapods-core (= 1.0.0) - cocoapods-deintegrate (>= 1.0.0, < 2.0) - cocoapods-downloader (>= 1.0.0, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.0.0, < 2.0) - cocoapods-try (>= 1.0.0, < 2.0) - colored (~> 1.2) - escape (~> 0.0.4) - fourflusher (~> 0.3.0) - molinillo (~> 0.4.5) - nap (~> 1.0) - xcodeproj (>= 1.0.0, < 2.0) - cocoapods-core (1.0.0) - activesupport (>= 4.0.2) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - cocoapods-deintegrate (1.0.0) - cocoapods-downloader (1.0.0) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.0) - cocoapods-stats (1.0.0) - cocoapods-trunk (1.0.0) - nap (>= 0.8, < 2.0) - netrc (= 0.7.8) - cocoapods-try (1.0.0) - colored (1.2) - escape (0.0.4) - fourflusher (0.3.0) - fuzzy_match (2.0.4) - i18n (0.7.0) - json (1.8.3) - minitest (5.8.4) - molinillo (0.4.5) - nap (1.1.0) - netrc (0.7.8) - thread_safe (0.3.5) - tzinfo (1.2.2) - thread_safe (~> 0.1) - xcodeproj (1.0.0) - activesupport (>= 3) - claide (>= 1.0.0, < 2.0) - colored (~> 1.2) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods - minitest diff --git a/CocoaPodsTests/Makefile b/CocoaPodsTests/Makefile deleted file mode 100644 index c9a3182e..00000000 --- a/CocoaPodsTests/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -test: install - @set -e; \ - for test in *_test.rb; do \ - bundle exec ./$$test; \ - done - -install: - @bundle install --path gems - -.PHONY: test install diff --git a/CocoaPodsTests/integration_test.rb b/CocoaPodsTests/integration_test.rb deleted file mode 100755 index 429a9c77..00000000 --- a/CocoaPodsTests/integration_test.rb +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env ruby - -require 'minitest/autorun' -require_relative 'test_running_validator' - -class IntegrationTest < Minitest::Test - - def test_validate_project - assert validator.validate, "validation failed: #{validator.failure_reason}" - end - - private - - def validator - @validator ||= TestRunningValidator.new(podspec, []).tap do |validator| - subspec = ENV["VALIDATOR_SUBSPEC"] - validator.test_files = Dir["#{project_test_dir}/*.swift"] - validator.config.verbose = true - validator.no_clean = true - validator.use_frameworks = true - validator.fail_fast = true - validator.local = true - validator.allow_warnings = true - if subspec == "none" - validator.no_subspecs = true - else - validator.only_subspec = subspec - end - end - end - - def podspec - File.expand_path(File.dirname(__FILE__) + '/../SQLite.swift.podspec') - end - - def project_test_dir - File.expand_path(File.dirname(__FILE__) + '/../SQLiteTests') - end -end diff --git a/CocoaPodsTests/test_running_validator.rb b/CocoaPodsTests/test_running_validator.rb deleted file mode 100644 index 10f4db61..00000000 --- a/CocoaPodsTests/test_running_validator.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'cocoapods' -require 'cocoapods/validator' -require 'fileutils' - -class TestRunningValidator < Pod::Validator - APP_TARGET = 'App' - TEST_TARGET = 'Tests' - - attr_accessor :test_files - - def create_app_project - super.tap do - project = Xcodeproj::Project.open(validation_dir + "#{APP_TARGET}.xcodeproj") - create_test_target(project) - end - end - - def install_pod - super.tap do - if local? - FileUtils.ln_s file.dirname, validation_dir + "Pods/#{spec.name}" - end - end - end - - def podfile_from_spec(*args) - super(*args).tap do |pod_file| - add_test_target(pod_file) - end - end - - def build_pod - super - Pod::UI.message "\Testing with xcodebuild.\n".yellow do - run_tests - end - end - - private - def create_test_target(project) - test_target = project.new_target(:unit_test_bundle, TEST_TARGET, consumer.platform_name, deployment_target) - group = project.new_group(TEST_TARGET) - test_target.add_file_references(test_files.map { |file| group.new_file(file) }) - project.save - create_test_scheme(project, test_target) - project - end - - def create_test_scheme(project, test_target) - project.recreate_user_schemes - test_scheme = Xcodeproj::XCScheme.new(test_scheme_path(project)) - test_scheme.add_test_target(test_target) - test_scheme.save! - end - - def test_scheme_path(project) - Xcodeproj::XCScheme.user_data_dir(project.path) + "#{TEST_TARGET}.xcscheme" - end - - def add_test_target(pod_file) - app_target = pod_file.target_definitions[APP_TARGET] - Pod::Podfile::TargetDefinition.new(TEST_TARGET, app_target) - end - - def run_tests - command = %W(clean test -workspace #{APP_TARGET}.xcworkspace -scheme #{TEST_TARGET} -configuration Debug) - case consumer.platform_name - when :ios - command += %w(CODE_SIGN_IDENTITY=- -sdk iphonesimulator) - command += Fourflusher::SimControl.new.destination('iPhone 4s', deployment_target) - when :osx - command += %w(LD_RUNPATH_SEARCH_PATHS=@loader_path/../Frameworks) - when :tvos - command += %w(CODE_SIGN_IDENTITY=- -sdk appletvsimulator) - command += Fourflusher::SimControl.new.destination('Apple TV 1080p', deployment_target) - else - return # skip watchos - end - - output, status = Dir.chdir(validation_dir) { _xcodebuild(command) } - unless status.success? - message = 'Returned an unsuccessful exit code.' - if config.verbose? - message += "\nXcode output: \n#{output}\n" - else - message += ' You can use `--verbose` for more information.' - end - error('xcodebuild', message) - end - end -end diff --git a/Documentation/Index.md b/Documentation/Index.md index 0579e6f3..0e8261f2 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -1,28 +1,37 @@ # SQLite.swift Documentation +- [SQLite.swift Documentation](#sqliteswift-documentation) - [Installation](#installation) + - [Swift Package Manager](#swift-package-manager) - [Carthage](#carthage) - [CocoaPods](#cocoapods) + - [Requiring a specific version of SQLite](#requiring-a-specific-version-of-sqlite) + - [Using SQLite.swift with SQLCipher](#using-sqliteswift-with-sqlcipher) - [Manual](#manual) - - [Frameworkless Targets](#frameworkless-targets) - [Getting Started](#getting-started) - [Connecting to a Database](#connecting-to-a-database) - [Read-Write Databases](#read-write-databases) - [Read-Only Databases](#read-only-databases) + - [In a shared group container](#in-a-shared-group-container) - [In-Memory Databases](#in-memory-databases) + - [URI parameters](#uri-parameters) - [Thread-Safety](#thread-safety) - [Building Type-Safe SQL](#building-type-safe-sql) - [Expressions](#expressions) - - [Compound Expressions](#compound-expressions) + - [Compound Expressions](#compound-expressions) - [Queries](#queries) - [Creating a Table](#creating-a-table) - [Create Table Options](#create-table-options) - [Column Constraints](#column-constraints) - [Table Constraints](#table-constraints) - [Inserting Rows](#inserting-rows) + - [Handling SQLite errors](#handling-sqlite-errors) - [Setters](#setters) + - [Infix Setters](#infix-setters) + - [Postfix Setters](#postfix-setters) - [Selecting Rows](#selecting-rows) - [Iterating and Accessing Values](#iterating-and-accessing-values) + - [Failable iteration](#failable-iteration) - [Plucking Rows](#plucking-rows) - [Building Complex Queries](#building-complex-queries) - [Selecting Columns](#selecting-columns) @@ -31,54 +40,97 @@ - [Table Aliasing](#table-aliasing) - [Filtering Rows](#filtering-rows) - [Filter Operators and Functions](#filter-operators-and-functions) + - [Infix Filter Operators](#infix-filter-operators) + - [Prefix Filter Operators](#prefix-filter-operators) + - [Filtering Functions](#filtering-functions) - [Sorting Rows](#sorting-rows) - [Limiting and Paging Results](#limiting-and-paging-results) + - [Recursive and Hierarchical Queries](#recursive-and-hierarchical-queries) - [Aggregation](#aggregation) + - [Upserting Rows](#upserting-rows) - [Updating Rows](#updating-rows) - [Deleting Rows](#deleting-rows) - [Transactions and Savepoints](#transactions-and-savepoints) + - [Querying the Schema](#querying-the-schema) + - [Indexes and Columns](#indexes-and-columns) - [Altering the Schema](#altering-the-schema) - [Renaming Tables](#renaming-tables) + - [Dropping Tables](#dropping-tables) - [Adding Columns](#adding-columns) - [Added Column Constraints](#added-column-constraints) + - [SchemaChanger](#schemachanger) + - [Adding Columns](#adding-columns-1) + - [Renaming Columns](#renaming-columns) + - [Dropping Columns](#dropping-columns) + - [Renaming/Dropping Tables](#renamingdropping-tables) + - [Creating Tables](#creating-tables) - [Indexes](#indexes) - [Creating Indexes](#creating-indexes) - [Dropping Indexes](#dropping-indexes) - - [Dropping Tables](#dropping-tables) - [Migrations and Schema Versioning](#migrations-and-schema-versioning) - [Custom Types](#custom-types) - [Date-Time Values](#date-time-values) - [Binary Data](#binary-data) - - [Custom Type Caveats](#custom-type-caveats) + - [Codable Types](#codable-types) + - [Inserting Codable Types](#inserting-codable-types) + - [Updating Codable Types](#updating-codable-types) + - [Retrieving Codable Types](#retrieving-codable-types) + - [Restrictions](#restrictions) - [Other Operators](#other-operators) + - [Other Infix Operators](#other-infix-operators) + - [Other Prefix Operators](#other-prefix-operators) - [Core SQLite Functions](#core-sqlite-functions) - [Aggregate SQLite Functions](#aggregate-sqlite-functions) + - [Window SQLite Functions](#window-sqlite-functions) + - [Date and Time functions](#date-and-time-functions) - [Custom SQL Functions](#custom-sql-functions) + - [Custom Aggregations](#custom-aggregations) - [Custom Collations](#custom-collations) - [Full-text Search](#full-text-search) + - [FTS5](#fts5) - [Executing Arbitrary SQL](#executing-arbitrary-sql) + - [Online Database Backup](#online-database-backup) + - [Attaching and detaching databases](#attaching-and-detaching-databases) - [Logging](#logging) - + - [Vacuum](#vacuum) [↩]: #sqliteswift-documentation ## Installation -> _Note:_ SQLite.swift requires Swift 2 (and [Xcode 7](https://developer.apple.com/xcode/downloads/)) or greater. +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. It’s integrated with the Swift build system to automate the +process of downloading, compiling, and linking dependencies. + + 1. Add the following to your `Package.swift` file: + + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ] + ``` + 2. Build your project: + + ```sh + $ swift build + ``` + +[Swift Package Manager]: https://swift.org/package-manager ### Carthage [Carthage][] is a simple, decentralized dependency manager for Cocoa. To install SQLite.swift with Carthage: - 1. Make sure Carthage is [installed][Carthage Installation]. 2. Update your Cartfile to include the following: - ``` - github "stephencelis/SQLite.swift" ~> 0.10.1 + ```ruby + github "stephencelis/SQLite.swift" ~> 0.15.4 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. @@ -93,181 +145,277 @@ install SQLite.swift with Carthage: [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: - 1. Make sure the latest CocoaPods beta is [installed][CocoaPods Installation]. (SQLite.swift requires version 1.0.0.beta.6 or greater.) + 1. Make sure CocoaPods is [installed][CocoaPods Installation] (SQLite.swift + requires version 1.6.1 or greater). - ``` sh + ```sh # Using the default Ruby install will require you to use sudo when # installing and updating gems. - sudo gem install --pre cocoapods + [sudo] gem install cocoapods ``` 2. Update your Podfile to include the following: - ``` ruby + ```ruby use_frameworks! - pod 'SQLite.swift', '~> 0.10.1' + target 'YourAppTargetName' do + pod 'SQLite.swift', '~> 0.15.4' + end ``` - 3. Run `pod install`. + 3. Run `pod install --repo-update`. + - #### Requiring a specific version of SQLite +#### Requiring a specific version of SQLite - If you want to use a more recent version of SQLite than what is provided with the OS you can require the `standalone` subspec: +If you want to use a more recent version of SQLite than what is provided +with the OS you can require the `standalone` subspec: -``` ruby - pod 'SQLite.swift/standalone', '~> 0.10.1' +```ruby +target 'YourAppTargetName' do + pod 'SQLite.swift/standalone', '~> 0.15.4' +end ``` -By default this will use the most recent version of SQLite without any extras. If you want you can further customize this by adding another dependency to sqlite3 or one of its subspecs: +By default this will use the most recent version of SQLite without any +extras. If you want you can further customize this by adding another +dependency to sqlite3 or one of its subspecs: -``` ruby - pod 'SQLite.swift/standalone', '~> 0.10.1' - pod 'sqlite3/fts5', '= 3.11.1' # SQLite 3.11.1 with FTS5 enabled +```ruby +target 'YourAppTargetName' do + pod 'SQLite.swift/standalone', '~> 0.15.4' + pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled +end ``` See the [sqlite3 podspec][sqlite3pod] for more details. +#### Using SQLite.swift with SQLCipher + +If you want to use [SQLCipher][] with SQLite.swift you can require the +`SQLCipher` subspec in your Podfile (SPM is not supported yet, see [#1084](https://github.com/stephencelis/SQLite.swift/issues/1084)): + +```ruby +target 'YourAppTargetName' do + # Make sure you only require the subspec, otherwise you app might link against + # the system SQLite, which means the SQLCipher-specific methods won't work. + pod 'SQLite.swift/SQLCipher', '~> 0.15.4' +end +``` + +This will automatically add a dependency to the SQLCipher pod as well as +extend `Connection` with methods to change the database key: + +```swift +import SQLite + +let db = try Connection("path/to/encrypted.sqlite3") +try db.key("secret") +try db.rekey("new secret") // changes encryption key on already encrypted db +``` + +To encrypt an existing database: + +```swift +let db = try Connection("path/to/unencrypted.sqlite3") +try db.sqlcipher_export(.uri("encrypted.sqlite3"), key: "secret") +``` + [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started [sqlite3pod]: https://github.com/clemensg/sqlite3pod - +[SQLCipher]: https://www.zetetic.net/sqlcipher/ ### Manual To install SQLite.swift as an Xcode sub-project: - 1. Drag the **SQLite.xcodeproj** file into your own project. ([Submodule](http://git-scm.com/book/en/Git-Tools-Submodules), clone, or [download](https://github.com/stephencelis/SQLite.swift/archive/master.zip) the project first.) + 1. Drag the **SQLite.xcodeproj** file into your own project. + ([Submodule](http://git-scm.com/book/en/Git-Tools-Submodules), clone, or + [download](https://github.com/stephencelis/SQLite.swift/archive/master.zip) + the project first.) - ![Installation Screen Shot](Documentation/Resources/installation@2x.png) + ![Installation Screen Shot](Resources/installation@2x.png) - 2. In your target’s **General** tab, click the **+** button under **Linked Frameworks and Libraries**. + 2. In your target’s **General** tab, click the **+** button under **Linked + Frameworks and Libraries**. 3. Select the appropriate **SQLite.framework** for your platform. 4. **Add**. -You should now be able to `import SQLite` from any of your target’s source files and begin using SQLite.swift. +You should now be able to `import SQLite` from any of your target’s source +files and begin using SQLite.swift. +Some additional steps are required to install the application on an actual +device: -### Frameworkless Targets + 5. In the **General** tab, click the **+** button under **Embedded + Binaries**. -It’s possible to use SQLite.swift in a target that doesn’t support frameworks, including iOS 7 apps and OS X command line tools, though it takes a little extra work. - - 1. In your target’s **Build Phases**, add **libsqlite3.dylib** to the **Link Binary With Libraries** build phase. - - 2. Copy the SQLite.swift source files (from its **SQLite** directory) into your Xcode project. - - 3. Add the following lines to your project’s [bridging header](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html#//apple_ref/doc/uid/TP40014216-CH10-XID_79) (a file usually in the form of `$(TARGET_NAME)-Bridging-Header.h`). - - ``` swift - #import - #import "SQLite-Bridging.h" - ``` - -> _Note:_ Adding SQLite.swift source files directly to your application will both remove the `SQLite` module namespace (no need—or ability—to `import SQLite`) and expose internal functions and variables. You will need to rename anything that conflicts with code of your own. Please [report any bugs](https://github.com/stephencelis/SQLite.swift/issues/new) (_e.g._, segfaults) you encounter. + 6. Select the appropriate **SQLite.framework** for your platform. + 7. **Add**. ## Getting Started -To use SQLite.swift classes or structures in your target’s source file, first import the `SQLite` module. +To use SQLite.swift classes or structures in your target’s source file, first +import the `SQLite` module. -``` swift +```swift import SQLite ``` ### Connecting to a Database -Database connections are established using the `Connection` class. A connection is initialized with a path to a database. SQLite will attempt to create the database file if it does not already exist. +Database connections are established using the `Connection` class. A +connection is initialized with a path to a database. SQLite will attempt to +create the database file if it does not already exist. -``` swift +```swift let db = try Connection("path/to/db.sqlite3") ``` #### Read-Write Databases -On iOS, you can create a writable database in your app’s **Documents** directory. +On iOS, you can create a writable database in your app’s **Documents** +directory. -``` swift +```swift let path = NSSearchPathForDirectoriesInDomains( - .DocumentDirectory, .UserDomainMask, true + .documentDirectory, .userDomainMask, true ).first! let db = try Connection("\(path)/db.sqlite3") ``` -On OS X, you can use your app’s **Application Support** directory: +If you have bundled it in your application, you can use FileManager to copy it to the Documents directory: + +```swift +func copyDatabaseIfNeeded(sourcePath: String) -> Bool { + let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let destinationPath = documents + "/db.sqlite3" + let exists = FileManager.default.fileExists(atPath: destinationPath) + guard !exists else { return false } + do { + try FileManager.default.copyItem(atPath: sourcePath, toPath: destinationPath) + return true + } catch { + print("error during file copy: \(error)") + return false + } +} +``` -``` swift +On macOS, you can use your app’s **Application Support** directory: + + +```swift +// set the path corresponding to application support var path = NSSearchPathForDirectoriesInDomains( - .ApplicationSupportDirectory, .UserDomainMask, true -).first! + NSBundle.mainBundle().bundleIdentifier! + .applicationSupportDirectory, .userDomainMask, true +).first! + "/" + Bundle.main.bundleIdentifier! -// create parent directory iff it doesn’t exist -try NSFileManager.defaultManager().createDirectoryAtPath( - path, withIntermediateDirectories: true, attributes: nil +// create parent directory inside application support if it doesn’t exist +try FileManager.default.createDirectory( +atPath: path, withIntermediateDirectories: true, attributes: nil ) let db = try Connection("\(path)/db.sqlite3") ``` - #### Read-Only Databases -If you bundle a database with your app (_i.e._, you’ve copied a database file into your Xcode project and added it to your application target), you can establish a _read-only_ connection to it. +If you bundle a database with your app (_i.e._, you’ve copied a database file +into your Xcode project and added it to your application target), you can +establish a _read-only_ connection to it. -``` swift -let path = NSBundle.mainBundle().pathForResource("db", ofType: "sqlite3")! +```swift +let path = Bundle.main.path(forResource: "db", ofType: "sqlite3")! let db = try Connection(path, readonly: true) ``` -> _Note:_ Signed applications cannot modify their bundle resources. If you bundle a database file with your app for the purpose of bootstrapping, copy it to a writable location _before_ establishing a connection (see [Read-Write Databases](#read-write-databases), above, for typical, writable locations). -> -> See these two Stack Overflow questions for more information about iOS apps with SQLite databases: [1](https://stackoverflow.com/questions/34609746/what-different-between-store-database-in-different-locations-in-ios), [2](https://stackoverflow.com/questions/34614968/ios-how-to-copy-pre-seeded-database-at-the-first-running-app-with-sqlite-swift). We welcome sample code to show how to successfully copy and use a bundled "seed" database for writing in an app. +> _Note:_ Signed applications cannot modify their bundle resources. If you +> bundle a database file with your app for the purpose of bootstrapping, copy +> it to a writable location _before_ establishing a connection (see +> [Read-Write Databases](#read-write-databases), above, for typical, writable +> locations). +> +> See these two Stack Overflow questions for more information about iOS apps +> with SQLite databases: [1](https://stackoverflow.com/questions/34609746/what-different-between-store-database-in-different-locations-in-ios), +> [2](https://stackoverflow.com/questions/34614968/ios-how-to-copy-pre-seeded-database-at-the-first-running-app-with-sqlite-swift). +> We welcome changes to the above sample code to show how to successfully copy and use a bundled "seed" +> database for writing in an app. + +#### In a shared group container + +It is not recommend to store databases in a [shared group container], +some users have reported crashes ([#1042](https://github.com/stephencelis/SQLite.swift/issues/1042)). + +[shared group container]: https://developer.apple.com/documentation/foundation/filemanager/1412643-containerurl# #### In-Memory Databases -If you omit the path, SQLite.swift will provision an [in-memory database](https://www.sqlite.org/inmemorydb.html). +If you omit the path, SQLite.swift will provision an [in-memory +database](https://www.sqlite.org/inmemorydb.html). -``` swift -let db = try Connection() // equivalent to `Connection(.InMemory)` +```swift +let db = try Connection() // equivalent to `Connection(.inMemory)` ``` To create a temporary, disk-backed database, pass an empty file name. -``` swift -let db = try Connection(.Temporary) +```swift +let db = try Connection(.temporary) ``` -In-memory databases are automatically deleted when the database connection is closed. +In-memory databases are automatically deleted when the database connection is +closed. +#### URI parameters + +We can pass `.uri` to the `Connection` initializer to control more aspects of +the database connection with the help of `URIQueryParameter`s: + +```swift +let db = try Connection(.uri("file.sqlite", parameters: [.cache(.private), .noLock(true)])) +``` + +See [Uniform Resource Identifiers](https://www.sqlite.org/uri.html#recognized_query_parameters) for more details. #### Thread-Safety -Every Connection comes equipped with its own serial queue for statement execution and can be safely accessed across threads. Threads that open transactions and savepoints will block other threads from executing statements while the transaction is open. +Every Connection comes equipped with its own serial queue for statement +execution and can be safely accessed across threads. Threads that open +transactions and savepoints will block other threads from executing +statements while the transaction is open. -If you maintain multiple connections for a single database, consider setting a timeout (in seconds) and/or a busy handler: +If you maintain multiple connections for a single database, consider setting a timeout +(in seconds) *or* a busy handler. There can only be one active at a time, so setting a busy +handler will effectively override `busyTimeout`. ```swift -db.busyTimeout = 5 +db.busyTimeout = 5 // error after 5 seconds (does multiple retries) db.busyHandler({ tries in - if tries >= 3 { - return false - } - return true + tries < 3 // error after 3 tries }) ``` -> _Note:_ The default timeout is 0, so if you see `database is locked` errors, you may be trying to access the same database simultaneously from multiple connections. +> _Note:_ The default timeout is 0, so if you see `database is locked` +> errors, you may be trying to access the same database simultaneously from +> multiple connections. ## Building Type-Safe SQL -SQLite.swift comes with a typed expression layer that directly maps [Swift types](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/) to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). +SQLite.swift comes with a typed expression layer that directly maps +[Swift types](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/) +to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). | Swift Type | SQLite Type | | --------------- | ----------- | @@ -276,23 +424,36 @@ SQLite.swift comes with a typed expression layer that directly maps [Swift types | `String` | `TEXT` | | `nil` | `NULL` | | `SQLite.Blob`† | `BLOB` | +| `URL` | `TEXT` | +| `UUID` | `TEXT` | +| `Date` | `TEXT` | -> *While `Int64` is the basic, raw type (to preserve 64-bit integers on 32-bit platforms), `Int` and `Bool` work transparently. +> *While `Int64` is the basic, raw type (to preserve 64-bit integers on +> 32-bit platforms), `Int` and `Bool` work transparently. > -> †SQLite.swift defines its own `Blob` structure, which safely wraps the underlying bytes. +> †SQLite.swift defines its own `Blob` structure, which safely wraps the +> underlying bytes. > -> See [Custom Types](#custom-types) for more information about extending other classes and structures to work with SQLite.swift. +> See [Custom Types](#custom-types) for more information about extending +> other classes and structures to work with SQLite.swift. > -> See [Executing Arbitrary SQL](#executing-arbitrary-sql) to forego the typed layer and execute raw SQL, instead. +> See [Executing Arbitrary SQL](#executing-arbitrary-sql) to forego the typed +> layer and execute raw SQL, instead. -These expressions (in the form of the structure, [`Expression`](#expressions)) build on one another and, with a query ([`QueryType`](#queries)), can create and execute SQL statements. +These expressions (in the form of the structure, +[`Expression`](#expressions)) build on one another and, with a query +([`QueryType`](#queries)), can create and execute SQL statements. ### Expressions -Expressions are generic structures associated with a type ([built-in](#building-type-safe-sql) or [custom](#custom-types)), raw SQL, and (optionally) values to bind to that SQL. Typically, you will only explicitly create expressions to describe your columns, and typically only once per column. +Expressions are generic structures associated with a type ([built-in +](#building-type-safe-sql) or [custom](#custom-types)), raw SQL, and +(optionally) values to bind to that SQL. Typically, you will only explicitly +create expressions to describe your columns, and typically only once per +column. -``` swift +```swift let id = Expression("id") let email = Expression("email") let balance = Expression("balance") @@ -301,34 +462,48 @@ let verified = Expression("verified") Use optional generics for expressions that can evaluate to `NULL`. -``` swift +```swift let name = Expression("name") ``` -> _Note:_ The default `Expression` initializer is for [quoted identifiers](https://www.sqlite.org/lang_keywords.html) (_i.e._, column names). To build a literal SQL expression, use `init(literal:)`. +> _Note:_ The default `Expression` initializer is for [quoted +> identifiers](https://www.sqlite.org/lang_keywords.html) (_i.e._, column +> names). To build a literal SQL expression, use `init(literal:)`. +> ### Compound Expressions -Expressions can be combined with other expressions and types using [filter operators and functions](#filter-operators-and-functions) (as well as other [non-filter operators](#other-operators) and [functions](#core-sqlite-functions)). These building blocks can create complex SQLite statements. +Expressions can be combined with other expressions and types using +[filter operators and functions](#filter-operators-and-functions) +(as well as other [non-filter operators](#other-operators) and +[functions](#core-sqlite-functions)). These building blocks can create complex SQLite statements. ### Queries -Queries are structures that reference a database and table name, and can be used to build a variety of statements using expressions. We can create a query by initializing a `Table`, `View`, or `VirtualTable`. +Queries are structures that reference a database and table name, and can be +used to build a variety of statements using expressions. We can create a +query by initializing a `Table`, `View`, or `VirtualTable`. -``` swift +```swift let users = Table("users") ``` -Assuming [the table exists](#creating-a-table), we can immediately [insert](#inserting-rows), [select](#selecting-rows), [update](#updating-rows), and [delete](#deleting-rows) rows. +Assuming [the table exists](#creating-a-table), we can immediately [insert +](#inserting-rows), [select](#selecting-rows), [update](#updating-rows), and +[delete](#deleting-rows) rows. ## Creating a Table -We can build [`CREATE TABLE` statements](https://www.sqlite.org/lang_createtable.html) by calling the `create` function on a `Table`. The following is a basic example of SQLite.swift code (using the [expressions](#expressions) and [query](#queries) above) and the corresponding SQL it generates. +We can build [`CREATE TABLE` +statements](https://www.sqlite.org/lang_createtable.html) by calling the +`create` function on a `Table`. The following is a basic example of +SQLite.swift code (using the [expressions](#expressions) and +[query](#queries) above) and the corresponding SQL it generates. -``` swift +```swift try db.run(users.create { t in // CREATE TABLE "users" ( t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL, t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL, @@ -336,125 +511,170 @@ try db.run(users.create { t in // CREATE TABLE "users" ( }) // ) ``` -> _Note:_ `Expression` structures (in this case, the `id` and `email` columns), generate `NOT NULL` constraints automatically, while `Expression` structures (`name`) do not. +> _Note:_ `Expression` structures (in this case, the `id` and `email` +> columns), generate `NOT NULL` constraints automatically, while +> `Expression` structures (`name`) do not. ### Create Table Options The `Table.create` function has several default parameters we can override. - - `temporary` adds a `TEMPORARY` clause to the `CREATE TABLE` statement (to create a temporary table that will automatically drop when the database connection closes). Default: `false`. + - `temporary` adds a `TEMPORARY` clause to the `CREATE TABLE` statement (to + create a temporary table that will automatically drop when the database + connection closes). Default: `false`. - ``` swift + ```swift try db.run(users.create(temporary: true) { t in /* ... */ }) // CREATE TEMPORARY TABLE "users" -- ... ``` - - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` statement (which will bail out gracefully if the table already exists). Default: `false`. + - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` + statement (which will bail out gracefully if the table already exists). + Default: `false`. - ``` swift + ```swift try db.run(users.create(ifNotExists: true) { t in /* ... */ }) // CREATE TABLE "users" IF NOT EXISTS -- ... ``` ### Column Constraints -The `column` function is used for a single column definition. It takes an [expression](#expressions) describing the column name and type, and accepts several parameters that map to various column constraints and clauses. +The `column` function is used for a single column definition. It takes an +[expression](#expressions) describing the column name and type, and accepts +several parameters that map to various column constraints and clauses. - `primaryKey` adds a `PRIMARY KEY` constraint to a single column. - ``` swift + ```swift t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL - t.column(id, primaryKey: .Autoincrement) + t.column(id, primaryKey: .autoincrement) // "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ``` - > _Note:_ The `primaryKey` parameter cannot be used alongside `references`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > _Note:_ The `primaryKey` parameter cannot be used alongside + > `references`. If you need to create a column that has a default value + > and is also a primary and/or foreign key, use the `primaryKey` and + > `foreignKey` functions mentioned under + > [Table Constraints](#table-constraints). > > Primary keys cannot be optional (_e.g._, `Expression`). > - > Only an `INTEGER PRIMARY KEY` can take `.Autoincrement`. + > Only an `INTEGER PRIMARY KEY` can take `.autoincrement`. - - `unique` adds a `UNIQUE` constraint to the column. (See the `unique` function under [Table Constraints](#table-constraints) for uniqueness over multiple columns). + - `unique` adds a `UNIQUE` constraint to the column. (See the `unique` + function under [Table Constraints](#table-constraints) for uniqueness + over multiple columns). - ``` swift + ```swift t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL ``` - - `check` attaches a `CHECK` constraint to a column definition in the form of a boolean expression (`Expression`). Boolean expressions can be easily built using [filter operators and functions](#filter-operators-and-functions). (See also the `check` function under [Table Constraints](#table-constraints).) + - `check` attaches a `CHECK` constraint to a column definition in the form + of a boolean expression (`Expression`). Boolean expressions can be + easily built using + [filter operators and functions](#filter-operators-and-functions). + (See also the `check` function under + [Table Constraints](#table-constraints).) - ``` swift + ```swift t.column(email, check: email.like("%@%")) // "email" TEXT NOT NULL CHECK ("email" LIKE '%@%') ``` - - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ accepts a value (or expression) matching the column’s type. This value is used if none is explicitly provided during [an `INSERT`](#inserting-rows). + - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ + accepts a value (or expression) matching the column’s type. This value is + used if none is explicitly provided during + [an `INSERT`](#inserting-rows). - ``` swift + ```swift t.column(name, defaultValue: "Anonymous") // "name" TEXT DEFAULT 'Anonymous' ``` - > _Note:_ The `defaultValue` parameter cannot be used alongside `primaryKey` and `references`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > _Note:_ The `defaultValue` parameter cannot be used alongside + > `primaryKey` and `references`. If you need to create a column that has + > a default value and is also a primary and/or foreign key, use the + > `primaryKey` and `foreignKey` functions mentioned under + > [Table Constraints](#table-constraints). - - `collate` adds a `COLLATE` clause to `Expression` (and `Expression`) column definitions with [a collating sequence](https://www.sqlite.org/datatype3.html#collation) defined in the `Collation` enumeration. + - `collate` adds a `COLLATE` clause to `Expression` (and + `Expression`) column definitions with + [a collating sequence](https://www.sqlite.org/datatype3.html#collation) + defined in the `Collation` enumeration. - ``` swift - t.column(email, collate: .Nocase) + ```swift + t.column(email, collate: .nocase) // "email" TEXT NOT NULL COLLATE "NOCASE" - t.column(name, collate: .Rtrim) + t.column(name, collate: .rtrim) // "name" TEXT COLLATE "RTRIM" ``` - - `references` adds a `REFERENCES` clause to `Expression` (and `Expression`) column definitions and accepts a table (`SchemaType`) or namespaced column expression. (See the `foreignKey` function under [Table Constraints](#table-constraints) for non-integer foreign key support.) + - `references` adds a `REFERENCES` clause to `Expression` (and + `Expression`) column definitions and accepts a table + (`SchemaType`) or namespaced column expression. (See the `foreignKey` + function under [Table Constraints](#table-constraints) for non-integer + foreign key support.) - ``` swift + ```swift t.column(user_id, references: users, id) // "user_id" INTEGER REFERENCES "users" ("id") - - - > _Note:_ The `references` parameter cannot be used alongside `primaryKey` and `defaultValue`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > _Note:_ The `references` parameter cannot be used alongside + > `primaryKey` and `defaultValue`. If you need to create a column that + > has a default value and is also a primary and/or foreign key, use the + > `primaryKey` and `foreignKey` functions mentioned under + > [Table Constraints](#table-constraints). ### Table Constraints -Additional constraints may be provided outside the scope of a single column using the following functions. +Additional constraints may be provided outside the scope of a single column +using the following functions. - - `primaryKey` adds a `PRIMARY KEY` constraint to the table. Unlike [the column constraint, above](#column-constraints), it supports all SQLite types, [ascending and descending orders](#sorting-rows), and composite (multiple column) keys. + - `primaryKey` adds a `PRIMARY KEY` constraint to the table. Unlike [the + column constraint, above](#column-constraints), it supports all SQLite + types, [ascending and descending orders](#sorting-rows), and composite + (multiple column) keys. - ``` swift + ```swift t.primaryKey(email.asc, name) // PRIMARY KEY("email" ASC, "name") ``` - - `unique` adds a `UNIQUE` constraint to the table. Unlike [the column constraint, above](#column-constraints), it supports composite (multiple column) constraints. + - `unique` adds a `UNIQUE` constraint to the table. Unlike + [the column constraint, above](#column-constraints), it + supports composite (multiplecolumn) constraints. - ``` swift + ```swift t.unique(local, domain) // UNIQUE("local", "domain") ``` - - `check` adds a `CHECK` constraint to the table in the form of a boolean expression (`Expression`). Boolean expressions can be easily built using [filter operators and functions](#filter-operators-and-functions). (See also the `check` parameter under [Column Constraints](#column-constraints).) + - `check` adds a `CHECK` constraint to the table in the form of a boolean + expression (`Expression`). Boolean expressions can be easily built + using [filter operators and functions](#filter-operators-and-functions). + (See also the `check` parameter under + [Column Constraints](#column-constraints).) - ``` swift + ```swift t.check(balance >= 0) // CHECK ("balance" >= 0.0) ``` - - `foreignKey` adds a `FOREIGN KEY` constraint to the table. Unlike [the `references` constraint, above](#column-constraints), it supports all SQLite types, both [`ON UPDATE` and `ON DELETE` actions](https://www.sqlite.org/foreignkeys.html#fk_actions), and composite (multiple column) keys. + - `foreignKey` adds a `FOREIGN KEY` constraint to the table. Unlike [the + `references` constraint, above](#column-constraints), it supports all + SQLite types, both [`ON UPDATE` and `ON DELETE` + actions](https://www.sqlite.org/foreignkeys.html#fk_actions), and + composite (multiple column) keys. - ``` swift - t.foreignKey(user_id, references: users, id, delete: .SetNull) + ```swift + t.foreignKey(user_id, references: users, id, delete: .setNull) // FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE SET NULL ``` @@ -465,19 +685,23 @@ Additional constraints may be provided outside the scope of a single column usin ## Inserting Rows -We can insert rows into a table by calling a [query’s](#queries) `insert` function with a list of [setters](#setters)—typically [typed column expressions](#expressions) and values (which can also be expressions)—each joined by the `<-` operator. +We can insert rows into a table by calling a [query’s](#queries) `insert` +function with a list of [setters](#setters)—typically [typed column +expressions](#expressions) and values (which can also be expressions)—each +joined by the `<-` operator. -``` swift +```swift try db.run(users.insert(email <- "alice@mac.com", name <- "Alice")) // INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice') -try db.run(users.insert(or: .Replace, email <- "alice@mac.com", name <- "Alice B.")) +try db.run(users.insert(or: .replace, email <- "alice@mac.com", name <- "Alice B.")) // INSERT OR REPLACE INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice B.') ``` -The `insert` function, when run successfully, returns an `Int64` representing the inserted row’s [`ROWID`][ROWID]. +The `insert` function, when run successfully, returns an `Int64` representing +the inserted row’s [`ROWID`][ROWID]. -``` swift +```swift do { let rowid = try db.run(users.insert(email <- "alice@mac.com")) print("inserted id: \(rowid)") @@ -486,37 +710,74 @@ do { } ``` -The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions follow similar patterns. +Multiple rows can be inserted at once by similarly calling `insertMany` with an array of +per-row [setters](#setters). -> _Note:_ If `insert` is called without any arguments, the statement will run with a `DEFAULT VALUES` clause. The table must not have any constraints that aren’t fulfilled by default values. +```swift +do { + let lastRowid = try db.run(users.insertMany([mail <- "alice@mac.com"], [email <- "geoff@mac.com"])) + print("last inserted id: \(lastRowid)") +} catch { + print("insertion failed: \(error)") +} +``` + + +The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions +follow similar patterns. + +> _Note:_ If `insert` is called without any arguments, the statement will run +> with a `DEFAULT VALUES` clause. The table must not have any constraints +> that aren’t fulfilled by default values. > -> ``` swift +> ```swift > try db.run(timestamps.insert()) > // INSERT INTO "timestamps" DEFAULT VALUES > ``` +### Handling SQLite errors + +You can pattern match on the error to selectively catch SQLite errors. For example, to +specifically handle constraint errors ([SQLITE_CONSTRAINT](https://sqlite.org/rescode.html#constraint)): + +```swift +do { + try db.run(users.insert(email <- "alice@mac.com")) + try db.run(users.insert(email <- "alice@mac.com")) +} catch let Result.error(message, code, statement) where code == SQLITE_CONSTRAINT { + print("constraint failed: \(message), in \(statement)") +} catch let error { + print("insertion failed: \(error)") +} +``` + +The `Result.error` type contains the English-language text that describes the error (`message`), +the error `code` (see [SQLite result code list](https://sqlite.org/rescode.html#primary_result_code_list) +for details) and a optional reference to the `statement` which produced the error. ### Setters -SQLite.swift typically uses the `<-` operator to set values during [inserts](#inserting-rows) and [updates](#updating-rows). +SQLite.swift typically uses the `<-` operator to set values during [inserts +](#inserting-rows) and [updates](#updating-rows). -``` swift +```swift try db.run(counter.update(count <- 0)) // UPDATE "counters" SET "count" = 0 WHERE ("id" = 1) ``` -There are also a number of convenience setters that take the existing value into account using native Swift operators. +There are also a number of convenience setters that take the existing value +into account using native Swift operators. For example, to atomically increment a column, we can use `++`: -``` swift +```swift try db.run(counter.update(count++)) // equivalent to `counter.update(count -> count + 1)` // UPDATE "counters" SET "count" = "count" + 1 WHERE ("id" = 1) ``` To take an amount and “move” it via transaction, we can use `-=` and `+=`: -``` swift +```swift let amount = 100.0 try db.transaction { try db.run(alice.update(balance -= amount)) @@ -542,7 +803,7 @@ try db.transaction { | `<<=` | `Int -> Int` | | `>>=` | `Int -> Int` | | `&=` | `Int -> Int` | -| `||=` | `Int -> Int` | +| `\|\|=` | `Int -> Int` | | `^=` | `Int -> Int` | | `+=` | `String -> String` | @@ -557,14 +818,18 @@ try db.transaction { ## Selecting Rows -[Query structures](#queries) are `SELECT` statements waiting to happen. They execute via [iteration](#iterating-and-accessing-values) and [other means](#plucking-values) of sequence access. +[Query structures](#queries) are `SELECT` statements waiting to happen. They +execute via [iteration](#iterating-and-accessing-values) and [other means +](#plucking-values) of sequence access. ### Iterating and Accessing Values -Prepared [queries](#queries) execute lazily upon iteration. Each row is returned as a `Row` object, which can be subscripted with a [column expression](#expressions) matching one of the columns returned. +Prepared [queries](#queries) execute lazily upon iteration. Each row is +returned as a `Row` object, which can be subscripted with a [column +expression](#expressions) matching one of the columns returned. -``` swift +```swift for user in try db.prepare(users) { print("id: \(user[id]), email: \(user[email]), name: \(user[name])") // id: 1, email: alice@mac.com, name: Optional("Alice") @@ -572,21 +837,73 @@ for user in try db.prepare(users) { // SELECT * FROM "users" ``` -`Expression` column values are _automatically unwrapped_ (we’ve made a promise to the compiler that they’ll never be `NULL`), while `Expression` values remain wrapped. +`Expression` column values are _automatically unwrapped_ (we’ve made a +promise to the compiler that they’ll never be `NULL`), while `Expression` +values remain wrapped. +⚠ Column subscripts on `Row` will force try and abort execution in error cases. +If you want to handle this yourself, use `Row.get(_ column: Expression)`: + +```swift +for user in try db.prepare(users) { + do { + print("name: \(try user.get(name))") + } catch { + // handle + } +} +``` + +Note that the iterator can throw *undeclared* database errors at any point during +iteration: + +```swift +let query = try db.prepare(users) +for user in query { + // 💥 can throw an error here +} +``` + +#### Failable iteration + +It is therefore recommended using the `RowIterator` API instead, +which has explicit error handling: + +```swift +// option 1: convert results into an Array of rows +let rowIterator = try db.prepareRowIterator(users) +for user in try Array(rowIterator) { + print("id: \(user[id]), email: \(user[email])") +} + +/// option 2: transform results using `map()` +let mapRowIterator = try db.prepareRowIterator(users) +let userIds = try mapRowIterator.map { $0[id] } + +/// option 3: handle each row individually with `failableNext()` +do { + while let row = try rowIterator.failableNext() { + // Handle row + } +} catch { + // Handle error +} +``` ### Plucking Rows -We can pluck the first row by passing a query to the `pluck` function on a database connection. +We can pluck the first row by passing a query to the `pluck` function on a +database connection. -``` swift +```swift if let user = try db.pluck(users) { /* ... */ } // Row // SELECT * FROM "users" LIMIT 1 ``` -To collect all rows into an array, we can simply wrap the sequence (though this is not always the most memory-efficient idea). +To collect all rows into an array, we can simply wrap the sequence (though +this is not always the most memory-efficient idea). -``` swift +```swift let all = Array(try db.prepare(users)) // SELECT * FROM "users" ``` @@ -594,9 +911,12 @@ let all = Array(try db.prepare(users)) ### Building Complex Queries -[Queries](#queries) have a number of chainable functions that can be used (with [expressions](#expressions)) to add and modify [a number of clauses](https://www.sqlite.org/lang_select.html) to the underlying statement. +[Queries](#queries) have a number of chainable functions that can be used +(with [expressions](#expressions)) to add and modify [a number of +clauses](https://www.sqlite.org/lang_select.html) to the underlying +statement. -``` swift +```swift let query = users.select(email) // SELECT "email" FROM "users" .filter(name != nil) // WHERE "name" IS NOT NULL .order(email.desc, name) // ORDER BY "email" DESC, "name" @@ -606,9 +926,11 @@ let query = users.select(email) // SELECT "email" FROM "users" #### Selecting Columns -By default, [queries](#queries) select every column of the result set (using `SELECT *`). We can use the `select` function with a list of [expressions](#expressions) to return specific columns instead. +By default, [queries](#queries) select every column of the result set (using +`SELECT *`). We can use the `select` function with a list of +[expressions](#expressions) to return specific columns instead. -``` swift +```swift for user in try db.prepare(users.select(id, email)) { print("id: \(user[id]), email: \(user[email])") // id: 1, email: alice@mac.com @@ -616,9 +938,10 @@ for user in try db.prepare(users.select(id, email)) { // SELECT "id", "email" FROM "users" ``` -We can access the results of more complex expressions by holding onto a reference of the expression itself. +We can access the results of more complex expressions by holding onto a +reference of the expression itself. -``` swift +```swift let sentence = name + " is " + cast(age) as Expression + " years old!" for user in users.select(sentence) { print(user[sentence]) @@ -632,35 +955,42 @@ for user in users.select(sentence) { We can join tables using a [query’s](#queries) `join` function. -``` swift +```swift users.join(posts, on: user_id == users[id]) // SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id") ``` -The `join` function takes a [query](#queries) object (for the table being joined on), a join condition (`on`), and is prefixed with an optional join type (default: `.Inner`). Join conditions can be built using [filter operators and functions](#filter-operators-and-functions), generally require [namespacing](#column-namespacing), and sometimes require [aliasing](#table-aliasing). +The `join` function takes a [query](#queries) object (for the table being +joined on), a join condition (`on`), and is prefixed with an optional join +type (default: `.inner`). Join conditions can be built using [filter +operators and functions](#filter-operators-and-functions), generally require +[namespacing](#column-namespacing), and sometimes require +[aliasing](#table-aliasing). ##### Column Namespacing -When joining tables, column names can become ambiguous. _E.g._, both tables may have an `id` column. +When joining tables, column names can become ambiguous. _E.g._, both tables +may have an `id` column. -``` swift +```swift let query = users.join(posts, on: user_id == id) // assertion failure: ambiguous column 'id' ``` We can disambiguate by namespacing `id`. -``` swift +```swift let query = users.join(posts, on: user_id == users[id]) // SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id") ``` -Namespacing is achieved by subscripting a [query](#queries) with a [column expression](#expressions) (_e.g._, `users[id]` above becomes `users.id`). +Namespacing is achieved by subscripting a [query](#queries) with a [column +expression](#expressions) (_e.g._, `users[id]` above becomes `users.id`). > _Note:_ We can namespace all of a table’s columns using `*`. > -> ``` swift +> ```swift > let query = users.select(users[*]) > // SELECT "users".* FROM "users" > ``` @@ -668,9 +998,11 @@ Namespacing is achieved by subscripting a [query](#queries) with a [column expre ##### Table Aliasing -Occasionally, we need to join a table to itself, in which case we must alias the table with another name. We can achieve this using the [query’s](#queries) `alias` function. +Occasionally, we need to join a table to itself, in which case we must alias +the table with another name. We can achieve this using the +[query’s](#queries) `alias` function. -``` swift +```swift let managers = users.alias("managers") let query = users.join(managers, on: managers[id] == users[managerId]) @@ -678,9 +1010,11 @@ let query = users.join(managers, on: managers[id] == users[managerId]) // INNER JOIN ("users") AS "managers" ON ("managers"."id" = "users"."manager_id") ``` -If query results can have ambiguous column names, row values should be accessed with namespaced [column expressions](#expressions). In the above case, `SELECT *` immediately namespaces all columns of the result set. +If query results can have ambiguous column names, row values should be +accessed with namespaced [column expressions](#expressions). In the above +case, `SELECT *` immediately namespaces all columns of the result set. -``` swift +```swift let user = try db.pluck(query) user[id] // fatal error: ambiguous column 'id' // (please disambiguate: ["users"."id", "managers"."id"]) @@ -692,9 +1026,10 @@ user[managers[id]] // returns "managers"."id" #### Filtering Rows -SQLite.swift filters rows using a [query’s](#queries) `filter` function with a boolean [expression](#expressions) (`Expression`). +SQLite.swift filters rows using a [query’s](#queries) `filter` function with +a boolean [expression](#expressions) (`Expression`). -``` swift +```swift users.filter(id == 1) // SELECT * FROM "users" WHERE ("id" = 1) @@ -711,14 +1046,21 @@ users.filter(verified || balance >= 10_000) // SELECT * FROM "users" WHERE ("verified" OR ("balance" >= 10000.0)) ``` -We can build our own boolean expressions by using one of the many [filter operators and functions](#filter-operators-and-functions). +We can build our own boolean expressions by using one of the many [filter +operators and functions](#filter-operators-and-functions). -> _Note:_ SQLite.swift defines `filter` instead of `where` because `where` is [a reserved keyword](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/LexicalStructure.html#//apple_ref/doc/uid/TP40014097-CH30-ID413). +Instead of `filter` we can also use the `where` function which is an alias: +```swift +users.where(id == 1) +// SELECT * FROM "users" WHERE ("id" = 1) +``` ##### Filter Operators and Functions -SQLite.swift defines a number of operators for building filtering predicates. Operators and functions work together in a type-safe manner, so attempting to equate or compare different types will prevent compilation. +SQLite.swift defines a number of operators for building filtering predicates. +Operators and functions work together in a type-safe manner, so attempting to +equate or compare different types will prevent compilation. ###### Infix Filter Operators @@ -733,9 +1075,12 @@ SQLite.swift defines a number of operators for building filtering predicates. Op | `<=` | `Comparable -> Bool` | `<=` | | `~=` | `(Interval, Comparable) -> Bool` | `BETWEEN` | | `&&` | `Bool -> Bool` | `AND` | -| `||` | `Bool -> Bool` | `OR` | +| `\|\|`| `Bool -> Bool` | `OR` | +| `===` | `Equatable -> Bool` | `IS` | +| `!==` | `Equatable -> Bool` | `IS NOT` | -> *When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` accordingly. +> * When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` +> accordingly. ###### Prefix Filter Operators @@ -766,16 +1111,18 @@ We can pre-sort returned rows using the [query’s](#queries) `order` function. _E.g._, to return users sorted by `email`, then `name`, in ascending order: -``` swift +```swift users.order(email, name) // SELECT * FROM "users" ORDER BY "email", "name" ``` The `order` function takes a list of [column expressions](#expressions). -`Expression` objects have two computed properties to assist sorting: `asc` and `desc`. These properties append the expression with `ASC` and `DESC` to mark ascending and descending order respectively. +`Expression` objects have two computed properties to assist sorting: `asc` +and `desc`. These properties append the expression with `ASC` and `DESC` to +mark ascending and descending order respectively. -``` swift +```swift users.order(email.desc, name.asc) // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC ``` @@ -783,9 +1130,10 @@ users.order(email.desc, name.asc) #### Limiting and Paging Results -We can limit and skip returned rows using a [query’s](#queries) `limit` function (and its optional `offset` parameter). +We can limit and skip returned rows using a [query’s](#queries) `limit` +function (and its optional `offset` parameter). -``` swift +```swift users.limit(5) // SELECT * FROM "users" LIMIT 5 @@ -794,96 +1142,200 @@ users.limit(5, offset: 5) ``` +#### Recursive and Hierarchical Queries + +We can perform a recursive or hierarchical query using a [query's](#queries) +[`WITH`](https://sqlite.org/lang_with.html) function. + +```swift +// Get the management chain for the manager with id == 8 + +let chain = Table("chain") +let id = Expression("id") +let managerId = Expression("manager_id") + +let query = managers + .where(id == 8) + .union(chain.join(managers, on: chain[managerId] == managers[id]) + +chain.with(chain, recursive: true, as: query) +// WITH RECURSIVE +// "chain" AS ( +// SELECT * FROM "managers" WHERE "id" = 8 +// UNION +// SELECT * from "chain" +// JOIN "managers" ON "chain"."manager_id" = "managers"."id" +// ) +// SELECT * FROM "chain" +``` + +Column names and a materialization hint can optionally be provided. + +```swift +// Add a "level" column to the query representing manager's position in the chain +let level = Expression("level") + +let queryWithLevel = + managers + .select(id, managerId, 0) + .where(id == 8) + .union( + chain + .select(managers[id], managers[manager_id], level + 1) + .join(managers, on: chain[managerId] == managers[id]) + ) + +chain.with(chain, + columns: [id, managerId, level], + recursive: true, + hint: .materialize, + as: queryWithLevel) +// WITH RECURSIVE +// "chain" ("id", "manager_id", "level") AS MATERIALIZED ( +// SELECT ("id", "manager_id", 0) FROM "managers" WHERE "id" = 8 +// UNION +// SELECT ("manager"."id", "manager"."manager_id", "level" + 1) FROM "chain" +// JOIN "managers" ON "chain"."manager_id" = "managers"."id" +// ) +// SELECT * FROM "chain" +``` + + #### Aggregation -[Queries](#queries) come with a number of functions that quickly return aggregate scalar values from the table. These mirror the [core aggregate functions](#aggregate-sqlite-functions) and are executed immediately against the query. +[Queries](#queries) come with a number of functions that quickly return +aggregate scalar values from the table. These mirror the [core aggregate +functions](#aggregate-sqlite-functions) and are executed immediately against +the query. -``` swift +```swift let count = try db.scalar(users.count) // SELECT count(*) FROM "users" ``` Filtered queries will appropriately filter aggregate values. -``` swift +```swift let count = try db.scalar(users.filter(name != nil).count) // SELECT count(*) FROM "users" WHERE "name" IS NOT NULL ``` - - `count` as a computed property on a query (see examples above) returns the total number of rows matching the query. + - `count` as a computed property on a query (see examples above) returns + the total number of rows matching the query. - `count` as a computed property on a column expression returns the total number of rows where that column is not `NULL`. + `count` as a computed property on a column expression returns the total + number of rows where that column is not `NULL`. - ``` swift + ```swift let count = try db.scalar(users.select(name.count)) // -> Int // SELECT count("name") FROM "users" ``` - - `max` takes a comparable column expression and returns the largest value if any exists. + - `max` takes a comparable column expression and returns the largest value + if any exists. - ``` swift + ```swift let max = try db.scalar(users.select(id.max)) // -> Int64? // SELECT max("id") FROM "users" ``` - - `min` takes a comparable column expression and returns the smallest value if any exists. + - `min` takes a comparable column expression and returns the smallest value + if any exists. - ``` swift + ```swift let min = try db.scalar(users.select(id.min)) // -> Int64? // SELECT min("id") FROM "users" ``` - - `average` takes a numeric column expression and returns the average row value (as a `Double`) if any exists. + - `average` takes a numeric column expression and returns the average row + value (as a `Double`) if any exists. - ``` swift + ```swift let average = try db.scalar(users.select(balance.average)) // -> Double? // SELECT avg("balance") FROM "users" ``` - - `sum` takes a numeric column expression and returns the sum total of all rows if any exist. + - `sum` takes a numeric column expression and returns the sum total of all + rows if any exist. - ``` swift + ```swift let sum = try db.scalar(users.select(balance.sum)) // -> Double? // SELECT sum("balance") FROM "users" ``` - - `total`, like `sum`, takes a numeric column expression and returns the sum total of all rows, but in this case always returns a `Double`, and returns `0.0` for an empty query. + - `total`, like `sum`, takes a numeric column expression and returns the + sum total of all rows, but in this case always returns a `Double`, and + returns `0.0` for an empty query. - ``` swift + ```swift let total = try db.scalar(users.select(balance.total)) // -> Double // SELECT total("balance") FROM "users" ``` -> _Note:_ Expressions can be prefixed with a `DISTINCT` clause by calling the `distinct` computed property. +> _Note:_ Expressions can be prefixed with a `DISTINCT` clause by calling the +> `distinct` computed property. > -> ``` swift +> ```swift > let count = try db.scalar(users.select(name.distinct.count) // -> Int > // SELECT count(DISTINCT "name") FROM "users" > ``` +## Upserting Rows + +We can upsert rows into a table by calling a [query’s](#queries) `upsert` +function with a list of [setters](#setters)—typically [typed column +expressions](#expressions) and values (which can also be expressions)—each +joined by the `<-` operator. Upserting is like inserting, except if there is a +conflict on the specified column value, SQLite will perform an update on the row instead. + +```swift +try db.run(users.upsert(email <- "alice@mac.com", name <- "Alice", onConflictOf: email)) +// INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice') ON CONFLICT (\"email\") DO UPDATE SET \"name\" = \"excluded\".\"name\" +``` + +The `upsert` function, when run successfully, returns an `Int64` representing +the inserted row’s [`ROWID`][ROWID]. + +```swift +do { + let rowid = try db.run(users.upsert(email <- "alice@mac.com", name <- "Alice", onConflictOf: email)) + print("inserted id: \(rowid)") +} catch { + print("insertion failed: \(error)") +} +``` + +The [`insert`](#inserting-rows), [`update`](#updating-rows), and [`delete`](#deleting-rows) functions +follow similar patterns. ## Updating Rows -We can update a table’s rows by calling a [query’s](#queries) `update` function with a list of [setters](#setters)—typically [typed column expressions](#expressions) and values (which can also be expressions)—each joined by the `<-` operator. +We can update a table’s rows by calling a [query’s](#queries) `update` +function with a list of [setters](#setters)—typically [typed column +expressions](#expressions) and values (which can also be expressions)—each +joined by the `<-` operator. -When an unscoped query calls `update`, it will update _every_ row in the table. +When an unscoped query calls `update`, it will update _every_ row in the +table. -``` swift +```swift try db.run(users.update(email <- "alice@me.com")) // UPDATE "users" SET "email" = 'alice@me.com' ``` -Be sure to scope `UPDATE` statements beforehand using [the `filter` function](#filtering-rows). +Be sure to scope `UPDATE` statements beforehand using [the `filter` function +](#filtering-rows). -``` swift +```swift let alice = users.filter(id == 1) try db.run(alice.update(email <- "alice@me.com")) // UPDATE "users" SET "email" = 'alice@me.com' WHERE ("id" = 1) ``` -The `update` function returns an `Int` representing the number of updated rows. +The `update` function returns an `Int` representing the number of updated +rows. -``` swift +```swift do { if try db.run(alice.update(email <- "alice@me.com")) > 0 { print("updated alice") @@ -898,26 +1350,30 @@ do { ## Deleting Rows -We can delete rows from a table by calling a [query’s](#queries) `delete` function. +We can delete rows from a table by calling a [query’s](#queries) `delete` +function. -When an unscoped query calls `delete`, it will delete _every_ row in the table. +When an unscoped query calls `delete`, it will delete _every_ row in the +table. -``` swift +```swift try db.run(users.delete()) // DELETE FROM "users" ``` -Be sure to scope `DELETE` statements beforehand using [the `filter` function](#filtering-rows). +Be sure to scope `DELETE` statements beforehand using +[the `filter` function](#filtering-rows). -``` swift +```swift let alice = users.filter(id == 1) try db.run(alice.delete()) // DELETE FROM "users" WHERE ("id" = 1) ``` -The `delete` function returns an `Int` representing the number of deleted rows. +The `delete` function returns an `Int` representing the number of deleted +rows. -``` swift +```swift do { if try db.run(alice.delete()) > 0 { print("deleted alice") @@ -932,9 +1388,11 @@ do { ## Transactions and Savepoints -Using the `transaction` and `savepoint` functions, we can run a series of statements in a transaction. If a single statement fails or the block throws an error, the changes will be rolled back. +Using the `transaction` and `savepoint` functions, we can run a series of +statements in a transaction. If a single statement fails or the block throws +an error, the changes will be rolled back. -``` swift +```swift try db.transaction { let rowid = try db.run(users.insert(email <- "betty@icloud.com")) try db.run(users.insert(email <- "cathy@icloud.com", managerId <- rowid)) @@ -947,96 +1405,227 @@ try db.transaction { > _Note:_ Transactions run in a serial queue. +## Querying the Schema -## Altering the Schema +We can obtain generic information about objects in the current schema with a `SchemaReader`: + +```swift +let schema = db.schema +``` -SQLite.swift comes with several functions (in addition to `Table.create`) for altering a database schema in a type-safe manner. +To query the data: +```swift +let indexes = try schema.objectDefinitions(type: .index) +let tables = try schema.objectDefinitions(type: .table) +let triggers = try schema.objectDefinitions(type: .trigger) +``` + +### Indexes and Columns + +Specialized methods are available to get more detailed information: + +```swift +let indexes = try schema.indexDefinitions("users") +let columns = try schema.columnDefinitions("users") + +for index in indexes { + print("\(index.name) columns:\(index.columns))") +} +for column in columns { + print("\(column.name) pk:\(column.primaryKey) nullable: \(column.nullable)") +} +``` + +## Altering the Schema + +SQLite.swift comes with several functions (in addition to `Table.create`) for +altering a database schema in a type-safe manner. ### Renaming Tables -We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` function on a `Table` or `VirtualTable`. +We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` +function on a `Table` or `VirtualTable`. -``` swift -try db.run(users.rename(Table("users_old")) +```swift +try db.run(users.rename(Table("users_old"))) // ALTER TABLE "users" RENAME TO "users_old" ``` +### Dropping Tables + +We can build +[`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) +by calling the `dropTable` function on a `SchemaType`. + +```swift +try db.run(users.drop()) +// DROP TABLE "users" +``` + +The `drop` function has one additional parameter, `ifExists`, which (when +`true`) adds an `IF EXISTS` clause to the statement. + +```swift +try db.run(users.drop(ifExists: true)) +// DROP TABLE IF EXISTS "users" +``` ### Adding Columns -We can add columns to a table by calling `addColumn` function on a `Table`. SQLite.swift enforces [the same limited subset](https://www.sqlite.org/lang_altertable.html) of `ALTER TABLE` that SQLite supports. +We can add columns to a table by calling `addColumn` function on a `Table`. +SQLite.swift enforces +[the same limited subset](https://www.sqlite.org/lang_altertable.html) of +`ALTER TABLE` that SQLite supports. -``` swift +```swift try db.run(users.addColumn(suffix)) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT ``` - #### Added Column Constraints -The `addColumn` function shares several of the same [`column` function parameters](#column-constraints) used when [creating tables](#creating-a-table). +The `addColumn` function shares several of the same [`column` function +parameters](#column-constraints) used when [creating +tables](#creating-a-table). - - `check` attaches a `CHECK` constraint to a column definition in the form of a boolean expression (`Expression`). (See also the `check` function under [Table Constraints](#table-constraints).) + - `check` attaches a `CHECK` constraint to a column definition in the form + of a boolean expression (`Expression`). (See also the `check` + function under [Table Constraints](#table-constraints).) - ``` swift + ```swift try db.run(users.addColumn(suffix, check: ["JR", "SR"].contains(suffix))) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT CHECK ("suffix" IN ('JR', 'SR')) ``` - - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ accepts a value matching the column’s type. This value is used if none is explicitly provided during [an `INSERT`](#inserting-rows). + - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ + accepts a value matching the column’s type. This value is used if none is + explicitly provided during [an `INSERT`](#inserting-rows). - ``` swift + ```swift try db.run(users.addColumn(suffix, defaultValue: "SR")) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT DEFAULT 'SR' ``` - > _Note:_ Unlike the [`CREATE TABLE` constraint](#table-constraints), default values may not be expression structures (including `CURRENT_TIME`, `CURRENT_DATE`, or `CURRENT_TIMESTAMP`). + > _Note:_ Unlike the [`CREATE TABLE` constraint](#table-constraints), + > default values may not be expression structures (including + > `CURRENT_TIME`, `CURRENT_DATE`, or `CURRENT_TIMESTAMP`). - - `collate` adds a `COLLATE` clause to `Expression` (and `Expression`) column definitions with [a collating sequence](https://www.sqlite.org/datatype3.html#collation) defined in the `Collation` enumeration. + - `collate` adds a `COLLATE` clause to `Expression` (and + `Expression`) column definitions with [a collating + sequence](https://www.sqlite.org/datatype3.html#collation) defined in the + `Collation` enumeration. - ``` swift - try db.run(users.addColumn(email, collate: .Nocase)) + ```swift + try db.run(users.addColumn(email, collate: .nocase)) // ALTER TABLE "users" ADD COLUMN "email" TEXT NOT NULL COLLATE "NOCASE" - try db.run(users.addColumn(name, collate: .Rtrim)) + try db.run(users.addColumn(name, collate: .rtrim)) // ALTER TABLE "users" ADD COLUMN "name" TEXT COLLATE "RTRIM" ``` - - `references` adds a `REFERENCES` clause to `Int64` (and `Int64?`) column definitions and accepts a table or namespaced column expression. (See the `foreignKey` function under [Table Constraints](#table-constraints) for non-integer foreign key support.) + - `references` adds a `REFERENCES` clause to `Int64` (and `Int64?`) column + definitions and accepts a table or namespaced column expression. (See the + `foreignKey` function under [Table Constraints](#table-constraints) for + non-integer foreign key support.) - ``` swift + ```swift try db.run(posts.addColumn(userId, references: users, id) // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" ("id") ``` +### SchemaChanger + +Version 0.14.0 introduces `SchemaChanger`, an alternative API to perform more complex +migrations such as renaming columns. These operations work with all versions of +SQLite but use SQL statements such as `ALTER TABLE RENAME COLUMN` when available. + +#### Adding Columns + +```swift +let newColumn = ColumnDefinition( + name: "new_text_column", + type: .TEXT, + nullable: true, + defaultValue: .stringLiteral("foo") +) + +let schemaChanger = SchemaChanger(connection: db) + +try schemaChanger.alter(table: "users") { table in + table.add(column: newColumn) +} +``` + +#### Renaming Columns + +```swift +let schemaChanger = SchemaChanger(connection: db) +try schemaChanger.alter(table: "users") { table in + table.rename(column: "old_name", to: "new_name") +} +``` + +#### Dropping Columns + +```swift +let schemaChanger = SchemaChanger(connection: db) +try schemaChanger.alter(table: "users") { table in + table.drop(column: "email") +} +``` + +#### Renaming/Dropping Tables + +```swift +let schemaChanger = SchemaChanger(connection: db) + +try schemaChanger.rename(table: "users", to: "users_new") +try schemaChanger.drop(table: "emails", ifExists: false) +``` + +#### Creating Tables + +```swift +let schemaChanger = SchemaChanger(connection: db) + +try schemaChanger.create(table: "users") { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + table.add(column: .init(name: "name", type: .TEXT, nullable: false)) +} +``` ### Indexes #### Creating Indexes -We can build [`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) by calling the `createIndex` function on a `SchemaType`. +We can build +[`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) +by calling the `createIndex` function on a `SchemaType`. -``` swift +```swift try db.run(users.createIndex(email)) // CREATE INDEX "index_users_on_email" ON "users" ("email") ``` -The index name is generated automatically based on the table and column names. +The index name is generated automatically based on the table and column +names. The `createIndex` function has a couple default parameters we can override. - `unique` adds a `UNIQUE` constraint to the index. Default: `false`. - ``` swift + ```swift try db.run(users.createIndex(email, unique: true)) // CREATE UNIQUE INDEX "index_users_on_email" ON "users" ("email") ``` - - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` statement (which will bail out gracefully if the table already exists). Default: `false`. + - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` + statement (which will bail out gracefully if the table already exists). + Default: `false`. - ``` swift + ```swift try db.run(users.createIndex(email, ifNotExists: true)) // CREATE INDEX IF NOT EXISTS "index_users_on_email" ON "users" ("email") ``` @@ -1044,43 +1633,32 @@ The `createIndex` function has a couple default parameters we can override. #### Dropping Indexes -We can build [`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by calling the `dropIndex` function on a `SchemaType`. +We can build +[`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by +calling the `dropIndex` function on a `SchemaType`. -``` swift +```swift try db.run(users.dropIndex(email)) // DROP INDEX "index_users_on_email" ``` -The `dropIndex` function has one additional parameter, `ifExists`, which (when `true`) adds an `IF EXISTS` clause to the statement. +The `dropIndex` function has one additional parameter, `ifExists`, which +(when `true`) adds an `IF EXISTS` clause to the statement. -``` swift +```swift try db.run(users.dropIndex(email, ifExists: true)) // DROP INDEX IF EXISTS "index_users_on_email" ``` +### Migrations and Schema Versioning -### Dropping Tables - -We can build [`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) by calling the `dropTable` function on a `SchemaType`. - -``` swift -try db.run(users.drop()) -// DROP TABLE "users" -``` - -The `drop` function has one additional parameter, `ifExists`, which (when `true`) adds an `IF EXISTS` clause to the statement. - -``` swift -try db.run(users.drop(ifExists: true)) -// DROP TABLE IF EXISTS "users" -``` - +You can use the convenience property on `Connection` to query and set the +[`PRAGMA user_version`](https://sqlite.org/pragma.html#pragma_user_version). - +``` +For more complex migration requirements check out the schema management +system [SQLiteMigrationManager.swift][]. ## Custom Types -SQLite.swift supports serializing and deserializing any custom type as long as it conforms to the `Value` protocol. - -> ``` swift -> protocol Value { -> typealias Datatype: Binding -> class var declaredDatatype: String { get } -> class func fromDatatypeValue(datatypeValue: Datatype) -> Self -> var datatypeValue: Datatype { get } -> } -> ``` +SQLite.swift supports serializing and deserializing any custom type as long +as it conforms to the `Value` protocol. -The `Datatype` must be one of the basic Swift types that values are bridged through before serialization and deserialization (see [Building Type-Safe SQL](#building-type-safe-sql) for a list of types). +```swift +protocol Value { + typealias Datatype: Binding + class var declaredDatatype: String { get } + class func fromDatatypeValue(datatypeValue: Datatype) -> Self + var datatypeValue: Datatype { get } +} +``` -> _Note:_ `Binding` is a protocol that SQLite.swift uses internally to directly map SQLite types to Swift types. **Do _not_** conform custom types to the `Binding` protocol. +The `Datatype` must be one of the basic Swift types that values are bridged +through before serialization and deserialization (see [Building Type-Safe SQL +](#building-type-safe-sql) for a list of types). -Once extended, the type can be used [_almost_](#custom-type-caveats) wherever typed expressions can be. +> ⚠ _Note:_ `Binding` is a protocol that SQLite.swift uses internally to +> directly map SQLite types to Swift types. **Do _not_** conform custom types +> to the `Binding` protocol. ### Date-Time Values -In SQLite, `DATETIME` columns can be treated as strings or numbers, so we can transparently bridge `NSDate` objects through Swift’s `String` or `Int` types. +In SQLite, `DATETIME` columns can be treated as strings or numbers, so we can +transparently bridge `Date` objects through Swift’s `String` types. -To serialize `NSDate` objects as `TEXT` values (in ISO 8601), we’ll use `String`. +We can use these types directly in SQLite statements. -``` swift -extension NSDate: Value { - class var declaredDatatype: String { - return String.declaredDatatype - } - class func fromDatatypeValue(stringValue: String) -> NSDate { - return SQLDateFormatter.dateFromString(stringValue)! - } - var datatypeValue: String { - return SQLDateFormatter.stringFromDate(self) - } -} +```swift +let published_at = Expression("published_at") + +let published = posts.filter(published_at <= Date()) +// SELECT * FROM "posts" WHERE "published_at" <= '2014-11-18T12:45:30.000' -let SQLDateFormatter: NSDateFormatter = { - let formatter = NSDateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") - formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) - return formatter -}() +let startDate = Date(timeIntervalSince1970: 0) +let published = posts.filter(startDate...Date() ~= published_at) +// SELECT * FROM "posts" WHERE "published_at" BETWEEN '1970-01-01T00:00:00.000' AND '2014-11-18T12:45:30.000' ``` -We can also treat them as `INTEGER` values using `Int`. -``` swift -extension NSDate: Value { - class var declaredDatatype: String { - return Int.declaredDatatype +### Binary Data + +We can bridge any type that can be initialized from and encoded to `Data`. + +```swift +extension UIImage: Value { + public class var declaredDatatype: String { + return Blob.declaredDatatype } - class func fromDatatypeValue(intValue: Int) -> Self { - return self(timeIntervalSince1970: NSTimeInterval(intValue)) + public class func fromDatatypeValue(blobValue: Blob) -> UIImage { + return UIImage(data: Data.fromDatatypeValue(blobValue))! } - var datatypeValue: Int { - return Int(timeIntervalSince1970) + public var datatypeValue: Blob { + return UIImagePNGRepresentation(self)!.datatypeValue } + } ``` -> _Note:_ SQLite’s `CURRENT_DATE`, `CURRENT_TIME`, and `CURRENT_TIMESTAMP` helpers return `TEXT` values. Because of this (and the fact that Unix time is far less human-readable when we’re faced with the raw data), we recommend using the `TEXT` extension. +> _Note:_ See the [Archives and Serializations Programming Guide][] for more +> information on encoding and decoding custom types. -Once defined, we can use these types directly in SQLite statements. -``` swift -let published_at = Expression("published_at") +[Archives and Serializations Programming Guide]: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html -let published = posts.filter(published_at <= NSDate()) -// extension where Datatype == String: -// SELECT * FROM "posts" WHERE "published_at" <= '2014-11-18 12:45:30' -// extension where Datatype == Int: -// SELECT * FROM "posts" WHERE "published_at" <= 1416314730 -``` +## Codable Types +[Codable types][Encoding and Decoding Custom Types] were introduced as a part +of Swift 4 to allow serializing and deserializing types. SQLite.swift supports +the insertion, updating, and retrieval of basic Codable types. -### Binary Data +[Encoding and Decoding Custom Types]: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types -Any object that can be encoded and decoded can be stored as a blob of data in SQL. +### Inserting Codable Types -We can create an `NSData` bridge rather trivially. +Queries have a method to allow inserting an [Encodable][] type. -``` swift -extension NSData: Value { - class var declaredDatatype: String { - return Blob.declaredDatatype - } - class func fromDatatypeValue(blobValue: Blob) -> Self { - return self(bytes: blobValue.bytes, length: blobValue.length) - } - var datatypeValue: Blob { - return Blob(bytes: bytes, length: length) - } +```swift +struct User: Encodable { + let name: String } +try db.run(users.insert(User(name: "test"))) + ``` -We can bridge any type that can be initialized from and encoded to `NSData`. +There are two other parameters also available to this method: -``` swift -// assumes NSData conformance, above -extension UIImage: Value { - public class var declaredDatatype: String { - return Blob.declaredDatatype - } - public class func fromDatatypeValue(blobValue: Blob) -> UIImage { - return UIImage(data: NSData.fromDatatypeValue(blobValue))! - } - public var datatypeValue: Blob { - return UIImagePNGRepresentation(self)!.datatypeValue - } +- `userInfo` is a dictionary that is passed to the encoder and made available + to encodable types to allow customizing their behavior. + +- `otherSetters` allows you to specify additional setters on top of those + that are generated from the encodable types themselves. + +[Encodable]: https://developer.apple.com/documentation/swift/encodable + +### Updating Codable Types + +Queries have a method to allow updating an Encodable type. + +```swift +try db.run(users.filter(id == userId).update(user)) -} ``` -> _Note:_ See the [Archives and Serializations Programming Guide](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html#//apple_ref/doc/uid/10000047i) for more information on encoding and decoding custom types. +> ⚠ Unless filtered, using the update method on an instance of a Codable +> type updates all table rows. +There are two other parameters also available to this method: -### Custom Type Caveats +- `userInfo` is a dictionary that is passed to the encoder and made available + to encodable types to allow customizing their behavior. -Swift does _not_ currently support generic subscripting, which means we cannot, by default, subscript Expressions with custom types to: +- `otherSetters` allows you to specify additional setters on top of those + that are generated from the encodable types themselves. - 1. **Namespace expressions**. Use the `namespace` function, instead: +### Retrieving Codable Types - ``` swift - let avatar = Expression("avatar") - users[avatar] // fails to compile - users.namespace(avatar) // "users"."avatar" - ``` +Rows have a method to decode a [Decodable][] type. - 2. **Access column data**. Use the `get` function, instead: +```swift +let loadedUsers: [User] = try db.prepare(users).map { row in + return try row.decode() +} +``` - ``` swift - let user = users.first! - user[avatar] // fails to compile - user.get(avatar) // UIImage? - ``` +You can also create a decoder to use manually yourself. This can be useful +for example if you are using the +[Facade pattern](https://en.wikipedia.org/wiki/Facade_pattern) to hide +subclasses behind a super class. For example, you may want to encode an Image +type that can be multiple different formats such as PNGImage, JPGImage, or +HEIFImage. You will need to determine the correct subclass before you know +which type to decode. -We can, of course, write extensions, but they’re rather wordy. +```swift +enum ImageCodingKeys: String, CodingKey { + case kind +} -``` swift -extension Query { - subscript(column: Expression) -> Expression { - return namespace(column) - } - subscript(column: Expression) -> Expression { - return namespace(column) - } +enum ImageKind: Int, Codable { + case png, jpg, heif } -extension Row { - subscript(column: Expression) -> UIImage { - return get(column) - } - subscript(column: Expression) -> UIImage? { - return get(column) +let loadedImages: [Image] = try db.prepare(images).map { row in + let decoder = row.decoder() + let container = try decoder.container(keyedBy: ImageCodingKeys.self) + switch try container.decode(ImageKind.self, forKey: .kind) { + case .png: + return try PNGImage(from: decoder) + case .jpg: + return try JPGImage(from: decoder) + case .heif: + return try HEIFImage(from: decoder) } } ``` +Both of the above methods also have the following optional parameter: + +- `userInfo` is a dictionary that is passed to the decoder and made available + to decodable types to allow customizing their behavior. + +[Decodable]: https://developer.apple.com/documentation/swift/decodable + +### Restrictions + +There are a few restrictions on using Codable types: + +- The encodable and decodable objects can only use the following types: + - Int, Bool, Float, Double, String, Date + - Nested Codable types that will be encoded as JSON to a single column +- These methods will not handle object relationships for you. You must write + your own Codable and Decodable implementations if you wish to support this. +- The Codable types may not try to access nested containers or nested unkeyed + containers +- The Codable types may not access single value containers or unkeyed + containers +- The Codable types may not access super decoders or encoders ## Other Operators -In addition to [filter operators](#filtering-infix-operators), SQLite.swift defines a number of operators that can modify expression values with arithmetic, bitwise operations, and concatenation. +In addition to [filter operators](#filtering-infix-operators), SQLite.swift +defines a number of operators that can modify expression values with +arithmetic, bitwise operations, and concatenation. ###### Other Infix Operators @@ -1272,10 +1871,11 @@ In addition to [filter operators](#filtering-infix-operators), SQLite.swift defi | `<<` | `Int -> Int` | `<<` | | `>>` | `Int -> Int` | `>>` | | `&` | `Int -> Int` | `&` | -| `|` | `Int -> Int` | `|` | -| `+` | `String -> String` | `||` | +| `\|` | `Int -> Int` | `\|` | +| `+` | `String -> String` | `\|\|` | -> _Note:_ SQLite.swift also defines a bitwise XOR operator, `^`, which expands the expression `lhs ^ rhs` to `~(lhs & rhs) & (lhs | rhs)`. +> _Note:_ SQLite.swift also defines a bitwise XOR operator, `^`, which +> expands the expression `lhs ^ rhs` to `~(lhs & rhs) & (lhs | rhs)`. ###### Other Prefix Operators @@ -1288,53 +1888,81 @@ In addition to [filter operators](#filtering-infix-operators), SQLite.swift defi ## Core SQLite Functions -Many of SQLite’s [core functions](https://www.sqlite.org/lang_corefunc.html) have been surfaced in and type-audited for SQLite.swift. +Many of SQLite’s [core functions](https://www.sqlite.org/lang_corefunc.html) +have been surfaced in and type-audited for SQLite.swift. > _Note:_ SQLite.swift aliases the `??` operator to the `ifnull` function. > -> ``` swift +> ```swift > name ?? email // ifnull("name", "email") > ``` ## Aggregate SQLite Functions -Most of SQLite’s [aggregate functions](https://www.sqlite.org/lang_aggfunc.html) have been surfaced in and type-audited for SQLite.swift. +Most of SQLite’s +[aggregate functions](https://www.sqlite.org/lang_aggfunc.html) have been +surfaced in and type-audited for SQLite.swift. + +## Window SQLite Functions + +Most of SQLite's [window functions](https://www.sqlite.org/windowfunctions.html) have been +surfaced in and type-audited for SQLite.swift. Currently only `OVER (ORDER BY ...)` windowing is possible. + +## Date and Time functions +SQLite's [date and time](https://www.sqlite.org/lang_datefunc.html) +functions are available: + +```swift +DateFunctions.date("now") +// date('now') +Date().date +// date('2007-01-09T09:41:00.000') +Expression("date").date +// date("date") +``` ## Custom SQL Functions -We can create custom SQL functions by calling `createFunction` on a database connection. +We can create custom SQL functions by calling `createFunction` on a database +connection. -For example, to give queries access to [`MobileCoreServices.UTTypeConformsTo`](https://developer.apple.com/library/ios/documentation/MobileCoreServices/Reference/UTTypeRef/index.html#//apple_ref/c/func/UTTypeConformsTo), we can write the following: +For example, to give queries access to +[`MobileCoreServices.UTTypeConformsTo`][UTTypeConformsTo], we can +write the following: -``` swift +```swift import MobileCoreServices -let typeConformsTo: (Expression, String) -> Expression = ( +let typeConformsTo: (Expression, Expression) -> Expression = ( try db.createFunction("typeConformsTo", deterministic: true) { UTI, conformsToUTI in return UTTypeConformsTo(UTI, conformsToUTI) } ) ``` -> _Note:_ The optional `deterministic` parameter is an optimization that causes the function to be created with [`SQLITE_DETERMINISTIC`](https://www.sqlite.org/c3ref/create_function.html). +> _Note:_ The optional `deterministic` parameter is an optimization that +> causes the function to be created with +> [`SQLITE_DETERMINISTIC`](https://www.sqlite.org/c3ref/c_deterministic.html). Note `typeConformsTo`’s signature: -``` swift -(Expression, String) -> Expression +```swift +(Expression, Expression) -> Expression ``` -Because of this, `createFunction` expects a block with the following signature: +Because of this, `createFunction` expects a block with the following +signature: -``` swift +```swift (String, String) -> Bool ``` -Once assigned, the closure can be called wherever boolean expressions are accepted. +Once assigned, the closure can be called wherever boolean expressions are +accepted. -``` swift +```swift let attachments = Table("attachments") let UTI = Expression("UTI") @@ -1342,48 +1970,77 @@ let images = attachments.filter(typeConformsTo(UTI, kUTTypeImage)) // SELECT * FROM "attachments" WHERE "typeConformsTo"("UTI", 'public.image') ``` -> _Note:_ The return type of a function must be [a core SQL type](#building-type-safe-sql) or [conform to `Value`](#custom-types). +> _Note:_ The return type of a function must be +> [a core SQL type](#building-type-safe-sql) or [conform to `Value`](#custom-types). -We can create loosely-typed functions by handling an array of raw arguments, instead. +We can create loosely-typed functions by handling an array of raw arguments, +instead. -``` swift +```swift db.createFunction("typeConformsTo", deterministic: true) { args in guard let UTI = args[0] as? String, conformsToUTI = args[1] as? String else { return nil } return UTTypeConformsTo(UTI, conformsToUTI) } ``` -Creating a loosely-typed function cannot return a closure and instead must be wrapped manually or executed [using raw SQL](#executing-arbitrary-sql). +Creating a loosely-typed function cannot return a closure and instead must be +wrapped manually or executed [using raw SQL](#executing-arbitrary-sql). -``` swift +```swift let stmt = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)") for row in stmt.bind(kUTTypeImage) { /* ... */ } ``` +> _Note:_ Prepared queries can be reused, and long lived prepared queries should be `reset()` after each use. Otherwise, the transaction (either [implicit or explicit](https://www.sqlite.org/lang_transaction.html#implicit_versus_explicit_transactions)) will be held open until the query is reset or finalized. This can affect performance. Statements are reset automatically during `deinit`. +> +> ```swift +> someObj.statement = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)") +> for row in someObj.statement.bind(kUTTypeImage) { /* ... */ } +> someObj.statement.reset() +> ``` + +[UTTypeConformsTo]: https://developer.apple.com/documentation/coreservices/1444079-uttypeconformsto + +## Custom Aggregations + +We can create custom aggregation functions by calling `createAggregation`: + +```swift +let reduce: (String, [Binding?]) -> String = { (last, bindings) in + last + " " + (bindings.first as? String ?? "") +} + +db.createAggregation("customConcat", initialValue: "", reduce: reduce, result: { $0 }) +let result = db.prepare("SELECT customConcat(email) FROM users").scalar() as! String +``` ## Custom Collations -We can create custom collating sequences by calling `createCollation` on a database connection. +We can create custom collating sequences by calling `createCollation` on a +database connection. -``` swift +```swift try db.createCollation("NODIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) + return lhs.compare(rhs, options: .diacriticInsensitiveSearch) } ``` -We can reference a custom collation using the `Custom` member of the `Collation` enumeration. +We can reference a custom collation using the `Custom` member of the +`Collation` enumeration. -``` swift -restaurants.order(collate(.Custom("NODIACRITIC"), name)) +```swift +restaurants.order(collate(.custom("NODIACRITIC"), name)) // SELECT * FROM "restaurants" ORDER BY "name" COLLATE "NODIACRITIC" ``` ## Full-text Search -We can create a virtual table using the [FTS4 module](http://www.sqlite.org/fts3.html) by calling `create` on a `VirtualTable`. +We can create a virtual table using the [FTS4 +module](http://www.sqlite.org/fts3.html) by calling `create` on a +`VirtualTable`. -``` swift +```swift let emails = VirtualTable("emails") let subject = Expression("subject") let body = Expression("body") @@ -1394,14 +2051,14 @@ try db.run(emails.create(.FTS4(subject, body))) We can specify a [tokenizer](http://www.sqlite.org/fts3.html#tokenizer) using the `tokenize` parameter. -``` swift +```swift try db.run(emails.create(.FTS4([subject, body], tokenize: .Porter))) // CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", tokenize=porter) ``` We can set the full range of parameters by creating a `FTS4Config` object. -``` swift +```swift let emails = VirtualTable("emails") let subject = Expression("subject") let body = Expression("body") @@ -1409,21 +2066,23 @@ let config = FTS4Config() .column(subject) .column(body, [.unindexed]) .languageId("lid") - .order(.Desc) + .order(.desc) try db.run(emails.create(.FTS4(config)) // CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", notindexed="body", languageid="lid", order="desc") ``` -Once we insert a few rows, we can search using the `match` function, which takes a table or column as its first argument and a query string as its second. +Once we insert a few rows, we can search using the `match` function, which +takes a table or column as its first argument and a query string as its +second. -``` swift +```swift try db.run(emails.insert( subject <- "Just Checking In", body <- "Hey, I was just wondering...did you get my last email?" )) -let wonderfulEmails = emails.match("wonder*") +let wonderfulEmails: QueryType = emails.match("wonder*") // SELECT * FROM "emails" WHERE "emails" MATCH 'wonder*' let replies = emails.filter(subject.match("Re:*")) @@ -1432,8 +2091,9 @@ let replies = emails.filter(subject.match("Re:*")) ### FTS5 -When linking against a version of SQLite with [FTS5](http://www.sqlite.org/fts5.html) enabled we can create the virtual table -in a similar fashion. +When linking against a version of SQLite with +[FTS5](http://www.sqlite.org/fts5.html) enabled we can create the virtual +table in a similar fashion. ```swift let emails = VirtualTable("emails") @@ -1443,89 +2103,155 @@ let config = FTS5Config() .column(subject) .column(body, [.unindexed]) -try db.run(emails.create(.FTS5(config)) +try db.run(emails.create(.FTS5(config))) // CREATE VIRTUAL TABLE "emails" USING fts5("subject", "body" UNINDEXED) + +// Note that FTS5 uses a different syntax to select columns, so we need to rewrite +// the last FTS4 query above as: +let replies = emails.filter(emails.match("subject:\"Re:\"*")) +// SELECT * FROM "emails" WHERE "emails" MATCH 'subject:"Re:"*' ``` ## Executing Arbitrary SQL -Though we recommend you stick with SQLite.swift’s [type-safe system](#building-type-safe-sql) whenever possible, it is possible to simply and safely prepare and execute raw SQL statements via a `Database` connection using the following functions. +Though we recommend you stick with SQLite.swift’s +[type-safe system](#building-type-safe-sql) whenever possible, it is possible +to simply and safely prepare and execute raw SQL statements via a `Database` connection +using the following functions. - `execute` runs an arbitrary number of SQL statements as a convenience. - ``` swift - try db.execute( - "BEGIN TRANSACTION;" + - "CREATE TABLE users (" + - "id INTEGER PRIMARY KEY NOT NULL," + - "email TEXT UNIQUE NOT NULL," + - "name TEXT" + - ");" + - "CREATE TABLE posts (" + - "id INTEGER PRIMARY KEY NOT NULL," + - "title TEXT NOT NULL," + - "body TEXT NOT NULL," + - "published_at DATETIME" + - ");" + - "PRAGMA user_version = 1;" + - "COMMIT TRANSACTION;" + ```swift + try db.execute(""" + BEGIN TRANSACTION; + CREATE TABLE users ( + id INTEGER PRIMARY KEY NOT NULL, + email TEXT UNIQUE NOT NULL, + name TEXT + ); + CREATE TABLE posts ( + id INTEGER PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + published_at DATETIME + ); + PRAGMA user_version = 1; + COMMIT TRANSACTION; + """ ) ``` - - `prepare` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), and returns the statement for deferred execution. + - `prepare` prepares a single `Statement` object from a SQL string, + optionally binds values to it (using the statement’s `bind` function), + and returns the statement for deferred execution. - ``` swift + ```swift let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") ``` - Once prepared, statements may be executed using `run`, binding any unbound parameters. + Once prepared, statements may be executed using `run`, binding any + unbound parameters. - ``` swift + ```swift try stmt.run("alice@mac.com") db.changes // -> {Some 1} ``` - Statements with results may be iterated over, using the columnNames if useful. + Statements with results may be iterated over, using the columnNames if + useful. - ``` swift + ```swift let stmt = try db.prepare("SELECT id, email FROM users") for row in stmt { - for (index, name) in stmt.columnNames.enumerate() { - print ("\(name)=\(row[index]!)") + for (index, name) in stmt.columnNames.enumerated() { + print ("\(name):\(row[index]!)") // id: Optional(1), email: Optional("alice@mac.com") } } ``` - - `run` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), executes, and returns the statement. + - `run` prepares a single `Statement` object from a SQL string, optionally + binds values to it (using the statement’s `bind` function), executes, + and returns the statement. - ``` swift + ```swift try db.run("INSERT INTO users (email) VALUES (?)", "alice@mac.com") ``` - - `scalar` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), executes, and returns the first value of the first row. + - `scalar` prepares a single `Statement` object from a SQL string, + optionally binds values to it (using the statement’s `bind` function), + executes, and returns the first value of the first row. - ``` swift + ```swift let count = try db.scalar("SELECT count(*) FROM users") as! Int64 ``` - Statements also have a `scalar` function, which can optionally re-bind values at execution. + Statements also have a `scalar` function, which can optionally re-bind + values at execution. - ``` swift + ```swift let stmt = try db.prepare("SELECT count (*) FROM users") let count = try stmt.scalar() as! Int64 ``` +## Online Database Backup + +To copy a database to another using the +[SQLite Online Backup API](https://sqlite.org/backup.html): + +```swift +// creates an in-memory copy of db.sqlite +let db = try Connection("db.sqlite") +let target = try Connection(.inMemory) + +let backup = try db.backup(usingConnection: target) +try backup.step() +``` + +## Attaching and detaching databases + +We can [ATTACH](https://www3.sqlite.org/lang_attach.html) and [DETACH](https://www3.sqlite.org/lang_detach.html) +databases to an existing connection: + +```swift +let db = try Connection("db.sqlite") + +try db.attach(.uri("external.sqlite", parameters: [.mode(.readOnly)]), as: "external") +// ATTACH DATABASE 'file:external.sqlite?mode=ro' AS 'external' + +let table = Table("table", database: "external") +let count = try db.scalar(table.count) +// SELECT count(*) FROM 'external.table' + +try db.detach("external") +// DETACH DATABASE 'external' +``` + +When compiled for SQLCipher, we can additionally pass a `key` parameter to `attach`: + +```swift +try db.attach(.uri("encrypted.sqlite"), as: "encrypted", key: "secret") +// ATTACH DATABASE 'encrypted.sqlite' AS 'encrypted' KEY 'secret' +``` ## Logging We can log SQL using the database’s `trace` function. -``` swift +```swift #if DEBUG - db.trace(print) + db.trace { print($0) } #endif ``` +## Vacuum + +To run the [vacuum](https://www.sqlite.org/lang_vacuum.html) command: + +```swift +try db.vacuum() +``` + [ROWID]: https://sqlite.org/lang_createtable.html#rowid +[SQLiteMigrationManager.swift]: https://github.com/garriguv/SQLiteMigrationManager.swift diff --git a/Documentation/Linux.md b/Documentation/Linux.md new file mode 100644 index 00000000..0c88ff5c --- /dev/null +++ b/Documentation/Linux.md @@ -0,0 +1,29 @@ +# Linux + +## Limitations + +* Custom functions/aggregations are currently not supported and crash, caused by a bug in Swift. +See [#1071](https://github.com/stephencelis/SQLite.swift/issues/1071). + +## Debugging + +### Create and launch docker container + +```shell +$ docker container create swift:focal +$ docker run --cap-add=SYS_PTRACE \ + --security-opt seccomp=unconfined \ + --security-opt apparmor=unconfined \ + -i -t swift:focal bash +``` + +### Compile and run tests in debugger + +```shell +$ apt-get update && apt-get install libsqlite3-dev +$ git clone https://github.com/stephencelis/SQLite.swift.git +$ swift test +$ lldb .build/x86_64-unknown-linux-gnu/debug/SQLite.swiftPackageTests.xctest +(lldb) target create ".build/x86_64-unknown-linux-gnu/debug/SQLite.swiftPackageTests.xctest" +(lldb) run +``` diff --git a/Documentation/Planning.md b/Documentation/Planning.md index 12555703..cdfca9c4 100644 --- a/Documentation/Planning.md +++ b/Documentation/Planning.md @@ -1,34 +1,33 @@ # SQLite.swift Planning -This document captures both near term steps (aka Roadmap) and feature requests. -The goal is to add some visibility and guidance for future additions and Pull Requests, as well as to keep the Issues list clear of enhancement requests so that bugs are more visible. +This document captures both near term steps (aka Roadmap) and feature +requests. The goal is to add some visibility and guidance for future +additions and Pull Requests, as well as to keep the Issues list clear of +enhancement requests so that bugs are more visible. + +> ⚠ This document is currently not actively maintained. See +> the [0.14.1 milestone](https://github.com/stephencelis/SQLite.swift/issues?q=is%3Aopen+is%3Aissue+milestone%3A0.14.1) +> on Github for additional information about planned features for the next release. ## Roadmap _Lists agreed upon next steps in approximate priority order._ - * ~~publish to the CocoaPods directory at https://cocoapods.org/, per [#257](https://github.com/stephencelis/SQLite.swift/issues/257)~~ _jan 7, 2016_) - * add SQLCipher back into the product as a separate repo, per [#311](https://github.com/stephencelis/SQLite.swift/issues/311), _in progress jan 6, 2016_ - - ## Feature Requests -_A gathering point for ideas for new features. In general, the corresponding issue will be closed once it is added here, with the assumption that it will be referred to when it comes time to add the corresponding feature._ - -### Packaging - - * add TV OS support, per [#272](https://github.com/stephencelis/SQLite.swift/issues/272) - currently pending SQLiteCipher merge and/or adding ability for user to pick their preferred [SQLCipher](https://github.com/sqlcipher/sqlcipher) branch - * linux support via Swift Package Manager, per [#315](https://github.com/stephencelis/SQLite.swift/issues/315) +_A gathering point for ideas for new features. In general, the corresponding +issue will be closed once it is added here, with the assumption that it will +be referred to when it comes time to add the corresponding feature._ ### Features - * encapsulate ATTACH DATABASE / DETACH DATABASE as methods, per [#30](https://github.com/stephencelis/SQLite.swift/issues/30) - * provide separate threads for update vs read, so updates don't block reads, per [#236](https://github.com/stephencelis/SQLite.swift/issues/236) - * expose more FTS4 options, e.g. virtual table support per [#164](https://github.com/stephencelis/SQLite.swift/issues/164) - * expose triggers, per [#164](https://github.com/stephencelis/SQLite.swift/issues/164) + * provide separate threads for update vs read, so updates don't block reads, + per [#236](https://github.com/stephencelis/SQLite.swift/issues/236) + * expose triggers, per + [#164](https://github.com/stephencelis/SQLite.swift/issues/164) ## Suspended Feature Requests -_Features that are not actively being considered, perhaps because of no clean type-safe way to implement them with the current Swift, or bugs, or just general uncertainty._ - - * provide a mechanism for INSERT INTO multiple values, per [#168](https://github.com/stephencelis/SQLite.swift/issues/168) +_Features that are not actively being considered, perhaps because of no clean +type-safe way to implement them with the current Swift, or bugs, or just +general uncertainty._ diff --git a/Documentation/Release.md b/Documentation/Release.md new file mode 100644 index 00000000..8ec67970 --- /dev/null +++ b/Documentation/Release.md @@ -0,0 +1,13 @@ +# SQLite.swift Release checklist + +* [ ] Make sure current master branch has a green build +* [ ] Make sure `SQLite.playground` runs without errors +* [ ] Make sure `CHANGELOG.md` is up-to-date +* [ ] Add content to `Documentation/Upgrading.md` if needed +* [ ] Update the version number in `SQLite.swift.podspec` +* [ ] Run `pod lib lint` locally +* [ ] Update the version numbers mentioned in `README.md`, `Documentation/Index.md` +* [ ] Update `MARKETING_VERSION` in `SQLite.xcodeproj/project.pbxproj` +* [ ] Create a tag with the version number (`x.y.z`) +* [ ] Publish to CocoaPods: `pod trunk push` +* [ ] Update the release information on GitHub diff --git a/Documentation/Resources/playground@2x.png b/Documentation/Resources/playground@2x.png index 32646d6a..da132718 100644 Binary files a/Documentation/Resources/playground@2x.png and b/Documentation/Resources/playground@2x.png differ diff --git a/Documentation/Upgrading.md b/Documentation/Upgrading.md new file mode 100644 index 00000000..0e12aacf --- /dev/null +++ b/Documentation/Upgrading.md @@ -0,0 +1,11 @@ +# Upgrading + +## 0.13 → 0.14 + +- `Expression.asSQL()` is no longer available. Expressions now implement `CustomStringConvertible`, + where `description` returns the SQL. +- `Statement.prepareRowIterator()` is no longer available. Instead, use the methods + of the same name on `Connection`. +- `Connection.registerTokenizer` is no longer available to register custom FTS4 tokenizers. +- `Setter.asSQL()` is no longer available. Instead, Setter now implement `CustomStringConvertible`, + where `description` returns the SQL. diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..2770e85d --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# https://github.com/CocoaPods/CocoaPods/pull/12816 +gem 'cocoapods', :git => 'https://github.com/jberkel/CocoaPods.git', branch: 'watchos-fourflusher' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..7dac2c18 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,122 @@ +GIT + remote: https://github.com/jberkel/CocoaPods.git + revision: 32a90c184bc5dc9ec8b7b9b8ad08e98b7253dec2 + branch: watchos-fourflusher + specs: + cocoapods (1.16.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.16.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (~> 4.1.0) + xcodeproj (>= 1.27.0, < 2.0) + +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.2.2.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) + claide (1.1.0) + cocoapods-core (1.16.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + drb (2.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.12.0) + logger (1.7.0) + minitest (5.25.5) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + netrc (0.11.0) + nkf (0.2.0) + public_suffix (4.0.7) + rexml (3.4.1) + ruby-macho (4.1.0) + securerandom (0.4.1) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + cocoapods! + +BUNDLED WITH + 2.6.9 diff --git a/Makefile b/Makefile index 7257b9b1..52a25a12 100644 --- a/Makefile +++ b/Makefile @@ -1,57 +1,76 @@ -BUILD_TOOL = xcodebuild +XCODEBUILD = xcodebuild BUILD_SCHEME = SQLite Mac +IOS_SIMULATOR = iPhone 14 +IOS_VERSION = 16.4 + +# tool settings +SWIFTLINT_VERSION=0.52.2 +SWIFTLINT=bin/swiftlint-$(SWIFTLINT_VERSION) +SWIFTLINT_URL=https://github.com/realm/SwiftLint/releases/download/$(SWIFTLINT_VERSION)/portable_swiftlint.zip +XCBEAUTIFY_VERSION=0.20.0 +XCBEAUTIFY=bin/xcbeautify-$(XCBEAUTIFY_VERSION) +ifeq ($(shell uname), Linux) + XCBEAUTIFY_PLATFORM=x86_64-unknown-linux-gnu.tar.xz +else + XCBEAUTIFY_PLATFORM=universal-apple-macosx.zip +endif +XCBEAUTIFY_URL=https://github.com/tuist/xcbeautify/releases/download/$(XCBEAUTIFY_VERSION)/xcbeautify-$(XCBEAUTIFY_VERSION)-$(XCBEAUTIFY_PLATFORM) +CURL_OPTS=--fail --silent -L --retry 3 + ifeq ($(BUILD_SCHEME),SQLite iOS) - BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -sdk iphonesimulator + BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -destination "platform=iOS Simulator,name=$(IOS_SIMULATOR),OS=$(IOS_VERSION)" else BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" endif -XCPRETTY := $(shell command -v xcpretty) -SWIFTCOV := $(shell command -v swiftcov) -GCOVR := $(shell command -v gcovr) +test: $(XCBEAUTIFY) + set -o pipefail; \ + $(XCODEBUILD) $(BUILD_ARGUMENTS) test | $(XCBEAUTIFY) -default: test +build: $(XCBEAUTIFY) + set -o pipefail; \ + $(XCODEBUILD) $(BUILD_ARGUMENTS) | $(XCBEAUTIFY) -build: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) +lint: $(SWIFTLINT) + $< --strict -test: -ifdef XCPRETTY - @set -o pipefail && $(BUILD_TOOL) $(BUILD_ARGUMENTS) test | $(XCPRETTY) -c -else - $(BUILD_TOOL) $(BUILD_ARGUMENTS) test -endif - -coverage: -ifdef SWIFTCOV - $(SWIFTCOV) generate --output coverage \ - $(BUILD_TOOL) $(BUILD_ARGUMENTS) -configuration Release test \ - -- ./SQLite/*.swift -ifdef GCOVR - $(GCOVR) \ - --root . \ - --use-gcov-files \ - --html \ - --html-details \ - --output coverage/index.html \ - --keep -else - @echo gcovr must be installed for HTML output: https://github.com/gcovr/gcovr -endif -else - @echo swiftcov must be installed for coverage: https://github.com/realm/SwiftCov - @exit 1 -endif +lint-fix: $(SWIFTLINT) + $< --fix clean: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) clean - rm -r coverage + $(XCODEBUILD) $(BUILD_ARGUMENTS) clean repl: - @$(BUILD_TOOL) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ - swift -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' + @$(XCODEBUILD) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ + swift repl -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' sloc: - @zsh -c "grep -vE '^ *//|^$$' SQLite/*/*.{swift,h,m} | wc -l" + @zsh -c "grep -vE '^ *//|^$$' Sources/**/*.{swift,h} | wc -l" + +$(SWIFTLINT): + set -e ; \ + curl $(CURL_OPTS) $(SWIFTLINT_URL) -o swiftlint.zip; \ + unzip -o swiftlint.zip swiftlint; \ + mkdir -p bin; \ + mv swiftlint $@ && rm -f swiftlint.zip + +$(XCBEAUTIFY): + set -e; \ + FILE=$(XCBEAUTIFY_PLATFORM); \ + curl $(CURL_OPTS) $(XCBEAUTIFY_URL) -o $$FILE; \ + case "$${FILE#*.}" in \ + "zip") \ + unzip -o $$FILE xcbeautify; \ + ;; \ + "tar.xz") \ + tar -xvf $$FILE xcbeautify; \ + ;; \ + *) \ + echo "unknown extension $${FILE#*.}!"; \ + exit 1; \ + ;; \ + esac; \ + mkdir -p bin; \ + mv xcbeautify $@ && rm -f $$FILE; -.PHONY: test coverage clean repl sloc +.PHONY: test clean repl sloc diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..56925d18 --- /dev/null +++ b/Package.swift @@ -0,0 +1,60 @@ +// swift-tools-version:5.9 +import PackageDescription + +let deps: [Package.Dependency] = [ + .github("swiftlang/swift-toolchain-sqlite", exact: "1.0.4") +] + +let targets: [Target] = [ + .target( + name: "SQLite", + dependencies: [ + .product(name: "SwiftToolchainCSQLite", package: "swift-toolchain-sqlite", condition: .when(platforms: [.linux, .windows, .android])) + ], + exclude: [ + "Info.plist" + ] + ) +] + +let testTargets: [Target] = [ + .testTarget( + name: "SQLiteTests", + dependencies: [ + "SQLite" + ], + path: "Tests/SQLiteTests", + exclude: [ + "Info.plist" + ], + resources: [ + .copy("Resources") + ] + ) +] + +let package = Package( + name: "SQLite.swift", + platforms: [ + .iOS(.v12), + .macOS(.v10_13), + .watchOS(.v4), + .tvOS(.v12), + .visionOS(.v1) + ], + products: [ + .library( + name: "SQLite", + targets: ["SQLite"] + ) + ], + dependencies: deps, + targets: targets + testTargets +) + +extension Package.Dependency { + + static func github(_ repo: String, exact ver: Version) -> Package.Dependency { + .package(url: "https://github.com/\(repo)", exact: ver) + } +} diff --git a/README.md b/README.md index 850d3374..a52b7ae5 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,12 @@ -# SQLite.swift +# SQLite.swift -[![Build Status][Badge]][Travis] [![CocoaPods Version](https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png)](http://cocoadocs.org/docsets/SQLite.swift) [![Platform](https://cocoapod-badges.herokuapp.com/p/SQLite.swift/badge.png)](http://cocoadocs.org/docsets/SQLite.swift) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Join the chat at https://gitter.im/stephencelis/SQLite.swift](https://badges.gitter.im/stephencelis/SQLite.swift.svg)](https://gitter.im/stephencelis/SQLite.swift) +![Build Status][GitHubActionBadge] [![CocoaPods Version][CocoaPodsVersionBadge]][CocoaPodsVersionLink] [![Swift5 compatible][Swift5Badge]][Swift5Link] [![Platform][PlatformBadge]][PlatformLink] [![Carthage compatible][CartagheBadge]][CarthageLink] [![Join the chat at https://gitter.im/stephencelis/SQLite.swift][GitterBadge]][GitterLink] A type-safe, [Swift][]-language layer over [SQLite3][]. [SQLite.swift][] provides compile-time confidence in SQL statement syntax _and_ intent. -[Badge]: https://img.shields.io/travis/stephencelis/SQLite.swift/master.svg?style=flat -[Travis]: https://travis-ci.org/stephencelis/SQLite.swift -[Swift]: https://developer.apple.com/swift/ -[SQLite3]: http://www.sqlite.org -[SQLite.swift]: https://github.com/stephencelis/SQLite.swift - - ## Features - A pure-Swift interface @@ -25,79 +18,101 @@ syntax _and_ intent. - [Full-text search][] support - [Well-documented][See Documentation] - Extensively tested - - Companion project has [SQLCipher support](https://github.com/stephencelis/SQLiteCipher.swift) - - Active support at [StackOverflow](http://stackoverflow.com/questions/tagged/sqlite.swift), and [Gitter Chat Room](https://gitter.im/stephencelis/SQLite.swift) (_experimental_) - + - [SQLCipher][] support via CocoaPods + - [Schema query/migration][] + - Works on [Linux](Documentation/Linux.md) (with some limitations) + - Active support at + [StackOverflow](https://stackoverflow.com/questions/tagged/sqlite.swift), + and [Gitter Chat Room](https://gitter.im/stephencelis/SQLite.swift) + (_experimental_) + +[SQLCipher]: https://www.zetetic.net/sqlcipher/ [Full-text search]: Documentation/Index.md#full-text-search +[Schema query/migration]: Documentation/Index.md#querying-the-schema [See Documentation]: Documentation/Index.md#sqliteswift-documentation ## Usage -``` swift +```swift import SQLite -let db = try Connection("path/to/db.sqlite3") - -let users = Table("users") -let id = Expression("id") -let name = Expression("name") -let email = Expression("email") - -try db.run(users.create { t in - t.column(id, primaryKey: true) - t.column(name) - t.column(email, unique: true) -}) -// CREATE TABLE "users" ( -// "id" INTEGER PRIMARY KEY NOT NULL, -// "name" TEXT, -// "email" TEXT NOT NULL UNIQUE -// ) - -let insert = users.insert(name <- "Alice", email <- "alice@mac.com") -let rowid = try db.run(insert) -// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') - -for user in try db.prepare(users) { - print("id: \(user[id]), name: \(user[name]), email: \(user[email])") - // id: 1, name: Optional("Alice"), email: alice@mac.com +// Wrap everything in a do...catch to handle errors +do { + let db = try Connection("path/to/db.sqlite3") + + let users = Table("users") + let id = SQLite.Expression("id") + let name = SQLite.Expression("name") + let email = SQLite.Expression("email") + + try db.run(users.create { t in + t.column(id, primaryKey: true) + t.column(name) + t.column(email, unique: true) + }) + // CREATE TABLE "users" ( + // "id" INTEGER PRIMARY KEY NOT NULL, + // "name" TEXT, + // "email" TEXT NOT NULL UNIQUE + // ) + + let insert = users.insert(name <- "Alice", email <- "alice@mac.com") + let rowid = try db.run(insert) + // INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') + + for user in try db.prepare(users) { + print("id: \(user[id]), name: \(user[name]), email: \(user[email])") + // id: 1, name: Optional("Alice"), email: alice@mac.com + } + // SELECT * FROM "users" + + let alice = users.filter(id == rowid) + + try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) + // UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') + // WHERE ("id" = 1) + + try db.run(alice.delete()) + // DELETE FROM "users" WHERE ("id" = 1) + + try db.scalar(users.count) // 0 + // SELECT count(*) FROM "users" +} catch { + print (error) } -// SELECT * FROM "users" - -let alice = users.filter(id == rowid) - -try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) -// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') -// WHERE ("id" = 1) - -try db.run(alice.delete()) -// DELETE FROM "users" WHERE ("id" = 1) - -db.scalar(users.count) // 0 -// SELECT count(*) FROM "users" ``` +Note that `Expression` should be written as `SQLite.Expression` to avoid +conflicts with the `SwiftUI.Expression` if you are using SwiftUI too. + SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C API. -``` swift -let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") -for email in ["betty@icloud.com", "cathy@icloud.com"] { - try stmt.run(email) +```swift +// Wrap everything in a do...catch to handle errors +do { + // ... + + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") + for email in ["betty@icloud.com", "cathy@icloud.com"] { + try stmt.run(email) + } + + db.totalChanges // 3 + db.changes // 1 + db.lastInsertRowid // 3 + + for row in try db.prepare("SELECT id, email FROM users") { + print("id: \(row[0]), email: \(row[1])") + // id: Optional(2), email: Optional("betty@icloud.com") + // id: Optional(3), email: Optional("cathy@icloud.com") + } + + try db.scalar("SELECT count(*) FROM users") // 2 +} catch { + print (error) } - -db.totalChanges // 3 -db.changes // 1 -db.lastInsertRowid // 3 - -for row in try db.prepare("SELECT id, email FROM users") { - print("id: \(row[0]), email: \(row[1])") - // id: Optional(2), email: Optional("betty@icloud.com") - // id: Optional(3), email: Optional("cathy@icloud.com") -} - -db.scalar("SELECT count(*) FROM users") // 2 ``` [Read the documentation][See Documentation] or explore more, @@ -105,17 +120,30 @@ interactively, from the Xcode project’s playground. ![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) -For a more comprehensive example, see [this article](http://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html) and the [companion repository](https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master). - ## Installation -> _Note:_ SQLite.swift requires Swift 2 (and [Xcode][] 7) or greater. -> -> The following instructions apply to targets that support embedded -> Swift frameworks. To use SQLite.swift in iOS 7 or an OS X command line -> tool, please read the [Frameworkless Targets][] section of the -> documentation. +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. + +1. Add the following to your `Package.swift` file: + + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ] + ``` + +2. Build your project: + + ```sh + $ swift build + ``` +See the [Tests/SPM](https://github.com/stephencelis/SQLite.swift/tree/master/Tests/SPM) folder for a small demo project which uses SPM. + +[Swift Package Manager]: https://swift.org/package-manager ### Carthage @@ -126,11 +154,12 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: - ``` - github "stephencelis/SQLite.swift" ~> 0.10.1 + ```ruby + github "stephencelis/SQLite.swift" ~> 0.15.4 ``` - 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. + 3. Run `carthage update` and + [add the appropriate framework][Carthage Usage]. [Carthage]: https://github.com/Carthage/Carthage @@ -143,29 +172,29 @@ install SQLite.swift with Carthage: [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: - 1. Make sure the latest CocoaPods beta is [installed][CocoaPods - Installation]. (SQLite.swift requires version 1.0.0.beta.6 or greater.) + 1. Make sure CocoaPods is [installed][CocoaPods Installation]. - ``` sh + ```sh # Using the default Ruby install will require you to use sudo when # installing and updating gems. - sudo gem install --pre cocoapods + [sudo] gem install cocoapods ``` 2. Update your Podfile to include the following: - ``` ruby + ```ruby use_frameworks! - pod 'SQLite.swift', '~> 0.10.1' + target 'YourAppTargetName' do + pod 'SQLite.swift', '~> 0.15.0' + end ``` - 3. Run `pod install`. + 3. Run `pod install --repo-update`. [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started - ### Manual To install SQLite.swift as an Xcode sub-project: @@ -182,16 +211,24 @@ To install SQLite.swift as an Xcode sub-project: 4. **Add**. -[Frameworkless Targets]: Documentation/Index.md#frameworkless-targets +Some additional steps are required to install the application on an actual +device: + + 5. In the **General** tab, click the **+** button under **Embedded + Binaries**. + + 6. Select the appropriate **SQLite.framework** for your platform. + + 7. **Add**. + + [Xcode]: https://developer.apple.com/xcode/downloads/ -[Submodule]: http://git-scm.com/book/en/Git-Tools-Submodules +[Submodule]: https://git-scm.com/book/en/Git-Tools-Submodules [download]: https://github.com/stephencelis/SQLite.swift/archive/master.zip ## Communication -[See the planning document] for a roadmap and existing feature requests. - [Read the contributing guidelines][]. The _TL;DR_ (but please; _R_): - Need **help** or have a **general question**? [Ask on Stack @@ -199,14 +236,13 @@ To install SQLite.swift as an Xcode sub-project: - Found a **bug** or have a **feature request**? [Open an issue][]. - Want to **contribute**? [Submit a pull request][]. -[See the planning document]: /Documentation/Planning.md [Read the contributing guidelines]: ./CONTRIBUTING.md#contributing -[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Ask on Stack Overflow]: https://stackoverflow.com/questions/tagged/sqlite.swift [Open an issue]: https://github.com/stephencelis/SQLite.swift/issues/new [Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork -## Author +## Original author - [Stephen Celis](mailto:stephen@stephencelis.com) ([@stephencelis](https://twitter.com/stephencelis)) @@ -221,19 +257,37 @@ file](./LICENSE.txt) for more information. These projects enhance or use SQLite.swift: - - [SQLiteCipher.swift](https://github.com/stephencelis/SQLiteCipher.swift) - - [SQLiteMigrationManager.swift](https://github.com/garriguv/SQLiteMigrationManager.swift) (inspired by [FMDBMigrationManager](https://github.com/layerhq/FMDBMigrationManager)) - + - [SQLiteMigrationManager.swift][] (inspired by + [FMDBMigrationManager][]) ## Alternatives Looking for something else? Try another Swift wrapper (or [FMDB][]): - - [Camembert](https://github.com/remirobert/Camembert) - [GRDB](https://github.com/groue/GRDB.swift) - [SQLiteDB](https://github.com/FahimF/SQLiteDB) - - [Squeal](https://github.com/nerdyc/Squeal) - - [SwiftData](https://github.com/ryanfowler/SwiftData) - - [SwiftSQLite](https://github.com/chrismsimpson/SwiftSQLite) +[Swift]: https://swift.org/ +[SQLite3]: https://www.sqlite.org +[SQLite.swift]: https://github.com/stephencelis/SQLite.swift + +[GitHubActionBadge]: https://img.shields.io/github/actions/workflow/status/stephencelis/SQLite.swift/build.yml?branch=master + +[CocoaPodsVersionBadge]: https://img.shields.io/cocoapods/v/SQLite.swift.svg?style=flat +[CocoaPodsVersionLink]: https://cocoapods.org/pods/SQLite.swift + +[PlatformBadge]: https://img.shields.io/cocoapods/p/SQLite.swift.svg?style=flat +[PlatformLink]: https://cocoapods.org/pods/SQLite.swift + +[CartagheBadge]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat +[CarthageLink]: https://github.com/Carthage/Carthage + +[GitterBadge]: https://badges.gitter.im/stephencelis/SQLite.swift.svg +[GitterLink]: https://gitter.im/stephencelis/SQLite.swift + +[Swift5Badge]: https://img.shields.io/badge/swift-5-orange.svg?style=flat +[Swift5Link]: https://developer.apple.com/swift/ + +[SQLiteMigrationManager.swift]: https://github.com/garriguv/SQLiteMigrationManager.swift [FMDB]: https://github.com/ccgus/fmdb +[FMDBMigrationManager]: https://github.com/layerhq/FMDBMigrationManager diff --git a/SQLite.playground/Contents.swift b/SQLite.playground/Contents.swift index 2015531a..d893e696 100644 --- a/SQLite.playground/Contents.swift +++ b/SQLite.playground/Contents.swift @@ -1,43 +1,132 @@ import SQLite -let db = try! Connection() +/// Create an in-memory database +let db = try Connection(.inMemory) +/// enable statement logging db.trace { print($0) } +/// define a "users" table with some fields let users = Table("users") let id = Expression("id") -let email = Expression("email") -let name = Expression("name") +let email = Expression("email") // non-null +let name = Expression("name") // nullable -try! db.run(users.create { t in +/// prepare the query +let statement = users.create { t in t.column(id, primaryKey: true) t.column(email, unique: true, check: email.like("%@%")) t.column(name) -}) +} + +/// …and run it +try db.run(statement) + +/// insert "alice" +let rowid = try db.run(users.insert(email <- "alice@mac.com")) + +/// insert multiple rows using `insertMany` +let lastRowid = try db.run(users.insertMany([ + [email <- "bob@mac.com"], + [email <- "mallory@evil.com"] +])) -let rowid = try! db.run(users.insert(email <- "alice@mac.com")) -let alice = users.filter(id == rowid) -for user in try! db.prepare(users) { +let query = try db.prepare(users) +for user in query { + print("id: \(user[id]), email: \(user[email])") +} + +// re-requery just rowid of Alice +let alice = try db.prepare(users.filter(id == rowid)) +for user in alice { print("id: \(user[id]), email: \(user[email])") } +/// using the `RowIterator` API +let rowIterator = try db.prepareRowIterator(users) +for user in try Array(rowIterator) { + print("id: \(user[id]), email: \(user[email])") +} + +/// also with `map()` +let mapRowIterator = try db.prepareRowIterator(users) + +let userIds = try mapRowIterator.map { $0[id] } + +/// using `failableNext()` on `RowIterator` +let iterator = try db.prepareRowIterator(users) +do { + while let row = try rowIterator.failableNext() { + print(row) + } +} catch { + // Handle error +} + +/// define a virtual table for the FTS index let emails = VirtualTable("emails") -let subject = Expression("subject") +let subject = Expression("subject") let body = Expression("body") -try! db.run(emails.create(.FTS4(subject, body))) +/// create the index +try db.run(emails.create(.FTS5( + FTS5Config() + .column(subject) + .column(body) +))) -try! db.run(emails.insert( +/// populate with data +try db.run(emails.insert( subject <- "Hello, world!", body <- "This is a hello world message." )) -let row = db.pluck(emails.match("hello")) +/// run a query +let ftsQuery = try db.prepare(emails.match("hello")) -let query = try! db.prepare(emails.match("hello")) -for row in query { +for row in ftsQuery { print(row[subject]) } + +/// custom aggregations +let reduce: (String, [Binding?]) -> String = { (last, bindings) in + last + " " + (bindings.first as? String ?? "") +} + +db.createAggregation("customConcat", + initialValue: "users:", + reduce: reduce, + result: { $0 }) +let result = try db.prepare("SELECT customConcat(email) FROM users").scalar() as! String +print(result) + +/// schema queries +let schema = db.schema +let objects = try schema.objectDefinitions() +print(objects) + +let columns = try schema.columnDefinitions(table: "users") +print(columns) + +/// schema alteration + +let schemaChanger = SchemaChanger(connection: db) +try schemaChanger.alter(table: "users") { table in + table.add(column: ColumnDefinition(name: "age", type: .INTEGER)) + table.rename(column: "email", to: "electronic_mail") + table.drop(column: "name") +} + +let changedColumns = try schema.columnDefinitions(table: "users") +print(changedColumns) + +let age = Expression("age") +let electronicMail = Expression("electronic_mail") + +let newRowid = try db.run(users.insert( + electronicMail <- "carol@mac.com", + age <- 33 +)) diff --git a/SQLite.playground/contents.xcplayground b/SQLite.playground/contents.xcplayground index fd676d5b..441c60ef 100644 --- a/SQLite.playground/contents.xcplayground +++ b/SQLite.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/SQLite.playground/playground.xcworkspace/contents.xcworkspacedata b/SQLite.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SQLite.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec index 6e5e25d5..73c6f705 100644 --- a/SQLite.swift.podspec +++ b/SQLite.swift.podspec @@ -1,12 +1,7 @@ -# -# `pod lib lint SQLite.swift.podspec' fails - see -# https://github.com/CocoaPods/CocoaPods/issues/4607 -# - Pod::Spec.new do |s| s.name = "SQLite.swift" - s.version = "0.10.1" - s.summary = "A type-safe, Swift-language layer over SQLite3 for iOS and OS X." + s.version = "0.15.4" + s.summary = "A type-safe, Swift-language layer over SQLite3." s.description = <<-DESC SQLite.swift provides compile-time confidence in SQL statement syntax and @@ -20,34 +15,64 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/stephencelis' s.module_name = 'SQLite' - s.ios.deployment_target = "8.0" - s.tvos.deployment_target = "9.0" - s.osx.deployment_target = "10.9" - s.watchos.deployment_target = "2.0" s.default_subspec = 'standard' + s.swift_versions = ['5'] - s.subspec 'standard' do |ss| - ss.source_files = 'SQLite/**/*.{c,h,m,swift}' - ss.private_header_files = 'SQLite/Core/fts3_tokenizer.h' + s.ios.deployment_target = '12.0' + s.tvos.deployment_target = '12.0' + s.osx.deployment_target = '10.13' + s.watchos.deployment_target = '4.0' + s.visionos.deployment_target = '1.0' + s.subspec 'standard' do |ss| + ss.source_files = 'Sources/SQLite/**/*.{c,h,m,swift}' + ss.exclude_files = 'Sources/**/Cipher.swift' ss.library = 'sqlite3' - ss.preserve_paths = 'CocoaPods/**/*' - ss.pod_target_xcconfig = { - 'SWIFT_INCLUDE_PATHS[sdk=macosx*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx', - 'SWIFT_INCLUDE_PATHS[sdk=iphoneos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphoneos', - 'SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphonesimulator', - 'SWIFT_INCLUDE_PATHS[sdk=appletvos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/appletvos', - 'SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/appletvsimulator', - 'SWIFT_INCLUDE_PATHS[sdk=watchos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/watchos', - 'SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/watchsimulator' - } + ss.resource_bundle = { 'SQLite.swift' => 'Sources/SQLite/PrivacyInfo.xcprivacy' } + + ss.test_spec 'tests' do |test_spec| + test_spec.resources = 'Tests/SQLiteTests/Resources/*' + test_spec.source_files = 'Tests/SQLiteTests/*.swift' + end end s.subspec 'standalone' do |ss| - ss.source_files = 'SQLite/**/*.{c,h,m,swift}' - ss.private_header_files = 'SQLite/Core/fts3_tokenizer.h' - ss.xcconfig = { 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE' } + ss.source_files = 'Sources/SQLite/**/*.{c,h,m,swift}' + ss.exclude_files = 'Sources/**/Cipher.swift' + ss.resource_bundle = { 'SQLite.swift' => 'Sources/SQLite/PrivacyInfo.xcprivacy' } + ss.xcconfig = { + 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_SWIFT_STANDALONE=1' + } ss.dependency 'sqlite3' + + ss.test_spec 'tests' do |test_spec| + test_spec.resources = 'Tests/SQLiteTests/Resources/*' + test_spec.source_files = 'Tests/SQLiteTests/*.swift' + end + end + + s.subspec 'SQLCipher' do |ss| + # Disable unsupported visionOS + # https://github.com/sqlcipher/sqlcipher/issues/483 + ss.ios.deployment_target = s.deployment_target(:ios) + ss.tvos.deployment_target = s.deployment_target(:tvos) + ss.osx.deployment_target = s.deployment_target(:osx) + ss.watchos.deployment_target = s.deployment_target(:watchos) + + ss.source_files = 'Sources/SQLite/**/*.{c,h,m,swift}' + ss.resource_bundle = { 'SQLite.swift' => 'Sources/SQLite/PrivacyInfo.xcprivacy' } + + ss.xcconfig = { + 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_SQLCIPHER', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1 SQLITE_SWIFT_SQLCIPHER=1' + } + ss.dependency 'SQLCipher', '>= 4.0.0' + + ss.test_spec 'tests' do |test_spec| + test_spec.resources = 'Tests/SQLiteTests/Resources/*' + test_spec.source_files = 'Tests/SQLiteTests/*.swift' + end end end diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index ec5b5882..ec0423e9 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -7,16 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 02A43A9822738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + 02A43A9922738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + 02A43A9A22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + 02A43A9B22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; 03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; - 03A65E711C6BB2CD0062603F /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; - 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E761C6BB2E60062603F /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; 03A65E771C6BB2E60062603F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; 03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; @@ -30,34 +30,275 @@ 03A65E841C6BB2FB0062603F /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; - 03A65E871C6BB3030062603F /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; - 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; - 03A65E891C6BB3030062603F /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; - 03A65E8A1C6BB3030062603F /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; - 03A65E8B1C6BB3030062603F /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; - 03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; - 03A65E8D1C6BB3030062603F /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; - 03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; - 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; - 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; - 03A65E911C6BB3030062603F /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; - 03A65E921C6BB3030062603F /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; - 03A65E931C6BB3030062603F /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; - 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; }; + 19A17018F250343BD0F9F4B0 /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + 19A17021286A4D8D6C2EF12D /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + 19A17026DCDCDA405B09A229 /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; + 19A17073552293CA063BEA66 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + 19A1708D3D58D7BC1168E55F /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A170ACC97B19730FB7BA4D /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; + 19A170AEBAA56DC3355A73B3 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */; }; + 19A170C56745F9D722A73D77 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */; }; + 19A170D938343E30119EDFB3 /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + 19A1714F7CF964D568AB14E0 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A2ED4E2640F197F48C /* BlobTests.swift */; }; + 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A1716BF8E15F91A6B5CB7A /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; - 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; - 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; - 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; + 19A17188B4D96636F9C0C209 /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; + 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A171F243A589C5EBC47937 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17475DCA068453F787613 /* OperatorsTests.swift */; }; + 19A1725658E480B9B378F28B /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; + 19A1726002D24C14F876C8FE /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17AE284BB1DF31D1B753E /* ValueTests.swift */; }; + 19A172F71EFD65342072D8D2 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A7714C6524093255C5 /* SchemaTests.swift */; }; + 19A173088B85A7E18E8582A7 /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */; }; + 19A173389E53CB24DFA8CEDD /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */; }; + 19A173465F23C64DF3DF469B /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + 19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A173EFEF0B3BD0B3ED406C /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + 19A173F25449876761347072 /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; + 19A173F429D7E46289EB2167 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1745BE8623D8C6808DB3C /* ResultTests.swift */; }; + 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A1740EACD47904AA24B8DC /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17411403D60640467209E /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170F141BF21946D159083 /* ExpressionTests.swift */; }; + 19A174118D11B93DA5DAAF79 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */; }; + 19A17437659BD7FD787D94A6 /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */; }; + 19A17444861E1443143DEB44 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */; }; + 19A174506543905D71BF0518 /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + 19A17457B0461F484AF6BE40 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; + 19A17482E6FC5E563F3E6A47 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17475DCA068453F787613 /* OperatorsTests.swift */; }; + 19A17490543609FCED53CACC /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A1750EF4A5F92954A451FF /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + 19A1755C49154C87304C9146 /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */; }; + 19A1760CE25615CA015E2E5F /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + 19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; + 19A1766135CE9786B1878603 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17AE284BB1DF31D1B753E /* ValueTests.swift */; }; + 19A1766AC10D13C4EFF349AD /* SchemaChangerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */; }; + 19A176B3316281F004F92276 /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + 19A177290558991BCC60E4E3 /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; + 19A1772EBE65173EDFB1AFCA /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */; }; + 19A1773155AC2BF2CA86A473 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1709D5BDD2691BA160012 /* SetterTests.swift */; }; + 19A1773A335CAB9D0AE14E8E /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; + 19A17746150A815944A6820B /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17EC0C43015063945D32E /* SelectTests.swift */; }; + 19A1776BD5127DFDF847FF1F /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */; }; + 19A177909023B7B940C5805E /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */; }; + 19A177AA5922527BBDC77CF9 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */; }; + 19A177C25834473FAB32CF3B /* SchemaChangerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */; }; + 19A177D5C6542E2D572162E5 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */; }; + 19A178072B371489E6A1E839 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A1781CBA8968ABD3E00877 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170F141BF21946D159083 /* ExpressionTests.swift */; }; + 19A1782444437C7FC6B75CBC /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A2ED4E2640F197F48C /* BlobTests.swift */; }; + 19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A178767223229E61C5066F /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */; }; + 19A17885B646CB0201BE4BD5 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + 19A178A8B2A34FB6B565DEDA /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + 19A178C041DDCF80B533AD13 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A2ED4E2640F197F48C /* BlobTests.swift */; }; + 19A178DA2BB5970778CCAF13 /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */; }; + 19A178DF5A96CFEFF1E271F6 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */; }; + 19A178F9008614B8A8425635 /* Connection+PragmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */; }; + 19A17900387FDCF578B31E3E /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17475DCA068453F787613 /* OperatorsTests.swift */; }; + 19A17912DB9D3AC8FECF948B /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1745BE8623D8C6808DB3C /* ResultTests.swift */; }; + 19A17923494236793893BF72 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; + 19A1793972BDDDB027C113BB /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */; }; + 19A179786A6826D58A70F8BC /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */; }; + 19A17986405D9A875698408F /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + 19A1799AF6643CF5081BFA15 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */; }; + 19A179B59450FE7C4811AB8A /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; + 19A179BB9A6665B2B99DA546 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; + 19A179BCD483DEA21661FD37 /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; + 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A17A33EA026C2E2CEBAF36 /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + 19A17A391BF056E3D729E70A /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A7714C6524093255C5 /* SchemaTests.swift */; }; + 19A17A52BF29D27C9AA229E7 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + 19A17A7B3E3B7E76364A2AEE /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + 19A17A7DF99B0379FD3396B1 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; + 19A17A9520802ACF45907970 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + 19A17ABCF0EB4808BDC5B5FF /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + 19A17B0DF1DDB6BBC9C95D64 /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17B1D9B5CEBE9CE09280C /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + 19A17B36ABC6006AB80F693C /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; + 19A17B62A4125AF4F6014CF5 /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; + 19A17BA13FD35F058787B7D3 /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17BACF4C032513DE1F879 /* Connection+PragmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */; }; + 19A17C74233AFC2EDAFA23DC /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1745BE8623D8C6808DB3C /* ResultTests.swift */; }; + 19A17CA4D7B63D845428A9C5 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + 19A17CA6ADB78A2E545BF836 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + 19A17CF65C0196E03BC64519 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + 19A17D1BEABA610ABF003D67 /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17D6EC40BC35A5DC81BA8 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + 19A17D993398B8215B73E1EA /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; + 19A17DAD5975D9367EAA46E2 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; + 19A17DD33C2E43DD6EE05A60 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170F141BF21946D159083 /* ExpressionTests.swift */; }; + 19A17DE1FCDB5695702AD24D /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17EC0C43015063945D32E /* SelectTests.swift */; }; + 19A17DE34C477232592A8F6B /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; + 19A17DF8D4F13A20F5D2269E /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + 19A17DFE05ED8B1F7C45F7EE /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; + 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17E0ABA6C415F014CD51C /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1709D5BDD2691BA160012 /* SetterTests.swift */; }; + 19A17E1DD976D5CE80018749 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */; }; + 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A17E3F47DA087E2B76D087 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + 19A17E80F736EEE8EE2AA4CE /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A17F0BF02896E1664F4090 /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + 19A17F1B3F0A3C96B5ED6D64 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + 19A17F2096E83A3181E03317 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A7714C6524093255C5 /* SchemaTests.swift */; }; + 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A17F7977364EC8CD33C3C3 /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17EC0C43015063945D32E /* SelectTests.swift */; }; + 19A17F907258E524B3CA2FAE /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1709D5BDD2691BA160012 /* SetterTests.swift */; }; + 19A17FACE8E4D54A50BA934E /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */; }; + 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17FBAA26953EB854E790D /* Connection+PragmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */; }; + 19A17FC04708C6ED637DDFD4 /* SchemaChangerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */; }; + 19A17FC07731779C1B8506FA /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; + 19A17FD22EF43DF428DD93BA /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17AE284BB1DF31D1B753E /* ValueTests.swift */; }; + 19A17FE78A39E86F330420EC /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; + 19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; + 3D67B3E61DB2469200A4F4C6 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */; }; + 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + 3D67B3EA1DB246D100A4F4C6 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + 3D67B3EB1DB246D100A4F4C6 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + 3D67B3EC1DB246D100A4F4C6 /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + 3D67B3ED1DB246D100A4F4C6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 3D67B3EE1DB246D100A4F4C6 /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + 3D67B3EF1DB246D100A4F4C6 /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + 3D67B3F01DB246D100A4F4C6 /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + 3D67B3F11DB246D100A4F4C6 /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + 3D67B3F21DB246D100A4F4C6 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + 3D67B3F31DB246D100A4F4C6 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + 3D67B3F41DB246D100A4F4C6 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + 3D67B3F51DB246D100A4F4C6 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + 3D67B3F61DB246D100A4F4C6 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78B28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B79628846FCC005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79828846FED005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79928847055005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 64A8EE432B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE442B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE452B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE462B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64B8E1702B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 64B8E1712B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 64B8E1722B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + 997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + 997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + 997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + DB58B21128FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21228FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21328FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21428FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21628FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB58B21728FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB58B21828FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB58B21928FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB7C5DA628D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DB7C5DA728D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DB7C5DA828D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DB7C5DA928D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DBB93D5A2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DBB93D5B2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DBB93D5C2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DEB306BA2B61CEF500F9D46B /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DEB306BC2B61CEF500F9D46B /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + DEB306BD2B61CEF500F9D46B /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + DEB306BE2B61CEF500F9D46B /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + DEB306BF2B61CEF500F9D46B /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + DEB306C02B61CEF500F9D46B /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + DEB306C12B61CEF500F9D46B /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + DEB306C22B61CEF500F9D46B /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + DEB306C32B61CEF500F9D46B /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + DEB306C42B61CEF500F9D46B /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + DEB306C52B61CEF500F9D46B /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + DEB306C62B61CEF500F9D46B /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + DEB306C72B61CEF500F9D46B /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + DEB306C82B61CEF500F9D46B /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + DEB306C92B61CEF500F9D46B /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + DEB306CA2B61CEF500F9D46B /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + DEB306CB2B61CEF500F9D46B /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + DEB306CC2B61CEF500F9D46B /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + DEB306CD2B61CEF500F9D46B /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + DEB306CE2B61CEF500F9D46B /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + DEB306CF2B61CEF500F9D46B /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + DEB306D02B61CEF500F9D46B /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + DEB306D12B61CEF500F9D46B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + DEB306D22B61CEF500F9D46B /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + DEB306D32B61CEF500F9D46B /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; + DEB306D42B61CEF500F9D46B /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + DEB306D52B61CEF500F9D46B /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DEB306D62B61CEF500F9D46B /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + DEB306D72B61CEF500F9D46B /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + DEB306D82B61CEF500F9D46B /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; + DEB306D92B61CEF500F9D46B /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DEB306DA2B61CEF500F9D46B /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; + DEB306DB2B61CEF500F9D46B /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + DEB306DC2B61CEF500F9D46B /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + DEB306DD2B61CEF500F9D46B /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + DEB306DE2B61CEF500F9D46B /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DEB306E02B61CEF500F9D46B /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180931C46EA210038162A /* libsqlite3.tbd */; }; + DEB306EB2B61CF9500F9D46B /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; + DEB306EC2B61CF9500F9D46B /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + DEB306ED2B61CF9500F9D46B /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + DEB306EE2B61CF9500F9D46B /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; + DEB306EF2B61CF9500F9D46B /* SchemaChangerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */; }; + DEB306F02B61CF9500F9D46B /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; + DEB306F12B61CF9500F9D46B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */; }; + DEB306F22B61CF9500F9D46B /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */; }; + DEB306F32B61CF9500F9D46B /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */; }; + DEB306F42B61CF9500F9D46B /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170F141BF21946D159083 /* ExpressionTests.swift */; }; + DEB306F52B61CF9500F9D46B /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + DEB306F62B61CF9500F9D46B /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + DEB306F72B61CF9500F9D46B /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + DEB306F82B61CF9500F9D46B /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A2ED4E2640F197F48C /* BlobTests.swift */; }; + DEB306F92B61CF9500F9D46B /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + DEB306FA2B61CF9500F9D46B /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; + DEB306FB2B61CF9500F9D46B /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */; }; + DEB306FC2B61CF9500F9D46B /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; + DEB306FD2B61CF9500F9D46B /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17475DCA068453F787613 /* OperatorsTests.swift */; }; + DEB306FE2B61CF9500F9D46B /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1745BE8623D8C6808DB3C /* ResultTests.swift */; }; + DEB306FF2B61CF9500F9D46B /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + DEB307002B61CF9500F9D46B /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A7714C6524093255C5 /* SchemaTests.swift */; }; + DEB307012B61CF9500F9D46B /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17EC0C43015063945D32E /* SelectTests.swift */; }; + DEB307022B61CF9500F9D46B /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17AE284BB1DF31D1B753E /* ValueTests.swift */; }; + DEB307032B61CF9500F9D46B /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */; }; + DEB307042B61CF9500F9D46B /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */; }; + DEB307052B61CF9500F9D46B /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */; }; + DEB307062B61CF9500F9D46B /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1709D5BDD2691BA160012 /* SetterTests.swift */; }; + DEB307072B61CF9500F9D46B /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + DEB307082B61CF9500F9D46B /* Connection+PragmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */; }; + DEB307092B61CF9500F9D46B /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; + DEB3070B2B61CF9500F9D46B /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; + DEB3070D2B61CF9500F9D46B /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + EAE5A0372B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; + EAE5A0382B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; + EAE5A0392B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; + EAE5A03A2B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; }; EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; EE247B081C3F06E900AE3E12 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; @@ -74,43 +315,13 @@ EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; EE247B171C3F127200AE3E12 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; - EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; - EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; - EE247B231C3F137700AE3E12 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; - EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; - EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; - EE247B271C3F137700AE3E12 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; - EE247B281C3F137700AE3E12 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; - EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; - EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; - EE247B2F1C3F141E00AE3E12 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; - EE247B301C3F141E00AE3E12 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; - EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; - EE247B341C3F142E00AE3E12 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; - EE247B351C3F142E00AE3E12 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; EE247B461C3F3ED000AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */; }; - EE247B531C3F3FC700AE3E12 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; - EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; - EE247B551C3F3FC700AE3E12 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; - EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; - EE247B571C3F3FC700AE3E12 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; - EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; - EE247B591C3F3FC700AE3E12 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; - EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; - EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; - EE247B5C1C3F3FC700AE3E12 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; - EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; - EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; - EE247B5F1C3F3FC700AE3E12 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; - EE247B601C3F3FC700AE3E12 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; EE247B611C3F3FC700AE3E12 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; @@ -124,10 +335,6 @@ EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; - EE91808C1C46E34A0038162A /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE9180901C46E8980038162A /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE9180941C46EA210038162A /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180931C46EA210038162A /* libsqlite3.tbd */; }; EE9180951C46EBCC0038162A /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180911C46E9D30038162A /* libsqlite3.tbd */; }; /* End PBXBuildFile section */ @@ -140,6 +347,13 @@ remoteGlobalIDString = 03A65E591C6BB0F50062603F; remoteInfo = "SQLite tvOS"; }; + DEB307142B61D07F00F9D46B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEB306B82B61CEF500F9D46B; + remoteInfo = "SQLite visionOS"; + }; EE247ADF1C3F04ED00AE3E12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; @@ -157,19 +371,73 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02A43A9722738CF100FEC494 /* Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backup.swift; sourceTree = ""; }; 03A65E5A1C6BB0F50062603F /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 03A65E961C6BB3210062603F /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; - 19A1721B8984686B9963B45D /* FTS5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tests.swift; sourceTree = ""; }; + 19A1709D5BDD2691BA160012 /* SetterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetterTests.swift; sourceTree = ""; }; + 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+Schema.swift"; sourceTree = ""; }; + 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+AttachTests.swift"; sourceTree = ""; }; + 19A170F141BF21946D159083 /* ExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionTests.swift; sourceTree = ""; }; + 19A1710E73A46D5AC721CDA9 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4Tests.swift; sourceTree = ""; }; + 19A17162C9861E5C4900455D /* RTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTreeTests.swift; sourceTree = ""; }; + 19A171A2ED4E2640F197F48C /* BlobTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobTests.swift; sourceTree = ""; }; + 19A171A7714C6524093255C5 /* SchemaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTests.swift; sourceTree = ""; }; + 19A171B262DDE8718513CFDA /* SchemaChanger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaChanger.swift; sourceTree = ""; }; + 19A171ED017645C8B04DF9F2 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; + 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaDefinitions.swift; sourceTree = ""; }; 19A1730E4390C775C25677D1 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = ""; }; - 39548A631CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A651CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A671CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A691CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A6B1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A6D1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A6F1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 19A1745BE8623D8C6808DB3C /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; + 19A17475DCA068453F787613 /* OperatorsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorsTests.swift; sourceTree = ""; }; + 19A174FE5B47A97937A27276 /* RowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowTests.swift; sourceTree = ""; }; + 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementTests.swift; sourceTree = ""; }; + 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+Aggregation.swift"; sourceTree = ""; }; + 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctionsTests.swift; sourceTree = ""; }; + 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaDefinitionsTests.swift; sourceTree = ""; }; + 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomAggregationTests.swift; sourceTree = ""; }; + 19A17855BD524FF888265B3C /* ConnectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTests.swift; sourceTree = ""; }; + 19A1787E16C8562C09C076F5 /* CipherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherTests.swift; sourceTree = ""; }; + 19A178A39ACA9667A62663CC /* Cipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; }; + 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctionTests.swift; sourceTree = ""; }; + 19A1794B7972D14330A65BBD /* Linux.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Linux.md; sourceTree = ""; }; + 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationTests.swift; sourceTree = ""; }; + 19A17AE284BB1DF31D1B753E /* ValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueTests.swift; sourceTree = ""; }; + 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctionsTests.swift; sourceTree = ""; }; + 19A17B93B48B5560E6E51791 /* Fixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; + 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tests.swift; sourceTree = ""; }; + 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctions.swift; sourceTree = ""; }; + 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTSIntegrationTests.swift; sourceTree = ""; }; + 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+PragmaTests.swift"; sourceTree = ""; }; + 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+SchemaTests.swift"; sourceTree = ""; }; + 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryIntegrationTests.swift; sourceTree = ""; }; + 19A17E723300E5ED3771DCB5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; + 19A17EA3A313F129011B3FA0 /* Release.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Release.md; sourceTree = ""; }; + 19A17EC0C43015063945D32E /* SelectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectTests.swift; sourceTree = ""; }; + 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+Pragmas.swift"; sourceTree = ""; }; + 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctionsTests.swift; sourceTree = ""; }; + 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaChangerTests.swift; sourceTree = ""; }; + 3D3C3CCB26E5568800759140 /* SQLite.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SQLite.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS3.0.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + 3DF7B78728842972005DD8CA /* Connection+Attach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Connection+Attach.swift"; sourceTree = ""; }; + 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIQueryParameter.swift; sourceTree = ""; }; + 3DF7B79528846FCC005DD8CA /* Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Resources; sourceTree = ""; }; + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + 3DF7B79B2884C901005DD8CA /* Planning.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Planning.md; sourceTree = ""; }; + 3DFC0B862886C239001C8FC9 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; + 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFunctions.swift; sourceTree = ""; }; + 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFunctionsTests.swift; sourceTree = ""; }; + 997DF2AD287FC06D00F8DF95 /* Query+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+with.swift"; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB58B21028FB864300F8EEA4 /* SchemaReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaReader.swift; sourceTree = ""; }; + DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteFeature.swift; sourceTree = ""; }; + DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteVersion.swift; sourceTree = ""; }; + DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaReaderTests.swift; sourceTree = ""; }; + DEB306E52B61CEF500F9D46B /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests visionOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + DEB307132B61D04500F9D46B /* SQLite visionOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "SQLite visionOS.xctestplan"; path = "Tests/SQLite visionOS.xctestplan"; sourceTree = ""; }; + EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = ""; }; EE247AD81C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -177,12 +445,10 @@ EE247AE41C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EE247AEE1C3F06E900AE3E12 /* Blob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blob.swift; sourceTree = ""; }; EE247AEF1C3F06E900AE3E12 /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; - EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fts3_tokenizer.h; sourceTree = ""; }; - EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SQLite-Bridging.m"; sourceTree = ""; }; EE247AF21C3F06E900AE3E12 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Statement.swift; sourceTree = ""; }; EE247AF31C3F06E900AE3E12 /* Value.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Value.swift; sourceTree = ""; }; EE247AF51C3F06E900AE3E12 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = ""; }; - EE247AF61C3F06E900AE3E12 /* RTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RTree.swift"; sourceTree = ""; }; + EE247AF61C3F06E900AE3E12 /* RTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; EE247AF71C3F06E900AE3E12 /* Foundation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Foundation.swift; sourceTree = ""; }; EE247AF81C3F06E900AE3E12 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctions.swift; sourceTree = ""; }; @@ -195,33 +461,16 @@ EE247B011C3F06E900AE3E12 /* Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Schema.swift; sourceTree = ""; }; EE247B021C3F06E900AE3E12 /* Setter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Setter.swift; sourceTree = ""; }; EE247B161C3F127200AE3E12 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; - EE247B181C3F134A00AE3E12 /* SetterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetterTests.swift; sourceTree = ""; }; - EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctionsTests.swift; sourceTree = ""; }; - EE247B1B1C3F137700AE3E12 /* BlobTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobTests.swift; sourceTree = ""; }; - EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTests.swift; sourceTree = ""; }; - EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctionsTests.swift; sourceTree = ""; }; - EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctionsTests.swift; sourceTree = ""; }; - EE247B201C3F137700AE3E12 /* ExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionTests.swift; sourceTree = ""; }; - EE247B211C3F137700AE3E12 /* FTS4Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4Tests.swift; sourceTree = ""; }; - EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorsTests.swift; sourceTree = ""; }; - EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; - EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RTreeTests.swift"; sourceTree = ""; }; - EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTests.swift; sourceTree = ""; }; - EE247B321C3F142E00AE3E12 /* StatementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementTests.swift; sourceTree = ""; }; - EE247B331C3F142E00AE3E12 /* ValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueTests.swift; sourceTree = ""; }; EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests Mac.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; EE247B771C3F40D700AE3E12 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; - EE247B8C1C3F821200AE3E12 /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = ""; }; EE247B8D1C3F821200AE3E12 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; EE247B8F1C3F822500AE3E12 /* Index.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Index.md; sourceTree = ""; }; EE247B911C3F822500AE3E12 /* installation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "installation@2x.png"; sourceTree = ""; }; EE247B921C3F822600AE3E12 /* playground@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playground@2x.png"; sourceTree = ""; }; EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SQLite.swift.podspec; sourceTree = ""; }; - EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = usr/include/sqlite3.h; sourceTree = SDKROOT; }; - EE91808D1C46E5230038162A /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQLite-Bridging.h"; sourceTree = ""; }; - EE9180911C46E9D30038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + EE9180911C46E9D30038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; EE9180931C46EA210038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -246,6 +495,23 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3D67B3E61DB2469200A4F4C6 /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306DF2B61CEF500F9D46B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306E02B61CEF500F9D46B /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB3070A2B61CF9500F9D46B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB3070B2B61CF9500F9D46B /* SQLite.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -284,83 +550,93 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 39548A611CA63C740003E3B5 /* CocoaPods */ = { - isa = PBXGroup; - children = ( - 39548A621CA63C740003E3B5 /* appletvos */, - 39548A641CA63C740003E3B5 /* appletvsimulator */, - 39548A661CA63C740003E3B5 /* iphoneos */, - 39548A681CA63C740003E3B5 /* iphonesimulator */, - 39548A6A1CA63C740003E3B5 /* macosx */, - 39548A6C1CA63C740003E3B5 /* watchos */, - 39548A6E1CA63C740003E3B5 /* watchsimulator */, - ); - path = CocoaPods; - sourceTree = ""; - }; - 39548A621CA63C740003E3B5 /* appletvos */ = { + 19A1792D261C689FC988A90A /* Schema */ = { isa = PBXGroup; children = ( - 39548A631CA63C740003E3B5 /* module.modulemap */, + DB58B21028FB864300F8EEA4 /* SchemaReader.swift */, + 19A171B262DDE8718513CFDA /* SchemaChanger.swift */, + 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */, + 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */, ); - path = appletvos; + path = Schema; sourceTree = ""; }; - 39548A641CA63C740003E3B5 /* appletvsimulator */ = { + 19A1798E3459573BEE50FA34 /* Core */ = { isa = PBXGroup; children = ( - 39548A651CA63C740003E3B5 /* module.modulemap */, + 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */, + 19A171A2ED4E2640F197F48C /* BlobTests.swift */, + 19A17855BD524FF888265B3C /* ConnectionTests.swift */, + 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */, + 19A1745BE8623D8C6808DB3C /* ResultTests.swift */, + 19A17AE284BB1DF31D1B753E /* ValueTests.swift */, + 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */, + 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */, ); - path = appletvsimulator; - sourceTree = ""; - }; - 39548A661CA63C740003E3B5 /* iphoneos */ = { - isa = PBXGroup; - children = ( - 39548A671CA63C740003E3B5 /* module.modulemap */, - ); - path = iphoneos; + path = Core; sourceTree = ""; }; - 39548A681CA63C740003E3B5 /* iphonesimulator */ = { + 19A17AECBF878B1DAE0AE3DD /* Typed */ = { isa = PBXGroup; children = ( - 39548A691CA63C740003E3B5 /* module.modulemap */, + 19A170F141BF21946D159083 /* ExpressionTests.swift */, + 19A171ED017645C8B04DF9F2 /* QueryTests.swift */, + 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */, + 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */, + 19A17475DCA068453F787613 /* OperatorsTests.swift */, + 19A17EC0C43015063945D32E /* SelectTests.swift */, + 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */, + 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */, + 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */, + 19A1709D5BDD2691BA160012 /* SetterTests.swift */, + 19A174FE5B47A97937A27276 /* RowTests.swift */, + 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */, ); - path = iphonesimulator; + path = Typed; sourceTree = ""; }; - 39548A6A1CA63C740003E3B5 /* macosx */ = { + 19A17B56FBA20E7245BC8AC0 /* Schema */ = { isa = PBXGroup; children = ( - 39548A6B1CA63C740003E3B5 /* module.modulemap */, + DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */, + 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */, + 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */, + 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */, + 19A171A7714C6524093255C5 /* SchemaTests.swift */, ); - path = macosx; + path = Schema; sourceTree = ""; }; - 39548A6C1CA63C740003E3B5 /* watchos */ = { + 19A17E470E4492D287C0D12F /* Extensions */ = { isa = PBXGroup; children = ( - 39548A6D1CA63C740003E3B5 /* module.modulemap */, + 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */, + 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */, + 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */, + 19A1787E16C8562C09C076F5 /* CipherTests.swift */, + 19A17162C9861E5C4900455D /* RTreeTests.swift */, ); - path = watchos; + path = Extensions; sourceTree = ""; }; - 39548A6E1CA63C740003E3B5 /* watchsimulator */ = { + 3D67B3E41DB2469200A4F4C6 /* Frameworks */ = { isa = PBXGroup; children = ( - 39548A6F1CA63C740003E3B5 /* module.modulemap */, + 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */, ); - path = watchsimulator; + name = Frameworks; sourceTree = ""; }; EE247AC91C3F04ED00AE3E12 = { isa = PBXGroup; children = ( + 3D3C3CCB26E5568800759140 /* SQLite.playground */, EE247AD51C3F04ED00AE3E12 /* SQLite */, EE247AE11C3F04ED00AE3E12 /* SQLiteTests */, + DEB307132B61D04500F9D46B /* SQLite visionOS.xctestplan */, EE247B8A1C3F81D000AE3E12 /* Metadata */, EE247AD41C3F04ED00AE3E12 /* Products */, + 3D67B3E41DB2469200A4F4C6 /* Frameworks */, ); indentWidth = 4; sourceTree = ""; @@ -376,6 +652,8 @@ 03A65E5A1C6BB0F50062603F /* SQLite.framework */, 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */, A121AC451CA35C79005A31D1 /* SQLite.framework */, + DEB306E52B61CEF500F9D46B /* SQLite.framework */, + DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */, ); name = Products; sourceTree = ""; @@ -383,52 +661,53 @@ EE247AD51C3F04ED00AE3E12 /* SQLite */ = { isa = PBXGroup; children = ( - EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */, EE247AD61C3F04ED00AE3E12 /* SQLite.h */, EE247AF71C3F06E900AE3E12 /* Foundation.swift */, EE247AF81C3F06E900AE3E12 /* Helpers.swift */, + EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */, EE247AD81C3F04ED00AE3E12 /* Info.plist */, EE247AED1C3F06E900AE3E12 /* Core */, EE247AF41C3F06E900AE3E12 /* Extensions */, EE247AF91C3F06E900AE3E12 /* Typed */, + 19A1792D261C689FC988A90A /* Schema */, ); - path = SQLite; + name = SQLite; + path = Sources/SQLite; sourceTree = ""; }; EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { isa = PBXGroup; children = ( - EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */, - EE247B1B1C3F137700AE3E12 /* BlobTests.swift */, - EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */, - EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */, - EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */, - EE247B201C3F137700AE3E12 /* ExpressionTests.swift */, - EE247B211C3F137700AE3E12 /* FTS4Tests.swift */, - EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */, - EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */, - EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */, - EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */, - EE247B181C3F134A00AE3E12 /* SetterTests.swift */, - EE247B321C3F142E00AE3E12 /* StatementTests.swift */, - EE247B331C3F142E00AE3E12 /* ValueTests.swift */, + 3DF7B79528846FCC005DD8CA /* Resources */, EE247B161C3F127200AE3E12 /* TestHelpers.swift */, EE247AE41C3F04ED00AE3E12 /* Info.plist */, - 19A1721B8984686B9963B45D /* FTS5Tests.swift */, - ); - path = SQLiteTests; + 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */, + 19A17B93B48B5560E6E51791 /* Fixtures.swift */, + 19A17B56FBA20E7245BC8AC0 /* Schema */, + 19A17E470E4492D287C0D12F /* Extensions */, + 19A1798E3459573BEE50FA34 /* Core */, + 19A17AECBF878B1DAE0AE3DD /* Typed */, + ); + name = SQLiteTests; + path = Tests/SQLiteTests; sourceTree = ""; }; EE247AED1C3F06E900AE3E12 /* Core */ = { isa = PBXGroup; children = ( - EE91808D1C46E5230038162A /* SQLite-Bridging.h */, EE247AEE1C3F06E900AE3E12 /* Blob.swift */, EE247AEF1C3F06E900AE3E12 /* Connection.swift */, - EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */, - EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */, EE247AF21C3F06E900AE3E12 /* Statement.swift */, EE247AF31C3F06E900AE3E12 /* Value.swift */, + 19A1710E73A46D5AC721CDA9 /* Errors.swift */, + 02A43A9722738CF100FEC494 /* Backup.swift */, + 19A17E723300E5ED3771DCB5 /* Result.swift */, + 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */, + 3DF7B78728842972005DD8CA /* Connection+Attach.swift */, + 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */, + DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */, + DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */, + 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */, ); path = Core; sourceTree = ""; @@ -436,9 +715,10 @@ EE247AF41C3F06E900AE3E12 /* Extensions */ = { isa = PBXGroup; children = ( + 19A178A39ACA9667A62663CC /* Cipher.swift */, EE247AF51C3F06E900AE3E12 /* FTS4.swift */, - EE247AF61C3F06E900AE3E12 /* RTree.swift */, 19A1730E4390C775C25677D1 /* FTS5.swift */, + EE247AF61C3F06E900AE3E12 /* RTree.swift */, ); path = Extensions; sourceTree = ""; @@ -447,14 +727,18 @@ isa = PBXGroup; children = ( EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */, + 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */, EE247AFB1C3F06E900AE3E12 /* Collation.swift */, EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */, EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */, EE247AFE1C3F06E900AE3E12 /* Expression.swift */, EE247AFF1C3F06E900AE3E12 /* Operators.swift */, EE247B001C3F06E900AE3E12 /* Query.swift */, + 997DF2AD287FC06D00F8DF95 /* Query+with.swift */, EE247B011C3F06E900AE3E12 /* Schema.swift */, EE247B021C3F06E900AE3E12 /* Setter.swift */, + 49EB68C31F7B3CB400D89D40 /* Coding.swift */, + 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */, ); path = Typed; sourceTree = ""; @@ -462,11 +746,11 @@ EE247B8A1C3F81D000AE3E12 /* Metadata */ = { isa = PBXGroup; children = ( - 39548A611CA63C740003E3B5 /* CocoaPods */, EE247B771C3F40D700AE3E12 /* README.md */, + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */, EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, - EE247B8C1C3F821200AE3E12 /* .travis.yml */, + 3DFC0B862886C239001C8FC9 /* Package.swift */, EE247B8D1C3F821200AE3E12 /* Makefile */, EE9180931C46EA210038162A /* libsqlite3.tbd */, EE9180911C46E9D30038162A /* libsqlite3.tbd */, @@ -480,7 +764,10 @@ isa = PBXGroup; children = ( EE247B8F1C3F822500AE3E12 /* Index.md */, + 3DF7B79B2884C901005DD8CA /* Planning.md */, EE247B901C3F822500AE3E12 /* Resources */, + 19A17EA3A313F129011B3FA0 /* Release.md */, + 19A1794B7972D14330A65BBD /* Linux.md */, ); path = Documentation; sourceTree = ""; @@ -501,9 +788,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */, - 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */, - 03A65E711C6BB2CD0062603F /* usr/include/sqlite3.h in Headers */, 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -512,6 +796,15 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306B92B61CEF500F9D46B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306BA2B61CEF500F9D46B /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -519,9 +812,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */, - EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */, - EE91808C1C46E34A0038162A /* usr/include/sqlite3.h in Headers */, EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -530,10 +820,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE9180901C46E8980038162A /* usr/include/sqlite3.h in Headers */, - EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */, EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, - EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -544,9 +831,9 @@ isa = PBXNativeTarget; buildConfigurationList = 03A65E6F1C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLite tvOS" */; buildPhases = ( + 03A65E571C6BB0F50062603F /* Headers */, 03A65E551C6BB0F50062603F /* Sources */, 03A65E561C6BB0F50062603F /* Frameworks */, - 03A65E571C6BB0F50062603F /* Headers */, 03A65E581C6BB0F50062603F /* Resources */, ); buildRules = ( @@ -580,9 +867,9 @@ isa = PBXNativeTarget; buildConfigurationList = A121AC4C1CA35C79005A31D1 /* Build configuration list for PBXNativeTarget "SQLite watchOS" */; buildPhases = ( + A121AC421CA35C79005A31D1 /* Headers */, A121AC401CA35C79005A31D1 /* Sources */, A121AC411CA35C79005A31D1 /* Frameworks */, - A121AC421CA35C79005A31D1 /* Headers */, A121AC431CA35C79005A31D1 /* Resources */, ); buildRules = ( @@ -594,13 +881,49 @@ productReference = A121AC451CA35C79005A31D1 /* SQLite.framework */; productType = "com.apple.product-type.framework"; }; + DEB306B82B61CEF500F9D46B /* SQLite visionOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEB306E22B61CEF500F9D46B /* Build configuration list for PBXNativeTarget "SQLite visionOS" */; + buildPhases = ( + DEB306B92B61CEF500F9D46B /* Headers */, + DEB306BB2B61CEF500F9D46B /* Sources */, + DEB306DF2B61CEF500F9D46B /* Frameworks */, + DEB306E12B61CEF500F9D46B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite visionOS"; + productName = SQLite; + productReference = DEB306E52B61CEF500F9D46B /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEB3070E2B61CF9500F9D46B /* Build configuration list for PBXNativeTarget "SQLiteTests visionOS" */; + buildPhases = ( + DEB306EA2B61CF9500F9D46B /* Sources */, + DEB3070A2B61CF9500F9D46B /* Frameworks */, + DEB3070C2B61CF9500F9D46B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEB307152B61D07F00F9D46B /* PBXTargetDependency */, + ); + name = "SQLiteTests visionOS"; + productName = "SQLite tvOSTests"; + productReference = DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; EE247AD21C3F04ED00AE3E12 /* SQLite iOS */ = { isa = PBXNativeTarget; buildConfigurationList = EE247AE71C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLite iOS" */; buildPhases = ( + EE247AD01C3F04ED00AE3E12 /* Headers */, EE247ACE1C3F04ED00AE3E12 /* Sources */, EE247ACF1C3F04ED00AE3E12 /* Frameworks */, - EE247AD01C3F04ED00AE3E12 /* Headers */, EE247AD11C3F04ED00AE3E12 /* Resources */, ); buildRules = ( @@ -634,9 +957,9 @@ isa = PBXNativeTarget; buildConfigurationList = EE247B511C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLite Mac" */; buildPhases = ( + EE247B391C3F3ED000AE3E12 /* Headers */, EE247B371C3F3ED000AE3E12 /* Sources */, EE247B381C3F3ED000AE3E12 /* Frameworks */, - EE247B391C3F3ED000AE3E12 /* Headers */, EE247B3A1C3F3ED000AE3E12 /* Resources */, ); buildRules = ( @@ -673,37 +996,45 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 1250; TargetAttributes = { 03A65E591C6BB0F50062603F = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; 03A65E621C6BB0F60062603F = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; A121AC441CA35C79005A31D1 = { CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0900; }; EE247AD21C3F04ED00AE3E12 = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; EE247ADC1C3F04ED00AE3E12 = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 1020; }; EE247B3B1C3F3ED000AE3E12 = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; EE247B441C3F3ED000AE3E12 = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; }; }; buildConfigurationList = EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = EE247AC91C3F04ED00AE3E12; productRefGroup = EE247AD41C3F04ED00AE3E12 /* Products */; @@ -717,6 +1048,8 @@ 03A65E591C6BB0F50062603F /* SQLite tvOS */, 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */, A121AC441CA35C79005A31D1 /* SQLite watchOS */, + DEB306B82B61CEF500F9D46B /* SQLite visionOS */, + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */, ); }; /* End PBXProject section */ @@ -726,6 +1059,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0392B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -733,6 +1067,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B79828846FED005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -740,6 +1075,22 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A03A2B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306E12B61CEF500F9D46B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB3070C2B61CF9500F9D46B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB3070D2B61CF9500F9D46B /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -747,6 +1098,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0372B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -754,6 +1106,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B79628846FCC005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -761,6 +1114,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0382B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -768,6 +1122,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B79928847055005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -779,16 +1134,19 @@ buildActionMask = 2147483647; files = ( 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */, + 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, - 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */, + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */, 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */, 03A65E821C6BB2FB0062603F /* Expression.swift in Sources */, 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */, 03A65E7F1C6BB2FB0062603F /* Collation.swift in Sources */, 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */, + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */, 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */, 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */, + 64A8EE452B095FBB00F583F7 /* WindowFunctions.swift in Sources */, 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */, 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */, 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */, @@ -797,6 +1155,20 @@ 03A65E771C6BB2E60062603F /* Connection.swift in Sources */, 03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */, 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */, + 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */, + 02A43A9A22738CF100FEC494 /* Backup.swift in Sources */, + 19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */, + 19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */, + DB7C5DA828D7C9B6006395CF /* SQLiteVersion.swift in Sources */, + 19A17073552293CA063BEA66 /* Result.swift in Sources */, + 997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */, + 19A179B59450FE7C4811AB8A /* Connection+Aggregation.swift in Sources */, + DB58B21828FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */, + 19A17FC07731779C1B8506FA /* SchemaChanger.swift in Sources */, + 19A1740EACD47904AA24B8DC /* SchemaDefinitions.swift in Sources */, + 19A1750EF4A5F92954A451FF /* Connection+Schema.swift in Sources */, + 19A17986405D9A875698408F /* Connection+Pragmas.swift in Sources */, + DB58B21328FB864300F8EEA4 /* SchemaReader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -804,22 +1176,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */, - 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */, - 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */, - 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */, - 03A65E8B1C6BB3030062603F /* CustomFunctionsTests.swift in Sources */, - 03A65E871C6BB3030062603F /* AggregateFunctionsTests.swift in Sources */, - 03A65E921C6BB3030062603F /* SetterTests.swift in Sources */, - 03A65E891C6BB3030062603F /* ConnectionTests.swift in Sources */, - 03A65E8A1C6BB3030062603F /* CoreFunctionsTests.swift in Sources */, - 03A65E931C6BB3030062603F /* StatementTests.swift in Sources */, - 03A65E911C6BB3030062603F /* SchemaTests.swift in Sources */, - 03A65E8D1C6BB3030062603F /* FTS4Tests.swift in Sources */, - 03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */, - 03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */, 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */, - 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */, + 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */, + 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */, + 19A17B62A4125AF4F6014CF5 /* SchemaDefinitionsTests.swift in Sources */, + 19A17FC04708C6ED637DDFD4 /* SchemaChangerTests.swift in Sources */, + 19A17188B4D96636F9C0C209 /* Connection+SchemaTests.swift in Sources */, + 19A17FACE8E4D54A50BA934E /* FTS5Tests.swift in Sources */, + 19A177909023B7B940C5805E /* FTSIntegrationTests.swift in Sources */, + 19A17E1DD976D5CE80018749 /* FTS4Tests.swift in Sources */, + DBB93D5C2A22A373009BB96E /* SchemaReaderTests.swift in Sources */, + 19A17411403D60640467209E /* ExpressionTests.swift in Sources */, + 19A17CA4D7B63D845428A9C5 /* StatementTests.swift in Sources */, + 19A17885B646CB0201BE4BD5 /* QueryTests.swift in Sources */, + 19A1708D3D58D7BC1168E55F /* CipherTests.swift in Sources */, + 19A178C041DDCF80B533AD13 /* BlobTests.swift in Sources */, + 19A17021286A4D8D6C2EF12D /* ConnectionTests.swift in Sources */, + 19A17DE34C477232592A8F6B /* CoreFunctionsTests.swift in Sources */, + 19A1799AF6643CF5081BFA15 /* DateAndTimeFunctionTests.swift in Sources */, + 19A17457B0461F484AF6BE40 /* CustomFunctionsTests.swift in Sources */, + 19A17900387FDCF578B31E3E /* OperatorsTests.swift in Sources */, + 19A17C74233AFC2EDAFA23DC /* ResultTests.swift in Sources */, + 19A17A9520802ACF45907970 /* RTreeTests.swift in Sources */, + 19A17A391BF056E3D729E70A /* SchemaTests.swift in Sources */, + 19A17746150A815944A6820B /* SelectTests.swift in Sources */, + 19A1766135CE9786B1878603 /* ValueTests.swift in Sources */, + 19A177D5C6542E2D572162E5 /* QueryIntegrationTests.swift in Sources */, + 64B8E1722B09748000545AFB /* WindowFunctionsTests.swift in Sources */, + 19A178DF5A96CFEFF1E271F6 /* AggregateFunctionsTests.swift in Sources */, + 19A17437659BD7FD787D94A6 /* CustomAggregationTests.swift in Sources */, + 19A17F907258E524B3CA2FAE /* SetterTests.swift in Sources */, + 19A17ABCF0EB4808BDC5B5FF /* RowTests.swift in Sources */, + 19A17BACF4C032513DE1F879 /* Connection+PragmaTests.swift in Sources */, + 19A173F25449876761347072 /* Connection+AttachTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -827,6 +1216,121 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B78B28842972005DD8CA /* Connection+Attach.swift in Sources */, + 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */, + 997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */, + 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */, + 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */, + DB58B21428FB864300F8EEA4 /* SchemaReader.swift in Sources */, + 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */, + DB7C5DA928D7C9B6006395CF /* SQLiteVersion.swift in Sources */, + 3D67B3EA1DB246D100A4F4C6 /* Value.swift in Sources */, + 3D67B3EB1DB246D100A4F4C6 /* FTS4.swift in Sources */, + DB58B21928FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */, + 3D67B3EC1DB246D100A4F4C6 /* RTree.swift in Sources */, + 3D67B3ED1DB246D100A4F4C6 /* FTS5.swift in Sources */, + 3D67B3EE1DB246D100A4F4C6 /* AggregateFunctions.swift in Sources */, + 3D67B3EF1DB246D100A4F4C6 /* Collation.swift in Sources */, + 3D67B3F01DB246D100A4F4C6 /* CoreFunctions.swift in Sources */, + 3D67B3F11DB246D100A4F4C6 /* CustomFunctions.swift in Sources */, + 3D67B3F21DB246D100A4F4C6 /* Expression.swift in Sources */, + 3D67B3F31DB246D100A4F4C6 /* Operators.swift in Sources */, + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */, + 3D67B3F41DB246D100A4F4C6 /* Query.swift in Sources */, + 3D67B3F51DB246D100A4F4C6 /* Schema.swift in Sources */, + 3D67B3F61DB246D100A4F4C6 /* Setter.swift in Sources */, + 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */, + 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */, + 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */, + 02A43A9B22738CF100FEC494 /* Backup.swift in Sources */, + 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */, + 19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */, + 19A17DF8D4F13A20F5D2269E /* Result.swift in Sources */, + 64A8EE462B095FBB00F583F7 /* WindowFunctions.swift in Sources */, + 19A17DFE05ED8B1F7C45F7EE /* SchemaChanger.swift in Sources */, + 19A17D1BEABA610ABF003D67 /* SchemaDefinitions.swift in Sources */, + 19A17A33EA026C2E2CEBAF36 /* Connection+Schema.swift in Sources */, + 19A178A8B2A34FB6B565DEDA /* Connection+Pragmas.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306BB2B61CEF500F9D46B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306BC2B61CEF500F9D46B /* CoreFunctions.swift in Sources */, + DEB306BD2B61CEF500F9D46B /* Coding.swift in Sources */, + DEB306BE2B61CEF500F9D46B /* RTree.swift in Sources */, + DEB306BF2B61CEF500F9D46B /* Blob.swift in Sources */, + DEB306C02B61CEF500F9D46B /* URIQueryParameter.swift in Sources */, + DEB306C12B61CEF500F9D46B /* Foundation.swift in Sources */, + DEB306C22B61CEF500F9D46B /* Connection.swift in Sources */, + DEB306C32B61CEF500F9D46B /* Expression.swift in Sources */, + DEB306C42B61CEF500F9D46B /* Helpers.swift in Sources */, + DEB306C52B61CEF500F9D46B /* Collation.swift in Sources */, + DEB306C62B61CEF500F9D46B /* Setter.swift in Sources */, + DEB306C72B61CEF500F9D46B /* Connection+Attach.swift in Sources */, + DEB306C82B61CEF500F9D46B /* CustomFunctions.swift in Sources */, + DEB306C92B61CEF500F9D46B /* FTS4.swift in Sources */, + DEB306CA2B61CEF500F9D46B /* Value.swift in Sources */, + DEB306CB2B61CEF500F9D46B /* Operators.swift in Sources */, + DEB306CC2B61CEF500F9D46B /* Schema.swift in Sources */, + DEB306CD2B61CEF500F9D46B /* Query.swift in Sources */, + DEB306CE2B61CEF500F9D46B /* Statement.swift in Sources */, + DEB306CF2B61CEF500F9D46B /* AggregateFunctions.swift in Sources */, + DEB306D02B61CEF500F9D46B /* FTS5.swift in Sources */, + DEB306D12B61CEF500F9D46B /* Cipher.swift in Sources */, + DEB306D22B61CEF500F9D46B /* Backup.swift in Sources */, + DEB306D32B61CEF500F9D46B /* Errors.swift in Sources */, + DEB306D42B61CEF500F9D46B /* DateAndTimeFunctions.swift in Sources */, + DEB306D52B61CEF500F9D46B /* SQLiteVersion.swift in Sources */, + DEB306D62B61CEF500F9D46B /* Result.swift in Sources */, + DEB306D72B61CEF500F9D46B /* Query+with.swift in Sources */, + DEB306D82B61CEF500F9D46B /* Connection+Aggregation.swift in Sources */, + DEB306D92B61CEF500F9D46B /* SQLiteFeature.swift in Sources */, + DEB306DA2B61CEF500F9D46B /* SchemaChanger.swift in Sources */, + DEB306DB2B61CEF500F9D46B /* SchemaDefinitions.swift in Sources */, + DEB306DC2B61CEF500F9D46B /* Connection+Schema.swift in Sources */, + DEB306DD2B61CEF500F9D46B /* Connection+Pragmas.swift in Sources */, + DEB306DE2B61CEF500F9D46B /* SchemaReader.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306EA2B61CF9500F9D46B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306EB2B61CF9500F9D46B /* TestHelpers.swift in Sources */, + DEB306EC2B61CF9500F9D46B /* FoundationTests.swift in Sources */, + DEB306ED2B61CF9500F9D46B /* Fixtures.swift in Sources */, + DEB306EE2B61CF9500F9D46B /* SchemaDefinitionsTests.swift in Sources */, + DEB306EF2B61CF9500F9D46B /* SchemaChangerTests.swift in Sources */, + DEB306F02B61CF9500F9D46B /* Connection+SchemaTests.swift in Sources */, + DEB306F12B61CF9500F9D46B /* FTS5Tests.swift in Sources */, + DEB306F22B61CF9500F9D46B /* FTSIntegrationTests.swift in Sources */, + DEB306F32B61CF9500F9D46B /* FTS4Tests.swift in Sources */, + DEB306F42B61CF9500F9D46B /* ExpressionTests.swift in Sources */, + DEB306F52B61CF9500F9D46B /* StatementTests.swift in Sources */, + DEB306F62B61CF9500F9D46B /* QueryTests.swift in Sources */, + DEB306F72B61CF9500F9D46B /* CipherTests.swift in Sources */, + DEB306F82B61CF9500F9D46B /* BlobTests.swift in Sources */, + DEB306F92B61CF9500F9D46B /* ConnectionTests.swift in Sources */, + DEB306FA2B61CF9500F9D46B /* CoreFunctionsTests.swift in Sources */, + DEB306FB2B61CF9500F9D46B /* DateAndTimeFunctionTests.swift in Sources */, + DEB306FC2B61CF9500F9D46B /* CustomFunctionsTests.swift in Sources */, + DEB306FD2B61CF9500F9D46B /* OperatorsTests.swift in Sources */, + DEB306FE2B61CF9500F9D46B /* ResultTests.swift in Sources */, + DEB306FF2B61CF9500F9D46B /* RTreeTests.swift in Sources */, + DEB307002B61CF9500F9D46B /* SchemaTests.swift in Sources */, + DEB307012B61CF9500F9D46B /* SelectTests.swift in Sources */, + DEB307022B61CF9500F9D46B /* ValueTests.swift in Sources */, + DEB307032B61CF9500F9D46B /* QueryIntegrationTests.swift in Sources */, + DEB307042B61CF9500F9D46B /* AggregateFunctionsTests.swift in Sources */, + DEB307052B61CF9500F9D46B /* CustomAggregationTests.swift in Sources */, + DEB307062B61CF9500F9D46B /* SetterTests.swift in Sources */, + DEB307072B61CF9500F9D46B /* RowTests.swift in Sources */, + DEB307082B61CF9500F9D46B /* Connection+PragmaTests.swift in Sources */, + DEB307092B61CF9500F9D46B /* Connection+AttachTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -835,24 +1339,41 @@ buildActionMask = 2147483647; files = ( EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */, + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */, EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */, EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */, EE247B111C3F06E900AE3E12 /* Expression.swift in Sources */, EE247B0C1C3F06E900AE3E12 /* Helpers.swift in Sources */, EE247B0E1C3F06E900AE3E12 /* Collation.swift in Sources */, EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */, + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */, + 64A8EE432B095FBB00F583F7 /* WindowFunctions.swift in Sources */, EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */, EE247B081C3F06E900AE3E12 /* Value.swift in Sources */, EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */, EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */, EE247B131C3F06E900AE3E12 /* Query.swift in Sources */, - EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */, EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */, EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */, 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */, + 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */, + 02A43A9822738CF100FEC494 /* Backup.swift in Sources */, + 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */, + 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */, + DB7C5DA628D7C9B6006395CF /* SQLiteVersion.swift in Sources */, + 19A173EFEF0B3BD0B3ED406C /* Result.swift in Sources */, + 997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */, + 19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */, + DB58B21628FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */, + 19A1773A335CAB9D0AE14E8E /* SchemaChanger.swift in Sources */, + 19A17BA13FD35F058787B7D3 /* SchemaDefinitions.swift in Sources */, + 19A174506543905D71BF0518 /* Connection+Schema.swift in Sources */, + 19A17018F250343BD0F9F4B0 /* Connection+Pragmas.swift in Sources */, + DB58B21128FB864300F8EEA4 /* SchemaReader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -860,22 +1381,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */, - EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */, - EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */, - EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */, EE247B171C3F127200AE3E12 /* TestHelpers.swift in Sources */, - EE247B281C3F137700AE3E12 /* ExpressionTests.swift in Sources */, - EE247B271C3F137700AE3E12 /* CustomFunctionsTests.swift in Sources */, - EE247B341C3F142E00AE3E12 /* StatementTests.swift in Sources */, - EE247B301C3F141E00AE3E12 /* RTreeTests.swift in Sources */, - EE247B231C3F137700AE3E12 /* BlobTests.swift in Sources */, - EE247B351C3F142E00AE3E12 /* ValueTests.swift in Sources */, - EE247B2F1C3F141E00AE3E12 /* QueryTests.swift in Sources */, - EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */, - EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */, - EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */, - 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */, + 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */, + 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */, + 19A17FE78A39E86F330420EC /* SchemaDefinitionsTests.swift in Sources */, + 19A177C25834473FAB32CF3B /* SchemaChangerTests.swift in Sources */, + 19A1725658E480B9B378F28B /* Connection+SchemaTests.swift in Sources */, + 19A178DA2BB5970778CCAF13 /* FTS5Tests.swift in Sources */, + 19A1755C49154C87304C9146 /* FTSIntegrationTests.swift in Sources */, + 19A17444861E1443143DEB44 /* FTS4Tests.swift in Sources */, + DBB93D5A2A22A373009BB96E /* SchemaReaderTests.swift in Sources */, + 19A17DD33C2E43DD6EE05A60 /* ExpressionTests.swift in Sources */, + 19A17D6EC40BC35A5DC81BA8 /* StatementTests.swift in Sources */, + 19A17E3F47DA087E2B76D087 /* QueryTests.swift in Sources */, + 19A17A7B3E3B7E76364A2AEE /* CipherTests.swift in Sources */, + 19A1782444437C7FC6B75CBC /* BlobTests.swift in Sources */, + 19A17CF65C0196E03BC64519 /* ConnectionTests.swift in Sources */, + 19A179BB9A6665B2B99DA546 /* CoreFunctionsTests.swift in Sources */, + 19A174118D11B93DA5DAAF79 /* DateAndTimeFunctionTests.swift in Sources */, + 19A17A7DF99B0379FD3396B1 /* CustomFunctionsTests.swift in Sources */, + 19A171F243A589C5EBC47937 /* OperatorsTests.swift in Sources */, + 19A173F429D7E46289EB2167 /* ResultTests.swift in Sources */, + 19A17B1D9B5CEBE9CE09280C /* RTreeTests.swift in Sources */, + 19A172F71EFD65342072D8D2 /* SchemaTests.swift in Sources */, + 19A17F7977364EC8CD33C3C3 /* SelectTests.swift in Sources */, + 19A17FD22EF43DF428DD93BA /* ValueTests.swift in Sources */, + 19A177AA5922527BBDC77CF9 /* QueryIntegrationTests.swift in Sources */, + 64B8E1702B09748000545AFB /* WindowFunctionsTests.swift in Sources */, + 19A179786A6826D58A70F8BC /* AggregateFunctionsTests.swift in Sources */, + 19A1793972BDDDB027C113BB /* CustomAggregationTests.swift in Sources */, + 19A1773155AC2BF2CA86A473 /* SetterTests.swift in Sources */, + 19A176B3316281F004F92276 /* RowTests.swift in Sources */, + 19A17FBAA26953EB854E790D /* Connection+PragmaTests.swift in Sources */, + 19A17026DCDCDA405B09A229 /* Connection+AttachTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -884,16 +1422,19 @@ buildActionMask = 2147483647; files = ( EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, - EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */, + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */, EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */, EE247B711C3F3FEC00AE3E12 /* Expression.swift in Sources */, EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */, EE247B6E1C3F3FEC00AE3E12 /* Collation.swift in Sources */, EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */, + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */, EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */, + 64A8EE442B095FBB00F583F7 /* WindowFunctions.swift in Sources */, EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */, EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */, EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */, @@ -902,6 +1443,20 @@ EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */, EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */, 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */, + 19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */, + 02A43A9922738CF100FEC494 /* Backup.swift in Sources */, + 19A17490543609FCED53CACC /* Errors.swift in Sources */, + 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */, + DB7C5DA728D7C9B6006395CF /* SQLiteVersion.swift in Sources */, + 19A17F1B3F0A3C96B5ED6D64 /* Result.swift in Sources */, + 997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */, + 19A170ACC97B19730FB7BA4D /* Connection+Aggregation.swift in Sources */, + DB58B21728FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */, + 19A177290558991BCC60E4E3 /* SchemaChanger.swift in Sources */, + 19A17B0DF1DDB6BBC9C95D64 /* SchemaDefinitions.swift in Sources */, + 19A17F0BF02896E1664F4090 /* Connection+Schema.swift in Sources */, + 19A1760CE25615CA015E2E5F /* Connection+Pragmas.swift in Sources */, + DB58B21228FB864300F8EEA4 /* SchemaReader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -909,22 +1464,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */, - EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */, - EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */, - EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */, - EE247B591C3F3FC700AE3E12 /* FTS4Tests.swift in Sources */, - EE247B531C3F3FC700AE3E12 /* AggregateFunctionsTests.swift in Sources */, - EE247B5F1C3F3FC700AE3E12 /* StatementTests.swift in Sources */, - EE247B5C1C3F3FC700AE3E12 /* RTreeTests.swift in Sources */, - EE247B571C3F3FC700AE3E12 /* CustomFunctionsTests.swift in Sources */, - EE247B601C3F3FC700AE3E12 /* ValueTests.swift in Sources */, - EE247B551C3F3FC700AE3E12 /* ConnectionTests.swift in Sources */, EE247B611C3F3FC700AE3E12 /* TestHelpers.swift in Sources */, - EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */, - EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */, - EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */, - 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */, + 19A178072B371489E6A1E839 /* FoundationTests.swift in Sources */, + 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */, + 19A17E80F736EEE8EE2AA4CE /* SchemaDefinitionsTests.swift in Sources */, + 19A1766AC10D13C4EFF349AD /* SchemaChangerTests.swift in Sources */, + 19A17B36ABC6006AB80F693C /* Connection+SchemaTests.swift in Sources */, + 19A1776BD5127DFDF847FF1F /* FTS5Tests.swift in Sources */, + 19A173088B85A7E18E8582A7 /* FTSIntegrationTests.swift in Sources */, + 19A178767223229E61C5066F /* FTS4Tests.swift in Sources */, + DBB93D5B2A22A373009BB96E /* SchemaReaderTests.swift in Sources */, + 19A1781CBA8968ABD3E00877 /* ExpressionTests.swift in Sources */, + 19A17923494236793893BF72 /* StatementTests.swift in Sources */, + 19A17A52BF29D27C9AA229E7 /* QueryTests.swift in Sources */, + 19A17CA6ADB78A2E545BF836 /* CipherTests.swift in Sources */, + 19A1714F7CF964D568AB14E0 /* BlobTests.swift in Sources */, + 19A173465F23C64DF3DF469B /* ConnectionTests.swift in Sources */, + 19A17D993398B8215B73E1EA /* CoreFunctionsTests.swift in Sources */, + 19A170AEBAA56DC3355A73B3 /* DateAndTimeFunctionTests.swift in Sources */, + 19A1716BF8E15F91A6B5CB7A /* CustomFunctionsTests.swift in Sources */, + 19A17482E6FC5E563F3E6A47 /* OperatorsTests.swift in Sources */, + 19A17912DB9D3AC8FECF948B /* ResultTests.swift in Sources */, + 19A17DAD5975D9367EAA46E2 /* RTreeTests.swift in Sources */, + 19A17F2096E83A3181E03317 /* SchemaTests.swift in Sources */, + 19A17DE1FCDB5695702AD24D /* SelectTests.swift in Sources */, + 19A1726002D24C14F876C8FE /* ValueTests.swift in Sources */, + 19A173389E53CB24DFA8CEDD /* QueryIntegrationTests.swift in Sources */, + 64B8E1712B09748000545AFB /* WindowFunctionsTests.swift in Sources */, + 19A170C56745F9D722A73D77 /* AggregateFunctionsTests.swift in Sources */, + 19A1772EBE65173EDFB1AFCA /* CustomAggregationTests.swift in Sources */, + 19A17E0ABA6C415F014CD51C /* SetterTests.swift in Sources */, + 19A170D938343E30119EDFB3 /* RowTests.swift in Sources */, + 19A178F9008614B8A8425635 /* Connection+PragmaTests.swift in Sources */, + 19A179BCD483DEA21661FD37 /* Connection+AttachTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -936,6 +1508,11 @@ target = 03A65E591C6BB0F50062603F /* SQLite tvOS */; targetProxy = 03A65E651C6BB0F60062603F /* PBXContainerItemProxy */; }; + DEB307152B61D07F00F9D46B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEB306B82B61CEF500F9D46B /* SQLite visionOS */; + targetProxy = DEB307142B61D07F00F9D46B /* PBXContainerItemProxy */; + }; EE247AE01C3F04ED00AE3E12 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EE247AD21C3F04ED00AE3E12 /* SQLite iOS */; @@ -952,64 +1529,72 @@ 03A65E6B1C6BB0F60062603F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = appletvos; SKIP_INSTALL = YES; - "SWIFT_INCLUDE_PATHS[sdk=appletvos*]" = "$(SRCROOT)/CocoaPods/appletvos"; - "SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]" = "$(SRCROOT)/CocoaPods/appletvsimulator"; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; 03A65E6C1C6BB0F60062603F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = appletvos; SKIP_INSTALL = YES; - "SWIFT_INCLUDE_PATHS[sdk=appletvos*]" = "$(SRCROOT)/CocoaPods/appletvos"; - "SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]" = "$(SRCROOT)/CocoaPods/appletvsimulator"; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; 03A65E6D1C6BB0F60062603F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; 03A65E6E1C6BB0F60062603F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -1018,19 +1603,22 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.2; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1039,19 +1627,97 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.2; + WATCHOS_DEPLOYMENT_TARGET = 4.0; + }; + name = Release; + }; + DEB306E32B61CEF500F9D46B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = xros; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Debug; + }; + DEB306E42B61CEF500F9D46B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = xros; + SKIP_INSTALL = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Release; + }; + DEB3070F2B61CF9500F9D46B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = xros; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Debug; + }; + DEB307102B61CF9500F9D46B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = xros; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; }; name = Release; }; @@ -1059,17 +1725,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1092,16 +1770,20 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MARKETING_VERSION = 0.15.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = ""; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1109,17 +1791,29 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1136,15 +1830,20 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MARKETING_VERSION = 0.15.4; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = ""; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; @@ -1153,20 +1852,21 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; - "SWIFT_INCLUDE_PATHS[sdk=iphoneos*]" = "$(SRCROOT)/CocoaPods/iphoneos"; - "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]" = "$(SRCROOT)/CocoaPods/iphonesimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; @@ -1175,39 +1875,46 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; - "SWIFT_INCLUDE_PATHS[sdk=iphoneos*]" = "$(SRCROOT)/CocoaPods/iphoneos"; - "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]" = "$(SRCROOT)/CocoaPods/iphonesimulator"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; EE247AEB1C3F04ED00AE3E12 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - INFOPLIST_FILE = SQLiteTests/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; EE247AEC1C3F04ED00AE3E12 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - INFOPLIST_FILE = SQLiteTests/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; @@ -1215,23 +1922,24 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_VERSION = A; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_INCLUDE_PATHS = ""; - "SWIFT_INCLUDE_PATHS[sdk=macosx*]" = "$(SRCROOT)/CocoaPods/macosx"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; @@ -1239,23 +1947,24 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_VERSION = A; - INFOPLIST_FILE = SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_INCLUDE_PATHS = ""; - "SWIFT_INCLUDE_PATHS[sdk=macosx*]" = "$(SRCROOT)/CocoaPods/macosx"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; @@ -1264,12 +1973,14 @@ buildSettings = { CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = SQLiteTests/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; @@ -1278,12 +1989,14 @@ buildSettings = { CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = SQLiteTests/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; @@ -1317,6 +2030,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DEB306E22B61CEF500F9D46B /* Build configuration list for PBXNativeTarget "SQLite visionOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEB306E32B61CEF500F9D46B /* Debug */, + DEB306E42B61CEF500F9D46B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEB3070E2B61CF9500F9D46B /* Build configuration list for PBXNativeTarget "SQLiteTests visionOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEB3070F2B61CF9500F9D46B /* Debug */, + DEB307102B61CF9500F9D46B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme index fa91858e..a0db21a2 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -40,17 +48,6 @@ - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -40,17 +48,6 @@ - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -40,17 +48,6 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme new file mode 100644 index 00000000..be1ef0ee --- /dev/null +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift deleted file mode 100644 index 04fdd901..00000000 --- a/SQLite/Core/Connection.swift +++ /dev/null @@ -1,690 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation.NSUUID -import Dispatch -#if SQLITE_SWIFT_STANDALONE -import sqlite3 -#else -import CSQLite -#endif - -/// A connection to SQLite. -public final class Connection { - - /// The location of a SQLite database. - public enum Location { - - /// An in-memory database (equivalent to `.URI(":memory:")`). - /// - /// See: - case InMemory - - /// A temporary, file-backed database (equivalent to `.URI("")`). - /// - /// See: - case Temporary - - /// A database located at the given URI filename (or path). - /// - /// See: - /// - /// - Parameter filename: A URI filename - case URI(String) - } - - public var handle: COpaquePointer { return _handle } - - private var _handle: COpaquePointer = nil - - /// Initializes a new SQLite connection. - /// - /// - Parameters: - /// - /// - location: The location of the database. Creates a new database if it - /// doesn’t already exist (unless in read-only mode). - /// - /// Default: `.InMemory`. - /// - /// - readonly: Whether or not to open the database in a read-only state. - /// - /// Default: `false`. - /// - /// - Returns: A new database connection. - public init(_ location: Location = .InMemory, readonly: Bool = false) throws { - let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE - try check(sqlite3_open_v2(location.description, &_handle, flags | SQLITE_OPEN_FULLMUTEX, nil)) - dispatch_queue_set_specific(queue, Connection.queueKey, queueContext, nil) - } - - /// Initializes a new connection to a database. - /// - /// - Parameters: - /// - /// - filename: The location of the database. Creates a new database if - /// it doesn’t already exist (unless in read-only mode). - /// - /// - readonly: Whether or not to open the database in a read-only state. - /// - /// Default: `false`. - /// - /// - Throws: `Result.Error` iff a connection cannot be established. - /// - /// - Returns: A new database connection. - public convenience init(_ filename: String, readonly: Bool = false) throws { - try self.init(.URI(filename), readonly: readonly) - } - - deinit { - sqlite3_close(handle) - } - - // MARK: - - - /// Whether or not the database was opened in a read-only state. - public var readonly: Bool { return sqlite3_db_readonly(handle, nil) == 1 } - - /// The last rowid inserted into the database via this connection. - public var lastInsertRowid: Int64? { - let rowid = sqlite3_last_insert_rowid(handle) - return rowid != 0 ? rowid : nil - } - - /// The last number of changes (inserts, updates, or deletes) made to the - /// database via this connection. - public var changes: Int { - return Int(sqlite3_changes(handle)) - } - - /// The total number of changes (inserts, updates, or deletes) made to the - /// database via this connection. - public var totalChanges: Int { - return Int(sqlite3_total_changes(handle)) - } - - // MARK: - Execute - - /// Executes a batch of SQL statements. - /// - /// - Parameter SQL: A batch of zero or more semicolon-separated SQL - /// statements. - /// - /// - Throws: `Result.Error` if query execution fails. - public func execute(SQL: String) throws { - try sync { try self.check(sqlite3_exec(self.handle, SQL, nil, nil, nil)) } - } - - // MARK: - Prepare - - /// Prepares a single SQL statement (with optional parameter bindings). - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A list of parameters to bind to the statement. - /// - /// - Returns: A prepared statement. - @warn_unused_result public func prepare(statement: String, _ bindings: Binding?...) throws -> Statement { - if !bindings.isEmpty { return try prepare(statement, bindings) } - return try Statement(self, statement) - } - - /// Prepares a single SQL statement and binds parameters to it. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A list of parameters to bind to the statement. - /// - /// - Returns: A prepared statement. - @warn_unused_result public func prepare(statement: String, _ bindings: [Binding?]) throws -> Statement { - return try prepare(statement).bind(bindings) - } - - /// Prepares a single SQL statement and binds parameters to it. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A dictionary of named parameters to bind to the statement. - /// - /// - Returns: A prepared statement. - @warn_unused_result public func prepare(statement: String, _ bindings: [String: Binding?]) throws -> Statement { - return try prepare(statement).bind(bindings) - } - - // MARK: - Run - - /// Runs a single SQL statement (with optional parameter bindings). - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A list of parameters to bind to the statement. - /// - /// - Throws: `Result.Error` if query execution fails. - /// - /// - Returns: The statement. - public func run(statement: String, _ bindings: Binding?...) throws -> Statement { - return try run(statement, bindings) - } - - /// Prepares, binds, and runs a single SQL statement. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A list of parameters to bind to the statement. - /// - /// - Throws: `Result.Error` if query execution fails. - /// - /// - Returns: The statement. - public func run(statement: String, _ bindings: [Binding?]) throws -> Statement { - return try prepare(statement).run(bindings) - } - - /// Prepares, binds, and runs a single SQL statement. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A dictionary of named parameters to bind to the statement. - /// - /// - Throws: `Result.Error` if query execution fails. - /// - /// - Returns: The statement. - public func run(statement: String, _ bindings: [String: Binding?]) throws -> Statement { - return try prepare(statement).run(bindings) - } - - // MARK: - Scalar - - /// Runs a single SQL statement (with optional parameter bindings), - /// returning the first value of the first row. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A list of parameters to bind to the statement. - /// - /// - Returns: The first value of the first row returned. - @warn_unused_result public func scalar(statement: String, _ bindings: Binding?...) throws -> Binding? { - return try scalar(statement, bindings) - } - - /// Runs a single SQL statement (with optional parameter bindings), - /// returning the first value of the first row. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A list of parameters to bind to the statement. - /// - /// - Returns: The first value of the first row returned. - @warn_unused_result public func scalar(statement: String, _ bindings: [Binding?]) throws -> Binding? { - return try prepare(statement).scalar(bindings) - } - - /// Runs a single SQL statement (with optional parameter bindings), - /// returning the first value of the first row. - /// - /// - Parameters: - /// - /// - statement: A single SQL statement. - /// - /// - bindings: A dictionary of named parameters to bind to the statement. - /// - /// - Returns: The first value of the first row returned. - @warn_unused_result public func scalar(statement: String, _ bindings: [String: Binding?]) throws -> Binding? { - return try prepare(statement).scalar(bindings) - } - - // MARK: - Transactions - - /// The mode in which a transaction acquires a lock. - public enum TransactionMode : String { - - /// Defers locking the database till the first read/write executes. - case Deferred = "DEFERRED" - - /// Immediately acquires a reserved lock on the database. - case Immediate = "IMMEDIATE" - - /// Immediately acquires an exclusive lock on all databases. - case Exclusive = "EXCLUSIVE" - - } - - // TODO: Consider not requiring a throw to roll back? - /// Runs a transaction with the given mode. - /// - /// - Note: Transactions cannot be nested. To nest transactions, see - /// `savepoint()`, instead. - /// - /// - Parameters: - /// - /// - mode: The mode in which a transaction acquires a lock. - /// - /// Default: `.Deferred` - /// - /// - block: A closure to run SQL statements within the transaction. - /// The transaction will be committed when the block returns. The block - /// must throw to roll the transaction back. - /// - /// - Throws: `Result.Error`, and rethrows. - public func transaction(mode: TransactionMode = .Deferred, block: () throws -> Void) throws { - try transaction("BEGIN \(mode.rawValue) TRANSACTION", block, "COMMIT TRANSACTION", or: "ROLLBACK TRANSACTION") - } - - // TODO: Consider not requiring a throw to roll back? - // TODO: Consider removing ability to set a name? - /// Runs a transaction with the given savepoint name (if omitted, it will - /// generate a UUID). - /// - /// - SeeAlso: `transaction()`. - /// - /// - Parameters: - /// - /// - savepointName: A unique identifier for the savepoint (optional). - /// - /// - block: A closure to run SQL statements within the transaction. - /// The savepoint will be released (committed) when the block returns. - /// The block must throw to roll the savepoint back. - /// - /// - Throws: `SQLite.Result.Error`, and rethrows. - public func savepoint(name: String = NSUUID().UUIDString, block: () throws -> Void) throws { - let name = name.quote("'") - let savepoint = "SAVEPOINT \(name)" - - try transaction(savepoint, block, "RELEASE \(savepoint)", or: "ROLLBACK TO \(savepoint)") - } - - private func transaction(begin: String, _ block: () throws -> Void, _ commit: String, or rollback: String) throws { - return try sync { - try self.run(begin) - do { - try block() - } catch { - try self.run(rollback) - throw error - } - try self.run(commit) - } - } - - /// Interrupts any long-running queries. - public func interrupt() { - sqlite3_interrupt(handle) - } - - // MARK: - Handlers - - /// The number of seconds a connection will attempt to retry a statement - /// after encountering a busy signal (lock). - public var busyTimeout: Double = 0 { - didSet { - sqlite3_busy_timeout(handle, Int32(busyTimeout * 1_000)) - } - } - - /// Sets a handler to call after encountering a busy signal (lock). - /// - /// - Parameter callback: This block is executed during a lock in which a - /// busy error would otherwise be returned. It’s passed the number of - /// times it’s been called for this lock. If it returns `true`, it will - /// try again. If it returns `false`, no further attempts will be made. - public func busyHandler(callback: ((tries: Int) -> Bool)?) { - guard let callback = callback else { - sqlite3_busy_handler(handle, nil, nil) - busyHandler = nil - return - } - - let box: BusyHandler = { callback(tries: Int($0)) ? 1 : 0 } - sqlite3_busy_handler(handle, { callback, tries in - unsafeBitCast(callback, BusyHandler.self)(tries) - }, unsafeBitCast(box, UnsafeMutablePointer.self)) - busyHandler = box - } - private typealias BusyHandler = @convention(block) Int32 -> Int32 - private var busyHandler: BusyHandler? - - /// Sets a handler to call when a statement is executed with the compiled - /// SQL. - /// - /// - Parameter callback: This block is invoked when a statement is executed - /// with the compiled SQL as its argument. - /// - /// db.trace { SQL in print(SQL) } - public func trace(callback: (String -> Void)?) { - guard let callback = callback else { - sqlite3_trace(handle, nil, nil) - trace = nil - return - } - - let box: Trace = { callback(String.fromCString($0)!) } - sqlite3_trace(handle, { callback, SQL in - unsafeBitCast(callback, Trace.self)(SQL) - }, unsafeBitCast(box, UnsafeMutablePointer.self)) - trace = box - } - private typealias Trace = @convention(block) UnsafePointer -> Void - private var trace: Trace? - - /// Registers a callback to be invoked whenever a row is inserted, updated, - /// or deleted in a rowid table. - /// - /// - Parameter callback: A callback invoked with the `Operation` (one of - /// `.Insert`, `.Update`, or `.Delete`), database name, table name, and - /// rowid. - public func updateHook(callback: ((operation: Operation, db: String, table: String, rowid: Int64) -> Void)?) { - guard let callback = callback else { - sqlite3_update_hook(handle, nil, nil) - updateHook = nil - return - } - - let box: UpdateHook = { - callback( - operation: Operation(rawValue: $0), - db: String.fromCString($1)!, - table: String.fromCString($2)!, - rowid: $3 - ) - } - sqlite3_update_hook(handle, { callback, operation, db, table, rowid in - unsafeBitCast(callback, UpdateHook.self)(operation, db, table, rowid) - }, unsafeBitCast(box, UnsafeMutablePointer.self)) - updateHook = box - } - private typealias UpdateHook = @convention(block) (Int32, UnsafePointer, UnsafePointer, Int64) -> Void - private var updateHook: UpdateHook? - - /// Registers a callback to be invoked whenever a transaction is committed. - /// - /// - Parameter callback: A callback invoked whenever a transaction is - /// committed. If this callback throws, the transaction will be rolled - /// back. - public func commitHook(callback: (() throws -> Void)?) { - guard let callback = callback else { - sqlite3_commit_hook(handle, nil, nil) - commitHook = nil - return - } - - let box: CommitHook = { - do { - try callback() - } catch { - return 1 - } - return 0 - } - sqlite3_commit_hook(handle, { callback in - unsafeBitCast(callback, CommitHook.self)() - }, unsafeBitCast(box, UnsafeMutablePointer.self)) - commitHook = box - } - private typealias CommitHook = @convention(block) () -> Int32 - private var commitHook: CommitHook? - - /// Registers a callback to be invoked whenever a transaction rolls back. - /// - /// - Parameter callback: A callback invoked when a transaction is rolled - /// back. - public func rollbackHook(callback: (() -> Void)?) { - guard let callback = callback else { - sqlite3_rollback_hook(handle, nil, nil) - rollbackHook = nil - return - } - - let box: RollbackHook = { callback() } - sqlite3_rollback_hook(handle, { callback in - unsafeBitCast(callback, RollbackHook.self)() - }, unsafeBitCast(box, UnsafeMutablePointer.self)) - rollbackHook = box - } - private typealias RollbackHook = @convention(block) () -> Void - private var rollbackHook: RollbackHook? - - /// Creates or redefines a custom SQL function. - /// - /// - Parameters: - /// - /// - function: The name of the function to create or redefine. - /// - /// - argumentCount: The number of arguments that the function takes. If - /// `nil`, the function may take any number of arguments. - /// - /// Default: `nil` - /// - /// - deterministic: Whether or not the function is deterministic (_i.e._ - /// the function always returns the same result for a given input). - /// - /// Default: `false` - /// - /// - block: A block of code to run when the function is called. The block - /// is called with an array of raw SQL values mapped to the function’s - /// parameters and should return a raw SQL value (or nil). - public func createFunction(function: String, argumentCount: UInt? = nil, deterministic: Bool = false, _ block: (args: [Binding?]) -> Binding?) { - let argc = argumentCount.map { Int($0) } ?? -1 - let box: Function = { context, argc, argv in - let arguments: [Binding?] = (0...self), { context, argc, value in - unsafeBitCast(sqlite3_user_data(context), Function.self)(context, argc, value) - }, nil, nil, nil) - if functions[function] == nil { self.functions[function] = [:] } - functions[function]?[argc] = box - } - private typealias Function = @convention(block) (COpaquePointer, Int32, UnsafeMutablePointer) -> Void - private var functions = [String: [Int: Function]]() - - /// The return type of a collation comparison function. - public typealias ComparisonResult = NSComparisonResult - - /// Defines a new collating sequence. - /// - /// - Parameters: - /// - /// - collation: The name of the collation added. - /// - /// - block: A collation function that takes two strings and returns the - /// comparison result. - public func createCollation(collation: String, _ block: (lhs: String, rhs: String) -> ComparisonResult) throws { - let box: Collation = { lhs, rhs in - Int32(block(lhs: String.fromCString(UnsafePointer(lhs))!, rhs: String.fromCString(UnsafePointer(rhs))!).rawValue) - } - try check(sqlite3_create_collation_v2(handle, collation, SQLITE_UTF8, unsafeBitCast(box, UnsafeMutablePointer.self), { callback, _, lhs, _, rhs in - unsafeBitCast(callback, Collation.self)(lhs, rhs) - }, nil)) - collations[collation] = box - } - private typealias Collation = @convention(block) (UnsafePointer, UnsafePointer) -> Int32 - private var collations = [String: Collation]() - - // MARK: - Error Handling - - func sync(block: () throws -> T) rethrows -> T { - var success: T? - var failure: ErrorType? - - let box: () -> Void = { - do { - success = try block() - } catch { - failure = error - } - } - - if dispatch_get_specific(Connection.queueKey) == queueContext { - box() - } else { - dispatch_sync(queue, box) // FIXME: rdar://problem/21389236 - } - - if let failure = failure { - try { () -> Void in throw failure }() - } - - return success! - } - - func check(resultCode: Int32, statement: Statement? = nil) throws -> Int32 { - guard let error = Result(errorCode: resultCode, connection: self, statement: statement) else { - return resultCode - } - - throw error - } - - private var queue = dispatch_queue_create("SQLite.Database", DISPATCH_QUEUE_SERIAL) - - private static let queueKey = unsafeBitCast(Connection.self, UnsafePointer.self) - - private lazy var queueContext: UnsafeMutablePointer = unsafeBitCast(self, UnsafeMutablePointer.self) - -} - -extension Connection : CustomStringConvertible { - - public var description: String { - return String.fromCString(sqlite3_db_filename(handle, nil))! - } - -} - -extension Connection.Location : CustomStringConvertible { - - public var description: String { - switch self { - case .InMemory: - return ":memory:" - case .Temporary: - return "" - case .URI(let URI): - return URI - } - } - -} - -/// An SQL operation passed to update callbacks. -public enum Operation { - - /// An INSERT operation. - case Insert - - /// An UPDATE operation. - case Update - - /// A DELETE operation. - case Delete - - private init(rawValue: Int32) { - switch rawValue { - case SQLITE_INSERT: - self = .Insert - case SQLITE_UPDATE: - self = .Update - case SQLITE_DELETE: - self = .Delete - default: - fatalError("unhandled operation code: \(rawValue)") - } - } - -} - -public enum Result : ErrorType { - - private static let successCodes: Set = [SQLITE_OK, SQLITE_ROW, SQLITE_DONE] - - case Error(message: String, code: Int32, statement: Statement?) - - init?(errorCode: Int32, connection: Connection, statement: Statement? = nil) { - guard !Result.successCodes.contains(errorCode) else { return nil } - - let message = String.fromCString(sqlite3_errmsg(connection.handle))! - self = Error(message: message, code: errorCode, statement: statement) - } - -} - -extension Result : CustomStringConvertible { - - public var description: String { - switch self { - case let .Error(message, _, statement): - guard let statement = statement else { return message } - - return "\(message) (\(statement))" - } - } - -} diff --git a/SQLite/Core/SQLite-Bridging.h b/SQLite/Core/SQLite-Bridging.h deleted file mode 100644 index d15e8d56..00000000 --- a/SQLite/Core/SQLite-Bridging.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -@import Foundation; - -#ifndef COCOAPODS -#import "sqlite3.h" -#endif - -typedef struct SQLiteHandle SQLiteHandle; // CocoaPods workaround - -NS_ASSUME_NONNULL_BEGIN -typedef NSString * _Nullable (^_SQLiteTokenizerNextCallback)(const char * input, int * inputOffset, int * inputLength); -int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * module, const char * tokenizer, _Nullable _SQLiteTokenizerNextCallback callback); -NS_ASSUME_NONNULL_END - diff --git a/SQLite/Core/SQLite-Bridging.m b/SQLite/Core/SQLite-Bridging.m deleted file mode 100644 index a5702cc7..00000000 --- a/SQLite/Core/SQLite-Bridging.m +++ /dev/null @@ -1,139 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -#import "SQLite-Bridging.h" -#import "sqlite3.h" -#import "fts3_tokenizer.h" - -#pragma mark - FTS - -typedef struct __SQLiteTokenizer { - sqlite3_tokenizer base; - __unsafe_unretained _SQLiteTokenizerNextCallback callback; -} __SQLiteTokenizer; - -typedef struct __SQLiteTokenizerCursor { - void * base; - const char * input; - int inputOffset; - int inputLength; - int idx; -} __SQLiteTokenizerCursor; - -static NSMutableDictionary * __SQLiteTokenizerMap; - -static int __SQLiteTokenizerCreate(int argc, const char * const * argv, sqlite3_tokenizer ** ppTokenizer) { - __SQLiteTokenizer * tokenizer = (__SQLiteTokenizer *)sqlite3_malloc(sizeof(__SQLiteTokenizer)); - if (!tokenizer) { - return SQLITE_NOMEM; - } - memset(tokenizer, 0, sizeof(* tokenizer)); - - NSString * key = [NSString stringWithUTF8String:argv[0]]; - tokenizer->callback = [__SQLiteTokenizerMap objectForKey:key]; - if (!tokenizer->callback) { - return SQLITE_ERROR; - } - - *ppTokenizer = &tokenizer->base; - return SQLITE_OK; -} - -static int __SQLiteTokenizerDestroy(sqlite3_tokenizer * pTokenizer) { - sqlite3_free(pTokenizer); - return SQLITE_OK; -} - -static int __SQLiteTokenizerOpen(sqlite3_tokenizer * pTokenizer, const char * pInput, int nBytes, sqlite3_tokenizer_cursor ** ppCursor) { - __SQLiteTokenizerCursor * cursor = (__SQLiteTokenizerCursor *)sqlite3_malloc(sizeof(__SQLiteTokenizerCursor)); - if (!cursor) { - return SQLITE_NOMEM; - } - - cursor->input = pInput; - cursor->inputOffset = 0; - cursor->inputLength = 0; - cursor->idx = 0; - - *ppCursor = (sqlite3_tokenizer_cursor *)cursor; - return SQLITE_OK; -} - -static int __SQLiteTokenizerClose(sqlite3_tokenizer_cursor * pCursor) { - sqlite3_free(pCursor); - return SQLITE_OK; -} - -static int __SQLiteTokenizerNext(sqlite3_tokenizer_cursor * pCursor, const char ** ppToken, int * pnBytes, int * piStartOffset, int * piEndOffset, int * piPosition) { - __SQLiteTokenizerCursor * cursor = (__SQLiteTokenizerCursor *)pCursor; - __SQLiteTokenizer * tokenizer = (__SQLiteTokenizer *)cursor->base; - - cursor->inputOffset += cursor->inputLength; - const char * input = cursor->input + cursor->inputOffset; - const char * token = [tokenizer->callback(input, &cursor->inputOffset, &cursor->inputLength) cStringUsingEncoding:NSUTF8StringEncoding]; - if (!token) { - return SQLITE_DONE; - } - - *ppToken = token; - *pnBytes = (int)strlen(token); - *piStartOffset = cursor->inputOffset; - *piEndOffset = cursor->inputOffset + cursor->inputLength; - *piPosition = cursor->idx++; - return SQLITE_OK; -} - -static const sqlite3_tokenizer_module __SQLiteTokenizerModule = { - 0, - __SQLiteTokenizerCreate, - __SQLiteTokenizerDestroy, - __SQLiteTokenizerOpen, - __SQLiteTokenizerClose, - __SQLiteTokenizerNext -}; - -int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * moduleName, const char * submoduleName, _SQLiteTokenizerNextCallback callback) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - __SQLiteTokenizerMap = [NSMutableDictionary new]; - }); - - sqlite3_stmt * stmt; - int status = sqlite3_prepare_v2((sqlite3 *)db, "SELECT fts3_tokenizer(?, ?)", -1, &stmt, 0); - if (status != SQLITE_OK ){ - return status; - } - const sqlite3_tokenizer_module * pModule = &__SQLiteTokenizerModule; - sqlite3_bind_text(stmt, 1, moduleName, -1, SQLITE_STATIC); - sqlite3_bind_blob(stmt, 2, &pModule, sizeof(pModule), SQLITE_STATIC); - sqlite3_step(stmt); - status = sqlite3_finalize(stmt); - if (status != SQLITE_OK ){ - return status; - } - - [__SQLiteTokenizerMap setObject:[callback copy] forKey:[NSString stringWithUTF8String:submoduleName]]; - - return SQLITE_OK; -} diff --git a/SQLite/Core/fts3_tokenizer.h b/SQLite/Core/fts3_tokenizer.h deleted file mode 100644 index d8a1e44b..00000000 --- a/SQLite/Core/fts3_tokenizer.h +++ /dev/null @@ -1,161 +0,0 @@ -/* -** 2006 July 10 -** -** The author disclaims copyright to this source code. -** -************************************************************************* -** Defines the interface to tokenizers used by fulltext-search. There -** are three basic components: -** -** sqlite3_tokenizer_module is a singleton defining the tokenizer -** interface functions. This is essentially the class structure for -** tokenizers. -** -** sqlite3_tokenizer is used to define a particular tokenizer, perhaps -** including customization information defined at creation time. -** -** sqlite3_tokenizer_cursor is generated by a tokenizer to generate -** tokens from a particular input. -*/ -#ifndef _FTS3_TOKENIZER_H_ -#define _FTS3_TOKENIZER_H_ - -/* TODO(shess) Only used for SQLITE_OK and SQLITE_DONE at this time. -** If tokenizers are to be allowed to call sqlite3_*() functions, then -** we will need a way to register the API consistently. -*/ -#import "sqlite3.h" - -/* -** Structures used by the tokenizer interface. When a new tokenizer -** implementation is registered, the caller provides a pointer to -** an sqlite3_tokenizer_module containing pointers to the callback -** functions that make up an implementation. -** -** When an fts3 table is created, it passes any arguments passed to -** the tokenizer clause of the CREATE VIRTUAL TABLE statement to the -** sqlite3_tokenizer_module.xCreate() function of the requested tokenizer -** implementation. The xCreate() function in turn returns an -** sqlite3_tokenizer structure representing the specific tokenizer to -** be used for the fts3 table (customized by the tokenizer clause arguments). -** -** To tokenize an input buffer, the sqlite3_tokenizer_module.xOpen() -** method is called. It returns an sqlite3_tokenizer_cursor object -** that may be used to tokenize a specific input buffer based on -** the tokenization rules supplied by a specific sqlite3_tokenizer -** object. -*/ -typedef struct sqlite3_tokenizer_module sqlite3_tokenizer_module; -typedef struct sqlite3_tokenizer sqlite3_tokenizer; -typedef struct sqlite3_tokenizer_cursor sqlite3_tokenizer_cursor; - -struct sqlite3_tokenizer_module { - - /* - ** Structure version. Should always be set to 0 or 1. - */ - int iVersion; - - /* - ** Create a new tokenizer. The values in the argv[] array are the - ** arguments passed to the "tokenizer" clause of the CREATE VIRTUAL - ** TABLE statement that created the fts3 table. For example, if - ** the following SQL is executed: - ** - ** CREATE .. USING fts3( ... , tokenizer arg1 arg2) - ** - ** then argc is set to 2, and the argv[] array contains pointers - ** to the strings "arg1" and "arg2". - ** - ** This method should return either SQLITE_OK (0), or an SQLite error - ** code. If SQLITE_OK is returned, then *ppTokenizer should be set - ** to point at the newly created tokenizer structure. The generic - ** sqlite3_tokenizer.pModule variable should not be initialized by - ** this callback. The caller will do so. - */ - int (*xCreate)( - int argc, /* Size of argv array */ - const char *const*argv, /* Tokenizer argument strings */ - sqlite3_tokenizer **ppTokenizer /* OUT: Created tokenizer */ - ); - - /* - ** Destroy an existing tokenizer. The fts3 module calls this method - ** exactly once for each successful call to xCreate(). - */ - int (*xDestroy)(sqlite3_tokenizer *pTokenizer); - - /* - ** Create a tokenizer cursor to tokenize an input buffer. The caller - ** is responsible for ensuring that the input buffer remains valid - ** until the cursor is closed (using the xClose() method). - */ - int (*xOpen)( - sqlite3_tokenizer *pTokenizer, /* Tokenizer object */ - const char *pInput, int nBytes, /* Input buffer */ - sqlite3_tokenizer_cursor **ppCursor /* OUT: Created tokenizer cursor */ - ); - - /* - ** Destroy an existing tokenizer cursor. The fts3 module calls this - ** method exactly once for each successful call to xOpen(). - */ - int (*xClose)(sqlite3_tokenizer_cursor *pCursor); - - /* - ** Retrieve the next token from the tokenizer cursor pCursor. This - ** method should either return SQLITE_OK and set the values of the - ** "OUT" variables identified below, or SQLITE_DONE to indicate that - ** the end of the buffer has been reached, or an SQLite error code. - ** - ** *ppToken should be set to point at a buffer containing the - ** normalized version of the token (i.e. after any case-folding and/or - ** stemming has been performed). *pnBytes should be set to the length - ** of this buffer in bytes. The input text that generated the token is - ** identified by the byte offsets returned in *piStartOffset and - ** *piEndOffset. *piStartOffset should be set to the index of the first - ** byte of the token in the input buffer. *piEndOffset should be set - ** to the index of the first byte just past the end of the token in - ** the input buffer. - ** - ** The buffer *ppToken is set to point at is managed by the tokenizer - ** implementation. It is only required to be valid until the next call - ** to xNext() or xClose(). - */ - /* TODO(shess) current implementation requires pInput to be - ** nul-terminated. This should either be fixed, or pInput/nBytes - ** should be converted to zInput. - */ - int (*xNext)( - sqlite3_tokenizer_cursor *pCursor, /* Tokenizer cursor */ - const char **ppToken, int *pnBytes, /* OUT: Normalized text for token */ - int *piStartOffset, /* OUT: Byte offset of token in input buffer */ - int *piEndOffset, /* OUT: Byte offset of end of token in input buffer */ - int *piPosition /* OUT: Number of tokens returned before this one */ - ); - - /*********************************************************************** - ** Methods below this point are only available if iVersion>=1. - */ - - /* - ** Configure the language id of a tokenizer cursor. - */ - int (*xLanguageid)(sqlite3_tokenizer_cursor *pCsr, int iLangid); -}; - -struct sqlite3_tokenizer { - const sqlite3_tokenizer_module *pModule; /* The module for this tokenizer */ - /* Tokenizer implementations will typically add additional fields */ -}; - -struct sqlite3_tokenizer_cursor { - sqlite3_tokenizer *pTokenizer; /* Tokenizer for this cursor. */ - /* Tokenizer implementations will typically add additional fields */ -}; - -int fts3_global_term_cnt(int iTerm, int iCol); -int fts3_term_cnt(int iTerm, int iCol); - - -#endif /* _FTS3_TOKENIZER_H_ */ diff --git a/SQLite/Foundation.swift b/SQLite/Foundation.swift deleted file mode 100644 index 52b2ea02..00000000 --- a/SQLite/Foundation.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -extension NSData : Value { - - public class var declaredDatatype: String { - return Blob.declaredDatatype - } - - public class func fromDatatypeValue(dataValue: Blob) -> NSData { - return NSData(bytes: dataValue.bytes, length: dataValue.bytes.count) - } - - public var datatypeValue: Blob { - return Blob(bytes: bytes, length: length) - } - -} - -extension NSDate : Value { - - public class var declaredDatatype: String { - return String.declaredDatatype - } - - public class func fromDatatypeValue(stringValue: String) -> NSDate { - return dateFormatter.dateFromString(stringValue)! - } - - public var datatypeValue: String { - return dateFormatter.stringFromDate(self) - } - -} - -/// A global date formatter used to serialize and deserialize `NSDate` objects. -/// If multiple date formats are used in an application’s database(s), use a -/// custom `Value` type per additional format. -public var dateFormatter: NSDateFormatter = { - let formatter = NSDateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") - formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) - return formatter -}() - -// FIXME: rdar://problem/18673897 // subscript… - -extension QueryType { - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - -} - -extension Row { - - public subscript(column: Expression) -> NSData { - return get(column) - } - public subscript(column: Expression) -> NSData? { - return get(column) - } - - public subscript(column: Expression) -> NSDate { - return get(column) - } - public subscript(column: Expression) -> NSDate? { - return get(column) - } - -} diff --git a/SQLite/Typed/CustomFunctions.swift b/SQLite/Typed/CustomFunctions.swift deleted file mode 100644 index 068d0340..00000000 --- a/SQLite/Typed/CustomFunctions.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -public extension Connection { - - /// Creates or redefines a custom SQL function. - /// - /// - Parameters: - /// - /// - function: The name of the function to create or redefine. - /// - /// - deterministic: Whether or not the function is deterministic (_i.e._ - /// the function always returns the same result for a given input). - /// - /// Default: `false` - /// - /// - block: A block of code to run when the function is called. - /// The assigned types must be explicit. - /// - /// - Returns: A closure returning an SQL expression to call the function. - public func createFunction(function: String, deterministic: Bool = false, _ block: () -> Z) throws -> (() -> Expression) { - let fn = try createFunction(function, 0, deterministic) { _ in block() } - return { fn([]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: () -> Z?) throws -> (() -> Expression) { - let fn = try createFunction(function, 0, deterministic) { _ in block() } - return { fn([]) } - } - - // MARK: - - - public func createFunction(function: String, deterministic: Bool = false, _ block: A -> Z) throws -> (Expression -> Expression) { - let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0])) } - return { arg in fn([arg]) } - } - - public func createFunction(function function: String, deterministic: Bool = false, _ block: A? -> Z) throws -> (Expression -> Expression) { - let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value)) } - return { arg in fn([arg]) } - } - - public func createFunction(function function: String, deterministic: Bool = false, _ block: A -> Z?) throws -> (Expression -> Expression) { - let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0])) } - return { arg in fn([arg]) } - } - - public func createFunction(function function: String, deterministic: Bool = false, _ block: A? -> Z?) throws -> (Expression -> Expression) { - let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value)) } - return { arg in fn([arg]) } - } - - // MARK: - - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A, B) -> Z) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), value(args[1])) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A?, B) -> Z) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), value(args[1])) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A, B?) -> Z) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), args[1].map(value)) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A, B) -> Z?) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), value(args[1])) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), args[1].map(value)) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A?, B) -> Z?) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), value(args[1])) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A, B?) -> Z?) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), args[1].map(value)) } - return { a, b in fn([a, b]) } - } - - public func createFunction(function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z?) throws -> (Expression, Expression) -> Expression { - let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), args[1].map(value)) } - return { a, b in fn([a, b]) } - } - - // MARK: - - - private func createFunction(function: String, _ argumentCount: UInt, _ deterministic: Bool, _ block: [Binding?] -> Z) throws -> ([Expressible] -> Expression) { - createFunction(function, argumentCount: argumentCount, deterministic: deterministic) { arguments in - block(arguments).datatypeValue - } - return { arguments in - function.quote().wrap(", ".join(arguments)) - } - } - - private func createFunction(function: String, _ argumentCount: UInt, _ deterministic: Bool, _ block: [Binding?] -> Z?) throws -> ([Expressible] -> Expression) { - createFunction(function, argumentCount: argumentCount, deterministic: deterministic) { arguments in - block(arguments)?.datatypeValue - } - return { arguments in - function.quote().wrap(", ".join(arguments)) - } - } - -} diff --git a/SQLite/Typed/Operators.swift b/SQLite/Typed/Operators.swift deleted file mode 100644 index 816560a5..00000000 --- a/SQLite/Typed/Operators.swift +++ /dev/null @@ -1,541 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -// TODO: use `@warn_unused_result` by the time operator functions support it - -public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) -} - -public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: String) -> Expression { - return "||".infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: String) -> Expression { - return "||".infix(lhs, rhs) -} -public func +(lhs: String, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) -} -public func +(lhs: String, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) -} - -// MARK: - - -public func +(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func +(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func -(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func -(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func *(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func *(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func /(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func /(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public prefix func -(rhs: Expression) -> Expression { - return wrap(rhs) -} -public prefix func -(rhs: Expression) -> Expression { - return wrap(rhs) -} - -// MARK: - - -public func %(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func %(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func <<(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <<(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func >>(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >>(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func &(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func &(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func |(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func |(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func ^(lhs: Expression, rhs: Expression) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: Expression, rhs: Expression) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: Expression, rhs: Expression) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: Expression, rhs: Expression) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: Expression, rhs: V) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: Expression, rhs: V) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: V, rhs: Expression) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} -public func ^(lhs: V, rhs: Expression) -> Expression { - return (~(lhs & rhs)) & (lhs | rhs) -} - -public prefix func ~(rhs: Expression) -> Expression { - return wrap(rhs) -} -public prefix func ~(rhs: Expression) -> Expression { - return wrap(rhs) -} - -// MARK: - - -public func ==(lhs: Expression, rhs: Expression) -> Expression { - return "=".infix(lhs, rhs) -} -public func ==(lhs: Expression, rhs: Expression) -> Expression { - return "=".infix(lhs, rhs) -} -public func ==(lhs: Expression, rhs: Expression) -> Expression { - return "=".infix(lhs, rhs) -} -public func ==(lhs: Expression, rhs: Expression) -> Expression { - return "=".infix(lhs, rhs) -} -public func ==(lhs: Expression, rhs: V) -> Expression { - return "=".infix(lhs, rhs) -} -public func ==(lhs: Expression, rhs: V?) -> Expression { - guard let rhs = rhs else { return "IS".infix(lhs, Expression(value: nil)) } - return "=".infix(lhs, rhs) -} -public func ==(lhs: V, rhs: Expression) -> Expression { - return "=".infix(lhs, rhs) -} -public func ==(lhs: V?, rhs: Expression) -> Expression { - guard let lhs = lhs else { return "IS".infix(Expression(value: nil), rhs) } - return "=".infix(lhs, rhs) -} - -public func !=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func !=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func !=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func !=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func !=(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func !=(lhs: Expression, rhs: V?) -> Expression { - guard let rhs = rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } - return infix(lhs, rhs) -} -public func !=(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func !=(lhs: V?, rhs: Expression) -> Expression { - guard let lhs = lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } - return infix(lhs, rhs) -} - -public func >(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func >=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func >=(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func <(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func <=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: Expression, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: Expression, rhs: V) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} -public func <=(lhs: V, rhs: Expression) -> Expression { - return infix(lhs, rhs) -} - -public func ~=, V.Datatype == I.Bound>(lhs: I, rhs: Expression) -> Expression { - return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.start, lhs.end]) -} -public func ~=, V.Datatype == I.Bound>(lhs: I, rhs: Expression) -> Expression { - return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.start, lhs.end]) -} - -// MARK: - - -public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Expression, rhs: Bool) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Expression, rhs: Bool) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Bool, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) -} -public func &&(lhs: Bool, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) -} - -public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Expression, rhs: Bool) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Expression, rhs: Bool) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Bool, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) -} -public func ||(lhs: Bool, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) -} - -public prefix func !(rhs: Expression) -> Expression { - return "NOT ".wrap(rhs) -} -public prefix func !(rhs: Expression) -> Expression { - return "NOT ".wrap(rhs) -} diff --git a/SQLite/Typed/Schema.swift b/SQLite/Typed/Schema.swift deleted file mode 100644 index 16e11c16..00000000 --- a/SQLite/Typed/Schema.swift +++ /dev/null @@ -1,519 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -extension SchemaType { - - // MARK: - DROP TABLE / VIEW / VIRTUAL TABLE - - public func drop(ifExists ifExists: Bool = false) -> String { - return drop("TABLE", tableName(), ifExists) - } - -} - -extension Table { - - // MARK: - CREATE TABLE - - public func create(temporary temporary: Bool = false, ifNotExists: Bool = false, @noescape block: TableBuilder -> Void) -> String { - let builder = TableBuilder() - - block(builder) - - let clauses: [Expressible?] = [ - create(Table.identifier, tableName(), temporary ? .Temporary : nil, ifNotExists), - "".wrap(builder.definitions) as Expression - ] - - return " ".join(clauses.flatMap { $0 }).asSQL() - } - - public func create(query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { - let clauses: [Expressible?] = [ - create(Table.identifier, tableName(), temporary ? .Temporary : nil, ifNotExists), - Expression(literal: "AS"), - query - ] - - return " ".join(clauses.flatMap { $0 }).asSQL() - } - - // MARK: - ALTER TABLE … ADD COLUMN - - public func addColumn(name: Expression, check: Expression? = nil, defaultValue: V) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) - } - - public func addColumn(name: Expression, check: Expression, defaultValue: V) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) - } - - public func addColumn(name: Expression, check: Expression? = nil, defaultValue: V? = nil) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, nil)) - } - - public func addColumn(name: Expression, check: Expression, defaultValue: V? = nil) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, nil)) - } - - public func addColumn(name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) - } - - public func addColumn(name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) - } - - public func addColumn(name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) - } - - public func addColumn(name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) - } - - public func addColumn(name: Expression, check: Expression? = nil, defaultValue: V, collate: Collation) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) - } - - public func addColumn(name: Expression, check: Expression, defaultValue: V, collate: Collation) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) - } - - public func addColumn(name: Expression, check: Expression? = nil, defaultValue: V? = nil, collate: Collation) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) - } - - public func addColumn(name: Expression, check: Expression, defaultValue: V? = nil, collate: Collation) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) - } - - private func addColumn(expression: Expressible) -> String { - return " ".join([ - Expression(literal: "ALTER TABLE"), - tableName(), - Expression(literal: "ADD COLUMN"), - expression - ]).asSQL() - } - - // MARK: - ALTER TABLE … RENAME TO - - public func rename(to: Table) -> String { - return rename(to: to) - } - - // MARK: - CREATE INDEX - - public func createIndex(columns: Expressible...) -> String { - return createIndex(columns) - } - - public func createIndex(columns: [Expressible], unique: Bool = false, ifNotExists: Bool = false) -> String { - let clauses: [Expressible?] = [ - create("INDEX", indexName(columns), unique ? .Unique : nil, ifNotExists), - Expression(literal: "ON"), - tableName(qualified: false), - "".wrap(columns) as Expression - ] - - return " ".join(clauses.flatMap { $0 }).asSQL() - } - - // MARK: - DROP INDEX - - public func dropIndex(columns: Expressible...) -> String { - return dropIndex(columns) - } - - public func dropIndex(columns: [Expressible], ifExists: Bool = false) -> String { - return drop("INDEX", indexName(columns), ifExists) - } - - private func indexName(columns: [Expressible]) -> Expressible { - let string = (["index", clauses.from.name, "on"] + columns.map { $0.expression.template }).joinWithSeparator(" ").lowercaseString - - let index = string.characters.reduce("") { underscored, character in - guard character != "\"" else { - return underscored - } - guard "a"..."z" ~= character || "0"..."9" ~= character else { - return underscored + "_" - } - return underscored + String(character) - } - - return database(namespace: index) - } - -} - -extension View { - - // MARK: - CREATE VIEW - - public func create(query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { - let clauses: [Expressible?] = [ - create(View.identifier, tableName(), temporary ? .Temporary : nil, ifNotExists), - Expression(literal: "AS"), - query - ] - - return " ".join(clauses.flatMap { $0 }).asSQL() - } - - // MARK: - DROP VIEW - - public func drop(ifExists ifExists: Bool = false) -> String { - return drop("VIEW", tableName(), ifExists) - } - -} - -extension VirtualTable { - - // MARK: - CREATE VIRTUAL TABLE - - public func create(using: Module, ifNotExists: Bool = false) -> String { - let clauses: [Expressible?] = [ - create(VirtualTable.identifier, tableName(), nil, ifNotExists), - Expression(literal: "USING"), - using - ] - - return " ".join(clauses.flatMap { $0 }).asSQL() - } - - // MARK: - ALTER TABLE … RENAME TO - - public func rename(to: VirtualTable) -> String { - return rename(to: to) - } - -} - -public final class TableBuilder { - - private var definitions = [Expressible]() - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: V) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil) { - column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression) { - column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V) { - column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil) { - column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression) { - column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: V) { - column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) - } - - public func column(name: Expression, primaryKey: Bool, check: Expression? = nil, defaultValue: Expression? = nil) { - column(name, V.declaredDatatype, primaryKey ? .Default : nil, false, false, check, defaultValue, nil, nil) - } - - public func column(name: Expression, primaryKey: Bool, check: Expression, defaultValue: Expression? = nil) { - column(name, V.declaredDatatype, primaryKey ? .Default : nil, false, false, check, defaultValue, nil, nil) - } - - public func column(name: Expression, primaryKey: PrimaryKey, check: Expression? = nil) { - column(name, V.declaredDatatype, primaryKey, false, false, check, nil, nil, nil) - } - - public func column(name: Expression, primaryKey: PrimaryKey, check: Expression) { - column(name, V.declaredDatatype, primaryKey, false, false, check, nil, nil, nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) { - column(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) { - column(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) { - column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) { - column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: V, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - public func column(name: Expression, unique: Bool = false, check: Expression, defaultValue: V, collate: Collation) { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) - } - - private func column(name: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) { - definitions.append(definition(name, datatype, primaryKey, null, unique, check, defaultValue, references, collate)) - } - - // MARK: - - - public func primaryKey(column: Expression) { - primaryKey([column]) - } - - public func primaryKey(compositeA: Expression, _ b: Expression) { - primaryKey([compositeA, b]) - } - - public func primaryKey(compositeA: Expression, _ b: Expression, _ c: Expression) { - primaryKey([compositeA, b, c]) - } - - private func primaryKey(composite: [Expressible]) { - definitions.append("PRIMARY KEY".prefix(composite)) - } - - public func unique(columns: Expressible...) { - unique(columns) - } - - public func unique(columns: [Expressible]) { - definitions.append("UNIQUE".prefix(columns)) - } - - public func check(condition: Expression) { - check(Expression(condition)) - } - - public func check(condition: Expression) { - definitions.append("CHECK".prefix(condition)) - } - - public enum Dependency: String { - - case NoAction = "NO ACTION" - - case Restrict = "RESTRICT" - - case SetNull = "SET NULL" - - case SetDefault = "SET DEFAULT" - - case Cascade = "CASCADE" - - } - - public func foreignKey(column: Expression, references table: QueryType, _ other: Expression, update: Dependency? = nil, delete: Dependency? = nil) { - foreignKey(column, (table, other), update, delete) - } - - public func foreignKey(column: Expression, references table: QueryType, _ other: Expression, update: Dependency? = nil, delete: Dependency? = nil) { - foreignKey(column, (table, other), update, delete) - } - - public func foreignKey(composite: (Expression, Expression), references table: QueryType, _ other: (Expression, Expression), update: Dependency? = nil, delete: Dependency? = nil) { - let composite = ", ".join([composite.0, composite.1]) - let references = (table, ", ".join([other.0, other.1])) - - foreignKey(composite, references, update, delete) - } - - public func foreignKey(composite: (Expression, Expression, Expression), references table: QueryType, _ other: (Expression, Expression, Expression), update: Dependency? = nil, delete: Dependency? = nil) { - let composite = ", ".join([composite.0, composite.1, composite.2]) - let references = (table, ", ".join([other.0, other.1, other.2])) - - foreignKey(composite, references, update, delete) - } - - private func foreignKey(column: Expressible, _ references: (QueryType, Expressible), _ update: Dependency?, _ delete: Dependency?) { - let clauses: [Expressible?] = [ - "FOREIGN KEY".prefix(column), - reference(references), - update.map { Expression(literal: "ON UPDATE \($0.rawValue)") }, - delete.map { Expression(literal: "ON DELETE \($0.rawValue)") } - ] - - definitions.append(" ".join(clauses.flatMap { $0 })) - } - -} - -public enum PrimaryKey { - - case Default - - case Autoincrement - -} - -public struct Module { - - private let name: String - - private let arguments: [Expressible] - - public init(_ name: String, _ arguments: [Expressible]) { - self.init(name: name.quote(), arguments: arguments) - } - - init(name: String, arguments: [Expressible]) { - self.name = name - self.arguments = arguments - } - -} - -extension Module : Expressible { - - public var expression: Expression { - return name.wrap(arguments) - } - -} - -// MARK: - Private - -private extension QueryType { - - func create(identifier: String, _ name: Expressible, _ modifier: Modifier?, _ ifNotExists: Bool) -> Expressible { - let clauses: [Expressible?] = [ - Expression(literal: "CREATE"), - modifier.map { Expression(literal: $0.rawValue) }, - Expression(literal: identifier), - ifNotExists ? Expression(literal: "IF NOT EXISTS") : nil, - name - ] - - return " ".join(clauses.flatMap { $0 }) - } - - func rename(to to: Self) -> String { - return " ".join([ - Expression(literal: "ALTER TABLE"), - tableName(), - Expression(literal: "RENAME TO"), - Expression(to.clauses.from.name) - ]).asSQL() - } - - func drop(identifier: String, _ name: Expressible, _ ifExists: Bool) -> String { - let clauses: [Expressible?] = [ - Expression(literal: "DROP \(identifier)"), - ifExists ? Expression(literal: "IF EXISTS") : nil, - name - ] - - return " ".join(clauses.flatMap { $0 }).asSQL() - } - -} - -private func definition(column: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) -> Expressible { - let clauses: [Expressible?] = [ - column, - Expression(literal: datatype), - primaryKey.map { Expression(literal: $0 == .Autoincrement ? "PRIMARY KEY AUTOINCREMENT" : "PRIMARY KEY") }, - null ? nil : Expression(literal: "NOT NULL"), - unique ? Expression(literal: "UNIQUE") : nil, - check.map { " ".join([Expression(literal: "CHECK"), $0]) }, - defaultValue.map { "DEFAULT".prefix($0) }, - references.map(reference), - collate.map { " ".join([Expression(literal: "COLLATE"), $0]) } - ] - - return " ".join(clauses.flatMap { $0 }) -} - -private func reference(primary: (QueryType, Expressible)) -> Expressible { - return " ".join([ - Expression(literal: "REFERENCES"), - primary.0.tableName(qualified: false), - "".wrap(primary.1) as Expression - ]) -} - -private enum Modifier : String { - - case Unique = "UNIQUE" - - case Temporary = "TEMPORARY" - -} diff --git a/SQLite/Typed/Setter.swift b/SQLite/Typed/Setter.swift deleted file mode 100644 index d8bf775a..00000000 --- a/SQLite/Typed/Setter.swift +++ /dev/null @@ -1,275 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -infix operator <- { - associativity left - precedence 135 - assignment -} - -public struct Setter { - - let column: Expressible - let value: Expressible - - private init(column: Expression, value: Expression) { - self.column = column - self.value = value - } - - private init(column: Expression, value: V) { - self.column = column - self.value = value - } - - private init(column: Expression, value: Expression) { - self.column = column - self.value = value - } - - private init(column: Expression, value: Expression) { - self.column = column - self.value = value - } - - private init(column: Expression, value: V?) { - self.column = column - self.value = Expression(value: value) - } - -} - -extension Setter : Expressible { - - public var expression: Expression { - return "=".infix(column, value, wrap: false) - } - -} - -public func <-(column: Expression, value: Expression) -> Setter { - return Setter(column: column, value: value) -} -public func <-(column: Expression, value: V) -> Setter { - return Setter(column: column, value: value) -} -public func <-(column: Expression, value: Expression) -> Setter { - return Setter(column: column, value: value) -} -public func <-(column: Expression, value: Expression) -> Setter { - return Setter(column: column, value: value) -} -public func <-(column: Expression, value: V?) -> Setter { - return Setter(column: column, value: value) -} - -public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: String) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: String) -> Setter { - return column <- column + value -} - -public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: V) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value -} -public func +=(column: Expression, value: V) -> Setter { - return column <- column + value -} - -public func -=(column: Expression, value: Expression) -> Setter { - return column <- column - value -} -public func -=(column: Expression, value: V) -> Setter { - return column <- column - value -} -public func -=(column: Expression, value: Expression) -> Setter { - return column <- column - value -} -public func -=(column: Expression, value: Expression) -> Setter { - return column <- column - value -} -public func -=(column: Expression, value: V) -> Setter { - return column <- column - value -} - -public func *=(column: Expression, value: Expression) -> Setter { - return column <- column * value -} -public func *=(column: Expression, value: V) -> Setter { - return column <- column * value -} -public func *=(column: Expression, value: Expression) -> Setter { - return column <- column * value -} -public func *=(column: Expression, value: Expression) -> Setter { - return column <- column * value -} -public func *=(column: Expression, value: V) -> Setter { - return column <- column * value -} - -public func /=(column: Expression, value: Expression) -> Setter { - return column <- column / value -} -public func /=(column: Expression, value: V) -> Setter { - return column <- column / value -} -public func /=(column: Expression, value: Expression) -> Setter { - return column <- column / value -} -public func /=(column: Expression, value: Expression) -> Setter { - return column <- column / value -} -public func /=(column: Expression, value: V) -> Setter { - return column <- column / value -} - -public func %=(column: Expression, value: Expression) -> Setter { - return column <- column % value -} -public func %=(column: Expression, value: V) -> Setter { - return column <- column % value -} -public func %=(column: Expression, value: Expression) -> Setter { - return column <- column % value -} -public func %=(column: Expression, value: Expression) -> Setter { - return column <- column % value -} -public func %=(column: Expression, value: V) -> Setter { - return column <- column % value -} - -public func <<=(column: Expression, value: Expression) -> Setter { - return column <- column << value -} -public func <<=(column: Expression, value: V) -> Setter { - return column <- column << value -} -public func <<=(column: Expression, value: Expression) -> Setter { - return column <- column << value -} -public func <<=(column: Expression, value: Expression) -> Setter { - return column <- column << value -} -public func <<=(column: Expression, value: V) -> Setter { - return column <- column << value -} - -public func >>=(column: Expression, value: Expression) -> Setter { - return column <- column >> value -} -public func >>=(column: Expression, value: V) -> Setter { - return column <- column >> value -} -public func >>=(column: Expression, value: Expression) -> Setter { - return column <- column >> value -} -public func >>=(column: Expression, value: Expression) -> Setter { - return column <- column >> value -} -public func >>=(column: Expression, value: V) -> Setter { - return column <- column >> value -} - -public func &=(column: Expression, value: Expression) -> Setter { - return column <- column & value -} -public func &=(column: Expression, value: V) -> Setter { - return column <- column & value -} -public func &=(column: Expression, value: Expression) -> Setter { - return column <- column & value -} -public func &=(column: Expression, value: Expression) -> Setter { - return column <- column & value -} -public func &=(column: Expression, value: V) -> Setter { - return column <- column & value -} - -public func |=(column: Expression, value: Expression) -> Setter { - return column <- column | value -} -public func |=(column: Expression, value: V) -> Setter { - return column <- column | value -} -public func |=(column: Expression, value: Expression) -> Setter { - return column <- column | value -} -public func |=(column: Expression, value: Expression) -> Setter { - return column <- column | value -} -public func |=(column: Expression, value: V) -> Setter { - return column <- column | value -} - -public func ^=(column: Expression, value: Expression) -> Setter { - return column <- column ^ value -} -public func ^=(column: Expression, value: V) -> Setter { - return column <- column ^ value -} -public func ^=(column: Expression, value: Expression) -> Setter { - return column <- column ^ value -} -public func ^=(column: Expression, value: Expression) -> Setter { - return column <- column ^ value -} -public func ^=(column: Expression, value: V) -> Setter { - return column <- column ^ value -} - -public postfix func ++(column: Expression) -> Setter { - return Expression(column) += 1 -} -public postfix func ++(column: Expression) -> Setter { - return Expression(column) += 1 -} - -public postfix func --(column: Expression) -> Setter { - return Expression(column) -= 1 -} -public postfix func --(column: Expression) -> Setter { - return Expression(column) -= 1 -} diff --git a/SQLiteTests/AggregateFunctionsTests.swift b/SQLiteTests/AggregateFunctionsTests.swift deleted file mode 100644 index 6b583ccf..00000000 --- a/SQLiteTests/AggregateFunctionsTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -import XCTest -import SQLite - -class AggregateFunctionsTests : XCTestCase { - - func test_distinct_prependsExpressionsWithDistinctKeyword() { - AssertSQL("DISTINCT \"int\"", int.distinct) - AssertSQL("DISTINCT \"intOptional\"", intOptional.distinct) - AssertSQL("DISTINCT \"double\"", double.distinct) - AssertSQL("DISTINCT \"doubleOptional\"", doubleOptional.distinct) - AssertSQL("DISTINCT \"string\"", string.distinct) - AssertSQL("DISTINCT \"stringOptional\"", stringOptional.distinct) - } - - func test_count_wrapsOptionalExpressionsWithCountFunction() { - AssertSQL("count(\"intOptional\")", intOptional.count) - AssertSQL("count(\"doubleOptional\")", doubleOptional.count) - AssertSQL("count(\"stringOptional\")", stringOptional.count) - } - - func test_max_wrapsComparableExpressionsWithMaxFunction() { - AssertSQL("max(\"int\")", int.max) - AssertSQL("max(\"intOptional\")", intOptional.max) - AssertSQL("max(\"double\")", double.max) - AssertSQL("max(\"doubleOptional\")", doubleOptional.max) - AssertSQL("max(\"string\")", string.max) - AssertSQL("max(\"stringOptional\")", stringOptional.max) - AssertSQL("max(\"date\")", date.max) - AssertSQL("max(\"dateOptional\")", dateOptional.max) - } - - func test_min_wrapsComparableExpressionsWithMinFunction() { - AssertSQL("min(\"int\")", int.min) - AssertSQL("min(\"intOptional\")", intOptional.min) - AssertSQL("min(\"double\")", double.min) - AssertSQL("min(\"doubleOptional\")", doubleOptional.min) - AssertSQL("min(\"string\")", string.min) - AssertSQL("min(\"stringOptional\")", stringOptional.min) - AssertSQL("min(\"date\")", date.min) - AssertSQL("min(\"dateOptional\")", dateOptional.min) - } - - func test_average_wrapsNumericExpressionsWithAvgFunction() { - AssertSQL("avg(\"int\")", int.average) - AssertSQL("avg(\"intOptional\")", intOptional.average) - AssertSQL("avg(\"double\")", double.average) - AssertSQL("avg(\"doubleOptional\")", doubleOptional.average) - } - - func test_sum_wrapsNumericExpressionsWithSumFunction() { - AssertSQL("sum(\"int\")", int.sum) - AssertSQL("sum(\"intOptional\")", intOptional.sum) - AssertSQL("sum(\"double\")", double.sum) - AssertSQL("sum(\"doubleOptional\")", doubleOptional.sum) - } - - func test_total_wrapsNumericExpressionsWithTotalFunction() { - AssertSQL("total(\"int\")", int.total) - AssertSQL("total(\"intOptional\")", intOptional.total) - AssertSQL("total(\"double\")", double.total) - AssertSQL("total(\"doubleOptional\")", doubleOptional.total) - } - - func test_count_withStar_wrapsStarWithCountFunction() { - AssertSQL("count(*)", count(*)) - } - -} diff --git a/SQLiteTests/BlobTests.swift b/SQLiteTests/BlobTests.swift deleted file mode 100644 index ba0a7c54..00000000 --- a/SQLiteTests/BlobTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XCTest -import SQLite - -class BlobTests : XCTestCase { - - func test_toHex() { - let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) - - XCTAssertEqual(blob.toHex(), "000a141e28323c46505a6496faff") - } - -} diff --git a/SQLiteTests/ConnectionTests.swift b/SQLiteTests/ConnectionTests.swift deleted file mode 100644 index 3d7dd3eb..00000000 --- a/SQLiteTests/ConnectionTests.swift +++ /dev/null @@ -1,317 +0,0 @@ -import XCTest -import SQLite - -class ConnectionTests : SQLiteTestCase { - - override func setUp() { - super.setUp() - - CreateUsersTable() - } - - func test_init_withInMemory_returnsInMemoryConnection() { - let db = try! Connection(.InMemory) - XCTAssertEqual("", db.description) - } - - func test_init_returnsInMemoryByDefault() { - let db = try! Connection() - XCTAssertEqual("", db.description) - } - - func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! Connection(.Temporary) - XCTAssertEqual("", db.description) - } - - func test_init_withURI_returnsURIConnection() { - let db = try! Connection(.URI("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) - XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) - } - - func test_init_withString_returnsURIConnection() { - let db = try! Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") - XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) - } - - func test_readonly_returnsFalseOnReadWriteConnections() { - XCTAssertFalse(db.readonly) - } - - func test_readonly_returnsTrueOnReadOnlyConnections() { - let db = try! Connection(readonly: true) - XCTAssertTrue(db.readonly) - } - - func test_lastInsertRowid_returnsNilOnNewConnections() { - XCTAssert(db.lastInsertRowid == nil) - } - - func test_lastInsertRowid_returnsLastIdAfterInserts() { - try! InsertUser("alice") - XCTAssertEqual(1, db.lastInsertRowid!) - } - - func test_changes_returnsZeroOnNewConnections() { - XCTAssertEqual(0, db.changes) - } - - func test_changes_returnsNumberOfChanges() { - try! InsertUser("alice") - XCTAssertEqual(1, db.changes) - try! InsertUser("betsy") - XCTAssertEqual(1, db.changes) - } - - func test_totalChanges_returnsTotalNumberOfChanges() { - XCTAssertEqual(0, db.totalChanges) - try! InsertUser("alice") - XCTAssertEqual(1, db.totalChanges) - try! InsertUser("betsy") - XCTAssertEqual(2, db.totalChanges) - } - - func test_prepare_preparesAndReturnsStatements() { - _ = try! db.prepare("SELECT * FROM users WHERE admin = 0") - _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", 0) - _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", [0]) - _ = try! db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) - } - - func test_run_preparesRunsAndReturnsStatements() { - try! db.run("SELECT * FROM users WHERE admin = 0") - try! db.run("SELECT * FROM users WHERE admin = ?", 0) - try! db.run("SELECT * FROM users WHERE admin = ?", [0]) - try! db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) - AssertSQL("SELECT * FROM users WHERE admin = 0", 4) - } - - func test_scalar_preparesRunsAndReturnsScalarValues() { - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = $admin", ["$admin": 0]) as? Int64) - AssertSQL("SELECT count(*) FROM users WHERE admin = 0", 4) - } - - func test_transaction_executesBeginDeferred() { - try! db.transaction(.Deferred) {} - - AssertSQL("BEGIN DEFERRED TRANSACTION") - } - - func test_transaction_executesBeginImmediate() { - try! db.transaction(.Immediate) {} - - AssertSQL("BEGIN IMMEDIATE TRANSACTION") - } - - func test_transaction_executesBeginExclusive() { - try! db.transaction(.Exclusive) {} - - AssertSQL("BEGIN EXCLUSIVE TRANSACTION") - } - - func test_transaction_beginsAndCommitsTransactions() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - - try! db.transaction { - try stmt.run() - } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") - AssertSQL("COMMIT TRANSACTION") - AssertSQL("ROLLBACK TRANSACTION", 0) - } - - func test_transaction_beginsAndRollsTransactionsBack() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - - do { - try db.transaction { - try stmt.run() - try stmt.run() - } - } catch { - } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) - AssertSQL("ROLLBACK TRANSACTION") - AssertSQL("COMMIT TRANSACTION", 0) - } - - func test_savepoint_beginsAndCommitsSavepoints() { - let db = self.db - - try! db.savepoint("1") { - try db.savepoint("2") { - try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") - } - } - - AssertSQL("SAVEPOINT '1'") - AssertSQL("SAVEPOINT '2'") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") - AssertSQL("RELEASE SAVEPOINT '2'") - AssertSQL("RELEASE SAVEPOINT '1'") - AssertSQL("ROLLBACK TO SAVEPOINT '2'", 0) - AssertSQL("ROLLBACK TO SAVEPOINT '1'", 0) - } - - func test_savepoint_beginsAndRollsSavepointsBack() { - let db = self.db - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - - do { - try db.savepoint("1") { - try db.savepoint("2") { - try stmt.run() - try stmt.run() - try stmt.run() - } - try db.savepoint("2") { - try stmt.run() - try stmt.run() - try stmt.run() - } - } - } catch { - } - - AssertSQL("SAVEPOINT '1'") - AssertSQL("SAVEPOINT '2'") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) - AssertSQL("ROLLBACK TO SAVEPOINT '2'") - AssertSQL("ROLLBACK TO SAVEPOINT '1'") - AssertSQL("RELEASE SAVEPOINT '2'", 0) - AssertSQL("RELEASE SAVEPOINT '1'", 0) - } - - func test_updateHook_setsUpdateHook_withInsert() { - async { done in - db.updateHook { operation, db, table, rowid in - XCTAssertEqual(Operation.Insert, operation) - XCTAssertEqual("main", db) - XCTAssertEqual("users", table) - XCTAssertEqual(1, rowid) - done() - } - try! InsertUser("alice") - } - } - - func test_updateHook_setsUpdateHook_withUpdate() { - try! InsertUser("alice") - async { done in - db.updateHook { operation, db, table, rowid in - XCTAssertEqual(Operation.Update, operation) - XCTAssertEqual("main", db) - XCTAssertEqual("users", table) - XCTAssertEqual(1, rowid) - done() - } - try! db.run("UPDATE users SET email = 'alice@example.com'") - } - } - - func test_updateHook_setsUpdateHook_withDelete() { - try! InsertUser("alice") - async { done in - db.updateHook { operation, db, table, rowid in - XCTAssertEqual(Operation.Delete, operation) - XCTAssertEqual("main", db) - XCTAssertEqual("users", table) - XCTAssertEqual(1, rowid) - done() - } - try! db.run("DELETE FROM users WHERE id = 1") - } - } - - func test_commitHook_setsCommitHook() { - async { done in - db.commitHook { - done() - } - try! db.transaction { - try self.InsertUser("alice") - } - XCTAssertEqual(1, try! db.scalar("SELECT count(*) FROM users") as? Int64) - } - } - - func test_rollbackHook_setsRollbackHook() { - async { done in - db.rollbackHook(done) - do { - try db.transaction { - try self.InsertUser("alice") - try self.InsertUser("alice") // throw - } - } catch { - } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) - } - } - - func test_commitHook_withRollback_rollsBack() { - async { done in - db.commitHook { - throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) - } - db.rollbackHook(done) - do { - try db.transaction { - try self.InsertUser("alice") - } - } catch { - } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) - } - } - - func test_createFunction_withArrayArguments() { - db.createFunction("hello") { $0[0].map { "Hello, \($0)!" } } - - XCTAssertEqual("Hello, world!", try! db.scalar("SELECT hello('world')") as? String) - XCTAssert(try! db.scalar("SELECT hello(NULL)") == nil) - } - - func test_createFunction_createsQuotableFunction() { - db.createFunction("hello world") { $0[0].map { "Hello, \($0)!" } } - - XCTAssertEqual("Hello, world!", try! db.scalar("SELECT \"hello world\"('world')") as? String) - XCTAssert(try! db.scalar("SELECT \"hello world\"(NULL)") == nil) - } - - func test_createCollation_createsCollation() { - try! db.createCollation("NODIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) - } - XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) - } - - func test_createCollation_createsQuotableCollation() { - try! db.createCollation("NO DIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) - } - XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) - } - - func test_interrupt_interruptsLongRunningQuery() { - try! InsertUsers("abcdefghijklmnopqrstuvwxyz".characters.map { String($0) }) - db.createFunction("sleep") { args in - usleep(UInt32((args[0] as? Double ?? Double(args[0] as? Int64 ?? 1)) * 1_000_000)) - return nil - } - - let stmt = try! db.prepare("SELECT *, sleep(?) FROM users", 0.1) - try! stmt.run() - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(10 * NSEC_PER_MSEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), db.interrupt) - AssertThrows(try stmt.run()) - } - -} diff --git a/SQLiteTests/CoreFunctionsTests.swift b/SQLiteTests/CoreFunctionsTests.swift deleted file mode 100644 index 8f7460d5..00000000 --- a/SQLiteTests/CoreFunctionsTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -import XCTest -import SQLite - -class CoreFunctionsTests : XCTestCase { - - func test_round_wrapsDoubleExpressionsWithRoundFunction() { - AssertSQL("round(\"double\")", double.round()) - AssertSQL("round(\"doubleOptional\")", doubleOptional.round()) - - AssertSQL("round(\"double\", 1)", double.round(1)) - AssertSQL("round(\"doubleOptional\", 2)", doubleOptional.round(2)) - } - - func test_random_generatesExpressionWithRandomFunction() { - AssertSQL("random()", Expression.random()) - AssertSQL("random()", Expression.random()) - } - - func test_length_wrapsStringExpressionWithLengthFunction() { - AssertSQL("length(\"string\")", string.length) - AssertSQL("length(\"stringOptional\")", stringOptional.length) - } - - func test_lowercaseString_wrapsStringExpressionWithLowerFunction() { - AssertSQL("lower(\"string\")", string.lowercaseString) - AssertSQL("lower(\"stringOptional\")", stringOptional.lowercaseString) - } - - func test_uppercaseString_wrapsStringExpressionWithUpperFunction() { - AssertSQL("upper(\"string\")", string.uppercaseString) - AssertSQL("upper(\"stringOptional\")", stringOptional.uppercaseString) - } - - func test_like_buildsExpressionWithLikeOperator() { - AssertSQL("(\"string\" LIKE 'a%')", string.like("a%")) - AssertSQL("(\"stringOptional\" LIKE 'b%')", stringOptional.like("b%")) - - AssertSQL("(\"string\" LIKE '%\\%' ESCAPE '\\')", string.like("%\\%", escape: "\\")) - AssertSQL("(\"stringOptional\" LIKE '_\\_' ESCAPE '\\')", stringOptional.like("_\\_", escape: "\\")) - } - - func test_glob_buildsExpressionWithGlobOperator() { - AssertSQL("(\"string\" GLOB 'a*')", string.glob("a*")) - AssertSQL("(\"stringOptional\" GLOB 'b*')", stringOptional.glob("b*")) - } - - func test_match_buildsExpressionWithMatchOperator() { - AssertSQL("(\"string\" MATCH 'a*')", string.match("a*")) - AssertSQL("(\"stringOptional\" MATCH 'b*')", stringOptional.match("b*")) - } - - func test_regexp_buildsExpressionWithRegexpOperator() { - AssertSQL("(\"string\" REGEXP '^.+@.+\\.com$')", string.regexp("^.+@.+\\.com$")) - AssertSQL("(\"stringOptional\" REGEXP '^.+@.+\\.net$')", stringOptional.regexp("^.+@.+\\.net$")) - } - - func test_collate_buildsExpressionWithCollateOperator() { - AssertSQL("(\"string\" COLLATE BINARY)", string.collate(.Binary)) - AssertSQL("(\"string\" COLLATE NOCASE)", string.collate(.Nocase)) - AssertSQL("(\"string\" COLLATE RTRIM)", string.collate(.Rtrim)) - AssertSQL("(\"string\" COLLATE \"CUSTOM\")", string.collate(.Custom("CUSTOM"))) - - AssertSQL("(\"stringOptional\" COLLATE BINARY)", stringOptional.collate(.Binary)) - AssertSQL("(\"stringOptional\" COLLATE NOCASE)", stringOptional.collate(.Nocase)) - AssertSQL("(\"stringOptional\" COLLATE RTRIM)", stringOptional.collate(.Rtrim)) - AssertSQL("(\"stringOptional\" COLLATE \"CUSTOM\")", stringOptional.collate(.Custom("CUSTOM"))) - } - - func test_ltrim_wrapsStringWithLtrimFunction() { - AssertSQL("ltrim(\"string\")", string.ltrim()) - AssertSQL("ltrim(\"stringOptional\")", stringOptional.ltrim()) - - AssertSQL("ltrim(\"string\", ' ')", string.ltrim([" "])) - AssertSQL("ltrim(\"stringOptional\", ' ')", stringOptional.ltrim([" "])) - } - - func test_ltrim_wrapsStringWithRtrimFunction() { - AssertSQL("rtrim(\"string\")", string.rtrim()) - AssertSQL("rtrim(\"stringOptional\")", stringOptional.rtrim()) - - AssertSQL("rtrim(\"string\", ' ')", string.rtrim([" "])) - AssertSQL("rtrim(\"stringOptional\", ' ')", stringOptional.rtrim([" "])) - } - - func test_ltrim_wrapsStringWithTrimFunction() { - AssertSQL("trim(\"string\")", string.trim()) - AssertSQL("trim(\"stringOptional\")", stringOptional.trim()) - - AssertSQL("trim(\"string\", ' ')", string.trim([" "])) - AssertSQL("trim(\"stringOptional\", ' ')", stringOptional.trim([" "])) - } - - func test_replace_wrapsStringWithReplaceFunction() { - AssertSQL("replace(\"string\", '@example.com', '@example.net')", string.replace("@example.com", with: "@example.net")) - AssertSQL("replace(\"stringOptional\", '@example.net', '@example.com')", stringOptional.replace("@example.net", with: "@example.com")) - } - - func test_substring_wrapsStringWithSubstrFunction() { - AssertSQL("substr(\"string\", 1, 2)", string.substring(1, length: 2)) - AssertSQL("substr(\"stringOptional\", 2, 1)", stringOptional.substring(2, length: 1)) - } - - func test_subscriptWithRange_wrapsStringWithSubstrFunction() { - AssertSQL("substr(\"string\", 1, 2)", string[1..<3]) - AssertSQL("substr(\"stringOptional\", 2, 1)", stringOptional[2..<3]) - } - - func test_nilCoalescingOperator_wrapsOptionalsWithIfnullFunction() { - AssertSQL("ifnull(\"intOptional\", 1)", intOptional ?? 1) - // AssertSQL("ifnull(\"doubleOptional\", 1.0)", doubleOptional ?? 1) // rdar://problem/21677256 - XCTAssertEqual("ifnull(\"doubleOptional\", 1.0)", (doubleOptional ?? 1).asSQL()) - AssertSQL("ifnull(\"stringOptional\", 'literal')", stringOptional ?? "literal") - - AssertSQL("ifnull(\"intOptional\", \"int\")", intOptional ?? int) - AssertSQL("ifnull(\"doubleOptional\", \"double\")", doubleOptional ?? double) - AssertSQL("ifnull(\"stringOptional\", \"string\")", stringOptional ?? string) - - AssertSQL("ifnull(\"intOptional\", \"intOptional\")", intOptional ?? intOptional) - AssertSQL("ifnull(\"doubleOptional\", \"doubleOptional\")", doubleOptional ?? doubleOptional) - AssertSQL("ifnull(\"stringOptional\", \"stringOptional\")", stringOptional ?? stringOptional) - } - - func test_absoluteValue_wrapsNumberWithAbsFucntion() { - AssertSQL("abs(\"int\")", int.absoluteValue) - AssertSQL("abs(\"intOptional\")", intOptional.absoluteValue) - - AssertSQL("abs(\"double\")", double.absoluteValue) - AssertSQL("abs(\"doubleOptional\")", doubleOptional.absoluteValue) - } - - func test_contains_buildsExpressionWithInOperator() { - AssertSQL("(\"string\" IN ('hello', 'world'))", ["hello", "world"].contains(string)) - AssertSQL("(\"stringOptional\" IN ('hello', 'world'))", ["hello", "world"].contains(stringOptional)) - } - -} diff --git a/SQLiteTests/CustomFunctionsTests.swift b/SQLiteTests/CustomFunctionsTests.swift deleted file mode 100644 index 67150ccf..00000000 --- a/SQLiteTests/CustomFunctionsTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -import SQLite - -class CustomFunctionsTests : XCTestCase { - -} diff --git a/SQLiteTests/ExpressionTests.swift b/SQLiteTests/ExpressionTests.swift deleted file mode 100644 index 036e10ce..00000000 --- a/SQLiteTests/ExpressionTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -import SQLite - -class ExpressionTests : XCTestCase { - -} diff --git a/SQLiteTests/OperatorsTests.swift b/SQLiteTests/OperatorsTests.swift deleted file mode 100644 index 86aa34ed..00000000 --- a/SQLiteTests/OperatorsTests.swift +++ /dev/null @@ -1,286 +0,0 @@ -import XCTest -import SQLite - -class OperatorsTests : XCTestCase { - - func test_stringExpressionPlusStringExpression_buildsConcatenatingStringExpression() { - AssertSQL("(\"string\" || \"string\")", string + string) - AssertSQL("(\"string\" || \"stringOptional\")", string + stringOptional) - AssertSQL("(\"stringOptional\" || \"string\")", stringOptional + string) - AssertSQL("(\"stringOptional\" || \"stringOptional\")", stringOptional + stringOptional) - AssertSQL("(\"string\" || 'literal')", string + "literal") - AssertSQL("(\"stringOptional\" || 'literal')", stringOptional + "literal") - AssertSQL("('literal' || \"string\")", "literal" + string) - AssertSQL("('literal' || \"stringOptional\")", "literal" + stringOptional) - } - - func test_numberExpression_plusNumberExpression_buildsAdditiveNumberExpression() { - AssertSQL("(\"int\" + \"int\")", int + int) - AssertSQL("(\"int\" + \"intOptional\")", int + intOptional) - AssertSQL("(\"intOptional\" + \"int\")", intOptional + int) - AssertSQL("(\"intOptional\" + \"intOptional\")", intOptional + intOptional) - AssertSQL("(\"int\" + 1)", int + 1) - AssertSQL("(\"intOptional\" + 1)", intOptional + 1) - AssertSQL("(1 + \"int\")", 1 + int) - AssertSQL("(1 + \"intOptional\")", 1 + intOptional) - - AssertSQL("(\"double\" + \"double\")", double + double) - AssertSQL("(\"double\" + \"doubleOptional\")", double + doubleOptional) - AssertSQL("(\"doubleOptional\" + \"double\")", doubleOptional + double) - AssertSQL("(\"doubleOptional\" + \"doubleOptional\")", doubleOptional + doubleOptional) - AssertSQL("(\"double\" + 1.0)", double + 1) - AssertSQL("(\"doubleOptional\" + 1.0)", doubleOptional + 1) - AssertSQL("(1.0 + \"double\")", 1 + double) - AssertSQL("(1.0 + \"doubleOptional\")", 1 + doubleOptional) - } - - func test_numberExpression_minusNumberExpression_buildsSubtractiveNumberExpression() { - AssertSQL("(\"int\" - \"int\")", int - int) - AssertSQL("(\"int\" - \"intOptional\")", int - intOptional) - AssertSQL("(\"intOptional\" - \"int\")", intOptional - int) - AssertSQL("(\"intOptional\" - \"intOptional\")", intOptional - intOptional) - AssertSQL("(\"int\" - 1)", int - 1) - AssertSQL("(\"intOptional\" - 1)", intOptional - 1) - AssertSQL("(1 - \"int\")", 1 - int) - AssertSQL("(1 - \"intOptional\")", 1 - intOptional) - - AssertSQL("(\"double\" - \"double\")", double - double) - AssertSQL("(\"double\" - \"doubleOptional\")", double - doubleOptional) - AssertSQL("(\"doubleOptional\" - \"double\")", doubleOptional - double) - AssertSQL("(\"doubleOptional\" - \"doubleOptional\")", doubleOptional - doubleOptional) - AssertSQL("(\"double\" - 1.0)", double - 1) - AssertSQL("(\"doubleOptional\" - 1.0)", doubleOptional - 1) - AssertSQL("(1.0 - \"double\")", 1 - double) - AssertSQL("(1.0 - \"doubleOptional\")", 1 - doubleOptional) - } - - func test_numberExpression_timesNumberExpression_buildsMultiplicativeNumberExpression() { - AssertSQL("(\"int\" * \"int\")", int * int) - AssertSQL("(\"int\" * \"intOptional\")", int * intOptional) - AssertSQL("(\"intOptional\" * \"int\")", intOptional * int) - AssertSQL("(\"intOptional\" * \"intOptional\")", intOptional * intOptional) - AssertSQL("(\"int\" * 1)", int * 1) - AssertSQL("(\"intOptional\" * 1)", intOptional * 1) - AssertSQL("(1 * \"int\")", 1 * int) - AssertSQL("(1 * \"intOptional\")", 1 * intOptional) - - AssertSQL("(\"double\" * \"double\")", double * double) - AssertSQL("(\"double\" * \"doubleOptional\")", double * doubleOptional) - AssertSQL("(\"doubleOptional\" * \"double\")", doubleOptional * double) - AssertSQL("(\"doubleOptional\" * \"doubleOptional\")", doubleOptional * doubleOptional) - AssertSQL("(\"double\" * 1.0)", double * 1) - AssertSQL("(\"doubleOptional\" * 1.0)", doubleOptional * 1) - AssertSQL("(1.0 * \"double\")", 1 * double) - AssertSQL("(1.0 * \"doubleOptional\")", 1 * doubleOptional) - } - - func test_numberExpression_dividedByNumberExpression_buildsDivisiveNumberExpression() { - AssertSQL("(\"int\" / \"int\")", int / int) - AssertSQL("(\"int\" / \"intOptional\")", int / intOptional) - AssertSQL("(\"intOptional\" / \"int\")", intOptional / int) - AssertSQL("(\"intOptional\" / \"intOptional\")", intOptional / intOptional) - AssertSQL("(\"int\" / 1)", int / 1) - AssertSQL("(\"intOptional\" / 1)", intOptional / 1) - AssertSQL("(1 / \"int\")", 1 / int) - AssertSQL("(1 / \"intOptional\")", 1 / intOptional) - - AssertSQL("(\"double\" / \"double\")", double / double) - AssertSQL("(\"double\" / \"doubleOptional\")", double / doubleOptional) - AssertSQL("(\"doubleOptional\" / \"double\")", doubleOptional / double) - AssertSQL("(\"doubleOptional\" / \"doubleOptional\")", doubleOptional / doubleOptional) - AssertSQL("(\"double\" / 1.0)", double / 1) - AssertSQL("(\"doubleOptional\" / 1.0)", doubleOptional / 1) - AssertSQL("(1.0 / \"double\")", 1 / double) - AssertSQL("(1.0 / \"doubleOptional\")", 1 / doubleOptional) - } - - func test_numberExpression_prefixedWithMinus_buildsInvertedNumberExpression() { - AssertSQL("-(\"int\")", -int) - AssertSQL("-(\"intOptional\")", -intOptional) - - AssertSQL("-(\"double\")", -double) - AssertSQL("-(\"doubleOptional\")", -doubleOptional) - } - - func test_integerExpression_moduloIntegerExpression_buildsModuloIntegerExpression() { - AssertSQL("(\"int\" % \"int\")", int % int) - AssertSQL("(\"int\" % \"intOptional\")", int % intOptional) - AssertSQL("(\"intOptional\" % \"int\")", intOptional % int) - AssertSQL("(\"intOptional\" % \"intOptional\")", intOptional % intOptional) - AssertSQL("(\"int\" % 1)", int % 1) - AssertSQL("(\"intOptional\" % 1)", intOptional % 1) - AssertSQL("(1 % \"int\")", 1 % int) - AssertSQL("(1 % \"intOptional\")", 1 % intOptional) - } - - func test_integerExpression_bitShiftLeftIntegerExpression_buildsLeftShiftedIntegerExpression() { - AssertSQL("(\"int\" << \"int\")", int << int) - AssertSQL("(\"int\" << \"intOptional\")", int << intOptional) - AssertSQL("(\"intOptional\" << \"int\")", intOptional << int) - AssertSQL("(\"intOptional\" << \"intOptional\")", intOptional << intOptional) - AssertSQL("(\"int\" << 1)", int << 1) - AssertSQL("(\"intOptional\" << 1)", intOptional << 1) - AssertSQL("(1 << \"int\")", 1 << int) - AssertSQL("(1 << \"intOptional\")", 1 << intOptional) - } - - func test_integerExpression_bitShiftRightIntegerExpression_buildsRightShiftedIntegerExpression() { - AssertSQL("(\"int\" >> \"int\")", int >> int) - AssertSQL("(\"int\" >> \"intOptional\")", int >> intOptional) - AssertSQL("(\"intOptional\" >> \"int\")", intOptional >> int) - AssertSQL("(\"intOptional\" >> \"intOptional\")", intOptional >> intOptional) - AssertSQL("(\"int\" >> 1)", int >> 1) - AssertSQL("(\"intOptional\" >> 1)", intOptional >> 1) - AssertSQL("(1 >> \"int\")", 1 >> int) - AssertSQL("(1 >> \"intOptional\")", 1 >> intOptional) - } - - func test_integerExpression_bitwiseAndIntegerExpression_buildsAndedIntegerExpression() { - AssertSQL("(\"int\" & \"int\")", int & int) - AssertSQL("(\"int\" & \"intOptional\")", int & intOptional) - AssertSQL("(\"intOptional\" & \"int\")", intOptional & int) - AssertSQL("(\"intOptional\" & \"intOptional\")", intOptional & intOptional) - AssertSQL("(\"int\" & 1)", int & 1) - AssertSQL("(\"intOptional\" & 1)", intOptional & 1) - AssertSQL("(1 & \"int\")", 1 & int) - AssertSQL("(1 & \"intOptional\")", 1 & intOptional) - } - - func test_integerExpression_bitwiseOrIntegerExpression_buildsOredIntegerExpression() { - AssertSQL("(\"int\" | \"int\")", int | int) - AssertSQL("(\"int\" | \"intOptional\")", int | intOptional) - AssertSQL("(\"intOptional\" | \"int\")", intOptional | int) - AssertSQL("(\"intOptional\" | \"intOptional\")", intOptional | intOptional) - AssertSQL("(\"int\" | 1)", int | 1) - AssertSQL("(\"intOptional\" | 1)", intOptional | 1) - AssertSQL("(1 | \"int\")", 1 | int) - AssertSQL("(1 | \"intOptional\")", 1 | intOptional) - } - - func test_integerExpression_bitwiseExclusiveOrIntegerExpression_buildsOredIntegerExpression() { - AssertSQL("(~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^ int) - AssertSQL("(~((\"int\" & \"intOptional\")) & (\"int\" | \"intOptional\"))", int ^ intOptional) - AssertSQL("(~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^ int) - AssertSQL("(~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^ intOptional) - AssertSQL("(~((\"int\" & 1)) & (\"int\" | 1))", int ^ 1) - AssertSQL("(~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^ 1) - AssertSQL("(~((1 & \"int\")) & (1 | \"int\"))", 1 ^ int) - AssertSQL("(~((1 & \"intOptional\")) & (1 | \"intOptional\"))", 1 ^ intOptional) - } - - func test_bitwiseNot_integerExpression_buildsComplementIntegerExpression() { - AssertSQL("~(\"int\")", ~int) - AssertSQL("~(\"intOptional\")", ~intOptional) - } - - func test_equalityOperator_withEquatableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" = \"bool\")", bool == bool) - AssertSQL("(\"bool\" = \"boolOptional\")", bool == boolOptional) - AssertSQL("(\"boolOptional\" = \"bool\")", boolOptional == bool) - AssertSQL("(\"boolOptional\" = \"boolOptional\")", boolOptional == boolOptional) - AssertSQL("(\"bool\" = 1)", bool == true) - AssertSQL("(\"boolOptional\" = 1)", boolOptional == true) - AssertSQL("(1 = \"bool\")", true == bool) - AssertSQL("(1 = \"boolOptional\")", true == boolOptional) - - AssertSQL("(\"boolOptional\" IS NULL)", boolOptional == nil) - AssertSQL("(NULL IS \"boolOptional\")", nil == boolOptional) - } - - func test_inequalityOperator_withEquatableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" != \"bool\")", bool != bool) - AssertSQL("(\"bool\" != \"boolOptional\")", bool != boolOptional) - AssertSQL("(\"boolOptional\" != \"bool\")", boolOptional != bool) - AssertSQL("(\"boolOptional\" != \"boolOptional\")", boolOptional != boolOptional) - AssertSQL("(\"bool\" != 1)", bool != true) - AssertSQL("(\"boolOptional\" != 1)", boolOptional != true) - AssertSQL("(1 != \"bool\")", true != bool) - AssertSQL("(1 != \"boolOptional\")", true != boolOptional) - - AssertSQL("(\"boolOptional\" IS NOT NULL)", boolOptional != nil) - AssertSQL("(NULL IS NOT \"boolOptional\")", nil != boolOptional) - } - - func test_greaterThanOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" > \"bool\")", bool > bool) - AssertSQL("(\"bool\" > \"boolOptional\")", bool > boolOptional) - AssertSQL("(\"boolOptional\" > \"bool\")", boolOptional > bool) - AssertSQL("(\"boolOptional\" > \"boolOptional\")", boolOptional > boolOptional) - AssertSQL("(\"bool\" > 1)", bool > true) - AssertSQL("(\"boolOptional\" > 1)", boolOptional > true) - AssertSQL("(1 > \"bool\")", true > bool) - AssertSQL("(1 > \"boolOptional\")", true > boolOptional) - } - - func test_greaterThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" >= \"bool\")", bool >= bool) - AssertSQL("(\"bool\" >= \"boolOptional\")", bool >= boolOptional) - AssertSQL("(\"boolOptional\" >= \"bool\")", boolOptional >= bool) - AssertSQL("(\"boolOptional\" >= \"boolOptional\")", boolOptional >= boolOptional) - AssertSQL("(\"bool\" >= 1)", bool >= true) - AssertSQL("(\"boolOptional\" >= 1)", boolOptional >= true) - AssertSQL("(1 >= \"bool\")", true >= bool) - AssertSQL("(1 >= \"boolOptional\")", true >= boolOptional) - } - - func test_lessThanOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" < \"bool\")", bool < bool) - AssertSQL("(\"bool\" < \"boolOptional\")", bool < boolOptional) - AssertSQL("(\"boolOptional\" < \"bool\")", boolOptional < bool) - AssertSQL("(\"boolOptional\" < \"boolOptional\")", boolOptional < boolOptional) - AssertSQL("(\"bool\" < 1)", bool < true) - AssertSQL("(\"boolOptional\" < 1)", boolOptional < true) - AssertSQL("(1 < \"bool\")", true < bool) - AssertSQL("(1 < \"boolOptional\")", true < boolOptional) - } - - func test_lessThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" <= \"bool\")", bool <= bool) - AssertSQL("(\"bool\" <= \"boolOptional\")", bool <= boolOptional) - AssertSQL("(\"boolOptional\" <= \"bool\")", boolOptional <= bool) - AssertSQL("(\"boolOptional\" <= \"boolOptional\")", boolOptional <= boolOptional) - AssertSQL("(\"bool\" <= 1)", bool <= true) - AssertSQL("(\"boolOptional\" <= 1)", boolOptional <= true) - AssertSQL("(1 <= \"bool\")", true <= bool) - AssertSQL("(1 <= \"boolOptional\")", true <= boolOptional) - } - - func test_patternMatchingOperator_withComparableInterval_buildsBetweenBooleanExpression() { - AssertSQL("\"int\" BETWEEN 0 AND 5", 0...5 ~= int) - AssertSQL("\"intOptional\" BETWEEN 0 AND 5", 0...5 ~= intOptional) - } - - func test_doubleAndOperator_withBooleanExpressions_buildsCompoundExpression() { - AssertSQL("(\"bool\" AND \"bool\")", bool && bool) - AssertSQL("(\"bool\" AND \"boolOptional\")", bool && boolOptional) - AssertSQL("(\"boolOptional\" AND \"bool\")", boolOptional && bool) - AssertSQL("(\"boolOptional\" AND \"boolOptional\")", boolOptional && boolOptional) - AssertSQL("(\"bool\" AND 1)", bool && true) - AssertSQL("(\"boolOptional\" AND 1)", boolOptional && true) - AssertSQL("(1 AND \"bool\")", true && bool) - AssertSQL("(1 AND \"boolOptional\")", true && boolOptional) - } - - func test_doubleOrOperator_withBooleanExpressions_buildsCompoundExpression() { - AssertSQL("(\"bool\" OR \"bool\")", bool || bool) - AssertSQL("(\"bool\" OR \"boolOptional\")", bool || boolOptional) - AssertSQL("(\"boolOptional\" OR \"bool\")", boolOptional || bool) - AssertSQL("(\"boolOptional\" OR \"boolOptional\")", boolOptional || boolOptional) - AssertSQL("(\"bool\" OR 1)", bool || true) - AssertSQL("(\"boolOptional\" OR 1)", boolOptional || true) - AssertSQL("(1 OR \"bool\")", true || bool) - AssertSQL("(1 OR \"boolOptional\")", true || boolOptional) - } - - func test_unaryNotOperator_withBooleanExpressions_buildsNotExpression() { - AssertSQL("NOT (\"bool\")", !bool) - AssertSQL("NOT (\"boolOptional\")", !boolOptional) - } - - func test_precedencePreserved() { - let n = Expression(value: 1) - AssertSQL("(((1 = 1) AND (1 = 1)) OR (1 = 1))", (n == n && n == n) || n == n) - AssertSQL("((1 = 1) AND ((1 = 1) OR (1 = 1)))", n == n && (n == n || n == n)) - } - -} \ No newline at end of file diff --git a/SQLiteTests/QueryTests.swift b/SQLiteTests/QueryTests.swift deleted file mode 100644 index f584bfcf..00000000 --- a/SQLiteTests/QueryTests.swift +++ /dev/null @@ -1,336 +0,0 @@ -import XCTest -import SQLite - -class QueryTests : XCTestCase { - - let users = Table("users") - let id = Expression("id") - let email = Expression("email") - let age = Expression("age") - let admin = Expression("admin") - - let posts = Table("posts") - let userId = Expression("user_id") - let categoryId = Expression("category_id") - let published = Expression("published") - - let categories = Table("categories") - let tag = Expression("tag") - - func test_select_withExpression_compilesSelectClause() { - AssertSQL("SELECT \"email\" FROM \"users\"", users.select(email)) - } - - func test_select_withStarExpression_compilesSelectClause() { - AssertSQL("SELECT * FROM \"users\"", users.select(*)) - } - - func test_select_withNamespacedStarExpression_compilesSelectClause() { - AssertSQL("SELECT \"users\".* FROM \"users\"", users.select(users[*])) - } - - func test_select_withVariadicExpressions_compilesSelectClause() { - AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select(email, count(*))) - } - - func test_select_withExpressions_compilesSelectClause() { - AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select([email, count(*)])) - } - - func test_selectDistinct_withExpression_compilesSelectClause() { - AssertSQL("SELECT DISTINCT \"age\" FROM \"users\"", users.select(distinct: age)) - } - - func test_selectDistinct_withExpressions_compilesSelectClause() { - AssertSQL("SELECT DISTINCT \"age\", \"admin\" FROM \"users\"", users.select(distinct: [age, admin])) - } - - func test_selectDistinct_withStar_compilesSelectClause() { - AssertSQL("SELECT DISTINCT * FROM \"users\"", users.select(distinct: *)) - } - - func test_join_compilesJoinClause() { - AssertSQL( - "SELECT * FROM \"users\" INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", - users.join(posts, on: posts[userId] == users[id]) - ) - } - - func test_join_withExplicitType_compilesJoinClauseWithType() { - AssertSQL( - "SELECT * FROM \"users\" LEFT OUTER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", - users.join(.LeftOuter, posts, on: posts[userId] == users[id]) - ) - - AssertSQL( - "SELECT * FROM \"users\" CROSS JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", - users.join(.Cross, posts, on: posts[userId] == users[id]) - ) - } - - func test_join_withTableCondition_compilesJoinClauseWithTableCondition() { - AssertSQL( - "SELECT * FROM \"users\" INNER JOIN \"posts\" ON ((\"posts\".\"user_id\" = \"users\".\"id\") AND \"published\")", - users.join(posts.filter(published), on: posts[userId] == users[id]) - ) - } - - func test_join_whenChained_compilesAggregateJoinClause() { - AssertSQL( - "SELECT * FROM \"users\" " + - "INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\") " + - "INNER JOIN \"categories\" ON (\"categories\".\"id\" = \"posts\".\"category_id\")", - users.join(posts, on: posts[userId] == users[id]).join(categories, on: categories[id] == posts[categoryId]) - ) - } - - func test_filter_compilesWhereClause() { - AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(admin == true)) - } - - func test_filter_whenChained_compilesAggregateWhereClause() { - AssertSQL( - "SELECT * FROM \"users\" WHERE ((\"age\" >= 35) AND \"admin\")", - users.filter(age >= 35).filter(admin) - ) - } - - func test_group_withSingleExpressionName_compilesGroupClause() { - AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\"", - users.group(age)) - } - - func test_group_withVariadicExpressionNames_compilesGroupClause() { - AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\", \"admin\"", users.group(age, admin)) - } - - func test_group_withExpressionNameAndHavingBindings_compilesGroupClause() { - AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\" HAVING \"admin\"", users.group(age, having: admin)) - AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\" HAVING (\"age\" >= 30)", users.group(age, having: age >= 30)) - } - - func test_group_withExpressionNamesAndHavingBindings_compilesGroupClause() { - AssertSQL( - "SELECT * FROM \"users\" GROUP BY \"age\", \"admin\" HAVING \"admin\"", - users.group([age, admin], having: admin) - ) - AssertSQL( - "SELECT * FROM \"users\" GROUP BY \"age\", \"admin\" HAVING (\"age\" >= 30)", - users.group([age, admin], having: age >= 30) - ) - } - - func test_order_withSingleExpressionName_compilesOrderClause() { - AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\"", users.order(age)) - } - - func test_order_withVariadicExpressionNames_compilesOrderClause() { - AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\", \"email\"", users.order(age, email)) - } - - func test_order_withArrayExpressionNames_compilesOrderClause() { - AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\", \"email\"", users.order([age, email])) - } - - func test_order_withExpressionAndSortDirection_compilesOrderClause() { -// AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age.desc, email.asc)) - } - - func test_order_whenChained_resetsOrderClause() { - AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\"", users.order(email).order(age)) - } - - func test_reverse_withoutOrder_ordersByRowIdDescending() { -// AssertSQL("SELECT * FROM \"users\" ORDER BY \"ROWID\" DESC", users.reverse()) - } - - func test_reverse_withOrder_reversesOrder() { -// AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age, email.desc).reverse()) - } - - func test_limit_compilesLimitClause() { - AssertSQL("SELECT * FROM \"users\" LIMIT 5", users.limit(5)) - } - - func test_limit_withOffset_compilesOffsetClause() { - AssertSQL("SELECT * FROM \"users\" LIMIT 5 OFFSET 5", users.limit(5, offset: 5)) - } - - func test_limit_whenChained_overridesLimit() { - let query = users.limit(5) - - AssertSQL("SELECT * FROM \"users\" LIMIT 10", query.limit(10)) - AssertSQL("SELECT * FROM \"users\"", query.limit(nil)) - } - - func test_limit_whenChained_withOffset_overridesOffset() { - let query = users.limit(5, offset: 5) - - AssertSQL("SELECT * FROM \"users\" LIMIT 10 OFFSET 20", query.limit(10, offset: 20)) - AssertSQL("SELECT * FROM \"users\"", query.limit(nil)) - } - - func test_alias_aliasesTable() { - let managerId = Expression("manager_id") - - let managers = users.alias("managers") - - AssertSQL( - "SELECT * FROM \"users\" " + - "INNER JOIN \"users\" AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\")", - users.join(managers, on: managers[id] == users[managerId]) - ) - } - - func test_insert_compilesInsertExpression() { - AssertSQL( - "INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", - users.insert(email <- "alice@example.com", age <- 30) - ) - } - - func test_insert_withOnConflict_compilesInsertOrOnConflictExpression() { - AssertSQL( - "INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", - users.insert(or: .Replace, email <- "alice@example.com", age <- 30) - ) - } - - func test_insert_compilesInsertExpressionWithDefaultValues() { - AssertSQL("INSERT INTO \"users\" DEFAULT VALUES", users.insert()) - } - - func test_insert_withQuery_compilesInsertExpressionWithSelectStatement() { - let emails = Table("emails") - - AssertSQL( - "INSERT INTO \"emails\" SELECT \"email\" FROM \"users\" WHERE \"admin\"", - emails.insert(users.select(email).filter(admin)) - ) - } - - func test_update_compilesUpdateExpression() { - AssertSQL( - "UPDATE \"users\" SET \"age\" = 30, \"admin\" = 1 WHERE (\"id\" = 1)", - users.filter(id == 1).update(age <- 30, admin <- true) - ) - } - - func test_delete_compilesDeleteExpression() { - AssertSQL( - "DELETE FROM \"users\" WHERE (\"id\" = 1)", - users.filter(id == 1).delete() - ) - } - - func test_delete_compilesExistsExpression() { - AssertSQL( - "SELECT EXISTS (SELECT * FROM \"users\")", - users.exists - ) - } - - func test_count_returnsCountExpression() { - AssertSQL("SELECT count(*) FROM \"users\"", users.count) - } - - func test_scalar_returnsScalarExpression() { - AssertSQL("SELECT \"int\" FROM \"table\"", table.select(int) as ScalarQuery) - AssertSQL("SELECT \"intOptional\" FROM \"table\"", table.select(intOptional) as ScalarQuery) - AssertSQL("SELECT DISTINCT \"int\" FROM \"table\"", table.select(distinct: int) as ScalarQuery) - AssertSQL("SELECT DISTINCT \"intOptional\" FROM \"table\"", table.select(distinct: intOptional) as ScalarQuery) - } - - func test_subscript_withExpression_returnsNamespacedExpression() { - let query = Table("query") - - AssertSQL("\"query\".\"blob\"", query[data]) - AssertSQL("\"query\".\"blobOptional\"", query[dataOptional]) - - AssertSQL("\"query\".\"bool\"", query[bool]) - AssertSQL("\"query\".\"boolOptional\"", query[boolOptional]) - - AssertSQL("\"query\".\"date\"", query[date]) - AssertSQL("\"query\".\"dateOptional\"", query[dateOptional]) - - AssertSQL("\"query\".\"double\"", query[double]) - AssertSQL("\"query\".\"doubleOptional\"", query[doubleOptional]) - - AssertSQL("\"query\".\"int\"", query[int]) - AssertSQL("\"query\".\"intOptional\"", query[intOptional]) - - AssertSQL("\"query\".\"int64\"", query[int64]) - AssertSQL("\"query\".\"int64Optional\"", query[int64Optional]) - - AssertSQL("\"query\".\"string\"", query[string]) - AssertSQL("\"query\".\"stringOptional\"", query[stringOptional]) - - AssertSQL("\"query\".*", query[*]) - } - - func test_tableNamespacedByDatabase() { - let table = Table("table", database: "attached") - - AssertSQL("SELECT * FROM \"attached\".\"table\"", table) - } - -} - -class QueryIntegrationTests : SQLiteTestCase { - - let id = Expression("id") - let email = Expression("email") - - override func setUp() { - super.setUp() - - CreateUsersTable() - } - - // MARK: - - - func test_select() { - for _ in try! db.prepare(users) { - // FIXME - } - - let managerId = Expression("manager_id") - let managers = users.alias("managers") - - let alice = try! db.run(users.insert(email <- "alice@example.com")) - try! db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) - - for user in try! db.prepare(users.join(managers, on: managers[id] == users[managerId])) { - user[users[managerId]] - } - } - - func test_scalar() { - XCTAssertEqual(0, try! db.scalar(users.count)) - XCTAssertEqual(false, try! db.scalar(users.exists)) - - try! InsertUsers("alice") - XCTAssertEqual(1, try! db.scalar(users.select(id.average))) - } - - func test_pluck() { - let rowid = try! db.run(users.insert(email <- "alice@example.com")) - XCTAssertEqual(rowid, try! db.pluck(users)![id]) - } - - func test_insert() { - let id = try! db.run(users.insert(email <- "alice@example.com")) - XCTAssertEqual(1, id) - } - - func test_update() { - let changes = try! db.run(users.update(email <- "alice@example.com")) - XCTAssertEqual(0, changes) - } - - func test_delete() { - let changes = try! db.run(users.delete()) - XCTAssertEqual(0, changes) - } - -} diff --git a/SQLiteTests/SetterTests.swift b/SQLiteTests/SetterTests.swift deleted file mode 100644 index d4f189d7..00000000 --- a/SQLiteTests/SetterTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -import XCTest -import SQLite - -class SetterTests : XCTestCase { - - func test_setterAssignmentOperator_buildsSetter() { - AssertSQL("\"int\" = \"int\"", int <- int) - AssertSQL("\"int\" = 1", int <- 1) - AssertSQL("\"intOptional\" = \"int\"", intOptional <- int) - AssertSQL("\"intOptional\" = \"intOptional\"", intOptional <- intOptional) - AssertSQL("\"intOptional\" = 1", intOptional <- 1) - AssertSQL("\"intOptional\" = NULL", intOptional <- nil) - } - - func test_plusEquals_withStringExpression_buildsSetter() { - AssertSQL("\"string\" = (\"string\" || \"string\")", string += string) - AssertSQL("\"string\" = (\"string\" || 'literal')", string += "literal") - AssertSQL("\"stringOptional\" = (\"stringOptional\" || \"string\")", stringOptional += string) - AssertSQL("\"stringOptional\" = (\"stringOptional\" || \"stringOptional\")", stringOptional += stringOptional) - AssertSQL("\"stringOptional\" = (\"stringOptional\" || 'literal')", stringOptional += "literal") - } - - func test_plusEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" + \"int\")", int += int) - AssertSQL("\"int\" = (\"int\" + 1)", int += 1) - AssertSQL("\"intOptional\" = (\"intOptional\" + \"int\")", intOptional += int) - AssertSQL("\"intOptional\" = (\"intOptional\" + \"intOptional\")", intOptional += intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional += 1) - - AssertSQL("\"double\" = (\"double\" + \"double\")", double += double) - AssertSQL("\"double\" = (\"double\" + 1.0)", double += 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"double\")", doubleOptional += double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"doubleOptional\")", doubleOptional += doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + 1.0)", doubleOptional += 1) - } - - func test_minusEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" - \"int\")", int -= int) - AssertSQL("\"int\" = (\"int\" - 1)", int -= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" - \"int\")", intOptional -= int) - AssertSQL("\"intOptional\" = (\"intOptional\" - \"intOptional\")", intOptional -= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional -= 1) - - AssertSQL("\"double\" = (\"double\" - \"double\")", double -= double) - AssertSQL("\"double\" = (\"double\" - 1.0)", double -= 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"double\")", doubleOptional -= double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"doubleOptional\")", doubleOptional -= doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - 1.0)", doubleOptional -= 1) - } - - func test_timesEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" * \"int\")", int *= int) - AssertSQL("\"int\" = (\"int\" * 1)", int *= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" * \"int\")", intOptional *= int) - AssertSQL("\"intOptional\" = (\"intOptional\" * \"intOptional\")", intOptional *= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" * 1)", intOptional *= 1) - - AssertSQL("\"double\" = (\"double\" * \"double\")", double *= double) - AssertSQL("\"double\" = (\"double\" * 1.0)", double *= 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"double\")", doubleOptional *= double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"doubleOptional\")", doubleOptional *= doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * 1.0)", doubleOptional *= 1) - } - - func test_dividedByEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" / \"int\")", int /= int) - AssertSQL("\"int\" = (\"int\" / 1)", int /= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" / \"int\")", intOptional /= int) - AssertSQL("\"intOptional\" = (\"intOptional\" / \"intOptional\")", intOptional /= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" / 1)", intOptional /= 1) - - AssertSQL("\"double\" = (\"double\" / \"double\")", double /= double) - AssertSQL("\"double\" = (\"double\" / 1.0)", double /= 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"double\")", doubleOptional /= double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"doubleOptional\")", doubleOptional /= doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / 1.0)", doubleOptional /= 1) - } - - func test_moduloEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" % \"int\")", int %= int) - AssertSQL("\"int\" = (\"int\" % 1)", int %= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" % \"int\")", intOptional %= int) - AssertSQL("\"intOptional\" = (\"intOptional\" % \"intOptional\")", intOptional %= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" % 1)", intOptional %= 1) - } - - func test_leftShiftEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" << \"int\")", int <<= int) - AssertSQL("\"int\" = (\"int\" << 1)", int <<= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" << \"int\")", intOptional <<= int) - AssertSQL("\"intOptional\" = (\"intOptional\" << \"intOptional\")", intOptional <<= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" << 1)", intOptional <<= 1) - } - - func test_rightShiftEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" >> \"int\")", int >>= int) - AssertSQL("\"int\" = (\"int\" >> 1)", int >>= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" >> \"int\")", intOptional >>= int) - AssertSQL("\"intOptional\" = (\"intOptional\" >> \"intOptional\")", intOptional >>= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" >> 1)", intOptional >>= 1) - } - - func test_bitwiseAndEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" & \"int\")", int &= int) - AssertSQL("\"int\" = (\"int\" & 1)", int &= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" & \"int\")", intOptional &= int) - AssertSQL("\"intOptional\" = (\"intOptional\" & \"intOptional\")", intOptional &= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" & 1)", intOptional &= 1) - } - - func test_bitwiseOrEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" | \"int\")", int |= int) - AssertSQL("\"int\" = (\"int\" | 1)", int |= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" | \"int\")", intOptional |= int) - AssertSQL("\"intOptional\" = (\"intOptional\" | \"intOptional\")", intOptional |= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" | 1)", intOptional |= 1) - } - - func test_bitwiseExclusiveOrEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^= int) - AssertSQL("\"int\" = (~((\"int\" & 1)) & (\"int\" | 1))", int ^= 1) - AssertSQL("\"intOptional\" = (~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^= int) - AssertSQL("\"intOptional\" = (~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^= intOptional) - AssertSQL("\"intOptional\" = (~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^= 1) - } - - func test_postfixPlus_withIntegerValue_buildsSetter() { - AssertSQL("\"int\" = (\"int\" + 1)", int++) - AssertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional++) - } - - func test_postfixMinus_withIntegerValue_buildsSetter() { - AssertSQL("\"int\" = (\"int\" - 1)", int--) - AssertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional--) - } - -} diff --git a/SQLiteTests/StatementTests.swift b/SQLiteTests/StatementTests.swift deleted file mode 100644 index 5965929c..00000000 --- a/SQLiteTests/StatementTests.swift +++ /dev/null @@ -1,5 +0,0 @@ -import XCTest -import SQLite - -class StatementTests : XCTestCase { -} diff --git a/SQLiteTests/TestHelpers.swift b/SQLiteTests/TestHelpers.swift deleted file mode 100644 index 464b9c27..00000000 --- a/SQLiteTests/TestHelpers.swift +++ /dev/null @@ -1,115 +0,0 @@ -import XCTest -import SQLite - -class SQLiteTestCase : XCTestCase { - - var trace = [String: Int]() - - let db = try! Connection() - - let users = Table("users") - - override func setUp() { - super.setUp() - - db.trace { SQL in - print(SQL) - self.trace[SQL] = (self.trace[SQL] ?? 0) + 1 - } - } - - func CreateUsersTable() { - try! db.execute( - "CREATE TABLE \"users\" (" + - "id INTEGER PRIMARY KEY, " + - "email TEXT NOT NULL UNIQUE, " + - "age INTEGER, " + - "salary REAL, " + - "admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), " + - "manager_id INTEGER, " + - "FOREIGN KEY(manager_id) REFERENCES users(id)" + - ")" - ) - } - - func InsertUsers(names: String...) throws { - try InsertUsers(names) - } - - func InsertUsers(names: [String]) throws { - for name in names { try InsertUser(name) } - } - - func InsertUser(name: String, age: Int? = nil, admin: Bool = false) throws -> Statement { - return try db.run( - "INSERT INTO \"users\" (email, age, admin) values (?, ?, ?)", - "\(name)@example.com", age?.datatypeValue, admin.datatypeValue - ) - } - - func AssertSQL(SQL: String, _ executions: Int = 1, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual( - executions, trace[SQL] ?? 0, - message ?? SQL, - file: file, line: line - ) - } - - func AssertSQL(SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { - try! statement.run() - AssertSQL(SQL, 1, message, file: file, line: line) - if let count = trace[SQL] { trace[SQL] = count - 1 } - } - -// func AssertSQL(SQL: String, _ query: Query, _ message: String? = nil, file: String = __FILE__, line: UInt = __LINE__) { -// for _ in query {} -// AssertSQL(SQL, 1, message, file: file, line: line) -// if let count = trace[SQL] { trace[SQL] = count - 1 } -// } - - func async(expect description: String = "async", timeout: Double = 5, @noescape block: (() -> Void) -> Void) { - let expectation = expectationWithDescription(description) - block(expectation.fulfill) - waitForExpectationsWithTimeout(timeout, handler: nil) - } - -} - -let bool = Expression("bool") -let boolOptional = Expression("boolOptional") - -let data = Expression("blob") -let dataOptional = Expression("blobOptional") - -let date = Expression("date") -let dateOptional = Expression("dateOptional") - -let double = Expression("double") -let doubleOptional = Expression("doubleOptional") - -let int = Expression("int") -let intOptional = Expression("intOptional") - -let int64 = Expression("int64") -let int64Optional = Expression("int64Optional") - -let string = Expression("string") -let stringOptional = Expression("stringOptional") - -func AssertSQL(@autoclosure expression1: () -> String, @autoclosure _ expression2: () -> Expressible, file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(expression1(), expression2().asSQL(), file: file, line: line) -} - -func AssertThrows(@autoclosure expression: () throws -> T, file: StaticString = #file, line: UInt = #line) { - do { - try expression() - XCTFail("expression expected to throw", file: file, line: line) - } catch { - XCTAssert(true, file: file, line: line) - } -} - -let table = Table("table") -let qualifiedTable = Table("table", database: "main") -let virtualTable = VirtualTable("virtual_table") -let _view = View("view") // avoid Mac XCTestCase collision diff --git a/Sources/SQLite/Core/Backup.swift b/Sources/SQLite/Core/Backup.swift new file mode 100644 index 00000000..023acc08 --- /dev/null +++ b/Sources/SQLite/Core/Backup.swift @@ -0,0 +1,173 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Dispatch +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +/// An object representing database backup. +/// +/// See: +public final class Backup { + + /// The name of the database to backup + public enum DatabaseName { + + /// The main database + case main + + /// The temporary database + case temp + + /// A database added to the connection with ATTACH statement + case attached(name: String) + + var name: String { + switch self { + case .main: + return "main" + case .temp: + return "temp" + case .attached(let name): + return name + } + } + } + + /// Number of pages to copy while performing a backup step + public enum Pages { + + /// Indicates all remaining pages should be copied + case all + + /// Indicates the maximal number of pages to be copied in single step + case limited(number: Int32) + + var number: Int32 { + switch self { + case .all: + return -1 + case .limited(let number): + return number + } + } + } + + /// Total number of pages to copy + /// + /// See: + public var pageCount: Int32 { + return handle.map { sqlite3_backup_pagecount($0) } ?? 0 + } + + /// Number of remaining pages to copy. + /// + /// See: + public var remainingPages: Int32 { + return handle.map { sqlite3_backup_remaining($0) } ?? 0 + } + + private let targetConnection: Connection + private let sourceConnection: Connection + + private var handle: OpaquePointer? + + /// Initializes a new SQLite backup. + /// + /// - Parameters: + /// + /// - sourceConnection: The connection to the database to backup. + /// - sourceName: The name of the database to backup. + /// Default: `.main`. + /// + /// - targetConnection: The connection to the database to save backup into. + /// - targetName: The name of the database to save backup into. + /// Default: `.main`. + /// + /// - Returns: A new database backup. + /// + /// See: + public init(sourceConnection: Connection, + sourceName: DatabaseName = .main, + targetConnection: Connection, + targetName: DatabaseName = .main) throws { + + self.targetConnection = targetConnection + self.sourceConnection = sourceConnection + + self.handle = sqlite3_backup_init(targetConnection.handle, + targetName.name, + sourceConnection.handle, + sourceName.name) + + if handle == nil, let error = Result(errorCode: sqlite3_errcode(targetConnection.handle), + connection: targetConnection) { + throw error + } + } + + /// Performs a backup step. + /// + /// - Parameter pagesToCopy: The maximal number of pages to copy in one step + /// + /// - Throws: `Result.Error` if step fails. + /// + /// See: + public func step(pagesToCopy pages: Pages = .all) throws { + let status = sqlite3_backup_step(handle, pages.number) + + guard status != SQLITE_DONE else { + finish() + return + } + + if let error = Result(errorCode: status, connection: targetConnection) { + throw error + } + } + + /// Finalizes backup. + /// + /// See: + public func finish() { + guard let handle = self.handle else { + return + } + + sqlite3_backup_finish(handle) + self.handle = nil + } + + deinit { + finish() + } +} diff --git a/SQLite/Core/Blob.swift b/Sources/SQLite/Core/Blob.swift similarity index 76% rename from SQLite/Core/Blob.swift rename to Sources/SQLite/Core/Blob.swift index 1b30ffa1..a709fb42 100644 --- a/SQLite/Core/Blob.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -30,32 +30,27 @@ public struct Blob { self.bytes = bytes } - public init(bytes: UnsafePointer, length: Int) { - self.init(bytes: [UInt8](UnsafeBufferPointer( - start: UnsafePointer(bytes), count: length - ))) + public init(bytes: UnsafeRawPointer, length: Int) { + let i8bufptr = UnsafeBufferPointer(start: bytes.assumingMemoryBound(to: UInt8.self), count: length) + self.init(bytes: [UInt8](i8bufptr)) } public func toHex() -> String { - return bytes.map { - ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) - }.joinWithSeparator("") + bytes.map { + ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) + }.joined(separator: "") } - } -extension Blob : CustomStringConvertible { - +extension Blob: CustomStringConvertible { public var description: String { - return "x'\(toHex())'" + "x'\(toHex())'" } - } -extension Blob : Equatable { - +extension Blob: Equatable { } public func ==(lhs: Blob, rhs: Blob) -> Bool { - return lhs.bytes == rhs.bytes + lhs.bytes == rhs.bytes } diff --git a/Sources/SQLite/Core/Connection+Aggregation.swift b/Sources/SQLite/Core/Connection+Aggregation.swift new file mode 100644 index 00000000..a1abb74a --- /dev/null +++ b/Sources/SQLite/Core/Connection+Aggregation.swift @@ -0,0 +1,155 @@ +import Foundation +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +extension Connection { + private typealias Aggregate = @convention(block) (Int, Context, Int32, Argv) -> Void + + /// Creates or redefines a custom SQL aggregate. + /// + /// - Parameters: + /// + /// - aggregate: The name of the aggregate to create or redefine. + /// + /// - argumentCount: The number of arguments that the aggregate takes. If + /// `nil`, the aggregate may take any number of arguments. + /// + /// Default: `nil` + /// + /// - deterministic: Whether or not the aggregate is deterministic (_i.e._ + /// the aggregate always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - step: A block of code to run for each row of an aggregation group. + /// The block is called with an array of raw SQL values mapped to the + /// aggregate’s parameters, and an UnsafeMutablePointer to a state + /// variable. + /// + /// - final: A block of code to run after each row of an aggregation group + /// is processed. The block is called with an UnsafeMutablePointer to a + /// state variable, and should return a raw SQL value (or nil). + /// + /// - state: A block of code to run to produce a fresh state variable for + /// each aggregation group. The block should return an + /// UnsafeMutablePointer to the fresh state variable. + public func createAggregation( + _ functionName: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + step: @escaping ([Binding?], UnsafeMutablePointer) -> Void, + final: @escaping (UnsafeMutablePointer) -> Binding?, + state: @escaping () -> UnsafeMutablePointer) { + + let argc = argumentCount.map { Int($0) } ?? -1 + let box: Aggregate = { (stepFlag: Int, context: Context, argc: Int32, argv: Argv) in + let nBytes = Int32(MemoryLayout>.size) + guard let aggregateContext = sqlite3_aggregate_context(context, nBytes) else { + fatalError("Could not get aggregate context") + } + let mutablePointer = aggregateContext.assumingMemoryBound(to: UnsafeMutableRawPointer.self) + if stepFlag > 0 { + let arguments = argv.getBindings(argc: argc) + if aggregateContext.assumingMemoryBound(to: Int64.self).pointee == 0 { + mutablePointer.pointee = UnsafeMutableRawPointer(mutating: state()) + } + step(arguments, mutablePointer.pointee.assumingMemoryBound(to: T.self)) + } else { + let result = final(mutablePointer.pointee.assumingMemoryBound(to: T.self)) + context.set(result: result) + } + } + + func xStep(context: Context, argc: Int32, value: Argv) { + unsafeBitCast(sqlite3_user_data(context), to: Aggregate.self)(1, context, argc, value) + } + + func xFinal(context: Context) { + unsafeBitCast(sqlite3_user_data(context), to: Aggregate.self)(0, context, 0, nil) + } + + let flags = SQLITE_UTF8 | (deterministic ? SQLITE_DETERMINISTIC : 0) + let resultCode = sqlite3_create_function_v2( + handle, + functionName, + Int32(argc), + flags, + /* pApp */ unsafeBitCast(box, to: UnsafeMutableRawPointer.self), + /* xFunc */ nil, xStep, xFinal, /* xDestroy */ nil + ) + if let result = Result(errorCode: resultCode, connection: self) { + fatalError("Error creating function: \(result)") + } + register(functionName, argc: argc, value: box) + } + + public func createAggregation( + _ aggregate: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + initialValue: T, + reduce: @escaping (T, [Binding?]) -> T, + result: @escaping (T) -> Binding? + ) { + let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, ptr) in + let pointer = ptr.pointee.assumingMemoryBound(to: T.self) + let current = Unmanaged.fromOpaque(pointer).takeRetainedValue() + let next = reduce(current, bindings) + ptr.pointee = Unmanaged.passRetained(next).toOpaque() + } + + let final: (UnsafeMutablePointer) -> Binding? = { ptr in + let pointer = ptr.pointee.assumingMemoryBound(to: T.self) + let obj = Unmanaged.fromOpaque(pointer).takeRetainedValue() + let value = result(obj) + ptr.deallocate() + return value + } + + let state: () -> UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = Unmanaged.passRetained(initialValue).toOpaque() + return pointer + } + + createAggregation(aggregate, step: step, final: final, state: state) + } + + public func createAggregation( + _ aggregate: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + initialValue: T, + reduce: @escaping (T, [Binding?]) -> T, + result: @escaping (T) -> Binding? + ) { + + let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, pointer) in + let current = pointer.pointee + let next = reduce(current, bindings) + pointer.pointee = next + } + + let final: (UnsafeMutablePointer) -> Binding? = { pointer in + let value = result(pointer.pointee) + pointer.deallocate() + return value + } + + let state: () -> UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: initialValue) + return pointer + } + + createAggregation(aggregate, step: step, final: final, state: state) + } + +} diff --git a/Sources/SQLite/Core/Connection+Attach.swift b/Sources/SQLite/Core/Connection+Attach.swift new file mode 100644 index 00000000..0c674ee6 --- /dev/null +++ b/Sources/SQLite/Core/Connection+Attach.swift @@ -0,0 +1,33 @@ +import Foundation +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +extension Connection { + #if SQLITE_SWIFT_SQLCIPHER + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#attach + public func attach(_ location: Location, as schemaName: String, key: String? = nil) throws { + if let key { + try run("ATTACH DATABASE ? AS ? KEY ?", location.description, schemaName, key) + } else { + try run("ATTACH DATABASE ? AS ?", location.description, schemaName) + } + } + #else + /// See https://www3.sqlite.org/lang_attach.html + public func attach(_ location: Location, as schemaName: String) throws { + try run("ATTACH DATABASE ? AS ?", location.description, schemaName) + } + #endif + + /// See https://www3.sqlite.org/lang_detach.html + public func detach(_ schemaName: String) throws { + try run("DETACH DATABASE ?", schemaName) + } +} diff --git a/Sources/SQLite/Core/Connection+Pragmas.swift b/Sources/SQLite/Core/Connection+Pragmas.swift new file mode 100644 index 00000000..2c4f0efb --- /dev/null +++ b/Sources/SQLite/Core/Connection+Pragmas.swift @@ -0,0 +1,51 @@ +import Foundation + +public typealias UserVersion = Int32 + +public extension Connection { + /// The user version of the database. + /// See SQLite [PRAGMA user_version](https://sqlite.org/pragma.html#pragma_user_version) + var userVersion: UserVersion? { + get { + (try? scalar("PRAGMA user_version") as? Int64)?.map(Int32.init) + } + set { + _ = try? run("PRAGMA user_version = \(newValue ?? 0)") + } + } + + /// The version of SQLite. + /// See SQLite [sqlite_version()](https://sqlite.org/lang_corefunc.html#sqlite_version) + var sqliteVersion: SQLiteVersion { + guard let version = (try? scalar("SELECT sqlite_version()")) as? String, + let splits = .some(version.split(separator: ".", maxSplits: 3)), splits.count == 3, + let major = Int(splits[0]), let minor = Int(splits[1]), let point = Int(splits[2]) else { + return .zero + } + return .init(major: major, minor: minor, point: point) + } + + // Changing the foreign_keys setting affects the execution of all statements prepared using the database + // connection, including those prepared before the setting was changed. + // + // https://sqlite.org/pragma.html#pragma_foreign_keys + var foreignKeys: Bool { + get { getBoolPragma("foreign_keys") } + set { setBoolPragma("foreign_keys", newValue) } + } + + var deferForeignKeys: Bool { + get { getBoolPragma("defer_foreign_keys") } + set { setBoolPragma("defer_foreign_keys", newValue) } + } + + private func getBoolPragma(_ key: String) -> Bool { + guard let binding = try? scalar("PRAGMA \(key)"), + let intBinding = binding as? Int64 else { return false } + return intBinding == 1 + } + + private func setBoolPragma(_ key: String, _ newValue: Bool) { + _ = try? run("PRAGMA \(key) = \(newValue ? "1" : "0")") + } +} diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift new file mode 100644 index 00000000..57521f83 --- /dev/null +++ b/Sources/SQLite/Core/Connection.swift @@ -0,0 +1,787 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Dispatch +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +/// A connection to SQLite. +public final class Connection { + + /// The location of a SQLite database. + public enum Location { + + /// An in-memory database (equivalent to `.uri(":memory:")`). + /// + /// See: + case inMemory + + /// A temporary, file-backed database (equivalent to `.uri("")`). + /// + /// See: + case temporary + + /// A database located at the given URI filename (or path). + /// + /// See: + /// + /// - Parameter filename: A URI filename + /// - Parameter parameters: optional query parameters + case uri(String, parameters: [URIQueryParameter] = []) + } + + /// An SQL operation passed to update callbacks. + public enum Operation { + + /// An INSERT operation. + case insert + + /// An UPDATE operation. + case update + + /// A DELETE operation. + case delete + + fileprivate init(rawValue: Int32) { + switch rawValue { + case SQLITE_INSERT: + self = .insert + case SQLITE_UPDATE: + self = .update + case SQLITE_DELETE: + self = .delete + default: + fatalError("unhandled operation code: \(rawValue)") + } + } + } + + public var handle: OpaquePointer { _handle! } + + fileprivate var _handle: OpaquePointer? + + /// Initializes a new SQLite connection. + /// + /// - Parameters: + /// + /// - location: The location of the database. Creates a new database if it + /// doesn’t already exist (unless in read-only mode). + /// + /// Default: `.inMemory`. + /// + /// - readonly: Whether or not to open the database in a read-only state. + /// + /// Default: `false`. + /// + /// - Returns: A new database connection. + public init(_ location: Location = .inMemory, readonly: Bool = false) throws { + let flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE) + try check(sqlite3_open_v2(location.description, + &_handle, + flags | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI, + nil)) + queue.setSpecific(key: Connection.queueKey, value: queueContext) + } + + /// Initializes a new connection to a database. + /// + /// - Parameters: + /// + /// - filename: The location of the database. Creates a new database if + /// it doesn’t already exist (unless in read-only mode). + /// + /// - readonly: Whether or not to open the database in a read-only state. + /// + /// Default: `false`. + /// + /// - Throws: `Result.Error` iff a connection cannot be established. + /// + /// - Returns: A new database connection. + public convenience init(_ filename: String, readonly: Bool = false) throws { + try self.init(.uri(filename), readonly: readonly) + } + + deinit { + sqlite3_close(handle) + } + + // MARK: - + + /// Whether or not the database was opened in a read-only state. + public var readonly: Bool { sqlite3_db_readonly(handle, nil) == 1 } + + /// The last rowid inserted into the database via this connection. + public var lastInsertRowid: Int64 { + sqlite3_last_insert_rowid(handle) + } + + /// The last number of changes (inserts, updates, or deletes) made to the + /// database via this connection. + public var changes: Int { + Int(sqlite3_changes(handle)) + } + + /// The total number of changes (inserts, updates, or deletes) made to the + /// database via this connection. + public var totalChanges: Int { + Int(sqlite3_total_changes(handle)) + } + + /// Whether or not the database will return extended error codes when errors are handled. + public var usesExtendedErrorCodes: Bool = false { + didSet { + sqlite3_extended_result_codes(handle, usesExtendedErrorCodes ? 1 : 0) + } + } + + // MARK: - Execute + + /// Executes a batch of SQL statements. + /// + /// - Parameter SQL: A batch of zero or more semicolon-separated SQL + /// statements. + /// + /// - Throws: `Result.Error` if query execution fails. + public func execute(_ SQL: String) throws { + _ = try sync { try check(sqlite3_exec(handle, SQL, nil, nil, nil)) } + } + + // MARK: - Prepare + + /// Prepares a single SQL statement (with optional parameter bindings). + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + public func prepare(_ statement: String, _ bindings: Binding?...) throws -> Statement { + if !bindings.isEmpty { return try prepare(statement, bindings) } + return try Statement(self, statement) + } + + /// Prepares a single SQL statement and binds parameters to it. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + public func prepare(_ statement: String, _ bindings: [Binding?]) throws -> Statement { + try prepare(statement).bind(bindings) + } + + /// Prepares a single SQL statement and binds parameters to it. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + public func prepare(_ statement: String, _ bindings: [String: Binding?]) throws -> Statement { + try prepare(statement).bind(bindings) + } + + // MARK: - Run + + /// Runs a single SQL statement (with optional parameter bindings). + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func run(_ statement: String, _ bindings: Binding?...) throws -> Statement { + try run(statement, bindings) + } + + /// Prepares, binds, and runs a single SQL statement. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func run(_ statement: String, _ bindings: [Binding?]) throws -> Statement { + try prepare(statement).run(bindings) + } + + /// Prepares, binds, and runs a single SQL statement. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func run(_ statement: String, _ bindings: [String: Binding?]) throws -> Statement { + try prepare(statement).run(bindings) + } + + // MARK: - VACUUM + + /// Run a vacuum on the database + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func vacuum() throws -> Statement { + try run("VACUUM") + } + + // MARK: - Scalar + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ statement: String, _ bindings: Binding?...) throws -> Binding? { + try scalar(statement, bindings) + } + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ statement: String, _ bindings: [Binding?]) throws -> Binding? { + try prepare(statement).scalar(bindings) + } + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ statement: String, _ bindings: [String: Binding?]) throws -> Binding? { + try prepare(statement).scalar(bindings) + } + + // MARK: - Transactions + + /// The mode in which a transaction acquires a lock. + public enum TransactionMode: String { + + /// Defers locking the database till the first read/write executes. + case deferred = "DEFERRED" + + /// Immediately acquires a reserved lock on the database. + case immediate = "IMMEDIATE" + + /// Immediately acquires an exclusive lock on all databases. + case exclusive = "EXCLUSIVE" + + } + + // TODO: Consider not requiring a throw to roll back? + /// Runs a transaction with the given mode. + /// + /// - Note: Transactions cannot be nested. To nest transactions, see + /// `savepoint()`, instead. + /// + /// - Parameters: + /// + /// - mode: The mode in which a transaction acquires a lock. + /// + /// Default: `.deferred` + /// + /// - block: A closure to run SQL statements within the transaction. + /// The transaction will be committed when the block returns. The block + /// must throw to roll the transaction back. + /// + /// - Throws: `Result.Error`, and rethrows. + public func transaction(_ mode: TransactionMode = .deferred, block: () throws -> Void) throws { + try transaction("BEGIN \(mode.rawValue) TRANSACTION", block, "COMMIT TRANSACTION", or: "ROLLBACK TRANSACTION") + } + + // TODO: Consider not requiring a throw to roll back? + // TODO: Consider removing ability to set a name? + /// Runs a transaction with the given savepoint name (if omitted, it will + /// generate a UUID). + /// + /// - SeeAlso: `transaction()`. + /// + /// - Parameters: + /// + /// - savepointName: A unique identifier for the savepoint (optional). + /// + /// - block: A closure to run SQL statements within the transaction. + /// The savepoint will be released (committed) when the block returns. + /// The block must throw to roll the savepoint back. + /// + /// - Throws: `SQLite.Result.Error`, and rethrows. + public func savepoint(_ name: String = UUID().uuidString, block: () throws -> Void) throws { + let name = name.quote("'") + let savepoint = "SAVEPOINT \(name)" + + try transaction(savepoint, block, "RELEASE \(savepoint)", or: "ROLLBACK TO \(savepoint)") + } + + fileprivate func transaction(_ begin: String, _ block: () throws -> Void, _ commit: String, or rollback: String) throws { + return try sync { + try self.run(begin) + do { + try block() + try self.run(commit) + } catch { + try self.run(rollback) + throw error + } + } + } + + /// Interrupts any long-running queries. + public func interrupt() { + sqlite3_interrupt(handle) + } + + // MARK: - Handlers + + /// The number of seconds a connection will attempt to retry a statement + /// after encountering a busy signal (lock). + public var busyTimeout: Double = 0 { + didSet { + sqlite3_busy_timeout(handle, Int32(busyTimeout * 1_000)) + } + } + + /// Sets a handler to call after encountering a busy signal (lock). + /// + /// - Parameter callback: This block is executed during a lock in which a + /// busy error would otherwise be returned. It’s passed the number of + /// times it’s been called for this lock. If it returns `true`, it will + /// try again. If it returns `false`, no further attempts will be made. + public func busyHandler(_ callback: ((_ tries: Int) -> Bool)?) { + guard let callback else { + sqlite3_busy_handler(handle, nil, nil) + busyHandler = nil + return + } + + let box: BusyHandler = { callback(Int($0)) ? 1 : 0 } + sqlite3_busy_handler(handle, { callback, tries in + unsafeBitCast(callback, to: BusyHandler.self)(tries) + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + busyHandler = box + } + fileprivate typealias BusyHandler = @convention(block) (Int32) -> Int32 + fileprivate var busyHandler: BusyHandler? + + /// Sets a handler to call when a statement is executed with the compiled + /// SQL. + /// + /// - Parameter callback: This block is invoked when a statement is executed + /// with the compiled SQL as its argument. + /// + /// db.trace { SQL in print(SQL) } + public func trace(_ callback: ((String) -> Void)?) { + if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { + trace_v2(callback) + } else { + trace_v1(callback) + } + } + + @available(OSX, deprecated: 10.12) + @available(iOS, deprecated: 10.0) + @available(watchOS, deprecated: 3.0) + @available(tvOS, deprecated: 10.0) + fileprivate func trace_v1(_ callback: ((String) -> Void)?) { + guard let callback else { + sqlite3_trace(handle, nil /* xCallback */, nil /* pCtx */) + trace = nil + return + } + let box: Trace = { (pointer: UnsafeRawPointer) in + callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) + } + sqlite3_trace(handle, { (context: UnsafeMutableRawPointer?, SQL: UnsafePointer?) in + if let context, let SQL { + unsafeBitCast(context, to: Trace.self)(SQL) + } + }, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self) + ) + trace = box + } + + @available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) + fileprivate func trace_v2(_ callback: ((String) -> Void)?) { + guard let callback else { + // If the X callback is NULL or if the M mask is zero, then tracing is disabled. + sqlite3_trace_v2(handle, 0 /* mask */, nil /* xCallback */, nil /* pCtx */) + trace = nil + return + } + + let box: Trace = { (pointer: UnsafeRawPointer) in + callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) + } + sqlite3_trace_v2(handle, UInt32(SQLITE_TRACE_STMT) /* mask */, { + // A trace callback is invoked with four arguments: callback(T,C,P,X). + // The T argument is one of the SQLITE_TRACE constants to indicate why the + // callback was invoked. The C argument is a copy of the context pointer. + // The P and X arguments are pointers whose meanings depend on T. + (_: UInt32, context: UnsafeMutableRawPointer?, pointer: UnsafeMutableRawPointer?, _: UnsafeMutableRawPointer?) in + if let pointer, + let expandedSQL = sqlite3_expanded_sql(OpaquePointer(pointer)) { + unsafeBitCast(context, to: Trace.self)(expandedSQL) + sqlite3_free(expandedSQL) + } + return Int32(0) // currently ignored + }, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self) /* pCtx */ + ) + trace = box + } + + fileprivate typealias Trace = @convention(block) (UnsafeRawPointer) -> Void + fileprivate var trace: Trace? + + /// Registers a callback to be invoked whenever a row is inserted, updated, + /// or deleted in a rowid table. + /// + /// - Parameter callback: A callback invoked with the `Operation` (one of + /// `.Insert`, `.Update`, or `.Delete`), database name, table name, and + /// rowid. + public func updateHook(_ callback: ((_ operation: Operation, _ db: String, _ table: String, _ rowid: Int64) -> Void)?) { + guard let callback else { + sqlite3_update_hook(handle, nil, nil) + updateHook = nil + return + } + + let box: UpdateHook = { + callback( + Operation(rawValue: $0), + String(cString: $1), + String(cString: $2), + $3 + ) + } + sqlite3_update_hook(handle, { callback, operation, db, table, rowid in + unsafeBitCast(callback, to: UpdateHook.self)(operation, db!, table!, rowid) + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + updateHook = box + } + fileprivate typealias UpdateHook = @convention(block) (Int32, UnsafePointer, UnsafePointer, Int64) -> Void + fileprivate var updateHook: UpdateHook? + + /// Registers a callback to be invoked whenever a transaction is committed. + /// + /// - Parameter callback: A callback invoked whenever a transaction is + /// committed. If this callback throws, the transaction will be rolled + /// back. + public func commitHook(_ callback: (() throws -> Void)?) { + guard let callback else { + sqlite3_commit_hook(handle, nil, nil) + commitHook = nil + return + } + + let box: CommitHook = { + do { + try callback() + } catch { + return 1 + } + return 0 + } + sqlite3_commit_hook(handle, { callback in + unsafeBitCast(callback, to: CommitHook.self)() + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + commitHook = box + } + fileprivate typealias CommitHook = @convention(block) () -> Int32 + fileprivate var commitHook: CommitHook? + + /// Registers a callback to be invoked whenever a transaction rolls back. + /// + /// - Parameter callback: A callback invoked when a transaction is rolled + /// back. + public func rollbackHook(_ callback: (() -> Void)?) { + guard let callback else { + sqlite3_rollback_hook(handle, nil, nil) + rollbackHook = nil + return + } + + let box: RollbackHook = { callback() } + sqlite3_rollback_hook(handle, { callback in + unsafeBitCast(callback, to: RollbackHook.self)() + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + rollbackHook = box + } + fileprivate typealias RollbackHook = @convention(block) () -> Void + fileprivate var rollbackHook: RollbackHook? + + /// Creates or redefines a custom SQL function. + /// + /// - Parameters: + /// + /// - function: The name of the function to create or redefine. + /// + /// - argumentCount: The number of arguments that the function takes. If + /// `nil`, the function may take any number of arguments. + /// + /// Default: `nil` + /// + /// - deterministic: Whether or not the function is deterministic (_i.e._ + /// the function always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - block: A block of code to run when the function is called. The block + /// is called with an array of raw SQL values mapped to the function’s + /// parameters and should return a raw SQL value (or nil). + public func createFunction(_ functionName: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + _ block: @escaping (_ args: [Binding?]) -> Binding?) { + let argc = argumentCount.map { Int($0) } ?? -1 + let box: Function = { (context: Context, argc, argv: Argv) in + context.set(result: block(argv.getBindings(argc: argc))) + } + func xFunc(context: Context, argc: Int32, value: Argv) { + unsafeBitCast(sqlite3_user_data(context), to: Function.self)(context, argc, value) + } + let flags = SQLITE_UTF8 | (deterministic ? SQLITE_DETERMINISTIC : 0) + let resultCode = sqlite3_create_function_v2( + handle, + functionName, + Int32(argc), + flags, + /* pApp */ unsafeBitCast(box, to: UnsafeMutableRawPointer.self), + xFunc, /*xStep*/ nil, /*xFinal*/ nil, /*xDestroy*/ nil + ) + + if let result = Result(errorCode: resultCode, connection: self) { + fatalError("Error creating function: \(result)") + } + register(functionName, argc: argc, value: box) + } + + func register(_ functionName: String, argc: Int, value: Any) { + if functions[functionName] == nil { + functions[functionName] = [:] // fails on Linux, https://github.com/stephencelis/SQLite.swift/issues/1071 + } + functions[functionName]?[argc] = value + } + + fileprivate typealias Function = @convention(block) (Context, Int32, Argv) -> Void + fileprivate var functions = [String: [Int: Any]]() + + /// Defines a new collating sequence. + /// + /// - Parameters: + /// + /// - collation: The name of the collation added. + /// + /// - block: A collation function that takes two strings and returns the + /// comparison result. + public func createCollation(_ collation: String, _ block: @escaping (_ lhs: String, _ rhs: String) -> ComparisonResult) throws { + let box: Collation = { (lhs: UnsafeRawPointer, rhs: UnsafeRawPointer) in + let lstr = String(cString: lhs.assumingMemoryBound(to: UInt8.self)) + let rstr = String(cString: rhs.assumingMemoryBound(to: UInt8.self)) + return Int32(block(lstr, rstr).rawValue) + } + try check(sqlite3_create_collation_v2(handle, collation, SQLITE_UTF8, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self), { (callback: UnsafeMutableRawPointer?, _, + lhs: UnsafeRawPointer?, _, rhs: UnsafeRawPointer?) in /* xCompare */ + if let lhs, let rhs { + return unsafeBitCast(callback, to: Collation.self)(lhs, rhs) + } else { + fatalError("sqlite3_create_collation_v2 callback called with NULL pointer") + } + }, nil /* xDestroy */)) + collations[collation] = box + } + fileprivate typealias Collation = @convention(block) (UnsafeRawPointer, UnsafeRawPointer) -> Int32 + fileprivate var collations = [String: Collation]() + + // MARK: - Backup + + /// Prepares a new backup for current connection. + /// + /// - Parameters: + /// + /// - databaseName: The name of the database to backup. + /// + /// Default: `.main` + /// + /// - targetConnection: The name of the database to save backup into. + /// + /// - targetDatabaseName: The name of the database to save backup into. + /// + /// Default: `.main`. + /// + /// - Returns: A new database backup. + public func backup(databaseName: Backup.DatabaseName = .main, + usingConnection targetConnection: Connection, + andDatabaseName targetDatabaseName: Backup.DatabaseName = .main) throws -> Backup { + try Backup(sourceConnection: self, sourceName: databaseName, targetConnection: targetConnection, + targetName: targetDatabaseName) + } + + // MARK: - Error Handling + + func sync(_ block: () throws -> T) rethrows -> T { + if DispatchQueue.getSpecific(key: Connection.queueKey) == queueContext { + return try block() + } else { + return try queue.sync(execute: block) + } + } + + @discardableResult func check(_ resultCode: Int32, statement: Statement? = nil) throws -> Int32 { + guard let error = Result(errorCode: resultCode, connection: self, statement: statement) else { + return resultCode + } + + throw error + } + + fileprivate var queue = DispatchQueue(label: "SQLite.Database", attributes: []) + + fileprivate static let queueKey = DispatchSpecificKey() + + fileprivate lazy var queueContext: Int = unsafeBitCast(self, to: Int.self) + +} + +extension Connection: CustomStringConvertible { + + public var description: String { + String(cString: sqlite3_db_filename(handle, nil)) + } + +} + +extension Connection.Location: CustomStringConvertible { + + public var description: String { + switch self { + case .inMemory: + return ":memory:" + case .temporary: + return "" + case let .uri(URI, parameters): + guard parameters.count > 0, + var components = URLComponents(string: URI) else { + return URI + } + components.queryItems = + (components.queryItems ?? []) + parameters.map(\.queryItem) + if components.scheme == nil { + components.scheme = "file" + } + return components.description + } + } + +} + +typealias Context = OpaquePointer? +extension Context { + func set(result: Binding?) { + switch result { + case let blob as Blob: + sqlite3_result_blob(self, blob.bytes, Int32(blob.bytes.count), nil) + case let double as Double: + sqlite3_result_double(self, double) + case let int as Int64: + sqlite3_result_int64(self, int) + case let string as String: + sqlite3_result_text(self, string, Int32(string.lengthOfBytes(using: .utf8)), SQLITE_TRANSIENT) + case .none: + sqlite3_result_null(self) + default: + fatalError("unsupported result type: \(String(describing: result))") + } + } +} + +typealias Argv = UnsafeMutablePointer? +extension Argv { + func getBindings(argc: Int32) -> [Binding?] { + (0.. sqlite_schema + case renameColumn // ALTER TABLE ... RENAME COLUMN + case dropColumn // ALTER TABLE ... DROP COLUMN + + func isSupported(by version: SQLiteVersion) -> Bool { + switch self { + case .partialIntegrityCheck, .sqliteSchemaTable: + return version >= .init(major: 3, minor: 33) + case .renameColumn: + return version >= .init(major: 3, minor: 25) + case .dropColumn: + return version >= .init(major: 3, minor: 35) + } + } +} + +extension Connection { + func supports(_ feature: SQLiteFeature) -> Bool { + feature.isSupported(by: sqliteVersion) + } +} diff --git a/Sources/SQLite/Core/SQLiteVersion.swift b/Sources/SQLite/Core/SQLiteVersion.swift new file mode 100644 index 00000000..fa7358da --- /dev/null +++ b/Sources/SQLite/Core/SQLiteVersion.swift @@ -0,0 +1,22 @@ +import Foundation + +public struct SQLiteVersion: Comparable, CustomStringConvertible { + public let major: Int + public let minor: Int + public var point: Int = 0 + + public var description: String { + "SQLite \(major).\(minor).\(point)" + } + + public static func <(lhs: SQLiteVersion, rhs: SQLiteVersion) -> Bool { + lhs.tuple < rhs.tuple + } + + public static func ==(lhs: SQLiteVersion, rhs: SQLiteVersion) -> Bool { + lhs.tuple == rhs.tuple + } + + static var zero: SQLiteVersion = .init(major: 0, minor: 0) + private var tuple: (Int, Int, Int) { (major, minor, point) } +} diff --git a/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift similarity index 60% rename from SQLite/Core/Statement.swift rename to Sources/SQLite/Core/Statement.swift index 5e172b68..82d535b9 100644 --- a/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -24,16 +24,20 @@ #if SQLITE_SWIFT_STANDALONE import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else -import CSQLite +import SQLite3 #endif /// A single SQL statement. public final class Statement { - private var handle: COpaquePointer = nil + fileprivate var handle: OpaquePointer? - private let connection: Connection + fileprivate let connection: Connection init(_ connection: Connection, _ SQL: String) throws { self.connection = connection @@ -44,10 +48,10 @@ public final class Statement { sqlite3_finalize(handle) } - public lazy var columnCount: Int = Int(sqlite3_column_count(self.handle)) + public lazy var columnCount: Int = Int(sqlite3_column_count(handle)) - public lazy var columnNames: [String] = (0.. Statement { - return bind(values) + public func bind(_ values: Binding?...) -> Statement { + bind(values) } /// Binds a list of parameters to a statement. @@ -67,7 +71,7 @@ public final class Statement { /// - Parameter values: A list of parameters to bind to the statement. /// /// - Returns: The statement object (useful for chaining). - public func bind(values: [Binding?]) -> Statement { + public func bind(_ values: [Binding?]) -> Statement { if values.isEmpty { return self } reset() guard values.count == Int(sqlite3_bind_parameter_count(handle)) else { @@ -83,7 +87,7 @@ public final class Statement { /// statement. /// /// - Returns: The statement object (useful for chaining). - public func bind(values: [String: Binding?]) -> Statement { + public func bind(_ values: [String: Binding?]) -> Statement { reset() for (name, value) in values { let idx = sqlite3_bind_parameter_index(handle, name) @@ -95,22 +99,25 @@ public final class Statement { return self } - private func bind(value: Binding?, atIndex idx: Int) { - if value == nil { + fileprivate func bind(_ value: Binding?, atIndex idx: Int) { + switch value { + case .none: sqlite3_bind_null(handle, Int32(idx)) - } else if let value = value as? Blob { + case let value as Blob where value.bytes.count == 0: + sqlite3_bind_zeroblob(handle, Int32(idx), 0) + case let value as Blob: sqlite3_bind_blob(handle, Int32(idx), value.bytes, Int32(value.bytes.count), SQLITE_TRANSIENT) - } else if let value = value as? Double { + case let value as Double: sqlite3_bind_double(handle, Int32(idx), value) - } else if let value = value as? Int64 { + case let value as Int64: sqlite3_bind_int64(handle, Int32(idx), value) - } else if let value = value as? String { + case let value as String: sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) - } else if let value = value as? Int { + case let value as Int: self.bind(value.datatypeValue, atIndex: idx) - } else if let value = value as? Bool { + case let value as Bool: self.bind(value.datatypeValue, atIndex: idx) - } else if let value = value { + case .some(let value): fatalError("tried to bind unexpected value \(value)") } } @@ -120,7 +127,7 @@ public final class Statement { /// - Throws: `Result.Error` if query execution fails. /// /// - Returns: The statement object (useful for chaining). - public func run(bindings: Binding?...) throws -> Statement { + @discardableResult public func run(_ bindings: Binding?...) throws -> Statement { guard bindings.isEmpty else { return try run(bindings) } @@ -135,8 +142,8 @@ public final class Statement { /// - Throws: `Result.Error` if query execution fails. /// /// - Returns: The statement object (useful for chaining). - public func run(bindings: [Binding?]) throws -> Statement { - return try bind(bindings).run() + @discardableResult public func run(_ bindings: [Binding?]) throws -> Statement { + try bind(bindings).run() } /// - Parameter bindings: A dictionary of named parameters to bind to the @@ -145,118 +152,160 @@ public final class Statement { /// - Throws: `Result.Error` if query execution fails. /// /// - Returns: The statement object (useful for chaining). - public func run(bindings: [String: Binding?]) throws -> Statement { - return try bind(bindings).run() + @discardableResult public func run(_ bindings: [String: Binding?]) throws -> Statement { + try bind(bindings).run() } /// - Parameter bindings: A list of parameters to bind to the statement. /// /// - Returns: The first value of the first row returned. - @warn_unused_result public func scalar(bindings: Binding?...) throws -> Binding? { + public func scalar(_ bindings: Binding?...) throws -> Binding? { guard bindings.isEmpty else { return try scalar(bindings) } reset(clearBindings: false) - try step() + _ = try step() return row[0] } /// - Parameter bindings: A list of parameters to bind to the statement. /// /// - Returns: The first value of the first row returned. - @warn_unused_result public func scalar(bindings: [Binding?]) throws -> Binding? { - return try bind(bindings).scalar() + public func scalar(_ bindings: [Binding?]) throws -> Binding? { + try bind(bindings).scalar() } - /// - Parameter bindings: A dictionary of named parameters to bind to the /// statement. /// /// - Returns: The first value of the first row returned. - @warn_unused_result public func scalar(bindings: [String: Binding?]) throws -> Binding? { - return try bind(bindings).scalar() + public func scalar(_ bindings: [String: Binding?]) throws -> Binding? { + try bind(bindings).scalar() } public func step() throws -> Bool { - return try connection.sync { try self.connection.check(sqlite3_step(self.handle)) == SQLITE_ROW } + try connection.sync { try connection.check(sqlite3_step(handle)) == SQLITE_ROW } + } + + public func reset() { + reset(clearBindings: true) } - private func reset(clearBindings shouldClear: Bool = true) { + fileprivate func reset(clearBindings shouldClear: Bool) { sqlite3_reset(handle) - if (shouldClear) { sqlite3_clear_bindings(handle) } + if shouldClear { sqlite3_clear_bindings(handle) } } } -extension Statement : SequenceType { +extension Statement: Sequence { - public func generate() -> Statement { + public func makeIterator() -> Statement { reset(clearBindings: false) return self } } -extension Statement : GeneratorType { +public protocol FailableIterator: IteratorProtocol { + func failableNext() throws -> Self.Element? +} + +extension FailableIterator { + public func next() -> Element? { + // swiftlint:disable:next force_try + try! failableNext() + } +} + +extension Array { + public init(_ failableIterator: I) throws where I.Element == Element { + self.init() + while let row = try failableIterator.failableNext() { + append(row) + } + } +} + +extension Statement: FailableIterator { + public typealias Element = [Binding?] + public func failableNext() throws -> [Binding?]? { + try step() ? Array(row) : nil + } +} - public func next() -> [Binding?]? { - return try! step() ? Array(row) : nil +extension Statement { + func prepareRowIterator() -> RowIterator { + RowIterator(statement: self, columnNames: columnNameMap) } + var columnNameMap: [String: Int] { + var result = [String: Int]() + for (index, name) in self.columnNames.enumerated() { + result[name.quote()] = index + } + + return result + } } -extension Statement : CustomStringConvertible { +extension Statement: CustomStringConvertible { public var description: String { - return String.fromCString(sqlite3_sql(handle))! + String(cString: sqlite3_sql(handle)) } } public struct Cursor { - private let handle: COpaquePointer + fileprivate let handle: OpaquePointer - private let columnCount: Int + fileprivate let columnCount: Int - private init(_ statement: Statement) { - handle = statement.handle + fileprivate init(_ statement: Statement) { + handle = statement.handle! columnCount = statement.columnCount } public subscript(idx: Int) -> Double { - return sqlite3_column_double(handle, Int32(idx)) + sqlite3_column_double(handle, Int32(idx)) } public subscript(idx: Int) -> Int64 { - return sqlite3_column_int64(handle, Int32(idx)) + sqlite3_column_int64(handle, Int32(idx)) } public subscript(idx: Int) -> String { - return String.fromCString(UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) ?? "" + String(cString: UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) } public subscript(idx: Int) -> Blob { - let bytes = sqlite3_column_blob(handle, Int32(idx)) - let length = Int(sqlite3_column_bytes(handle, Int32(idx))) - return Blob(bytes: bytes, length: length) + if let pointer = sqlite3_column_blob(handle, Int32(idx)) { + let length = Int(sqlite3_column_bytes(handle, Int32(idx))) + return Blob(bytes: pointer, length: length) + } else { + // The return value from sqlite3_column_blob() for a zero-length BLOB is a NULL pointer. + // https://www.sqlite.org/c3ref/column_blob.html + return Blob(bytes: []) + } } // MARK: - public subscript(idx: Int) -> Bool { - return Bool.fromDatatypeValue(self[idx]) + Bool.fromDatatypeValue(self[idx]) } public subscript(idx: Int) -> Int { - return Int.fromDatatypeValue(self[idx]) + Int.fromDatatypeValue(self[idx]) } } /// Cursors provide direct access to a statement’s current row. -extension Cursor : SequenceType { +extension Cursor: Sequence { public subscript(idx: Int) -> Binding? { switch sqlite3_column_type(handle, Int32(idx)) { @@ -275,11 +324,11 @@ extension Cursor : SequenceType { } } - public func generate() -> AnyGenerator { + public func makeIterator() -> AnyIterator { var idx = 0 - return AnyGenerator { - if idx >= self.columnCount { - return Optional.None + return AnyIterator { + if idx >= columnCount { + return .none } else { idx += 1 return self[idx - 1] diff --git a/Sources/SQLite/Core/URIQueryParameter.swift b/Sources/SQLite/Core/URIQueryParameter.swift new file mode 100644 index 00000000..abbab2e7 --- /dev/null +++ b/Sources/SQLite/Core/URIQueryParameter.swift @@ -0,0 +1,53 @@ +import Foundation + +/// See https://www.sqlite.org/uri.html +public enum URIQueryParameter: CustomStringConvertible { + public enum FileMode: String { + case readOnly = "ro", readWrite = "rw", readWriteCreate = "rwc", memory + } + + public enum CacheMode: String { + case shared, `private` + } + + /// The cache query parameter determines if the new database is opened using shared cache mode or with a private cache. + case cache(CacheMode) + + /// The immutable query parameter is a boolean that signals to SQLite that the underlying database file is held on read-only media + /// and cannot be modified, even by another process with elevated privileges. + case immutable(Bool) + + /// When creating a new database file during `sqlite3_open_v2()` on unix systems, SQLite will try to set the permissions of the new database + /// file to match the existing file "filename". + case modeOf(String) + + /// The mode query parameter determines if the new database is opened read-only, read-write, read-write and created if it does not exist, + /// or that the database is a pure in-memory database that never interacts with disk, respectively. + case mode(FileMode) + + /// The nolock query parameter is a boolean that disables all calls to the `xLock`, ` xUnlock`, and `xCheckReservedLock` methods + /// of the VFS when true. + case nolock(Bool) + + /// The psow query parameter overrides the `powersafe_overwrite` property of the database file being opened. + case powersafeOverwrite(Bool) + + /// The vfs query parameter causes the database connection to be opened using the VFS called NAME. + case vfs(String) + + public var description: String { + queryItem.description + } + + var queryItem: URLQueryItem { + switch self { + case .cache(let mode): return .init(name: "cache", value: mode.rawValue) + case .immutable(let bool): return .init(name: "immutable", value: NSNumber(value: bool).description) + case .modeOf(let filename): return .init(name: "modeOf", value: filename) + case .mode(let fileMode): return .init(name: "mode", value: fileMode.rawValue) + case .nolock(let bool): return .init(name: "nolock", value: NSNumber(value: bool).description) + case .powersafeOverwrite(let bool): return .init(name: "psow", value: NSNumber(value: bool).description) + case .vfs(let name): return .init(name: "vfs", value: name) + } + } +} diff --git a/SQLite/Core/Value.swift b/Sources/SQLite/Core/Value.swift similarity index 65% rename from SQLite/Core/Value.swift rename to Sources/SQLite/Core/Value.swift index 2cfb45ed..249a7728 100644 --- a/SQLite/Core/Value.swift +++ b/Sources/SQLite/Core/Value.swift @@ -29,104 +29,104 @@ /// protocol, instead. public protocol Binding {} -public protocol Number : Binding {} +public protocol Number: Binding {} -public protocol Value : Expressible { // extensions cannot have inheritance clauses +public protocol Value: Expressible { // extensions cannot have inheritance clauses associatedtype ValueType = Self - associatedtype Datatype : Binding + associatedtype Datatype: Binding static var declaredDatatype: String { get } - static func fromDatatypeValue(datatypeValue: Datatype) -> ValueType + static func fromDatatypeValue(_ datatypeValue: Datatype) throws -> ValueType var datatypeValue: Datatype { get } } -extension Double : Number, Value { +extension Double: Number, Value { public static let declaredDatatype = "REAL" - public static func fromDatatypeValue(datatypeValue: Double) -> Double { - return datatypeValue + public static func fromDatatypeValue(_ datatypeValue: Double) -> Double { + datatypeValue } public var datatypeValue: Double { - return self + self } } -extension Int64 : Number, Value { +extension Int64: Number, Value { public static let declaredDatatype = "INTEGER" - public static func fromDatatypeValue(datatypeValue: Int64) -> Int64 { - return datatypeValue + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int64 { + datatypeValue } public var datatypeValue: Int64 { - return self + self } } -extension String : Binding, Value { +extension String: Binding, Value { public static let declaredDatatype = "TEXT" - public static func fromDatatypeValue(datatypeValue: String) -> String { - return datatypeValue + public static func fromDatatypeValue(_ datatypeValue: String) -> String { + datatypeValue } public var datatypeValue: String { - return self + self } } -extension Blob : Binding, Value { +extension Blob: Binding, Value { public static let declaredDatatype = "BLOB" - public static func fromDatatypeValue(datatypeValue: Blob) -> Blob { - return datatypeValue + public static func fromDatatypeValue(_ datatypeValue: Blob) -> Blob { + datatypeValue } public var datatypeValue: Blob { - return self + self } } // MARK: - -extension Bool : Binding, Value { +extension Bool: Binding, Value { public static var declaredDatatype = Int64.declaredDatatype - public static func fromDatatypeValue(datatypeValue: Int64) -> Bool { - return datatypeValue != 0 + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Bool { + datatypeValue != 0 } public var datatypeValue: Int64 { - return self ? 1 : 0 + self ? 1 : 0 } } -extension Int : Number, Value { +extension Int: Number, Value { public static var declaredDatatype = Int64.declaredDatatype - public static func fromDatatypeValue(datatypeValue: Int64) -> Int { - return Int(datatypeValue) + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int { + Int(datatypeValue) } public var datatypeValue: Int64 { - return Int64(self) + Int64(self) } } diff --git a/Sources/SQLite/Extensions/Cipher.swift b/Sources/SQLite/Extensions/Cipher.swift new file mode 100644 index 00000000..03194ef1 --- /dev/null +++ b/Sources/SQLite/Extensions/Cipher.swift @@ -0,0 +1,114 @@ +#if SQLITE_SWIFT_SQLCIPHER +import SQLCipher + +/// Extension methods for [SQLCipher](https://www.zetetic.net/sqlcipher/). +/// @see [sqlcipher api](https://www.zetetic.net/sqlcipher/sqlcipher-api/) +extension Connection { + + /// - Returns: the SQLCipher version + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_version + public var cipherVersion: String? { + (try? scalar("PRAGMA cipher_version")) as? String + } + + /// Specify the key for an encrypted database. This routine should be + /// called right after sqlite3_open(). + /// + /// @param key The key to use.The key itself can be a passphrase, which is converted to a key + /// using [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) key derivation. The result + /// is used as the encryption key for the database. + /// + /// Alternatively, it is possible to specify an exact byte sequence using a blob literal. + /// With this method, it is the calling application's responsibility to ensure that the data + /// provided is a 64 character hex string, which will be converted directly to 32 bytes (256 bits) + /// of key data. + /// e.g. x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99' + /// @param db name of the database, defaults to 'main' + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#sqlite3_key + public func key(_ key: String, db: String = "main") throws { + try _key_v2(db: db, keyPointer: key, keySize: key.utf8.count) + } + + public func key(_ key: Blob, db: String = "main") throws { + try _key_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count) + } + + /// Same as `key(_ key: String, db: String = "main")`, running "PRAGMA cipher_migrate;" + /// immediately after calling `sqlite3_key_v2`, which performs the migration of + /// SQLCipher database created by older major version of SQLCipher, to be able to + /// open this database with new major version of SQLCipher + /// (e.g. to open database created by SQLCipher version 3.x.x with SQLCipher version 4.x.x). + /// As "PRAGMA cipher_migrate;" is time-consuming, it is recommended to use this function + /// only after failure of `key(_ key: String, db: String = "main")`, if older versions of + /// your app may ise older version of SQLCipher + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_migrate + /// and https://discuss.zetetic.net/t/upgrading-to-sqlcipher-4/3283 + /// for more details regarding SQLCipher upgrade + public func keyAndMigrate(_ key: String, db: String = "main") throws { + try _key_v2(db: db, keyPointer: key, keySize: key.utf8.count, migrate: true) + } + + /// Same as `[`keyAndMigrate(_ key: String, db: String = "main")` accepting byte array as key + public func keyAndMigrate(_ key: Blob, db: String = "main") throws { + try _key_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count, migrate: true) + } + + /// Change the key on an open database. NB: only works if the database is already encrypted. + /// + /// To change the key on an existing encrypted database, it must first be unlocked with the + /// current encryption key. Once the database is readable and writeable, rekey can be used + /// to re-encrypt every page in the database with a new key. + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#sqlite3_rekey + public func rekey(_ key: String, db: String = "main") throws { + try _rekey_v2(db: db, keyPointer: key, keySize: key.utf8.count) + } + + public func rekey(_ key: Blob, db: String = "main") throws { + try _rekey_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count) + } + + /// Converts a non-encrypted database to an encrypted one. + /// + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#sqlcipher_export + public func sqlcipher_export(_ location: Location, key: String) throws { + let schemaName = "cipher_export" + + try attach(location, as: schemaName, key: key) + try run("SELECT sqlcipher_export(?)", schemaName) + try detach(schemaName) + } + + // MARK: - private + private func _key_v2(db: String, + keyPointer: UnsafePointer, + keySize: Int, + migrate: Bool = false) throws { + try check(sqlite3_key_v2(handle, db, keyPointer, Int32(keySize))) + if migrate { + // Run "PRAGMA cipher_migrate;" immediately after `sqlite3_key_v2` + // per recommendation of SQLCipher authors + let migrateResult = try scalar("PRAGMA cipher_migrate;") + if (migrateResult as? String) != "0" { + // "0" is the result of successful migration + throw Result.error(message: "Error in cipher migration, result \(migrateResult.debugDescription)", code: 1, statement: nil) + } + } + try cipher_key_check() + } + + private func _rekey_v2(db: String, keyPointer: UnsafePointer, keySize: Int) throws { + try check(sqlite3_rekey_v2(handle, db, keyPointer, Int32(keySize))) + } + + // When opening an existing database, sqlite3_key_v2 will not immediately throw an error if + // the key provided is incorrect. To test that the database can be successfully opened with the + // provided key, it is necessary to perform some operation on the database (i.e. read from it). + private func cipher_key_check() throws { + _ = try scalar("SELECT count(*) FROM sqlite_master;") + } +} +#endif diff --git a/SQLite/Extensions/FTS4.swift b/Sources/SQLite/Extensions/FTS4.swift similarity index 53% rename from SQLite/Extensions/FTS4.swift rename to Sources/SQLite/Extensions/FTS4.swift index 466c42c7..c02cfdc1 100644 --- a/SQLite/Extensions/FTS4.swift +++ b/Sources/SQLite/Extensions/FTS4.swift @@ -24,16 +24,16 @@ extension Module { - @warn_unused_result public static func FTS4(column: Expressible, _ more: Expressible...) -> Module { - return FTS4([column] + more) + public static func FTS4(_ column: Expressible, _ more: Expressible...) -> Module { + FTS4([column] + more) } - @warn_unused_result public static func FTS4(columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module { - return FTS4(FTS4Config().columns(columns).tokenizer(tokenizer)) + public static func FTS4(_ columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module { + FTS4(FTS4Config().columns(columns).tokenizer(tokenizer)) } - @warn_unused_result public static func FTS4(config: FTS4Config) -> Module { - return Module(name: "fts4", arguments: config.arguments()) + public static func FTS4(_ config: FTS4Config) -> Module { + Module(name: "fts4", arguments: config.arguments()) } } @@ -51,16 +51,16 @@ extension VirtualTable { /// /// - Returns: An expression appended with a `MATCH` query against the given /// pattern. - @warn_unused_result public func match(pattern: String) -> Expression { - return "MATCH".infix(tableName(), pattern) + public func match(_ pattern: String) -> Expression { + "MATCH".infix(tableName(), pattern) } - @warn_unused_result public func match(pattern: Expression) -> Expression { - return "MATCH".infix(tableName(), pattern) + public func match(_ pattern: Expression) -> Expression { + "MATCH".infix(tableName(), pattern) } - @warn_unused_result public func match(pattern: Expression) -> Expression { - return "MATCH".infix(tableName(), pattern) + public func match(_ pattern: Expression) -> Expression { + "MATCH".infix(tableName(), pattern) } /// Builds a copy of the query with a `WHERE … MATCH` clause. @@ -73,91 +73,81 @@ extension VirtualTable { /// - Parameter pattern: A pattern to match. /// /// - Returns: A query with the given `WHERE … MATCH` clause applied. - @warn_unused_result public func match(pattern: String) -> QueryType { - return filter(match(pattern)) + public func match(_ pattern: String) -> QueryType { + filter(match(pattern)) } - @warn_unused_result public func match(pattern: Expression) -> QueryType { - return filter(match(pattern)) + public func match(_ pattern: Expression) -> QueryType { + filter(match(pattern)) } - @warn_unused_result public func match(pattern: Expression) -> QueryType { - return filter(match(pattern)) + public func match(_ pattern: Expression) -> QueryType { + filter(match(pattern)) } } +// swiftlint:disable identifier_name public struct Tokenizer { public static let Simple = Tokenizer("simple") - public static let Porter = Tokenizer("porter") - @warn_unused_result public static func Unicode61(removeDiacritics removeDiacritics: Bool? = nil, tokenchars: Set = [], separators: Set = []) -> Tokenizer { + public static func Unicode61(removeDiacritics: Bool? = nil, + tokenchars: Set = [], + separators: Set = []) -> Tokenizer { var arguments = [String]() - if let removeDiacritics = removeDiacritics { - arguments.append("removeDiacritics=\(removeDiacritics ? 1 : 0)".quote()) + if let removeDiacritics { + arguments.append("remove_diacritics=\(removeDiacritics ? 1 : 0)".quote()) } if !tokenchars.isEmpty { - let joined = tokenchars.map { String($0) }.joinWithSeparator("") + let joined = tokenchars.map { String($0) }.joined(separator: "") arguments.append("tokenchars=\(joined)".quote()) } if !separators.isEmpty { - let joined = separators.map { String($0) }.joinWithSeparator("") + let joined = separators.map { String($0) }.joined(separator: "") arguments.append("separators=\(joined)".quote()) } return Tokenizer("unicode61", arguments) } - @warn_unused_result public static func Custom(name: String) -> Tokenizer { - return Tokenizer(Tokenizer.moduleName.quote(), [name.quote()]) + // https://sqlite.org/fts5.html#the_experimental_trigram_tokenizer + public static func Trigram(caseSensitive: Bool = false) -> Tokenizer { + Tokenizer("trigram", ["case_sensitive", caseSensitive ? "1" : "0"]) + } + + public static func Custom(_ name: String) -> Tokenizer { + Tokenizer(Tokenizer.moduleName.quote(), [name.quote()]) } public let name: String public let arguments: [String] - private init(_ name: String, _ arguments: [String] = []) { + fileprivate init(_ name: String, _ arguments: [String] = []) { self.name = name self.arguments = arguments } - private static let moduleName = "SQLite.swift" + fileprivate static let moduleName = "SQLite.swift" } -extension Tokenizer : CustomStringConvertible { +extension Tokenizer: CustomStringConvertible { public var description: String { - return ([name] + arguments).joinWithSeparator(" ") - } - -} - -extension Connection { - - public func registerTokenizer(submoduleName: String, next: String -> (String, Range)?) throws { - try check(_SQLiteRegisterTokenizer(handle, Tokenizer.moduleName, submoduleName) { input, offset, length in - let string = String.fromCString(input)! - - guard let (token, range) = next(string) else { return nil } - - let view = string.utf8 - offset.memory += string.substringToIndex(range.startIndex).utf8.count - length.memory = Int32(range.startIndex.samePositionIn(view).distanceTo(range.endIndex.samePositionIn(view))) - return token - }) + ([name] + arguments).joined(separator: " ") } } /// Configuration options shared between the [FTS4](https://www.sqlite.org/fts3.html) and /// [FTS5](https://www.sqlite.org/fts5.html) extensions. -public class FTSConfig { +open class FTSConfig { public enum ColumnOption { /// [The notindexed= option](https://www.sqlite.org/fts3.html#section_6_5) case unindexed @@ -171,12 +161,12 @@ public class FTSConfig { var isContentless: Bool = false /// Adds a column definition - public func column(column: Expressible, _ options: [ColumnOption] = []) -> Self { - self.columnDefinitions.append((column, options)) + @discardableResult open func column(_ column: Expressible, _ options: [ColumnOption] = []) -> Self { + columnDefinitions.append((column, options)) return self } - public func columns(columns: [Expressible]) -> Self { + @discardableResult open func columns(_ columns: [Expressible]) -> Self { for column in columns { self.column(column) } @@ -184,47 +174,47 @@ public class FTSConfig { } /// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer) - public func tokenizer(tokenizer: Tokenizer?) -> Self { + @discardableResult open func tokenizer(_ tokenizer: Tokenizer?) -> Self { self.tokenizer = tokenizer return self } /// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6) - public func prefix(prefix: [Int]) -> Self { - self.prefixes += prefix + @discardableResult open func prefix(_ prefix: [Int]) -> Self { + prefixes += prefix return self } /// [The content= option](https://www.sqlite.org/fts3.html#section_6_2) - public func externalContent(schema: SchemaType) -> Self { - self.externalContentSchema = schema + @discardableResult open func externalContent(_ schema: SchemaType) -> Self { + externalContentSchema = schema return self } /// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1) - public func contentless() -> Self { - self.isContentless = true + @discardableResult open func contentless() -> Self { + isContentless = true return self } func formatColumnDefinitions() -> [Expressible] { - return columnDefinitions.map { $0.0 } + columnDefinitions.map { $0.0 } } func arguments() -> [Expressible] { - return options().arguments + options().arguments } func options() -> Options { var options = Options() options.append(formatColumnDefinitions()) - if let tokenizer = tokenizer { + if let tokenizer { options.append("tokenize", value: Expression(literal: tokenizer.description)) } - options.appendCommaSeparated("prefix", values:prefixes.sort().map { String($0) }) + options.appendCommaSeparated("prefix", values: prefixes.sorted().map { String($0) }) if isContentless { options.append("content", value: "") - } else if let externalContentSchema = externalContentSchema { + } else if let externalContentSchema { options.append("content", value: externalContentSchema.tableName()) } return options @@ -233,59 +223,43 @@ public class FTSConfig { struct Options { var arguments = [Expressible]() - mutating func append(columns: [Expressible]) -> Options { - arguments.appendContentsOf(columns) + @discardableResult mutating func append(_ columns: [Expressible]) -> Options { + arguments.append(contentsOf: columns) return self } - mutating func appendCommaSeparated(key: String, values: [String]) -> Options { + @discardableResult mutating func appendCommaSeparated(_ key: String, values: [String]) -> Options { if values.isEmpty { return self } else { - return append(key, value: values.joinWithSeparator(",")) + return append(key, value: values.joined(separator: ",")) } } - mutating func append(key: String, value: CustomStringConvertible?) -> Options { - return append(key, value: value?.description) - } - - mutating func append(key: String, value: String?) -> Options { - return append(key, value: value.map { Expression($0) }) + @discardableResult mutating func append(_ key: String, value: String) -> Options { + append(key, value: Expression(value)) } - mutating func append(key: String, value: Expressible?) -> Options { - if let value = value { - arguments.append("=".join([Expression(literal: key), value])) - } + @discardableResult mutating func append(_ key: String, value: Expressible) -> Options { + arguments.append("=".join([Expression(literal: key), value])) return self } } } /// Configuration for the [FTS4](https://www.sqlite.org/fts3.html) extension. -public class FTS4Config : FTSConfig { +open class FTS4Config: FTSConfig { /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) - public enum MatchInfo : CustomStringConvertible { - case FTS3 - public var description: String { - return "fts3" - } + public enum MatchInfo: String { + case fts3 } /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) - public enum Order : CustomStringConvertible { + public enum Order: String { /// Data structures are optimized for returning results in ascending order by docid (default) - case Asc + case asc /// FTS4 stores its data in such a way as to optimize returning results in descending order by docid. - case Desc - - public var description: String { - switch self { - case Asc: return "asc" - case Desc: return "desc" - } - } + case desc } var compressFunction: String? @@ -298,31 +272,31 @@ public class FTS4Config : FTSConfig { } /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) - public func compress(functionName: String) -> Self { - self.compressFunction = functionName + @discardableResult open func compress(_ functionName: String) -> Self { + compressFunction = functionName return self } /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) - public func uncompress(functionName: String) -> Self { - self.uncompressFunction = functionName + @discardableResult open func uncompress(_ functionName: String) -> Self { + uncompressFunction = functionName return self } /// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3) - public func languageId(columnName: String) -> Self { - self.languageId = columnName + @discardableResult open func languageId(_ columnName: String) -> Self { + languageId = columnName return self } /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) - public func matchInfo(matchInfo: MatchInfo) -> Self { + @discardableResult open func matchInfo(_ matchInfo: MatchInfo) -> Self { self.matchInfo = matchInfo return self } /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) - public func order(order: Order) -> Self { + @discardableResult open func order(_ order: Order) -> Self { self.order = order return self } @@ -332,11 +306,21 @@ public class FTS4Config : FTSConfig { for (column, _) in (columnDefinitions.filter { $0.options.contains(.unindexed) }) { options.append("notindexed", value: column) } - options.append("languageid", value: languageId) - options.append("compress", value: compressFunction) - options.append("uncompress", value: uncompressFunction) - options.append("matchinfo", value: matchInfo) - options.append("order", value: order) + if let languageId { + options.append("languageid", value: languageId) + } + if let compressFunction { + options.append("compress", value: compressFunction) + } + if let uncompressFunction { + options.append("uncompress", value: uncompressFunction) + } + if let matchInfo { + options.append("matchinfo", value: matchInfo.rawValue) + } + if let order { + options.append("order", value: order.rawValue) + } return options } } diff --git a/SQLite/Extensions/FTS5.swift b/Sources/SQLite/Extensions/FTS5.swift similarity index 73% rename from SQLite/Extensions/FTS5.swift rename to Sources/SQLite/Extensions/FTS5.swift index 97056819..3e84e171 100644 --- a/SQLite/Extensions/FTS5.swift +++ b/Sources/SQLite/Extensions/FTS5.swift @@ -23,8 +23,8 @@ // extension Module { - @warn_unused_result public static func FTS5(config: FTS5Config) -> Module { - return Module(name: "fts5", arguments: config.arguments()) + public static func FTS5(_ config: FTS5Config) -> Module { + Module(name: "fts5", arguments: config.arguments()) } } @@ -32,22 +32,14 @@ extension Module { /// /// **Note:** this is currently only applicable when using SQLite.swift together with a FTS5-enabled version /// of SQLite. -public class FTS5Config : FTSConfig { - public enum Detail : CustomStringConvertible { +open class FTS5Config: FTSConfig { + public enum Detail: String { /// store rowid, column number, term offset - case Full + case full /// store rowid, column number - case Column + case column /// store rowid - case None - - public var description: String { - switch self { - case Full: return "full" - case Column: return "column" - case None: return "none" - } - } + case none } var detail: Detail? @@ -58,35 +50,39 @@ public class FTS5Config : FTSConfig { } /// [External Content Tables](https://www.sqlite.org/fts5.html#section_4_4_2) - public func contentRowId(column: Expressible) -> Self { - self.contentRowId = column + @discardableResult open func contentRowId(_ column: Expressible) -> Self { + contentRowId = column return self } /// [The Columnsize Option](https://www.sqlite.org/fts5.html#section_4_5) - public func columnSize(size: Int) -> Self { - self.columnSize = size + @discardableResult open func columnSize(_ size: Int) -> Self { + columnSize = size return self } /// [The Detail Option](https://www.sqlite.org/fts5.html#section_4_6) - public func detail(detail: Detail) -> Self { + @discardableResult open func detail(_ detail: Detail) -> Self { self.detail = detail return self } override func options() -> Options { var options = super.options() - options.append("content_rowid", value: contentRowId) - if let columnSize = columnSize { + if let contentRowId { + options.append("content_rowid", value: contentRowId) + } + if let columnSize { options.append("columnsize", value: Expression(value: columnSize)) } - options.append("detail", value: detail) + if let detail { + options.append("detail", value: detail.rawValue) + } return options } override func formatColumnDefinitions() -> [Expressible] { - return columnDefinitions.map { definition in + columnDefinitions.map { definition in if definition.options.contains(.unindexed) { return " ".join([definition.0, Expression(literal: "UNINDEXED")]) } else { diff --git a/SQLite/Extensions/RTree.swift b/Sources/SQLite/Extensions/RTree.swift similarity index 80% rename from SQLite/Extensions/RTree.swift rename to Sources/SQLite/Extensions/RTree.swift index a5571ea6..5ecdf78b 100644 --- a/SQLite/Extensions/RTree.swift +++ b/Sources/SQLite/Extensions/RTree.swift @@ -23,15 +23,15 @@ // extension Module { - - @warn_unused_result public static func RTree(primaryKey: Expression, _ pairs: (Expression, Expression)...) -> Module { + public static func RTree(_ primaryKey: Expression, + _ pairs: (Expression, Expression)...) + -> Module where T.Datatype == Int64, U.Datatype == Double { var arguments: [Expressible] = [primaryKey] for pair in pairs { - arguments.appendContentsOf([pair.0, pair.1] as [Expressible]) + arguments.append(contentsOf: [pair.0, pair.1] as [Expressible]) } return Module(name: "rtree", arguments: arguments) } - } diff --git a/Sources/SQLite/Foundation.swift b/Sources/SQLite/Foundation.swift new file mode 100644 index 00000000..44a31736 --- /dev/null +++ b/Sources/SQLite/Foundation.swift @@ -0,0 +1,102 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension Data: Value { + + public static var declaredDatatype: String { + Blob.declaredDatatype + } + + public static func fromDatatypeValue(_ dataValue: Blob) -> Data { + Data(dataValue.bytes) + } + + public var datatypeValue: Blob { + withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> Blob in + Blob(bytes: pointer.baseAddress!, length: count) + } + } + +} + +extension Date: Value { + + public static var declaredDatatype: String { + String.declaredDatatype + } + + public static func fromDatatypeValue(_ stringValue: String) -> Date { + dateFormatter.date(from: stringValue)! + } + + public var datatypeValue: String { + dateFormatter.string(from: self) + } + +} + +/// A global date formatter used to serialize and deserialize `NSDate` objects. +/// If multiple date formats are used in an application’s database(s), use a +/// custom `Value` type per additional format. +public var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter +}() + +extension UUID: Value { + + public static var declaredDatatype: String { + String.declaredDatatype + } + + public static func fromDatatypeValue(_ stringValue: String) -> UUID { + UUID(uuidString: stringValue)! + } + + public var datatypeValue: String { + uuidString + } + +} + +extension URL: Value { + + public static var declaredDatatype: String { + String.declaredDatatype + } + + public static func fromDatatypeValue(_ stringValue: String) -> URL { + URL(string: stringValue)! + } + + public var datatypeValue: String { + absoluteString + } + +} diff --git a/SQLite/Helpers.swift b/Sources/SQLite/Helpers.swift similarity index 50% rename from SQLite/Helpers.swift rename to Sources/SQLite/Helpers.swift index cc6da27c..e3c84589 100644 --- a/SQLite/Helpers.swift +++ b/Sources/SQLite/Helpers.swift @@ -24,90 +24,92 @@ #if SQLITE_SWIFT_STANDALONE import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else -import CSQLite +import SQLite3 #endif public typealias Star = (Expression?, Expression?) -> Expression public func *(_: Expression?, _: Expression?) -> Expression { - return Expression(literal: "*") + Expression(literal: "*") } +// swiftlint:disable:next type_name public protocol _OptionalType { associatedtype WrappedType } -extension Optional : _OptionalType { +extension Optional: _OptionalType { public typealias WrappedType = Wrapped } // let SQLITE_STATIC = unsafeBitCast(0, sqlite3_destructor_type.self) -let SQLITE_TRANSIENT = unsafeBitCast(-1, sqlite3_destructor_type.self) +let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) extension String { - - @warn_unused_result func quote(mark: Character = "\"") -> String { - let escaped = characters.reduce("") { string, character in - string + (character == mark ? "\(mark)\(mark)" : "\(character)") + func quote(_ mark: Character = "\"") -> String { + var quoted = "" + quoted.append(mark) + for character in self { + quoted.append(character) + if character == mark { + quoted.append(character) + } } - return "\(mark)\(escaped)\(mark)" + quoted.append(mark) + return quoted } - @warn_unused_result func join(expressions: [Expressible]) -> Expressible { + func join(_ expressions: [Expressible]) -> Expressible { var (template, bindings) = ([String](), [Binding?]()) for expressible in expressions { let expression = expressible.expression template.append(expression.template) - bindings.appendContentsOf(expression.bindings) + bindings.append(contentsOf: expression.bindings) } - return Expression(template.joinWithSeparator(self), bindings) + return Expression(template.joined(separator: self), bindings) + } + + func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { + infix([lhs, rhs], wrap: wrap) } - @warn_unused_result func infix(lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { - let expression = Expression(" \(self) ".join([lhs, rhs]).expression) + func infix(_ terms: [Expressible], wrap: Bool = true) -> Expression { + let expression = Expression(" \(self) ".join(terms).expression) guard wrap else { return expression } return "".wrap(expression) } - @warn_unused_result func prefix(expressions: Expressible) -> Expressible { - return "\(self) ".wrap(expressions) as Expression + func prefix(_ expressions: Expressible) -> Expressible { + "\(self) ".wrap(expressions) as Expression } - @warn_unused_result func prefix(expressions: [Expressible]) -> Expressible { - return "\(self) ".wrap(expressions) as Expression + func prefix(_ expressions: [Expressible]) -> Expressible { + "\(self) ".wrap(expressions) as Expression } - @warn_unused_result func wrap(expression: Expressible) -> Expression { - return Expression("\(self)(\(expression.expression.template))", expression.expression.bindings) + func wrap(_ expression: Expressible) -> Expression { + Expression("\(self)(\(expression.expression.template))", expression.expression.bindings) } - @warn_unused_result func wrap(expressions: [Expressible]) -> Expression { - return wrap(", ".join(expressions)) + func wrap(_ expressions: [Expressible]) -> Expression { + wrap(", ".join(expressions)) } } -@warn_unused_result func infix(lhs: Expressible, _ rhs: Expressible, wrap: Bool = true, function: String = #function) -> Expression { - return function.infix(lhs, rhs, wrap: wrap) -} - -@warn_unused_result func wrap(expression: Expressible, function: String = #function) -> Expression { - return function.wrap(expression) -} - -@warn_unused_result func wrap(expressions: [Expressible], function: String = #function) -> Expression { - return function.wrap(", ".join(expressions)) -} - -@warn_unused_result func transcode(literal: Binding?) -> String { - guard let literal = literal else { return "NULL" } +func transcode(_ literal: Binding?) -> String { + guard let literal else { return "NULL" } switch literal { case let blob as Blob: @@ -119,10 +121,11 @@ extension String { } } -@warn_unused_result func value(v: Binding) -> A { - return A.fromDatatypeValue(v as! A.Datatype) as! A +// swiftlint:disable force_cast force_try +func value(_ binding: Binding) -> A { + try! A.fromDatatypeValue(binding as! A.Datatype) as! A } -@warn_unused_result func value(v: Binding?) -> A { - return value(v!) +func value(_ binding: Binding?) -> A { + value(binding!) } diff --git a/SQLite/Info.plist b/Sources/SQLite/Info.plist similarity index 95% rename from SQLite/Info.plist rename to Sources/SQLite/Info.plist index d93473a7..ca23c84f 100644 --- a/SQLite/Info.plist +++ b/Sources/SQLite/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.10.1 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/Sources/SQLite/PrivacyInfo.xcprivacy b/Sources/SQLite/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..987771fa --- /dev/null +++ b/Sources/SQLite/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + NSPrivacyTracking + + + diff --git a/SQLite/SQLite.h b/Sources/SQLite/SQLite.h similarity index 78% rename from SQLite/SQLite.h rename to Sources/SQLite/SQLite.h index 693ce323..21ed899f 100644 --- a/SQLite/SQLite.h +++ b/Sources/SQLite/SQLite.h @@ -2,5 +2,3 @@ FOUNDATION_EXPORT double SQLiteVersionNumber; FOUNDATION_EXPORT const unsigned char SQLiteVersionString[]; - -#import diff --git a/Sources/SQLite/Schema/Connection+Schema.swift b/Sources/SQLite/Schema/Connection+Schema.swift new file mode 100644 index 00000000..2977af17 --- /dev/null +++ b/Sources/SQLite/Schema/Connection+Schema.swift @@ -0,0 +1,35 @@ +import Foundation + +public extension Connection { + var schema: SchemaReader { SchemaReader(connection: self) } + + // There are four columns in each result row. + // The first column is the name of the table that + // contains the REFERENCES clause. + // The second column is the rowid of the row that contains the + // invalid REFERENCES clause, or NULL if the child table is a WITHOUT ROWID table. + // The third column is the name of the table that is referred to. + // The fourth column is the index of the specific foreign key constraint that failed. + // + // https://sqlite.org/pragma.html#pragma_foreign_key_check + func foreignKeyCheck(table: String? = nil) throws -> [ForeignKeyError] { + try run("PRAGMA foreign_key_check" + (table.map { "(\($0.quote()))" } ?? "")) + .compactMap { (row: [Binding?]) -> ForeignKeyError? in + guard let table = row[0] as? String, + let rowId = row[1] as? Int64, + let target = row[2] as? String else { return nil } + + return ForeignKeyError(from: table, rowId: rowId, to: target) + } + } + + // This pragma does a low-level formatting and consistency check of the database. + // https://sqlite.org/pragma.html#pragma_integrity_check + func integrityCheck(table: String? = nil) throws -> [String] { + precondition(table == nil || supports(.partialIntegrityCheck), "partial integrity check not supported") + + return try run("PRAGMA integrity_check" + (table.map { "(\($0.quote()))" } ?? "")) + .compactMap { $0[0] as? String } + .filter { $0 != "ok" } + } +} diff --git a/Sources/SQLite/Schema/SchemaChanger.swift b/Sources/SQLite/Schema/SchemaChanger.swift new file mode 100644 index 00000000..6fae532a --- /dev/null +++ b/Sources/SQLite/Schema/SchemaChanger.swift @@ -0,0 +1,372 @@ +import Foundation + +/* + https://www.sqlite.org/lang_altertable.html + + The only schema altering commands directly supported by SQLite are the "rename table" and "add column" + commands shown above. + + (SQLite 3.25.0: RENAME COLUMN) + (SQLite 3.35.0: DROP COLUMN) + + However, applications can make other arbitrary changes to the format of a table using a + simple sequence of operations. The steps to make arbitrary changes to the schema design of some table X are as follows: + + 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF. + 2. Start a transaction. + 3. Remember the format of all indexes and triggers associated with table X + (SELECT sql FROM sqlite_master WHERE tbl_name='X' AND type='index') + 4. Use CREATE TABLE to construct a new table "new_X" that is in the desired revised format of table X. + 5. Transfer content from X into new_X using a statement like: INSERT INTO new_X SELECT ... FROM X. + 6. Drop the old table X: DROP TABLE X. + 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X. + 8. Use CREATE INDEX and CREATE TRIGGER to reconstruct indexes and triggers associated with table X. + 9. If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW + 10. If foreign key constraints were originally enabled then run PRAGMA foreign_key_check + 11. Commit the transaction started in step 2. + 12. If foreign keys constraints were originally enabled, reenable them now. +*/ +public class SchemaChanger: CustomStringConvertible { + public enum Error: LocalizedError { + case invalidColumnDefinition(String) + case foreignKeyError([ForeignKeyError]) + + public var errorDescription: String? { + switch self { + case .foreignKeyError(let errors): + return "Foreign key errors: \(errors)" + case .invalidColumnDefinition(let message): + return "Invalid column definition: \(message)" + } + } + } + + public enum Operation { + case addColumn(ColumnDefinition) + case addIndex(IndexDefinition, ifNotExists: Bool) + case dropColumn(String) + case dropIndex(String, ifExists: Bool) + case renameColumn(String, String) + case renameTable(String) + case createTable(columns: [ColumnDefinition], ifNotExists: Bool) + + /// Returns non-nil if the operation can be executed with a simple SQL statement + func toSQL(_ table: String, version: SQLiteVersion) -> String? { + switch self { + case .addColumn(let definition): + return "ALTER TABLE \(table.quote()) ADD COLUMN \(definition.toSQL())" + case .addIndex(let definition, let ifNotExists): + return definition.toSQL(ifNotExists: ifNotExists) + case .renameColumn(let from, let to) where SQLiteFeature.renameColumn.isSupported(by: version): + return "ALTER TABLE \(table.quote()) RENAME COLUMN \(from.quote()) TO \(to.quote())" + case .dropColumn(let column) where SQLiteFeature.dropColumn.isSupported(by: version): + return "ALTER TABLE \(table.quote()) DROP COLUMN \(column.quote())" + case .dropIndex(let name, let ifExists): + return "DROP INDEX \(ifExists ? " IF EXISTS " : "") \(name.quote())" + case .createTable(let columns, let ifNotExists): + return "CREATE TABLE \(ifNotExists ? " IF NOT EXISTS " : "") \(table.quote()) (" + + columns.map { $0.toSQL() }.joined(separator: ", ") + + ")" + default: return nil + } + } + + func validate() throws { + switch self { + case .addColumn(let definition): + // The new column may take any of the forms permissible in a CREATE TABLE statement, with the following restrictions: + // - The column may not have a PRIMARY KEY or UNIQUE constraint. + // - The column may not have a default value of CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP, or an expression in parentheses + // - If a NOT NULL constraint is specified, then the column must have a default value other than NULL. + guard definition.primaryKey == nil else { + throw Error.invalidColumnDefinition("can not add primary key column") + } + let invalidValues: [LiteralValue] = [.CURRENT_TIME, .CURRENT_DATE, .CURRENT_TIMESTAMP] + if invalidValues.contains(definition.defaultValue) { + throw Error.invalidColumnDefinition("Invalid default value") + } + if !definition.nullable && definition.defaultValue == .NULL { + throw Error.invalidColumnDefinition("NOT NULL columns must have a default value other than NULL") + } + case .dropColumn: + // The DROP COLUMN command only works if the column is not referenced by any other parts of the schema + // and is not a PRIMARY KEY and does not have a UNIQUE constraint + break + default: break + } + } + } + + public class AlterTableDefinition { + fileprivate var operations: [Operation] = [] + + public let name: String + + init(name: String) { + self.name = name + } + + public func add(column: ColumnDefinition) { + operations.append(.addColumn(column)) + } + + public func add(index: IndexDefinition, ifNotExists: Bool = false) { + operations.append(.addIndex(index, ifNotExists: ifNotExists)) + } + + public func drop(column: String) { + operations.append(.dropColumn(column)) + } + + public func drop(index: String, ifExists: Bool = false) { + operations.append(.dropIndex(index, ifExists: ifExists)) + } + + public func rename(column: String, to: String) { + operations.append(.renameColumn(column, to)) + } + } + + public class CreateTableDefinition { + fileprivate var columnDefinitions: [ColumnDefinition] = [] + fileprivate var indexDefinitions: [IndexDefinition] = [] + + let name: String + let ifNotExists: Bool + + init(name: String, ifNotExists: Bool) { + self.name = name + self.ifNotExists = ifNotExists + } + + public func add(column: ColumnDefinition) { + columnDefinitions.append(column) + } + + public func add(expression: Expression) where T: Value { + add(column: .init(name: columnName(for: expression), type: .init(expression: expression), nullable: false)) + } + + public func add(expression: Expression) where T: Value { + add(column: .init(name: columnName(for: expression), type: .init(expression: expression), nullable: true)) + } + + public func add(index: IndexDefinition) { + indexDefinitions.append(index) + } + + var operations: [Operation] { + precondition(!columnDefinitions.isEmpty) + return [ + .createTable(columns: columnDefinitions, ifNotExists: ifNotExists) + ] + indexDefinitions.map { .addIndex($0, ifNotExists: ifNotExists) } + } + + private func columnName(for expression: Expression) -> String { + switch LiteralValue(expression.template) { + case .stringLiteral(let string): return string + default: fatalError("expression is not a literal string value") + } + } + } + + private let connection: Connection + private let schemaReader: SchemaReader + private let version: SQLiteVersion + static let tempPrefix = "tmp_" + typealias Block = () throws -> Void + public typealias AlterTableDefinitionBlock = (AlterTableDefinition) -> Void + public typealias CreateTableDefinitionBlock = (CreateTableDefinition) -> Void + + struct Options: OptionSet { + let rawValue: Int + static let `default`: Options = [] + static let temp = Options(rawValue: 1) + } + + public convenience init(connection: Connection) { + self.init(connection: connection, + version: connection.sqliteVersion) + } + + init(connection: Connection, version: SQLiteVersion) { + self.connection = connection + schemaReader = connection.schema + self.version = version + } + + public func alter(table: String, block: AlterTableDefinitionBlock) throws { + let alterTableDefinition = AlterTableDefinition(name: table) + block(alterTableDefinition) + + for operation in alterTableDefinition.operations { + try run(table: table, operation: operation) + } + } + + public func create(table: String, ifNotExists: Bool = false, block: CreateTableDefinitionBlock) throws { + let createTableDefinition = CreateTableDefinition(name: table, ifNotExists: ifNotExists) + block(createTableDefinition) + + for operation in createTableDefinition.operations { + try run(table: table, operation: operation) + } + } + + public func drop(table: String, ifExists: Bool = true) throws { + try dropTable(table, ifExists: ifExists) + } + + // Beginning with release 3.25.0 (2018-09-15), references to the table within trigger bodies and + // view definitions are also renamed. + public func rename(table: String, to: String) throws { + try connection.run("ALTER TABLE \(table.quote()) RENAME TO \(to.quote())") + } + + // Runs arbitrary SQL. Should only be used if no predefined operations exist. + @discardableResult + public func run(_ sql: String, _ bindings: Binding?...) throws -> Statement { + return try connection.run(sql, bindings) + } + + private func run(table: String, operation: Operation) throws { + try operation.validate() + + if let sql = operation.toSQL(table, version: version) { + try connection.run(sql) + } else { + try doTheTableDance(table: table, operation: operation) + } + } + + private func doTheTableDance(table: String, operation: Operation) throws { + try connection.transaction { + try disableRefIntegrity { + let tempTable = "\(SchemaChanger.tempPrefix)\(table)" + try moveTable(from: table, to: tempTable, options: [.temp], operation: operation) + try rename(table: tempTable, to: table) + let foreignKeyErrors = try connection.foreignKeyCheck() + if foreignKeyErrors.count > 0 { + throw Error.foreignKeyError(foreignKeyErrors) + } + } + } + } + + private func disableRefIntegrity(block: Block) throws { + let oldForeignKeys = connection.foreignKeys + let oldDeferForeignKeys = connection.deferForeignKeys + + connection.deferForeignKeys = true + connection.foreignKeys = false + + defer { + connection.deferForeignKeys = oldDeferForeignKeys + connection.foreignKeys = oldForeignKeys + } + + try block() + } + + private func moveTable(from: String, to: String, options: Options = .default, operation: Operation? = nil) throws { + try copyTable(from: from, to: to, options: options, operation: operation) + try dropTable(from, ifExists: true) + } + + private func copyTable(from: String, to: String, options: Options = .default, operation: Operation?) throws { + let fromDefinition = TableDefinition( + name: from, + columns: try schemaReader.columnDefinitions(table: from), + indexes: try schemaReader.indexDefinitions(table: from) + ) + let toDefinition = fromDefinition + .apply(.renameTable(to)) + .apply(operation) + + try createTable(definition: toDefinition, options: options) + try createTableIndexes(definition: toDefinition) + if case .dropColumn = operation { + try copyTableContents(from: fromDefinition.apply(operation), to: toDefinition) + } else { + try copyTableContents(from: fromDefinition, to: toDefinition) + } + } + + private func createTable(definition: TableDefinition, options: Options) throws { + try connection.run(definition.toSQL(temporary: options.contains(.temp))) + } + + private func createTableIndexes(definition: TableDefinition) throws { + for index in definition.indexes { + try index.validate() + try connection.run(index.toSQL()) + } + } + + private func dropTable(_ table: String, ifExists: Bool) throws { + try connection.run("DROP TABLE \(ifExists ? "IF EXISTS" : "") \(table.quote())") + } + + private func copyTableContents(from: TableDefinition, to: TableDefinition) throws { + try connection.run(from.copySQL(to: to)) + } + + public var description: String { + "SQLiteSchemaChanger: \(connection.description)" + } +} + +extension IndexDefinition { + func renameTable(to: String) -> IndexDefinition { + func indexName() -> String { + if to.starts(with: SchemaChanger.tempPrefix) { + return "\(SchemaChanger.tempPrefix)\(name)" + } else if table.starts(with: SchemaChanger.tempPrefix) { + return name.replacingOccurrences(of: SchemaChanger.tempPrefix, with: "") + } else { + return name + } + } + return IndexDefinition(table: to, name: indexName(), unique: unique, columns: columns, where: `where`, orders: orders) + } + + func renameColumn(from: String, to: String) -> IndexDefinition { + IndexDefinition(table: table, name: name, unique: unique, columns: columns.map { + $0 == from ? to : $0 + }, where: `where`, orders: orders) + } +} + +extension TableDefinition { + func apply(_ operation: SchemaChanger.Operation?) -> TableDefinition { + switch operation { + case .none: return self + case .createTable, .addIndex, .dropIndex: fatalError() + case .addColumn: fatalError("Use 'ALTER TABLE ADD COLUMN (...)'") + + case .dropColumn(let column): + return TableDefinition(name: name, + columns: columns.filter { $0.name != column }, + indexes: indexes.filter { !$0.columns.contains(column) } + ) + case .renameColumn(let from, let to): + return TableDefinition( + name: name, + columns: columns.map { $0.rename(from: from, to: to) }, + indexes: indexes.map { $0.renameColumn(from: from, to: to) } + ) + case .renameTable(let to): + return TableDefinition(name: to, columns: columns, indexes: indexes.map { $0.renameTable(to: to) }) + } + } +} + +extension ColumnDefinition.Affinity { + init(expression: Expression) where T: Value { + self.init(T.declaredDatatype) + } + + init(expression: Expression) where T: Value { + self.init(T.declaredDatatype) + } +} diff --git a/Sources/SQLite/Schema/SchemaDefinitions.swift b/Sources/SQLite/Schema/SchemaDefinitions.swift new file mode 100644 index 00000000..d7803999 --- /dev/null +++ b/Sources/SQLite/Schema/SchemaDefinitions.swift @@ -0,0 +1,433 @@ +import Foundation + +struct TableDefinition: Equatable { + let name: String + let columns: [ColumnDefinition] + let indexes: [IndexDefinition] + + var quotedColumnList: String { + columns.map { $0.name.quote() }.joined(separator: ", ") + } +} + +// https://sqlite.org/schematab.html#interpretation_of_the_schema_table +public struct ObjectDefinition: Equatable { + public enum ObjectType: String { + case table, index, view, trigger + } + public let type: ObjectType + + // The name of the object + public let name: String + + // The name of a table or view that the object is associated with. + // * For a table or view, a copy of the name column. + // * For an index, the name of the table that is indexed + // * For a trigger, the column stores the name of the table or view that causes the trigger to fire. + public let tableName: String + + // The page number of the root b-tree page for tables and indexes, otherwise 0 or NULL + public let rootpage: Int64 + + // SQL text that describes the object (NULL for the internal indexes) + public let sql: String? + + public var isInternal: Bool { + name.starts(with: "sqlite_") || sql == nil + } +} + +// https://sqlite.org/syntax/column-def.html +// column-name -> type-name -> column-constraint* +public struct ColumnDefinition: Equatable { + + // The type affinity of a column is the recommended type for data stored in that column. + // The important idea here is that the type is recommended, not required. Any column can still + // store any type of data. It is just that some columns, given the choice, will prefer to use one + // storage class over another. The preferred storage class for a column is called its "affinity". + public enum Affinity: String, CustomStringConvertible, CaseIterable { + case INTEGER + case NUMERIC + case REAL + case TEXT + case BLOB + + public var description: String { + rawValue + } + + init(_ string: String) { + let test = string.uppercased() + // https://sqlite.org/datatype3.html#determination_of_column_affinity + if test.contains("INT") { // Rule 1 + self = .INTEGER + } else if ["CHAR", "CLOB", "TEXT"].first(where: {test.contains($0)}) != nil { // Rule 2 + self = .TEXT + } else if string.contains("BLOB") { // Rule 3 + self = .BLOB + } else if ["REAL", "FLOA", "DOUB"].first(where: {test.contains($0)}) != nil { // Rule 4 + self = .REAL + } else { // Rule 5 + self = .NUMERIC + } + } + } + + public enum OnConflict: String, CaseIterable { + case ROLLBACK + case ABORT + case FAIL + case IGNORE + case REPLACE + + init?(_ string: String) { + guard let value = (OnConflict.allCases.first { $0.rawValue == string }) else { return nil } + self = value + } + } + + public struct PrimaryKey: Equatable { + let autoIncrement: Bool + let onConflict: OnConflict? + + // swiftlint:disable:next force_try + static let pattern = try! NSRegularExpression(pattern: "PRIMARY KEY\\s*(?:ASC|DESC)?\\s*(?:ON CONFLICT (\\w+)?)?\\s*(AUTOINCREMENT)?") + + public init(autoIncrement: Bool = true, onConflict: OnConflict? = nil) { + self.autoIncrement = autoIncrement + self.onConflict = onConflict + } + + init?(sql: String) { + guard let match = PrimaryKey.pattern.firstMatch( + in: sql, + range: NSRange(location: 0, length: sql.count)) else { + return nil + } + let conflict = match.range(at: 1) + let onConflict: ColumnDefinition.OnConflict? + if conflict.location != NSNotFound { + onConflict = OnConflict((sql as NSString).substring(with: conflict)) + } else { + onConflict = nil + } + let autoIncrement = match.range(at: 2).location != NSNotFound + self.init(autoIncrement: autoIncrement, onConflict: onConflict) + } + } + + public struct ForeignKey: Equatable { + let fromColumn: String + let toTable: String + // when null, use primary key of "toTable" + let toColumn: String? + let onUpdate: String? + let onDelete: String? + + public init(toTable: String, toColumn: String? = nil, onUpdate: String? = nil, onDelete: String? = nil) { + self.init(fromColumn: "", toTable: toTable, toColumn: toColumn, onUpdate: onUpdate, onDelete: onDelete) + } + + public init(fromColumn: String, toTable: String, toColumn: String? = nil, onUpdate: String? = nil, onDelete: String? = nil) { + self.fromColumn = fromColumn + self.toTable = toTable + self.toColumn = toColumn + self.onUpdate = onUpdate + self.onDelete = onDelete + } + } + + public let name: String + public let primaryKey: PrimaryKey? + public let type: Affinity + public let nullable: Bool + public let unique: Bool + public let defaultValue: LiteralValue + public let references: ForeignKey? + + public init(name: String, + primaryKey: PrimaryKey? = nil, + type: Affinity, + nullable: Bool = true, + unique: Bool = false, + defaultValue: LiteralValue = .NULL, + references: ForeignKey? = nil) { + self.name = name + self.primaryKey = primaryKey + self.type = type + self.nullable = nullable + self.unique = unique + self.defaultValue = defaultValue + self.references = references + } + + func rename(from: String, to: String) -> ColumnDefinition { + guard from == name else { return self } + return ColumnDefinition(name: to, primaryKey: primaryKey, type: type, nullable: nullable, defaultValue: defaultValue, references: references) + } +} + +public enum LiteralValue: Equatable, CustomStringConvertible { + // swiftlint:disable force_try + private static let singleQuote = try! NSRegularExpression(pattern: "^'(.*)'$") + private static let doubleQuote = try! NSRegularExpression(pattern: "^\"(.*)\"$") + private static let blob = try! NSRegularExpression(pattern: "^[xX]\'(.*)\'$") + // swiftlint:enable force_try + + case numericLiteral(String) + case stringLiteral(String) + // BLOB literals are string literals containing hexadecimal data and preceded by a single "x" or "X" + // character. Example: X'53514C697465' + case blobLiteral(String) + + // If there is no explicit DEFAULT clause attached to a column definition, then the default value of the + // column is NULL + case NULL + + // Beginning with SQLite 3.23.0 (2018-04-02), SQLite recognizes the identifiers "TRUE" and + // "FALSE" as boolean literals, if and only if those identifiers are not already used for some other + // meaning. + // + // The boolean identifiers TRUE and FALSE are usually just aliases for the integer values 1 and 0, respectively. + case TRUE + case FALSE + // swiftlint:disable identifier_name + case CURRENT_TIME + case CURRENT_DATE + case CURRENT_TIMESTAMP + // swiftlint:enable identifier_name + + init(_ string: String?) { + guard let string else { + self = .NULL + return + } + switch string { + case "NULL": self = .NULL + case "TRUE": self = .TRUE + case "FALSE": self = .FALSE + case "CURRENT_TIME": self = .CURRENT_TIME + case "CURRENT_TIMESTAMP": self = .CURRENT_TIMESTAMP + case "CURRENT_DATE": self = .CURRENT_DATE + default: self = LiteralValue.parse(string) + } + } + + public var description: String { + switch self { + case .NULL: return "NULL" + case .TRUE: return "TRUE" + case .FALSE: return "FALSE" + case .CURRENT_TIMESTAMP: return "CURRENT_TIMESTAMP" + case .CURRENT_TIME: return "CURRENT_TIME" + case .CURRENT_DATE: return "CURRENT_DATE" + case .stringLiteral(let value): return value.quote("'") + case .blobLiteral(let value): return "X\(value.quote("'"))" + case .numericLiteral(let value): return value + } + } + + func map(block: (LiteralValue) -> U) -> U? { + if self == .NULL { + return nil + } else { + return block(self) + } + } + private static func parse(_ string: String) -> LiteralValue { + if let match = LiteralValue.singleQuote.firstMatch(in: string, range: NSRange(location: 0, length: string.count)) { + return .stringLiteral((string as NSString).substring(with: match.range(at: 1)).replacingOccurrences(of: "''", with: "'")) + } else if let match = LiteralValue.doubleQuote.firstMatch(in: string, range: NSRange(location: 0, length: string.count)) { + return .stringLiteral((string as NSString).substring(with: match.range(at: 1)).replacingOccurrences(of: "\"\"", with: "\"")) + } else if let match = LiteralValue.blob.firstMatch(in: string, range: NSRange(location: 0, length: string.count)) { + return .blobLiteral((string as NSString).substring(with: match.range(at: 1))) + } else { + return .numericLiteral(string) + } + } +} + +// https://sqlite.org/lang_createindex.html +// schema-name.index-name ON table-name ( indexed-column+ ) WHERE expr +public struct IndexDefinition: Equatable { + // SQLite supports index names up to 64 characters. + static let maxIndexLength = 64 + + // swiftlint:disable force_try + static let whereRe = try! NSRegularExpression(pattern: "\\sWHERE\\s+(.+)$") + static let orderRe = try! NSRegularExpression(pattern: "\"?(\\w+)\"? DESC") + // swiftlint:enable force_try + + public enum Order: String { case ASC, DESC } + + public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil, + orders: [String: Order]? = nil, origin: Origin? = nil) { + self.table = table + self.name = name + self.unique = unique + self.columns = columns + self.where = `where` + self.orders = orders + self.origin = origin + } + + init (table: String, name: String, unique: Bool, columns: [String], indexSQL: String?, origin: Origin? = nil) { + func wherePart(sql: String) -> String? { + IndexDefinition.whereRe.firstMatch(in: sql, options: [], range: NSRange(location: 0, length: sql.count)).map { + (sql as NSString).substring(with: $0.range(at: 1)) + } + } + + func orders(sql: String) -> [String: IndexDefinition.Order] { + IndexDefinition.orderRe + .matches(in: sql, range: NSRange(location: 0, length: sql.count)) + .reduce([String: IndexDefinition.Order]()) { (memo, result) in + var memo2 = memo + let column = (sql as NSString).substring(with: result.range(at: 1)) + memo2[column] = .DESC + return memo2 + } + } + + let orders = indexSQL.flatMap(orders) + + self.init(table: table, + name: name, + unique: unique, + columns: columns, + where: indexSQL.flatMap(wherePart), + orders: (orders?.isEmpty ?? false) ? nil : orders, + origin: origin) + } + + public let table: String + public let name: String + public let unique: Bool + public let columns: [String] + public let `where`: String? + public let orders: [String: Order]? + public let origin: Origin? + + public enum Origin: String { + case uniqueConstraint = "u" // index created from a "CREATE TABLE (... UNIQUE)" column constraint + case createIndex = "c" // index created explicitly via "CREATE INDEX ..." + case primaryKey = "pk" // index created from a "CREATE TABLE PRIMARY KEY" column constraint + } + + enum IndexError: LocalizedError { + case tooLong(String, String) + + var errorDescription: String? { + switch self { + case .tooLong(let name, let table): + return "Index name '\(name)' on table '\(table)' is too long; the limit is " + + "\(IndexDefinition.maxIndexLength) characters" + } + } + } + + // Indices with names of the form "sqlite_autoindex_TABLE_N" that are used to implement UNIQUE and PRIMARY KEY + // constraints on ordinary tables. + // https://sqlite.org/fileformat2.html#intschema + var isInternal: Bool { + name.starts(with: "sqlite_autoindex_") + } + + func validate() throws { + if name.count > IndexDefinition.maxIndexLength { + throw IndexError.tooLong(name, table) + } + } +} + +public struct ForeignKeyError: CustomStringConvertible { + public let from: String + public let rowId: Int64 + public let to: String + + public var description: String { + "\(from) [\(rowId)] => \(to)" + } +} + +extension TableDefinition { + func toSQL(temporary: Bool = false) -> String { + precondition(columns.count > 0, "no columns to create") + + return ([ + "CREATE", + temporary ? "TEMPORARY" : nil, + "TABLE", + name, + "(", + columns.map { $0.toSQL() }.joined(separator: ",\n"), + ")" + ] as [String?]).compactMap { $0 } + .joined(separator: " ") + } + + func copySQL(to: TableDefinition) -> String { + precondition(columns.count > 0) + precondition(columns.count == to.columns.count, "column counts don't match") + return "INSERT INTO \(to.name.quote()) (\(to.quotedColumnList)) SELECT \(quotedColumnList) FROM \(name.quote())" + } +} + +extension ColumnDefinition { + func toSQL() -> String { + [ + name.quote(), + type.rawValue, + defaultValue.map { "DEFAULT \($0)" }, + primaryKey.map { $0.toSQL() }, + nullable ? nil : "NOT NULL", + unique ? "UNIQUE" : nil, + references.map { $0.toSQL() } + ].compactMap { $0 } + .joined(separator: " ") + } +} + +extension IndexDefinition { + public func toSQL(ifNotExists: Bool = false) -> String { + let commaSeparatedColumns = columns.map { (column: String) -> String in + column.quote() + (orders?[column].map { " \($0.rawValue)" } ?? "") + }.joined(separator: ", ") + + return ([ + "CREATE", + unique ? "UNIQUE" : nil, + "INDEX", + ifNotExists ? "IF NOT EXISTS" : nil, + name.quote(), + "ON", + table.quote(), + "(\(commaSeparatedColumns))", + `where`.map { "WHERE \($0)" } + ] as [String?]).compactMap { $0 } + .joined(separator: " ") + } +} + +extension ColumnDefinition.ForeignKey { + func toSQL() -> String { + ([ + "REFERENCES", + toTable.quote(), + toColumn.map { "(\($0.quote()))" }, + onUpdate.map { "ON UPDATE \($0)" }, + onDelete.map { "ON DELETE \($0)" } + ] as [String?]).compactMap { $0 } + .joined(separator: " ") + } +} + +extension ColumnDefinition.PrimaryKey { + func toSQL() -> String { + [ + "PRIMARY KEY", + autoIncrement ? "AUTOINCREMENT" : nil, + onConflict.map { "ON CONFLICT \($0.rawValue)" } + ].compactMap { $0 }.joined(separator: " ") + } +} diff --git a/Sources/SQLite/Schema/SchemaReader.swift b/Sources/SQLite/Schema/SchemaReader.swift new file mode 100644 index 00000000..995cd4d7 --- /dev/null +++ b/Sources/SQLite/Schema/SchemaReader.swift @@ -0,0 +1,220 @@ +import Foundation + +public class SchemaReader { + private let connection: Connection + + init(connection: Connection) { + self.connection = connection + } + + // https://sqlite.org/pragma.html#pragma_table_info + // + // This pragma returns one row for each column in the named table. Columns in the result set include the + // column name, data type, whether or not the column can be NULL, and the default value for the column. The + // "pk" column in the result set is zero for columns that are not part of the primary key, and is the + // index of the column in the primary key for columns that are part of the primary key. + public func columnDefinitions(table: String) throws -> [ColumnDefinition] { + func parsePrimaryKey(column: String) throws -> ColumnDefinition.PrimaryKey? { + try createTableSQL(name: table).flatMap { .init(sql: $0) } + } + + let foreignKeys: [String: [ColumnDefinition.ForeignKey]] = + Dictionary(grouping: try foreignKeys(table: table), by: { $0.fromColumn }) + + let columnDefinitions = try connection.prepareRowIterator("PRAGMA table_info(\(table.quote()))") + .map { (row: Row) -> ColumnDefinition in + ColumnDefinition( + name: row[TableInfoTable.nameColumn], + primaryKey: (row[TableInfoTable.primaryKeyColumn] ?? 0) > 0 ? + try parsePrimaryKey(column: row[TableInfoTable.nameColumn]) : nil, + type: ColumnDefinition.Affinity(row[TableInfoTable.typeColumn]), + nullable: row[TableInfoTable.notNullColumn] == 0, + unique: false, + defaultValue: LiteralValue(row[TableInfoTable.defaultValueColumn]), + references: foreignKeys[row[TableInfoTable.nameColumn]]?.first + ) + } + + let internalIndexes = try indexDefinitions(table: table).filter { $0.isInternal } + return columnDefinitions.map { definition in + if let index = internalIndexes.first(where: { $0.columns.contains(definition.name) }), index.origin == .uniqueConstraint { + + ColumnDefinition(name: definition.name, + primaryKey: definition.primaryKey, + type: definition.type, + nullable: definition.nullable, + unique: true, + defaultValue: definition.defaultValue, + references: definition.references) + } else { + definition + } + } + } + + public func objectDefinitions(name: String? = nil, + type: ObjectDefinition.ObjectType? = nil, + temp: Bool = false) throws -> [ObjectDefinition] { + var query: QueryType = SchemaTable.get(for: connection, temp: temp) + if let name { + query = query.where(SchemaTable.nameColumn == name) + } + if let type { + query = query.where(SchemaTable.typeColumn == type.rawValue) + } + return try connection.prepare(query).map { row -> ObjectDefinition in + guard let type = ObjectDefinition.ObjectType(rawValue: row[SchemaTable.typeColumn]) else { + fatalError("unexpected type") + } + return ObjectDefinition( + type: type, + name: row[SchemaTable.nameColumn], + tableName: row[SchemaTable.tableNameColumn], + rootpage: row[SchemaTable.rootPageColumn] ?? 0, + sql: row[SchemaTable.sqlColumn] + ) + } + } + + public func indexDefinitions(table: String) throws -> [IndexDefinition] { + func indexSQL(name: String) throws -> String? { + try objectDefinitions(name: name, type: .index) + .compactMap(\.sql) + .first + } + + func indexInfos(name: String) throws -> [IndexInfo] { + try connection.prepareRowIterator("PRAGMA index_info(\(name.quote()))") + .compactMap { row in + IndexInfo(name: row[IndexInfoTable.nameColumn], + columnRank: row[IndexInfoTable.seqnoColumn], + columnRankWithinTable: row[IndexInfoTable.cidColumn]) + + } + } + + return try connection.prepareRowIterator("PRAGMA index_list(\(table.quote()))") + .compactMap { row -> IndexDefinition? in + let name = row[IndexListTable.nameColumn] + return IndexDefinition( + table: table, + name: name, + unique: row[IndexListTable.uniqueColumn] == 1, + columns: try indexInfos(name: name).compactMap { $0.name }, + indexSQL: try indexSQL(name: name), + origin: IndexDefinition.Origin(rawValue: row[IndexListTable.originColumn]) + ) + } + } + + func foreignKeys(table: String) throws -> [ColumnDefinition.ForeignKey] { + try connection.prepareRowIterator("PRAGMA foreign_key_list(\(table.quote()))") + .map { row in + ColumnDefinition.ForeignKey( + fromColumn: row[ForeignKeyListTable.fromColumn], + toTable: row[ForeignKeyListTable.tableColumn], + toColumn: row[ForeignKeyListTable.toColumn], + onUpdate: row[ForeignKeyListTable.onUpdateColumn] == TableBuilder.Dependency.noAction.rawValue + ? nil : row[ForeignKeyListTable.onUpdateColumn], + onDelete: row[ForeignKeyListTable.onDeleteColumn] == TableBuilder.Dependency.noAction.rawValue + ? nil : row[ForeignKeyListTable.onDeleteColumn] + ) + } + } + + func tableDefinitions() throws -> [TableDefinition] { + try objectDefinitions(type: .table) + .map { table in + TableDefinition( + name: table.name, + columns: try columnDefinitions(table: table.name), + indexes: try indexDefinitions(table: table.name) + ) + } + } + + private func createTableSQL(name: String) throws -> String? { + try ( + objectDefinitions(name: name, type: .table) + + objectDefinitions(name: name, type: .table, temp: true) + ).compactMap(\.sql).first + } + + struct IndexInfo { + let name: String? + // The rank of the column within the index. (0 means left-most.) + let columnRank: Int + // The rank of the column within the table being indexed. + // A value of -1 means rowid and a value of -2 means that an expression is being used + let columnRankWithinTable: Int + } +} + +private enum SchemaTable { + private static let name = Table("sqlite_schema", database: "main") + private static let tempName = Table("sqlite_schema", database: "temp") + // legacy names (< 3.33.0) + private static let masterName = Table("sqlite_master") + private static let tempMasterName = Table("sqlite_temp_master") + + static func get(for connection: Connection, temp: Bool = false) -> Table { + if connection.supports(.sqliteSchemaTable) { + return temp ? SchemaTable.tempName : SchemaTable.name + } else { + return temp ? SchemaTable.tempMasterName : SchemaTable.masterName + } + } + + // columns + static let typeColumn = Expression("type") + static let nameColumn = Expression("name") + static let tableNameColumn = Expression("tbl_name") + static let rootPageColumn = Expression("rootpage") + static let sqlColumn = Expression("sql") +} + +private enum TableInfoTable { + static let idColumn = Expression("cid") + static let nameColumn = Expression("name") + static let typeColumn = Expression("type") + static let notNullColumn = Expression("notnull") + static let defaultValueColumn = Expression("dflt_value") + static let primaryKeyColumn = Expression("pk") +} + +private enum IndexInfoTable { + // The rank of the column within the index. (0 means left-most.) + static let seqnoColumn = Expression("seqno") + // The rank of the column within the table being indexed. + // A value of -1 means rowid and a value of -2 means that an expression is being used. + static let cidColumn = Expression("cid") + // The name of the column being indexed. + // This columns is NULL if the column is the rowid or an expression. + static let nameColumn = Expression("name") +} + +private enum IndexListTable { + // A sequence number assigned to each index for internal tracking purposes. + static let seqColumn = Expression("seq") + // The name of the index + static let nameColumn = Expression("name") + // "1" if the index is UNIQUE and "0" if not. + static let uniqueColumn = Expression("unique") + // "c" if the index was created by a CREATE INDEX statement, + // "u" if the index was created by a UNIQUE constraint, or + // "pk" if the index was created by a PRIMARY KEY constraint. + static let originColumn = Expression("origin") + // "1" if the index is a partial index and "0" if not. + static let partialColumn = Expression("partial") +} + +private enum ForeignKeyListTable { + static let idColumn = Expression("id") + static let seqColumn = Expression("seq") + static let tableColumn = Expression("table") + static let fromColumn = Expression("from") + static let toColumn = Expression("to") // when null, use primary key + static let onUpdateColumn = Expression("on_update") + static let onDeleteColumn = Expression("on_delete") + static let matchColumn = Expression("match") +} diff --git a/SQLite/Typed/AggregateFunctions.swift b/Sources/SQLite/Typed/AggregateFunctions.swift similarity index 78% rename from SQLite/Typed/AggregateFunctions.swift rename to Sources/SQLite/Typed/AggregateFunctions.swift index 5775e0fe..17fc4a20 100644 --- a/SQLite/Typed/AggregateFunctions.swift +++ b/Sources/SQLite/Typed/AggregateFunctions.swift @@ -22,7 +22,20 @@ // THE SOFTWARE. // -extension ExpressionType where UnderlyingType : Value { +private enum Function: String { + case count + case max + case min + case avg + case sum + case total + + func wrap(_ expression: Expressible) -> Expression { + self.rawValue.wrap(expression) + } +} + +extension ExpressionType where UnderlyingType: Value { /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. /// @@ -33,7 +46,7 @@ extension ExpressionType where UnderlyingType : Value { /// - Returns: A copy of the expression prefixed with the `DISTINCT` /// keyword. public var distinct: Expression { - return Expression("DISTINCT \(template)", bindings) + Expression("DISTINCT \(template)", bindings) } /// Builds a copy of the expression wrapped with the `count` aggregate @@ -48,12 +61,12 @@ extension ExpressionType where UnderlyingType : Value { /// - Returns: A copy of the expression wrapped with the `count` aggregate /// function. public var count: Expression { - return wrap(self) + Function.count.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value { /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. /// @@ -64,7 +77,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression prefixed with the `DISTINCT` /// keyword. public var distinct: Expression { - return Expression("DISTINCT \(template)", bindings) + Expression("DISTINCT \(template)", bindings) } /// Builds a copy of the expression wrapped with the `count` aggregate @@ -79,12 +92,12 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `count` aggregate /// function. public var count: Expression { - return wrap(self) + Function.count.wrap(self) } } -extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : Comparable { +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: Comparable { /// Builds a copy of the expression wrapped with the `max` aggregate /// function. @@ -96,7 +109,7 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// - Returns: A copy of the expression wrapped with the `max` aggregate /// function. public var max: Expression { - return wrap(self) + Function.max.wrap(self) } /// Builds a copy of the expression wrapped with the `min` aggregate @@ -109,12 +122,12 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var min: Expression { - return wrap(self) + Function.min.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value, UnderlyingType.WrappedType.Datatype : Comparable { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value, UnderlyingType.WrappedType.Datatype: Comparable { /// Builds a copy of the expression wrapped with the `max` aggregate /// function. @@ -126,7 +139,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `max` aggregate /// function. public var max: Expression { - return wrap(self) + Function.max.wrap(self) } /// Builds a copy of the expression wrapped with the `min` aggregate @@ -139,12 +152,12 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var min: Expression { - return wrap(self) + Function.min.wrap(self) } } -extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : Number { +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: Number { /// Builds a copy of the expression wrapped with the `avg` aggregate /// function. @@ -153,10 +166,10 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// salary.average /// // avg("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `avg` aggregate /// function. public var average: Expression { - return "avg".wrap(self) + Function.avg.wrap(self) } /// Builds a copy of the expression wrapped with the `sum` aggregate @@ -166,10 +179,10 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// salary.sum /// // sum("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `sum` aggregate /// function. public var sum: Expression { - return wrap(self) + Function.sum.wrap(self) } /// Builds a copy of the expression wrapped with the `total` aggregate @@ -179,15 +192,15 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// salary.total /// // total("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `total` aggregate /// function. public var total: Expression { - return wrap(self) + Function.total.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value, UnderlyingType.WrappedType.Datatype : Number { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value, UnderlyingType.WrappedType.Datatype: Number { /// Builds a copy of the expression wrapped with the `avg` aggregate /// function. @@ -199,7 +212,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var average: Expression { - return "avg".wrap(self) + Function.avg.wrap(self) } /// Builds a copy of the expression wrapped with the `sum` aggregate @@ -212,7 +225,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var sum: Expression { - return wrap(self) + Function.sum.wrap(self) } /// Builds a copy of the expression wrapped with the `total` aggregate @@ -225,15 +238,15 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var total: Expression { - return wrap(self) + Function.total.wrap(self) } } extension ExpressionType where UnderlyingType == Int { - @warn_unused_result static func count(star: Star) -> Expression { - return wrap(star(nil, nil)) + static func count(_ star: Star) -> Expression { + Function.count.wrap(star(nil, nil)) } } @@ -246,6 +259,6 @@ extension ExpressionType where UnderlyingType == Int { /// /// - Returns: An expression returning `count(*)` (when called with the `*` /// function literal). -@warn_unused_result public func count(star: Star) -> Expression { - return Expression.count(star) +public func count(_ star: Star) -> Expression { + Expression.count(star) } diff --git a/Sources/SQLite/Typed/Coding.swift b/Sources/SQLite/Typed/Coding.swift new file mode 100644 index 00000000..d0061851 --- /dev/null +++ b/Sources/SQLite/Typed/Coding.swift @@ -0,0 +1,562 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension QueryType { + /// Creates an `INSERT` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `INSERT` statement for the encodable object + public func insert(_ encodable: Encodable, userInfo: [CodingUserInfoKey: Any] = [:], otherSetters: [Setter] = []) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.insert(encoder.setters + otherSetters) + } + + /// Creates an `INSERT` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// The onConflict will be passed to the actual insert function to define what should happen + /// when an error occurs during the insert operation. + /// + /// - Parameters: + /// + /// - onConlict: Define what happens when an insert operation fails + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `INSERT` statement fort the encodable object + public func insert(or onConflict: OnConflict, encodable: Encodable, userInfo: [CodingUserInfoKey: Any] = [:], + otherSetters: [Setter] = []) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.insert(or: onConflict, encoder.setters + otherSetters) + } + + /// Creates a batch `INSERT` statement by encoding the array of given objects + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodables: Encodable objects to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the inserts, per row/object. + /// + /// - Returns: An `INSERT` statement for the encodable objects + public func insertMany(_ encodables: [Encodable], userInfo: [CodingUserInfoKey: Any] = [:], + otherSetters: [Setter] = []) throws -> Insert { + let combinedSettersWithoutNils = try encodables.map { encodable -> [Setter] in + let encoder = SQLiteEncoder(userInfo: userInfo, forcingNilValueSetters: false) + try encodable.encode(to: encoder) + return encoder.setters + otherSetters + } + // requires the same number of setters per encodable + guard Set(combinedSettersWithoutNils.map(\.count)).count == 1 else { + // asymmetric sets of value insertions (some nil, some not), requires NULL value to satisfy INSERT query + let combinedSymmetricSetters = try encodables.map { encodable -> [Setter] in + let encoder = SQLiteEncoder(userInfo: userInfo, forcingNilValueSetters: true) + try encodable.encode(to: encoder) + return encoder.setters + otherSetters + } + return self.insertMany(combinedSymmetricSetters) + } + return self.insertMany(combinedSettersWithoutNils) + } + + /// Creates an `INSERT ON CONFLICT DO UPDATE` statement, aka upsert, by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - onConflictOf: The column that if conflicts should trigger an update instead of insert. + /// + /// - Returns: An `INSERT` statement fort the encodable object + public func upsert(_ encodable: Encodable, userInfo: [CodingUserInfoKey: Any] = [:], + otherSetters: [Setter] = [], onConflictOf conflicting: Expressible) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.upsert(encoder.setters + otherSetters, onConflictOf: conflicting) + } + + /// Creates an `UPDATE` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `UPDATE` statement fort the encodable object + public func update(_ encodable: Encodable, userInfo: [CodingUserInfoKey: Any] = [:], + otherSetters: [Setter] = []) throws -> Update { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.update(encoder.setters + otherSetters) + } +} + +extension Row { + /// Decode an object from this row + /// This method expects any custom nested types to be in the form of JSON data and does not handle + /// any sort of object relationships. If you want to support relationships between objects you will + /// have to provide your own Decodable implementations that decodes the correct columns. + /// + /// - Parameter: userInfo + /// + /// - Returns: a decoded object from this row + public func decode(userInfo: [CodingUserInfoKey: Any] = [:]) throws -> V { + try V(from: decoder(userInfo: userInfo)) + } + + public func decoder(userInfo: [CodingUserInfoKey: Any] = [:]) -> Decoder { + SQLiteDecoder(row: self, userInfo: userInfo) + } +} + +/// Generates a list of settings for an Encodable object +private class SQLiteEncoder: Encoder { + class SQLiteKeyedEncodingContainer: KeyedEncodingContainerProtocol { + // swiftlint:disable nesting + typealias Key = MyKey + + let encoder: SQLiteEncoder + let codingPath: [CodingKey] = [] + let forcingNilValueSetters: Bool + + init(encoder: SQLiteEncoder, forcingNilValueSetters: Bool = false) { + self.encoder = encoder + self.forcingNilValueSetters = forcingNilValueSetters + } + + func superEncoder() -> Swift.Encoder { + fatalError("SQLiteEncoding does not support super encoders") + } + + func superEncoder(forKey key: Key) -> Swift.Encoder { + fatalError("SQLiteEncoding does not support super encoders") + } + + func encodeNil(forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + + func encode(_ value: Int, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: Bool, forKey key: Key) throws { + encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: Float, forKey key: Key) throws { + encoder.setters.append(Expression(key.stringValue) <- Double(value)) + } + + func encode(_ value: Double, forKey key: Key) throws { + encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: String, forKey key: Key) throws { + encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encodeIfPresent(_ value: Int?, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: Bool?, forKey key: Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: Float?, forKey key: Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: Double?, forKey key: Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: String?, forKey key: MyKey) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encode(_ value: T, forKey key: Key) throws where T: Swift.Encodable { + switch value { + case let data as Data: + encoder.setters.append(Expression(key.stringValue) <- data) + case let date as Date: + encoder.setters.append(Expression(key.stringValue) <- date.datatypeValue) + case let uuid as UUID: + encoder.setters.append(Expression(key.stringValue) <- uuid.datatypeValue) + default: + let encoded = try JSONEncoder().encode(value) + let string = String(data: encoded, encoding: .utf8) + encoder.setters.append(Expression(key.stringValue) <- string) + } + } + + func encodeIfPresent(_ value: T?, forKey key: Key) throws where T: Swift.Encodable { + guard let value else { + guard forcingNilValueSetters else { + return + } + encoder.setters.append(Expression(key.stringValue) <- nil) + return + } + try encode(value, forKey: key) + } + + func encode(_ value: Int8, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an Int8 is not supported")) + } + + func encode(_ value: Int16, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an Int16 is not supported")) + } + + func encode(_ value: Int32, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an Int32 is not supported")) + } + + func encode(_ value: Int64, forKey key: Key) throws { + encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: UInt, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an UInt is not supported")) + } + + func encode(_ value: UInt8, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an UInt8 is not supported")) + } + + func encode(_ value: UInt16, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an UInt16 is not supported")) + } + + func encode(_ value: UInt32, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an UInt32 is not supported")) + } + + func encode(_ value: UInt64, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an UInt64 is not supported")) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) + -> KeyedEncodingContainer where NestedKey: CodingKey { + fatalError("encoding a nested container is not supported") + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + fatalError("encoding nested values is not supported") + } + } + + fileprivate var setters: [Setter] = [] + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey: Any] + let forcingNilValueSetters: Bool + + init(userInfo: [CodingUserInfoKey: Any], forcingNilValueSetters: Bool = false) { + self.userInfo = userInfo + self.forcingNilValueSetters = forcingNilValueSetters + } + + func singleValueContainer() -> SingleValueEncodingContainer { + fatalError("not supported") + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("not supported") + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self, forcingNilValueSetters: forcingNilValueSetters)) + } +} + +private class SQLiteDecoder: Decoder { + class SQLiteKeyedDecodingContainer: KeyedDecodingContainerProtocol { + typealias Key = MyKey + + let codingPath: [CodingKey] = [] + let row: Row + + init(row: Row) { + self.row = row + } + + var allKeys: [Key] { + row.columnNames.keys.compactMap({ Key(stringValue: $0) }) + } + + func contains(_ key: Key) -> Bool { + row.hasValue(for: key.stringValue) + } + + func decodeNil(forKey key: Key) throws -> Bool { + !contains(key) + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + try row.get(Expression(key.stringValue)) + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + try row.get(Expression(key.stringValue)) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an Int8 is not supported")) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an Int16 is not supported")) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an Int32 is not supported")) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + try row.get(Expression(key.stringValue)) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an UInt is not supported")) + + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an UInt8 is not supported")) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an UInt16 is not supported")) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an UInt32 is not supported")) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an UInt64 is not supported")) + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + Float(try row.get(Expression(key.stringValue))) + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + try row.get(Expression(key.stringValue)) + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + try row.get(Expression(key.stringValue)) + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Swift.Decodable { + // swiftlint:disable force_cast + switch type { + case is Data.Type: + let data = try row.get(Expression(key.stringValue)) + return data as! T + case is Date.Type: + let date = try row.get(Expression(key.stringValue)) + return date as! T + case is UUID.Type: + let uuid = try row.get(Expression(key.stringValue)) + return uuid as! T + default: + // swiftlint:enable force_cast + guard let JSONString = try row.get(Expression(key.stringValue)) else { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, + debugDescription: "an unsupported type was found")) + } + guard let data = JSONString.data(using: .utf8) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "invalid utf8 data found")) + } + return try JSONDecoder().decode(type, from: data) + } + } + + func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? { + try? row.get(Expression(key.stringValue)) + } + + func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? { + try? row.get(Expression(key.stringValue)) + } + + func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? { + try? row.get(Expression(key.stringValue)) + } + + func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? { + try? Float(row.get(Expression(key.stringValue))) + } + + func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? { + try? row.get(Expression(key.stringValue)) + } + + func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? { + try? row.get(Expression(key.stringValue)) + } + + func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? where T: Swift.Decodable { + switch type { + case is Data.Type: + return try? row.get(Expression(key.stringValue)) as? T + case is Date.Type: + return try? row.get(Expression(key.stringValue)) as? T + case is UUID.Type: + return try? row.get(Expression(key.stringValue)) as? T + default: + guard let JSONString = try row.get(Expression(key.stringValue)) else { + return nil + } + guard let data = JSONString.data(using: .utf8) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "invalid utf8 data found")) + } + return try JSONDecoder().decode(type, from: data) + } + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws + -> KeyedDecodingContainer where NestedKey: CodingKey { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding nested containers is not supported")) + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding unkeyed containers is not supported")) + } + + func superDecoder() throws -> Swift.Decoder { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding super encoders containers is not supported")) + } + + func superDecoder(forKey key: Key) throws -> Swift.Decoder { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding super decoders is not supported")) + } + } + + let row: Row + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey: Any] + + init(row: Row, userInfo: [CodingUserInfoKey: Any]) { + self.row = row + self.userInfo = userInfo + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { + KeyedDecodingContainer(SQLiteKeyedDecodingContainer(row: row)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding an unkeyed container is not supported")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding a single value container is not supported")) + } +} diff --git a/SQLite/Typed/Collation.swift b/Sources/SQLite/Typed/Collation.swift similarity index 84% rename from SQLite/Typed/Collation.swift rename to Sources/SQLite/Typed/Collation.swift index 5a632055..fec66129 100644 --- a/SQLite/Typed/Collation.swift +++ b/Sources/SQLite/Typed/Collation.swift @@ -28,40 +28,40 @@ public enum Collation { /// Compares string by raw data. - case Binary + case binary /// Like binary, but folds uppercase ASCII letters into their lowercase /// equivalents. - case Nocase + case nocase /// Like binary, but strips trailing space. - case Rtrim + case rtrim /// A custom collating sequence identified by the given string, registered /// using `Database.create(collation:…)` - case Custom(String) + case custom(String) } -extension Collation : Expressible { +extension Collation: Expressible { public var expression: Expression { - return Expression(literal: description) + Expression(literal: description) } } -extension Collation : CustomStringConvertible { +extension Collation: CustomStringConvertible { - public var description : String { + public var description: String { switch self { - case Binary: + case .binary: return "BINARY" - case Nocase: + case .nocase: return "NOCASE" - case Rtrim: + case .rtrim: return "RTRIM" - case Custom(let collation): + case .custom(let collation): return collation.quote() } } diff --git a/SQLite/Typed/CoreFunctions.swift b/Sources/SQLite/Typed/CoreFunctions.swift similarity index 64% rename from SQLite/Typed/CoreFunctions.swift rename to Sources/SQLite/Typed/CoreFunctions.swift index b597f3ba..c4359d8b 100644 --- a/SQLite/Typed/CoreFunctions.swift +++ b/Sources/SQLite/Typed/CoreFunctions.swift @@ -21,11 +21,44 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // +import Foundation + +private enum Function: String { + case abs + case round + case random + case randomblob + case zeroblob + case length + case lower + case upper + case ltrim + case rtrim + case trim + case replace + case substr + case like = "LIKE" + case `in` = "IN" + case glob = "GLOB" + case match = "MATCH" + case regexp = "REGEXP" + case collate = "COLLATE" + case ifnull + + func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { + self.rawValue.infix(lhs, rhs, wrap: wrap) + } + + func wrap(_ expression: Expressible) -> Expression { + self.rawValue.wrap(expression) + } + + func wrap(_ expressions: [Expressible]) -> Expression { + self.rawValue.wrap(", ".join(expressions)) + } +} -import Foundation.NSData - - -extension ExpressionType where UnderlyingType : Number { +extension ExpressionType where UnderlyingType: Number { /// Builds a copy of the expression wrapped with the `abs` function. /// @@ -34,13 +67,13 @@ extension ExpressionType where UnderlyingType : Number { /// // abs("x") /// /// - Returns: A copy of the expression wrapped with the `abs` function. - public var absoluteValue : Expression { - return "abs".wrap(self) + public var absoluteValue: Expression { + Function.abs.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Number { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Number { /// Builds a copy of the expression wrapped with the `abs` function. /// @@ -49,8 +82,8 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// // abs("x") /// /// - Returns: A copy of the expression wrapped with the `abs` function. - public var absoluteValue : Expression { - return "abs".wrap(self) + public var absoluteValue: Expression { + Function.abs.wrap(self) } } @@ -66,11 +99,11 @@ extension ExpressionType where UnderlyingType == Double { /// // round("salary", 2) /// /// - Returns: A copy of the expression wrapped with the `round` function. - @warn_unused_result public func round(precision: Int? = nil) -> Expression { - guard let precision = precision else { - return wrap([self]) + public func round(_ precision: Int? = nil) -> Expression { + guard let precision else { + return Function.round.wrap([self]) } - return wrap([self, Int(precision)]) + return Function.round.wrap([self, Int(precision)]) } } @@ -86,16 +119,16 @@ extension ExpressionType where UnderlyingType == Double? { /// // round("salary", 2) /// /// - Returns: A copy of the expression wrapped with the `round` function. - @warn_unused_result public func round(precision: Int? = nil) -> Expression { - guard let precision = precision else { - return wrap(self) + public func round(_ precision: Int? = nil) -> Expression { + guard let precision else { + return Function.round.wrap(self) } - return wrap([self, Int(precision)]) + return Function.round.wrap([self, Int(precision)]) } } -extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype == Int64 { +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype == Int64 { /// Builds an expression representing the `random` function. /// @@ -103,13 +136,13 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype = /// // random() /// /// - Returns: An expression calling the `random` function. - @warn_unused_result public static func random() -> Expression { - return "random".wrap([]) + public static func random() -> Expression { + Function.random.wrap([]) } } -extension ExpressionType where UnderlyingType == NSData { +extension ExpressionType where UnderlyingType == Data { /// Builds an expression representing the `randomblob` function. /// @@ -119,8 +152,8 @@ extension ExpressionType where UnderlyingType == NSData { /// - Parameter length: Length in bytes. /// /// - Returns: An expression calling the `randomblob` function. - @warn_unused_result public static func random(length: Int) -> Expression { - return "randomblob".wrap([]) + public static func random(_ length: Int) -> Expression { + Function.randomblob.wrap([]) } /// Builds an expression representing the `zeroblob` function. @@ -131,8 +164,8 @@ extension ExpressionType where UnderlyingType == NSData { /// - Parameter length: Length in bytes. /// /// - Returns: An expression calling the `zeroblob` function. - @warn_unused_result public static func allZeros(length: Int) -> Expression { - return "zeroblob".wrap([]) + public static func allZeros(_ length: Int) -> Expression { + Function.zeroblob.wrap([]) } /// Builds a copy of the expression wrapped with the `length` function. @@ -143,12 +176,12 @@ extension ExpressionType where UnderlyingType == NSData { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } } -extension ExpressionType where UnderlyingType == NSData? { +extension ExpressionType where UnderlyingType == Data? { /// Builds a copy of the expression wrapped with the `length` function. /// @@ -158,7 +191,7 @@ extension ExpressionType where UnderlyingType == NSData? { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } } @@ -173,7 +206,7 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } /// Builds a copy of the expression wrapped with the `lower` function. @@ -184,18 +217,18 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `lower` function. public var lowercaseString: Expression { - return "lower".wrap(self) + Function.lower.wrap(self) } /// Builds a copy of the expression wrapped with the `upper` function. /// /// let name = Expression("name") /// name.uppercaseString - /// // lower("name") + /// // upper("name") /// /// - Returns: A copy of the expression wrapped with the `upper` function. public var uppercaseString: Expression { - return "upper".wrap(self) + Function.upper.wrap(self) } /// Builds a copy of the expression appended with a `LIKE` query against the @@ -216,13 +249,38 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression appended with a `LIKE` query against /// the given pattern. - @warn_unused_result public func like(pattern: String, escape character: Character? = nil) -> Expression { - guard let character = character else { + public func like(_ pattern: String, escape character: Character? = nil) -> Expression { + guard let character else { return "LIKE".infix(self, pattern) } return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) } + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// let pattern = Expression("pattern") + /// email.like(pattern) + /// // "email" LIKE "pattern" + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: Expression, escape character: Character? = nil) -> Expression { + guard let character else { + return Function.like.infix(self, pattern) + } + let like: Expression = Function.like.infix(self, pattern, wrap: false) + return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) + } + /// Builds a copy of the expression appended with a `GLOB` query against the /// given pattern. /// @@ -234,8 +292,8 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression appended with a `GLOB` query against /// the given pattern. - @warn_unused_result public func glob(pattern: String) -> Expression { - return "GLOB".infix(self, pattern) + public func glob(_ pattern: String) -> Expression { + Function.glob.infix(self, pattern) } /// Builds a copy of the expression appended with a `MATCH` query against @@ -249,8 +307,8 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression appended with a `MATCH` query /// against the given pattern. - @warn_unused_result public func match(pattern: String) -> Expression { - return "MATCH".infix(self, pattern) + public func match(_ pattern: String) -> Expression { + Function.match.infix(self, pattern) } /// Builds a copy of the expression appended with a `REGEXP` query against @@ -260,8 +318,8 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression appended with a `REGEXP` query /// against the given pattern. - @warn_unused_result public func regexp(pattern: String) -> Expression { - return "REGEXP".infix(self, pattern) + public func regexp(_ pattern: String) -> Expression { + Function.regexp.infix(self, pattern) } /// Builds a copy of the expression appended with a `COLLATE` clause with @@ -275,8 +333,8 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression appended with a `COLLATE` clause /// with the given sequence. - @warn_unused_result public func collate(collation: Collation) -> Expression { - return "COLLATE".infix(self, collation) + public func collate(_ collation: Collation) -> Expression { + Function.collate.infix(self, collation) } /// Builds a copy of the expression wrapped with the `ltrim` function. @@ -290,11 +348,11 @@ extension ExpressionType where UnderlyingType == String { /// - Parameter characters: A set of characters to trim. /// /// - Returns: A copy of the expression wrapped with the `ltrim` function. - @warn_unused_result public func ltrim(characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + public func ltrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.ltrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.ltrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `rtrim` function. @@ -308,11 +366,11 @@ extension ExpressionType where UnderlyingType == String { /// - Parameter characters: A set of characters to trim. /// /// - Returns: A copy of the expression wrapped with the `rtrim` function. - @warn_unused_result public func rtrim(characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + public func rtrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.rtrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.rtrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `trim` function. @@ -326,11 +384,11 @@ extension ExpressionType where UnderlyingType == String { /// - Parameter characters: A set of characters to trim. /// /// - Returns: A copy of the expression wrapped with the `trim` function. - @warn_unused_result public func trim(characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap([self]) + public func trim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.trim.wrap([self]) } - return wrap([self, String(characters)]) + return Function.trim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `replace` function. @@ -346,19 +404,19 @@ extension ExpressionType where UnderlyingType == String { /// - replacement: The replacement string. /// /// - Returns: A copy of the expression wrapped with the `replace` function. - @warn_unused_result public func replace(pattern: String, with replacement: String) -> Expression { - return "replace".wrap([self, pattern, replacement]) + public func replace(_ pattern: String, with replacement: String) -> Expression { + Function.replace.wrap([self, pattern, replacement]) } - @warn_unused_result public func substring(location: Int, length: Int? = nil) -> Expression { - guard let length = length else { - return "substr".wrap([self, location]) + public func substring(_ location: Int, length: Int? = nil) -> Expression { + guard let length else { + return Function.substr.wrap([self, location]) } - return "substr".wrap([self, location, length]) + return Function.substr.wrap([self, location, length]) } public subscript(range: Range) -> Expression { - return substring(range.startIndex, length: range.endIndex - range.startIndex) + substring(range.lowerBound, length: range.upperBound - range.lowerBound) } } @@ -373,7 +431,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } /// Builds a copy of the expression wrapped with the `lower` function. @@ -384,7 +442,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `lower` function. public var lowercaseString: Expression { - return "lower".wrap(self) + Function.lower.wrap(self) } /// Builds a copy of the expression wrapped with the `upper` function. @@ -395,7 +453,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `upper` function. public var uppercaseString: Expression { - return "upper".wrap(self) + Function.upper.wrap(self) } /// Builds a copy of the expression appended with a `LIKE` query against the @@ -416,13 +474,38 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression appended with a `LIKE` query against /// the given pattern. - @warn_unused_result public func like(pattern: String, escape character: Character? = nil) -> Expression { - guard let character = character else { - return "LIKE".infix(self, pattern) + public func like(_ pattern: String, escape character: Character? = nil) -> Expression { + guard let character else { + return Function.like.infix(self, pattern) } return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) } + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// let pattern = Expression("pattern") + /// email.like(pattern) + /// // "email" LIKE "pattern" + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: Expression, escape character: Character? = nil) -> Expression { + guard let character else { + return Function.like.infix(self, pattern) + } + let like: Expression = Function.like.infix(self, pattern, wrap: false) + return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) + } + /// Builds a copy of the expression appended with a `GLOB` query against the /// given pattern. /// @@ -434,8 +517,8 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression appended with a `GLOB` query against /// the given pattern. - @warn_unused_result public func glob(pattern: String) -> Expression { - return "GLOB".infix(self, pattern) + public func glob(_ pattern: String) -> Expression { + Function.glob.infix(self, pattern) } /// Builds a copy of the expression appended with a `MATCH` query against @@ -449,8 +532,8 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression appended with a `MATCH` query /// against the given pattern. - @warn_unused_result public func match(pattern: String) -> Expression { - return "MATCH".infix(self, pattern) + public func match(_ pattern: String) -> Expression { + Function.match.infix(self, pattern) } /// Builds a copy of the expression appended with a `REGEXP` query against @@ -460,8 +543,8 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression appended with a `REGEXP` query /// against the given pattern. - @warn_unused_result public func regexp(pattern: String) -> Expression { - return "REGEXP".infix(self, pattern) + public func regexp(_ pattern: String) -> Expression { + Function.regexp.infix(self, pattern) } /// Builds a copy of the expression appended with a `COLLATE` clause with @@ -475,8 +558,8 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression appended with a `COLLATE` clause /// with the given sequence. - @warn_unused_result public func collate(collation: Collation) -> Expression { - return "COLLATE".infix(self, collation) + public func collate(_ collation: Collation) -> Expression { + Function.collate.infix(self, collation) } /// Builds a copy of the expression wrapped with the `ltrim` function. @@ -490,11 +573,11 @@ extension ExpressionType where UnderlyingType == String? { /// - Parameter characters: A set of characters to trim. /// /// - Returns: A copy of the expression wrapped with the `ltrim` function. - @warn_unused_result public func ltrim(characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + public func ltrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.ltrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.ltrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `rtrim` function. @@ -508,11 +591,11 @@ extension ExpressionType where UnderlyingType == String? { /// - Parameter characters: A set of characters to trim. /// /// - Returns: A copy of the expression wrapped with the `rtrim` function. - @warn_unused_result public func rtrim(characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + public func rtrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.rtrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.rtrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `trim` function. @@ -526,11 +609,11 @@ extension ExpressionType where UnderlyingType == String? { /// - Parameter characters: A set of characters to trim. /// /// - Returns: A copy of the expression wrapped with the `trim` function. - @warn_unused_result public func trim(characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + public func trim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.trim.wrap(self) } - return wrap([self, String(characters)]) + return Function.trim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `replace` function. @@ -546,8 +629,8 @@ extension ExpressionType where UnderlyingType == String? { /// - replacement: The replacement string. /// /// - Returns: A copy of the expression wrapped with the `replace` function. - @warn_unused_result public func replace(pattern: String, with replacement: String) -> Expression { - return "replace".wrap([self, pattern, replacement]) + public func replace(_ pattern: String, with replacement: String) -> Expression { + Function.replace.wrap([self, pattern, replacement]) } /// Builds a copy of the expression wrapped with the `substr` function. @@ -565,11 +648,11 @@ extension ExpressionType where UnderlyingType == String? { /// - length: An optional substring length. /// /// - Returns: A copy of the expression wrapped with the `substr` function. - @warn_unused_result public func substring(location: Int, length: Int? = nil) -> Expression { - guard let length = length else { - return "substr".wrap([self, location]) + public func substring(_ location: Int, length: Int? = nil) -> Expression { + guard let length else { + return Function.substr.wrap([self, location]) } - return "substr".wrap([self, location, length]) + return Function.substr.wrap([self, location, length]) } /// Builds a copy of the expression wrapped with the `substr` function. @@ -582,12 +665,12 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `substr` function. public subscript(range: Range) -> Expression { - return substring(range.startIndex, length: range.endIndex - range.startIndex) + substring(range.lowerBound, length: range.upperBound - range.lowerBound) } } -extension CollectionType where Generator.Element : Value, Index.Distance == Int { +extension Collection where Iterator.Element: Value { /// Builds a copy of the expression prepended with an `IN` check against the /// collection. @@ -600,9 +683,9 @@ extension CollectionType where Generator.Element : Value, Index.Distance == Int /// /// - Returns: A copy of the expression prepended with an `IN` check against /// the collection. - @warn_unused_result public func contains(expression: Expression) -> Expression { - let templates = [String](count: count, repeatedValue: "?").joinWithSeparator(", ") - return "IN".infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + public func contains(_ expression: Expression) -> Expression { + let templates = [String](repeating: "?", count: count).joined(separator: ", ") + return Function.in.infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) } /// Builds a copy of the expression prepended with an `IN` check against the @@ -616,9 +699,38 @@ extension CollectionType where Generator.Element : Value, Index.Distance == Int /// /// - Returns: A copy of the expression prepended with an `IN` check against /// the collection. - @warn_unused_result public func contains(expression: Expression) -> Expression { - let templates = [String](count: count, repeatedValue: "?").joinWithSeparator(", ") - return "IN".infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + public func contains(_ expression: Expression) -> Expression { + let templates = [String](repeating: "?", count: count).joined(separator: ", ") + return Function.in.infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + } + +} + +extension String { + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = "some@thing.com" + /// let pattern = Expression("pattern") + /// email.like(pattern) + /// // 'some@thing.com' LIKE "pattern" + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: Expression, escape character: Character? = nil) -> Expression { + guard let character else { + return Function.like.infix(self, pattern) + } + let like: Expression = Function.like.infix(self, pattern, wrap: false) + return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) } } @@ -638,8 +750,8 @@ extension CollectionType where Generator.Element : Value, Index.Distance == Int /// /// - Returns: A copy of the given expressions wrapped with the `ifnull` /// function. -public func ??(optional: Expression, defaultValue: V) -> Expression { - return "ifnull".wrap([optional, defaultValue]) +public func ??(optional: Expression, defaultValue: V) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) } /// Builds a copy of the given expressions wrapped with the `ifnull` function. @@ -658,8 +770,8 @@ public func ??(optional: Expression, defaultValue: V) -> Expressi /// /// - Returns: A copy of the given expressions wrapped with the `ifnull` /// function. -public func ??(optional: Expression, defaultValue: Expression) -> Expression { - return "ifnull".wrap([optional, defaultValue]) +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) } /// Builds a copy of the given expressions wrapped with the `ifnull` function. @@ -678,6 +790,6 @@ public func ??(optional: Expression, defaultValue: Expression) /// /// - Returns: A copy of the given expressions wrapped with the `ifnull` /// function. -public func ??(optional: Expression, defaultValue: Expression) -> Expression { - return "ifnull".wrap([optional, defaultValue]) +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) } diff --git a/Sources/SQLite/Typed/CustomFunctions.swift b/Sources/SQLite/Typed/CustomFunctions.swift new file mode 100644 index 00000000..0eaa8cf3 --- /dev/null +++ b/Sources/SQLite/Typed/CustomFunctions.swift @@ -0,0 +1,162 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public extension Connection { + + /// Creates or redefines a custom SQL function. + /// + /// - Parameters: + /// + /// - function: The name of the function to create or redefine. + /// + /// - deterministic: Whether or not the function is deterministic (_i.e._ + /// the function always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - block: A block of code to run when the function is called. + /// The assigned types must be explicit. + /// + /// - Returns: A closure returning an SQL expression to call the function. + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z) throws + -> () -> Expression { + let fn = try createFunction(function, 0, deterministic) { _ in block() } + return { fn([]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z?) throws + -> () -> Expression { + let fn = try createFunction(function, 0, deterministic) { _ in block() } + return { fn([]) } + } + + // MARK: - + + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A) -> Z) throws + -> (Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0])) } + return { arg in fn([arg]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?) -> Z) throws + -> (Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value)) } + return { arg in fn([arg]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A) -> Z?) throws + -> (Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0])) } + return { arg in fn([arg]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?) -> Z?) throws + -> (Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value)) } + return { arg in fn([arg]) } + } + + // MARK: - + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A, B) -> Z) throws -> (Expression, Expression) + -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(value(args[0]), value(args[1])) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A?, B) -> Z) throws + -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(args[0].map(value), value(args[1])) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A, B?) -> Z) throws -> + (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(value(args[0]), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A, B) -> Z?) throws + -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(value(args[0]), value(args[1])) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A?, B?) -> Z) throws + -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(args[0].map(value), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A?, B) -> Z?) throws + -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(args[0].map(value), value(args[1])) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A, B?) -> Z?) throws + -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(value(args[0]), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + func createFunction(_ function: String, deterministic: Bool = false, + _ block: @escaping (A?, B?) -> Z?) throws + -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 2, deterministic) { args in block(args[0].map(value), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + // MARK: - + + fileprivate func createFunction(_ function: String, _ argumentCount: UInt, _ deterministic: Bool, + _ block: @escaping ([Binding?]) -> Z) throws + -> ([Expressible]) -> Expression { + createFunction(function, argumentCount: argumentCount, deterministic: deterministic) { arguments in + block(arguments).datatypeValue + } + return { arguments in + function.quote().wrap(", ".join(arguments)) + } + } + + fileprivate func createFunction(_ function: String, _ argumentCount: UInt, _ deterministic: Bool, + _ block: @escaping ([Binding?]) -> Z?) throws + -> ([Expressible]) -> Expression { + createFunction(function, argumentCount: argumentCount, deterministic: deterministic) { arguments in + block(arguments)?.datatypeValue + } + return { arguments in + function.quote().wrap(", ".join(arguments)) + } + } + +} diff --git a/Sources/SQLite/Typed/DateAndTimeFunctions.swift b/Sources/SQLite/Typed/DateAndTimeFunctions.swift new file mode 100644 index 00000000..b4382194 --- /dev/null +++ b/Sources/SQLite/Typed/DateAndTimeFunctions.swift @@ -0,0 +1,106 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// All five date and time functions take a time string as an argument. +/// The time string is followed by zero or more modifiers. +/// The strftime() function also takes a format string as its first argument. +/// +/// https://www.sqlite.org/lang_datefunc.html +public class DateFunctions { + /// The date() function returns the date in this format: YYYY-MM-DD. + public static func date(_ timestring: String, _ modifiers: String...) -> Expression { + timefunction("date", timestring: timestring, modifiers: modifiers) + } + + /// The time() function returns the time as HH:MM:SS. + public static func time(_ timestring: String, _ modifiers: String...) -> Expression { + timefunction("time", timestring: timestring, modifiers: modifiers) + } + + /// The datetime() function returns "YYYY-MM-DD HH:MM:SS". + public static func datetime(_ timestring: String, _ modifiers: String...) -> Expression { + timefunction("datetime", timestring: timestring, modifiers: modifiers) + } + + /// The julianday() function returns the Julian day - + /// the number of days since noon in Greenwich on November 24, 4714 B.C. + public static func julianday(_ timestring: String, _ modifiers: String...) -> Expression { + timefunction("julianday", timestring: timestring, modifiers: modifiers) + } + + /// The strftime() routine returns the date formatted according to the format string specified as the first argument. + public static func strftime(_ format: String, _ timestring: String, _ modifiers: String...) -> Expression { + if !modifiers.isEmpty { + let templates = [String](repeating: "?", count: modifiers.count).joined(separator: ", ") + return Expression("strftime(?, ?, \(templates))", [format, timestring] + modifiers) + } + return Expression("strftime(?, ?)", [format, timestring]) + } + + private static func timefunction(_ name: String, timestring: String, modifiers: [String]) -> Expression { + if !modifiers.isEmpty { + let templates = [String](repeating: "?", count: modifiers.count).joined(separator: ", ") + return Expression("\(name)(?, \(templates))", [timestring] + modifiers) + } + return Expression("\(name)(?)", [timestring]) + } +} + +extension Date { + public var date: Expression { + DateFunctions.date(dateFormatter.string(from: self)) + } + + public var time: Expression { + DateFunctions.time(dateFormatter.string(from: self)) + } + + public var datetime: Expression { + DateFunctions.datetime(dateFormatter.string(from: self)) + } + + public var julianday: Expression { + DateFunctions.julianday(dateFormatter.string(from: self)) + } +} + +extension Expression where UnderlyingType == Date { + public var date: Expression { + Expression("date(\(template))", bindings) + } + + public var time: Expression { + Expression("time(\(template))", bindings) + } + + public var datetime: Expression { + Expression("datetime(\(template))", bindings) + } + + public var julianday: Expression { + Expression("julianday(\(template))", bindings) + } +} diff --git a/SQLite/Typed/Expression.swift b/Sources/SQLite/Typed/Expression.swift similarity index 66% rename from SQLite/Typed/Expression.swift rename to Sources/SQLite/Typed/Expression.swift index 2e71b970..dcc44fe4 100644 --- a/SQLite/Typed/Expression.swift +++ b/Sources/SQLite/Typed/Expression.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public protocol ExpressionType : Expressible { // extensions cannot have inheritance clauses +public protocol ExpressionType: Expressible, CustomStringConvertible { // extensions cannot have inheritance clauses associatedtype UnderlyingType = Void @@ -43,14 +43,17 @@ extension ExpressionType { self.init(literal: identifier.quote()) } - public init(_ expression: U) { + public init(_ expression: U) { self.init(expression.template, expression.bindings) } + public var description: String { + asSQL() + } } /// An `Expression` represents a raw SQL fragment and any associated bindings. -public struct Expression : ExpressionType { +public struct Expression: ExpressionType { public typealias UnderlyingType = Datatype @@ -73,42 +76,38 @@ public protocol Expressible { extension Expressible { // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE - // FIXME: use @testable and make internal - public func asSQL() -> String { + func asSQL() -> String { let expressed = expression - var idx = 0 - return expressed.template.characters.reduce("") { template, character in - let transcoded: String - + return expressed.template.reduce(("", 0)) { memo, character in + let (template, index) = memo + if character == "?" { - transcoded = transcode(expressed.bindings[idx]) - idx += 1 + precondition(index < expressed.bindings.count, "not enough bindings for expression") + return (template + transcode(expressed.bindings[index]), index + 1) } else { - transcoded = String(character) + return (template + String(character), index) } - return template + transcoded - } + }.0 } - } extension ExpressionType { public var expression: Expression { - return Expression(template, bindings) + Expression(template, bindings) } public var asc: Expressible { - return " ".join([self, Expression(literal: "ASC")]) + " ".join([self, Expression(literal: "ASC")]) } public var desc: Expressible { - return " ".join([self, Expression(literal: "DESC")]) + " ".join([self, Expression(literal: "DESC")]) } } -extension ExpressionType where UnderlyingType : Value { +extension ExpressionType where UnderlyingType: Value { public init(value: UnderlyingType) { self.init("?", [value.datatypeValue]) @@ -116,10 +115,10 @@ extension ExpressionType where UnderlyingType : Value { } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value { public static var null: Self { - return self.init(value: nil) + self.init(value: nil) } public init(value: UnderlyingType.WrappedType?) { @@ -131,17 +130,17 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr extension Value { public var expression: Expression { - return Expression(value: self).expression + Expression(value: self).expression } } public let rowid = Expression("ROWID") -public func cast(expression: Expression) -> Expression { - return Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) +public func cast(_ expression: Expression) -> Expression { + Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) } -public func cast(expression: Expression) -> Expression { - return Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) +public func cast(_ expression: Expression) -> Expression { + Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) } diff --git a/Sources/SQLite/Typed/Operators.swift b/Sources/SQLite/Typed/Operators.swift new file mode 100644 index 00000000..1c611cbc --- /dev/null +++ b/Sources/SQLite/Typed/Operators.swift @@ -0,0 +1,674 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// TODO: use `@warn_unused_result` by the time operator functions support it + +private enum Operator: String { + case plus = "+" + case minus = "-" + case or = "OR" + case and = "AND" + case not = "NOT " + case mul = "*" + case div = "/" + case mod = "%" + case bitwiseLeft = "<<" + case bitwiseRight = ">>" + case bitwiseAnd = "&" + case bitwiseOr = "|" + case bitwiseXor = "~" + case eq = "=" + case neq = "!=" + case gt = ">" + case lt = "<" + case gte = ">=" + case lte = "<=" + case concatenate = "||" + + func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { + self.rawValue.infix(lhs, rhs, wrap: wrap) + } + + func wrap(_ expression: Expressible) -> Expression { + self.rawValue.wrap(expression) + } +} + +public func +(lhs: Expression, rhs: Expression) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} + +public func +(lhs: Expression, rhs: Expression) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: String) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: String) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} +public func +(lhs: String, rhs: Expression) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} +public func +(lhs: String, rhs: Expression) -> Expression { + Operator.concatenate.infix(lhs, rhs) +} + +// MARK: - + +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} +public func +(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.plus.infix(lhs, rhs) +} + +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} +public func -(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.infix(lhs, rhs) +} + +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} +public func *(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.mul.infix(lhs, rhs) +} + +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: V) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} +public func /(lhs: V, rhs: Expression) -> Expression where V.Datatype: Number { + Operator.div.infix(lhs, rhs) +} + +public prefix func -(rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.wrap(rhs) +} +public prefix func -(rhs: Expression) -> Expression where V.Datatype: Number { + Operator.minus.wrap(rhs) +} + +// MARK: - + +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} +public func %(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.mod.infix(lhs, rhs) +} + +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} +public func <<(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseLeft.infix(lhs, rhs) +} + +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} +public func >>(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseRight.infix(lhs, rhs) +} + +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} +public func &(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseAnd.infix(lhs, rhs) +} + +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} +public func |(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseOr.infix(lhs, rhs) +} + +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + (~(lhs & rhs)) & (lhs | rhs) +} + +public prefix func ~(rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseXor.wrap(rhs) +} +public prefix func ~(rhs: Expression) -> Expression where V.Datatype == Int64 { + Operator.bitwiseXor.wrap(rhs) +} + +// MARK: - + +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: V) -> Expression where V.Datatype: Equatable { + Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: V?) -> Expression where V.Datatype: Equatable { + guard let rhs else { return "IS".infix(lhs, Expression(value: nil)) } + return Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.eq.infix(lhs, rhs) +} +public func ==(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { + guard let lhs else { return "IS".infix(Expression(value: nil), rhs) } + return Operator.eq.infix(lhs, rhs) +} + +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: V) -> Expression where V.Datatype: Equatable { + "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: V?) -> Expression where V.Datatype: Equatable { + guard let rhs else { return "IS".infix(lhs, Expression(value: nil)) } + return "IS".infix(lhs, rhs) +} +public func ===(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS".infix(lhs, rhs) +} +public func ===(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { + guard let lhs else { return "IS".infix(Expression(value: nil), rhs) } + return "IS".infix(lhs, rhs) +} + +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: V) -> Expression where V.Datatype: Equatable { + Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: V?) -> Expression where V.Datatype: Equatable { + guard let rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } + return Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { + Operator.neq.infix(lhs, rhs) +} +public func !=(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { + guard let lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } + return Operator.neq.infix(lhs, rhs) +} + +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: V) -> Expression where V.Datatype: Equatable { + "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: V?) -> Expression where V.Datatype: Equatable { + guard let rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { + "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { + guard let lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } + return "IS NOT".infix(lhs, rhs) +} + +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} +public func >(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gt.infix(lhs, rhs) +} + +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} +public func >=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) +} + +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} +public func <(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lt.infix(lhs, rhs) +} + +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: V) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} +public func <=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.lte.infix(lhs, rhs) +} + +public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: Range, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) >= ? AND \(rhs.template) < ?", + rhs.bindings + [lhs.lowerBound.datatypeValue] + rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: Range, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) >= ? AND \(rhs.template) < ?", + rhs.bindings + [lhs.lowerBound.datatypeValue] + rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) <= ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) <= ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeUpTo, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) < ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeUpTo, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) < ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) >= ?", rhs.bindings + [lhs.lowerBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { + Expression("\(rhs.template) >= ?", rhs.bindings + [lhs.lowerBound.datatypeValue]) +} + +// MARK: - + +public func and(_ terms: Expression...) -> Expression { + "AND".infix(terms) +} +public func and(_ terms: [Expression]) -> Expression { + "AND".infix(terms) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Bool) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Bool) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Bool, rhs: Expression) -> Expression { + Operator.and.infix(lhs, rhs) +} +public func &&(lhs: Bool, rhs: Expression) -> Expression { + Operator.and.infix(lhs, rhs) +} + +public func or(_ terms: Expression...) -> Expression { + "OR".infix(terms) +} +public func or(_ terms: [Expression]) -> Expression { + "OR".infix(terms) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Bool) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Bool) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Bool, rhs: Expression) -> Expression { + Operator.or.infix(lhs, rhs) +} +public func ||(lhs: Bool, rhs: Expression) -> Expression { + Operator.or.infix(lhs, rhs) +} + +public prefix func !(rhs: Expression) -> Expression { + Operator.not.wrap(rhs) +} + +public prefix func !(rhs: Expression) -> Expression { + Operator.not.wrap(rhs) +} diff --git a/Sources/SQLite/Typed/Query+with.swift b/Sources/SQLite/Typed/Query+with.swift new file mode 100644 index 00000000..d06c8896 --- /dev/null +++ b/Sources/SQLite/Typed/Query+with.swift @@ -0,0 +1,117 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +import Foundation + +extension QueryType { + + /// Sets a `WITH` clause on the query. + /// + /// let users = Table("users") + /// let id = Expression("email") + /// let name = Expression("name") + /// + /// let userNames = Table("user_names") + /// userCategories.with(userNames, as: users.select(name)) + /// // WITH "user_names" as (SELECT "name" FROM "users") SELECT * FROM "user_names" + /// + /// - Parameters: + /// + /// - alias: A name to assign to the table expression. + /// + /// - recursive: Whether to evaluate the expression recursively. + /// + /// - hint: Provides a hint to the query planner for how the expression should be implemented. + /// + /// - subquery: A query that generates the rows for the table expression. + /// + /// - Returns: A query with the given `ORDER BY` clause applied. + public func with(_ alias: Table, columns: [Expressible]? = nil, recursive: Bool = false, + hint: MaterializationHint? = nil, as subquery: QueryType) -> Self { + var query = self + let clause = WithClauses.Clause(alias: alias, columns: columns, hint: hint, query: subquery) + query.clauses.with.recursive = query.clauses.with.recursive || recursive + query.clauses.with.clauses.append(clause) + return query + } + + /// self.clauses.with transformed to an Expressible + var withClause: Expressible? { + guard !clauses.with.clauses.isEmpty else { + return nil + } + + let innerClauses = ", ".join(clauses.with.clauses.map { (clause) in + let hintExpr: Expression? + if let hint = clause.hint { + hintExpr = Expression(literal: hint.rawValue) + } else { + hintExpr = nil + } + + let columnExpr: Expression? + if let columns = clause.columns { + columnExpr = "".wrap(", ".join(columns)) + } else { + columnExpr = nil + } + + let expressions: [Expressible?] = [ + clause.alias.tableName(), + columnExpr, + Expression(literal: "AS"), + hintExpr, + "".wrap(clause.query) as Expression + ] + + return " ".join(expressions.compactMap { $0 }) + }) + + return " ".join([ + Expression(literal: clauses.with.recursive ? "WITH RECURSIVE" : "WITH"), + innerClauses + ]) + } +} + +/// Materialization hints for `WITH` clause +public enum MaterializationHint: String { + + case materialized = "MATERIALIZED" + + case notMaterialized = "NOT MATERIALIZED" +} + +struct WithClauses { + struct Clause { + var alias: Table + var columns: [Expressible]? + var hint: MaterializationHint? + var query: QueryType + } + /// The `RECURSIVE` flag is applied to the entire `WITH` clause + var recursive: Bool = false + + /// Each `WITH` clause may have multiple subclauses + var clauses: [Clause] = [] +} diff --git a/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift similarity index 56% rename from SQLite/Typed/Query.swift rename to Sources/SQLite/Typed/Query.swift index 36b3b2e1..6162fcc7 100644 --- a/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -21,8 +21,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // +import Foundation -public protocol QueryType : Expressible { +public protocol QueryType: Expressible { var clauses: QueryClauses { get set } @@ -30,7 +31,7 @@ public protocol QueryType : Expressible { } -public protocol SchemaType : QueryType { +public protocol SchemaType: QueryType { static var identifier: String { get } @@ -50,8 +51,8 @@ extension SchemaType { /// - Parameter all: A list of expressions to select. /// /// - Returns: A query with the given `SELECT` clause applied. - public func select(column1: Expressible, _ more: Expressible...) -> Self { - return select(false, [column1] + more) + public func select(_ column1: Expressible, _ more: Expressible...) -> Self { + select(false, [column1] + more) } /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. @@ -66,7 +67,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT DISTINCT` clause applied. public func select(distinct column1: Expressible, _ more: Expressible...) -> Self { - return select(true, [column1] + more) + select(true, [column1] + more) } /// Builds a copy of the query with the `SELECT` clause applied. @@ -81,8 +82,8 @@ extension SchemaType { /// - Parameter all: A list of expressions to select. /// /// - Returns: A query with the given `SELECT` clause applied. - public func select(all: [Expressible]) -> Self { - return select(false, all) + public func select(_ all: [Expressible]) -> Self { + select(false, all) } /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. @@ -97,7 +98,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT DISTINCT` clause applied. public func select(distinct columns: [Expressible]) -> Self { - return select(true, columns) + select(true, columns) } /// Builds a copy of the query with the `SELECT *` clause applied. @@ -110,8 +111,8 @@ extension SchemaType { /// - Parameter star: A star literal. /// /// - Returns: A query with the given `SELECT *` clause applied. - public func select(star: Star) -> Self { - return select([star(nil, nil)]) + public func select(_ star: Star) -> Self { + select([star(nil, nil)]) } /// Builds a copy of the query with the `SELECT DISTINCT *` clause applied. @@ -125,7 +126,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT DISTINCT *` clause applied. public func select(distinct star: Star) -> Self { - return select(distinct: [star(nil, nil)]) + select(distinct: [star(nil, nil)]) } /// Builds a scalar copy of the query with the `SELECT` clause applied. @@ -139,11 +140,11 @@ extension SchemaType { /// - Parameter all: A list of expressions to select. /// /// - Returns: A query with the given `SELECT` clause applied. - public func select(column: Expression) -> ScalarQuery { - return select(false, [column]) + public func select(_ column: Expression) -> ScalarQuery { + select(false, [column]) } - public func select(column: Expression) -> ScalarQuery { - return select(false, [column]) + public func select(_ column: Expression) -> ScalarQuery { + select(false, [column]) } /// Builds a scalar copy of the query with the `SELECT DISTINCT` clause @@ -158,28 +159,51 @@ extension SchemaType { /// - Parameter column: A list of expressions to select. /// /// - Returns: A query with the given `SELECT DISTINCT` clause applied. - public func select(distinct column: Expression) -> ScalarQuery { - return select(true, [column]) + public func select(distinct column: Expression) -> ScalarQuery { + select(true, [column]) } - public func select(distinct column: Expression) -> ScalarQuery { - return select(true, [column]) + public func select(distinct column: Expression) -> ScalarQuery { + select(true, [column]) } public var count: ScalarQuery { - return select(Expression.count(*)) + select(Expression.count(*)) } } extension QueryType { - private func select(distinct: Bool, _ columns: [Expressible]) -> Q { + fileprivate func select(_ distinct: Bool, _ columns: [Expressible]) -> Q { var query = Q.init(clauses.from.name, database: clauses.from.database) query.clauses = clauses query.clauses.select = (distinct, columns) return query } + // MARK: UNION + + /// Adds a `UNION` clause to the query. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.filter(email == "alice@example.com").union(users.filter(email == "sally@example.com")) + /// // SELECT * FROM "users" WHERE email = 'alice@example.com' UNION SELECT * FROM "users" WHERE email = 'sally@example.com' + /// + /// - Parameters: + /// + /// - all: If false, duplicate rows are removed from the result. + /// + /// - table: A query representing the other table. + /// + /// - Returns: A query with the given `UNION` clause applied. + public func union(all: Bool = false, _ table: QueryType) -> Self { + var query = self + query.clauses.union.append((all, table)) + return query + } + // MARK: JOIN /// Adds a `JOIN` clause to the query. @@ -199,8 +223,8 @@ extension QueryType { /// - condition: A boolean expression describing the join condition. /// /// - Returns: A query with the given `JOIN` clause applied. - public func join(table: QueryType, on condition: Expression) -> Self { - return join(table, on: Expression(condition)) + public func join(_ table: QueryType, on condition: Expression) -> Self { + join(table, on: Expression(condition)) } /// Adds a `JOIN` clause to the query. @@ -220,8 +244,8 @@ extension QueryType { /// - condition: A boolean expression describing the join condition. /// /// - Returns: A query with the given `JOIN` clause applied. - public func join(table: QueryType, on condition: Expression) -> Self { - return join(.Inner, table, on: condition) + public func join(_ table: QueryType, on condition: Expression) -> Self { + join(.inner, table, on: condition) } /// Adds a `JOIN` clause to the query. @@ -243,8 +267,8 @@ extension QueryType { /// - condition: A boolean expression describing the join condition. /// /// - Returns: A query with the given `JOIN` clause applied. - public func join(type: JoinType, _ table: QueryType, on condition: Expression) -> Self { - return join(type, table, on: Expression(condition)) + public func join(_ type: JoinType, _ table: QueryType, on condition: Expression) -> Self { + join(type, table, on: Expression(condition)) } /// Adds a `JOIN` clause to the query. @@ -266,9 +290,10 @@ extension QueryType { /// - condition: A boolean expression describing the join condition. /// /// - Returns: A query with the given `JOIN` clause applied. - public func join(type: JoinType, _ table: QueryType, on condition: Expression) -> Self { + public func join(_ type: JoinType, _ table: QueryType, on condition: Expression) -> Self { var query = self - query.clauses.join.append((type: type, query: table, condition: table.clauses.filters.map { condition && $0 } ?? condition as Expressible)) + query.clauses.join.append((type: type, query: table, + condition: table.clauses.filters.map { condition && $0 } ?? condition as Expressible)) return query } @@ -285,8 +310,8 @@ extension QueryType { /// - Parameter condition: A boolean expression to filter on. /// /// - Returns: A query with the given `WHERE` clause applied. - public func filter(predicate: Expression) -> Self { - return filter(Expression(predicate)) + public func filter(_ predicate: Expression) -> Self { + filter(Expression(predicate)) } /// Adds a condition to the query’s `WHERE` clause. @@ -300,12 +325,24 @@ extension QueryType { /// - Parameter condition: A boolean expression to filter on. /// /// - Returns: A query with the given `WHERE` clause applied. - public func filter(predicate: Expression) -> Self { + public func filter(_ predicate: Expression) -> Self { var query = self query.clauses.filters = query.clauses.filters.map { $0 && predicate } ?? predicate return query } + /// Adds a condition to the query’s `WHERE` clause. + /// This is an alias for `filter(predicate)` + public func `where`(_ predicate: Expression) -> Self { + `where`(Expression(predicate)) + } + + /// Adds a condition to the query’s `WHERE` clause. + /// This is an alias for `filter(predicate)` + public func `where`(_ predicate: Expression) -> Self { + filter(predicate) + } + // MARK: GROUP BY /// Sets a `GROUP BY` clause on the query. @@ -313,8 +350,8 @@ extension QueryType { /// - Parameter by: A list of columns to group by. /// /// - Returns: A query with the given `GROUP BY` clause applied. - public func group(by: Expressible...) -> Self { - return group(by) + public func group(_ by: Expressible...) -> Self { + group(by) } /// Sets a `GROUP BY` clause on the query. @@ -322,8 +359,8 @@ extension QueryType { /// - Parameter by: A list of columns to group by. /// /// - Returns: A query with the given `GROUP BY` clause applied. - public func group(by: [Expressible]) -> Self { - return group(by, nil) + public func group(_ by: [Expressible]) -> Self { + group(by, nil) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -335,8 +372,8 @@ extension QueryType { /// - having: A condition determining which groups are returned. /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. - public func group(by: Expressible, having: Expression) -> Self { - return group([by], having: having) + public func group(_ by: Expressible, having: Expression) -> Self { + group([by], having: having) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -348,8 +385,8 @@ extension QueryType { /// - having: A condition determining which groups are returned. /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. - public func group(by: Expressible, having: Expression) -> Self { - return group([by], having: having) + public func group(_ by: Expressible, having: Expression) -> Self { + group([by], having: having) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -361,8 +398,8 @@ extension QueryType { /// - having: A condition determining which groups are returned. /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. - public func group(by: [Expressible], having: Expression) -> Self { - return group(by, Expression(having)) + public func group(_ by: [Expressible], having: Expression) -> Self { + group(by, Expression(having)) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -374,11 +411,11 @@ extension QueryType { /// - having: A condition determining which groups are returned. /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. - public func group(by: [Expressible], having: Expression) -> Self { - return group(by, having) + public func group(_ by: [Expressible], having: Expression) -> Self { + group(by, having) } - private func group(by: [Expressible], _ having: Expression?) -> Self { + fileprivate func group(_ by: [Expressible], _ having: Expression?) -> Self { var query = self query.clauses.group = (by, having) return query @@ -398,10 +435,10 @@ extension QueryType { /// - Parameter by: An ordered list of columns and directions to sort by. /// /// - Returns: A query with the given `ORDER BY` clause applied. - public func order(by: Expressible...) -> Self { - return order(by) + public func order(_ by: Expressible...) -> Self { + order(by) } - + /// Sets an `ORDER BY` clause on the query. /// /// let users = Table("users") @@ -414,7 +451,7 @@ extension QueryType { /// - Parameter by: An ordered list of columns and directions to sort by. /// /// - Returns: A query with the given `ORDER BY` clause applied. - public func order(by: [Expressible]) -> Self { + public func order(_ by: [Expressible]) -> Self { var query = self query.clauses.order = by return query @@ -433,8 +470,8 @@ extension QueryType { /// return unlimited rows). /// /// - Returns: A query with the given LIMIT clause applied. - public func limit(length: Int?) -> Self { - return limit(length, nil) + public func limit(_ length: Int?) -> Self { + limit(length, nil) } /// Sets LIMIT and OFFSET clauses on the query. @@ -451,12 +488,12 @@ extension QueryType { /// - offset: The number of rows to skip. /// /// - Returns: A query with the given LIMIT and OFFSET clauses applied. - public func limit(length: Int, offset: Int) -> Self { - return limit(length, offset) + public func limit(_ length: Int, offset: Int) -> Self { + limit(length, offset) } // prevents limit(nil, offset: 5) - private func limit(length: Int?, _ offset: Int?) -> Self { + fileprivate func limit(_ length: Int?, _ offset: Int?) -> Self { var query = self query.clauses.limit = length.map { ($0, offset) } return query @@ -468,22 +505,24 @@ extension QueryType { // MARK: - - private var selectClause: Expressible { - return " ".join([ - Expression(literal: clauses.select.distinct ? "SELECT DISTINCT" : "SELECT"), - ", ".join(clauses.select.columns), - Expression(literal: "FROM"), - tableName(alias: true) - ]) + fileprivate var selectClause: Expressible { + " ".join([ + Expression(literal: + clauses.select.distinct ? "SELECT DISTINCT" : "SELECT"), + ", ".join(clauses.select.columns), + Expression(literal: "FROM"), + tableName(alias: true) + ]) } - private var joinClause: Expressible? { + fileprivate var joinClause: Expressible? { guard !clauses.join.isEmpty else { return nil } - return " ".join(clauses.join.map { type, query, condition in - " ".join([ + return " ".join(clauses.join.map { arg in + let (type, query, condition) = arg + return " ".join([ Expression(literal: "\(type.rawValue) JOIN"), query.tableName(alias: true), Expression(literal: "ON"), @@ -492,7 +531,7 @@ extension QueryType { }) } - private var whereClause: Expressible? { + fileprivate var whereClause: Expressible? { guard let filters = clauses.filters else { return nil } @@ -503,7 +542,7 @@ extension QueryType { ]) } - private var groupByClause: Expressible? { + fileprivate var groupByClause: Expressible? { guard let group = clauses.group else { return nil } @@ -526,7 +565,7 @@ extension QueryType { ]) } - private var orderClause: Expressible? { + fileprivate var orderClause: Expressible? { guard !clauses.order.isEmpty else { return nil } @@ -537,7 +576,7 @@ extension QueryType { ]) } - private var limitOffsetClause: Expressible? { + fileprivate var limitOffsetClause: Expressible? { guard let limit = clauses.limit else { return nil } @@ -554,9 +593,22 @@ extension QueryType { ]) } + fileprivate var unionClause: Expressible? { + guard !clauses.union.isEmpty else { + return nil + } + + return " ".join(clauses.union.map { (all, query) in + " ".join([ + Expression(literal: all ? "UNION ALL" : "UNION"), + query + ]) + }) + } + // MARK: - - public func alias(aliasName: String) -> Self { + public func alias(_ aliasName: String) -> Self { var query = self query.clauses.from = (clauses.from.name, aliasName, clauses.from.database) return query @@ -566,23 +618,35 @@ extension QueryType { // // MARK: INSERT - public func insert(value: Setter, _ more: Setter...) -> Insert { - return insert([value] + more) + public func insert(_ value: Setter, _ more: Setter...) -> Insert { + insert([value] + more) } - public func insert(values: [Setter]) -> Insert { - return insert(nil, values) + public func insert(_ values: [Setter]) -> Insert { + insert(nil, values) } public func insert(or onConflict: OnConflict, _ values: Setter...) -> Insert { - return insert(or: onConflict, values) + insert(or: onConflict, values) } public func insert(or onConflict: OnConflict, _ values: [Setter]) -> Insert { - return insert(onConflict, values) + insert(onConflict, values) + } + + public func insertMany( _ values: [[Setter]]) -> Insert { + insertMany(nil, values) } - private func insert(or: OnConflict?, _ values: [Setter]) -> Insert { + public func insertMany(or onConflict: OnConflict, _ values: [[Setter]]) -> Insert { + insertMany(onConflict, values) + } + + public func insertMany(or onConflict: OnConflict, _ values: [Setter]...) -> Insert { + insertMany(onConflict, values) + } + + fileprivate func insert(_ or: OnConflict?, _ values: [Setter]) -> Insert { let insert = values.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in (insert.columns + [setter.column], insert.values + [setter.value]) } @@ -598,12 +662,37 @@ extension QueryType { whereClause ] - return Insert(" ".join(clauses.flatMap { $0 }).expression) + return Insert(" ".join(clauses.compactMap { $0 }).expression) + } + + fileprivate func insertMany(_ or: OnConflict?, _ values: [[Setter]]) -> Insert { + guard let firstInsert = values.first else { + // must be at least 1 object or else we don't know columns. Default to default inserts. + return insert() + } + let columns = firstInsert.map { $0.column } + let insertValues = values.map { rowValues in + rowValues.reduce([Expressible]()) { insert, setter in + insert + [setter.value] + } + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + or.map { Expression(literal: "OR \($0.rawValue)") }, + Expression(literal: "INTO"), + tableName(), + "".wrap(columns) as Expression, + Expression(literal: "VALUES"), + ", ".join(insertValues.map({ "".wrap($0) as Expression })), + whereClause + ] + return Insert(" ".join(clauses.compactMap { $0 }).expression) } /// Runs an `INSERT` statement against the query with `DEFAULT VALUES`. public func insert() -> Insert { - return Insert(" ".join([ + Insert(" ".join([ Expression(literal: "INSERT INTO"), tableName(), Expression(literal: "DEFAULT VALUES") @@ -616,30 +705,70 @@ extension QueryType { /// - Parameter query: A query to `SELECT` results from. /// /// - Returns: The number of updated rows and statement. - public func insert(query: QueryType) -> Update { - return Update(" ".join([ + public func insert(_ query: QueryType) -> Update { + Update(" ".join([ Expression(literal: "INSERT INTO"), tableName(), query.expression - ]).expression) + ]).expression) + } + + // MARK: UPSERT + + public func upsert(_ insertValues: Setter..., onConflictOf conflicting: Expressible) -> Insert { + upsert(insertValues, onConflictOf: conflicting) + } + + public func upsert(_ insertValues: [Setter], onConflictOf conflicting: Expressible) -> Insert { + let setValues = insertValues.filter { $0.column.asSQL() != conflicting.asSQL() } + .map { Setter(excluded: $0.column) } + return upsert(insertValues, onConflictOf: conflicting, set: setValues) + } + + public func upsert(_ insertValues: Setter..., onConflictOf conflicting: Expressible, set setValues: [Setter]) -> Insert { + upsert(insertValues, onConflictOf: conflicting, set: setValues) + } + + public func upsert(_ insertValues: [Setter], onConflictOf conflicting: Expressible, set setValues: [Setter]) -> Insert { + let insert = insertValues.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in + (insert.columns + [setter.column], insert.values + [setter.value]) + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + Expression(literal: "INTO"), + tableName(), + "".wrap(insert.columns) as Expression, + Expression(literal: "VALUES"), + "".wrap(insert.values) as Expression, + whereClause, + Expression(literal: "ON CONFLICT"), + "".wrap(conflicting) as Expression, + Expression(literal: "DO UPDATE SET"), + ", ".join(setValues.map { $0.expression }) + ] + + return Insert(" ".join(clauses.compactMap { $0 }).expression) } // MARK: UPDATE - public func update(values: Setter...) -> Update { - return update(values) + public func update(_ values: Setter...) -> Update { + update(values) } - public func update(values: [Setter]) -> Update { + public func update(_ values: [Setter]) -> Update { let clauses: [Expressible?] = [ Expression(literal: "UPDATE"), tableName(), Expression(literal: "SET"), ", ".join(values.map { " = ".join([$0.column, $0.value]) }), - whereClause + whereClause, + orderClause, + limitOffsetClause ] - return Update(" ".join(clauses.flatMap { $0 }).expression) + return Update(" ".join(clauses.compactMap { $0 }).expression) } // MARK: DELETE @@ -648,16 +777,18 @@ extension QueryType { let clauses: [Expressible?] = [ Expression(literal: "DELETE FROM"), tableName(), - whereClause + whereClause, + orderClause, + limitOffsetClause ] - return Delete(" ".join(clauses.flatMap { $0 }).expression) + return Delete(" ".join(clauses.compactMap { $0 }).expression) } // MARK: EXISTS public var exists: Select { - return Select(" ".join([ + Select(" ".join([ Expression(literal: "SELECT EXISTS"), "".wrap(expression) as Expression ]).expression) @@ -671,52 +802,16 @@ extension QueryType { /// /// - Returns: A column expression namespaced with the query’s table name or /// alias. - public func namespace(column: Expression) -> Expression { - return Expression(".".join([tableName(), column]).expression) - } - - // FIXME: rdar://problem/18673897 // subscript… - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) + public func namespace(_ column: Expression) -> Expression { + Expression(".".join([tableName(), column]).expression) } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) + public subscript(column: Expression) -> Expression { + namespace(column) } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) + public subscript(column: Expression) -> Expression { + namespace(column) } /// Prefixes a star with the query’s table name or alias. @@ -726,14 +821,14 @@ extension QueryType { /// - Returns: A `*` expression namespaced with the query’s table name or /// alias. public subscript(star: Star) -> Expression { - return namespace(star(nil, nil)) + namespace(star(nil, nil)) } // MARK: - // TODO: alias support func tableName(alias aliased: Bool = false) -> Expressible { - guard let alias = clauses.from.alias where aliased else { + guard let alias = clauses.from.alias, aliased else { return database(namespace: clauses.from.alias ?? clauses.from.name) } @@ -744,7 +839,7 @@ extension QueryType { ]) } - func tableName(qualified qualified: Bool) -> Expressible { + func tableName(qualified: Bool) -> Expressible { if qualified { return tableName() } @@ -763,15 +858,17 @@ extension QueryType { public var expression: Expression { let clauses: [Expressible?] = [ + withClause, selectClause, joinClause, whereClause, groupByClause, + unionClause, orderClause, limitOffsetClause ] - return " ".join(clauses.flatMap { $0 }).expression + return " ".join(clauses.compactMap { $0 }).expression } } @@ -780,7 +877,7 @@ extension QueryType { /// Queries a collection of chainable helper functions and expressions to build /// executable SQL statements. -public struct Table : SchemaType { +public struct Table: SchemaType { public static let identifier = "TABLE" @@ -792,7 +889,7 @@ public struct Table : SchemaType { } -public struct View : SchemaType { +public struct View: SchemaType { public static let identifier = "VIEW" @@ -804,7 +901,7 @@ public struct View : SchemaType { } -public struct VirtualTable : SchemaType { +public struct VirtualTable: SchemaType { public static let identifier = "VIRTUAL TABLE" @@ -818,7 +915,7 @@ public struct VirtualTable : SchemaType { // TODO: make `ScalarQuery` work in `QueryType.select()`, `.filter()`, etc. -public struct ScalarQuery : QueryType { +public struct ScalarQuery: QueryType { public var clauses: QueryClauses @@ -830,7 +927,7 @@ public struct ScalarQuery : QueryType { // TODO: decide: simplify the below with a boxed type instead -public struct Select : ExpressionType { +public struct Select: ExpressionType { public var template: String public var bindings: [Binding?] @@ -842,7 +939,7 @@ public struct Select : ExpressionType { } -public struct Insert : ExpressionType { +public struct Insert: ExpressionType { public var template: String public var bindings: [Binding?] @@ -854,7 +951,7 @@ public struct Insert : ExpressionType { } -public struct Update : ExpressionType { +public struct Update: ExpressionType { public var template: String public var bindings: [Binding?] @@ -866,7 +963,7 @@ public struct Update : ExpressionType { } -public struct Delete : ExpressionType { +public struct Delete: ExpressionType { public var template: String public var bindings: [Binding?] @@ -878,84 +975,144 @@ public struct Delete : ExpressionType { } +public struct RowIterator: FailableIterator { + public typealias Element = Row + let statement: Statement + let columnNames: [String: Int] + + public func failableNext() throws -> Row? { + try statement.failableNext().flatMap { Row(columnNames, $0) } + } + + public func map(_ transform: (Element) throws -> T) throws -> [T] { + var elements = [T]() + while let row = try failableNext() { + elements.append(try transform(row)) + } + return elements + } + + public func compactMap(_ transform: (Element) throws -> T?) throws -> [T] { + var elements = [T]() + while let row = try failableNext() { + guard let element = try transform(row) else { continue } + elements.append(element) + } + return elements + } +} + extension Connection { - public func prepare(query: QueryType) throws -> AnySequence { + public func prepare(_ query: QueryType) throws -> AnySequence { let expression = query.expression let statement = try prepare(expression.template, expression.bindings) - let columnNames: [String: Int] = try { - var (columnNames, idx) = ([String: Int](), 0) - column: for each in query.clauses.select.columns ?? [Expression(literal: "*")] { - var names = each.expression.template.characters.split { $0 == "." }.map(String.init) - let column = names.removeLast() - let namespace = names.joinWithSeparator(".") - - func expandGlob(namespace: Bool) -> (QueryType throws -> Void) { - return { (query: QueryType) throws -> (Void) in - var q = query.dynamicType.init(query.clauses.from.name, database: query.clauses.from.database) - q.clauses.select = query.clauses.select - let e = q.expression - var names = try self.prepare(e.template, e.bindings).columnNames.map { $0.quote() } - if namespace { names = names.map { "\(query.tableName().expression.template).\($0)" } } - for name in names { columnNames[name] = idx; idx += 1 } - } + let columnNames = try columnNamesForQuery(query) + + return AnySequence { + AnyIterator { statement.next().map { Row(columnNames, $0) } } + } + } + + public func prepareRowIterator(_ query: QueryType) throws -> RowIterator { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) + return RowIterator(statement: statement, columnNames: try columnNamesForQuery(query)) + } + + public func prepareRowIterator(_ statement: String, bindings: Binding?...) throws -> RowIterator { + try prepare(statement, bindings).prepareRowIterator() + } + + public func prepareRowIterator(_ statement: String, bindings: [Binding?]) throws -> RowIterator { + try prepare(statement, bindings).prepareRowIterator() + } + + private func columnNamesForQuery(_ query: QueryType) throws -> [String: Int] { + var (columnNames, idx) = ([String: Int](), 0) + column: for each in query.clauses.select.columns { + var names = each.expression.template.split { $0 == "." }.map(String.init) + let column = names.removeLast() + let namespace = names.joined(separator: ".") + + // Return a copy of the input "with" clause stripping all subclauses besides "select", "join", and "with". + func strip(_ with: WithClauses) -> WithClauses { + var stripped = WithClauses() + stripped.recursive = with.recursive + for subclause in with.clauses { + let query = subclause.query + var strippedQuery = type(of: query).init(query.clauses.from.name, database: query.clauses.from.database) + strippedQuery.clauses.select = query.clauses.select + strippedQuery.clauses.join = query.clauses.join + strippedQuery.clauses.with = strip(query.clauses.with) + + var strippedSubclause = WithClauses.Clause(alias: subclause.alias, query: strippedQuery) + strippedSubclause.columns = subclause.columns + stripped.clauses.append(strippedSubclause) } + return stripped + } - if column == "*" { - var select = query - select.clauses.select = (false, [Expression(literal: "*") as Expressible]) - let queries = [select] + query.clauses.join.map { $0.query } - if !namespace.isEmpty { - for q in queries { - if q.tableName().expression.template == namespace { - try expandGlob(true)(q) - continue column - } - } - fatalError("no such table: \(namespace)") - } - for q in queries { - try expandGlob(query.clauses.join.count > 0)(q) - } - continue + func expandGlob(_ namespace: Bool) -> (QueryType) throws -> Void { + { (queryType: QueryType) throws in + var query = type(of: queryType).init(queryType.clauses.from.name, database: queryType.clauses.from.database) + query.clauses.select = queryType.clauses.select + query.clauses.with = strip(queryType.clauses.with) + let expression = query.expression + var names = try self.prepare(expression.template, expression.bindings).columnNames.map { $0.quote() } + if namespace { names = names.map { "\(queryType.tableName().expression.template).\($0)" } } + for name in names { columnNames[name] = idx; idx += 1 } } + } - columnNames[each.expression.template] = idx - idx += 1 + if column == "*" { + var select = query + select.clauses.select = (false, [Expression(literal: "*") as Expressible]) + let queries = [select] + query.clauses.join.map { $0.query } + if !namespace.isEmpty { + for q in queries where q.tableName().expression.template == namespace { + try expandGlob(true)(q) + continue column + } + throw QueryError.noSuchTable(name: namespace) + } + for q in queries { + try expandGlob(query.clauses.join.count > 0)(q) + } + continue } - return columnNames - }() - return AnySequence { - AnyGenerator { statement.next().map { Row(columnNames, $0) } } + columnNames[each.expression.template] = idx + idx += 1 } + return columnNames } - public func scalar(query: ScalarQuery) throws -> V { + public func scalar(_ query: ScalarQuery) throws -> V { let expression = query.expression return value(try scalar(expression.template, expression.bindings)) } - public func scalar(query: ScalarQuery) throws -> V.ValueType? { + public func scalar(_ query: ScalarQuery) throws -> V.ValueType? { let expression = query.expression guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) + return try V.fromDatatypeValue(value) } - public func scalar(query: Select) throws -> V { + public func scalar(_ query: Select) throws -> V { let expression = query.expression return value(try scalar(expression.template, expression.bindings)) } - public func scalar(query: Select) throws -> V.ValueType? { + public func scalar(_ query: Select) throws -> V.ValueType? { let expression = query.expression guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) + return try V.fromDatatypeValue(value) } - public func pluck(query: QueryType) throws -> Row? { - return try prepare(query.limit(1, query.clauses.limit?.offset)).generate().next() + public func pluck(_ query: QueryType) throws -> Row? { + try prepareRowIterator(query.limit(1, query.clauses.limit?.offset)).failableNext() } /// Runs an `Insert` query. @@ -963,16 +1120,18 @@ extension Connection { /// - SeeAlso: `QueryType.insert(value:_:)` /// - SeeAlso: `QueryType.insert(values:)` /// - SeeAlso: `QueryType.insert(or:_:)` + /// - SeeAlso: `QueryType.insertMany(values:)` + /// - SeeAlso: `QueryType.insertMany(or:_:)` /// - SeeAlso: `QueryType.insert()` /// /// - Parameter query: An insert query. /// /// - Returns: The insert’s rowid. - public func run(query: Insert) throws -> Int64 { + @discardableResult public func run(_ query: Insert) throws -> Int64 { let expression = query.expression return try sync { try self.run(expression.template, expression.bindings) - return self.lastInsertRowid! + return lastInsertRowid } } @@ -984,11 +1143,11 @@ extension Connection { /// - Parameter query: An update query. /// /// - Returns: The number of updated rows. - public func run(query: Update) throws -> Int { + @discardableResult public func run(_ query: Update) throws -> Int { let expression = query.expression return try sync { try self.run(expression.template, expression.bindings) - return self.changes + return changes } } @@ -999,11 +1158,11 @@ extension Connection { /// - Parameter query: A delete query. /// /// - Returns: The number of deleted rows. - public func run(query: Delete) throws -> Int { + @discardableResult public func run(_ query: Delete) throws -> Int { let expression = query.expression return try sync { try self.run(expression.template, expression.bindings) - return self.changes + return changes } } @@ -1011,117 +1170,103 @@ extension Connection { public struct Row { - private let columnNames: [String: Int] + let columnNames: [String: Int] - private let values: [Binding?] + fileprivate let values: [Binding?] - private init(_ columnNames: [String: Int], _ values: [Binding?]) { + internal init(_ columnNames: [String: Int], _ values: [Binding?]) { self.columnNames = columnNames self.values = values } + func hasValue(for column: String) -> Bool { + guard let idx = columnNames[column.quote()] else { + return false + } + return values[idx] != nil + } + /// Returns a row’s value for the given column. /// /// - Parameter column: An expression representing a column selected in a Query. /// /// - Returns: The value for the given column. - public func get(column: Expression) -> V { - return get(Expression(column))! + public func get(_ column: Expression) throws -> V { + if let value = try get(Expression(column)) { + return value + } else { + throw QueryError.unexpectedNullValue(name: column.template) + } } - public func get(column: Expression) -> V? { - func valueAtIndex(idx: Int) -> V? { + + public func get(_ column: Expression) throws -> V? { + func valueAtIndex(_ idx: Int) throws -> V? { guard let value = values[idx] as? V.Datatype else { return nil } - return (V.fromDatatypeValue(value) as? V)! + return try V.fromDatatypeValue(value) as? V } guard let idx = columnNames[column.template] else { - let similar = Array(columnNames.keys).filter { $0.hasSuffix(".\(column.template)") } - - switch similar.count { - case 0: - fatalError("no such column '\(column.template)' in columns: \(columnNames.keys.sort())") - case 1: - return valueAtIndex(columnNames[similar[0]]!) - default: - fatalError("ambiguous column '\(column.template)' (please disambiguate: \(similar))") + func similar(_ name: String) -> Bool { + return name.hasSuffix(".\(column.template)") } - } - - return valueAtIndex(idx) - } - // FIXME: rdar://problem/18673897 // subscript… - - public subscript(column: Expression) -> Blob { - return get(column) - } - public subscript(column: Expression) -> Blob? { - return get(column) - } + guard let firstIndex = columnNames.firstIndex(where: { similar($0.key) }) else { + throw QueryError.noSuchColumn(name: column.template, columns: columnNames.keys.sorted()) + } - public subscript(column: Expression) -> Bool { - return get(column) - } - public subscript(column: Expression) -> Bool? { - return get(column) - } + let secondIndex = columnNames + .suffix(from: columnNames.index(after: firstIndex)) + .firstIndex(where: { similar($0.key) }) - public subscript(column: Expression) -> Double { - return get(column) - } - public subscript(column: Expression) -> Double? { - return get(column) - } + guard secondIndex == nil else { + throw QueryError.ambiguousColumn( + name: column.template, + similar: columnNames.keys.filter(similar).sorted() + ) + } + return try valueAtIndex(columnNames[firstIndex].value) + } - public subscript(column: Expression) -> Int { - return get(column) - } - public subscript(column: Expression) -> Int? { - return get(column) + return try valueAtIndex(idx) } - public subscript(column: Expression) -> Int64 { - return get(column) - } - public subscript(column: Expression) -> Int64? { - return get(column) + public subscript(column: Expression) -> T { + // swiftlint:disable:next force_try + try! get(column) } - public subscript(column: Expression) -> String { - return get(column) + public subscript(column: Expression) -> T? { + // swiftlint:disable:next force_try + try! get(column) } - public subscript(column: Expression) -> String? { - return get(column) - } - } /// Determines the join operator for a query’s `JOIN` clause. -public enum JoinType : String { +public enum JoinType: String { /// A `CROSS` join. - case Cross = "CROSS" + case cross = "CROSS" /// An `INNER` join. - case Inner = "INNER" + case inner = "INNER" /// A `LEFT OUTER` join. - case LeftOuter = "LEFT OUTER" + case leftOuter = "LEFT OUTER" } /// ON CONFLICT resolutions. public enum OnConflict: String { - case Replace = "REPLACE" + case replace = "REPLACE" - case Rollback = "ROLLBACK" + case rollback = "ROLLBACK" - case Abort = "ABORT" + case abort = "ABORT" - case Fail = "FAIL" + case fail = "FAIL" - case Ignore = "IGNORE" + case ignore = "IGNORE" } @@ -1143,8 +1288,12 @@ public struct QueryClauses { var limit: (length: Int, offset: Int?)? - private init(_ name: String, alias: String?, database: String?) { - self.from = (name, alias, database) + var union = [(all: Bool, table: QueryType)]() + + var with = WithClauses() + + fileprivate init(_ name: String, alias: String?, database: String?) { + from = (name, alias, database) } } diff --git a/Sources/SQLite/Typed/Schema.swift b/Sources/SQLite/Typed/Schema.swift new file mode 100644 index 00000000..919042ab --- /dev/null +++ b/Sources/SQLite/Typed/Schema.swift @@ -0,0 +1,601 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +extension SchemaType { + + // MARK: - DROP TABLE / VIEW / VIRTUAL TABLE + + public func drop(ifExists: Bool = false) -> String { + drop("TABLE", tableName(), ifExists) + } + +} + +extension Table { + + // MARK: - CREATE TABLE + + public func create(temporary: Bool = false, ifNotExists: Bool = false, withoutRowid: Bool = false, + block: (TableBuilder) -> Void) -> String { + let builder = TableBuilder() + + block(builder) + + let clauses: [Expressible?] = [ + create(Table.identifier, tableName(), temporary ? .temporary : nil, ifNotExists), + "".wrap(builder.definitions) as Expression, + withoutRowid ? Expression(literal: "WITHOUT ROWID") : nil + ] + + return " ".join(clauses.compactMap { $0 }).asSQL() + } + + public func create(_ query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create(Table.identifier, tableName(), temporary ? .temporary : nil, ifNotExists), + Expression(literal: "AS"), + query + ] + + return " ".join(clauses.compactMap { $0 }).asSQL() + } + + // MARK: - ALTER TABLE … ADD COLUMN + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V) -> String { + addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V) -> String { + addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V? = nil) -> String { + addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V? = nil) -> String { + addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression? = nil, + references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression, + references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression? = nil, + references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression, + references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V, + collate: Collation) -> String where V.Datatype == String { + addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V, + collate: Collation) -> String where V.Datatype == String { + addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) + } + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V? = nil, + collate: Collation) -> String where V.Datatype == String { + addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V? = nil, + collate: Collation) -> String where V.Datatype == String { + addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) + } + + fileprivate func addColumn(_ expression: Expressible) -> String { + " ".join([ + Expression(literal: "ALTER TABLE"), + tableName(), + Expression(literal: "ADD COLUMN"), + expression + ]).asSQL() + } + + // MARK: - ALTER TABLE … RENAME TO + + public func rename(_ to: Table) -> String { + rename(to: to) + } + + // MARK: - CREATE INDEX + + public func createIndex(_ columns: [Expressible], unique: Bool = false, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create("INDEX", indexName(columns), unique ? .unique : nil, ifNotExists), + Expression(literal: "ON"), + tableName(qualified: false), + "".wrap(columns) as Expression + ] + + return " ".join(clauses.compactMap { $0 }).asSQL() + } + + public func createIndex(_ columns: Expressible..., unique: Bool = false, ifNotExists: Bool = false) -> String { + return createIndex(Array(columns), unique: unique, ifNotExists: ifNotExists) + } + + // MARK: - DROP INDEX + + public func dropIndex(_ columns: [Expressible], ifExists: Bool = false) -> String { + drop("INDEX", indexName(columns), ifExists) + } + + public func dropIndex(_ columns: Expressible..., ifExists: Bool = false) -> String { + dropIndex(Array(columns), ifExists: ifExists) + } + + fileprivate func indexName(_ columns: [Expressible]) -> Expressible { + let string = (["index", clauses.from.name, "on"] + columns.map { $0.expression.template }).joined(separator: " ").lowercased() + + let index = string.reduce("") { underscored, character in + guard character != "\"" else { + return underscored + } + guard "a"..."z" ~= character || "0"..."9" ~= character else { + return underscored + "_" + } + return underscored + String(character) + } + + return database(namespace: index) + } + +} + +extension View { + + // MARK: - CREATE VIEW + + public func create(_ query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create(View.identifier, tableName(), temporary ? .temporary : nil, ifNotExists), + Expression(literal: "AS"), + query + ] + + return " ".join(clauses.compactMap { $0 }).asSQL() + } + + // MARK: - DROP VIEW + + public func drop(ifExists: Bool = false) -> String { + drop("VIEW", tableName(), ifExists) + } + +} + +extension VirtualTable { + + // MARK: - CREATE VIRTUAL TABLE + + public func create(_ using: Module, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create(VirtualTable.identifier, tableName(), nil, ifNotExists), + Expression(literal: "USING"), + using + ] + + return " ".join(clauses.compactMap { $0 }).asSQL() + } + + // MARK: - ALTER TABLE … RENAME TO + + public func rename(_ to: VirtualTable) -> String { + rename(to: to) + } + +} + +public final class TableBuilder { + + fileprivate var definitions = [Expressible]() + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: V) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: V) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: Expression) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: V) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: Expression) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: V) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression? = nil, + defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression, + defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, primaryKey: PrimaryKey, + check: Expression? = nil) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey, false, false, check, nil, nil, nil) + } + + public func column(_ name: Expression, primaryKey: PrimaryKey, + check: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey, false, false, check, nil, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression? = nil, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression? = nil, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, true, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, true, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: Expression, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, + defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: Expression, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, + defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) + } + + // swiftlint:disable:next function_parameter_count + fileprivate func column(_ name: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, + _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, + _ references: (QueryType, Expressible)?, _ collate: Collation?) { + definitions.append(definition(name, datatype, primaryKey, null, unique, check, defaultValue, references, collate)) + } + + // MARK: - + + public func primaryKey(_ column: Expression) { + primaryKey([column]) + } + + public func primaryKey(_ compositeA: Expression, + _ expr: Expression) { + primaryKey([compositeA, expr]) + } + + public func primaryKey(_ compositeA: Expression, + _ expr1: Expression, + _ expr2: Expression) { + primaryKey([compositeA, expr1, expr2]) + } + + public func primaryKey(_ compositeA: Expression, + _ expr1: Expression, + _ expr2: Expression, + _ expr3: Expression) { + primaryKey([compositeA, expr1, expr2, expr3]) + } + + fileprivate func primaryKey(_ composite: [Expressible]) { + definitions.append("PRIMARY KEY".prefix(composite)) + } + + public func unique(_ columns: Expressible...) { + unique(columns) + } + + public func unique(_ columns: [Expressible]) { + definitions.append("UNIQUE".prefix(columns)) + } + + public func check(_ condition: Expression) { + check(Expression(condition)) + } + + public func check(_ condition: Expression) { + definitions.append("CHECK".prefix(condition)) + } + + public enum Dependency: String { + + case noAction = "NO ACTION" + + case restrict = "RESTRICT" + + case setNull = "SET NULL" + + case setDefault = "SET DEFAULT" + + case cascade = "CASCADE" + + } + + public func foreignKey(_ column: Expression, references table: QueryType, _ other: Expression, + update: Dependency? = nil, delete: Dependency? = nil) { + foreignKey(column, (table, other), update, delete) + } + + public func foreignKey(_ column: Expression, references table: QueryType, _ other: Expression, + update: Dependency? = nil, delete: Dependency? = nil) { + foreignKey(column, (table, other), update, delete) + } + + public func foreignKey(_ composite: (Expression, Expression), + references table: QueryType, _ other: (Expression, Expression), + update: Dependency? = nil, delete: Dependency? = nil) { + let composite = ", ".join([composite.0, composite.1]) + let references = (table, ", ".join([other.0, other.1])) + + foreignKey(composite, references, update, delete) + } + + public func foreignKey(_ composite: (Expression, Expression, Expression), + references table: QueryType, + _ other: (Expression, Expression, Expression), + update: Dependency? = nil, delete: Dependency? = nil) { + let composite = ", ".join([composite.0, composite.1, composite.2]) + let references = (table, ", ".join([other.0, other.1, other.2])) + + foreignKey(composite, references, update, delete) + } + + fileprivate func foreignKey(_ column: Expressible, _ references: (QueryType, Expressible), + _ update: Dependency?, _ delete: Dependency?) { + let clauses: [Expressible?] = [ + "FOREIGN KEY".prefix(column), + reference(references), + update.map { Expression(literal: "ON UPDATE \($0.rawValue)") }, + delete.map { Expression(literal: "ON DELETE \($0.rawValue)") } + ] + + definitions.append(" ".join(clauses.compactMap { $0 })) + } + +} + +public enum PrimaryKey { + + case `default` + + case autoincrement + +} + +public struct Module { + + fileprivate let name: String + + fileprivate let arguments: [Expressible] + + public init(_ name: String, _ arguments: [Expressible]) { + self.init(name: name.quote(), arguments: arguments) + } + + init(name: String, arguments: [Expressible]) { + self.name = name + self.arguments = arguments + } + +} + +extension Module: Expressible { + + public var expression: Expression { + name.wrap(arguments) + } + +} + +// MARK: - Private + +private extension QueryType { + + func create(_ identifier: String, _ name: Expressible, _ modifier: Modifier?, _ ifNotExists: Bool) -> Expressible { + let clauses: [Expressible?] = [ + Expression(literal: "CREATE"), + modifier.map { Expression(literal: $0.rawValue) }, + Expression(literal: identifier), + ifNotExists ? Expression(literal: "IF NOT EXISTS") : nil, + name + ] + + return " ".join(clauses.compactMap { $0 }) + } + + func rename(to: Self) -> String { + " ".join([ + Expression(literal: "ALTER TABLE"), + tableName(), + Expression(literal: "RENAME TO"), + Expression(to.clauses.from.name) + ]).asSQL() + } + + func drop(_ identifier: String, _ name: Expressible, _ ifExists: Bool) -> String { + let clauses: [Expressible?] = [ + Expression(literal: "DROP \(identifier)"), + ifExists ? Expression(literal: "IF EXISTS") : nil, + name + ] + + return " ".join(clauses.compactMap { $0 }).asSQL() + } + +} + +// swiftlint:disable:next function_parameter_count +private func definition(_ column: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, + _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, + _ references: (QueryType, Expressible)?, _ collate: Collation?) -> Expressible { + let clauses: [Expressible?] = [ + column, + Expression(literal: datatype), + primaryKey.map { Expression(literal: $0 == .autoincrement ? "PRIMARY KEY AUTOINCREMENT" : "PRIMARY KEY") }, + null ? nil : Expression(literal: "NOT NULL"), + unique ? Expression(literal: "UNIQUE") : nil, + check.map { " ".join([Expression(literal: "CHECK"), $0]) }, + defaultValue.map { "DEFAULT".prefix($0) }, + references.map(reference), + collate.map { " ".join([Expression(literal: "COLLATE"), $0]) } + ] + + return " ".join(clauses.compactMap { $0 }) +} + +private func reference(_ primary: (QueryType, Expressible)) -> Expressible { + " ".join([ + Expression(literal: "REFERENCES"), + primary.0.tableName(qualified: false), + "".wrap(primary.1) as Expression + ]) +} + +private enum Modifier: String { + + case unique = "UNIQUE" + + case temporary = "TEMPORARY" + +} diff --git a/Sources/SQLite/Typed/Setter.swift b/Sources/SQLite/Typed/Setter.swift new file mode 100644 index 00000000..8dc8a0e0 --- /dev/null +++ b/Sources/SQLite/Typed/Setter.swift @@ -0,0 +1,288 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +precedencegroup ColumnAssignment { + associativity: left + assignment: true + lowerThan: AssignmentPrecedence +} + +infix operator <- : ColumnAssignment + +public struct Setter { + + let column: Expressible + let value: Expressible + + fileprivate init(column: Expression, value: Expression) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: V) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: Expression) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: Expression) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: V?) { + self.column = column + self.value = Expression(value: value) + } + + init(excluded column: Expressible) { + let excluded = Expression("excluded") + self.column = column + value = ".".join([excluded, column.expression]) + } +} + +extension Setter: Expressible { + + public var expression: Expression { + "=".infix(column, value, wrap: false) + } + +} + +extension Setter: CustomStringConvertible { + public var description: String { + asSQL() + } +} + +public func <-(column: Expression, value: Expression) -> Setter { + Setter(column: column, value: value) +} +public func <-(column: Expression, value: V) -> Setter { + Setter(column: column, value: value) +} +public func <-(column: Expression, value: Expression) -> Setter { + Setter(column: column, value: value) +} +public func <-(column: Expression, value: Expression) -> Setter { + Setter(column: column, value: value) +} +public func <-(column: Expression, value: V?) -> Setter { + Setter(column: column, value: value) +} + +public func +=(column: Expression, value: Expression) -> Setter { + column <- column + value +} +public func +=(column: Expression, value: String) -> Setter { + column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter { + column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter { + column <- column + value +} +public func +=(column: Expression, value: String) -> Setter { + column <- column + value +} + +public func +=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column + value +} +public func +=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column + value +} +public func +=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column + value +} + +public func -=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column - value +} +public func -=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column - value +} +public func -=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column - value +} +public func -=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column - value +} +public func -=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column - value +} + +public func *=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column * value +} +public func *=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column * value +} +public func *=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column * value +} +public func *=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column * value +} +public func *=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column * value +} + +public func /=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column / value +} +public func /=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column / value +} +public func /=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column / value +} +public func /=(column: Expression, value: Expression) -> Setter where V.Datatype: Number { + column <- column / value +} +public func /=(column: Expression, value: V) -> Setter where V.Datatype: Number { + column <- column / value +} + +public func %=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column % value +} +public func %=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column % value +} +public func %=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column % value +} +public func %=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column % value +} +public func %=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column % value +} + +public func <<=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column << value +} +public func <<=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column << value +} +public func <<=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column << value +} +public func <<=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column << value +} +public func <<=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column << value +} + +public func >>=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column >> value +} +public func >>=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column >> value +} +public func >>=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column >> value +} +public func >>=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column >> value +} +public func >>=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column >> value +} + +public func &=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column & value +} +public func &=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column & value +} +public func &=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column & value +} +public func &=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column & value +} +public func &=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column & value +} + +public func |=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column | value +} +public func |=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column | value +} +public func |=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column | value +} +public func |=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column | value +} +public func |=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column | value +} + +public func ^=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column ^ value +} +public func ^=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column ^ value +} +public func ^=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column ^ value +} +public func ^=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + column <- column ^ value +} +public func ^=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + column <- column ^ value +} + +public postfix func ++(column: Expression) -> Setter where V.Datatype == Int64 { + Expression(column) += 1 +} +public postfix func ++(column: Expression) -> Setter where V.Datatype == Int64 { + Expression(column) += 1 +} + +public postfix func --(column: Expression) -> Setter where V.Datatype == Int64 { + Expression(column) -= 1 +} +public postfix func --(column: Expression) -> Setter where V.Datatype == Int64 { + Expression(column) -= 1 +} diff --git a/Sources/SQLite/Typed/WindowFunctions.swift b/Sources/SQLite/Typed/WindowFunctions.swift new file mode 100644 index 00000000..e71e1ada --- /dev/null +++ b/Sources/SQLite/Typed/WindowFunctions.swift @@ -0,0 +1,145 @@ +import Foundation + +// see https://www.sqlite.org/windowfunctions.html#builtins +private enum WindowFunction: String { + // swiftlint:disable identifier_name + case ntile + case row_number + case rank + case dense_rank + case percent_rank + case cume_dist + case lag + case lead + case first_value + case last_value + case nth_value + // swiftlint:enable identifier_name + + func wrap(_ value: Int? = nil) -> Expression { + if let value { + return self.rawValue.wrap(Expression(value: value)) + } + return Expression(literal: "\(rawValue)()") + } + + func over(value: Int? = nil, _ orderBy: Expressible) -> Expression { + return Expression(" ".join([ + self.wrap(value), + Expression("OVER (ORDER BY \(orderBy.expression.template))", orderBy.expression.bindings) + ]).expression) + } + + func over(valueExpr: Expressible, _ orderBy: Expressible) -> Expression { + return Expression(" ".join([ + self.rawValue.wrap(valueExpr), + Expression("OVER (ORDER BY \(orderBy.expression.template))", orderBy.expression.bindings) + ]).expression) + } +} + +extension ExpressionType where UnderlyingType: Value { + /// Builds a copy of the expression with `lag(self, offset, default) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `lag(self, offset, default) OVER (ORDER BY {orderBy})` window function + public func lag(offset: Int = 0, default: Expressible? = nil, _ orderBy: Expressible) -> Expression { + if let defaultExpression = `default` { + return Expression( + "lag(\(template), \(offset), \(defaultExpression.asSQL())) OVER (ORDER BY \(orderBy.expression.template))", + bindings + orderBy.expression.bindings + ) + + } + return Expression("lag(\(template), \(offset)) OVER (ORDER BY \(orderBy.expression.template))", bindings + orderBy.expression.bindings) + } + + /// Builds a copy of the expression with `lead(self, offset, default) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `lead(self, offset, default) OVER (ORDER BY {orderBy})` window function + public func lead(offset: Int = 0, default: Expressible? = nil, _ orderBy: Expressible) -> Expression { + if let defaultExpression = `default` { + return Expression( + "lead(\(template), \(offset), \(defaultExpression.asSQL())) OVER (ORDER BY \(orderBy.expression.template))", + bindings + orderBy.expression.bindings) + + } + return Expression("lead(\(template), \(offset)) OVER (ORDER BY \(orderBy.expression.template))", bindings + orderBy.expression.bindings) + } + + /// Builds a copy of the expression with `first_value(self) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `first_value(self) OVER (ORDER BY {orderBy})` window function + public func firstValue(_ orderBy: Expressible) -> Expression { + WindowFunction.first_value.over(valueExpr: self, orderBy) + } + + /// Builds a copy of the expression with `last_value(self) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `last_value(self) OVER (ORDER BY {orderBy})` window function + public func lastValue(_ orderBy: Expressible) -> Expression { + WindowFunction.last_value.over(valueExpr: self, orderBy) + } + + /// Builds a copy of the expression with `nth_value(self) OVER (ORDER BY {orderBy})` window function + /// + /// - Parameter index: Row N of the window frame to return + /// - Parameter orderBy: Expression to evaluate window order + /// - Returns: An expression returning `nth_value(self) OVER (ORDER BY {orderBy})` window function + public func value(_ index: Int, _ orderBy: Expressible) -> Expression { + Expression("nth_value(\(template), \(index)) OVER (ORDER BY \(orderBy.expression.template))", bindings + orderBy.expression.bindings) + } +} + +/// Builds an expression representing `ntile(size) OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `ntile(size) OVER (ORDER BY {orderBy})` +public func ntile(_ size: Int, _ orderBy: Expressible) -> Expression { +// Expression.ntile(size, orderBy) + + WindowFunction.ntile.over(value: size, orderBy) +} + +/// Builds an expression representing `row_count() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `row_count() OVER (ORDER BY {orderBy})` +public func rowNumber(_ orderBy: Expressible) -> Expression { + WindowFunction.row_number.over(orderBy) +} + +/// Builds an expression representing `rank() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `rank() OVER (ORDER BY {orderBy})` +public func rank(_ orderBy: Expressible) -> Expression { + WindowFunction.rank.over(orderBy) +} + +/// Builds an expression representing `dense_rank() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `dense_rank() OVER ('over')` +public func denseRank(_ orderBy: Expressible) -> Expression { + WindowFunction.dense_rank.over(orderBy) +} + +/// Builds an expression representing `percent_rank() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `percent_rank() OVER (ORDER BY {orderBy})` +public func percentRank(_ orderBy: Expressible) -> Expression { + WindowFunction.percent_rank.over(orderBy) +} + +/// Builds an expression representing `cume_dist() OVER (ORDER BY {orderBy})` +/// +/// - Parameter orderBy: Expression to evaluate window order +/// - Returns: An expression returning `cume_dist() OVER (ORDER BY {orderBy})` +public func cumeDist(_ orderBy: Expressible) -> Expression { + WindowFunction.cume_dist.over(orderBy) +} diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml new file mode 100644 index 00000000..81d6c314 --- /dev/null +++ b/Tests/.swiftlint.yml @@ -0,0 +1,20 @@ +parent_config: ../.swiftlint.yml + +disabled_rules: + - force_cast + - force_try + - identifier_name + +type_body_length: + warning: 1000 + error: 1000 + +function_body_length: + warning: 200 + error: 200 + +file_length: + warning: 1000 + error: 1000 + + diff --git a/Tests/Carthage/.gitignore b/Tests/Carthage/.gitignore new file mode 100644 index 00000000..2d43454a --- /dev/null +++ b/Tests/Carthage/.gitignore @@ -0,0 +1,3 @@ +Carthage/ +Cartfile +Cartfile.resolved diff --git a/Tests/Carthage/Makefile b/Tests/Carthage/Makefile new file mode 100644 index 00000000..fe708ab0 --- /dev/null +++ b/Tests/Carthage/Makefile @@ -0,0 +1,17 @@ +CARTHAGE := $(shell which carthage) +CARTHAGE_PLATFORM := iOS +CARTHAGE_CONFIGURATION := Release +CARTHAGE_DIR := Carthage +CARTHAGE_ARGS := --no-use-binaries --use-xcframeworks +# CARTHAGE_TOOLCHAIN := com.apple.dt.toolchain.Swift_3_0 +CARTHAGE_TOOLCHAIN := com.apple.dt.toolchain.XcodeDefault +CARTHAGE_CMDLINE := --configuration $(CARTHAGE_CONFIGURATION) --platform $(CARTHAGE_PLATFORM) --toolchain $(CARTHAGE_TOOLCHAIN) $(CARTHAGE_ARGS) + +test: $(CARTHAGE) Cartfile + $< bootstrap $(CARTHAGE_CMDLINE) + +Cartfile: + echo 'git "$(GITHUB_WORKSPACE)" "HEAD"' > $@ + +clean: + @rm -f Cartfile Cartfile.resolved diff --git a/Tests/SPM/.gitignore b/Tests/SPM/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Tests/SPM/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Tests/SPM/Package.swift b/Tests/SPM/Package.swift new file mode 100644 index 00000000..a5a4afc4 --- /dev/null +++ b/Tests/SPM/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "test", + platforms: [ + .iOS(.v11), + .macOS(.v10_13), + .watchOS(.v4), + .tvOS(.v11) + ], + dependencies: [ + // for testing from same repository + .package(path: "../..") + // normally this would be: + // .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ], + targets: [ + .executableTarget(name: "test", dependencies: [.product(name: "SQLite", package: "SQLite.swift")]) + ] +) diff --git a/Tests/SPM/Sources/test/main.swift b/Tests/SPM/Sources/test/main.swift new file mode 100644 index 00000000..939b3391 --- /dev/null +++ b/Tests/SPM/Sources/test/main.swift @@ -0,0 +1,10 @@ +import SQLite + +let table = Table("test") +let name = Expression("name") + +let db = try Connection("db.sqlite", readonly: true) + +for row in try db.prepare(table) { + print(row[name]) +} diff --git a/Tests/SPM/db.sqlite b/Tests/SPM/db.sqlite new file mode 100644 index 00000000..f7b2e84c Binary files /dev/null and b/Tests/SPM/db.sqlite differ diff --git a/Tests/SQLite visionOS.xctestplan b/Tests/SQLite visionOS.xctestplan new file mode 100644 index 00000000..9b4e90ed --- /dev/null +++ b/Tests/SQLite visionOS.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "72B91FD4-441C-4C06-9E92-CAEDCB7325AB", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:SQLite.xcodeproj", + "identifier" : "DEB306E72B61CF9500F9D46B", + "name" : "SQLiteTests visionOS" + } + } + ], + "version" : 1 +} diff --git a/Tests/SQLiteTests/Core/BlobTests.swift b/Tests/SQLiteTests/Core/BlobTests.swift new file mode 100644 index 00000000..f2e9435e --- /dev/null +++ b/Tests/SQLiteTests/Core/BlobTests.swift @@ -0,0 +1,47 @@ +import XCTest +import SQLite + +class BlobTests: XCTestCase { + + func test_toHex() { + let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) + XCTAssertEqual(blob.toHex(), "000a141e28323c46505a6496faff") + } + + func test_toHex_empty() { + let blob = Blob(bytes: []) + XCTAssertEqual(blob.toHex(), "") + } + + func test_description() { + let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) + XCTAssertEqual(blob.description, "x'000a141e28323c46505a6496faff'") + } + + func test_description_empty() { + let blob = Blob(bytes: []) + XCTAssertEqual(blob.description, "x''") + } + + func test_init_array() { + let blob = Blob(bytes: [42, 43, 44]) + XCTAssertEqual(blob.bytes, [42, 43, 44]) + } + + func test_init_unsafeRawPointer() { + let pointer = UnsafeMutablePointer.allocate(capacity: 3) + pointer.initialize(repeating: 42, count: 3) + let blob = Blob(bytes: pointer, length: 3) + XCTAssertEqual(blob.bytes, [42, 42, 42]) + } + + func test_equality() { + let blob1 = Blob(bytes: [42, 42, 42]) + let blob2 = Blob(bytes: [42, 42, 42]) + let blob3 = Blob(bytes: [42, 42, 43]) + + XCTAssertEqual(Blob(bytes: []), Blob(bytes: [])) + XCTAssertEqual(blob1, blob2) + XCTAssertNotEqual(blob1, blob3) + } +} diff --git a/Tests/SQLiteTests/Core/Connection+AttachTests.swift b/Tests/SQLiteTests/Core/Connection+AttachTests.swift new file mode 100644 index 00000000..0e185da5 --- /dev/null +++ b/Tests/SQLiteTests/Core/Connection+AttachTests.swift @@ -0,0 +1,62 @@ +import XCTest +import Foundation +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +class ConnectionAttachTests: SQLiteTestCase { + func test_attach_detach_memory_database() throws { + let schemaName = "test" + + try db.attach(.inMemory, as: schemaName) + + let table = Table("attached_users", database: schemaName) + let name = SQLite.Expression("string") + + // create a table, insert some data + try db.run(table.create { builder in + builder.column(name) + }) + _ = try db.run(table.insert(name <- "test")) + + // query data + let rows = try db.prepare(table.select(name)).map { $0[name] } + XCTAssertEqual(["test"], rows) + + try db.detach(schemaName) + } + + func test_attach_detach_file_database() throws { + let schemaName = "test" + let testDb = fixture("test", withExtension: "sqlite") + + try db.attach(.uri(testDb, parameters: [.mode(.readOnly)]), as: schemaName) + + let table = Table("tests", database: schemaName) + let email = SQLite.Expression("email") + + let rows = try db.prepare(table.select(email)).map { $0[email] } + XCTAssertEqual(["foo@bar.com"], rows) + + try db.detach(schemaName) + } + + func test_detach_invalid_schema_name_errors_with_no_such_database() throws { + XCTAssertThrowsError(try db.detach("no-exist")) { error in + if case let Result.error(message, code, _) = error { + XCTAssertEqual(code, SQLITE_ERROR) + XCTAssertEqual("no such database: no-exist", message) + } else { + XCTFail("unexpected error: \(error)") + } + } + } +} diff --git a/Tests/SQLiteTests/Core/Connection+PragmaTests.swift b/Tests/SQLiteTests/Core/Connection+PragmaTests.swift new file mode 100644 index 00000000..d1d4ab04 --- /dev/null +++ b/Tests/SQLiteTests/Core/Connection+PragmaTests.swift @@ -0,0 +1,42 @@ +import XCTest +import Foundation +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +class ConnectionPragmaTests: SQLiteTestCase { + func test_userVersion() { + db.userVersion = 2 + XCTAssertEqual(2, db.userVersion!) + } + + func test_sqlite_version() { + XCTAssertTrue(db.sqliteVersion >= .init(major: 3, minor: 0)) + } + + func test_foreignKeys_defaults_to_false() { + XCTAssertFalse(db.foreignKeys) + } + + func test_foreignKeys_sets_value() { + db.foreignKeys = true + XCTAssertTrue(db.foreignKeys) + } + + func test_defer_foreignKeys_defaults_to_false() { + XCTAssertFalse(db.deferForeignKeys) + } + + func test_defer_foreignKeys_sets_value() { + db.deferForeignKeys = true + XCTAssertTrue(db.deferForeignKeys) + } +} diff --git a/Tests/SQLiteTests/Core/ConnectionTests.swift b/Tests/SQLiteTests/Core/ConnectionTests.swift new file mode 100644 index 00000000..abdb3e1b --- /dev/null +++ b/Tests/SQLiteTests/Core/ConnectionTests.swift @@ -0,0 +1,447 @@ +import XCTest +import Foundation +import Dispatch +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +class ConnectionTests: SQLiteTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + } + + func test_init_withInMemory_returnsInMemoryConnection() throws { + let db = try Connection(.inMemory) + XCTAssertEqual("", db.description) + } + + func test_init_returnsInMemoryByDefault() throws { + let db = try Connection() + XCTAssertEqual("", db.description) + } + + func test_init_withTemporary_returnsTemporaryConnection() throws { + let db = try Connection(.temporary) + XCTAssertEqual("", db.description) + } + + func test_init_withURI_returnsURIConnection() throws { + let db = try Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + let url = URL(fileURLWithPath: db.description) + XCTAssertEqual(url.lastPathComponent, "SQLite.swift Tests.sqlite3") + } + + func test_init_withString_returnsURIConnection() throws { + let db = try Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") + let url = URL(fileURLWithPath: db.description) + XCTAssertEqual(url.lastPathComponent, "SQLite.swift Tests.sqlite3") + } + + func test_init_with_Uri_and_Parameters() throws { + let testDb = fixture("test", withExtension: "sqlite") + _ = try Connection(.uri(testDb, parameters: [.cache(.shared)])) + } + + func test_location_without_Uri_parameters() { + let location: Connection.Location = .uri("foo") + XCTAssertEqual(location.description, "foo") + } + + func test_location_with_Uri_parameters() { + let location: Connection.Location = .uri("foo", parameters: [.mode(.readOnly), .cache(.private)]) + XCTAssertEqual(location.description, "file:foo?mode=ro&cache=private") + } + + func test_readonly_returnsFalseOnReadWriteConnections() { + XCTAssertFalse(db.readonly) + } + + func test_readonly_returnsTrueOnReadOnlyConnections() throws { + let db = try Connection(readonly: true) + XCTAssertTrue(db.readonly) + } + + func test_changes_returnsZeroOnNewConnections() { + XCTAssertEqual(0, db.changes) + } + + func test_lastInsertRowid_returnsLastIdAfterInserts() throws { + try insertUser("alice") + XCTAssertEqual(1, db.lastInsertRowid) + } + + func test_lastInsertRowid_doesNotResetAfterError() throws { + XCTAssert(db.lastInsertRowid == 0) + try insertUser("alice") + XCTAssertEqual(1, db.lastInsertRowid) + XCTAssertThrowsError( + try db.run("INSERT INTO \"users\" (email, age, admin) values ('invalid@example.com', 12, 'invalid')") + ) { error in + if case SQLite.Result.error(_, let code, _) = error { + XCTAssertEqual(SQLITE_CONSTRAINT, code) + } else { + XCTFail("expected error") + } + } + XCTAssertEqual(1, db.lastInsertRowid) + } + + func test_changes_returnsNumberOfChanges() throws { + try insertUser("alice") + XCTAssertEqual(1, db.changes) + try insertUser("betsy") + XCTAssertEqual(1, db.changes) + } + + func test_totalChanges_returnsTotalNumberOfChanges() throws { + XCTAssertEqual(0, db.totalChanges) + try insertUser("alice") + XCTAssertEqual(1, db.totalChanges) + try insertUser("betsy") + XCTAssertEqual(2, db.totalChanges) + } + + func test_useExtendedErrorCodes_returnsFalseDefault() throws { + XCTAssertFalse(db.usesExtendedErrorCodes) + } + + func test_prepare_preparesAndReturnsStatements() throws { + _ = try db.prepare("SELECT * FROM users WHERE admin = 0") + _ = try db.prepare("SELECT * FROM users WHERE admin = ?", 0) + _ = try db.prepare("SELECT * FROM users WHERE admin = ?", [0]) + _ = try db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) + } + + func test_run_preparesRunsAndReturnsStatements() throws { + try db.run("SELECT * FROM users WHERE admin = 0") + try db.run("SELECT * FROM users WHERE admin = ?", 0) + try db.run("SELECT * FROM users WHERE admin = ?", [0]) + try db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) + assertSQL("SELECT * FROM users WHERE admin = 0", 4) + } + + func test_vacuum() throws { + try db.vacuum() + } + + func test_scalar_preparesRunsAndReturnsScalarValues() throws { + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = $admin", ["$admin": 0]) as? Int64) + assertSQL("SELECT count(*) FROM users WHERE admin = 0", 4) + } + + func test_execute_comment() throws { + try db.run("-- this is a comment\nSELECT 1") + assertSQL("-- this is a comment", 0) + assertSQL("SELECT 1", 0) + } + + func test_transaction_executesBeginDeferred() throws { + try db.transaction(.deferred) {} + + assertSQL("BEGIN DEFERRED TRANSACTION") + } + + func test_transaction_executesBeginImmediate() throws { + try db.transaction(.immediate) {} + + assertSQL("BEGIN IMMEDIATE TRANSACTION") + } + + func test_transaction_executesBeginExclusive() throws { + try db.transaction(.exclusive) {} + + assertSQL("BEGIN EXCLUSIVE TRANSACTION") + } + + func test_backup_copiesDatabase() throws { + let target = try Connection() + + try insertUsers("alice", "betsy") + + let backup = try db.backup(usingConnection: target) + try backup.step() + + let users = try target.prepare("SELECT email FROM users ORDER BY email") + XCTAssertEqual(users.map { $0[0] as? String }, ["alice@example.com", "betsy@example.com"]) + } + + func test_transaction_beginsAndCommitsTransactions() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + + try db.transaction { + try stmt.run() + } + + assertSQL("BEGIN DEFERRED TRANSACTION") + assertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") + assertSQL("COMMIT TRANSACTION") + assertSQL("ROLLBACK TRANSACTION", 0) + } + + func test_transaction_rollsBackTransactionsIfCommitsFail() throws { + let sqliteVersion = String(describing: try db.scalar("SELECT sqlite_version()")!) + .split(separator: ".").compactMap { Int($0) } + // PRAGMA defer_foreign_keys only supported in SQLite >= 3.8.0 + guard sqliteVersion[0] == 3 && sqliteVersion[1] >= 8 else { + NSLog("skipping test for SQLite version \(sqliteVersion)") + return + } + // This test case needs to emulate an environment where the individual statements succeed, but committing the + // transaction fails. Using deferred foreign keys is one option to achieve this. + try db.execute("PRAGMA foreign_keys = ON;") + try db.execute("PRAGMA defer_foreign_keys = ON;") + let stmt = try db.prepare("INSERT INTO users (email, manager_id) VALUES (?, ?)", "alice@example.com", 100) + + do { + try db.transaction { + try stmt.run() + } + XCTFail("expected error") + } catch let Result.error(_, code, _) { + XCTAssertEqual(SQLITE_CONSTRAINT, code) + } catch let error { + XCTFail("unexpected error: \(error)") + } + + assertSQL("BEGIN DEFERRED TRANSACTION") + assertSQL("INSERT INTO users (email, manager_id) VALUES ('alice@example.com', 100)") + assertSQL("COMMIT TRANSACTION") + assertSQL("ROLLBACK TRANSACTION") + + // Run another transaction to ensure that a subsequent transaction does not fail with an "cannot start a + // transaction within a transaction" error. + let stmt2 = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + try db.transaction { + try stmt2.run() + } + } + + func test_transaction_beginsAndRollsTransactionsBack() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + + do { + try db.transaction { + try stmt.run() + try stmt.run() + } + } catch { + } + + assertSQL("BEGIN DEFERRED TRANSACTION") + assertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) + assertSQL("ROLLBACK TRANSACTION") + assertSQL("COMMIT TRANSACTION", 0) + } + + func test_savepoint_beginsAndCommitsSavepoints() throws { + try db.savepoint("1") { + try db.savepoint("2") { + try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") + } + } + + assertSQL("SAVEPOINT '1'") + assertSQL("SAVEPOINT '2'") + assertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") + assertSQL("RELEASE SAVEPOINT '2'") + assertSQL("RELEASE SAVEPOINT '1'") + assertSQL("ROLLBACK TO SAVEPOINT '2'", 0) + assertSQL("ROLLBACK TO SAVEPOINT '1'", 0) + } + + func test_savepoint_beginsAndRollsSavepointsBack() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + + do { + try db.savepoint("1") { + try db.savepoint("2") { + try stmt.run() + try stmt.run() + try stmt.run() + } + try db.savepoint("2") { + try stmt.run() + try stmt.run() + try stmt.run() + } + } + } catch { + } + + assertSQL("SAVEPOINT '1'") + assertSQL("SAVEPOINT '2'") + assertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) + assertSQL("ROLLBACK TO SAVEPOINT '2'") + assertSQL("ROLLBACK TO SAVEPOINT '1'") + assertSQL("RELEASE SAVEPOINT '2'", 0) + assertSQL("RELEASE SAVEPOINT '1'", 0) + } + + func test_updateHook_setsUpdateHook_withInsert() throws { + try async { done in + db.updateHook { operation, db, table, rowid in + XCTAssertEqual(Connection.Operation.insert, operation) + XCTAssertEqual("main", db) + XCTAssertEqual("users", table) + XCTAssertEqual(1, rowid) + done() + } + try insertUser("alice") + } + } + + func test_updateHook_setsUpdateHook_withUpdate() throws { + try insertUser("alice") + try async { done in + db.updateHook { operation, db, table, rowid in + XCTAssertEqual(Connection.Operation.update, operation) + XCTAssertEqual("main", db) + XCTAssertEqual("users", table) + XCTAssertEqual(1, rowid) + done() + } + try db.run("UPDATE users SET email = 'alice@example.com'") + } + } + + func test_updateHook_setsUpdateHook_withDelete() throws { + try insertUser("alice") + try async { done in + db.updateHook { operation, db, table, rowid in + XCTAssertEqual(Connection.Operation.delete, operation) + XCTAssertEqual("main", db) + XCTAssertEqual("users", table) + XCTAssertEqual(1, rowid) + done() + } + try db.run("DELETE FROM users WHERE id = 1") + } + } + + func test_commitHook_setsCommitHook() throws { + try async { done in + db.commitHook { + done() + } + try db.transaction { + try insertUser("alice") + } + XCTAssertEqual(1, try db.scalar("SELECT count(*) FROM users") as? Int64) + } + } + + func test_rollbackHook_setsRollbackHook() throws { + try async { done in + db.rollbackHook(done) + do { + try db.transaction { + try insertUser("alice") + try insertUser("alice") // throw + } + } catch { + } + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users") as? Int64) + } + } + + func test_commitHook_withRollback_rollsBack() throws { + try async { done in + db.commitHook { + throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) + } + db.rollbackHook(done) + do { + try db.transaction { + try insertUser("alice") + } + } catch { + } + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users") as? Int64) + } + } + + // https://github.com/stephencelis/SQLite.swift/issues/1071 + #if !(os(Linux) || os(Android)) + func test_createFunction_withArrayArguments() throws { + db.createFunction("hello") { $0[0].map { "Hello, \($0)!" } } + + XCTAssertEqual("Hello, world!", try db.scalar("SELECT hello('world')") as? String) + XCTAssert(try db.scalar("SELECT hello(NULL)") == nil) + } + + func test_createFunction_createsQuotableFunction() throws { + db.createFunction("hello world") { $0[0].map { "Hello, \($0)!" } } + + XCTAssertEqual("Hello, world!", try db.scalar("SELECT \"hello world\"('world')") as? String) + XCTAssert(try db.scalar("SELECT \"hello world\"(NULL)") == nil) + } + + func test_createCollation_createsCollation() throws { + try db.createCollation("NODIACRITIC") { lhs, rhs in + lhs.compare(rhs, options: .diacriticInsensitive) + } + XCTAssertEqual(1, try db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) + } + + func test_createCollation_createsQuotableCollation() throws { + try db.createCollation("NO DIACRITIC") { lhs, rhs in + lhs.compare(rhs, options: .diacriticInsensitive) + } + XCTAssertEqual(1, try db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) + } + + func XXX_test_interrupt_interruptsLongRunningQuery() throws { + let semaphore = DispatchSemaphore(value: 0) + db.createFunction("sleep") { _ in + DispatchQueue.global(qos: .background).async { + self.db.interrupt() + semaphore.signal() + } + semaphore.wait() + return nil + } + let stmt = try db.prepare("SELECT sleep()") + XCTAssertThrowsError(try stmt.run()) { error in + if case Result.error(_, let code, _) = error { + XCTAssertEqual(code, SQLITE_INTERRUPT) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + #endif + + func test_concurrent_access_single_connection() throws { + // test can fail on iOS/tvOS 9.x: SQLite compile-time differences? + guard #available(iOS 10.0, OSX 10.10, tvOS 10.0, watchOS 2.2, *) else { return } + + let conn = try Connection("\(NSTemporaryDirectory())/\(UUID().uuidString)") + try conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") + try conn.run("INSERT INTO test(value) VALUES(?)", 0) + let queue = DispatchQueue(label: "Readers", attributes: [.concurrent]) + + let nReaders = 5 + let semaphores = Array(repeating: DispatchSemaphore(value: 100), count: nReaders) + for index in 0...random()) + assertSQL("random()", SQLite.Expression.random()) + } + + func test_length_wrapsStringExpressionWithLengthFunction() { + assertSQL("length(\"string\")", string.length) + assertSQL("length(\"stringOptional\")", stringOptional.length) + } + + func test_lowercaseString_wrapsStringExpressionWithLowerFunction() { + assertSQL("lower(\"string\")", string.lowercaseString) + assertSQL("lower(\"stringOptional\")", stringOptional.lowercaseString) + } + + func test_uppercaseString_wrapsStringExpressionWithUpperFunction() { + assertSQL("upper(\"string\")", string.uppercaseString) + assertSQL("upper(\"stringOptional\")", stringOptional.uppercaseString) + } + + func test_like_buildsExpressionWithLikeOperator() { + assertSQL("(\"string\" LIKE 'a%')", string.like("a%")) + assertSQL("(\"stringOptional\" LIKE 'b%')", stringOptional.like("b%")) + + assertSQL("(\"string\" LIKE '%\\%' ESCAPE '\\')", string.like("%\\%", escape: "\\")) + assertSQL("(\"stringOptional\" LIKE '_\\_' ESCAPE '\\')", stringOptional.like("_\\_", escape: "\\")) + + assertSQL("(\"string\" LIKE \"a\")", string.like(SQLite.Expression("a"))) + assertSQL("(\"stringOptional\" LIKE \"a\")", stringOptional.like(SQLite.Expression("a"))) + + assertSQL("(\"string\" LIKE \"a\" ESCAPE '\\')", string.like(SQLite.Expression("a"), escape: "\\")) + assertSQL("(\"stringOptional\" LIKE \"a\" ESCAPE '\\')", stringOptional.like(SQLite.Expression("a"), escape: "\\")) + + assertSQL("('string' LIKE \"a\")", "string".like(SQLite.Expression("a"))) + assertSQL("('string' LIKE \"a\" ESCAPE '\\')", "string".like(SQLite.Expression("a"), escape: "\\")) + } + + func test_glob_buildsExpressionWithGlobOperator() { + assertSQL("(\"string\" GLOB 'a*')", string.glob("a*")) + assertSQL("(\"stringOptional\" GLOB 'b*')", stringOptional.glob("b*")) + } + + func test_match_buildsExpressionWithMatchOperator() { + assertSQL("(\"string\" MATCH 'a*')", string.match("a*")) + assertSQL("(\"stringOptional\" MATCH 'b*')", stringOptional.match("b*")) + } + + func test_regexp_buildsExpressionWithRegexpOperator() { + assertSQL("(\"string\" REGEXP '^.+@.+\\.com$')", string.regexp("^.+@.+\\.com$")) + assertSQL("(\"stringOptional\" REGEXP '^.+@.+\\.net$')", stringOptional.regexp("^.+@.+\\.net$")) + } + + func test_collate_buildsExpressionWithCollateOperator() { + assertSQL("(\"string\" COLLATE BINARY)", string.collate(.binary)) + assertSQL("(\"string\" COLLATE NOCASE)", string.collate(.nocase)) + assertSQL("(\"string\" COLLATE RTRIM)", string.collate(.rtrim)) + assertSQL("(\"string\" COLLATE \"CUSTOM\")", string.collate(.custom("CUSTOM"))) + + assertSQL("(\"stringOptional\" COLLATE BINARY)", stringOptional.collate(.binary)) + assertSQL("(\"stringOptional\" COLLATE NOCASE)", stringOptional.collate(.nocase)) + assertSQL("(\"stringOptional\" COLLATE RTRIM)", stringOptional.collate(.rtrim)) + assertSQL("(\"stringOptional\" COLLATE \"CUSTOM\")", stringOptional.collate(.custom("CUSTOM"))) + } + + func test_ltrim_wrapsStringWithLtrimFunction() { + assertSQL("ltrim(\"string\")", string.ltrim()) + assertSQL("ltrim(\"stringOptional\")", stringOptional.ltrim()) + + assertSQL("ltrim(\"string\", ' ')", string.ltrim([" "])) + assertSQL("ltrim(\"stringOptional\", ' ')", stringOptional.ltrim([" "])) + } + + func test_ltrim_wrapsStringWithRtrimFunction() { + assertSQL("rtrim(\"string\")", string.rtrim()) + assertSQL("rtrim(\"stringOptional\")", stringOptional.rtrim()) + + assertSQL("rtrim(\"string\", ' ')", string.rtrim([" "])) + assertSQL("rtrim(\"stringOptional\", ' ')", stringOptional.rtrim([" "])) + } + + func test_ltrim_wrapsStringWithTrimFunction() { + assertSQL("trim(\"string\")", string.trim()) + assertSQL("trim(\"stringOptional\")", stringOptional.trim()) + + assertSQL("trim(\"string\", ' ')", string.trim([" "])) + assertSQL("trim(\"stringOptional\", ' ')", stringOptional.trim([" "])) + } + + func test_replace_wrapsStringWithReplaceFunction() { + assertSQL("replace(\"string\", '@example.com', '@example.net')", string.replace("@example.com", with: "@example.net")) + assertSQL("replace(\"stringOptional\", '@example.net', '@example.com')", stringOptional.replace("@example.net", with: "@example.com")) + } + + func test_substring_wrapsStringWithSubstrFunction() { + assertSQL("substr(\"string\", 1, 2)", string.substring(1, length: 2)) + assertSQL("substr(\"stringOptional\", 2, 1)", stringOptional.substring(2, length: 1)) + } + + func test_subscriptWithRange_wrapsStringWithSubstrFunction() { + assertSQL("substr(\"string\", 1, 2)", string[1..<3]) + assertSQL("substr(\"stringOptional\", 2, 1)", stringOptional[2..<3]) + } + + func test_nilCoalescingOperator_wrapsOptionalsWithIfnullFunction() { + assertSQL("ifnull(\"intOptional\", 1)", intOptional ?? 1) + // AssertSQL("ifnull(\"doubleOptional\", 1.0)", doubleOptional ?? 1) // rdar://problem/21677256 + XCTAssertEqual("ifnull(\"doubleOptional\", 1.0)", (doubleOptional ?? 1).asSQL()) + assertSQL("ifnull(\"stringOptional\", 'literal')", stringOptional ?? "literal") + + assertSQL("ifnull(\"intOptional\", \"int\")", intOptional ?? int) + assertSQL("ifnull(\"doubleOptional\", \"double\")", doubleOptional ?? double) + assertSQL("ifnull(\"stringOptional\", \"string\")", stringOptional ?? string) + + assertSQL("ifnull(\"intOptional\", \"intOptional\")", intOptional ?? intOptional) + assertSQL("ifnull(\"doubleOptional\", \"doubleOptional\")", doubleOptional ?? doubleOptional) + assertSQL("ifnull(\"stringOptional\", \"stringOptional\")", stringOptional ?? stringOptional) + } + + func test_absoluteValue_wrapsNumberWithAbsFucntion() { + assertSQL("abs(\"int\")", int.absoluteValue) + assertSQL("abs(\"intOptional\")", intOptional.absoluteValue) + + assertSQL("abs(\"double\")", double.absoluteValue) + assertSQL("abs(\"doubleOptional\")", doubleOptional.absoluteValue) + } + + func test_contains_buildsExpressionWithInOperator() { + assertSQL("(\"string\" IN ('hello', 'world'))", ["hello", "world"].contains(string)) + assertSQL("(\"stringOptional\" IN ('hello', 'world'))", ["hello", "world"].contains(stringOptional)) + } + +} diff --git a/Tests/SQLiteTests/Core/ResultTests.swift b/Tests/SQLiteTests/Core/ResultTests.swift new file mode 100644 index 00000000..03415f3a --- /dev/null +++ b/Tests/SQLiteTests/Core/ResultTests.swift @@ -0,0 +1,69 @@ +import XCTest +import Foundation +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +class ResultTests: XCTestCase { + var connection: Connection! + + override func setUpWithError() throws { + connection = try Connection(.inMemory) + } + + func test_init_with_ok_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_OK, connection: connection, statement: nil) as Result?) + } + + func test_init_with_row_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_ROW, connection: connection, statement: nil) as Result?) + } + + func test_init_with_done_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_DONE, connection: connection, statement: nil) as Result?) + } + + func test_init_with_other_code_returns_error() { + if case .some(.error(let message, let code, let statement)) = + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil) { + XCTAssertEqual("not an error", message) + XCTAssertEqual(SQLITE_MISUSE, code) + XCTAssertNil(statement) + XCTAssert(connection === connection) + } else { + XCTFail("no error") + } + } + + func test_description_contains_error_code() { + XCTAssertEqual("not an error (code: 21)", + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil)?.description) + } + + func test_description_contains_statement_and_error_code() throws { + let statement = try Statement(connection, "SELECT 1") + XCTAssertEqual("not an error (SELECT 1) (code: 21)", + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: statement)?.description) + } + + func test_init_extended_with_other_code_returns_error() { + connection.usesExtendedErrorCodes = true + if case .some(.extendedError(let message, let extendedCode, let statement)) = + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil) { + XCTAssertEqual("not an error", message) + XCTAssertEqual(extendedCode, 0) + XCTAssertNil(statement) + XCTAssert(connection === connection) + } else { + XCTFail("no error") + } + } +} diff --git a/Tests/SQLiteTests/Core/StatementTests.swift b/Tests/SQLiteTests/Core/StatementTests.swift new file mode 100644 index 00000000..3c90d941 --- /dev/null +++ b/Tests/SQLiteTests/Core/StatementTests.swift @@ -0,0 +1,74 @@ +import XCTest +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +class StatementTests: SQLiteTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + } + + func test_cursor_to_blob() throws { + try insertUsers("alice") + let statement = try db.prepare("SELECT email FROM users") + XCTAssert(try statement.step()) + let blob = statement.row[0] as Blob + XCTAssertEqual("alice@example.com", String(bytes: blob.bytes, encoding: .utf8)!) + } + + func test_zero_sized_blob_returns_null() throws { + let blobs = Table("blobs") + let blobColumn = SQLite.Expression("blob_column") + try db.run(blobs.create { $0.column(blobColumn) }) + try db.run(blobs.insert(blobColumn <- Blob(bytes: []))) + let blobValue = try db.scalar(blobs.select(blobColumn).limit(1, offset: 0)) + XCTAssertEqual([], blobValue.bytes) + } + + func test_prepareRowIterator() throws { + let names = ["a", "b", "c"] + try insertUsers(names) + + let emailColumn = SQLite.Expression("email") + let statement = try db.prepare("SELECT email FROM users") + let emails = try statement.prepareRowIterator().map { $0[emailColumn] } + + XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) + } + + /// Check that a statement reset will close the implicit transaction, allowing wal file to checkpoint + func test_reset_statement() throws { + // insert single row + try insertUsers("bob") + + // prepare a statement and read a single row. This will increment the cursor which + // prevents the implicit transaction from closing. + // https://www.sqlite.org/lang_transaction.html#implicit_versus_explicit_transactions + let statement = try db.prepare("SELECT email FROM users") + _ = try statement.step() + + // verify implicit transaction is not closed, and the users table is still locked + XCTAssertThrowsError(try db.run("DROP TABLE users")) { error in + if case let Result.error(_, code, _) = error { + XCTAssertEqual(code, SQLITE_LOCKED) + } else { + XCTFail("unexpected error") + } + } + + // reset the prepared statement, unlocking the table and allowing the implicit transaction to close + statement.reset() + + // truncate succeeds + try db.run("DROP TABLE users") + } +} diff --git a/SQLiteTests/ValueTests.swift b/Tests/SQLiteTests/Core/ValueTests.swift similarity index 50% rename from SQLiteTests/ValueTests.swift rename to Tests/SQLiteTests/Core/ValueTests.swift index bda2b4b3..f880cb34 100644 --- a/SQLiteTests/ValueTests.swift +++ b/Tests/SQLiteTests/Core/ValueTests.swift @@ -1,6 +1,6 @@ import XCTest import SQLite -class ValueTests : XCTestCase { +class ValueTests: XCTestCase { } diff --git a/Tests/SQLiteTests/Extensions/CipherTests.swift b/Tests/SQLiteTests/Extensions/CipherTests.swift new file mode 100644 index 00000000..bc89cfa2 --- /dev/null +++ b/Tests/SQLiteTests/Extensions/CipherTests.swift @@ -0,0 +1,118 @@ +#if SQLITE_SWIFT_SQLCIPHER +import XCTest +import SQLite +import SQLCipher + +class CipherTests: XCTestCase { + var db1: Connection! + var db2: Connection! + + override func setUpWithError() throws { + db1 = try Connection() + db2 = try Connection() + // db1 + + try db1.key("hello") + + try db1.run("CREATE TABLE foo (bar TEXT)") + try db1.run("INSERT INTO foo (bar) VALUES ('world')") + + // db2 + let key2 = keyData() + try db2.key(Blob(bytes: key2.bytes, length: key2.length)) + + try db2.run("CREATE TABLE foo (bar TEXT)") + try db2.run("INSERT INTO foo (bar) VALUES ('world')") + + try super.setUpWithError() + } + + func test_key() throws { + XCTAssertEqual(1, try db1.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_key_blob_literal() throws { + let db = try Connection() + try db.key("x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'") + } + + func test_rekey() throws { + try db1.rekey("goodbye") + XCTAssertEqual(1, try db1.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_data_key() throws { + XCTAssertEqual(1, try db2.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_data_rekey() throws { + let newKey = keyData() + try db2.rekey(Blob(bytes: newKey.bytes, length: newKey.length)) + XCTAssertEqual(1, try db2.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_keyFailure() throws { + let path = "\(NSTemporaryDirectory())/db.sqlite3" + _ = try? FileManager.default.removeItem(atPath: path) + + let connA = try Connection(path) + defer { try? FileManager.default.removeItem(atPath: path) } + + try connA.key("hello") + try connA.run("CREATE TABLE foo (bar TEXT)") + + let connB = try Connection(path, readonly: true) + + do { + try connB.key("world") + XCTFail("expected exception") + } catch Result.error(_, let code, _) { + XCTAssertEqual(SQLITE_NOTADB, code) + } catch { + XCTFail("unexpected error: \(error)") + } + } + + func test_open_db_encrypted_with_sqlcipher() throws { + // $ sqlcipher Tests/SQLiteTests/fixtures/encrypted-[version].x.sqlite + // sqlite> pragma key = 'sqlcipher-test'; + // sqlite> CREATE TABLE foo (bar TEXT); + // sqlite> INSERT INTO foo (bar) VALUES ('world'); + guard let cipherVersion: String = db1.cipherVersion, + cipherVersion.starts(with: "3.") || cipherVersion.starts(with: "4.") else { return } + + let encryptedFile = cipherVersion.starts(with: "3.") ? + fixture("encrypted-3.x", withExtension: "sqlite") : + fixture("encrypted-4.x", withExtension: "sqlite") + + try FileManager.default.setAttributes([FileAttributeKey.immutable: 1], ofItemAtPath: encryptedFile) + XCTAssertFalse(FileManager.default.isWritableFile(atPath: encryptedFile)) + + defer { + // ensure file can be cleaned up afterwards + try? FileManager.default.setAttributes([FileAttributeKey.immutable: 0], ofItemAtPath: encryptedFile) + } + + let conn = try Connection(encryptedFile) + try conn.key("sqlcipher-test") + XCTAssertEqual(1, try conn.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_export() throws { + let tmp = temporaryFile() + try db1.sqlcipher_export(.uri(tmp), key: "mykey") + + let conn = try Connection(tmp) + try conn.key("mykey") + XCTAssertEqual(1, try conn.scalar("SELECT count(*) FROM foo") as? Int64) + } + + private func keyData(length: Int = 64) -> NSData { + let keyData = NSMutableData(length: length)! + let result = SecRandomCopyBytes(kSecRandomDefault, length, + keyData.mutableBytes.assumingMemoryBound(to: UInt8.self)) + XCTAssertEqual(0, result) + return NSData(data: keyData) + } +} +#endif diff --git a/SQLiteTests/FTS4Tests.swift b/Tests/SQLiteTests/Extensions/FTS4Tests.swift similarity index 59% rename from SQLiteTests/FTS4Tests.swift rename to Tests/SQLiteTests/Extensions/FTS4Tests.swift index 8b4e4f96..f7258fb5 100644 --- a/SQLiteTests/FTS4Tests.swift +++ b/Tests/SQLiteTests/Extensions/FTS4Tests.swift @@ -1,7 +1,7 @@ import XCTest import SQLite -class FTS4Tests : XCTestCase { +class FTS4Tests: XCTestCase { func test_create_onVirtualTable_withFTS4_compilesCreateVirtualTableExpression() { XCTAssertEqual( @@ -21,30 +21,37 @@ class FTS4Tests : XCTestCase { virtualTable.create(.FTS4([string], tokenize: .Porter)) ) XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=0\")", + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"remove_diacritics=0\")", virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: false))) ) XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", - virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"]))) + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"remove_diacritics=1\" + \"tokenchars=.\" \"separators=X\") + """.replacingOccurrences(of: "\n", with: ""), + virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: true, + tokenchars: ["."], separators: ["X"]))) ) } func test_match_onVirtualTableAsExpression_compilesMatchExpression() { - AssertSQL("(\"virtual_table\" MATCH 'string')", virtualTable.match("string") as Expression) - AssertSQL("(\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as Expression) - AssertSQL("(\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as Expression) + assertSQL("(\"virtual_table\" MATCH 'string')", virtualTable.match("string") as SQLite.Expression) + assertSQL("(\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as SQLite.Expression) + assertSQL("(\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as SQLite.Expression) } func test_match_onVirtualTableAsQueryType_compilesMatchExpression() { - AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH 'string')", virtualTable.match("string") as QueryType) - AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as QueryType) - AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as QueryType) + assertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH 'string')", + virtualTable.match("string") as QueryType) + assertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"string\")", + virtualTable.match(string) as QueryType) + assertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"stringOptional\")", + virtualTable.match(stringOptional) as QueryType) } } -class FTS4ConfigTests : XCTestCase { +class FTS4ConfigTests: XCTestCase { var config: FTS4Config! override func setUp() { @@ -108,7 +115,10 @@ class FTS4ConfigTests : XCTestCase { func test_tokenizer_unicode61_with_options() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"remove_diacritics=1\" + \"tokenchars=.\" \"separators=X\") + """.replacingOccurrences(of: "\n", with: ""), sql(config.tokenizer(.Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"])))) } @@ -121,19 +131,19 @@ class FTS4ConfigTests : XCTestCase { func test_config_matchinfo() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(matchinfo=\"fts3\")", - sql(config.matchInfo(.FTS3))) + sql(config.matchInfo(.fts3))) } func test_config_order_asc() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(order=\"asc\")", - sql(config.order(.Asc))) + sql(config.order(.asc))) } func test_config_order_desc() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(order=\"desc\")", - sql(config.order(.Desc))) + sql(config.order(.desc))) } func test_config_compress() { @@ -156,53 +166,25 @@ class FTS4ConfigTests : XCTestCase { func test_config_all() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"int\", \"string\", \"date\", tokenize=porter, prefix=\"2,4\", content=\"table\", notindexed=\"string\", notindexed=\"date\", languageid=\"lid\", matchinfo=\"fts3\", order=\"desc\")", + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"int\", \"string\", \"date\", + tokenize=porter, prefix=\"2,4\", content=\"table\", notindexed=\"string\", notindexed=\"date\", + languageid=\"lid\", matchinfo=\"fts3\", order=\"desc\") + """.replacingOccurrences(of: "\n", with: ""), sql(config .tokenizer(.Porter) .column(int) .column(string, [.unindexed]) .column(date, [.unindexed]) .externalContent(table) - .matchInfo(.FTS3) + .matchInfo(.fts3) .languageId("lid") - .order(.Desc) + .order(.desc) .prefix([2, 4])) ) } - func sql(config: FTS4Config) -> String { - return virtualTable.create(.FTS4(config)) + func sql(_ config: FTS4Config) -> String { + virtualTable.create(.FTS4(config)) } } - -class FTS4IntegrationTests : SQLiteTestCase { -#if !SQLITE_SWIFT_STANDALONE - func test_registerTokenizer_registersTokenizer() { - let emails = VirtualTable("emails") - let subject = Expression("subject") - let body = Expression("body") - - let locale = CFLocaleCopyCurrent() - let tokenizerName = "tokenizer" - let tokenizer = CFStringTokenizerCreate(nil, "", CFRangeMake(0, 0), UInt(kCFStringTokenizerUnitWord), locale) - try! db.registerTokenizer(tokenizerName) { string in - CFStringTokenizerSetString(tokenizer, string, CFRangeMake(0, CFStringGetLength(string))) - if CFStringTokenizerAdvanceToNextToken(tokenizer) == .None { - return nil - } - let range = CFStringTokenizerGetCurrentTokenRange(tokenizer) - let input = CFStringCreateWithSubstring(kCFAllocatorDefault, string, range) - let token = CFStringCreateMutableCopy(nil, range.length, input) - CFStringLowercase(token, locale) - CFStringTransform(token, nil, kCFStringTransformStripDiacritics, false) - return (token as String, string.rangeOfString(input as String)!) - } - - try! db.run(emails.create(.FTS4([subject, body], tokenize: .Custom(tokenizerName)))) - AssertSQL("CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=\"SQLite.swift\" \"tokenizer\")") - - try! db.run(emails.insert(subject <- "Aún más cáfe!")) - XCTAssertEqual(1, try! db.scalar(emails.filter(emails.match("aun")).count)) - } -#endif -} diff --git a/SQLiteTests/FTS5Tests.swift b/Tests/SQLiteTests/Extensions/FTS5Tests.swift similarity index 78% rename from SQLiteTests/FTS5Tests.swift rename to Tests/SQLiteTests/Extensions/FTS5Tests.swift index aa3965bd..199ec415 100644 --- a/SQLiteTests/FTS5Tests.swift +++ b/Tests/SQLiteTests/Extensions/FTS5Tests.swift @@ -77,10 +77,20 @@ class FTS5Tests: XCTestCase { func test_tokenizer_unicode61_with_options() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61 \"remove_diacritics=1\" \"tokenchars=.\" \"separators=X\")", sql(config.tokenizer(.Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"])))) } + func test_tokenizer_trigram() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=trigram case_sensitive 0)", + sql(config.tokenizer(.Trigram()))) + + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=trigram case_sensitive 1)", + sql(config.tokenizer(.Trigram(caseSensitive: true)))) + } + func test_column_size() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(columnsize=1)", @@ -90,24 +100,27 @@ class FTS5Tests: XCTestCase { func test_detail_full() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"full\")", - sql(config.detail(.Full))) + sql(config.detail(.full))) } func test_detail_column() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"column\")", - sql(config.detail(.Column))) + sql(config.detail(.column))) } func test_detail_none() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"none\")", - sql(config.detail(.None))) + sql(config.detail(.none))) } func test_fts5_config_all() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"int\", \"string\" UNINDEXED, \"date\" UNINDEXED, tokenize=porter, prefix=\"2,4\", content=\"table\")", + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"int\", \"string\" UNINDEXED, \"date\" UNINDEXED, + tokenize=porter, prefix=\"2,4\", content=\"table\") + """.replacingOccurrences(of: "\n", with: ""), sql(config .tokenizer(.Porter) .column(int) @@ -118,7 +131,7 @@ class FTS5Tests: XCTestCase { ) } - func sql(config: FTS5Config) -> String { - return virtualTable.create(.FTS5(config)) + func sql(_ config: FTS5Config) -> String { + virtualTable.create(.FTS5(config)) } } diff --git a/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift new file mode 100644 index 00000000..b3b9e617 --- /dev/null +++ b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift @@ -0,0 +1,80 @@ +import XCTest +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif +@testable import SQLite + +class FTSIntegrationTests: SQLiteTestCase { + let email = SQLite.Expression("email") + let index = VirtualTable("index") + + private func createIndex() throws { + try createOrSkip { db in + try db.run(index.create(.FTS5( + FTS5Config() + .column(email) + .tokenizer(.Unicode61())) + )) + } + + for user in try db.prepare(users) { + try db.run(index.insert(email <- user[email])) + } + } + + private func createTrigramIndex() throws { + try createOrSkip { db in + try db.run(index.create(.FTS5( + FTS5Config() + .column(email) + .tokenizer(.Trigram(caseSensitive: false))) + )) + } + + for user in try db.prepare(users) { + try db.run(index.insert(email <- user[email])) + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + try insertUsers("John", "Paul", "George", "Ringo") + } + + func testMatch() throws { + try createIndex() + let matches = Array(try db.prepare(index.match("Paul"))) + XCTAssertEqual(matches.map { $0[email ]}, ["Paul@example.com"]) + } + + func testMatchPartial() throws { + try insertUsers("Paula") + try createIndex() + let matches = Array(try db.prepare(index.match("Pa*"))) + XCTAssertEqual(matches.map { $0[email ]}, ["Paul@example.com", "Paula@example.com"]) + } + + func testTrigramIndex() throws { + try createTrigramIndex() + let matches = Array(try db.prepare(index.match("Paul"))) + XCTAssertEqual(1, matches.count) + } + + private func createOrSkip(_ createIndex: (Connection) throws -> Void) throws { + do { + try createIndex(db) + } catch let error as Result { + try XCTSkipIf(error.description.starts(with: "no such module:") || + error.description.starts(with: "parse error") + ) + throw error + } + } +} diff --git a/SQLiteTests/RTreeTests.swift b/Tests/SQLiteTests/Extensions/RTreeTests.swift similarity index 94% rename from SQLiteTests/RTreeTests.swift rename to Tests/SQLiteTests/Extensions/RTreeTests.swift index 7147533e..5525da26 100644 --- a/SQLiteTests/RTreeTests.swift +++ b/Tests/SQLiteTests/Extensions/RTreeTests.swift @@ -1,7 +1,7 @@ import XCTest import SQLite -class RTreeTests : XCTestCase { +class RTreeTests: XCTestCase { func test_create_onVirtualTable_withRTree_createVirtualTableExpression() { XCTAssertEqual( @@ -14,4 +14,4 @@ class RTreeTests : XCTestCase { ) } -} \ No newline at end of file +} diff --git a/Tests/SQLiteTests/Fixtures.swift b/Tests/SQLiteTests/Fixtures.swift new file mode 100644 index 00000000..bd261d2c --- /dev/null +++ b/Tests/SQLiteTests/Fixtures.swift @@ -0,0 +1,22 @@ +import Foundation + +func fixture(_ name: String, withExtension: String?) -> String { + #if SWIFT_PACKAGE + let testBundle = Bundle.module + #else + let testBundle = Bundle(for: SQLiteTestCase.self) + #endif + + for resource in [name, "Resources/\(name)"] { + if let url = testBundle.url( + forResource: resource, + withExtension: withExtension) { + return url.path + } + } + fatalError("Cannot find \(name).\(withExtension ?? "")") +} + +func temporaryFile() -> String { + URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).path +} diff --git a/Tests/SQLiteTests/FoundationTests.swift b/Tests/SQLiteTests/FoundationTests.swift new file mode 100644 index 00000000..453febcd --- /dev/null +++ b/Tests/SQLiteTests/FoundationTests.swift @@ -0,0 +1,40 @@ +import XCTest +import SQLite + +class FoundationTests: XCTestCase { + func testDataFromBlob() { + let data = Data([1, 2, 3]) + let blob = data.datatypeValue + XCTAssertEqual([1, 2, 3], blob.bytes) + } + + func testBlobToData() { + let blob = Blob(bytes: [1, 2, 3]) + let data = Data.fromDatatypeValue(blob) + XCTAssertEqual(Data([1, 2, 3]), data) + } + + func testStringFromUUID() { + let uuid = UUID(uuidString: "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3")! + let string = uuid.datatypeValue + XCTAssertEqual("4ABE10C9-FF12-4CD4-90C1-4B429001BAD3", string) + } + + func testUUIDFromString() { + let string = "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3" + let uuid = UUID.fromDatatypeValue(string) + XCTAssertEqual(UUID(uuidString: "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3"), uuid) + } + + func testURLFromString() { + let string = "http://foo.com" + let url = URL.fromDatatypeValue(string) + XCTAssertEqual(URL(string: string), url) + } + + func testStringFromURL() { + let url = URL(string: "http://foo.com")! + let string = url.datatypeValue + XCTAssertEqual("http://foo.com", string) + } +} diff --git a/SQLiteTests/Info.plist b/Tests/SQLiteTests/Info.plist similarity index 100% rename from SQLiteTests/Info.plist rename to Tests/SQLiteTests/Info.plist diff --git a/Tests/SQLiteTests/Resources/encrypted-3.x.sqlite b/Tests/SQLiteTests/Resources/encrypted-3.x.sqlite new file mode 100644 index 00000000..4b3c4d0e Binary files /dev/null and b/Tests/SQLiteTests/Resources/encrypted-3.x.sqlite differ diff --git a/Tests/SQLiteTests/Resources/encrypted-4.x.sqlite b/Tests/SQLiteTests/Resources/encrypted-4.x.sqlite new file mode 100644 index 00000000..36890365 Binary files /dev/null and b/Tests/SQLiteTests/Resources/encrypted-4.x.sqlite differ diff --git a/Tests/SQLiteTests/Resources/test.sqlite b/Tests/SQLiteTests/Resources/test.sqlite new file mode 100644 index 00000000..7b39b423 Binary files /dev/null and b/Tests/SQLiteTests/Resources/test.sqlite differ diff --git a/Tests/SQLiteTests/Schema/Connection+SchemaTests.swift b/Tests/SQLiteTests/Schema/Connection+SchemaTests.swift new file mode 100644 index 00000000..57e2726b --- /dev/null +++ b/Tests/SQLiteTests/Schema/Connection+SchemaTests.swift @@ -0,0 +1,52 @@ +import XCTest +@testable import SQLite + +class ConnectionSchemaTests: SQLiteTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + } + + func test_foreignKeyCheck() throws { + let errors = try db.foreignKeyCheck() + XCTAssert(errors.isEmpty) + } + + func test_foreignKeyCheck_with_table() throws { + let errors = try db.foreignKeyCheck(table: "users") + XCTAssert(errors.isEmpty) + } + + func test_foreignKeyCheck_table_not_found() throws { + XCTAssertThrowsError(try db.foreignKeyCheck(table: "xxx")) { error in + guard case Result.error(let message, _, _) = error else { + assertionFailure("invalid error type") + return + } + XCTAssertEqual(message, "no such table: xxx") + } + } + + func test_integrityCheck_global() throws { + let results = try db.integrityCheck() + XCTAssert(results.isEmpty) + } + + func test_partial_integrityCheck_table() throws { + guard db.supports(.partialIntegrityCheck) else { return } + let results = try db.integrityCheck(table: "users") + XCTAssert(results.isEmpty) + } + + func test_integrityCheck_table_not_found() throws { + guard db.supports(.partialIntegrityCheck) else { return } + XCTAssertThrowsError(try db.integrityCheck(table: "xxx")) { error in + guard case Result.error(let message, _, _) = error else { + assertionFailure("invalid error type") + return + } + XCTAssertEqual(message, "no such table: xxx") + } + } +} diff --git a/Tests/SQLiteTests/Schema/SchemaChangerTests.swift b/Tests/SQLiteTests/Schema/SchemaChangerTests.swift new file mode 100644 index 00000000..f5a6de42 --- /dev/null +++ b/Tests/SQLiteTests/Schema/SchemaChangerTests.swift @@ -0,0 +1,383 @@ +import XCTest +@testable import SQLite + +class SchemaChangerTests: SQLiteTestCase { + var schemaChanger: SchemaChanger! + var schema: SchemaReader! + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + + try insertUsers("bob") + + schema = SchemaReader(connection: db) + schemaChanger = SchemaChanger(connection: db) + } + + func test_empty_migration_does_not_change_column_definitions() throws { + let previous = try schema.columnDefinitions(table: "users") + try schemaChanger.alter(table: "users") { _ in + } + let current = try schema.columnDefinitions(table: "users") + + XCTAssertEqual(previous, current) + } + + func test_empty_migration_does_not_change_index_definitions() throws { + let previous = try schema.indexDefinitions(table: "users") + try schemaChanger.alter(table: "users") { _ in + } + let current = try schema.indexDefinitions(table: "users") + + XCTAssertEqual(previous, current) + } + + func test_empty_migration_does_not_change_foreign_key_definitions() throws { + let previous = try schema.foreignKeys(table: "users") + try schemaChanger.alter(table: "users") { _ in + } + let current = try schema.foreignKeys(table: "users") + + XCTAssertEqual(previous, current) + } + + func test_empty_migration_does_not_change_the_row_count() throws { + let previous = try db.scalar(users.count) + try schemaChanger.alter(table: "users") { _ in + } + let current = try db.scalar(users.count) + + XCTAssertEqual(previous, current) + } + + func test_drop_column() throws { + try schemaChanger.alter(table: "users") { table in + table.drop(column: "age") + } + let columns = try schema.columnDefinitions(table: "users").map(\.name) + XCTAssertFalse(columns.contains("age")) + } + + func test_drop_column_legacy() throws { + schemaChanger = .init(connection: db, version: .init(major: 3, minor: 24)) // DROP COLUMN introduced in 3.35.0 + + try schemaChanger.alter(table: "users") { table in + table.drop(column: "age") + } + let columns = try schema.columnDefinitions(table: "users").map(\.name) + XCTAssertFalse(columns.contains("age")) + } + + func test_rename_column() throws { + try schemaChanger.alter(table: "users") { table in + table.rename(column: "age", to: "age2") + } + + let columns = try schema.columnDefinitions(table: "users").map(\.name) + XCTAssertFalse(columns.contains("age")) + XCTAssertTrue(columns.contains("age2")) + } + + func test_rename_column_legacy() throws { + schemaChanger = .init(connection: db, version: .init(major: 3, minor: 24)) // RENAME COLUMN introduced in 3.25.0 + + try schemaChanger.alter(table: "users") { table in + table.rename(column: "age", to: "age2") + } + + let columns = try schema.columnDefinitions(table: "users").map(\.name) + XCTAssertFalse(columns.contains("age")) + XCTAssertTrue(columns.contains("age2")) + } + + func test_add_column() throws { + let column = SQLite.Expression("new_column") + let newColumn = ColumnDefinition(name: "new_column", + type: .TEXT, + nullable: true, + defaultValue: .stringLiteral("foo")) + + try schemaChanger.alter(table: "users") { table in + table.add(column: newColumn) + } + + let columns = try schema.columnDefinitions(table: "users") + XCTAssertTrue(columns.contains(newColumn)) + + XCTAssertEqual(try db.pluck(users.select(column))?[column], "foo") + } + + func test_add_column_primary_key_fails() throws { + let newColumn = ColumnDefinition(name: "new_column", + primaryKey: .init(autoIncrement: false, onConflict: nil), + type: .TEXT) + + XCTAssertThrowsError(try schemaChanger.alter(table: "users") { table in + table.add(column: newColumn) + }) { error in + if case SchemaChanger.Error.invalidColumnDefinition(_) = error { + XCTAssertEqual("Invalid column definition: can not add primary key column", error.localizedDescription) + } else { + XCTFail("invalid error: \(error)") + } + } + } + + func test_add_index() throws { + try schemaChanger.alter(table: "users") { table in + table.add(index: .init(table: table.name, name: "age_index", unique: false, columns: ["age"], indexSQL: nil)) + } + + let indexes = try schema.indexDefinitions(table: "users").filter { !$0.isInternal } + XCTAssertEqual([ + IndexDefinition(table: "users", + name: "age_index", + unique: false, + columns: ["age"], + where: nil, + orders: nil, + origin: .createIndex) + ], indexes) + } + + func test_add_index_if_not_exists() throws { + let index = IndexDefinition(table: "users", name: "age_index", unique: false, columns: ["age"], indexSQL: nil) + try schemaChanger.alter(table: "users") { table in + table.add(index: index) + } + + try schemaChanger.alter(table: "users") { table in + table.add(index: index, ifNotExists: true) + } + + XCTAssertThrowsError( + try schemaChanger.alter(table: "users") { table in + table.add(index: index, ifNotExists: false) + } + ) + } + + func test_drop_index() throws { + try db.execute(""" + CREATE INDEX age_index ON users(age) + """) + + try schemaChanger.alter(table: "users") { table in + table.drop(index: "age_index") + } + let indexes = try schema.indexDefinitions(table: "users").filter { !$0.isInternal } + XCTAssertEqual(0, indexes.count) + } + + func test_drop_index_if_exists() throws { + try db.execute(""" + CREATE INDEX age_index ON users(age) + """) + + try schemaChanger.alter(table: "users") { table in + table.drop(index: "age_index") + } + + try schemaChanger.alter(table: "users") { table in + table.drop(index: "age_index", ifExists: true) + } + + XCTAssertThrowsError( + try schemaChanger.alter(table: "users") { table in + table.drop(index: "age_index", ifExists: false) + } + ) { error in + if case Result.error(let message, _, _) = error { + XCTAssertEqual(message, "no such index: age_index") + } else { + XCTFail("unexpected error \(error)") + } + } + } + + func test_drop_table() throws { + try schemaChanger.drop(table: "users") + XCTAssertThrowsError(try db.scalar(users.count)) { error in + if case Result.error(let message, _, _) = error { + XCTAssertEqual(message, "no such table: users") + } else { + XCTFail("unexpected error \(error)") + } + } + } + + func test_drop_table_if_exists_true() throws { + try schemaChanger.drop(table: "xxx", ifExists: true) + } + + func test_drop_table_if_exists_false() throws { + XCTAssertThrowsError(try schemaChanger.drop(table: "xxx", ifExists: false)) { error in + if case Result.error(let message, _, _) = error { + XCTAssertEqual(message, "no such table: xxx") + } else { + XCTFail("unexpected error \(error)") + } + } + } + + func test_rename_table() throws { + try schemaChanger.rename(table: "users", to: "users_new") + let users_new = Table("users_new") + XCTAssertEqual((try db.scalar(users_new.count)) as Int, 1) + } + + func test_create_table() throws { + try schemaChanger.create(table: "foo") { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + table.add(column: .init(name: "name", type: .TEXT, nullable: false, unique: true)) + table.add(column: .init(name: "age", type: .INTEGER)) + + table.add(index: .init(table: table.name, + name: "nameIndex", + unique: true, + columns: ["name"], + where: nil, + orders: nil)) + } + + // make sure new table can be queried + let foo = Table("foo") + XCTAssertEqual((try db.scalar(foo.count)) as Int, 0) + + let columns = try schema.columnDefinitions(table: "foo") + XCTAssertEqual(columns, [ + ColumnDefinition(name: "id", + primaryKey: .init(autoIncrement: true, onConflict: nil), + type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "name", + primaryKey: nil, + type: .TEXT, + nullable: false, + unique: true, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "age", + primaryKey: nil, + type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil) + ]) + + let indexes = try schema.indexDefinitions(table: "foo").filter { !$0.isInternal } + XCTAssertEqual(indexes, [ + IndexDefinition(table: "foo", name: "nameIndex", unique: true, columns: ["name"], where: nil, orders: nil, origin: .createIndex) + ]) + } + + func test_create_table_add_column_expression() throws { + try schemaChanger.create(table: "foo") { table in + table.add(expression: SQLite.Expression("name")) + table.add(expression: SQLite.Expression("age")) + table.add(expression: SQLite.Expression("salary")) + } + + let columns = try schema.columnDefinitions(table: "foo") + XCTAssertEqual(columns, [ + ColumnDefinition(name: "name", + primaryKey: nil, + type: .TEXT, + nullable: false, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "age", + primaryKey: nil, + type: .INTEGER, + nullable: false, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "salary", + primaryKey: nil, + type: .REAL, + nullable: true, + defaultValue: .NULL, + references: nil) + ]) + } + + func test_create_table_if_not_exists() throws { + try schemaChanger.create(table: "foo") { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + } + + try schemaChanger.create(table: "foo", ifNotExists: true) { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + } + + XCTAssertThrowsError( + try schemaChanger.create(table: "foo", ifNotExists: false) { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + } + ) { error in + if case Result.error(_, let code, _) = error { + XCTAssertEqual(code, 1) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + func test_create_table_if_not_exists_with_index() throws { + try schemaChanger.create(table: "foo") { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + table.add(column: .init(name: "name", type: .TEXT)) + table.add(index: .init(table: "foo", name: "name_index", unique: true, columns: ["name"], indexSQL: nil)) + } + + // ifNotExists needs to apply to index creation as well + try schemaChanger.create(table: "foo", ifNotExists: true) { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + table.add(index: .init(table: "foo", name: "name_index", unique: true, columns: ["name"], indexSQL: nil)) + } + } + + func test_create_table_with_foreign_key_reference() throws { + try schemaChanger.create(table: "foo") { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + } + + try schemaChanger.create(table: "bars") { table in + table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER)) + table.add(column: .init(name: "foo_id", + type: .INTEGER, + nullable: false, + references: .init(toTable: "foo", toColumn: "id"))) + } + + let barColumns = try schema.columnDefinitions(table: "bars") + + XCTAssertEqual([ + ColumnDefinition(name: "id", + primaryKey: .init(autoIncrement: true, onConflict: nil), + type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil), + + ColumnDefinition(name: "foo_id", + primaryKey: nil, + type: .INTEGER, + nullable: false, + unique: false, + defaultValue: .NULL, + references: .init(fromColumn: "foo_id", toTable: "foo", toColumn: "id", onUpdate: nil, onDelete: nil)) + ], barColumns) + } + + func test_run_arbitrary_sql() throws { + try schemaChanger.run("DROP TABLE users") + XCTAssertEqual(0, try schema.objectDefinitions(name: "users", type: .table).count) + } +} diff --git a/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift b/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift new file mode 100644 index 00000000..f3a0ba83 --- /dev/null +++ b/Tests/SQLiteTests/Schema/SchemaDefinitionsTests.swift @@ -0,0 +1,446 @@ +import XCTest +@testable import SQLite + +class ColumnDefinitionTests: XCTestCase { + var definition: ColumnDefinition! + var expected: String! + + static let definitions: [(String, ColumnDefinition)] = [ + ("\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", + ColumnDefinition(name: "id", primaryKey: .init(), type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil)), + + ("\"other_id\" INTEGER NOT NULL REFERENCES \"other_table\" (\"some_id\")", + ColumnDefinition(name: "other_id", primaryKey: nil, type: .INTEGER, nullable: false, defaultValue: .NULL, + references: .init(fromColumn: "", toTable: "other_table", toColumn: "some_id", onUpdate: nil, onDelete: nil))), + + ("\"text\" TEXT", + ColumnDefinition(name: "text", primaryKey: nil, type: .TEXT, nullable: true, defaultValue: .NULL, references: nil)), + + ("\"text\" TEXT NOT NULL", + ColumnDefinition(name: "text", primaryKey: nil, type: .TEXT, nullable: false, defaultValue: .NULL, references: nil)), + + ("\"text_column\" TEXT DEFAULT 'fo\"o'", + ColumnDefinition(name: "text_column", primaryKey: nil, type: .TEXT, nullable: true, + defaultValue: .stringLiteral("fo\"o"), references: nil)), + + ("\"integer_column\" INTEGER DEFAULT 123", + ColumnDefinition(name: "integer_column", primaryKey: nil, type: .INTEGER, nullable: true, + defaultValue: .numericLiteral("123"), references: nil)), + + ("\"real_column\" REAL DEFAULT 123.123", + ColumnDefinition(name: "real_column", primaryKey: nil, type: .REAL, nullable: true, + defaultValue: .numericLiteral("123.123"), references: nil)) + ] + + #if !(os(Linux) || os(Android)) + override class var defaultTestSuite: XCTestSuite { + let suite = XCTestSuite(forTestCaseClass: ColumnDefinitionTests.self) + + for (expected, column) in ColumnDefinitionTests.definitions { + let test = ColumnDefinitionTests(selector: #selector(verify)) + test.definition = column + test.expected = expected + suite.addTest(test) + } + return suite + } + + @objc func verify() { + XCTAssertEqual(definition.toSQL(), expected) + } + #endif + + func testNullableByDefault() { + let test = ColumnDefinition(name: "test", type: .REAL) + XCTAssertEqual(test.name, "test") + XCTAssertTrue(test.nullable) + XCTAssertEqual(test.defaultValue, .NULL) + XCTAssertEqual(test.type, .REAL) + XCTAssertNil(test.references) + XCTAssertNil(test.primaryKey) + } +} + +class AffinityTests: XCTestCase { + func test_init() { + XCTAssertEqual(ColumnDefinition.Affinity("TEXT"), .TEXT) + XCTAssertEqual(ColumnDefinition.Affinity("text"), .TEXT) + XCTAssertEqual(ColumnDefinition.Affinity("INTEGER"), .INTEGER) + XCTAssertEqual(ColumnDefinition.Affinity("BLOB"), .BLOB) + XCTAssertEqual(ColumnDefinition.Affinity("REAL"), .REAL) + XCTAssertEqual(ColumnDefinition.Affinity("NUMERIC"), .NUMERIC) + } + + // [Determination Of Column Affinity](https://sqlite.org/datatype3.html#determination_of_column_affinity) + // Rule 1 + func testIntegerAffinity() { + let declared = [ + "INT", + "INTEGER", + "TINYINT", + "SMALLINT", + "MEDIUMINT", + "BIGINT", + "UNSIGNED BIG INT", + "INT2", + "INT8" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .INTEGER})) + } + + // Rule 2 + func testTextAffinity() { + let declared = [ + "CHARACTER(20)", + "VARCHAR(255)", + "VARYING CHARACTER(255)", + "NCHAR(55)", + "NATIVE CHARACTER(70)", + "NVARCHAR(100)", + "TEXT", + "CLOB" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .TEXT})) + } + + // Rule 3 + func testBlobAffinity() { + XCTAssertEqual(ColumnDefinition.Affinity("BLOB"), .BLOB) + } + + // Rule 4 + func testRealAffinity() { + let declared = [ + "REAL", + "DOUBLE", + "DOUBLE PRECISION", + "FLOAT" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .REAL})) + } + + // Rule 5 + func testNumericAffinity() { + let declared = [ + "NUMERIC", + "DECIMAL(10,5)", + "BOOLEAN", + "DATE", + "DATETIME" + ] + XCTAssertTrue(declared.allSatisfy({ColumnDefinition.Affinity($0) == .NUMERIC})) + } + + func test_returns_NUMERIC_for_unknown_type() { + XCTAssertEqual(ColumnDefinition.Affinity("baz"), .NUMERIC) + } +} + +class IndexDefinitionTests: XCTestCase { + var definition: IndexDefinition! + var expected: String! + var ifNotExists: Bool! + + static let definitions: [(IndexDefinition, Bool, String)] = [ + (IndexDefinition(table: "tests", name: "index_tests", + unique: false, + columns: ["test_column"], + where: nil, + orders: nil), + false, + "CREATE INDEX \"index_tests\" ON \"tests\" (\"test_column\")"), + + (IndexDefinition(table: "tests", name: "index_tests", + unique: true, + columns: ["test_column"], + where: nil, + orders: nil), + false, + "CREATE UNIQUE INDEX \"index_tests\" ON \"tests\" (\"test_column\")"), + + (IndexDefinition(table: "tests", name: "index_tests", + unique: true, + columns: ["test_column", "bar_column"], + where: "test_column IS NOT NULL", + orders: nil), + false, + "CREATE UNIQUE INDEX \"index_tests\" ON \"tests\" (\"test_column\", \"bar_column\") WHERE test_column IS NOT NULL"), + + (IndexDefinition(table: "tests", name: "index_tests", + unique: true, + columns: ["test_column", "bar_column"], + where: nil, + orders: ["test_column": .DESC]), + false, + "CREATE UNIQUE INDEX \"index_tests\" ON \"tests\" (\"test_column\" DESC, \"bar_column\")"), + + (IndexDefinition(table: "tests", name: "index_tests", + unique: false, + columns: ["test_column"], + where: nil, + orders: nil), + true, + "CREATE INDEX IF NOT EXISTS \"index_tests\" ON \"tests\" (\"test_column\")") + ] + + #if !(os(Linux) || os(Android)) + override class var defaultTestSuite: XCTestSuite { + let suite = XCTestSuite(forTestCaseClass: IndexDefinitionTests.self) + + for (column, ifNotExists, expected) in IndexDefinitionTests.definitions { + let test = IndexDefinitionTests(selector: #selector(verify)) + test.definition = column + test.expected = expected + test.ifNotExists = ifNotExists + suite.addTest(test) + } + return suite + } + + @objc func verify() { + XCTAssertEqual(definition.toSQL(ifNotExists: ifNotExists), expected) + } + #endif + + func test_validate() { + + let longIndex = IndexDefinition( + table: "tests", + name: String(repeating: "x", count: 65), + unique: false, + columns: ["test_column"], + where: nil, + orders: nil) + + XCTAssertThrowsError(try longIndex.validate()) { error in + XCTAssertEqual(error.localizedDescription, + "Index name 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' " + + "on table 'tests' is too long; the limit is 64 characters") + } + } + + func test_rename() { + let index = IndexDefinition(table: "tests", name: "index_tests_something", + unique: true, + columns: ["test_column"], + where: "test_column IS NOT NULL", + orders: nil) + + let renamedIndex = index.renameTable(to: "foo") + + XCTAssertEqual(renamedIndex, + IndexDefinition( + table: "foo", + name: "index_tests_something", + unique: true, + columns: ["test_column"], + where: "test_column IS NOT NULL", + orders: nil + ) + ) + } +} + +class ForeignKeyDefinitionTests: XCTestCase { + func test_toSQL() { + XCTAssertEqual( + ColumnDefinition.ForeignKey( + fromColumn: "bar", + toTable: "foo", + toColumn: "bar_id", + onUpdate: nil, + onDelete: "SET NULL" + ).toSQL(), """ + REFERENCES "foo" ("bar_id") ON DELETE SET NULL + """ + ) + } +} + +class TableDefinitionTests: XCTestCase { + func test_quoted_columnList() { + let definition = TableDefinition(name: "foo", columns: [ + ColumnDefinition(name: "id", primaryKey: .init(), type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil), + ColumnDefinition(name: "baz", primaryKey: nil, type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil) + ], indexes: []) + + XCTAssertEqual(definition.quotedColumnList, """ + "id", "baz" + """) + } + + func test_toSQL() { + let definition = TableDefinition(name: "foo", columns: [ + ColumnDefinition(name: "id", primaryKey: .init(), type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil) + ], indexes: []) + + XCTAssertEqual(definition.toSQL(), """ + CREATE TABLE foo ( \"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ) + """) + } + + func test_toSQL_temp_table() { + let definition = TableDefinition(name: "foo", columns: [ + ColumnDefinition(name: "id", primaryKey: .init(), type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil) + ], indexes: []) + + XCTAssertEqual(definition.toSQL(temporary: true), """ + CREATE TEMPORARY TABLE foo ( \"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ) + """) + } + + func test_copySQL() { + let from = TableDefinition(name: "from_table", columns: [ + ColumnDefinition(name: "id", primaryKey: .init(), type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil) + ], indexes: []) + + let to = TableDefinition(name: "to_table", columns: [ + ColumnDefinition(name: "id", primaryKey: .init(), type: .INTEGER, nullable: false, defaultValue: .NULL, references: nil) + ], indexes: []) + + XCTAssertEqual(from.copySQL(to: to), """ + INSERT INTO "to_table" ("id") SELECT "id" FROM "from_table" + """) + } +} + +class PrimaryKeyTests: XCTestCase { + func test_toSQL() { + XCTAssertEqual( + ColumnDefinition.PrimaryKey(autoIncrement: false).toSQL(), + "PRIMARY KEY" + ) + } + + func test_toSQL_autoincrement() { + XCTAssertEqual( + ColumnDefinition.PrimaryKey(autoIncrement: true).toSQL(), + "PRIMARY KEY AUTOINCREMENT" + ) + } + + func test_toSQL_on_conflict() { + XCTAssertEqual( + ColumnDefinition.PrimaryKey(autoIncrement: false, onConflict: .ROLLBACK).toSQL(), + "PRIMARY KEY ON CONFLICT ROLLBACK" + ) + } + + func test_fromSQL() { + XCTAssertEqual( + ColumnDefinition.PrimaryKey(sql: "PRIMARY KEY"), + ColumnDefinition.PrimaryKey(autoIncrement: false) + ) + } + + func test_fromSQL_invalid_sql_is_nil() { + XCTAssertNil(ColumnDefinition.PrimaryKey(sql: "FOO")) + } + + func test_fromSQL_autoincrement() { + XCTAssertEqual( + ColumnDefinition.PrimaryKey(sql: "PRIMARY KEY AUTOINCREMENT"), + ColumnDefinition.PrimaryKey(autoIncrement: true) + ) + } + + func test_fromSQL_on_conflict() { + XCTAssertEqual( + ColumnDefinition.PrimaryKey(sql: "PRIMARY KEY ON CONFLICT ROLLBACK"), + ColumnDefinition.PrimaryKey(autoIncrement: false, onConflict: .ROLLBACK) + ) + } +} + +class LiteralValueTests: XCTestCase { + func test_recognizes_TRUE() { + XCTAssertEqual(LiteralValue("TRUE"), .TRUE) + } + + func test_recognizes_FALSE() { + XCTAssertEqual(LiteralValue("FALSE"), .FALSE) + } + + func test_recognizes_NULL() { + XCTAssertEqual(LiteralValue("NULL"), .NULL) + } + + func test_recognizes_nil() { + XCTAssertEqual(LiteralValue(nil), .NULL) + } + + func test_recognizes_CURRENT_TIME() { + XCTAssertEqual(LiteralValue("CURRENT_TIME"), .CURRENT_TIME) + } + + func test_recognizes_CURRENT_TIMESTAMP() { + XCTAssertEqual(LiteralValue("CURRENT_TIMESTAMP"), .CURRENT_TIMESTAMP) + } + + func test_recognizes_CURRENT_DATE() { + XCTAssertEqual(LiteralValue("CURRENT_DATE"), .CURRENT_DATE) + } + + func test_recognizes_double_quote_string_literals() { + XCTAssertEqual(LiteralValue("\"foo\""), .stringLiteral("foo")) + } + + func test_recognizes_single_quote_string_literals() { + XCTAssertEqual(LiteralValue("\'foo\'"), .stringLiteral("foo")) + } + + func test_unquotes_double_quote_string_literals() { + XCTAssertEqual(LiteralValue("\"fo\"\"o\""), .stringLiteral("fo\"o")) + } + + func test_unquotes_single_quote_string_literals() { + XCTAssertEqual(LiteralValue("'fo''o'"), .stringLiteral("fo'o")) + } + + func test_recognizes_numeric_literals() { + XCTAssertEqual(LiteralValue("1.2"), .numericLiteral("1.2")) + XCTAssertEqual(LiteralValue("0xdeadbeef"), .numericLiteral("0xdeadbeef")) + } + + func test_recognizes_blob_literals() { + XCTAssertEqual(LiteralValue("X'deadbeef'"), .blobLiteral("deadbeef")) + XCTAssertEqual(LiteralValue("x'deadbeef'"), .blobLiteral("deadbeef")) + } + + func test_description_TRUE() { + XCTAssertEqual(LiteralValue.TRUE.description, "TRUE") + } + + func test_description_FALSE() { + XCTAssertEqual(LiteralValue.FALSE.description, "FALSE") + } + + func test_description_NULL() { + XCTAssertEqual(LiteralValue.NULL.description, "NULL") + } + + func test_description_CURRENT_TIME() { + XCTAssertEqual(LiteralValue.CURRENT_TIME.description, "CURRENT_TIME") + } + + func test_description_CURRENT_TIMESTAMP() { + XCTAssertEqual(LiteralValue.CURRENT_TIMESTAMP.description, "CURRENT_TIMESTAMP") + } + + func test_description_CURRENT_DATE() { + XCTAssertEqual(LiteralValue.CURRENT_DATE.description, "CURRENT_DATE") + } + + func test_description_string_literal() { + XCTAssertEqual(LiteralValue.stringLiteral("foo").description, "'foo'") + } + + func test_description_numeric_literal() { + XCTAssertEqual(LiteralValue.numericLiteral("1.2").description, "1.2") + XCTAssertEqual(LiteralValue.numericLiteral("0xdeadbeef").description, "0xdeadbeef") + } + + func test_description_blob_literal() { + XCTAssertEqual(LiteralValue.blobLiteral("deadbeef").description, "X'deadbeef'") + } +} diff --git a/Tests/SQLiteTests/Schema/SchemaReaderTests.swift b/Tests/SQLiteTests/Schema/SchemaReaderTests.swift new file mode 100644 index 00000000..8963070f --- /dev/null +++ b/Tests/SQLiteTests/Schema/SchemaReaderTests.swift @@ -0,0 +1,315 @@ +import XCTest +@testable import SQLite + +class SchemaReaderTests: SQLiteTestCase { + private var schemaReader: SchemaReader! + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + + schemaReader = db.schema + } + + func test_columnDefinitions() throws { + let columns = try schemaReader.columnDefinitions(table: "users") + XCTAssertEqual(columns, [ + ColumnDefinition(name: "id", + primaryKey: .init(autoIncrement: false, onConflict: nil), + type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "email", + primaryKey: nil, + type: .TEXT, + nullable: false, + unique: true, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "age", + primaryKey: nil, + type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "salary", + primaryKey: nil, + type: .REAL, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil), + ColumnDefinition(name: "admin", + primaryKey: nil, + type: .NUMERIC, + nullable: false, + unique: false, + defaultValue: .numericLiteral("0"), + references: nil), + ColumnDefinition(name: "manager_id", + primaryKey: nil, type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: .init(fromColumn: "manager_id", toTable: "users", toColumn: "id", onUpdate: nil, onDelete: nil)), + ColumnDefinition(name: "created_at", + primaryKey: nil, + type: .NUMERIC, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil) + ]) + } + + func test_columnDefinitions_parses_conflict_modifier() throws { + try db.run("CREATE TABLE t (\"id\" INTEGER PRIMARY KEY ON CONFLICT IGNORE AUTOINCREMENT)") + + XCTAssertEqual( + try schemaReader.columnDefinitions(table: "t"), [ + ColumnDefinition( + name: "id", + primaryKey: .init(autoIncrement: true, onConflict: .IGNORE), + type: .INTEGER, + nullable: true, + unique: false, + defaultValue: .NULL, + references: nil) + ] + ) + } + + func test_columnDefinitions_parses_unique() throws { + try db.run("CREATE TABLE t (name TEXT UNIQUE)") + + let columns = try schemaReader.columnDefinitions(table: "t") + XCTAssertEqual(columns, [ + ColumnDefinition( + name: "name", + primaryKey: nil, + type: .TEXT, + nullable: true, + unique: true, + defaultValue: .NULL, + references: nil) + ] + ) + } + + func test_columnDefinitions_detects_missing_autoincrement() throws { + try db.run("CREATE TABLE t (\"id\" INTEGER PRIMARY KEY)") + + XCTAssertEqual( + try schemaReader.columnDefinitions(table: "t"), [ + ColumnDefinition( + name: "id", + primaryKey: .init(autoIncrement: false), + type: .INTEGER, + nullable: true, + defaultValue: .NULL, + references: nil) + ] + ) + } + + func test_columnDefinitions_composite_primary_keys() throws { + try db.run(""" + CREATE TABLE t ( + col1 INTEGER, + col2 INTEGER, + col3 INTEGER, + PRIMARY KEY (col1, col2) + ); + """) + + XCTAssertEqual( + try schemaReader.columnDefinitions(table: "t"), [ + ColumnDefinition( + name: "col1", + primaryKey: .init(autoIncrement: false), + type: .INTEGER, + nullable: true, + defaultValue: .NULL, + references: nil), + ColumnDefinition( + name: "col2", + primaryKey: .init(autoIncrement: false), + type: .INTEGER, + nullable: true, + defaultValue: .NULL, + references: nil), + ColumnDefinition( + name: "col3", + primaryKey: nil, + type: .INTEGER, + nullable: true, + defaultValue: .NULL, + references: nil) + ] + ) + } + + func test_indexDefinitions_no_index() throws { + let indexes = try schemaReader.indexDefinitions(table: "users").filter { !$0.isInternal } + XCTAssertTrue(indexes.isEmpty) + } + + func test_indexDefinitions_with_index() throws { + try db.run("CREATE UNIQUE INDEX index_users ON users (age DESC) WHERE age IS NOT NULL") + let indexes = try schemaReader.indexDefinitions(table: "users").filter { !$0.isInternal } + + XCTAssertEqual(indexes, [ + IndexDefinition( + table: "users", + name: "index_users", + unique: true, + columns: ["age"], + where: "age IS NOT NULL", + orders: ["age": .DESC], + origin: .createIndex + ) + ]) + } + + func test_foreignKeys_info_empty() throws { + try db.run("CREATE TABLE t (\"id\" INTEGER PRIMARY KEY)") + + let foreignKeys = try schemaReader.foreignKeys(table: "t") + XCTAssertTrue(foreignKeys.isEmpty) + } + + func test_foreignKeys() throws { + let linkTable = Table("test_links") + + let idColumn = SQLite.Expression("id") + let testIdColumn = SQLite.Expression("test_id") + + try db.run(linkTable.create(block: { definition in + definition.column(idColumn, primaryKey: .autoincrement) + definition.column(testIdColumn, unique: false, check: nil, references: users, SQLite.Expression("id")) + })) + + let foreignKeys = try schemaReader.foreignKeys(table: "test_links") + XCTAssertEqual(foreignKeys, [ + .init(fromColumn: "test_id", toTable: "users", toColumn: "id", onUpdate: nil, onDelete: nil) + ]) + } + + func test_foreignKeys_references_column() throws { + let sql = """ + CREATE TABLE artist( + artistid INTEGER PRIMARY KEY, + artistname TEXT + ); + CREATE TABLE track( + trackid INTEGER, + trackname TEXT, + trackartist INTEGER REFERENCES artist(artistid) + ); + """ + try db.execute(sql) + let trackColumns = try db.schema.foreignKeys(table: "track") + XCTAssertEqual(trackColumns.map { $0.toSQL() }.joined(separator: "\n"), """ + REFERENCES "artist" ("artistid") + """) + } + + func test_foreignKeys_references_null_column() throws { + let sql = """ + CREATE TABLE artist( + artistid INTEGER PRIMARY KEY, + artistname TEXT + ); + CREATE TABLE track( + trackid INTEGER, + trackname TEXT, + trackartist INTEGER REFERENCES artist + ); + """ + try db.execute(sql) + let trackColumns = try db.schema.foreignKeys(table: "track") + XCTAssertEqual(trackColumns.map { $0.toSQL() }.joined(separator: "\n"), """ + REFERENCES "artist" + """) + } + + func test_tableDefinitions() throws { + let tables = try schemaReader.tableDefinitions() + XCTAssertEqual(tables.count, 1) + XCTAssertEqual(tables.first?.name, "users") + } + + func test_objectDefinitions() throws { + let tables = try schemaReader.objectDefinitions() + + XCTAssertEqual(tables.map { table in [table.name, table.tableName, table.type.rawValue]}, [ + ["users", "users", "table"], + ["sqlite_autoindex_users_1", "users", "index"] + ]) + } + + func test_objectDefinitions_temporary() throws { + let tables = try schemaReader.objectDefinitions(temp: true) + XCTAssert(tables.isEmpty) + + try db.run("CREATE TEMPORARY TABLE foo (bar TEXT)") + + let tables2 = try schemaReader.objectDefinitions(temp: true) + XCTAssertEqual(tables2.map { table in [table.name, table.tableName, table.type.rawValue]}, [ + ["foo", "foo", "table"] + ]) + } + + func test_objectDefinitions_indexes() throws { + let emailIndex = users.createIndex(SQLite.Expression("email"), unique: false, ifNotExists: true) + try db.run(emailIndex) + + let indexes = try schemaReader.objectDefinitions(type: .index) + .filter { !$0.isInternal } + + XCTAssertEqual(indexes.map { index in [index.name, index.tableName, index.type.rawValue, index.sql]}, [ + ["index_users_on_email", + "users", + "index", + "CREATE INDEX \"index_users_on_email\" ON \"users\" (\"email\")"] + ]) + } + + func test_objectDefinitions_triggers() throws { + let trigger = """ + CREATE TRIGGER test_trigger + AFTER INSERT ON users BEGIN + UPDATE USERS SET name = "update" WHERE id = NEW.rowid; + END; + """ + + try db.run(trigger) + + let triggers = try schemaReader.objectDefinitions(type: .trigger) + + XCTAssertEqual(triggers.map { trigger in [trigger.name, trigger.tableName, trigger.type.rawValue]}, [ + ["test_trigger", "users", "trigger"] + ]) + } + + func test_objectDefinitionsFilterByType() throws { + let tables = try schemaReader.objectDefinitions(type: .table) + + XCTAssertEqual(tables.map { table in [table.name, table.tableName, table.type.rawValue]}, [ + ["users", "users", "table"] + ]) + XCTAssertTrue((try schemaReader.objectDefinitions(type: .trigger)).isEmpty) + } + + func test_objectDefinitionsFilterByName() throws { + let tables = try schemaReader.objectDefinitions(name: "users") + + XCTAssertEqual(tables.map { table in [table.name, table.tableName, table.type.rawValue]}, [ + ["users", "users", "table"] + ]) + XCTAssertTrue((try schemaReader.objectDefinitions(name: "xxx")).isEmpty) + } +} diff --git a/SQLiteTests/SchemaTests.swift b/Tests/SQLiteTests/Schema/SchemaTests.swift similarity index 78% rename from SQLiteTests/SchemaTests.swift rename to Tests/SQLiteTests/Schema/SchemaTests.swift index 416b3646..4f4a49d1 100644 --- a/SQLiteTests/SchemaTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaTests.swift @@ -1,7 +1,7 @@ import XCTest import SQLite -class SchemaTests : XCTestCase { +class SchemaTests: XCTestCase { func test_drop_compilesDropTableExpression() { XCTAssertEqual("DROP TABLE \"table\"", table.drop()) @@ -53,6 +53,15 @@ class SchemaTests : XCTestCase { "CREATE TEMPORARY TABLE IF NOT EXISTS \"table\" (\"int64\" INTEGER NOT NULL)", table.create(temporary: true, ifNotExists: true) { $0.column(int64) } ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL) WITHOUT ROWID", + table.create(withoutRowid: true) { $0.column(int64) } + ) + XCTAssertEqual( + "CREATE TEMPORARY TABLE IF NOT EXISTS \"table\" (\"int64\" INTEGER NOT NULL) WITHOUT ROWID", + table.create(temporary: true, ifNotExists: true, withoutRowid: true) { $0.column(int64) } + ) } func test_create_withQuery_compilesCreateTableExpression() { @@ -63,6 +72,7 @@ class SchemaTests : XCTestCase { } // thoroughness test for ambiguity + // swiftlint:disable:next function_body_length func test_column_compilesColumnDefinitionExpression() { XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL)", @@ -283,15 +293,15 @@ class SchemaTests : XCTestCase { func test_column_withIntegerExpression_compilesPrimaryKeyAutoincrementColumnDefinitionExpression() { XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - table.create { t in t.column(int64, primaryKey: .Autoincrement) } + table.create { t in t.column(int64, primaryKey: .autoincrement) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (\"int64\" > 0))", - table.create { t in t.column(int64, primaryKey: .Autoincrement, check: int64 > 0) } + table.create { t in t.column(int64, primaryKey: .autoincrement, check: int64 > 0) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (\"int64Optional\" > 0))", - table.create { t in t.column(int64, primaryKey: .Autoincrement, check: int64Optional > 0) } + table.create { t in t.column(int64, primaryKey: .autoincrement, check: int64Optional > 0) } ) } @@ -308,6 +318,10 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64, unique: true, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64, check: int64 > 0, references: table, int64) } @@ -321,9 +335,23 @@ class SchemaTests : XCTestCase { table.create { t in t.column(int64, unique: true, check: int64 > 0, references: table, int64) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + """ + CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES + \"table\" (\"int64\")) + """.replacingOccurrences(of: "\n", with: ""), table.create { t in t.column(int64, unique: true, check: int64Optional > 0, references: table, int64) } ) + XCTAssertEqual( + """ + CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0) REFERENCES + \"table\" (\"int64\")) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64Optional\" INTEGER REFERENCES \"table\" (\"int64\"))", @@ -333,6 +361,10 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, unique: true, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER PRIMARY KEY REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, primaryKey: true, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, check: int64 > 0, references: table, int64) } @@ -345,181 +377,237 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER PRIMARY KEY CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, primaryKey: true, check: int64 > 0, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER PRIMARY KEY CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, primaryKey: true, check: int64Optional > 0, references: table, int64) } + ) } + // swiftlint:disable:next function_body_length func test_column_withStringExpression_compilesCollatedColumnDefinitionExpression() { XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL COLLATE RTRIM)", - table.create { t in t.column(string, collate: .Rtrim) } + table.create { t in t.column(string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, collate: .Rtrim) } + table.create { t in t.column(string, unique: true, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"string\" != '') COLLATE RTRIM)", - table.create { t in t.column(string, check: string != "", collate: .Rtrim) } + table.create { t in t.column(string, check: string != "", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') COLLATE RTRIM)", - table.create { t in t.column(string, check: stringOptional != "", collate: .Rtrim) } + table.create { t in t.column(string, check: stringOptional != "", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(string, defaultValue: string, collate: .Rtrim) } + table.create { t in t.column(string, defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(string, defaultValue: "string", collate: .Rtrim) } + table.create { t in t.column(string, defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, check: string != "", collate: .Rtrim) } + table.create { t in t.column(string, unique: true, check: string != "", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, check: stringOptional != "", collate: .Rtrim) } + table.create { t in t.column(string, unique: true, check: stringOptional != "", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, defaultValue: string, collate: .Rtrim) } + table.create { t in t.column(string, unique: true, defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, defaultValue: "string", collate: .Rtrim) } + table.create { t in t.column(string, unique: true, defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, check: string != "", defaultValue: string, collate: .Rtrim) } + table.create { t in t.column(string, unique: true, check: string != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, check: stringOptional != "", defaultValue: string, collate: .Rtrim) } + table.create { t in t.column(string, unique: true, check: stringOptional != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, check: string != "", defaultValue: "string", collate: .Rtrim) } + table.create { t in t.column(string, unique: true, check: string != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(string, unique: true, check: stringOptional != "", defaultValue: "string", collate: .Rtrim) } + table.create { t in t.column(string, unique: true, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(string, check: string != "", defaultValue: string, collate: .Rtrim) } + table.create { t in t.column(string, check: string != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(string, check: stringOptional != "", defaultValue: string, collate: .Rtrim) } + table.create { t in t.column(string, check: stringOptional != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(string, check: string != "", defaultValue: "string", collate: .Rtrim) } + table.create { t in t.column(string, check: string != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(string, check: stringOptional != "", defaultValue: "string", collate: .Rtrim) } + table.create { t in t.column(string, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL COLLATE RTRIM)", - table.create { t in t.column(stringOptional, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT COLLATE RTRIM)", + table.create { t in t.column(stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: string != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: stringOptional != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, defaultValue: string, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, defaultValue: stringOptional, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, defaultValue: "string", collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: string != "", collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: string != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, defaultValue: string, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, defaultValue: stringOptional, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, defaultValue: "string", collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: string, collate: .Rtrim) } + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: stringOptional, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, + unique: true, + check: string != "", + defaultValue: stringOptional, + collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: string, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') + DEFAULT (\"string\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", + defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: stringOptional, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", + defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: "string", collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: string != "", + defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: "string", collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", + defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", defaultValue: string, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') + DEFAULT (\"string\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: string != "", + defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: string, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') + DEFAULT (\"string\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", defaultValue: stringOptional, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: string != "", + defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: stringOptional, collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", defaultValue: "string", collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: string != "", + defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .Rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: "string", collate: .rtrim) } ) } @@ -536,6 +624,10 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\", \"double\"))", table.create { t in t.primaryKey(int64, string, double) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\", \"double\", \"date\"))", + table.create { t in t.primaryKey(int64, string, double, date) } + ) } func test_unique_compilesUniqueExpression() { @@ -568,7 +660,7 @@ class SchemaTests : XCTestCase { XCTAssertEqual( "CREATE TABLE \"table\" (FOREIGN KEY (\"string\") REFERENCES \"table\" (\"string\") ON UPDATE CASCADE ON DELETE SET NULL)", - table.create { t in t.foreignKey(string, references: table, string, update: .Cascade, delete: .SetNull) } + table.create { t in t.foreignKey(string, references: table, string, update: .cascade, delete: .setNull) } ) XCTAssertEqual( @@ -676,36 +768,36 @@ class SchemaTests : XCTestCase { func test_addColumn_withStringExpression_compilesCollatedAlterTableExpression() { XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"string\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM", - table.addColumn(string, defaultValue: "string", collate: .Rtrim) + table.addColumn(string, defaultValue: "string", collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"string\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM", - table.addColumn(string, check: string != "", defaultValue: "string", collate: .Rtrim) + table.addColumn(string, check: string != "", defaultValue: "string", collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM", - table.addColumn(string, check: stringOptional != "", defaultValue: "string", collate: .Rtrim) + table.addColumn(string, check: stringOptional != "", defaultValue: "string", collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT COLLATE RTRIM", - table.addColumn(stringOptional, collate: .Rtrim) + table.addColumn(stringOptional, collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"string\" != '') COLLATE RTRIM", - table.addColumn(stringOptional, check: string != "", collate: .Rtrim) + table.addColumn(stringOptional, check: string != "", collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"stringOptional\" != '') COLLATE RTRIM", - table.addColumn(stringOptional, check: stringOptional != "", collate: .Rtrim) + table.addColumn(stringOptional, check: stringOptional != "", collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM", - table.addColumn(stringOptional, check: string != "", defaultValue: "string", collate: .Rtrim) + table.addColumn(stringOptional, check: string != "", defaultValue: "string", collate: .rtrim) ) XCTAssertEqual( "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM", - table.addColumn(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .Rtrim) + table.addColumn(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .rtrim) ) } @@ -718,25 +810,25 @@ class SchemaTests : XCTestCase { XCTAssertEqual( "CREATE UNIQUE INDEX \"index_table_on_int64\" ON \"table\" (\"int64\")", - table.createIndex([int64], unique: true) + table.createIndex(int64, unique: true) ) XCTAssertEqual( "CREATE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", - table.createIndex([int64], ifNotExists: true) + table.createIndex(int64, ifNotExists: true) ) XCTAssertEqual( "CREATE UNIQUE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", - table.createIndex([int64], unique: true, ifNotExists: true) + table.createIndex(int64, unique: true, ifNotExists: true) ) XCTAssertEqual( "CREATE UNIQUE INDEX IF NOT EXISTS \"main\".\"index_table_on_int64\" ON \"table\" (\"int64\")", - qualifiedTable.createIndex([int64], unique: true, ifNotExists: true) + qualifiedTable.createIndex(int64, unique: true, ifNotExists: true) ) } func test_dropIndex_compilesCreateIndexExpression() { XCTAssertEqual("DROP INDEX \"index_table_on_int64\"", table.dropIndex(int64)) - XCTAssertEqual("DROP INDEX IF EXISTS \"index_table_on_int64\"", table.dropIndex([int64], ifExists: true)) + XCTAssertEqual("DROP INDEX IF EXISTS \"index_table_on_int64\"", table.dropIndex(int64, ifExists: true)) } func test_create_onView_compilesCreateViewExpression() { diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift new file mode 100644 index 00000000..e62b2a23 --- /dev/null +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -0,0 +1,167 @@ +import XCTest +@testable import SQLite + +class SQLiteTestCase: XCTestCase { + private var trace: [String: Int]! + var db: Connection! + let users = Table("users") + + override func setUpWithError() throws { + try super.setUpWithError() + db = try Connection() + trace = [String: Int]() + + db.trace { SQL in + // print("SQL: \(SQL)") + self.trace[SQL, default: 0] += 1 + } + } + + func createUsersTable() throws { + try db.execute(""" + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + age INTEGER, + salary REAL, + admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), + manager_id INTEGER, + created_at DATETIME, + FOREIGN KEY(manager_id) REFERENCES users(id) + ) + """ + ) + } + + func insertUsers(_ names: String...) throws { + try insertUsers(names) + } + + func insertUsers(_ names: [String]) throws { + for name in names { try insertUser(name) } + } + + @discardableResult func insertUser(_ name: String, age: Int? = nil, admin: Bool = false) throws -> Statement { + try db.run("INSERT INTO \"users\" (email, age, admin) values (?, ?, ?)", + "\(name)@example.com", age?.datatypeValue, admin.datatypeValue) + } + + func assertSQL(_ SQL: String, _ executions: Int = 1, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual( + executions, trace[SQL] ?? 0, + message ?? SQL, + file: file, line: line + ) + } + + func assertSQL(_ SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) throws { + try statement.run() + assertSQL(SQL, 1, message, file: file, line: line) + if let count = trace[SQL] { trace[SQL] = count - 1 } + } + +// func AssertSQL(SQL: String, _ query: Query, _ message: String? = nil, file: String = __FILE__, line: UInt = __LINE__) { +// for _ in query {} +// AssertSQL(SQL, 1, message, file: file, line: line) +// if let count = trace[SQL] { trace[SQL] = count - 1 } +// } + + func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) throws -> Void) throws { + let expectation = self.expectation(description: description) + try block({ expectation.fulfill() }) + waitForExpectations(timeout: timeout, handler: nil) + } + +} + +let bool = SQLite.Expression("bool") +let boolOptional = SQLite.Expression("boolOptional") + +let data = SQLite.Expression("blob") +let dataOptional = SQLite.Expression("blobOptional") + +let date = SQLite.Expression("date") +let dateOptional = SQLite.Expression("dateOptional") + +let double = SQLite.Expression("double") +let doubleOptional = SQLite.Expression("doubleOptional") + +let int = SQLite.Expression("int") +let intOptional = SQLite.Expression("intOptional") + +let int64 = SQLite.Expression("int64") +let int64Optional = SQLite.Expression("int64Optional") + +let string = SQLite.Expression("string") +let stringOptional = SQLite.Expression("stringOptional") + +let uuid = SQLite.Expression("uuid") +let uuidOptional = SQLite.Expression("uuidOptional") + +let testUUIDValue = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")! + +func assertSQL(_ expression1: @autoclosure () -> String, _ expression2: @autoclosure () -> Expressible, + file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(expression1(), expression2().asSQL(), file: file, line: line) +} + +func extractAndReplace(_ value: String, regex: String, with replacement: String) -> (String, String) { + // We cannot use `Regex` because it is not available before iOS 16 :( + let regex = try! NSRegularExpression(pattern: regex) + let valueRange = NSRange(location: 0, length: value.utf16.count) + let match = regex.firstMatch(in: value, options: [], range: valueRange)!.range + let range = Range(match, in: value)! + let extractedValue = String(value[range]) + return (value.replacingCharacters(in: range, with: replacement), extractedValue) +} + +let table = Table("table") +let qualifiedTable = Table("table", database: "main") +let virtualTable = VirtualTable("virtual_table") +let _view = View("view") // avoid Mac XCTestCase collision + +class TestCodable: Codable, Equatable { + let int: Int + let string: String + let bool: Bool + let float: Float + let double: Double + let date: Date + let uuid: UUID + let optional: String? + let sub: TestCodable? + + init(int: Int, string: String, bool: Bool, float: Float, double: Double, date: Date, uuid: UUID, optional: String?, sub: TestCodable?) { + self.int = int + self.string = string + self.bool = bool + self.float = float + self.double = double + self.date = date + self.uuid = uuid + self.optional = optional + self.sub = sub + } + + static func == (lhs: TestCodable, rhs: TestCodable) -> Bool { + lhs.int == rhs.int && + lhs.string == rhs.string && + lhs.bool == rhs.bool && + lhs.float == rhs.float && + lhs.double == rhs.double && + lhs.date == rhs.date && + lhs.uuid == lhs.uuid && + lhs.optional == rhs.optional && + lhs.sub == rhs.sub + } +} + +struct TestOptionalCodable: Codable, Equatable { + let int: Int? + let string: String? + let bool: Bool? + let float: Float? + let double: Double? + let date: Date? + let uuid: UUID? +} diff --git a/Tests/SQLiteTests/Typed/AggregateFunctionsTests.swift b/Tests/SQLiteTests/Typed/AggregateFunctionsTests.swift new file mode 100644 index 00000000..71ac79fb --- /dev/null +++ b/Tests/SQLiteTests/Typed/AggregateFunctionsTests.swift @@ -0,0 +1,68 @@ +import XCTest +import SQLite + +class AggregateFunctionsTests: XCTestCase { + + func test_distinct_prependsExpressionsWithDistinctKeyword() { + assertSQL("DISTINCT \"int\"", int.distinct) + assertSQL("DISTINCT \"intOptional\"", intOptional.distinct) + assertSQL("DISTINCT \"double\"", double.distinct) + assertSQL("DISTINCT \"doubleOptional\"", doubleOptional.distinct) + assertSQL("DISTINCT \"string\"", string.distinct) + assertSQL("DISTINCT \"stringOptional\"", stringOptional.distinct) + } + + func test_count_wrapsOptionalExpressionsWithCountFunction() { + assertSQL("count(\"intOptional\")", intOptional.count) + assertSQL("count(\"doubleOptional\")", doubleOptional.count) + assertSQL("count(\"stringOptional\")", stringOptional.count) + } + + func test_max_wrapsComparableExpressionsWithMaxFunction() { + assertSQL("max(\"int\")", int.max) + assertSQL("max(\"intOptional\")", intOptional.max) + assertSQL("max(\"double\")", double.max) + assertSQL("max(\"doubleOptional\")", doubleOptional.max) + assertSQL("max(\"string\")", string.max) + assertSQL("max(\"stringOptional\")", stringOptional.max) + assertSQL("max(\"date\")", date.max) + assertSQL("max(\"dateOptional\")", dateOptional.max) + } + + func test_min_wrapsComparableExpressionsWithMinFunction() { + assertSQL("min(\"int\")", int.min) + assertSQL("min(\"intOptional\")", intOptional.min) + assertSQL("min(\"double\")", double.min) + assertSQL("min(\"doubleOptional\")", doubleOptional.min) + assertSQL("min(\"string\")", string.min) + assertSQL("min(\"stringOptional\")", stringOptional.min) + assertSQL("min(\"date\")", date.min) + assertSQL("min(\"dateOptional\")", dateOptional.min) + } + + func test_average_wrapsNumericExpressionsWithAvgFunction() { + assertSQL("avg(\"int\")", int.average) + assertSQL("avg(\"intOptional\")", intOptional.average) + assertSQL("avg(\"double\")", double.average) + assertSQL("avg(\"doubleOptional\")", doubleOptional.average) + } + + func test_sum_wrapsNumericExpressionsWithSumFunction() { + assertSQL("sum(\"int\")", int.sum) + assertSQL("sum(\"intOptional\")", intOptional.sum) + assertSQL("sum(\"double\")", double.sum) + assertSQL("sum(\"doubleOptional\")", doubleOptional.sum) + } + + func test_total_wrapsNumericExpressionsWithTotalFunction() { + assertSQL("total(\"int\")", int.total) + assertSQL("total(\"intOptional\")", intOptional.total) + assertSQL("total(\"double\")", double.total) + assertSQL("total(\"doubleOptional\")", doubleOptional.total) + } + + func test_count_withStar_wrapsStarWithCountFunction() { + assertSQL("count(*)", count(*)) + } + +} diff --git a/Tests/SQLiteTests/Typed/CustomAggregationTests.swift b/Tests/SQLiteTests/Typed/CustomAggregationTests.swift new file mode 100644 index 00000000..b052d236 --- /dev/null +++ b/Tests/SQLiteTests/Typed/CustomAggregationTests.swift @@ -0,0 +1,162 @@ +import XCTest +import Foundation +import Dispatch +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +// https://github.com/stephencelis/SQLite.swift/issues/1071 +#if !(os(Linux) || os(Android)) + +class CustomAggregationTests: SQLiteTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + try insertUser("Alice", age: 30, admin: true) + try insertUser("Bob", age: 25, admin: true) + try insertUser("Eve", age: 28, admin: false) + } + + func testUnsafeCustomSum() throws { + let step = { (bindings: [Binding?], state: UnsafeMutablePointer) in + if let v = bindings[0] as? Int64 { + state.pointee += v + } + } + + let final = { (state: UnsafeMutablePointer) -> Binding? in + let v = state.pointee + let p = UnsafeMutableBufferPointer(start: state, count: 1) + p.deallocate() + return v + } + db.createAggregation("mySUM1", step: step, final: final) { + let v = UnsafeMutableBufferPointer.allocate(capacity: 1) + v[0] = 0 + return v.baseAddress! + } + let result = try db.prepare("SELECT mySUM1(age) AS s FROM users") + let i = result.columnNames.firstIndex(of: "s")! + for row in result { + let value = row[i] as? Int64 + XCTAssertEqual(83, value) + } + } + + func testUnsafeCustomSumGrouping() throws { + let step = { (bindings: [Binding?], state: UnsafeMutablePointer) in + if let v = bindings[0] as? Int64 { + state.pointee += v + } + } + let final = { (state: UnsafeMutablePointer) -> Binding? in + let v = state.pointee + let p = UnsafeMutableBufferPointer(start: state, count: 1) + p.deallocate() + return v + } + db.createAggregation("mySUM2", step: step, final: final) { + let v = UnsafeMutableBufferPointer.allocate(capacity: 1) + v[0] = 0 + return v.baseAddress! + } + let result = try db.prepare("SELECT mySUM2(age) AS s FROM users GROUP BY admin ORDER BY s") + let i = result.columnNames.firstIndex(of: "s")! + let values = result.compactMap { $0[i] as? Int64 } + XCTAssertTrue(values.elementsEqual([28, 55])) + } + + func testCustomSum() throws { + let reduce: (Int64, [Binding?]) -> Int64 = { (last, bindings) in + let v = (bindings[0] as? Int64) ?? 0 + return last + v + } + db.createAggregation("myReduceSUM1", initialValue: Int64(2000), reduce: reduce, result: { $0 }) + let result = try db.prepare("SELECT myReduceSUM1(age) AS s FROM users") + let i = result.columnNames.firstIndex(of: "s")! + for row in result { + let value = row[i] as? Int64 + XCTAssertEqual(2083, value) + } + } + + func testCustomSumGrouping() throws { + let reduce: (Int64, [Binding?]) -> Int64 = { (last, bindings) in + let v = (bindings[0] as? Int64) ?? 0 + return last + v + } + db.createAggregation("myReduceSUM2", initialValue: Int64(3000), reduce: reduce, result: { $0 }) + let result = try db.prepare("SELECT myReduceSUM2(age) AS s FROM users GROUP BY admin ORDER BY s") + let i = result.columnNames.firstIndex(of: "s")! + let values = result.compactMap { $0[i] as? Int64 } + XCTAssertTrue(values.elementsEqual([3028, 3055])) + } + + func testCustomStringAgg() throws { + let initial = String(repeating: " ", count: 64) + let reduce: (String, [Binding?]) -> String = { (last, bindings) in + let v = (bindings[0] as? String) ?? "" + return last + v + } + db.createAggregation("myReduceSUM3", initialValue: initial, reduce: reduce, result: { $0 }) + let result = try db.prepare("SELECT myReduceSUM3(email) AS s FROM users") + + let i = result.columnNames.firstIndex(of: "s")! + for row in result { + let value = row[i] as? String + XCTAssertEqual("\(initial)Alice@example.comBob@example.comEve@example.com", value) + } + } + + func testCustomObjectSum() throws { + { + let initial = TestObject(value: 1000) + let reduce: (TestObject, [Binding?]) -> TestObject = { (last, bindings) in + let v = (bindings[0] as? Int64) ?? 0 + return TestObject(value: last.value + v) + } + db.createAggregation("myReduceSUMX", initialValue: initial, reduce: reduce, result: { $0.value }) + // end this scope to ensure that the initial value is retained + // by the createAggregation call. + // swiftlint:disable:next trailing_semicolon + }(); + + { + XCTAssertEqual(TestObject.inits, 1) + let result = try! db.prepare("SELECT myReduceSUMX(age) AS s FROM users") + let i = result.columnNames.firstIndex(of: "s")! + for row in result { + let value = row[i] as? Int64 + XCTAssertEqual(1083, value) + } + }() + XCTAssertEqual(TestObject.inits, 4) + XCTAssertEqual(TestObject.deinits, 3) // the initial value is still retained by the aggregate's state block, so deinits is one less than inits + } +} +#endif + +/// This class is used to test that aggregation state variables +/// can be reference types and are properly memory managed when +/// crossing the Swift<->C boundary multiple times. +class TestObject { + static var inits = 0 + static var deinits = 0 + + var value: Int64 + init(value: Int64) { + self.value = value + TestObject.inits += 1 + } + deinit { + TestObject.deinits += 1 + } +} diff --git a/Tests/SQLiteTests/Typed/CustomFunctionsTests.swift b/Tests/SQLiteTests/Typed/CustomFunctionsTests.swift new file mode 100644 index 00000000..fd9ae6b9 --- /dev/null +++ b/Tests/SQLiteTests/Typed/CustomFunctionsTests.swift @@ -0,0 +1,151 @@ +import XCTest +import SQLite + +// https://github.com/stephencelis/SQLite.swift/issues/1071 +#if !(os(Linux) || os(Android)) + +class CustomFunctionNoArgsTests: SQLiteTestCase { + typealias FunctionNoOptional = () -> SQLite.Expression + typealias FunctionResultOptional = () -> SQLite.Expression + + func testFunctionNoOptional() throws { + let _: FunctionNoOptional = try db.createFunction("test", deterministic: true) { + "a" + } + let result = try db.prepare("SELECT test()").scalar() as! String + XCTAssertEqual("a", result) + } + + func testFunctionResultOptional() throws { + let _: FunctionResultOptional = try db.createFunction("test", deterministic: true) { + "a" + } + let result = try db.prepare("SELECT test()").scalar() as! String? + XCTAssertEqual("a", result) + } +} + +class CustomFunctionWithOneArgTests: SQLiteTestCase { + typealias FunctionNoOptional = (SQLite.Expression) -> SQLite.Expression + typealias FunctionLeftOptional = (SQLite.Expression) -> SQLite.Expression + typealias FunctionResultOptional = (SQLite.Expression) -> SQLite.Expression + typealias FunctionLeftResultOptional = (SQLite.Expression) -> SQLite.Expression + + func testFunctionNoOptional() throws { + let _: FunctionNoOptional = try db.createFunction("test", deterministic: true) { a in + "b" + a + } + let result = try db.prepare("SELECT test(?)").scalar("a") as! String + XCTAssertEqual("ba", result) + } + + func testFunctionLeftOptional() throws { + let _: FunctionLeftOptional = try db.createFunction("test", deterministic: true) { a in + "b" + a! + } + let result = try db.prepare("SELECT test(?)").scalar("a") as! String + XCTAssertEqual("ba", result) + } + + func testFunctionResultOptional() throws { + let _: FunctionResultOptional = try db.createFunction("test", deterministic: true) { a in + "b" + a + } + let result = try db.prepare("SELECT test(?)").scalar("a") as! String + XCTAssertEqual("ba", result) + } + + func testFunctionLeftResultOptional() throws { + let _: FunctionLeftResultOptional = try db.createFunction("test", deterministic: true) { (a: String?) -> String? in + "b" + a! + } + let result = try db.prepare("SELECT test(?)").scalar("a") as! String + XCTAssertEqual("ba", result) + } +} + +class CustomFunctionWithTwoArgsTests: SQLiteTestCase { + typealias FunctionNoOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionLeftOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionRightOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionResultOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionLeftRightOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionLeftResultOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionRightResultOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + typealias FunctionLeftRightResultOptional = (SQLite.Expression, SQLite.Expression) -> SQLite.Expression + + func testNoOptional() throws { + let _: FunctionNoOptional = try db.createFunction("test", deterministic: true) { a, b in + a + b + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + XCTAssertEqual("ab", result) + } + + func testLeftOptional() throws { + let _: FunctionLeftOptional = try db.createFunction("test", deterministic: true) { a, b in + a! + b + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + XCTAssertEqual("ab", result) + } + + func testRightOptional() throws { + let _: FunctionRightOptional = try db.createFunction("test", deterministic: true) { a, b in + a + b! + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + XCTAssertEqual("ab", result) + } + + func testResultOptional() throws { + let _: FunctionResultOptional = try db.createFunction("test", deterministic: true) { a, b in + a + b + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + XCTAssertEqual("ab", result) + } + + func testFunctionLeftRightOptional() throws { + let _: FunctionLeftRightOptional = try db.createFunction("test", deterministic: true) { a, b in + a! + b! + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + XCTAssertEqual("ab", result) + } + + func testFunctionLeftResultOptional() throws { + let _: FunctionLeftResultOptional = try db.createFunction("test", deterministic: true) { a, b in + a! + b + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + XCTAssertEqual("ab", result) + } + + func testFunctionRightResultOptional() throws { + let _: FunctionRightResultOptional = try db.createFunction("test", deterministic: true) { a, b in + a + b! + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + XCTAssertEqual("ab", result) + } + + func testFunctionLeftRightResultOptional() throws { + let _: FunctionLeftRightResultOptional = try db.createFunction("test", deterministic: true) { a, b in + a! + b! + } + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + XCTAssertEqual("ab", result) + } +} + +class CustomFunctionTruncation: SQLiteTestCase { + // https://github.com/stephencelis/SQLite.swift/issues/468 + func testStringTruncation() throws { + _ = try db.createFunction("customLower") { (value: String) in value.lowercased() } + let result = try db.prepare("SELECT customLower(?)").scalar("TÖL-AA 12") as? String + XCTAssertEqual("töl-aa 12", result) + } +} + +#endif diff --git a/Tests/SQLiteTests/Typed/DateAndTimeFunctionTests.swift b/Tests/SQLiteTests/Typed/DateAndTimeFunctionTests.swift new file mode 100644 index 00000000..393e9c7c --- /dev/null +++ b/Tests/SQLiteTests/Typed/DateAndTimeFunctionTests.swift @@ -0,0 +1,66 @@ +import XCTest +@testable import SQLite + +class DateAndTimeFunctionsTests: XCTestCase { + + func test_date() { + assertSQL("date('now')", DateFunctions.date("now")) + assertSQL("date('now', 'localtime')", DateFunctions.date("now", "localtime")) + } + + func test_time() { + assertSQL("time('now')", DateFunctions.time("now")) + assertSQL("time('now', 'localtime')", DateFunctions.time("now", "localtime")) + } + + func test_datetime() { + assertSQL("datetime('now')", DateFunctions.datetime("now")) + assertSQL("datetime('now', 'localtime')", DateFunctions.datetime("now", "localtime")) + } + + func test_julianday() { + assertSQL("julianday('now')", DateFunctions.julianday("now")) + assertSQL("julianday('now', 'localtime')", DateFunctions.julianday("now", "localtime")) + } + + func test_strftime() { + assertSQL("strftime('%Y-%m-%d', 'now')", DateFunctions.strftime("%Y-%m-%d", "now")) + assertSQL("strftime('%Y-%m-%d', 'now', 'localtime')", DateFunctions.strftime("%Y-%m-%d", "now", "localtime")) + } +} + +class DateExtensionTests: XCTestCase { + func test_time() { + assertSQL("time('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).time) + } + + func test_date() { + assertSQL("date('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).date) + } + + func test_datetime() { + assertSQL("datetime('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).datetime) + } + + func test_julianday() { + assertSQL("julianday('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).julianday) + } +} + +class DateExpressionTests: XCTestCase { + func test_date() { + assertSQL("date(\"date\")", date.date) + } + + func test_time() { + assertSQL("time(\"date\")", date.time) + } + + func test_datetime() { + assertSQL("datetime(\"date\")", date.datetime) + } + + func test_julianday() { + assertSQL("julianday(\"date\")", date.julianday) + } +} diff --git a/Tests/SQLiteTests/Typed/ExpressionTests.swift b/Tests/SQLiteTests/Typed/ExpressionTests.swift new file mode 100644 index 00000000..1b97fe94 --- /dev/null +++ b/Tests/SQLiteTests/Typed/ExpressionTests.swift @@ -0,0 +1,35 @@ +import XCTest +@testable import SQLite + +class ExpressionTests: XCTestCase { + + func test_asSQL_expression_bindings() { + let expression = SQLite.Expression("foo ? bar", ["baz"]) + XCTAssertEqual(expression.asSQL(), "foo 'baz' bar") + } + + func test_asSQL_expression_bindings_quoting() { + let expression = SQLite.Expression("foo ? bar", ["'baz'"]) + XCTAssertEqual(expression.asSQL(), "foo '''baz''' bar") + } + + func test_expression_custom_string_convertible() { + let expression = SQLite.Expression("foo ? bar", ["baz"]) + XCTAssertEqual(expression.asSQL(), expression.description) + } + + func test_builtin_unambiguously_custom_string_convertible() { + let integer: Int = 45 + XCTAssertEqual(integer.description, "45") + } + + func test_init_literal() { + let expression = SQLite.Expression(literal: "literal") + XCTAssertEqual(expression.template, "literal") + } + + func test_init_identifier() { + let expression = SQLite.Expression("identifier") + XCTAssertEqual(expression.template, "\"identifier\"") + } +} diff --git a/Tests/SQLiteTests/Typed/OperatorsTests.swift b/Tests/SQLiteTests/Typed/OperatorsTests.swift new file mode 100644 index 00000000..7a5d83da --- /dev/null +++ b/Tests/SQLiteTests/Typed/OperatorsTests.swift @@ -0,0 +1,390 @@ +import XCTest +import SQLite + +class OperatorsTests: XCTestCase { + + func test_stringExpressionPlusStringExpression_buildsConcatenatingStringExpression() { + assertSQL("(\"string\" || \"string\")", string + string) + assertSQL("(\"string\" || \"stringOptional\")", string + stringOptional) + assertSQL("(\"stringOptional\" || \"string\")", stringOptional + string) + assertSQL("(\"stringOptional\" || \"stringOptional\")", stringOptional + stringOptional) + assertSQL("(\"string\" || 'literal')", string + "literal") + assertSQL("(\"stringOptional\" || 'literal')", stringOptional + "literal") + assertSQL("('literal' || \"string\")", "literal" + string) + assertSQL("('literal' || \"stringOptional\")", "literal" + stringOptional) + } + + func test_numberExpression_plusNumberExpression_buildsAdditiveNumberExpression() { + assertSQL("(\"int\" + \"int\")", int + int) + assertSQL("(\"int\" + \"intOptional\")", int + intOptional) + assertSQL("(\"intOptional\" + \"int\")", intOptional + int) + assertSQL("(\"intOptional\" + \"intOptional\")", intOptional + intOptional) + assertSQL("(\"int\" + 1)", int + 1) + assertSQL("(\"intOptional\" + 1)", intOptional + 1) + assertSQL("(1 + \"int\")", 1 + int) + assertSQL("(1 + \"intOptional\")", 1 + intOptional) + + assertSQL("(\"double\" + \"double\")", double + double) + assertSQL("(\"double\" + \"doubleOptional\")", double + doubleOptional) + assertSQL("(\"doubleOptional\" + \"double\")", doubleOptional + double) + assertSQL("(\"doubleOptional\" + \"doubleOptional\")", doubleOptional + doubleOptional) + assertSQL("(\"double\" + 1.0)", double + 1) + assertSQL("(\"doubleOptional\" + 1.0)", doubleOptional + 1) + assertSQL("(1.0 + \"double\")", 1 + double) + assertSQL("(1.0 + \"doubleOptional\")", 1 + doubleOptional) + } + + func test_numberExpression_minusNumberExpression_buildsSubtractiveNumberExpression() { + assertSQL("(\"int\" - \"int\")", int - int) + assertSQL("(\"int\" - \"intOptional\")", int - intOptional) + assertSQL("(\"intOptional\" - \"int\")", intOptional - int) + assertSQL("(\"intOptional\" - \"intOptional\")", intOptional - intOptional) + assertSQL("(\"int\" - 1)", int - 1) + assertSQL("(\"intOptional\" - 1)", intOptional - 1) + assertSQL("(1 - \"int\")", 1 - int) + assertSQL("(1 - \"intOptional\")", 1 - intOptional) + + assertSQL("(\"double\" - \"double\")", double - double) + assertSQL("(\"double\" - \"doubleOptional\")", double - doubleOptional) + assertSQL("(\"doubleOptional\" - \"double\")", doubleOptional - double) + assertSQL("(\"doubleOptional\" - \"doubleOptional\")", doubleOptional - doubleOptional) + assertSQL("(\"double\" - 1.0)", double - 1) + assertSQL("(\"doubleOptional\" - 1.0)", doubleOptional - 1) + assertSQL("(1.0 - \"double\")", 1 - double) + assertSQL("(1.0 - \"doubleOptional\")", 1 - doubleOptional) + } + + func test_numberExpression_timesNumberExpression_buildsMultiplicativeNumberExpression() { + assertSQL("(\"int\" * \"int\")", int * int) + assertSQL("(\"int\" * \"intOptional\")", int * intOptional) + assertSQL("(\"intOptional\" * \"int\")", intOptional * int) + assertSQL("(\"intOptional\" * \"intOptional\")", intOptional * intOptional) + assertSQL("(\"int\" * 1)", int * 1) + assertSQL("(\"intOptional\" * 1)", intOptional * 1) + assertSQL("(1 * \"int\")", 1 * int) + assertSQL("(1 * \"intOptional\")", 1 * intOptional) + + assertSQL("(\"double\" * \"double\")", double * double) + assertSQL("(\"double\" * \"doubleOptional\")", double * doubleOptional) + assertSQL("(\"doubleOptional\" * \"double\")", doubleOptional * double) + assertSQL("(\"doubleOptional\" * \"doubleOptional\")", doubleOptional * doubleOptional) + assertSQL("(\"double\" * 1.0)", double * 1) + assertSQL("(\"doubleOptional\" * 1.0)", doubleOptional * 1) + assertSQL("(1.0 * \"double\")", 1 * double) + assertSQL("(1.0 * \"doubleOptional\")", 1 * doubleOptional) + } + + func test_numberExpression_dividedByNumberExpression_buildsDivisiveNumberExpression() { + assertSQL("(\"int\" / \"int\")", int / int) + assertSQL("(\"int\" / \"intOptional\")", int / intOptional) + assertSQL("(\"intOptional\" / \"int\")", intOptional / int) + assertSQL("(\"intOptional\" / \"intOptional\")", intOptional / intOptional) + assertSQL("(\"int\" / 1)", int / 1) + assertSQL("(\"intOptional\" / 1)", intOptional / 1) + assertSQL("(1 / \"int\")", 1 / int) + assertSQL("(1 / \"intOptional\")", 1 / intOptional) + + assertSQL("(\"double\" / \"double\")", double / double) + assertSQL("(\"double\" / \"doubleOptional\")", double / doubleOptional) + assertSQL("(\"doubleOptional\" / \"double\")", doubleOptional / double) + assertSQL("(\"doubleOptional\" / \"doubleOptional\")", doubleOptional / doubleOptional) + assertSQL("(\"double\" / 1.0)", double / 1) + assertSQL("(\"doubleOptional\" / 1.0)", doubleOptional / 1) + assertSQL("(1.0 / \"double\")", 1 / double) + assertSQL("(1.0 / \"doubleOptional\")", 1 / doubleOptional) + } + + func test_numberExpression_prefixedWithMinus_buildsInvertedNumberExpression() { + assertSQL("-(\"int\")", -int) + assertSQL("-(\"intOptional\")", -intOptional) + + assertSQL("-(\"double\")", -double) + assertSQL("-(\"doubleOptional\")", -doubleOptional) + } + + func test_integerExpression_moduloIntegerExpression_buildsModuloIntegerExpression() { + assertSQL("(\"int\" % \"int\")", int % int) + assertSQL("(\"int\" % \"intOptional\")", int % intOptional) + assertSQL("(\"intOptional\" % \"int\")", intOptional % int) + assertSQL("(\"intOptional\" % \"intOptional\")", intOptional % intOptional) + assertSQL("(\"int\" % 1)", int % 1) + assertSQL("(\"intOptional\" % 1)", intOptional % 1) + assertSQL("(1 % \"int\")", 1 % int) + assertSQL("(1 % \"intOptional\")", 1 % intOptional) + } + + func test_integerExpression_bitShiftLeftIntegerExpression_buildsLeftShiftedIntegerExpression() { + assertSQL("(\"int\" << \"int\")", int << int) + assertSQL("(\"int\" << \"intOptional\")", int << intOptional) + assertSQL("(\"intOptional\" << \"int\")", intOptional << int) + assertSQL("(\"intOptional\" << \"intOptional\")", intOptional << intOptional) + assertSQL("(\"int\" << 1)", int << 1) + assertSQL("(\"intOptional\" << 1)", intOptional << 1) + assertSQL("(1 << \"int\")", 1 << int) + assertSQL("(1 << \"intOptional\")", 1 << intOptional) + } + + func test_integerExpression_bitShiftRightIntegerExpression_buildsRightShiftedIntegerExpression() { + assertSQL("(\"int\" >> \"int\")", int >> int) + assertSQL("(\"int\" >> \"intOptional\")", int >> intOptional) + assertSQL("(\"intOptional\" >> \"int\")", intOptional >> int) + assertSQL("(\"intOptional\" >> \"intOptional\")", intOptional >> intOptional) + assertSQL("(\"int\" >> 1)", int >> 1) + assertSQL("(\"intOptional\" >> 1)", intOptional >> 1) + assertSQL("(1 >> \"int\")", 1 >> int) + assertSQL("(1 >> \"intOptional\")", 1 >> intOptional) + } + + func test_integerExpression_bitwiseAndIntegerExpression_buildsAndedIntegerExpression() { + assertSQL("(\"int\" & \"int\")", int & int) + assertSQL("(\"int\" & \"intOptional\")", int & intOptional) + assertSQL("(\"intOptional\" & \"int\")", intOptional & int) + assertSQL("(\"intOptional\" & \"intOptional\")", intOptional & intOptional) + assertSQL("(\"int\" & 1)", int & 1) + assertSQL("(\"intOptional\" & 1)", intOptional & 1) + assertSQL("(1 & \"int\")", 1 & int) + assertSQL("(1 & \"intOptional\")", 1 & intOptional) + } + + func test_integerExpression_bitwiseOrIntegerExpression_buildsOredIntegerExpression() { + assertSQL("(\"int\" | \"int\")", int | int) + assertSQL("(\"int\" | \"intOptional\")", int | intOptional) + assertSQL("(\"intOptional\" | \"int\")", intOptional | int) + assertSQL("(\"intOptional\" | \"intOptional\")", intOptional | intOptional) + assertSQL("(\"int\" | 1)", int | 1) + assertSQL("(\"intOptional\" | 1)", intOptional | 1) + assertSQL("(1 | \"int\")", 1 | int) + assertSQL("(1 | \"intOptional\")", 1 | intOptional) + } + + func test_integerExpression_bitwiseExclusiveOrIntegerExpression_buildsOredIntegerExpression() { + assertSQL("(~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^ int) + assertSQL("(~((\"int\" & \"intOptional\")) & (\"int\" | \"intOptional\"))", int ^ intOptional) + assertSQL("(~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^ int) + assertSQL("(~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^ intOptional) + assertSQL("(~((\"int\" & 1)) & (\"int\" | 1))", int ^ 1) + assertSQL("(~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^ 1) + assertSQL("(~((1 & \"int\")) & (1 | \"int\"))", 1 ^ int) + assertSQL("(~((1 & \"intOptional\")) & (1 | \"intOptional\"))", 1 ^ intOptional) + } + + func test_bitwiseNot_integerExpression_buildsComplementIntegerExpression() { + assertSQL("~(\"int\")", ~int) + assertSQL("~(\"intOptional\")", ~intOptional) + } + + func test_equalityOperator_withEquatableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" = \"bool\")", bool == bool) + assertSQL("(\"bool\" = \"boolOptional\")", bool == boolOptional) + assertSQL("(\"boolOptional\" = \"bool\")", boolOptional == bool) + assertSQL("(\"boolOptional\" = \"boolOptional\")", boolOptional == boolOptional) + assertSQL("(\"bool\" = 1)", bool == true) + assertSQL("(\"boolOptional\" = 1)", boolOptional == true) + assertSQL("(1 = \"bool\")", true == bool) + assertSQL("(1 = \"boolOptional\")", true == boolOptional) + + assertSQL("(\"boolOptional\" IS NULL)", boolOptional == nil) + assertSQL("(NULL IS \"boolOptional\")", nil == boolOptional) + } + + func test_isOperator_withEquatableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" IS \"bool\")", bool === bool) + assertSQL("(\"bool\" IS \"boolOptional\")", bool === boolOptional) + assertSQL("(\"boolOptional\" IS \"bool\")", boolOptional === bool) + assertSQL("(\"boolOptional\" IS \"boolOptional\")", boolOptional === boolOptional) + assertSQL("(\"bool\" IS 1)", bool === true) + assertSQL("(\"boolOptional\" IS 1)", boolOptional === true) + assertSQL("(1 IS \"bool\")", true === bool) + assertSQL("(1 IS \"boolOptional\")", true === boolOptional) + + assertSQL("(\"boolOptional\" IS NULL)", boolOptional === nil) + assertSQL("(NULL IS \"boolOptional\")", nil === boolOptional) + } + + func test_isNotOperator_withEquatableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" IS NOT \"bool\")", bool !== bool) + assertSQL("(\"bool\" IS NOT \"boolOptional\")", bool !== boolOptional) + assertSQL("(\"boolOptional\" IS NOT \"bool\")", boolOptional !== bool) + assertSQL("(\"boolOptional\" IS NOT \"boolOptional\")", boolOptional !== boolOptional) + assertSQL("(\"bool\" IS NOT 1)", bool !== true) + assertSQL("(\"boolOptional\" IS NOT 1)", boolOptional !== true) + assertSQL("(1 IS NOT \"bool\")", true !== bool) + assertSQL("(1 IS NOT \"boolOptional\")", true !== boolOptional) + + assertSQL("(\"boolOptional\" IS NOT NULL)", boolOptional !== nil) + assertSQL("(NULL IS NOT \"boolOptional\")", nil !== boolOptional) + } + + func test_inequalityOperator_withEquatableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" != \"bool\")", bool != bool) + assertSQL("(\"bool\" != \"boolOptional\")", bool != boolOptional) + assertSQL("(\"boolOptional\" != \"bool\")", boolOptional != bool) + assertSQL("(\"boolOptional\" != \"boolOptional\")", boolOptional != boolOptional) + assertSQL("(\"bool\" != 1)", bool != true) + assertSQL("(\"boolOptional\" != 1)", boolOptional != true) + assertSQL("(1 != \"bool\")", true != bool) + assertSQL("(1 != \"boolOptional\")", true != boolOptional) + + assertSQL("(\"boolOptional\" IS NOT NULL)", boolOptional != nil) + assertSQL("(NULL IS NOT \"boolOptional\")", nil != boolOptional) + } + + func test_greaterThanOperator_withComparableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" > \"bool\")", bool > bool) + assertSQL("(\"bool\" > \"boolOptional\")", bool > boolOptional) + assertSQL("(\"boolOptional\" > \"bool\")", boolOptional > bool) + assertSQL("(\"boolOptional\" > \"boolOptional\")", boolOptional > boolOptional) + assertSQL("(\"bool\" > 1)", bool > true) + assertSQL("(\"boolOptional\" > 1)", boolOptional > true) + assertSQL("(1 > \"bool\")", true > bool) + assertSQL("(1 > \"boolOptional\")", true > boolOptional) + } + + func test_greaterThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" >= \"bool\")", bool >= bool) + assertSQL("(\"bool\" >= \"boolOptional\")", bool >= boolOptional) + assertSQL("(\"boolOptional\" >= \"bool\")", boolOptional >= bool) + assertSQL("(\"boolOptional\" >= \"boolOptional\")", boolOptional >= boolOptional) + assertSQL("(\"bool\" >= 1)", bool >= true) + assertSQL("(\"boolOptional\" >= 1)", boolOptional >= true) + assertSQL("(1 >= \"bool\")", true >= bool) + assertSQL("(1 >= \"boolOptional\")", true >= boolOptional) + } + + func test_lessThanOperator_withComparableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" < \"bool\")", bool < bool) + assertSQL("(\"bool\" < \"boolOptional\")", bool < boolOptional) + assertSQL("(\"boolOptional\" < \"bool\")", boolOptional < bool) + assertSQL("(\"boolOptional\" < \"boolOptional\")", boolOptional < boolOptional) + assertSQL("(\"bool\" < 1)", bool < true) + assertSQL("(\"boolOptional\" < 1)", boolOptional < true) + assertSQL("(1 < \"bool\")", true < bool) + assertSQL("(1 < \"boolOptional\")", true < boolOptional) + } + + func test_lessThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { + assertSQL("(\"bool\" <= \"bool\")", bool <= bool) + assertSQL("(\"bool\" <= \"boolOptional\")", bool <= boolOptional) + assertSQL("(\"boolOptional\" <= \"bool\")", boolOptional <= bool) + assertSQL("(\"boolOptional\" <= \"boolOptional\")", boolOptional <= boolOptional) + assertSQL("(\"bool\" <= 1)", bool <= true) + assertSQL("(\"boolOptional\" <= 1)", boolOptional <= true) + assertSQL("(1 <= \"bool\")", true <= bool) + assertSQL("(1 <= \"boolOptional\")", true <= boolOptional) + } + + func test_patternMatchingOperator_withComparableCountableClosedRange_buildsBetweenBooleanExpression() { + assertSQL("\"int\" BETWEEN 0 AND 5", 0...5 ~= int) + assertSQL("\"intOptional\" BETWEEN 0 AND 5", 0...5 ~= intOptional) + } + + func test_patternMatchingOperator_withComparableClosedRange_buildsBetweenBooleanExpression() { + assertSQL("\"double\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= double) + assertSQL("\"doubleOptional\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparableRange_buildsBooleanExpression() { + assertSQL("\"double\" >= 1.2 AND \"double\" < 4.5", 1.2..<4.5 ~= double) + assertSQL("\"doubleOptional\" >= 1.2 AND \"doubleOptional\" < 4.5", 1.2..<4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparablePartialRangeThrough_buildsBooleanExpression() { + assertSQL("\"double\" <= 4.5", ...4.5 ~= double) + assertSQL("\"doubleOptional\" <= 4.5", ...4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparablePartialRangeUpTo_buildsBooleanExpression() { + assertSQL("\"double\" < 4.5", ..<4.5 ~= double) + assertSQL("\"doubleOptional\" < 4.5", ..<4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparablePartialRangeFrom_buildsBooleanExpression() { + assertSQL("\"double\" >= 4.5", 4.5... ~= double) + assertSQL("\"doubleOptional\" >= 4.5", 4.5... ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparableClosedRangeString_buildsBetweenBooleanExpression() { + assertSQL("\"string\" BETWEEN 'a' AND 'b'", "a"..."b" ~= string) + assertSQL("\"stringOptional\" BETWEEN 'a' AND 'b'", "a"..."b" ~= stringOptional) + } + + func test_doubleAndOperator_withBooleanExpressions_buildsCompoundExpression() { + assertSQL("(\"bool\" AND \"bool\")", bool && bool) + assertSQL("(\"bool\" AND \"boolOptional\")", bool && boolOptional) + assertSQL("(\"boolOptional\" AND \"bool\")", boolOptional && bool) + assertSQL("(\"boolOptional\" AND \"boolOptional\")", boolOptional && boolOptional) + assertSQL("(\"bool\" AND 1)", bool && true) + assertSQL("(\"boolOptional\" AND 1)", boolOptional && true) + assertSQL("(1 AND \"bool\")", true && bool) + assertSQL("(1 AND \"boolOptional\")", true && boolOptional) + } + + func test_andFunction_withBooleanExpressions_buildsCompoundExpression() { + assertSQL("(\"bool\" AND \"bool\" AND \"bool\")", and([bool, bool, bool])) + assertSQL("(\"bool\" AND \"bool\")", and([bool, bool])) + assertSQL("(\"bool\")", and([bool])) + + assertSQL("(\"bool\" AND \"bool\" AND \"bool\")", and(bool, bool, bool)) + assertSQL("(\"bool\" AND \"bool\")", and(bool, bool)) + assertSQL("(\"bool\")", and(bool)) + } + + func test_doubleOrOperator_withBooleanExpressions_buildsCompoundExpression() { + assertSQL("(\"bool\" OR \"bool\")", bool || bool) + assertSQL("(\"bool\" OR \"boolOptional\")", bool || boolOptional) + assertSQL("(\"boolOptional\" OR \"bool\")", boolOptional || bool) + assertSQL("(\"boolOptional\" OR \"boolOptional\")", boolOptional || boolOptional) + assertSQL("(\"bool\" OR 1)", bool || true) + assertSQL("(\"boolOptional\" OR 1)", boolOptional || true) + assertSQL("(1 OR \"bool\")", true || bool) + assertSQL("(1 OR \"boolOptional\")", true || boolOptional) + } + + func test_orFunction_withBooleanExpressions_buildsCompoundExpression() { + assertSQL("(\"bool\" OR \"bool\" OR \"bool\")", or([bool, bool, bool])) + assertSQL("(\"bool\" OR \"bool\")", or([bool, bool])) + assertSQL("(\"bool\")", or([bool])) + + assertSQL("(\"bool\" OR \"bool\" OR \"bool\")", or(bool, bool, bool)) + assertSQL("(\"bool\" OR \"bool\")", or(bool, bool)) + assertSQL("(\"bool\")", or(bool)) + } + + func test_unaryNotOperator_withBooleanExpressions_buildsNotExpression() { + assertSQL("NOT (\"bool\")", !bool) + assertSQL("NOT (\"boolOptional\")", !boolOptional) + } + + func test_precedencePreserved() { + let n = SQLite.Expression(value: 1) + assertSQL("(((1 = 1) AND (1 = 1)) OR (1 = 1))", (n == n && n == n) || n == n) + assertSQL("((1 = 1) AND ((1 = 1) OR (1 = 1)))", n == n && (n == n || n == n)) + } + + func test_dateExpressionLessGreater() { + let begin = Date(timeIntervalSince1970: 0) + assertSQL("(\"date\" < '1970-01-01T00:00:00.000')", date < begin) + assertSQL("(\"date\" > '1970-01-01T00:00:00.000')", date > begin) + assertSQL("(\"date\" >= '1970-01-01T00:00:00.000')", date >= begin) + assertSQL("(\"date\" <= '1970-01-01T00:00:00.000')", date <= begin) + } + + func test_dateExpressionRange() { + let begin = Date(timeIntervalSince1970: 0) + let end = Date(timeIntervalSince1970: 5000) + assertSQL( + "\"date\" >= '1970-01-01T00:00:00.000' AND \"date\" < '1970-01-01T01:23:20.000'", + (begin..("id") + let email = SQLite.Expression("email") + let age = SQLite.Expression("age") + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + } + + // MARK: - + + func test_select() throws { + let managerId = SQLite.Expression("manager_id") + let managers = users.alias("managers") + + let alice = try db.run(users.insert(email <- "alice@example.com")) + _ = try db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) + + for user in try db.prepare(users.join(managers, on: managers[id] == users[managerId])) { + _ = user[users[managerId]] + } + } + + func test_prepareRowIterator() throws { + let names = ["a", "b", "c"] + try insertUsers(names) + + let emailColumn = SQLite.Expression("email") + let emails = try db.prepareRowIterator(users).map { $0[emailColumn] } + + XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) + } + + func test_ambiguousMap() throws { + let names = ["a", "b", "c"] + try insertUsers(names) + + let emails = try db.prepare("select email from users", []).map { $0[0] as! String } + + XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) + } + + func test_select_optional() throws { + let managerId = SQLite.Expression("manager_id") + let managers = users.alias("managers") + + let alice = try db.run(users.insert(email <- "alice@example.com")) + _ = try db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) + + for user in try db.prepare(users.join(managers, on: managers[id] == users[managerId])) { + _ = user[users[managerId]] + } + } + + func test_select_codable() throws { + let table = Table("codable") + try db.run(table.create { builder in + builder.column(SQLite.Expression("int")) + builder.column(SQLite.Expression("string")) + builder.column(SQLite.Expression("bool")) + builder.column(SQLite.Expression("float")) + builder.column(SQLite.Expression("double")) + builder.column(SQLite.Expression("date")) + builder.column(SQLite.Expression("uuid")) + builder.column(SQLite.Expression("optional")) + builder.column(SQLite.Expression("sub")) + }) + + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, + date: Date(timeIntervalSince1970: 5000), uuid: testUUIDValue, optional: "optional", sub: value1) + try db.run(table.insert(value)) + + let rows = try db.prepare(table) + let values: [TestCodable] = try rows.map({ try $0.decode() }) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].int, 5) + XCTAssertEqual(values[0].string, "6") + XCTAssertEqual(values[0].bool, true) + XCTAssertEqual(values[0].float, 7) + XCTAssertEqual(values[0].double, 8) + XCTAssertEqual(values[0].date, Date(timeIntervalSince1970: 5000)) + XCTAssertEqual(values[0].uuid, testUUIDValue) + XCTAssertEqual(values[0].optional, "optional") + XCTAssertEqual(values[0].sub?.int, 1) + XCTAssertEqual(values[0].sub?.string, "2") + XCTAssertEqual(values[0].sub?.bool, true) + XCTAssertEqual(values[0].sub?.float, 3) + XCTAssertEqual(values[0].sub?.double, 4) + XCTAssertEqual(values[0].sub?.date, Date(timeIntervalSince1970: 0)) + XCTAssertNil(values[0].sub?.optional) + XCTAssertNil(values[0].sub?.sub) + } + + func test_scalar() throws { + XCTAssertEqual(0, try db.scalar(users.count)) + XCTAssertEqual(false, try db.scalar(users.exists)) + + try insertUsers("alice") + XCTAssertEqual(1, try db.scalar(users.select(id.average))) + } + + func test_pluck() throws { + let rowid = try db.run(users.insert(email <- "alice@example.com")) + XCTAssertEqual(rowid, try db.pluck(users)![id]) + } + + func test_insert() throws { + let id = try db.run(users.insert(email <- "alice@example.com")) + XCTAssertEqual(1, id) + } + + func test_insert_many() throws { + let id = try db.run(users.insertMany([[email <- "alice@example.com"], [email <- "geoff@example.com"]])) + XCTAssertEqual(2, id) + } + + func test_insert_many_encodables() throws { + let table = Table("codable") + try db.run(table.create { builder in + builder.column(SQLite.Expression("int")) + builder.column(SQLite.Expression("string")) + builder.column(SQLite.Expression("bool")) + builder.column(SQLite.Expression("float")) + builder.column(SQLite.Expression("double")) + builder.column(SQLite.Expression("date")) + builder.column(SQLite.Expression("uuid")) + }) + + let value1 = TestOptionalCodable(int: 5, string: "6", bool: true, float: 7, double: 8, + date: Date(timeIntervalSince1970: 5000), uuid: testUUIDValue) + let valueWithNils = TestOptionalCodable(int: nil, string: nil, bool: nil, float: nil, double: nil, date: nil, uuid: nil) + try db.run(table.insertMany([value1, valueWithNils])) + + let rows = try db.prepare(table) + let values: [TestOptionalCodable] = try rows.map({ try $0.decode() }) + XCTAssertEqual(values.count, 2) + } + + func test_insert_custom_encodable_type() throws { + struct TestTypeWithOptionalArray: Codable { + var myInt: Int + var myString: String + var myOptionalArray: [Int]? + } + + let table = Table("custom_codable") + try db.run(table.create { builder in + builder.column(SQLite.Expression("myInt")) + builder.column(SQLite.Expression("myString")) + builder.column(SQLite.Expression("myOptionalArray")) + }) + + let customType = TestTypeWithOptionalArray(myInt: 13, myString: "foo", myOptionalArray: [1, 2, 3]) + try db.run(table.insert(customType)) + let rows = try db.prepare(table) + let values: [TestTypeWithOptionalArray] = try rows.map({ try $0.decode() }) + XCTAssertEqual(values.count, 1, "return one optional custom type") + + let customTypeWithNil = TestTypeWithOptionalArray(myInt: 123, myString: "String", myOptionalArray: nil) + try db.run(table.insert(customTypeWithNil)) + let rowsNil = try db.prepare(table) + let valuesNil: [TestTypeWithOptionalArray] = try rowsNil.map({ try $0.decode() }) + XCTAssertEqual(valuesNil.count, 2, "return two custom objects, including one that contains a nil optional") + } + + func test_upsert() throws { + try XCTSkipUnless(db.satisfiesMinimumVersion(minor: 24)) + let fetchAge = { () throws -> Int? in + try self.db.pluck(self.users.filter(self.email == "alice@example.com")).flatMap { $0[self.age] } + } + + let id = try db.run(users.upsert(email <- "alice@example.com", age <- 30, onConflictOf: email)) + XCTAssertEqual(1, id) + XCTAssertEqual(30, try fetchAge()) + + let nextId = try db.run(users.upsert(email <- "alice@example.com", age <- 42, onConflictOf: email)) + XCTAssertEqual(1, nextId) + XCTAssertEqual(42, try fetchAge()) + } + + func test_update() throws { + let changes = try db.run(users.update(email <- "alice@example.com")) + XCTAssertEqual(0, changes) + } + + func test_delete() throws { + let changes = try db.run(users.delete()) + XCTAssertEqual(0, changes) + } + + func test_union() throws { + let expectedIDs = [ + try db.run(users.insert(email <- "alice@example.com")), + try db.run(users.insert(email <- "sally@example.com")) + ] + + let query1 = users.filter(email == "alice@example.com") + let query2 = users.filter(email == "sally@example.com") + + let actualIDs = try db.prepare(query1.union(query2)).map { $0[id] } + XCTAssertEqual(expectedIDs, actualIDs) + + let query3 = users.select(users[*], SQLite.Expression(literal: "1 AS weight")).filter(email == "sally@example.com") + let query4 = users.select(users[*], SQLite.Expression(literal: "2 AS weight")).filter(email == "alice@example.com") + + let sql = query3.union(query4).order(SQLite.Expression(literal: "weight")).asSQL() + XCTAssertEqual(sql, + """ + SELECT "users".*, 1 AS weight FROM "users" WHERE ("email" = 'sally@example.com') UNION \ + SELECT "users".*, 2 AS weight FROM "users" WHERE ("email" = 'alice@example.com') ORDER BY weight + """) + + let orderedIDs = try db.prepare(query3.union(query4).order(SQLite.Expression(literal: "weight"), email)).map { $0[id] } + XCTAssertEqual(Array(expectedIDs.reversed()), orderedIDs) + } + + func test_no_such_column() throws { + let doesNotExist = SQLite.Expression("doesNotExist") + try insertUser("alice") + let row = try db.pluck(users.filter(email == "alice@example.com"))! + + XCTAssertThrowsError(try row.get(doesNotExist)) { error in + if case QueryError.noSuchColumn(let name, _) = error { + XCTAssertEqual("\"doesNotExist\"", name) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + func test_catchConstraintError() throws { + try db.run(users.insert(email <- "alice@example.com")) + do { + try db.run(users.insert(email <- "alice@example.com")) + XCTFail("expected error") + } catch let Result.error(_, code, _) where code == SQLITE_CONSTRAINT { + // expected + } catch let error { + XCTFail("unexpected error: \(error)") + } + } + + func test_extendedErrorCodes_catchConstraintError() throws { + db.usesExtendedErrorCodes = true + try db.run(users.insert(email <- "alice@example.com")) + do { + try db.run(users.insert(email <- "alice@example.com")) + XCTFail("expected error") + } catch let Result.extendedError(_, extendedCode, _) where extendedCode == 2_067 { + // SQLITE_CONSTRAINT_UNIQUE expected + } catch let error { + XCTFail("unexpected error: \(error)") + } + } + + // https://github.com/stephencelis/SQLite.swift/issues/285 + func test_order_by_random() throws { + try insertUsers(["a", "b", "c'"]) + let result = Array(try db.prepare(users.select(email).order(SQLite.Expression.random()).limit(1))) + XCTAssertEqual(1, result.count) + } + + func test_with_recursive() throws { + let nodes = Table("nodes") + let id = SQLite.Expression("id") + let parent = SQLite.Expression("parent") + let value = SQLite.Expression("value") + + try db.run(nodes.create { builder in + builder.column(id) + builder.column(parent) + builder.column(value) + }) + + try db.run(nodes.insertMany([ + [id <- 0, parent <- nil, value <- 2], + [id <- 1, parent <- 0, value <- 4], + [id <- 2, parent <- 0, value <- 9], + [id <- 3, parent <- 2, value <- 8], + [id <- 4, parent <- 2, value <- 7], + [id <- 5, parent <- 4, value <- 3] + ])) + + // Compute the sum of the values of node 5 and its ancestors + let ancestors = Table("ancestors") + let sum = try db.scalar( + ancestors + .select(value.sum) + .with(ancestors, + columns: [id, parent, value], + recursive: true, + as: nodes + .where(id == 5) + .union(all: true, + nodes.join(ancestors, on: nodes[id] == ancestors[parent]) + .select(nodes[id], nodes[parent], nodes[value]) + ) + ) + ) + + XCTAssertEqual(21, sum) + } + + /// Verify that `*` is properly expanded in a SELECT statement following a WITH clause. + func test_with_glob_expansion() throws { + let names = Table("names") + let name = SQLite.Expression("name") + try db.run(names.create { builder in + builder.column(email) + builder.column(name) + }) + + try db.run(users.insert(email <- "alice@example.com")) + try db.run(names.insert(email <- "alice@example.com", name <- "Alice")) + + // WITH intermediate AS ( SELECT ... ) SELECT * FROM intermediate + let intermediate = Table("intermediate") + let rows = try db.prepare( + intermediate + .with(intermediate, + as: users + .select([id, users[email], name]) + .join(names, on: names[email] == users[email]) + .where(users[email] == "alice@example.com") + )) + + // There should be at least one row in the result. + let row = try XCTUnwrap(rows.makeIterator().next()) + + // Verify the column names + XCTAssertEqual(row.columnNames.count, 3) + XCTAssertNotNil(row[id]) + XCTAssertNotNil(row[name]) + XCTAssertNotNil(row[email]) + } + + func test_select_ntile_function() throws { + let users = Table("users") + + try insertUser("Joey") + try insertUser("Timmy") + try insertUser("Jimmy") + try insertUser("Billy") + + let bucket = ntile(1, id.asc) + try db.prepare(users.select(id, bucket)).forEach { + XCTAssertEqual($0[bucket], 1) // only 1 window + } + } + + func test_select_cume_dist_function() throws { + let users = Table("users") + + try insertUser("Joey") + try insertUser("Timmy") + try insertUser("Jimmy") + try insertUser("Billy") + + let cumeDist = cumeDist(email) + let results = try db.prepare(users.select(id, cumeDist)).map { + $0[cumeDist] + } + XCTAssertEqual([0.25, 0.5, 0.75, 1], results) + } + + func test_select_window_row_number() throws { + let users = Table("users") + + try insertUser("Billy") + try insertUser("Jimmy") + try insertUser("Joey") + try insertUser("Timmy") + + let rowNumber = rowNumber(email.asc) + var expectedRowNum = 1 + try db.prepare(users.select(id, rowNumber)).forEach { + // should retrieve row numbers in order of INSERT above + XCTAssertEqual($0[rowNumber], expectedRowNum) + expectedRowNum += 1 + } + } + + func test_select_window_ranking() throws { + let users = Table("users") + + try insertUser("Billy") + try insertUser("Jimmy") + try insertUser("Joey") + try insertUser("Timmy") + + let percentRank = percentRank(email) + let actualPercentRank: [Int] = try db.prepare(users.select(id, percentRank)).map { + Int($0[percentRank] * 100) + } + XCTAssertEqual([0, 33, 66, 100], actualPercentRank) + + let rank = rank(email) + let actualRank: [Int] = try db.prepare(users.select(id, rank)).map { + $0[rank] + } + XCTAssertEqual([1, 2, 3, 4], actualRank) + + let denseRank = denseRank(email) + let actualDenseRank: [Int] = try db.prepare(users.select(id, denseRank)).map { + $0[denseRank] + } + XCTAssertEqual([1, 2, 3, 4], actualDenseRank) + } + + func test_select_window_values() throws { + let users = Table("users") + + try insertUser("Billy") + try insertUser("Jimmy") + try insertUser("Joey") + try insertUser("Timmy") + + let firstValue = email.firstValue(email.desc) + try db.prepare(users.select(id, firstValue)).forEach { + XCTAssertEqual($0[firstValue], "Timmy@example.com") // should grab last email alphabetically + } + + let lastValue = email.lastValue(email.asc) + var row = try db.pluck(users.select(id, lastValue))! + XCTAssertEqual(row[lastValue], "Billy@example.com") + + let nthValue = email.value(1, email.asc) + row = try db.pluck(users.select(id, nthValue))! + XCTAssertEqual(row[nthValue], "Billy@example.com") + } +} + +extension Connection { + func satisfiesMinimumVersion(minor: Int, patch: Int = 0) -> Bool { + guard let version = try? scalar("SELECT sqlite_version()") as? String else { return false } + let components = version.split(separator: ".", maxSplits: 3).compactMap { Int($0) } + guard components.count == 3 else { return false } + + return components[1] >= minor && components[2] >= patch + } +} diff --git a/Tests/SQLiteTests/Typed/QueryTests.swift b/Tests/SQLiteTests/Typed/QueryTests.swift new file mode 100644 index 00000000..257b5245 --- /dev/null +++ b/Tests/SQLiteTests/Typed/QueryTests.swift @@ -0,0 +1,584 @@ +import XCTest +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif +@testable import SQLite + +class QueryTests: XCTestCase { + + let users = Table("users") + let id = SQLite.Expression("id") + let email = SQLite.Expression("email") + let age = SQLite.Expression("age") + let admin = SQLite.Expression("admin") + let optionalAdmin = SQLite.Expression("admin") + + let posts = Table("posts") + let userId = SQLite.Expression("user_id") + let categoryId = SQLite.Expression("category_id") + let published = SQLite.Expression("published") + + let categories = Table("categories") + let tag = SQLite.Expression("tag") + + func test_select_withExpression_compilesSelectClause() { + assertSQL("SELECT \"email\" FROM \"users\"", users.select(email)) + } + + func test_select_withStarExpression_compilesSelectClause() { + assertSQL("SELECT * FROM \"users\"", users.select(*)) + } + + func test_select_withNamespacedStarExpression_compilesSelectClause() { + assertSQL("SELECT \"users\".* FROM \"users\"", users.select(users[*])) + } + + func test_select_withVariadicExpressions_compilesSelectClause() { + assertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select(email, count(*))) + } + + func test_select_withExpressions_compilesSelectClause() { + assertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select([email, count(*)])) + } + + func test_selectDistinct_withExpression_compilesSelectClause() { + assertSQL("SELECT DISTINCT \"age\" FROM \"users\"", users.select(distinct: age)) + } + + func test_selectDistinct_withExpressions_compilesSelectClause() { + assertSQL("SELECT DISTINCT \"age\", \"admin\" FROM \"users\"", users.select(distinct: [age, admin])) + } + + func test_selectDistinct_withStar_compilesSelectClause() { + assertSQL("SELECT DISTINCT * FROM \"users\"", users.select(distinct: *)) + } + + func test_union_compilesUnionClause() { + assertSQL("SELECT * FROM \"users\" UNION SELECT * FROM \"posts\"", users.union(posts)) + } + + func test_union_compilesUnionAllClause() { + assertSQL("SELECT * FROM \"users\" UNION ALL SELECT * FROM \"posts\"", users.union(all: true, posts)) + } + + func test_join_compilesJoinClause() { + assertSQL( + "SELECT * FROM \"users\" INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", + users.join(posts, on: posts[userId] == users[id]) + ) + } + + func test_join_withExplicitType_compilesJoinClauseWithType() { + assertSQL( + "SELECT * FROM \"users\" LEFT OUTER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", + users.join(.leftOuter, posts, on: posts[userId] == users[id]) + ) + + assertSQL( + "SELECT * FROM \"users\" CROSS JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", + users.join(.cross, posts, on: posts[userId] == users[id]) + ) + } + + func test_join_withTableCondition_compilesJoinClauseWithTableCondition() { + assertSQL( + "SELECT * FROM \"users\" INNER JOIN \"posts\" ON ((\"posts\".\"user_id\" = \"users\".\"id\") AND \"published\")", + users.join(posts.filter(published), on: posts[userId] == users[id]) + ) + } + + func test_join_whenChained_compilesAggregateJoinClause() { + assertSQL( + "SELECT * FROM \"users\" " + + "INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\") " + + "INNER JOIN \"categories\" ON (\"categories\".\"id\" = \"posts\".\"category_id\")", + users.join(posts, on: posts[userId] == users[id]).join(categories, on: categories[id] == posts[categoryId]) + ) + } + + func test_filter_compilesWhereClause() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(admin == true)) + } + + func test_filter_compilesWhereClause_false() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.filter(admin == false)) + } + + func test_filter_compilesWhereClause_optional() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(optionalAdmin == true)) + } + + func test_filter_compilesWhereClause_optional_false() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.filter(optionalAdmin == false)) + } + + func test_where_compilesWhereClause() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.where(admin == true)) + } + + func test_where_compilesWhereClause_false() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.where(admin == false)) + } + + func test_where_compilesWhereClause_optional() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.where(optionalAdmin == true)) + } + + func test_where_compilesWhereClause_optional_false() { + assertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.where(optionalAdmin == false)) + } + + func test_filter_whenChained_compilesAggregateWhereClause() { + assertSQL( + "SELECT * FROM \"users\" WHERE ((\"age\" >= 35) AND \"admin\")", + users.filter(age >= 35).filter(admin) + ) + } + + func test_group_withSingleExpressionName_compilesGroupClause() { + assertSQL("SELECT * FROM \"users\" GROUP BY \"age\"", + users.group(age)) + } + + func test_group_withVariadicExpressionNames_compilesGroupClause() { + assertSQL("SELECT * FROM \"users\" GROUP BY \"age\", \"admin\"", users.group(age, admin)) + } + + func test_group_withExpressionNameAndHavingBindings_compilesGroupClause() { + assertSQL("SELECT * FROM \"users\" GROUP BY \"age\" HAVING \"admin\"", users.group(age, having: admin)) + assertSQL("SELECT * FROM \"users\" GROUP BY \"age\" HAVING (\"age\" >= 30)", users.group(age, having: age >= 30)) + } + + func test_group_withExpressionNamesAndHavingBindings_compilesGroupClause() { + assertSQL( + "SELECT * FROM \"users\" GROUP BY \"age\", \"admin\" HAVING \"admin\"", + users.group([age, admin], having: admin) + ) + assertSQL( + "SELECT * FROM \"users\" GROUP BY \"age\", \"admin\" HAVING (\"age\" >= 30)", + users.group([age, admin], having: age >= 30) + ) + } + + func test_order_withSingleExpressionName_compilesOrderClause() { + assertSQL("SELECT * FROM \"users\" ORDER BY \"age\"", users.order(age)) + } + + func test_order_withVariadicExpressionNames_compilesOrderClause() { + assertSQL("SELECT * FROM \"users\" ORDER BY \"age\", \"email\"", users.order(age, email)) + } + + func test_order_withArrayExpressionNames_compilesOrderClause() { + assertSQL("SELECT * FROM \"users\" ORDER BY \"age\", \"email\"", users.order([age, email])) + } + + func test_order_withExpressionAndSortDirection_compilesOrderClause() { +// AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age.desc, email.asc)) + } + + func test_order_whenChained_resetsOrderClause() { + assertSQL("SELECT * FROM \"users\" ORDER BY \"age\"", users.order(email).order(age)) + } + + func test_reverse_withoutOrder_ordersByRowIdDescending() { +// AssertSQL("SELECT * FROM \"users\" ORDER BY \"ROWID\" DESC", users.reverse()) + } + + func test_reverse_withOrder_reversesOrder() { +// AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age, email.desc).reverse()) + } + + func test_limit_compilesLimitClause() { + assertSQL("SELECT * FROM \"users\" LIMIT 5", users.limit(5)) + } + + func test_limit_withOffset_compilesOffsetClause() { + assertSQL("SELECT * FROM \"users\" LIMIT 5 OFFSET 5", users.limit(5, offset: 5)) + } + + func test_limit_whenChained_overridesLimit() { + let query = users.limit(5) + + assertSQL("SELECT * FROM \"users\" LIMIT 10", query.limit(10)) + assertSQL("SELECT * FROM \"users\"", query.limit(nil)) + } + + func test_limit_whenChained_withOffset_overridesOffset() { + let query = users.limit(5, offset: 5) + + assertSQL("SELECT * FROM \"users\" LIMIT 10 OFFSET 20", query.limit(10, offset: 20)) + assertSQL("SELECT * FROM \"users\"", query.limit(nil)) + } + + func test_alias_aliasesTable() { + let managerId = SQLite.Expression("manager_id") + + let managers = users.alias("managers") + + assertSQL( + "SELECT * FROM \"users\" " + + "INNER JOIN \"users\" AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\")", + users.join(managers, on: managers[id] == users[managerId]) + ) + } + + func test_with_compilesWithClause() { + let temp = Table("temp") + + assertSQL("WITH \"temp\" AS (SELECT * FROM \"users\") SELECT * FROM \"temp\"", + temp.with(temp, as: users)) + } + + func test_with_compilesWithRecursiveClause() { + let temp = Table("temp") + + assertSQL("WITH RECURSIVE \"temp\" AS (SELECT * FROM \"users\") SELECT * FROM \"temp\"", + temp.with(temp, recursive: true, as: users)) + } + + func test_with_compilesWithMaterializedClause() { + let temp = Table("temp") + + assertSQL("WITH \"temp\" AS MATERIALIZED (SELECT * FROM \"users\") SELECT * FROM \"temp\"", + temp.with(temp, hint: .materialized, as: users)) + } + + func test_with_compilesWithNotMaterializedClause() { + let temp = Table("temp") + + assertSQL("WITH \"temp\" AS NOT MATERIALIZED (SELECT * FROM \"users\") SELECT * FROM \"temp\"", + temp.with(temp, hint: .notMaterialized, as: users)) + } + + func test_with_columns_compilesWithClause() { + let temp = Table("temp") + + assertSQL("WITH \"temp\" (\"id\", \"email\") AS (SELECT * FROM \"users\") SELECT * FROM \"temp\"", + temp.with(temp, columns: [id, email], recursive: false, hint: nil, as: users)) + } + + func test_with_multiple_compilesWithClause() { + let temp = Table("temp") + let second = Table("second") + let third = Table("third") + + let query = temp + .with(temp, recursive: true, as: users) + .with(second, recursive: true, as: posts) + .with(third, hint: .materialized, as: categories) + + assertSQL( + """ + WITH RECURSIVE \"temp\" AS (SELECT * FROM \"users\"), + \"second\" AS (SELECT * FROM \"posts\"), + \"third\" AS MATERIALIZED (SELECT * FROM \"categories\") + SELECT * FROM \"temp\" + """.replacingOccurrences(of: "\n", with: ""), + query + ) + } + + func test_insert_compilesInsertExpression() { + assertSQL( + "INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", + users.insert(email <- "alice@example.com", age <- 30) + ) + } + + func test_insert_withOnConflict_compilesInsertOrOnConflictExpression() { + assertSQL( + "INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", + users.insert(or: .replace, email <- "alice@example.com", age <- 30) + ) + } + + func test_insert_compilesInsertExpressionWithDefaultValues() { + assertSQL("INSERT INTO \"users\" DEFAULT VALUES", users.insert()) + } + + func test_insert_withQuery_compilesInsertExpressionWithSelectStatement() { + let emails = Table("emails") + + assertSQL( + "INSERT INTO \"emails\" SELECT \"email\" FROM \"users\" WHERE \"admin\"", + emails.insert(users.select(email).filter(admin)) + ) + } + + func test_insert_many_compilesInsertManyExpression() { + assertSQL( + """ + INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30), ('geoff@example.com', 32), + ('alex@example.com', 83) + """.replacingOccurrences(of: "\n", with: ""), + users.insertMany([[email <- "alice@example.com", age <- 30], + [email <- "geoff@example.com", age <- 32], [email <- "alex@example.com", age <- 83]]) + ) + } + func test_insert_many_compilesInsertManyNoneExpression() { + assertSQL( + "INSERT INTO \"users\" DEFAULT VALUES", + users.insertMany([]) + ) + } + + func test_insert_many_withOnConflict_compilesInsertManyOrOnConflictExpression() { + assertSQL( + """ + INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30), + ('geoff@example.com', 32), ('alex@example.com', 83) + """.replacingOccurrences(of: "\n", with: ""), + users.insertMany(or: .replace, [[email <- "alice@example.com", age <- 30], + [email <- "geoff@example.com", age <- 32], + [email <- "alex@example.com", age <- 83]]) + ) + } + + func test_insert_encodable() throws { + let emails = Table("emails") + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let insert = try emails.insert(value) + assertSQL( + """ + INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"uuid\") + VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F') + """.replacingOccurrences(of: "\n", with: ""), + insert + ) + } + + #if !os(Linux) // depends on exact JSON serialization + func test_insert_encodable_with_nested_encodable() throws { + let emails = Table("emails") + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: "optional", sub: value1) + let insert = try emails.insert(value) + let encodedJSON = try JSONEncoder().encode(value1) + let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! + + let expectedSQL = + """ + INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"uuid\", \"optional\", + \"sub\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', + 'optional', '\(encodedJSONString)') + """.replacingOccurrences(of: "\n", with: "") + + // As JSON serialization gives a different result each time, we extract JSON and compare it by deserializing it + // and keep comparing the query but with the json replaced by the `JSON` string + let (expectedQuery, expectedJSON) = extractAndReplace(expectedSQL, regex: "\\{.*\\}", with: "JSON") + let (actualQuery, actualJSON) = extractAndReplace(insert.asSQL(), regex: "\\{.*\\}", with: "JSON") + XCTAssertEqual(expectedQuery, actualQuery) + XCTAssertEqual( + try JSONDecoder().decode(TestCodable.self, from: expectedJSON.data(using: .utf8)!), + try JSONDecoder().decode(TestCodable.self, from: actualJSON.data(using: .utf8)!) + ) + } + #endif + + func test_insert_and_search_for_UUID() throws { + struct Test: Codable { + var uuid: UUID + var string: String + } + let testUUID = UUID() + let testValue = Test(uuid: testUUID, string: "value") + let db = try Connection(.temporary) + try db.run(table.create { t in + t.column(uuid) + t.column(string) + } + ) + + let iQuery = try table.insert(testValue) + try db.run(iQuery) + + let fQuery = table.filter(uuid == testUUID) + if let result = try db.pluck(fQuery) { + let testValueReturned = Test(uuid: result[uuid], string: result[string]) + XCTAssertEqual(testUUID, testValueReturned.uuid) + } else { + XCTFail("Search for uuid failed") + } + } + + func test_upsert_withOnConflict_compilesInsertOrOnConflictExpression() { + assertSQL( + """ + INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30) ON CONFLICT (\"email\") + DO UPDATE SET \"age\" = \"excluded\".\"age\" + """.replacingOccurrences(of: "\n", with: ""), + users.upsert(email <- "alice@example.com", age <- 30, onConflictOf: email) + ) + } + + func test_upsert_encodable() throws { + let emails = Table("emails") + let string = SQLite.Expression("string") + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let insert = try emails.upsert(value, onConflictOf: string) + assertSQL( + """ + INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"uuid\") + VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F') ON CONFLICT (\"string\") + DO UPDATE SET \"int\" = \"excluded\".\"int\", \"bool\" = \"excluded\".\"bool\", + \"float\" = \"excluded\".\"float\", \"double\" = \"excluded\".\"double\", \"date\" = \"excluded\".\"date\", + \"uuid\" = \"excluded\".\"uuid\" + """.replacingOccurrences(of: "\n", with: ""), + insert + ) + } + + func test_insert_many_encodables() throws { + let emails = Table("emails") + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let value2 = TestCodable(int: 2, string: "3", bool: true, float: 3, double: 5, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: "optional", sub: nil) + let value3 = TestCodable(int: 3, string: "4", bool: true, float: 3, double: 6, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let insert = try emails.insertMany([value1, value2, value3]) + assertSQL( + """ + INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"uuid\", \"optional\", \"sub\") + VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', NULL, NULL), + (2, '3', 1, 3.0, 5.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', 'optional', NULL), + (3, '4', 1, 3.0, 6.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', NULL, NULL) + """.replacingOccurrences(of: "\n", with: ""), + insert + ) + } + + func test_update_compilesUpdateExpression() { + assertSQL( + "UPDATE \"users\" SET \"age\" = 30, \"admin\" = 1 WHERE (\"id\" = 1)", + users.filter(id == 1).update(age <- 30, admin <- true) + ) + } + + func test_update_compilesUpdateLimitOrderExpression() { + assertSQL( + "UPDATE \"users\" SET \"age\" = 30 ORDER BY \"id\" LIMIT 1", + users.order(id).limit(1).update(age <- 30) + ) + } + + func test_update_encodable() throws { + let emails = Table("emails") + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let update = try emails.update(value) + assertSQL( + """ + UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, + \"date\" = '1970-01-01T00:00:00.000', \"uuid\" = 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F' + """.replacingOccurrences(of: "\n", with: ""), + update + ) + } + + func test_update_encodable_with_nested_encodable() throws { + let emails = Table("emails") + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, + date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: value1) + let update = try emails.update(value) + + // NOTE: As Linux JSON decoding doesn't order keys the same way, we need to check prefix, suffix, + // and extract JSON to decode it and check the decoded object. + + let expectedPrefix = + """ + UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, + \"date\" = '1970-01-01T00:00:00.000', \"uuid\" = 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', \"sub\" = ' + """.replacingOccurrences(of: "\n", with: "") + let expectedSuffix = "'" + + let sql = update.asSQL() + XCTAssert(sql.hasPrefix(expectedPrefix)) + XCTAssert(sql.hasSuffix(expectedSuffix)) + + let extractedJSON = String(sql[ + sql.index(sql.startIndex, offsetBy: expectedPrefix.count) ..< + sql.index(sql.endIndex, offsetBy: -expectedSuffix.count) + ]) + let decodedJSON = try JSONDecoder().decode(TestCodable.self, from: extractedJSON.data(using: .utf8)!) + XCTAssertEqual(decodedJSON, value1) + } + + func test_delete_compilesDeleteExpression() { + assertSQL( + "DELETE FROM \"users\" WHERE (\"id\" = 1)", + users.filter(id == 1).delete() + ) + } + + func test_delete_compilesDeleteLimitOrderExpression() { + assertSQL( + "DELETE FROM \"users\" ORDER BY \"id\" LIMIT 1", + users.order(id).limit(1).delete() + ) + } + + func test_delete_compilesExistsExpression() { + assertSQL( + "SELECT EXISTS (SELECT * FROM \"users\")", + users.exists + ) + } + + func test_count_returnsCountExpression() { + assertSQL("SELECT count(*) FROM \"users\"", users.count) + } + + func test_scalar_returnsScalarExpression() { + assertSQL("SELECT \"int\" FROM \"table\"", table.select(int) as ScalarQuery) + assertSQL("SELECT \"intOptional\" FROM \"table\"", table.select(intOptional) as ScalarQuery) + assertSQL("SELECT DISTINCT \"int\" FROM \"table\"", table.select(distinct: int) as ScalarQuery) + assertSQL("SELECT DISTINCT \"intOptional\" FROM \"table\"", table.select(distinct: intOptional) as ScalarQuery) + } + + func test_subscript_withExpression_returnsNamespacedExpression() { + let query = Table("query") + + assertSQL("\"query\".\"blob\"", query[data]) + assertSQL("\"query\".\"blobOptional\"", query[dataOptional]) + + assertSQL("\"query\".\"bool\"", query[bool]) + assertSQL("\"query\".\"boolOptional\"", query[boolOptional]) + + assertSQL("\"query\".\"date\"", query[date]) + assertSQL("\"query\".\"dateOptional\"", query[dateOptional]) + + assertSQL("\"query\".\"double\"", query[double]) + assertSQL("\"query\".\"doubleOptional\"", query[doubleOptional]) + + assertSQL("\"query\".\"int\"", query[int]) + assertSQL("\"query\".\"intOptional\"", query[intOptional]) + + assertSQL("\"query\".\"int64\"", query[int64]) + assertSQL("\"query\".\"int64Optional\"", query[int64Optional]) + + assertSQL("\"query\".\"string\"", query[string]) + assertSQL("\"query\".\"stringOptional\"", query[stringOptional]) + + assertSQL("\"query\".*", query[*]) + } + + func test_tableNamespacedByDatabase() { + let table = Table("table", database: "attached") + + assertSQL("SELECT * FROM \"attached\".\"table\"", table) + } + +} diff --git a/Tests/SQLiteTests/Typed/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift new file mode 100644 index 00000000..1aed00a7 --- /dev/null +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -0,0 +1,118 @@ +import XCTest +@testable import SQLite + +class RowTests: XCTestCase { + + public func test_get_value() throws { + let row = Row(["\"foo\"": 0], ["value"]) + let result = try row.get(SQLite.Expression("foo")) + + XCTAssertEqual("value", result) + } + + public func test_get_value_subscript() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = row[SQLite.Expression("foo")] + + XCTAssertEqual("value", result) + } + + public func test_get_value_optional() throws { + let row = Row(["\"foo\"": 0], ["value"]) + let result = try row.get(SQLite.Expression("foo")) + + XCTAssertEqual("value", result) + } + + public func test_get_value_optional_subscript() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = row[SQLite.Expression("foo")] + + XCTAssertEqual("value", result) + } + + public func test_get_value_optional_nil() throws { + let row = Row(["\"foo\"": 0], [nil]) + let result = try row.get(SQLite.Expression("foo")) + + XCTAssertNil(result) + } + + public func test_get_value_optional_nil_subscript() { + let row = Row(["\"foo\"": 0], [nil]) + let result = row[SQLite.Expression("foo")] + + XCTAssertNil(result) + } + + public func test_get_type_mismatch_throws_unexpected_null_value() { + let row = Row(["\"foo\"": 0], ["value"]) + XCTAssertThrowsError(try row.get(SQLite.Expression("foo"))) { error in + if case QueryError.unexpectedNullValue(let name) = error { + XCTAssertEqual("\"foo\"", name) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + public func test_get_type_mismatch_optional_returns_nil() throws { + let row = Row(["\"foo\"": 0], ["value"]) + let result = try row.get(SQLite.Expression("foo")) + XCTAssertNil(result) + } + + public func test_get_non_existent_column_throws_no_such_column() { + let row = Row(["\"foo\"": 0], ["value"]) + XCTAssertThrowsError(try row.get(SQLite.Expression("bar"))) { error in + if case QueryError.noSuchColumn(let name, let columns) = error { + XCTAssertEqual("\"bar\"", name) + XCTAssertEqual(["\"foo\""], columns) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + public func test_get_ambiguous_column_throws() { + let row = Row(["table1.\"foo\"": 0, "table2.\"foo\"": 1], ["value"]) + XCTAssertThrowsError(try row.get(SQLite.Expression("foo"))) { error in + if case QueryError.ambiguousColumn(let name, let columns) = error { + XCTAssertEqual("\"foo\"", name) + XCTAssertEqual(["table1.\"foo\"", "table2.\"foo\""], columns.sorted()) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + public func test_get_datatype_throws() { + // swiftlint:disable nesting + struct MyType: Value { + enum MyError: Error { + case failed + } + + public static var declaredDatatype: String { + Blob.declaredDatatype + } + + public static func fromDatatypeValue(_ dataValue: Blob) throws -> Data { + throw MyError.failed + } + + public var datatypeValue: Blob { + return Blob(bytes: []) + } + } + + let row = Row(["\"foo\"": 0], [Blob(bytes: [])]) + XCTAssertThrowsError(try row.get(SQLite.Expression("foo"))) { error in + if case MyType.MyError.failed = error { + XCTAssertTrue(true) + } else { + XCTFail("unexpected error: \(error)") + } + } + } +} diff --git a/Tests/SQLiteTests/Typed/SelectTests.swift b/Tests/SQLiteTests/Typed/SelectTests.swift new file mode 100644 index 00000000..5fa3cd30 --- /dev/null +++ b/Tests/SQLiteTests/Typed/SelectTests.swift @@ -0,0 +1,44 @@ +import XCTest +@testable import SQLite + +class SelectTests: SQLiteTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + try createUsersDataTable() + } + + func createUsersDataTable() throws { + try db.execute(""" + CREATE TABLE users_name ( + id INTEGER, + user_id INTEGER REFERENCES users(id), + name TEXT + ) + """ + ) + } + + func test_select_columns_from_multiple_tables() throws { + let usersData = Table("users_name") + let users = Table("users") + + let name = SQLite.Expression("name") + let id = SQLite.Expression("id") + let userID = SQLite.Expression("user_id") + let email = SQLite.Expression("email") + + try insertUser("Joey") + try db.run(usersData.insert( + id <- 1, + userID <- 1, + name <- "Joey" + )) + + try db.prepare(users.select(name, email).join(usersData, on: userID == users[id])).forEach { + XCTAssertEqual($0[name], "Joey") + XCTAssertEqual($0[email], "Joey@example.com") + } + } +} diff --git a/Tests/SQLiteTests/Typed/SetterTests.swift b/Tests/SQLiteTests/Typed/SetterTests.swift new file mode 100644 index 00000000..05da57a4 --- /dev/null +++ b/Tests/SQLiteTests/Typed/SetterTests.swift @@ -0,0 +1,140 @@ +import XCTest +import SQLite + +class SetterTests: XCTestCase { + + func test_setterAssignmentOperator_buildsSetter() { + assertSQL("\"int\" = \"int\"", int <- int) + assertSQL("\"int\" = 1", int <- 1) + assertSQL("\"intOptional\" = \"int\"", intOptional <- int) + assertSQL("\"intOptional\" = \"intOptional\"", intOptional <- intOptional) + assertSQL("\"intOptional\" = 1", intOptional <- 1) + assertSQL("\"intOptional\" = NULL", intOptional <- nil) + } + + func test_plusEquals_withStringExpression_buildsSetter() { + assertSQL("\"string\" = (\"string\" || \"string\")", string += string) + assertSQL("\"string\" = (\"string\" || 'literal')", string += "literal") + assertSQL("\"stringOptional\" = (\"stringOptional\" || \"string\")", stringOptional += string) + assertSQL("\"stringOptional\" = (\"stringOptional\" || \"stringOptional\")", stringOptional += stringOptional) + assertSQL("\"stringOptional\" = (\"stringOptional\" || 'literal')", stringOptional += "literal") + } + + func test_plusEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" + \"int\")", int += int) + assertSQL("\"int\" = (\"int\" + 1)", int += 1) + assertSQL("\"intOptional\" = (\"intOptional\" + \"int\")", intOptional += int) + assertSQL("\"intOptional\" = (\"intOptional\" + \"intOptional\")", intOptional += intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional += 1) + + assertSQL("\"double\" = (\"double\" + \"double\")", double += double) + assertSQL("\"double\" = (\"double\" + 1.0)", double += 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"double\")", doubleOptional += double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"doubleOptional\")", doubleOptional += doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" + 1.0)", doubleOptional += 1) + } + + func test_minusEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" - \"int\")", int -= int) + assertSQL("\"int\" = (\"int\" - 1)", int -= 1) + assertSQL("\"intOptional\" = (\"intOptional\" - \"int\")", intOptional -= int) + assertSQL("\"intOptional\" = (\"intOptional\" - \"intOptional\")", intOptional -= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional -= 1) + + assertSQL("\"double\" = (\"double\" - \"double\")", double -= double) + assertSQL("\"double\" = (\"double\" - 1.0)", double -= 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"double\")", doubleOptional -= double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"doubleOptional\")", doubleOptional -= doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" - 1.0)", doubleOptional -= 1) + } + + func test_timesEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" * \"int\")", int *= int) + assertSQL("\"int\" = (\"int\" * 1)", int *= 1) + assertSQL("\"intOptional\" = (\"intOptional\" * \"int\")", intOptional *= int) + assertSQL("\"intOptional\" = (\"intOptional\" * \"intOptional\")", intOptional *= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" * 1)", intOptional *= 1) + + assertSQL("\"double\" = (\"double\" * \"double\")", double *= double) + assertSQL("\"double\" = (\"double\" * 1.0)", double *= 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"double\")", doubleOptional *= double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"doubleOptional\")", doubleOptional *= doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" * 1.0)", doubleOptional *= 1) + } + + func test_dividedByEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" / \"int\")", int /= int) + assertSQL("\"int\" = (\"int\" / 1)", int /= 1) + assertSQL("\"intOptional\" = (\"intOptional\" / \"int\")", intOptional /= int) + assertSQL("\"intOptional\" = (\"intOptional\" / \"intOptional\")", intOptional /= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" / 1)", intOptional /= 1) + + assertSQL("\"double\" = (\"double\" / \"double\")", double /= double) + assertSQL("\"double\" = (\"double\" / 1.0)", double /= 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"double\")", doubleOptional /= double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"doubleOptional\")", doubleOptional /= doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" / 1.0)", doubleOptional /= 1) + } + + func test_moduloEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" % \"int\")", int %= int) + assertSQL("\"int\" = (\"int\" % 1)", int %= 1) + assertSQL("\"intOptional\" = (\"intOptional\" % \"int\")", intOptional %= int) + assertSQL("\"intOptional\" = (\"intOptional\" % \"intOptional\")", intOptional %= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" % 1)", intOptional %= 1) + } + + func test_leftShiftEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" << \"int\")", int <<= int) + assertSQL("\"int\" = (\"int\" << 1)", int <<= 1) + assertSQL("\"intOptional\" = (\"intOptional\" << \"int\")", intOptional <<= int) + assertSQL("\"intOptional\" = (\"intOptional\" << \"intOptional\")", intOptional <<= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" << 1)", intOptional <<= 1) + } + + func test_rightShiftEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" >> \"int\")", int >>= int) + assertSQL("\"int\" = (\"int\" >> 1)", int >>= 1) + assertSQL("\"intOptional\" = (\"intOptional\" >> \"int\")", intOptional >>= int) + assertSQL("\"intOptional\" = (\"intOptional\" >> \"intOptional\")", intOptional >>= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" >> 1)", intOptional >>= 1) + } + + func test_bitwiseAndEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" & \"int\")", int &= int) + assertSQL("\"int\" = (\"int\" & 1)", int &= 1) + assertSQL("\"intOptional\" = (\"intOptional\" & \"int\")", intOptional &= int) + assertSQL("\"intOptional\" = (\"intOptional\" & \"intOptional\")", intOptional &= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" & 1)", intOptional &= 1) + } + + func test_bitwiseOrEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" | \"int\")", int |= int) + assertSQL("\"int\" = (\"int\" | 1)", int |= 1) + assertSQL("\"intOptional\" = (\"intOptional\" | \"int\")", intOptional |= int) + assertSQL("\"intOptional\" = (\"intOptional\" | \"intOptional\")", intOptional |= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" | 1)", intOptional |= 1) + } + + func test_bitwiseExclusiveOrEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^= int) + assertSQL("\"int\" = (~((\"int\" & 1)) & (\"int\" | 1))", int ^= 1) + assertSQL("\"intOptional\" = (~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^= int) + assertSQL("\"intOptional\" = (~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^= intOptional) + assertSQL("\"intOptional\" = (~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^= 1) + } + + func test_postfixPlus_withIntegerValue_buildsSetter() { + assertSQL("\"int\" = (\"int\" + 1)", int++) + assertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional++) + } + + func test_postfixMinus_withIntegerValue_buildsSetter() { + assertSQL("\"int\" = (\"int\" - 1)", int--) + assertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional--) + } + + func test_setter_custom_string_convertible() { + XCTAssertEqual("\"int\" = \"int\"", (int <- int).description) + } +} diff --git a/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift b/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift new file mode 100644 index 00000000..01e88297 --- /dev/null +++ b/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift @@ -0,0 +1,58 @@ +import XCTest +import SQLite + +class WindowFunctionsTests: XCTestCase { + + func test_ntile_wrapsExpressionWithOverClause() { + assertSQL("ntile(1) OVER (ORDER BY \"int\" DESC)", ntile(1, int.desc)) + assertSQL("ntile(20) OVER (ORDER BY \"intOptional\" ASC)", ntile(20, intOptional.asc)) + assertSQL("ntile(20) OVER (ORDER BY \"double\" ASC)", ntile(20, double.asc)) + assertSQL("ntile(1) OVER (ORDER BY \"doubleOptional\" ASC)", ntile(1, doubleOptional.asc)) + assertSQL("ntile(1) OVER (ORDER BY \"int\" DESC)", ntile(1, int.desc)) + } + + func test_row_number_wrapsExpressionWithOverClause() { + assertSQL("row_number() OVER (ORDER BY \"int\" DESC)", rowNumber(int.desc)) + } + + func test_rank_wrapsExpressionWithOverClause() { + assertSQL("rank() OVER (ORDER BY \"int\" DESC)", rank(int.desc)) + } + + func test_dense_rank_wrapsExpressionWithOverClause() { + assertSQL("dense_rank() OVER (ORDER BY \"int\" DESC)", denseRank(int.desc)) + } + + func test_percent_rank_wrapsExpressionWithOverClause() { + assertSQL("percent_rank() OVER (ORDER BY \"int\" DESC)", percentRank(int.desc)) + } + + func test_cume_dist_wrapsExpressionWithOverClause() { + assertSQL("cume_dist() OVER (ORDER BY \"int\" DESC)", cumeDist(int.desc)) + } + + func test_lag_wrapsExpressionWithOverClause() { + assertSQL("lag(\"int\", 0) OVER (ORDER BY \"int\" DESC)", int.lag(int.desc)) + assertSQL("lag(\"int\", 7) OVER (ORDER BY \"int\" DESC)", int.lag(offset: 7, int.desc)) + assertSQL("lag(\"int\", 1, 3) OVER (ORDER BY \"int\" DESC)", int.lag(offset: 1, default: SQLite.Expression(value: 3), int.desc)) + } + + func test_lead_wrapsExpressionWithOverClause() { + assertSQL("lead(\"int\", 0) OVER (ORDER BY \"int\" DESC)", int.lead(int.desc)) + assertSQL("lead(\"int\", 7) OVER (ORDER BY \"int\" DESC)", int.lead(offset: 7, int.desc)) + assertSQL("lead(\"int\", 1, 3) OVER (ORDER BY \"int\" DESC)", int.lead(offset: 1, default: SQLite.Expression(value: 3), int.desc)) + } + + func test_firstValue_wrapsExpressionWithOverClause() { + assertSQL("first_value(\"int\") OVER (ORDER BY \"int\" DESC)", int.firstValue(int.desc)) + assertSQL("first_value(\"double\") OVER (ORDER BY \"int\" DESC)", double.firstValue(int.desc)) + } + + func test_lastValue_wrapsExpressionWithOverClause() { + assertSQL("last_value(\"int\") OVER (ORDER BY \"int\" DESC)", int.lastValue(int.desc)) + } + + func test_nth_value_wrapsExpressionWithOverClause() { + assertSQL("nth_value(\"int\", 3) OVER (ORDER BY \"int\" DESC)", int.value(3, int.desc)) + } +} diff --git a/run-tests.sh b/run-tests.sh index 7a35fb02..3ffba810 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,7 +1,25 @@ #!/bin/bash set -ev if [ -n "$BUILD_SCHEME" ]; then - make test BUILD_SCHEME="$BUILD_SCHEME" + if [ -n "$IOS_SIMULATOR" ]; then + make test BUILD_SCHEME="$BUILD_SCHEME" IOS_SIMULATOR="$IOS_SIMULATOR" IOS_VERSION="$IOS_VERSION" + else + make test BUILD_SCHEME="$BUILD_SCHEME" + fi elif [ -n "$VALIDATOR_SUBSPEC" ]; then - cd CocoaPodsTests && make test + bundle install + case "$VALIDATOR_SUBSPEC" in + none) + bundle exec pod lib lint --no-subspecs --fail-fast + ;; + *) + bundle exec pod lib lint --subspec="${VALIDATOR_SUBSPEC}" --fail-fast + ;; + esac +elif [ -n "$CARTHAGE_PLATFORM" ]; then + cd Tests/Carthage && make test CARTHAGE_PLATFORM="$CARTHAGE_PLATFORM" +elif [ -n "$SPM" ]; then + cd Tests/SPM && swift "${SPM}" +elif [ -n "${PACKAGE_MANAGER_COMMAND}" ]; then + swift ${PACKAGE_MANAGER_COMMAND} fi