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 e3ecdc74..e7b2ad4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,31 @@ +# OS X +.DS_Store + # Xcode build/ -timeline.xctimeline -xcuserdata/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate -# System -.DS_Store +# Carthage +/Carthage/ + +# Makefile +bin/ + +# Swift Package Manager +.build +Packages/ +.swiftpm/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 2c40393d..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "sqlcipher"] - path = Vendor/sqlcipher - url = https://github.com/sqlcipher/sqlcipher.git 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/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 new file mode 100644 index 00000000..6969a705 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,108 @@ +# Contributing + +The where and when to open an [issue](#issues) or [pull +request](#pull-requests). + + +## Issues + +Issues are used to track **bugs** and **feature requests**. Need **help** or +have a **general question**? [Ask on Stack Overflow][] (tag `sqlite.swift`). + +Before reporting a bug or requesting a feature, [run a few searches][Search] to +see if a similar issue has already been opened and ensure you’re not submitting +a duplicate. + +If you find a similar issue, read the existing conversation and see if it +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]: https://stackoverflow.com/questions/tagged/sqlite.swift +[Search]: https://github.com/stephencelis/SQLite.swift/search?type=Issues + + +### Bugs + +Think you’ve discovered a new **bug**? Let’s try troubleshooting a few things +first. + + - **Is it an installation issue?** + + If this is your first time building SQLite.swift in your project, you may + encounter a build error, _e.g._: + + No such module 'SQLite' + + Please carefully re-read the [installation instructions][] to make sure + everything is in order. + + - **Have you read the documentation?** + + If you can’t seem to get something working, check + [the documentation][See Documentation] to see if the solution is there. + + - **Are you up-to-date?** + + If you’re perusing [the documentation][See Documentation] online and find + that an example is just not working, please upgrade to the latest version + of SQLite.swift and try again before continuing. + + - **Is it an unhelpful build error?** + + While Swift error messaging is improving with each release, complex + expressions still lend themselves to misleading errors. If you encounter an + error on a complex line, breaking it down into smaller pieces generally + yields a more understandable error. + + - **Is it an _even more_ unhelpful build error?** + + Have you updated Xcode recently? Did your project stop building out of the + blue? + + Hold down the **option** key and select **Clean Build Folder…** from the + **Product** menu (⌥⇧⌘K). + +Made it through everything above and still having trouble? Sorry! +[Open an issue][]! And _please_: + + - 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. + - Include the SQLite.swift commit or branch experiencing the issue. + - Include devices and operating systems affected. + - Include build information: the Xcode and macOS versions affected. + +[installation instructions]: Documentation/Index.md#installation +[See Documentation]: Documentation/Index.md#sqliteswift-documentation +[Open an issue]: https://github.com/stephencelis/SQLite.swift/issues/new + + +### Feature Requests + +Have an innovative **feature request**? [Open an issue][]! Be thorough! Provide +context and examples. Be open to discussion. + + +## Pull Requests + +Interested in contributing but don’t know where to start? Try the [`help +wanted`][help wanted] label. + +Ready to submit a fix or a feature? [Submit a pull request][]! And _please_: + + - If code changes, run the tests and make sure everything still works. + - Write new tests for new functionality. + - Update documentation comments where applicable. + - Maintain the existing style. + - Don’t forget to have fun. + +While we cannot guarantee a merge to every pull request, we do read each one +and love your input. + + +[help wanted]: https://github.com/stephencelis/SQLite.swift/labels/help%20wanted +[Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork diff --git a/Documentation/Index.md b/Documentation/Index.md index aadeb466..0e8261f2 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -1,26 +1,37 @@ # SQLite.swift Documentation +- [SQLite.swift Documentation](#sqliteswift-documentation) - [Installation](#installation) - - [SQLCipher](#sqlcipher) - - [Frameworkless Targets](#frameworkless-targets) + - [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) - [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) - - [A Note on Thread-Safety](#a-note-on-thread-safety) + - [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) @@ -29,175 +40,382 @@ - [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 1.1 (and [Xcode 6.1](https://developer.apple.com/xcode/downloads/)) or greater. +### Swift Package Manager -To install SQLite.swift as an Xcode sub-project: +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. 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. Add the following to your `Package.swift` file: - ![Installation](Resources/installation@2x.png) + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ] + ``` - 2. In your target’s **Build Phases**, add **SQLite** to the **Target Dependencies** build phase. + 2. Build your project: - 3. Add **SQLite.framework** to the **Link Binary With Libraries** build phase. + ```sh + $ swift build + ``` - 4. Add **SQLite.framework** to a **Copy Files** build phase with a **Frameworks** destination. (Add a new build phase if need be.) +[Swift Package Manager]: https://swift.org/package-manager -You should now be able to `import SQLite` from any of your target’s source files and begin using SQLite.swift. +### Carthage +[Carthage][] is a simple, decentralized dependency manager for Cocoa. To +install SQLite.swift with Carthage: + 1. Make sure Carthage is [installed][Carthage Installation]. -### SQLCipher + 2. Update your Cartfile to include the following: -To install SQLite.swift with [SQLCipher](http://sqlcipher.net) support: + ```ruby + github "stephencelis/SQLite.swift" ~> 0.15.4 + ``` - 1. Make sure the **sqlcipher** working copy is checked out in Xcode. If **sqlcipher.xcodeproj** is unavailable (_i.e._, it appears red), go to the **Source Control** menu and select **Check Out sqlcipher…** from the **sqlcipher** menu item. + 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. - 2. Follow [the instructions above](#installation) with the **SQLiteCipher** target, instead. -> _Note:_ By default, SQLCipher compiles [without support for full-text search](https://github.com/sqlcipher/sqlcipher/issues/102). If you intend to use [FTS4](#full-text-search), make sure you add the following to **Other C Flags** in the **Build Settings** of the **sqlcipher** target (in the **sqlcipher.xcodeproj** project): -> -> - `-DSQLITE_ENABLE_FTS4` -> - `-DSQLITE_ENABLE_FTS3_PARENTHESIS` +[Carthage]: https://github.com/Carthage/Carthage +[Carthage Installation]: https://github.com/Carthage/Carthage#installing-carthage +[Carthage Usage]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application -### Frameworkless Targets +### CocoaPods -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. +[CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: - 1. In your target’s **Build Phases**, add **libsqlite3.dylib** to the **Link Binary With Libraries** build phase. + 1. Make sure CocoaPods is [installed][CocoaPods Installation] (SQLite.swift + requires version 1.6.1 or greater). - 2. Copy the SQLite.swift source files (from its **SQLite** directory) into your Xcode project. + ```sh + # Using the default Ruby install will require you to use sudo when + # installing and updating gems. + [sudo] gem install cocoapods + ``` - 3. Remove `import sqlite3` (and `@import sqlite3;`) from the SQLite.swift source files that call it. + 2. Update your Podfile to include the following: - 4. 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`. + ```ruby + use_frameworks! - ``` swift - #import - #import "SQLite-Bridging.h" + target 'YourAppTargetName' do + pod 'SQLite.swift', '~> 0.15.4' + end ``` -> _Note:_ Adding SQLite.swift source files directly to your application will both remove the `SQLite` module namespace and expose internal functions and variables. Please [report any namespace collisions and bugs](https://github.com/stephencelis/SQLite.swift/issues/new) you encounter. + 3. Run `pod install --repo-update`. + + +#### 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: + +```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: + +```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.) + + ![Installation Screen Shot](Resources/installation@2x.png) + + 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. + +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**. ## 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 `Database` class. A database is initialized with a path. 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 -let db = Database("path/to/db.sqlite3") +```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 -).first as! String + .documentDirectory, .userDomainMask, true +).first! -let db = Database("\(path)/db.sqlite3") +let db = try Connection("\(path)/db.sqlite3") +``` + +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 + } +} ``` -On OS X, you can use your app’s **Application Support** directory: +On macOS, you can use your app’s **Application Support** directory: -``` swift + +```swift +// set the path corresponding to application support var path = NSSearchPathForDirectoriesInDomains( - .ApplicationSupportDirectory, .UserDomainMask, true -).first as! String + NSBundle.mainBundle().bundleIdentifier! + .applicationSupportDirectory, .userDomainMask, true +).first! + "/" + Bundle.main.bundleIdentifier! -// create parent directory iff it doesn’t exist -NSFileManager.defaultManager().createDirectoryAtPath( - path, withIntermediateDirectories: true, attributes: nil, error: nil +// create parent directory inside application support if it doesn’t exist +try FileManager.default.createDirectory( +atPath: path, withIntermediateDirectories: true, attributes: nil ) -let db = Database("\(path)/db.sqlite3") +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 = Database(path, readonly: true) +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). +> _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 = Database() // equivalent to `Database(":memory:")` +```swift +let db = try Connection() // equivalent to `Connection(.inMemory)` ``` To create a temporary, disk-backed database, pass an empty file name. -``` swift -let db = Database("") +```swift +let db = try Connection(.temporary) +``` + +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)])) ``` -In-memory databases are automatically deleted when the database connection is closed. +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. +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`. -### A Note on Thread-Safety +```swift +db.busyTimeout = 5 // error after 5 seconds (does multiple retries) + +db.busyHandler({ tries in + tries < 3 // error after 3 tries +}) +``` -> _Note:_ Every database comes equipped with its own serial queue for statement execution and can be safely accessed across threads. Threads that open transactions and savepoints, however, do not block other threads from executing statements within the transaction. +> _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 | | --------------- | ----------- | @@ -206,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 ([`Query`](#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") @@ -231,158 +462,219 @@ 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 subscripting a database connection with a table name. +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 -let users = db["users"] +```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 run [`CREATE TABLE` statements](https://www.sqlite.org/lang_createtable.html) by calling the `create(table:)` function on a database connection. 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 -db.create(table: users) { t in // CREATE TABLE "users" ( +```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, t.column(name) // "name" TEXT -} // ) +}) // ) ``` -> _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 `create(table:)` function has several default parameters we can override. +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 - db.create(table: users, temporary: true) { t in /* ... */ } + ```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 - db.create(table: users, ifNotExists: true) { t in /* ... */ } + ```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 - t.column(email, check: like("%@%", email)) + ```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 (`Query`) 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 - t.column(user_id, references: users[id]) - // "user_id" INTEGER REFERENCES "users"("id") - - t.column(user_id, references: users) - // "user_id" INTEGER REFERENCES "users" - // -- assumes "users" has a PRIMARY KEY + ```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, and 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, on: 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 ``` @@ -393,93 +685,108 @@ 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 -users.insert(email <- "alice@mac.com", name <- "Alice")? +```swift +try db.run(users.insert(email <- "alice@mac.com", name <- "Alice")) // INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice') -``` - -The `insert` function can return several different types that are useful in different contexts. - - - An `Int64?` representing the inserted row’s [`ROWID`][ROWID] (or `nil` on failure), for simplicity. - ``` swift - if let insertId = users.insert(email <- "alice@mac.com") { - println("inserted id: \(insertId)") - } - ``` +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.') +``` - If a value is always expected, we can disambiguate with a `!`. +The `insert` function, when run successfully, returns an `Int64` representing +the inserted row’s [`ROWID`][ROWID]. - ``` swift - users.insert(email <- "alice@mac.com")! - ``` +```swift +do { + let rowid = try db.run(users.insert(email <- "alice@mac.com")) + print("inserted id: \(rowid)") +} catch { + print("insertion failed: \(error)") +} +``` - - A `Statement`, for [the transaction and savepoint helpers](#transactions-and-savepoints) that take a list of statements. - - ``` swift - db.transaction() - && users.insert(email <- "alice@mac.com") - && users.insert(email <- "betty@mac.com") - && db.commit() || db.rollback() - // BEGIN DEFERRED TRANSACTION; - // INSERT INTO "users" ("email") VALUES ('alice@mac.com'); - // INSERT INTO "users" ("email") VALUES ('betty@mac.com'); - // COMMIT TRANSACTION; - ``` +Multiple rows can be inserted at once by similarly calling `insertMany` with an array of +per-row [setters](#setters). - - A tuple of the above [`ROWID`][ROWID] and statement: `(rowid: Int64?, statement: Statement)`, for flexibility. +```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)") +} +``` - ``` swift - let (rowid, statement) = users.insert(email <- "alice@mac.com") - if let rowid = rowid { - println("inserted id: \(rowid)") - } else if statement.failed { - println("insertion failed: \(statement.reason)") - } - ``` -The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions follow similar patterns. +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. +> _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 -> timestamps.insert()! +> ```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 -views.update(count <- 0) -// UPDATE "views" SET "count" = 0 WHERE ("id" = 1) +```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 -views.update(count++) // equivalent to `views.update(count -> count + 1)` -// UPDATE "views" SET "count" = "count" + 1 WHERE ("id" = 1) +```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 -db.transaction() - && alice.update(balance -= amount) - && betty.update(balance += amount) - && db.commit() || db.rollback() -// BEGIN DEFERRED TRANSACTION; -// UPDATE "users" SET "balance" = "balance" - 100.0 WHERE ("id" = 1); -// UPDATE "users" SET "balance" = "balance" + 100.0 WHERE ("id" = 2); -// COMMIT TRANSACTION; +try db.transaction { + try db.run(alice.update(balance -= amount)) + try db.run(betty.update(balance += amount)) +} +// BEGIN DEFERRED TRANSACTION +// UPDATE "users" SET "balance" = "balance" - 100.0 WHERE ("id" = 1) +// UPDATE "users" SET "balance" = "balance" + 100.0 WHERE ("id" = 2) +// COMMIT TRANSACTION ``` @@ -496,7 +803,7 @@ db.transaction() | `<<=` | `Int -> Int` | | `>>=` | `Int -> Int` | | `&=` | `Int -> Int` | -| `||=` | `Int -> Int` | +| `\|\|=` | `Int -> Int` | | `^=` | `Int -> Int` | | `+=` | `String -> String` | @@ -511,46 +818,105 @@ 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 -[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 -for user in users { - println("id: \(user[id]), email: \(user[email]), name: \(user[name])") +```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") } // 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 calling the `first` computed property on [`Query`](#queries). +We can pluck the first row by passing a query to the `pluck` function on a +database connection. -``` swift -if let user = users.first { /* ... */ } // Row +```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 -let all = Array(users) +```swift +let all = Array(try db.prepare(users)) // SELECT * FROM "users" ``` ### Building Complex Queries -[`Query`](#queries) structures 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" @@ -560,56 +926,71 @@ let query = users.select(email) // SELECT "email" FROM "users" #### Selecting Columns -By default, [`Query`](#queries) objects 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 -let query = users.select(id, email) +```swift +for user in try db.prepare(users.select(id, email)) { + print("id: \(user[id]), email: \(user[email])") + // id: 1, email: alice@mac.com +} // SELECT "id", "email" FROM "users" ``` - #### Joining Other Tables 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" > ``` @@ -617,20 +998,24 @@ 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[manager_id]) +let query = users.join(managers, on: managers[id] == users[managerId]) // SELECT * FROM "users" // 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 -let user = query.first! +```swift +let user = try db.pluck(query) user[id] // fatal error: ambiguous column 'id' // (please disambiguate: ["users"."id", "managers"."id"]) @@ -641,33 +1026,41 @@ 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) -users.filter(contains([1, 2, 3, 4, 5], id)) +users.filter([1, 2, 3, 4, 5].contains(id)) // SELECT * FROM "users" WHERE ("id" IN (1, 2, 3, 4, 5)) -users.filter(like("%@mac.com", email)) +users.filter(email.like("%@mac.com")) // SELECT * FROM "users" WHERE ("email" LIKE '%@mac.com') -users.filter(verified && lower(name) == "alice") +users.filter(verified && name.lowercaseString == "alice") // SELECT * FROM "users" WHERE ("verified" AND (lower("name") == 'alice')) 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-XID_906). +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 @@ -682,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 @@ -715,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 ``` @@ -732,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 @@ -743,307 +1142,523 @@ 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 -[`Query`](#queries) structures come with a number of functions that quickly return aggregate 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 -users.count +```swift +let count = try db.scalar(users.count) // SELECT count(*) FROM "users" ``` Filtered queries will appropriately filter aggregate values. -``` swift -users.filter(name != nil).count +```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 (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 function takes a [column name](#expressions) and 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 - users.count(name) // -> Int + ```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 - users.max(id) // -> Int64? + ```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 - users.min(id) // -> Int64? + ```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 - users.average(balance) // -> Double? + ```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 - users.sum(balance) // -> Double? + ```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 - users.total(balance) // -> Double + ```swift + let total = try db.scalar(users.select(balance.total)) // -> Double // SELECT total("balance") FROM "users" ``` -> _Note:_ Most of the above aggregate functions (except `max` and `min`) can be called with a `distinct` parameter to aggregate `DISTINCT` values only. +> _Note:_ Expressions can be prefixed with a `DISTINCT` clause by calling the +> `distinct` computed property. > -> ``` swift -> users.count(distinct: name) +> ```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 -users.update(email <- "alice@me.com")? +```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) -alice.update(email <- "alice@me.com")? +try db.run(alice.update(email <- "alice@me.com")) // UPDATE "users" SET "email" = 'alice@me.com' WHERE ("id" = 1) ``` -Like [`insert`](#inserting-rows) (and [`delete`](#updating-rows)), `update` can return several different types that are useful in different contexts. - - - An `Int?` representing the number of updated rows (or `nil` on failure), for simplicity. +The `update` function returns an `Int` representing the number of updated +rows. - ``` swift - if alice.update(email <- "alice@me.com") > 0 { - println("updated Alice") +```swift +do { + if try db.run(alice.update(email <- "alice@me.com")) > 0 { + print("updated alice") + } else { + print("alice not found") } - ``` - - If a value is always expected, we can disambiguate with a `!`. - - ``` swift - alice.update(email <- "alice@me.com")! - ``` - - - A `Statement`, for [the transaction and savepoint helpers](#transactions-and-savepoints) that take a list of statements. - - - A tuple of the above number of updated rows and statement: `(changes: Int?, Statement)`, for flexibility. +} catch { + print("update failed: \(error)") +} +``` ## 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 -users.delete()? +```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) -alice.delete()? +try db.run(alice.delete()) // DELETE FROM "users" WHERE ("id" = 1) ``` -Like [`insert`](#inserting-rows) and [`update`](#updating-rows), `delete` can return several different types that are useful in different contexts. - - - An `Int?` representing the number of deleted rows (or `nil` on failure), for simplicity. +The `delete` function returns an `Int` representing the number of deleted +rows. - ``` swift - if alice.delete() > 0 { - println("deleted Alice") +```swift +do { + if try db.run(alice.delete()) > 0 { + print("deleted alice") + } else { + print("alice not found") } - ``` +} catch { + print("delete failed: \(error)") +} +``` - If a value is always expected, we can disambiguate with a `!`. - ``` swift - alice.delete()! - ``` +## Transactions and Savepoints - - A `Statement`, for [the transaction and savepoint helpers](#transactions-and-savepoints) that take a list of statements. +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. - - A tuple of the above number of deleted rows and statement: `(changes: Int?, Statement)`, for flexibility. +```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)) +} +// BEGIN DEFERRED TRANSACTION +// INSERT INTO "users" ("email") VALUES ('betty@icloud.com') +// INSERT INTO "users" ("email", "manager_id") VALUES ('cathy@icloud.com', 2) +// COMMIT TRANSACTION +``` +> _Note:_ Transactions run in a serial queue. -## Transactions and Savepoints +## Querying the Schema -Using the `transaction` and `savepoint` functions, we can run a series of statements, committing the changes to the database if they all succeed. If a single statement fails, we can bail out early and roll back. +We can obtain generic information about objects in the current schema with a `SchemaReader`: -``` swift -db.transaction() - && users.insert(email <- "betty@icloud.com") - && users.insert(email <- "cathy@icloud.com", manager_id <- db.lastInsertRowid) - && db.commit() || db.rollback() +```swift +let schema = db.schema ``` -> _Note:_ Each statement is captured in an auto-closure and won’t execute till the preceding statement succeeds. This means we can use the `lastInsertRowid` property on `Database` to reference the previous statement’s insert [`ROWID`][ROWID]. +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) +``` -## Altering the Schema +### 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)") +} +``` -SQLite.swift comes with several functions (in addition to `create(table:)`) for altering a database schema in a type-safe manner. +## 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 rename a table by calling the `rename(table:to:)` function on a database connection. +We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` +function on a `Table` or `VirtualTable`. -``` swift -db.rename(users, to: "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 `alter` function on a database connection. 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 -db.alter(table: users, add: suffix) +```swift +try db.run(users.addColumn(suffix)) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT ``` - #### Added Column Constraints -The `alter` 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 - let check = contains(["JR", "SR"], suffix) - db.alter(table: users, add: suffix, check: check) - // ALTER TABLE "users" - // ADD COLUMN "suffix" TEXT CHECK ("suffix" IN ('JR', 'SR')) + ```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 - db.alter(table: users, add: suffix, defaultValue: "SR") + ```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 - t.column(email, collate: .Nocase) - // email TEXT NOT NULL COLLATE "NOCASE" + ```swift + try db.run(users.addColumn(email, collate: .nocase)) + // ALTER TABLE "users" ADD COLUMN "email" TEXT NOT NULL COLLATE "NOCASE" - t.column(name, collate: .Rtrim) - // name TEXT 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 - db.alter(table: posts, add: user_id, references: users[id]) - // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users"("id") - - db.alter(table: posts, add: user_id, references: users) - // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" - // -- assumes "users" has a PRIMARY KEY + ```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 run [`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) by calling the `create(index:)` function on a database connection. +We can build +[`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) +by calling the `createIndex` function on a `SchemaType`. -``` swift -db.create(index: users, on: email) +```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 `create(index:)` function has a couple default parameters we can override. +The `createIndex` function has a couple default parameters we can override. - `unique` adds a `UNIQUE` constraint to the index. Default: `false`. - ``` swift - db.create(index: users, on: email, unique: true) + ```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 - db.create(index: users, on: email, ifNotExists: true) + ```swift + try db.run(users.createIndex(email, ifNotExists: true)) // CREATE INDEX IF NOT EXISTS "index_users_on_email" ON "users" ("email") ``` #### Dropping Indexes -We can run [`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by calling the `drop(index:)` function on a database connection. +We can build +[`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by +calling the `dropIndex` function on a `SchemaType`. -``` swift -db.drop(index: users, on: email) +```swift +try db.run(users.dropIndex(email)) // DROP INDEX "index_users_on_email" ``` -The `drop(index:)` 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 -db.drop(index: users, on: email, ifExists: true) +```swift +try db.run(users.dropIndex(email, ifExists: true)) // DROP INDEX IF EXISTS "index_users_on_email" ``` - -### Dropping Tables - -We can run [`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) by calling the `drop(table:)` function on a database connection. - -``` swift -db.drop(table: users) -// DROP TABLE "users" -``` - -The `drop(table:)` function has one additional parameter, `ifExists`, which (when `true`) adds an `IF EXISTS` clause to the statement. - -``` swift -db.drop(table: users, ifExists: true) -// DROP TABLE IF EXISTS "users" -``` - - ### Migrations and Schema Versioning -SQLite.swift provides a convenience property on `Database` to query and set the [`PRAGMA user_version`](https://sqlite.org/pragma.html#pragma_schema_version). This is a great way to manage your schema’s version over migrations. +You can use the convenience property on `Connection` to query and set the +[`PRAGMA user_version`](https://sqlite.org/pragma.html#pragma_user_version). + +This is a great way to manage your schema’s version over migrations. +You can conditionally run your migrations along the lines of: -``` swift +```swift if db.userVersion == 0 { // handle first migration db.userVersion = 1 @@ -1054,172 +1669,194 @@ if db.userVersion == 1 { } ``` +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 { - class var declaredDatatype: String { - return NSData.declaredDatatype - } - class func fromDatatypeValue(blobValue: Blob) -> Self { - return self(data: NSData.fromDatatypeValue(blobValue)) - } - 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. -> _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. +- `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 -### Custom Type Caveats +### Updating Codable Types -Swift does _not_ currently support generic subscripting, which means we cannot, by default, subscript Expressions with custom types to: +Queries have a method to allow updating an Encodable type. - 1. **Namespace expressions**. Use the `namespace` function, instead: +```swift +try db.run(users.filter(id == userId).update(user)) - ``` swift - let avatar = Expression("avatar") - users[avatar] // fails to compile - users.namespace(avatar) // "users"."avatar" - ``` +``` - 2. **Access column data**. Use the `get` function, instead: +> ⚠ Unless filtered, using the update method on an instance of a Codable +> type updates all table rows. - ``` swift - let user = users.first! - user[avatar] // fails to compile - user.get(avatar) // UIImage? - ``` +There are two other parameters also available to this method: -We can, of course, write extensions, but they’re rather wordy. +- `userInfo` is a dictionary that is passed to the encoder and made available + to encodable types to allow customizing their behavior. -``` swift -extension Query { - subscript(column: Expression) -> Expression { - return namespace(column) - } - subscript(column: Expression) -> Expression { - return namespace(column) - } +- `otherSetters` allows you to specify additional setters on top of those + that are generated from the encodable types themselves. + +### Retrieving Codable Types + +Rows have a method to decode a [Decodable][] type. + +```swift +let loadedUsers: [User] = try db.prepare(users).map { row in + return try row.decode() } +``` -extension Row { - subscript(column: Expression) -> UIImage { - return get(column) - } - subscript(column: Expression) -> UIImage? { - return get(column) +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. + +```swift +enum ImageCodingKeys: String, CodingKey { + case kind +} + +enum ImageKind: Int, Codable { + case png, jpg, heif +} + +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 @@ -1234,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 @@ -1250,212 +1888,370 @@ 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 `create(function:)` 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: (String, Expression) -> Expression = ( - db.create(function: "typeConformsTo", deterministic: true) { UTI, conformsToUTI in - return UTTypeConformsTo(UTI, conformsToUTI) != 0 +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, `create(function:)` 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 -let attachments = db["attachments"] +```swift +let attachments = Table("attachments") let UTI = Expression("UTI") -attachments.filter(typeConformsTo(UTI, kUTTypeImage)) +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 -db.create(function: "typeConformsTo", deterministic: true) { args in - if let UTI = args[0] as? String, conformsToUTI = args[1] as? String { - return Int(UTTypeConformsTo(UTI, conformsToUTI)) - } - return nil +```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 -let stmt = db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)") +```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 `create(collation:)` on a database connection. +We can create custom collating sequences by calling `createCollation` on a +database connection. -``` swift -db.create(collation: "NODIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) +```swift +try db.createCollation("NODIACRITIC") { lhs, rhs in + 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(vtable:)` on a database connection. +We can create a virtual table using the [FTS4 +module](http://www.sqlite.org/fts3.html) by calling `create` on a +`VirtualTable`. -``` swift -let emails = db["emails"] +```swift +let emails = VirtualTable("emails") let subject = Expression("subject") let body = Expression("body") -db.create(vtable: emails, using: fts4(subject, body)) +try db.run(emails.create(.FTS4(subject, body))) // CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body") ``` We can specify a [tokenizer](http://www.sqlite.org/fts3.html#tokenizer) using the `tokenize` parameter. -``` swift -db.create(vtable: emails, using: fts4([subject, body], tokenize: .Porter)) +```swift +try db.run(emails.create(.FTS4([subject, body], tokenize: .Porter))) // CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", tokenize=porter) ``` -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. +We can set the full range of parameters by creating a `FTS4Config` object. + +```swift +let emails = VirtualTable("emails") +let subject = Expression("subject") +let body = Expression("body") +let config = FTS4Config() + .column(subject) + .column(body, [.unindexed]) + .languageId("lid") + .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. -``` swift -emails.insert( +```swift +try db.run(emails.insert( subject <- "Just Checking In", body <- "Hey, I was just wondering...did you get my last email?" -)! +)) -emails.filter(match(emails, "wonder*")) +let wonderfulEmails: QueryType = emails.match("wonder*") // SELECT * FROM "emails" WHERE "emails" MATCH 'wonder*' -emails.filter(match(subject, "Re:*")) +let replies = emails.filter(subject.match("Re:*")) // SELECT * FROM "emails" WHERE "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. + +```swift +let emails = VirtualTable("emails") +let subject = Expression("subject") +let body = Expression("body") +let config = FTS5Config() + .column(subject) + .column(body, [.unindexed]) + +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 - 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 - let stmt = db.prepare("INSERT INTO users (email) VALUES (?)") + ```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 - stmt.run("alice@mac.com") + ```swift + try stmt.run("alice@mac.com") db.changes // -> {Some 1} ``` - Statements with results may be iterated over. + Statements with results may be iterated over, using the columnNames if + useful. - ``` swift - let stmt = db.prepare("SELECT id, email FROM users") + ```swift + let stmt = try db.prepare("SELECT id, email FROM users") for row in stmt { - println("id: \(row[0]), email: \(row[1])") - // id: Optional(1), email: Optional("alice@mac.com") + 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 - db.run("INSERT INTO users (email) VALUES (?)", "alice@mac.com") + ```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 - db.scalar("SELECT count(*) FROM users") as! Int64 + ```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 - let stmt = db.prepare("SELECT count (*) FROM users") - stmt.scalar() as! Int64 + ```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(println) + 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 new file mode 100644 index 00000000..cdfca9c4 --- /dev/null +++ b/Documentation/Planning.md @@ -0,0 +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 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._ + +## 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._ + +### Features + + * 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._ 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/installation@2x.png b/Documentation/Resources/installation@2x.png index 7a730e53..6b31f459 100644 Binary files a/Documentation/Resources/installation@2x.png and b/Documentation/Resources/installation@2x.png differ diff --git a/Documentation/Resources/playground@2x.png b/Documentation/Resources/playground@2x.png index b1c62e23..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 70142cb1..52a25a12 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,76 @@ -BUILD_TOOL = xcodebuild -BUILD_SDK = macosx -BUILD_ARGUMENTS = -scheme SQLite -sdk $(BUILD_SDK) +XCODEBUILD = xcodebuild +BUILD_SCHEME = SQLite Mac +IOS_SIMULATOR = iPhone 14 +IOS_VERSION = 16.4 -default: test +# 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 -build: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) +ifeq ($(BUILD_SCHEME),SQLite iOS) + BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -destination "platform=iOS Simulator,name=$(IOS_SIMULATOR),OS=$(IOS_VERSION)" +else + BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" +endif -test: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) test +test: $(XCBEAUTIFY) + set -o pipefail; \ + $(XCODEBUILD) $(BUILD_ARGUMENTS) test | $(XCBEAUTIFY) + +build: $(XCBEAUTIFY) + set -o pipefail; \ + $(XCODEBUILD) $(BUILD_ARGUMENTS) | $(XCBEAUTIFY) + +lint: $(SWIFTLINT) + $< --strict + +lint-fix: $(SWIFTLINT) + $< --fix clean: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) clean + $(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,c} | 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 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 8280c68b..a52b7ae5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ # 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. -[Swift]: https://developer.apple.com/swift/ -[SQLite3]: http://www.sqlite.org -[SQLite.swift]: https://github.com/stephencelis/SQLite.swift - - ## Features - A pure-Swift interface @@ -19,160 +16,233 @@ syntax _and_ intent. - A lightweight, uncomplicated query and parameter binding interface - Developer-friendly error handling and debugging - [Full-text search][] support - - [SQLCipher](#sqlcipher) support - [Well-documented][See Documentation] - Extensively tested - + - [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 = Database("path/to/db.sqlite3") +// 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) +} +``` -let users = db["users"] -let id = Expression("id") -let name = Expression("name") -let email = Expression("email") +Note that `Expression` should be written as `SQLite.Expression` to avoid +conflicts with the `SwiftUI.Expression` if you are using SwiftUI too. -db.create(table: users) { 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 -// ) - -var alice: Query? -if let insertId = users.insert(name <- "Alice", email <- "alice@mac.com") { - println("inserted id: \(insertId)") - // inserted id: 1 - alice = users.filter(id == insertId) -} -// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') +SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C +API. -for user in users { - println("id: \(user[id]), name: \(user[name]), email: \(user[email])") - // id: 1, name: Optional("Alice"), email: alice@mac.com +```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) } -// SELECT * FROM "users" +``` -alice?.update(email <- replace(email, "mac.com", "me.com"))? -// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') -// WHERE ("id" = 1) +[Read the documentation][See Documentation] or explore more, +interactively, from the Xcode project’s playground. -alice?.delete()? -// DELETE FROM "users" WHERE ("id" = 1) +![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) -users.count -// SELECT count(*) FROM "users" -``` +## Installation -SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C -API. +### Swift Package Manager -``` swift -let stmt = db.prepare("INSERT INTO users (email) VALUES (?)") -for email in ["betty@icloud.com", "cathy@icloud.com"] { - stmt.run(email) -} +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. -db.totalChanges // 3 -db.changes // 1 -db.lastInsertRowid // 3 +1. Add the following to your `Package.swift` file: -for row in db.prepare("SELECT id, email FROM users") { - println("id: \(row[0]), email: \(row[1])") - // id: Optional(2), email: Optional("betty@icloud.com") - // id: Optional(3), email: Optional("cathy@icloud.com") -} + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ] + ``` -db.scalar("SELECT count(*) FROM users") // 2 -``` +2. Build your project: -[Read the documentation][See Documentation] or explore more, -interactively, from the Xcode project’s playground. + ```sh + $ swift build + ``` -![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) +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 -## Installation +### Carthage -> _Note:_ SQLite.swift requires Swift 1.2 (and [Xcode][] 6.3) 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. +[Carthage][] is a simple, decentralized dependency manager for Cocoa. To +install SQLite.swift with Carthage: -To install SQLite.swift: + 1. Make sure Carthage is [installed][Carthage Installation]. - 1. Drag the **SQLite.xcodeproj** file into your own project. - ([Submodule][], clone, or [download][] the project first.) + 2. Update your Cartfile to include the following: - ![](Documentation/Resources/installation@2x.png) + ```ruby + github "stephencelis/SQLite.swift" ~> 0.15.4 + ``` - 2. In your target’s **Build Phases**, add **SQLite** to the **Target - Dependencies** build phase. + 3. Run `carthage update` and + [add the appropriate framework][Carthage Usage]. - 3. Add **SQLite.framework** to the **Link Binary With Libraries** build - phase. - 4. Add **SQLite.framework** to a **Copy Files** build phase with a - **Frameworks** destination. (Add a new build phase if need be.) +[Carthage]: https://github.com/Carthage/Carthage +[Carthage Installation]: https://github.com/Carthage/Carthage#installing-carthage +[Carthage Usage]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application -[Frameworkless Targets]: Documentation/Index.md#frameworkless-targets -[Xcode]: https://developer.apple.com/xcode/downloads/ -[Submodule]: http://git-scm.com/book/en/Git-Tools-Submodules -[download]: https://github.com/stephencelis/SQLite.swift/archive/master.zip +### CocoaPods -### SQLCipher +[CocoaPods][] is a dependency manager for Cocoa projects. To install +SQLite.swift with CocoaPods: -To install SQLite.swift with [SQLCipher][] support: + 1. Make sure CocoaPods is [installed][CocoaPods Installation]. - 1. Make sure the **sqlcipher** working copy is checked out in Xcode. If - **sqlcipher.xcodeproj** is unavailable (_i.e._, it appears red), go to the - **Source Control** menu and select **Check Out sqlcipher…** from the - **sqlcipher** menu item. + ```sh + # Using the default Ruby install will require you to use sudo when + # installing and updating gems. + [sudo] gem install cocoapods + ``` - 2. Follow [the instructions above](#installation) with the - **SQLiteCipher** target, instead. + 2. Update your Podfile to include the following: -> _Note:_ By default, SQLCipher compiles [without support for full-text -> search][]. If you intend to use [FTS4][], make sure you add the -> following to **Other C Flags** in the **Build Settings** of the -> **sqlcipher** target (in the **sqlcipher.xcodeproj** project): -> -> - `-DSQLITE_ENABLE_FTS4` -> - `-DSQLITE_ENABLE_FTS3_PARENTHESIS` + ```ruby + use_frameworks! -[SQLCipher]: http://sqlcipher.net -[without support for full-text search]: https://github.com/sqlcipher/sqlcipher/issues/102 -[FTS4]: http://www.sqlite.org/fts3.html + target 'YourAppTargetName' do + pod 'SQLite.swift', '~> 0.15.0' + end + ``` + + 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: + + 1. Drag the **SQLite.xcodeproj** file into your own project. + ([Submodule][], clone, or [download][] the project first.) + + ![Installation Screen Shot](Documentation/Resources/installation@2x.png) + + 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**. + +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]: https://git-scm.com/book/en/Git-Tools-Submodules +[download]: https://github.com/stephencelis/SQLite.swift/archive/master.zip ## Communication +[Read the contributing guidelines][]. The _TL;DR_ (but please; _R_): + - Need **help** or have a **general question**? [Ask on Stack Overflow][] (tag `sqlite.swift`). - Found a **bug** or have a **feature request**? [Open an issue][]. - Want to **contribute**? [Submit a pull request][]. -[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Read the contributing guidelines]: ./CONTRIBUTING.md#contributing +[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)) @@ -183,16 +253,41 @@ To install SQLite.swift with [SQLCipher][] support: SQLite.swift is available under the MIT license. See [the LICENSE file](./LICENSE.txt) for more information. +## Related + +These projects enhance or use SQLite.swift: + + - [SQLiteMigrationManager.swift][] (inspired by + [FMDBMigrationManager][]) ## Alternatives Looking for something else? Try another Swift wrapper (or [FMDB][]): - - [Camembert](https://github.com/remirobert/Camembert) - - [EonilSQLite3](https://github.com/Eonil/SQLite3) + - [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 Tests/DatabaseTests.swift b/SQLite Tests/DatabaseTests.swift deleted file mode 100644 index 07cd1961..00000000 --- a/SQLite Tests/DatabaseTests.swift +++ /dev/null @@ -1,390 +0,0 @@ -import XCTest -import SQLite - -class DatabaseTests: SQLiteTestCase { - - override func setUp() { - super.setUp() - - createUsersTable() - } - - func test_readonly_returnsFalseOnReadWriteConnections() { - XCTAssert(!db.readonly) - } - - func test_readonly_returnsTrueOnReadOnlyConnections() { - let db = Database(readonly: true) - XCTAssert(db.readonly) - } - - func test_lastInsertRowid_returnsNilOnNewConnections() { - XCTAssert(db.lastInsertRowid == nil) - } - - func test_lastInsertRowid_returnsLastIdAfterInserts() { - insertUser("alice") - XCTAssert(db.lastInsertRowid! == 1) - } - - func test_changes_returnsZeroOnNewConnections() { - XCTAssertEqual(0, db.changes) - } - - func test_changes_returnsNumberOfChanges() { - insertUser("alice") - XCTAssertEqual(1, db.changes) - insertUser("betsy") - XCTAssertEqual(1, db.changes) - } - - func test_totalChanges_returnsTotalNumberOfChanges() { - XCTAssertEqual(0, db.totalChanges) - insertUser("alice") - XCTAssertEqual(1, db.totalChanges) - insertUser("betsy") - XCTAssertEqual(2, db.totalChanges) - } - - func test_prepare_preparesAndReturnsStatements() { - db.prepare("SELECT * FROM users WHERE admin = 0") - db.prepare("SELECT * FROM users WHERE admin = ?", 0) - db.prepare("SELECT * FROM users WHERE admin = ?", [0]) - db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) - // no-op assert-nothing-asserted - } - - func test_run_preparesRunsAndReturnsStatements() { - db.run("SELECT * FROM users WHERE admin = 0") - db.run("SELECT * FROM users WHERE admin = ?", 0) - db.run("SELECT * FROM users WHERE admin = ?", [0]) - db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) - AssertSQL("SELECT * FROM users WHERE admin = 0", 4) - } - - func test_scalar_preparesRunsAndReturnsScalarValues() { - XCTAssertEqual(0, db.scalar("SELECT count(*) FROM users WHERE admin = 0") as! Int64) - XCTAssertEqual(0, db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as! Int64) - XCTAssertEqual(0, db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as! Int64) - XCTAssertEqual(0, 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() { - db.transaction(.Deferred) { _ in .Commit } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - } - - func test_transaction_executesBeginImmediate() { - db.transaction(.Immediate) { _ in .Commit } - - AssertSQL("BEGIN IMMEDIATE TRANSACTION") - } - - func test_transaction_executesBeginExclusive() { - db.transaction(.Exclusive) { _ in .Commit } - - AssertSQL("BEGIN EXCLUSIVE TRANSACTION") - } - - func test_commit_commitsTransaction() { - db.transaction() - - db.commit() - - AssertSQL("COMMIT TRANSACTION") - } - - func test_rollback_rollsTransactionBack() { - db.transaction() - - db.rollback() - - AssertSQL("ROLLBACK TRANSACTION") - } - - func test_transaction_beginsAndCommitsTransactions() { - let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)", "alice@example.com", 1) - - db.transaction { _ in stmt.run().failed ? .Rollback : .Commit } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - AssertSQL("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)") - AssertSQL("COMMIT TRANSACTION") - AssertSQL("ROLLBACK TRANSACTION", 0) - } - - func test_transaction_beginsAndRollsTransactionsBack() { - let stmt = db.run("INSERT INTO users (email, admin) VALUES (?, ?)", "alice@example.com", 1) - - db.transaction { _ in stmt.run().failed ? .Rollback : .Commit } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - AssertSQL("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)", 2) - AssertSQL("ROLLBACK TRANSACTION") - AssertSQL("COMMIT TRANSACTION", 0) - } - - func test_transaction_withOperators_allowsForFlowControl() { - let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)") - let txn = db.transaction() && - stmt.bind("alice@example.com", 1) && - stmt.bind("alice@example.com", 1) && - stmt.bind("alice@example.com", 1) && - db.commit() - txn || db.rollback() - - XCTAssertTrue(txn.failed) - XCTAssert(txn.reason!.lowercaseString.rangeOfString("unique") != nil) - - AssertSQL("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)", 2) - AssertSQL("ROLLBACK TRANSACTION") - AssertSQL("COMMIT TRANSACTION", 0) - } - - func test_savepoint_quotesSavepointNames() { - db.savepoint("That's all, Folks!") - - AssertSQL("SAVEPOINT 'That''s all, Folks!'") - } - - func test_release_quotesSavepointNames() { - let savepointName = "That's all, Folks!" - db.savepoint(savepointName) - - db.release(savepointName) - - AssertSQL("RELEASE SAVEPOINT 'That''s all, Folks!'") - } - - func test_release_defaultsToCurrentSavepointName() { - db.savepoint("Hello, World!") - - db.release() - - AssertSQL("RELEASE SAVEPOINT 'Hello, World!'") - } - - func test_release_maintainsTheSavepointNameStack() { - db.savepoint("1") - db.savepoint("2") - db.savepoint("3") - - db.release("2") - db.release() - - AssertSQL("RELEASE SAVEPOINT '2'") - AssertSQL("RELEASE SAVEPOINT '1'") - AssertSQL("RELEASE SAVEPOINT '3'", 0) - } - - func test_rollback_quotesSavepointNames() { - let savepointName = "That's all, Folks!" - db.savepoint(savepointName) - - db.rollback(savepointName) - - AssertSQL("ROLLBACK TO SAVEPOINT 'That''s all, Folks!'") - } - - func test_rollback_defaultsToCurrentSavepointName() { - db.savepoint("Hello, World!") - - db.rollback() - - AssertSQL("ROLLBACK TO SAVEPOINT 'Hello, World!'") - } - - func test_rollback_maintainsTheSavepointNameStack() { - db.savepoint("1") - db.savepoint("2") - db.savepoint("3") - - db.rollback("2") - db.rollback() - - AssertSQL("ROLLBACK TO SAVEPOINT '2'") - AssertSQL("ROLLBACK TO SAVEPOINT '1'") - AssertSQL("ROLLBACK TO SAVEPOINT '3'", 0) - } - - func test_savepoint_beginsAndReleasesTransactions() { - let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)", "alice@example.com", 1) - - db.savepoint("1") { _ in stmt.run().failed ? .Rollback : .Release } - - AssertSQL("SAVEPOINT '1'") - AssertSQL("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)") - AssertSQL("RELEASE SAVEPOINT '1'") - AssertSQL("ROLLBACK TO SAVEPOINT '1'", 0) - } - - func test_savepoint_beginsAndRollsTransactionsBack() { - let stmt = db.run("INSERT INTO users (email, admin) VALUES (?, ?)", "alice@example.com", 1) - - db.savepoint("1") { _ in stmt.run().failed ? .Rollback : .Release } - - AssertSQL("SAVEPOINT '1'") - AssertSQL("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)", 2) - AssertSQL("ROLLBACK TO SAVEPOINT '1'", 1) - AssertSQL("RELEASE SAVEPOINT '1'", 0) - } - - func test_savepoint_withOperators_allowsForFlowControl() { - let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)") - - var txn = db.savepoint("1") - - txn = txn && ( - db.savepoint("2") && - stmt.run("alice@example.com", 1) && - stmt.run("alice@example.com", 1) && - stmt.run("alice@example.com", 1) && - db.release() - ) - txn || db.rollback() - - txn = txn && ( - db.savepoint("2") && - stmt.run("alice@example.com", 1) && - stmt.run("alice@example.com", 1) && - stmt.run("alice@example.com", 1) && - db.release() - ) - txn || db.rollback() - - txn && db.release() || db.rollback() - - AssertSQL("SAVEPOINT '1'") - AssertSQL("SAVEPOINT '2'") - AssertSQL("RELEASE SAVEPOINT '2'", 0) - AssertSQL("RELEASE SAVEPOINT '1'", 0) - AssertSQL("ROLLBACK TO SAVEPOINT '1'") - AssertSQL("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)", 2) - } - - func test_interrupt_interruptsLongRunningQuery() { - insertUsers(map("abcdefghijklmnopqrstuvwxyz") { String($0) }) - db.create(function: "sleep") { args in - usleep(UInt32(Double(args[0] as? Double ?? Double(args[0] as? Int64 ?? 1)) * 1_000_000)) - return nil - } - - let stmt = db.prepare("SELECT *, sleep(?) FROM users", 0.1) - stmt.run() - XCTAssert(!stmt.failed) - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(10 * NSEC_PER_MSEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), db.interrupt) - stmt.run() - XCTAssert(stmt.failed) - } - - func test_userVersion_getsAndSetsUserVersion() { - XCTAssertEqual(0, db.userVersion) - db.userVersion = 1 - XCTAssertEqual(1, db.userVersion) - } - - func test_foreignKeys_getsAndSetsForeignKeys() { - XCTAssertEqual(false, db.foreignKeys) - db.foreignKeys = true - XCTAssertEqual(true, db.foreignKeys) - } - - func test_updateHook_setsUpdateHook() { - let updateHook = expectationWithDescription("updateHook") - db.updateHook { operation, db, table, rowid in - XCTAssertEqual(.Insert, operation) - XCTAssertEqual("main", db) - XCTAssertEqual("users", table) - XCTAssertEqual(1, rowid) - updateHook.fulfill() - } - executeAndWait { - insertUser("alice") - } - } - - func test_commitHook_setsCommitHook() { - let commitHook = expectationWithDescription("commitHook") - db.commitHook { - commitHook.fulfill() - return .Commit - } - executeAndWait { - self.db.transaction { _ in - self.insertUser("alice") - return .Commit - } - XCTAssertEqual(1, self.db.scalar("SELECT count(*) FROM users") as! Int64) - } - } - - func test_rollbackHook_setsRollbackHook() { - let rollbackHook = expectationWithDescription("commitHook") - db.rollbackHook { - rollbackHook.fulfill() - } - executeAndWait { - self.db.transaction { _ in - self.insertUser("alice") - return .Rollback - } - XCTAssertEqual(0, self.db.scalar("SELECT count(*) FROM users") as! Int64) - } - } - - func test_commitHook_withRollback_rollsBack() { - let rollbackHook = expectationWithDescription("commitHook") - db.commitHook { .Rollback } - db.rollbackHook { - rollbackHook.fulfill() - } - executeAndWait { - self.db.transaction { _ in - self.insertUser("alice") - return .Commit - } - XCTAssertEqual(0, self.db.scalar("SELECT count(*) FROM users") as! Int64) - } - } - - func test_createFunction_withArrayArguments() { - db.create(function: "hello") { $0[0].map { "Hello, \($0)!" } } - - XCTAssertEqual("Hello, world!", db.scalar("SELECT hello('world')") as! String) - XCTAssert(db.scalar("SELECT hello(NULL)") == nil) - } - - func test_createFunction_createsQuotableFunction() { - db.create(function: "hello world") { $0[0].map { "Hello, \($0)!" } } - - XCTAssertEqual("Hello, world!", db.scalar("SELECT \"hello world\"('world')") as! String) - XCTAssert(db.scalar("SELECT \"hello world\"(NULL)") == nil) - } - - func test_createCollation_createsCollation() { - db.create(collation: "NODIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) - } - XCTAssertEqual(1, db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as! Int64) - } - - func test_createCollation_createsQuotableCollation() { - db.create(collation: "NO DIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) - } - XCTAssertEqual(1, db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as! Int64) - } - - func executeAndWait(block: () -> ()) { - dispatch_async(dispatch_get_main_queue(), block) - waitForExpectationsWithTimeout(5) { error in - if let error = error { - fatalError(error.localizedDescription) - } - } - } - -} diff --git a/SQLite Tests/ExpressionTests.swift b/SQLite Tests/ExpressionTests.swift deleted file mode 100644 index c69c52cc..00000000 --- a/SQLite Tests/ExpressionTests.swift +++ /dev/null @@ -1,757 +0,0 @@ -import XCTest -import SQLite - -let stringA = Expression(value: "A") -let stringB = Expression(value: "B") - -let int1 = Expression(value: 1) -let int2 = Expression(value: 2) - -let double1 = Expression(value: 1.5) -let double2 = Expression(value: 2.5) - -let bool0 = Expression(value: false) -let bool1 = Expression(value: true) - -class ExpressionTests: SQLiteTestCase { - - func AssertSQLContains(SQL: String, _ expression: Expression, _ message: String? = nil, file: String = __FILE__, line: UInt = __LINE__) { - AssertSQL("SELECT \(SQL) FROM \"users\"", users.select(expression), file: file, line: line) - } - - override func setUp() { - createUsersTable() - - super.setUp() - } - - func test_alias_aliasesExpression() { - let aliased = stringA.alias("string_a") - AssertSQLContains("('A') AS \"string_a\"", aliased) - } - - func test_stringExpressionPlusStringExpression_buildsConcatenatingStringExpression() { - AssertSQLContains("('A' || 'A')", stringA + stringA) - AssertSQLContains("('A' || 'B')", stringA + stringB) - AssertSQLContains("('B' || 'A')", stringB + stringA) - AssertSQLContains("('B' || 'B')", stringB + stringB) - AssertSQLContains("('A' || 'B')", stringA + "B") - AssertSQLContains("('B' || 'A')", stringB + "A") - AssertSQLContains("('B' || 'A')", "B" + stringA) - AssertSQLContains("('A' || 'B')", "A" + stringB) - } - - func test_integerExpression_plusIntegerExpression_buildsAdditiveIntegerExpression() { - AssertSQLContains("(1 + 1)", int1 + int1) - AssertSQLContains("(1 + 2)", int1 + int2) - AssertSQLContains("(2 + 1)", int2 + int1) - AssertSQLContains("(2 + 2)", int2 + int2) - AssertSQLContains("(1 + 2)", int1 + 2) - AssertSQLContains("(2 + 1)", int2 + 1) - AssertSQLContains("(2 + 1)", 2 + int1) - AssertSQLContains("(1 + 2)", 1 + int2) - } - - func test_doubleExpression_plusDoubleExpression_buildsAdditiveDoubleExpression() { - AssertSQLContains("(1.5 + 1.5)", double1 + double1) - AssertSQLContains("(1.5 + 2.5)", double1 + double2) - AssertSQLContains("(2.5 + 1.5)", double2 + double1) - AssertSQLContains("(2.5 + 2.5)", double2 + double2) - AssertSQLContains("(1.5 + 2.5)", double1 + 2.5) - AssertSQLContains("(2.5 + 1.5)", double2 + 1.5) - AssertSQLContains("(2.5 + 1.5)", 2.5 + double1) - AssertSQLContains("(1.5 + 2.5)", 1.5 + double2) - } - - func test_integerExpression_minusIntegerExpression_buildsSubtractiveIntegerExpression() { - AssertSQLContains("(1 - 1)", int1 - int1) - AssertSQLContains("(1 - 2)", int1 - int2) - AssertSQLContains("(2 - 1)", int2 - int1) - AssertSQLContains("(2 - 2)", int2 - int2) - AssertSQLContains("(1 - 2)", int1 - 2) - AssertSQLContains("(2 - 1)", int2 - 1) - AssertSQLContains("(2 - 1)", 2 - int1) - AssertSQLContains("(1 - 2)", 1 - int2) - } - - func test_doubleExpression_minusDoubleExpression_buildsSubtractiveDoubleExpression() { - AssertSQLContains("(1.5 - 1.5)", double1 - double1) - AssertSQLContains("(1.5 - 2.5)", double1 - double2) - AssertSQLContains("(2.5 - 1.5)", double2 - double1) - AssertSQLContains("(2.5 - 2.5)", double2 - double2) - AssertSQLContains("(1.5 - 2.5)", double1 - 2.5) - AssertSQLContains("(2.5 - 1.5)", double2 - 1.5) - AssertSQLContains("(2.5 - 1.5)", 2.5 - double1) - AssertSQLContains("(1.5 - 2.5)", 1.5 - double2) - } - - func test_integerExpression_timesIntegerExpression_buildsMultiplicativeIntegerExpression() { - AssertSQLContains("(1 * 1)", int1 * int1) - AssertSQLContains("(1 * 2)", int1 * int2) - AssertSQLContains("(2 * 1)", int2 * int1) - AssertSQLContains("(2 * 2)", int2 * int2) - AssertSQLContains("(1 * 2)", int1 * 2) - AssertSQLContains("(2 * 1)", int2 * 1) - AssertSQLContains("(2 * 1)", 2 * int1) - AssertSQLContains("(1 * 2)", 1 * int2) - } - - func test_doubleExpression_timesDoubleExpression_buildsMultiplicativeDoubleExpression() { - AssertSQLContains("(1.5 * 1.5)", double1 * double1) - AssertSQLContains("(1.5 * 2.5)", double1 * double2) - AssertSQLContains("(2.5 * 1.5)", double2 * double1) - AssertSQLContains("(2.5 * 2.5)", double2 * double2) - AssertSQLContains("(1.5 * 2.5)", double1 * 2.5) - AssertSQLContains("(2.5 * 1.5)", double2 * 1.5) - AssertSQLContains("(2.5 * 1.5)", 2.5 * double1) - AssertSQLContains("(1.5 * 2.5)", 1.5 * double2) - } - - func test_integerExpression_dividedByIntegerExpression_buildsDivisiveIntegerExpression() { - AssertSQLContains("(1 / 1)", int1 / int1) - AssertSQLContains("(1 / 2)", int1 / int2) - AssertSQLContains("(2 / 1)", int2 / int1) - AssertSQLContains("(2 / 2)", int2 / int2) - AssertSQLContains("(1 / 2)", int1 / 2) - AssertSQLContains("(2 / 1)", int2 / 1) - AssertSQLContains("(2 / 1)", 2 / int1) - AssertSQLContains("(1 / 2)", 1 / int2) - } - - func test_doubleExpression_dividedByDoubleExpression_buildsDivisiveDoubleExpression() { - AssertSQLContains("(1.5 / 1.5)", double1 / double1) - AssertSQLContains("(1.5 / 2.5)", double1 / double2) - AssertSQLContains("(2.5 / 1.5)", double2 / double1) - AssertSQLContains("(2.5 / 2.5)", double2 / double2) - AssertSQLContains("(1.5 / 2.5)", double1 / 2.5) - AssertSQLContains("(2.5 / 1.5)", double2 / 1.5) - AssertSQLContains("(2.5 / 1.5)", 2.5 / double1) - AssertSQLContains("(1.5 / 2.5)", 1.5 / double2) - } - - func test_integerExpression_moduloIntegerExpression_buildsModuloIntegerExpression() { - AssertSQLContains("(1 % 1)", int1 % int1) - AssertSQLContains("(1 % 2)", int1 % int2) - AssertSQLContains("(2 % 1)", int2 % int1) - AssertSQLContains("(2 % 2)", int2 % int2) - AssertSQLContains("(1 % 2)", int1 % 2) - AssertSQLContains("(2 % 1)", int2 % 1) - AssertSQLContains("(2 % 1)", 2 % int1) - AssertSQLContains("(1 % 2)", 1 % int2) - } - - func test_integerExpression_bitShiftLeftIntegerExpression_buildsLeftShiftedIntegerExpression() { - AssertSQLContains("(1 << 1)", int1 << int1) - AssertSQLContains("(1 << 2)", int1 << int2) - AssertSQLContains("(2 << 1)", int2 << int1) - AssertSQLContains("(2 << 2)", int2 << int2) - AssertSQLContains("(1 << 2)", int1 << 2) - AssertSQLContains("(2 << 1)", int2 << 1) - AssertSQLContains("(2 << 1)", 2 << int1) - AssertSQLContains("(1 << 2)", 1 << int2) - } - - func test_integerExpression_bitShiftRightIntegerExpression_buildsRightShiftedIntegerExpression() { - AssertSQLContains("(1 >> 1)", int1 >> int1) - AssertSQLContains("(1 >> 2)", int1 >> int2) - AssertSQLContains("(2 >> 1)", int2 >> int1) - AssertSQLContains("(2 >> 2)", int2 >> int2) - AssertSQLContains("(1 >> 2)", int1 >> 2) - AssertSQLContains("(2 >> 1)", int2 >> 1) - AssertSQLContains("(2 >> 1)", 2 >> int1) - AssertSQLContains("(1 >> 2)", 1 >> int2) - } - - func test_integerExpression_bitwiseAndIntegerExpression_buildsAndedIntegerExpression() { - AssertSQLContains("(1 & 1)", int1 & int1) - AssertSQLContains("(1 & 2)", int1 & int2) - AssertSQLContains("(2 & 1)", int2 & int1) - AssertSQLContains("(2 & 2)", int2 & int2) - AssertSQLContains("(1 & 2)", int1 & 2) - AssertSQLContains("(2 & 1)", int2 & 1) - AssertSQLContains("(2 & 1)", 2 & int1) - AssertSQLContains("(1 & 2)", 1 & int2) - } - - func test_integerExpression_bitwiseOrIntegerExpression_buildsOredIntegerExpression() { - AssertSQLContains("(1 | 1)", int1 | int1) - AssertSQLContains("(1 | 2)", int1 | int2) - AssertSQLContains("(2 | 1)", int2 | int1) - AssertSQLContains("(2 | 2)", int2 | int2) - AssertSQLContains("(1 | 2)", int1 | 2) - AssertSQLContains("(2 | 1)", int2 | 1) - AssertSQLContains("(2 | 1)", 2 | int1) - AssertSQLContains("(1 | 2)", 1 | int2) - } - - func test_integerExpression_bitwiseExclusiveOrIntegerExpression_buildsOredIntegerExpression() { - AssertSQLContains("(~((1 & 1)) & (1 | 1))", int1 ^ int1) - AssertSQLContains("(~((1 & 2)) & (1 | 2))", int1 ^ int2) - AssertSQLContains("(~((2 & 1)) & (2 | 1))", int2 ^ int1) - AssertSQLContains("(~((2 & 2)) & (2 | 2))", int2 ^ int2) - AssertSQLContains("(~((1 & 2)) & (1 | 2))", int1 ^ 2) - AssertSQLContains("(~((2 & 1)) & (2 | 1))", int2 ^ 1) - AssertSQLContains("(~((2 & 1)) & (2 | 1))", 2 ^ int1) - AssertSQLContains("(~((1 & 2)) & (1 | 2))", 1 ^ int2) - } - - func test_bitwiseNot_integerExpression_buildsComplementIntegerExpression() { - AssertSQLContains("~(1)", ~int1) - AssertSQLContains("~(2)", ~int2) - } - - func test_equalityOperator_withEquatableExpressions_buildsBooleanExpression() { - AssertSQLContains("(0 = 0)", bool0 == bool0) - AssertSQLContains("(0 = 1)", bool0 == bool1) - AssertSQLContains("(1 = 0)", bool1 == bool0) - AssertSQLContains("(1 = 1)", bool1 == bool1) - AssertSQLContains("(0 = 1)", bool0 == true) - AssertSQLContains("(1 = 0)", bool1 == false) - AssertSQLContains("(1 = 0)", true == bool0) - AssertSQLContains("(0 = 1)", false == bool1) - } - - func test_inequalityOperator_withEquatableExpressions_buildsBooleanExpression() { - AssertSQLContains("(0 != 0)", bool0 != bool0) - AssertSQLContains("(0 != 1)", bool0 != bool1) - AssertSQLContains("(1 != 0)", bool1 != bool0) - AssertSQLContains("(1 != 1)", bool1 != bool1) - AssertSQLContains("(0 != 1)", bool0 != true) - AssertSQLContains("(1 != 0)", bool1 != false) - AssertSQLContains("(1 != 0)", true != bool0) - AssertSQLContains("(0 != 1)", false != bool1) - } - - func test_greaterThanOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQLContains("(1 > 1)", int1 > int1) - AssertSQLContains("(1 > 2)", int1 > int2) - AssertSQLContains("(2 > 1)", int2 > int1) - AssertSQLContains("(2 > 2)", int2 > int2) - AssertSQLContains("(1 > 2)", int1 > 2) - AssertSQLContains("(2 > 1)", int2 > 1) - AssertSQLContains("(2 > 1)", 2 > int1) - AssertSQLContains("(1 > 2)", 1 > int2) - } - - func test_greaterThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQLContains("(1 >= 1)", int1 >= int1) - AssertSQLContains("(1 >= 2)", int1 >= int2) - AssertSQLContains("(2 >= 1)", int2 >= int1) - AssertSQLContains("(2 >= 2)", int2 >= int2) - AssertSQLContains("(1 >= 2)", int1 >= 2) - AssertSQLContains("(2 >= 1)", int2 >= 1) - AssertSQLContains("(2 >= 1)", 2 >= int1) - AssertSQLContains("(1 >= 2)", 1 >= int2) - } - - func test_lessThanOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQLContains("(1 < 1)", int1 < int1) - AssertSQLContains("(1 < 2)", int1 < int2) - AssertSQLContains("(2 < 1)", int2 < int1) - AssertSQLContains("(2 < 2)", int2 < int2) - AssertSQLContains("(1 < 2)", int1 < 2) - AssertSQLContains("(2 < 1)", int2 < 1) - AssertSQLContains("(2 < 1)", 2 < int1) - AssertSQLContains("(1 < 2)", 1 < int2) - } - - func test_lessThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQLContains("(1 <= 1)", int1 <= int1) - AssertSQLContains("(1 <= 2)", int1 <= int2) - AssertSQLContains("(2 <= 1)", int2 <= int1) - AssertSQLContains("(2 <= 2)", int2 <= int2) - AssertSQLContains("(1 <= 2)", int1 <= 2) - AssertSQLContains("(2 <= 1)", int2 <= 1) - AssertSQLContains("(2 <= 1)", 2 <= int1) - AssertSQLContains("(1 <= 2)", 1 <= int2) - } - - func test_unaryMinusOperator_withIntegerExpression_buildsNegativeIntegerExpression() { - AssertSQLContains("-(1)", -int1) - AssertSQLContains("-(2)", -int2) - } - - func test_unaryMinusOperator_withDoubleExpression_buildsNegativeDoubleExpression() { - AssertSQLContains("-(1.5)", -double1) - AssertSQLContains("-(2.5)", -double2) - } - - func test_betweenOperator_withComparableExpression_buildsBetweenBooleanExpression() { - AssertSQLContains("1 BETWEEN 0 AND 5", 0...5 ~= int1) - AssertSQLContains("2 BETWEEN 0 AND 5", 0...5 ~= int2) - } - - func test_likeOperator_withStringExpression_buildsLikeExpression() { - AssertSQLContains("('A' LIKE 'B%')", like("B%", stringA)) - AssertSQLContains("('B' LIKE 'A%')", like("A%", stringB)) - } - - func test_globOperator_withStringExpression_buildsGlobExpression() { - AssertSQLContains("('A' GLOB 'B*')", glob("B*", stringA)) - AssertSQLContains("('B' GLOB 'A*')", glob("A*", stringB)) - } - - func test_matchOperator_withStringExpression_buildsMatchExpression() { - AssertSQLContains("('A' MATCH 'B')", match("B", stringA)) - AssertSQLContains("('B' MATCH 'A')", match("A", stringB)) - } - - func test_collateOperator_withStringExpression_buildsCollationExpression() { - AssertSQLContains("('A' COLLATE \"BINARY\")", collate(.Binary, stringA)) - AssertSQLContains("('B' COLLATE \"NOCASE\")", collate(.Nocase, stringB)) - AssertSQLContains("('A' COLLATE \"RTRIM\")", collate(.Rtrim, stringA)) - - db.create(collation: "NODIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .DiacriticInsensitiveSearch) - } - AssertSQLContains("('A' COLLATE \"NODIACRITIC\")", collate(.Custom("NODIACRITIC"), stringA)) - } - - func test_cast_buildsCastingExpressions() { - let string1 = Expression(value: "10") - let string2 = Expression(value: "10") - AssertSQLContains("CAST ('10' AS REAL)", cast(string1) as Expression) - AssertSQLContains("CAST ('10' AS INTEGER)", cast(string1) as Expression) - AssertSQLContains("CAST ('10' AS TEXT)", cast(string1) as Expression) - AssertSQLContains("CAST ('10' AS REAL)", cast(string2) as Expression) - AssertSQLContains("CAST ('10' AS INTEGER)", cast(string2) as Expression) - AssertSQLContains("CAST ('10' AS TEXT)", cast(string2) as Expression) - } - - func test_doubleAndOperator_withBooleanExpressions_buildsCompoundExpression() { - AssertSQLContains("(0 AND 0)", bool0 && bool0) - AssertSQLContains("(0 AND 1)", bool0 && bool1) - AssertSQLContains("(1 AND 0)", bool1 && bool0) - AssertSQLContains("(1 AND 1)", bool1 && bool1) - AssertSQLContains("(0 AND 1)", bool0 && true) - AssertSQLContains("(1 AND 0)", bool1 && false) - AssertSQLContains("(1 AND 0)", true && bool0) - AssertSQLContains("(0 AND 1)", false && bool1) - } - - func test_doubleOrOperator_withBooleanExpressions_buildsCompoundExpression() { - AssertSQLContains("(0 OR 0)", bool0 || bool0) - AssertSQLContains("(0 OR 1)", bool0 || bool1) - AssertSQLContains("(1 OR 0)", bool1 || bool0) - AssertSQLContains("(1 OR 1)", bool1 || bool1) - AssertSQLContains("(0 OR 1)", bool0 || true) - AssertSQLContains("(1 OR 0)", bool1 || false) - AssertSQLContains("(1 OR 0)", true || bool0) - AssertSQLContains("(0 OR 1)", false || bool1) - } - - func test_unaryNotOperator_withBooleanExpressions_buildsNotExpression() { - AssertSQLContains("NOT (0)", !bool0) - AssertSQLContains("NOT (1)", !bool1) - } - - func test_absFunction_withNumberExpressions_buildsAbsExpression() { - let int1 = Expression(value: -1) - let int2 = Expression(value: -2) - - AssertSQLContains("abs(-1)", abs(int1)) - AssertSQLContains("abs(-2)", abs(int2)) - } - - func test_coalesceFunction_withValueExpressions_buildsCoalesceExpression() { - let int1 = Expression(value: nil as Int?) - let int2 = Expression(value: nil as Int?) - let int3 = Expression(value: 3) - - AssertSQLContains("coalesce(NULL, NULL, 3)", coalesce(int1, int2, int3)) - } - - func test_ifNullFunction_withValueExpressionAndValue_buildsIfNullExpression() { - let int1 = Expression(value: nil as Int?) - let int2 = Expression(value: 2) - let int3 = Expression(value: 3) - - AssertSQLContains("ifnull(NULL, 1)", ifnull(int1, 1)) - AssertSQLContains("ifnull(NULL, 1)", int1 ?? 1) - AssertSQLContains("ifnull(NULL, 2)", ifnull(int1, int2)) - AssertSQLContains("ifnull(NULL, 2)", int1 ?? int2) - AssertSQLContains("ifnull(NULL, 3)", ifnull(int1, int3)) - AssertSQLContains("ifnull(NULL, 3)", int1 ?? int3) - } - - func test_lengthFunction_withValueExpression_buildsLengthIntExpression() { - AssertSQLContains("length('A')", length(stringA)) - AssertSQLContains("length('B')", length(stringB)) - } - - func test_lowerFunction_withStringExpression_buildsLowerStringExpression() { - AssertSQLContains("lower('A')", lower(stringA)) - AssertSQLContains("lower('B')", lower(stringB)) - } - - func test_ltrimFunction_withStringExpression_buildsTrimmedStringExpression() { - AssertSQLContains("ltrim('A')", ltrim(stringA)) - AssertSQLContains("ltrim('B')", ltrim(stringB)) - } - - func test_ltrimFunction_withStringExpressionAndReplacementCharacters_buildsTrimmedStringExpression() { - AssertSQLContains("ltrim('A', 'A?')", ltrim(stringA, "A?")) - AssertSQLContains("ltrim('B', 'B?')", ltrim(stringB, "B?")) - } - - func test_randomFunction_buildsRandomIntExpression() { - AssertSQLContains("random()", random()) - } - - func test_replaceFunction_withStringExpressionAndFindReplaceStrings_buildsReplacedStringExpression() { - AssertSQLContains("replace('A', 'A', 'B')", replace(stringA, "A", "B")) - AssertSQLContains("replace('B', 'B', 'A')", replace(stringB, "B", "A")) - } - - func test_roundFunction_withDoubleExpression_buildsRoundedDoubleExpression() { - AssertSQLContains("round(1.5)", round(double1)) - AssertSQLContains("round(2.5)", round(double2)) - } - - func test_roundFunction_withDoubleExpressionAndPrecision_buildsRoundedDoubleExpression() { - AssertSQLContains("round(1.5, 1)", round(double1, 1)) - AssertSQLContains("round(2.5, 1)", round(double2, 1)) - } - - func test_rtrimFunction_withStringExpression_buildsTrimmedStringExpression() { - AssertSQLContains("rtrim('A')", rtrim(stringA)) - AssertSQLContains("rtrim('B')", rtrim(stringB)) - } - - func test_rtrimFunction_withStringExpressionAndReplacementCharacters_buildsTrimmedStringExpression() { - AssertSQLContains("rtrim('A', 'A?')", rtrim(stringA, "A?")) - AssertSQLContains("rtrim('B', 'B?')", rtrim(stringB, "B?")) - } - - func test_substrFunction_withStringExpressionAndStartIndex_buildsSubstringExpression() { - AssertSQLContains("substr('A', 1)", substr(stringA, 1)) - AssertSQLContains("substr('B', 1)", substr(stringB, 1)) - } - - func test_substrFunction_withStringExpressionPositionAndLength_buildsSubstringExpression() { - AssertSQLContains("substr('A', 1, 2)", substr(stringA, 1, 2)) - AssertSQLContains("substr('B', 1, 2)", substr(stringB, 1, 2)) - } - - func test_substrFunction_withStringExpressionAndRange_buildsSubstringExpression() { - AssertSQLContains("substr('A', 1, 2)", substr(stringA, 1..<3)) - AssertSQLContains("substr('B', 1, 2)", substr(stringB, 1..<3)) - } - - func test_trimFunction_withStringExpression_buildsTrimmedStringExpression() { - AssertSQLContains("trim('A')", trim(stringA)) - AssertSQLContains("trim('B')", trim(stringB)) - } - - func test_trimFunction_withStringExpressionAndReplacementCharacters_buildsTrimmedStringExpression() { - AssertSQLContains("trim('A', 'A?')", trim(stringA, "A?")) - AssertSQLContains("trim('B', 'B?')", trim(stringB, "B?")) - } - - func test_upperFunction_withStringExpression_buildsLowerStringExpression() { - AssertSQLContains("upper('A')", upper(stringA)) - AssertSQLContains("upper('B')", upper(stringB)) - } - - let email2 = Expression("email") - let age2 = Expression("age") - let salary2 = Expression("salary") - let admin2 = Expression("admin") - - func test_countFunction_withExpression_buildsCountExpression() { - AssertSQLContains("count(\"age\")", count(age)) - AssertSQLContains("count(\"email\")", count(email2)) - AssertSQLContains("count(\"salary\")", count(salary2)) - AssertSQLContains("count(\"admin\")", count(admin2)) - AssertSQLContains("count(DISTINCT \"id\")", count(distinct: id)) - AssertSQLContains("count(DISTINCT \"age\")", count(distinct: age)) - AssertSQLContains("count(DISTINCT \"email\")", count(distinct: email)) - AssertSQLContains("count(DISTINCT \"email\")", count(distinct: email2)) - AssertSQLContains("count(DISTINCT \"salary\")", count(distinct: salary)) - AssertSQLContains("count(DISTINCT \"salary\")", count(distinct: salary2)) - AssertSQLContains("count(DISTINCT \"admin\")", count(distinct: admin)) - AssertSQLContains("count(DISTINCT \"admin\")", count(distinct: admin2)) - } - - func test_countFunction_withStar_buildsCountExpression() { - AssertSQLContains("count(*)", count(*)) - } - - func test_maxFunction_withExpression_buildsMaxExpression() { - AssertSQLContains("max(\"id\")", max(id)) - AssertSQLContains("max(\"age\")", max(age)) - AssertSQLContains("max(\"email\")", max(email)) - AssertSQLContains("max(\"email\")", max(email2)) - AssertSQLContains("max(\"salary\")", max(salary)) - AssertSQLContains("max(\"salary\")", max(salary2)) - } - - func test_minFunction_withExpression_buildsMinExpression() { - AssertSQLContains("min(\"id\")", min(id)) - AssertSQLContains("min(\"age\")", min(age)) - AssertSQLContains("min(\"email\")", min(email)) - AssertSQLContains("min(\"email\")", min(email2)) - AssertSQLContains("min(\"salary\")", min(salary)) - AssertSQLContains("min(\"salary\")", min(salary2)) - } - - func test_averageFunction_withExpression_buildsAverageExpression() { - AssertSQLContains("avg(\"id\")", average(id)) - AssertSQLContains("avg(\"age\")", average(age)) - AssertSQLContains("avg(\"salary\")", average(salary)) - AssertSQLContains("avg(\"salary\")", average(salary2)) - AssertSQLContains("avg(DISTINCT \"id\")", average(distinct: id)) - AssertSQLContains("avg(DISTINCT \"age\")", average(distinct: age)) - AssertSQLContains("avg(DISTINCT \"salary\")", average(distinct: salary)) - AssertSQLContains("avg(DISTINCT \"salary\")", average(distinct: salary2)) - } - - func test_sumFunction_withExpression_buildsSumExpression() { - AssertSQLContains("sum(\"id\")", sum(id)) - AssertSQLContains("sum(\"age\")", sum(age)) - AssertSQLContains("sum(\"salary\")", sum(salary)) - AssertSQLContains("sum(\"salary\")", sum(salary2)) - AssertSQLContains("sum(DISTINCT \"id\")", sum(distinct: id)) - AssertSQLContains("sum(DISTINCT \"age\")", sum(distinct: age)) - AssertSQLContains("sum(DISTINCT \"salary\")", sum(distinct: salary)) - AssertSQLContains("sum(DISTINCT \"salary\")", sum(distinct: salary2)) - } - - func test_totalFunction_withExpression_buildsTotalExpression() { - AssertSQLContains("total(\"id\")", total(id)) - AssertSQLContains("total(\"age\")", total(age)) - AssertSQLContains("total(\"salary\")", total(salary)) - AssertSQLContains("total(\"salary\")", total(salary2)) - AssertSQLContains("total(DISTINCT \"id\")", total(distinct: id)) - AssertSQLContains("total(DISTINCT \"age\")", total(distinct: age)) - AssertSQLContains("total(DISTINCT \"salary\")", total(distinct: salary)) - AssertSQLContains("total(DISTINCT \"salary\")", total(distinct: salary2)) - } - - func test_containsFunction_withValueExpressionAndValueArray_buildsInExpression() { - AssertSQLContains("(\"id\" IN (1, 2, 3))", contains([1, 2, 3], id)) - AssertSQLContains("(\"age\" IN (20, 30, 40))", contains([20, 30, 40], age)) - - AssertSQLContains("(\"id\" IN (1))", contains(Set([1]), id)) - AssertSQLContains("(\"age\" IN (20))", contains(Set([20]), age)) - } - - func test_containsFunction_withValueExpressionAndQuery_buildsInExpression() { - let query = users.select(max(age)).group(id) - AssertSQLContains("(\"id\" IN (SELECT max(\"age\") FROM \"users\" GROUP BY \"id\"))", contains(query, id)) - } - - func test_plusEquals_withStringExpression_buildsSetter() { - users.update(email += email)! - users.update(email += email2)! - users.update(email2 += email)! - users.update(email2 += email2)! - AssertSQL("UPDATE \"users\" SET \"email\" = (\"email\" || \"email\")", 4) - } - - func test_plusEquals_withStringValue_buildsSetter() { - users.update(email += ".com")! - users.update(email2 += ".com")! - - AssertSQL("UPDATE \"users\" SET \"email\" = (\"email\" || '.com')", 2) - } - - func test_plusEquals_withNumberExpression_buildsSetter() { - users.update(age += age)! - users.update(age += age2)! - users.update(age2 += age)! - users.update(age2 += age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" + \"age\")", 4) - - users.update(salary += salary)! - users.update(salary += salary2)! - users.update(salary2 += salary)! - users.update(salary2 += salary2)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" + \"salary\")", 4) - } - - func test_plusEquals_withNumberValue_buildsSetter() { - users.update(age += 1)! - users.update(age2 += 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" + 1)", 2) - - users.update(salary += 100)! - users.update(salary2 += 100)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" + 100.0)", 2) - } - - func test_minusEquals_withNumberExpression_buildsSetter() { - users.update(age -= age)! - users.update(age -= age2)! - users.update(age2 -= age)! - users.update(age2 -= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" - \"age\")", 4) - - users.update(salary -= salary)! - users.update(salary -= salary2)! - users.update(salary2 -= salary)! - users.update(salary2 -= salary2)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" - \"salary\")", 4) - } - - func test_minusEquals_withNumberValue_buildsSetter() { - users.update(age -= 1)! - users.update(age2 -= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" - 1)", 2) - - users.update(salary -= 100)! - users.update(salary2 -= 100)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" - 100.0)", 2) - } - - func test_timesEquals_withNumberExpression_buildsSetter() { - users.update(age *= age)! - users.update(age *= age2)! - users.update(age2 *= age)! - users.update(age2 *= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" * \"age\")", 4) - - users.update(salary *= salary)! - users.update(salary *= salary2)! - users.update(salary2 *= salary)! - users.update(salary2 *= salary2)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" * \"salary\")", 4) - } - - func test_timesEquals_withNumberValue_buildsSetter() { - users.update(age *= 1)! - users.update(age2 *= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" * 1)", 2) - - users.update(salary *= 100)! - users.update(salary2 *= 100)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" * 100.0)", 2) - } - - func test_divideEquals_withNumberExpression_buildsSetter() { - users.update(age /= age)! - users.update(age /= age2)! - users.update(age2 /= age)! - users.update(age2 /= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" / \"age\")", 4) - - users.update(salary /= salary)! - users.update(salary /= salary2)! - users.update(salary2 /= salary)! - users.update(salary2 /= salary2)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" / \"salary\")", 4) - } - - func test_divideEquals_withNumberValue_buildsSetter() { - users.update(age /= 1)! - users.update(age2 /= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" / 1)", 2) - - users.update(salary /= 100)! - users.update(salary2 /= 100)! - AssertSQL("UPDATE \"users\" SET \"salary\" = (\"salary\" / 100.0)", 2) - } - - func test_moduloEquals_withIntegerExpression_buildsSetter() { - users.update(age %= age)! - users.update(age %= age2)! - users.update(age2 %= age)! - users.update(age2 %= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" % \"age\")", 4) - } - - func test_moduloEquals_withIntegerValue_buildsSetter() { - users.update(age %= 10)! - users.update(age2 %= 10)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" % 10)", 2) - } - - func test_rightShiftEquals_withIntegerExpression_buildsSetter() { - users.update(age >>= age)! - users.update(age >>= age2)! - users.update(age2 >>= age)! - users.update(age2 >>= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" >> \"age\")", 4) - } - - func test_rightShiftEquals_withIntegerValue_buildsSetter() { - users.update(age >>= 1)! - users.update(age2 >>= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" >> 1)", 2) - } - - func test_leftShiftEquals_withIntegerExpression_buildsSetter() { - users.update(age <<= age)! - users.update(age <<= age2)! - users.update(age2 <<= age)! - users.update(age2 <<= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" << \"age\")", 4) - } - - func test_leftShiftEquals_withIntegerValue_buildsSetter() { - users.update(age <<= 1)! - users.update(age2 <<= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" << 1)", 2) - } - - func test_bitwiseAndEquals_withIntegerExpression_buildsSetter() { - users.update(age &= age)! - users.update(age &= age2)! - users.update(age2 &= age)! - users.update(age2 &= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" & \"age\")", 4) - } - - func test_bitwiseAndEquals_withIntegerValue_buildsSetter() { - users.update(age &= 1)! - users.update(age2 &= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" & 1)", 2) - } - - func test_bitwiseOrEquals_withIntegerExpression_buildsSetter() { - users.update(age |= age)! - users.update(age |= age2)! - users.update(age2 |= age)! - users.update(age2 |= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" | \"age\")", 4) - } - - func test_bitwiseOrEquals_withIntegerValue_buildsSetter() { - users.update(age |= 1)! - users.update(age2 |= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" | 1)", 2) - } - - func test_bitwiseExclusiveOrEquals_withIntegerExpression_buildsSetter() { - users.update(age ^= age)! - users.update(age ^= age2)! - users.update(age2 ^= age)! - users.update(age2 ^= age2)! - AssertSQL("UPDATE \"users\" SET \"age\" = (~((\"age\" & \"age\")) & (\"age\" | \"age\"))", 4) - } - - func test_bitwiseExclusiveOrEquals_withIntegerValue_buildsSetter() { - users.update(age ^= 1)! - users.update(age2 ^= 1)! - AssertSQL("UPDATE \"users\" SET \"age\" = (~((\"age\" & 1)) & (\"age\" | 1))", 2) - } - - func test_postfixPlus_withIntegerValue_buildsSetter() { - users.update(age++)! - users.update(age2++)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" + 1)", 2) - } - - func test_postfixMinus_withIntegerValue_buildsSetter() { - users.update(age--)! - users.update(age2--)! - AssertSQL("UPDATE \"users\" SET \"age\" = (\"age\" - 1)", 2) - } - - func test_precedencePreserved() { - let n = Expression(value: 1) - AssertSQLContains("(((1 = 1) AND (1 = 1)) OR (1 = 1))", (n == n && n == n) || n == n) - AssertSQLContains("((1 = 1) AND ((1 = 1) OR (1 = 1)))", n == n && (n == n || n == n)) - } - -} diff --git a/SQLite Tests/FTSTests.swift b/SQLite Tests/FTSTests.swift deleted file mode 100644 index d3208c14..00000000 --- a/SQLite Tests/FTSTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -import XCTest -import SQLite - -let subject = Expression("subject") -let body = Expression("body") - -class FTSTests: SQLiteTestCase { - - var emails: Query { return db["emails"] } - - func test_createVtable_usingFts4_createsVirtualTable() { - db.create(vtable: emails, using: fts4(subject, body)) - - AssertSQL("CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\")") - } - - func test_createVtable_usingFts4_withPorterTokenizer_createsVirtualTableWithTokenizer() { - db.create(vtable: emails, using: fts4([subject, body], tokenize: .Porter)) - - AssertSQL("CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=porter)") - } - - func test_match_withColumnExpression_buildsMatchExpressionWithColumnIdentifier() { - db.create(vtable: emails, using: fts4(subject, body)) - - AssertSQL("SELECT * FROM \"emails\" WHERE (\"subject\" MATCH 'hello')", emails.filter(match("hello", subject))) - } - - func test_match_withQuery_buildsMatchExpressionWithTableIdentifier() { - db.create(vtable: emails, using: fts4(subject, body)) - - AssertSQL("SELECT * FROM \"emails\" WHERE (\"emails\" MATCH 'hello')", emails.filter(match("hello", emails))) - } - - func test_registerTokenizer_registersTokenizer() { - let locale = CFLocaleCopyCurrent() - let tokenizer = CFStringTokenizerCreate(nil, "", CFRangeMake(0, 0), UInt(kCFStringTokenizerUnitWord), locale) - - db.register(tokenizer: "tokenizer") { 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) - var token = CFStringCreateMutableCopy(nil, range.length, input) - CFStringLowercase(token, locale) - CFStringTransform(token, nil, kCFStringTransformStripDiacritics, 0) - return (token as String, string.rangeOfString(input as String)!) - } - - db.create(vtable: emails, using: fts4([subject, body], tokenize: .Custom("tokenizer"))) - - AssertSQL("CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=\"SQLite.swift\" 'tokenizer')") - - emails.insert(subject <- "Aún más cáfe!")! - - XCTAssertEqual(1, emails.filter(match("aun", emails)).count) - } - -} diff --git a/SQLite Tests/FunctionsTests.swift b/SQLite Tests/FunctionsTests.swift deleted file mode 100644 index 5a2e3386..00000000 --- a/SQLite Tests/FunctionsTests.swift +++ /dev/null @@ -1,164 +0,0 @@ -import XCTest -import SQLite - -class FunctionsTests: SQLiteTestCase { - - func test_createFunction_withZeroArguments() { - let f1: () -> Expression = db.create(function: "f1") { true } - let f2: () -> Expression = db.create(function: "f2") { nil } - - let table = db["table"] - db.create(table: table) { $0.column(Expression("id"), primaryKey: true) } - table.insert()! - - XCTAssert(table.select(f1()).first![f1()]) - AssertSQL("SELECT \"f1\"() FROM \"table\" LIMIT 1") - - XCTAssertNil(table.select(f2()).first![f2()]) - AssertSQL("SELECT \"f2\"() FROM \"table\" LIMIT 1") - } - - func test_createFunction_withOneArgument() { - let f1: Expression -> Expression = db.create(function: "f1") { _ in return true } - let f2: Expression -> Expression = db.create(function: "f2") { _ in return false } - let f3: Expression -> Expression = db.create(function: "f3") { _ in return true } - let f4: Expression -> Expression = db.create(function: "f4") { _ in return nil } - - let table = db["table"] - let s1 = Expression("s1") - let s2 = Expression("s2") - db.create(table: table) { t in - t.column(s1) - t.column(s2) - } - table.insert(s1 <- "s1")! - - let null = Expression(value: nil as String?) - - XCTAssert(table.select(f1(s1)).first![f1(s1)]) - AssertSQL("SELECT \"f1\"(\"s1\") FROM \"table\" LIMIT 1") - - XCTAssert(!table.select(f2(s2)).first![f2(s2)]) - AssertSQL("SELECT \"f2\"(\"s2\") FROM \"table\" LIMIT 1") - - XCTAssert(table.select(f3(s1)).first![f3(s1)]!) - AssertSQL("SELECT \"f3\"(\"s1\") FROM \"table\" LIMIT 1") - - XCTAssertNil(table.select(f4(null)).first![f4(null)]) - AssertSQL("SELECT \"f4\"(NULL) FROM \"table\" LIMIT 1") - } - - func test_createFunction_withValueArgument() { - let f1: Expression -> Expression = ( - db.create(function: "f1") { (a: Bool) -> Bool in - return a - } - ) - - let table = db["table"] - let b = Expression("b") - db.create(table: table) { t in - t.column(b) - } - table.insert(b <- true)! - - XCTAssert(table.select(f1(b)).first![f1(b)]) - AssertSQL("SELECT \"f1\"(\"b\") FROM \"table\" LIMIT 1") - } - - func test_createFunction_withTwoArguments() { - let table = db["table"] - let b1 = Expression("b1") - let b2 = Expression("b2") - db.create(table: table) { t in - t.column(b1) - t.column(b2) - } - table.insert(b1 <- true)! - - let f1: (Bool, Expression) -> Expression = db.create(function: "f1") { $0 && $1 } - let f2: (Bool?, Expression) -> Expression = db.create(function: "f2") { $0 ?? $1 } - let f3: (Bool, Expression) -> Expression = db.create(function: "f3") { $0 && $1 != nil } - let f4: (Bool?, Expression) -> Expression = db.create(function: "f4") { $0 ?? $1 != nil } - - XCTAssert(table.select(f1(true, b1)).first![f1(true, b1)]) - AssertSQL("SELECT \"f1\"(1, \"b1\") FROM \"table\" LIMIT 1") - XCTAssert(table.select(f2(nil, b1)).first![f2(nil, b1)]) - AssertSQL("SELECT \"f2\"(NULL, \"b1\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f3(false, b2)).first![f3(false, b2)]) - AssertSQL("SELECT \"f3\"(0, \"b2\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f4(nil, b2)).first![f4(nil, b2)]) - AssertSQL("SELECT \"f4\"(NULL, \"b2\") FROM \"table\" LIMIT 1") - - let f5: (Bool, Expression) -> Expression = db.create(function: "f5") { $0 && $1 } - let f6: (Bool?, Expression) -> Expression = db.create(function: "f6") { $0 ?? $1 } - let f7: (Bool, Expression) -> Expression = db.create(function: "f7") { $0 && $1 != nil } - let f8: (Bool?, Expression) -> Expression = db.create(function: "f8") { $0 ?? $1 != nil } - - XCTAssert(table.select(f5(true, b1)).first![f5(true, b1)]!) - AssertSQL("SELECT \"f5\"(1, \"b1\") FROM \"table\" LIMIT 1") - XCTAssert(table.select(f6(nil, b1)).first![f6(nil, b1)]!) - AssertSQL("SELECT \"f6\"(NULL, \"b1\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f7(false, b2)).first![f7(false, b2)]!) - AssertSQL("SELECT \"f7\"(0, \"b2\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f8(nil, b2)).first![f8(nil, b2)]!) - AssertSQL("SELECT \"f8\"(NULL, \"b2\") FROM \"table\" LIMIT 1") - - let f9: (Expression, Expression) -> Expression = db.create(function: "f9") { $0 && $1 } - let f10: (Expression, Expression) -> Expression = db.create(function: "f10") { $0 ?? $1 } - let f11: (Expression, Expression) -> Expression = db.create(function: "f11") { $0 && $1 != nil } - let f12: (Expression, Expression) -> Expression = db.create(function: "f12") { $0 ?? $1 != nil } - - XCTAssert(table.select(f9(b1, b1)).first![f9(b1, b1)]) - AssertSQL("SELECT \"f9\"(\"b1\", \"b1\") FROM \"table\" LIMIT 1") - XCTAssert(table.select(f10(b2, b1)).first![f10(b2, b1)]) - AssertSQL("SELECT \"f10\"(\"b2\", \"b1\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f11(b1, b2)).first![f11(b1, b2)]) - AssertSQL("SELECT \"f11\"(\"b1\", \"b2\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f12(b2, b2)).first![f12(b2, b2)]) - AssertSQL("SELECT \"f12\"(\"b2\", \"b2\") FROM \"table\" LIMIT 1") - - let f13: (Expression, Expression) -> Expression = db.create(function: "f13") { $0 && $1 } - let f14: (Expression, Expression) -> Expression = db.create(function: "f14") { $0 ?? $1 } - let f15: (Expression, Expression) -> Expression = db.create(function: "f15") { $0 && $1 != nil } - let f16: (Expression, Expression) -> Expression = db.create(function: "f16") { $0 ?? $1 != nil } - - XCTAssert(table.select(f13(b1, b1)).first![f13(b1, b1)]!) - AssertSQL("SELECT \"f13\"(\"b1\", \"b1\") FROM \"table\" LIMIT 1") - XCTAssert(table.select(f14(b2, b1)).first![f14(b2, b1)]!) - AssertSQL("SELECT \"f14\"(\"b2\", \"b1\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f15(b1, b2)).first![f15(b1, b2)]!) - AssertSQL("SELECT \"f15\"(\"b1\", \"b2\") FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f16(b2, b2)).first![f16(b2, b2)]!) - AssertSQL("SELECT \"f16\"(\"b2\", \"b2\") FROM \"table\" LIMIT 1") - - let f17: (Expression, Bool) -> Expression = db.create(function: "f17") { $0 && $1 } - let f18: (Expression, Bool) -> Expression = db.create(function: "f18") { $0 ?? $1 } - let f19: (Expression, Bool?) -> Expression = db.create(function: "f19") { $0 && $1 != nil } - let f20: (Expression, Bool?) -> Expression = db.create(function: "f20") { $0 ?? $1 != nil } - - XCTAssert(table.select(f17(b1, true)).first![f17(b1, true)]) - AssertSQL("SELECT \"f17\"(\"b1\", 1) FROM \"table\" LIMIT 1") - XCTAssert(table.select(f18(b2, true)).first![f18(b2, true)]) - AssertSQL("SELECT \"f18\"(\"b2\", 1) FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f19(b1, nil)).first![f19(b1, nil)]) - AssertSQL("SELECT \"f19\"(\"b1\", NULL) FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f20(b2, nil)).first![f20(b2, nil)]) - AssertSQL("SELECT \"f20\"(\"b2\", NULL) FROM \"table\" LIMIT 1") - - let f21: (Expression, Bool) -> Expression = db.create(function: "f21") { $0 && $1 } - let f22: (Expression, Bool) -> Expression = db.create(function: "f22") { $0 ?? $1 } - let f23: (Expression, Bool?) -> Expression = db.create(function: "f23") { $0 && $1 != nil } - let f24: (Expression, Bool?) -> Expression = db.create(function: "f24") { $0 ?? $1 != nil } - - XCTAssert(table.select(f21(b1, true)).first![f21(b1, true)]!) - AssertSQL("SELECT \"f21\"(\"b1\", 1) FROM \"table\" LIMIT 1") - XCTAssert(table.select(f22(b2, true)).first![f22(b2, true)]!) - AssertSQL("SELECT \"f22\"(\"b2\", 1) FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f23(b1, nil)).first![f23(b1, nil)]!) - AssertSQL("SELECT \"f23\"(\"b1\", NULL) FROM \"table\" LIMIT 1") - XCTAssertFalse(table.select(f24(b2, nil)).first![f24(b2, nil)]!) - AssertSQL("SELECT \"f24\"(\"b2\", NULL) FROM \"table\" LIMIT 1") - } - -} diff --git a/SQLite Tests/QueryTests.swift b/SQLite Tests/QueryTests.swift deleted file mode 100644 index b439316a..00000000 --- a/SQLite Tests/QueryTests.swift +++ /dev/null @@ -1,456 +0,0 @@ -import XCTest -import SQLite - -class QueryTests: SQLiteTestCase { - - override func setUp() { - createUsersTable() - - super.setUp() - } - - func test_select_withExpression_compilesSelectClause() { - AssertSQL("SELECT \"email\" FROM \"users\"", users.select(email)) - } - - func test_select_withVariadicExpressions_compilesSelectClause() { - AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select(email, count(*))) - } - - func test_select_withStar_resetsSelectClause() { - let query = users.select(email) - - AssertSQL("SELECT * FROM \"users\"", query.select(*)) - } - - func test_selectDistinct_withExpression_compilesSelectClause() { - AssertSQL("SELECT DISTINCT \"age\" FROM \"users\"", users.select(distinct: age)) - } - - func test_selectDistinct_withStar_compilesSelectClause() { - AssertSQL("SELECT DISTINCT * FROM \"users\"", users.select(distinct: *)) - } - - func test_select_withSubquery() { - let subquery = users.select(id) - - AssertSQL("SELECT (SELECT \"id\" FROM \"users\") FROM \"users\"", users.select(subquery)) - AssertSQL("SELECT (SELECT \"id\" FROM (\"users\") AS \"u\") AS \"u\" FROM \"users\"", - users.select(subquery.alias("u"))) - } - - func test_join_compilesJoinClause() { - let managers = db["users"].alias("managers") - - let SQL = "SELECT * FROM \"users\" " + - "INNER JOIN (\"users\") AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\")" - AssertSQL(SQL, users.join(managers, on: managers[id] == users[manager_id])) - } - - func test_join_withExplicitType_compilesJoinClauseWithType() { - let managers = db["users"].alias("managers") - - let SQL = "SELECT * FROM \"users\" " + - "LEFT OUTER JOIN (\"users\") AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\")" - AssertSQL(SQL, users.join(.LeftOuter, managers, on: managers[id] == users[manager_id])) - } - - func test_join_withTableCondition_compilesJoinClauseWithTableCondition() { - var managers = db["users"].alias("managers") - managers = managers.filter(managers[admin]) - - let SQL = "SELECT * FROM \"users\" " + - "INNER JOIN (\"users\") AS \"managers\" " + - "ON ((\"managers\".\"id\" = \"users\".\"manager_id\") " + - "AND \"managers\".\"admin\")" - AssertSQL(SQL, users.join(managers, on: managers[id] == users[manager_id])) - } - - func test_join_whenChained_compilesAggregateJoinClause() { - let managers = users.alias("managers") - let managed = users.alias("managed") - - let middleManagers = users - .join(managers, on: managers[id] == users[manager_id]) - .join(managed, on: managed[manager_id] == users[id]) - - let SQL = "SELECT * FROM \"users\" " + - "INNER JOIN (\"users\") AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\") " + - "INNER JOIN (\"users\") AS \"managed\" ON (\"managed\".\"manager_id\" = \"users\".\"id\")" - AssertSQL(SQL, middleManagers) - } - - func test_join_withNamespacedStar_expandsColumnNames() { - let managers = db["users"].alias("managers") - - let aliceId = users.insert(email <- "alice@example.com")! - users.insert(email <- "betty@example.com", manager_id <- Int64(aliceId))! - - let query = users - .select(users[*], managers[*]) - .join(managers, on: managers[id] == users[manager_id]) - - let SQL = "SELECT \"users\".*, \"managers\".* FROM \"users\" " + - "INNER JOIN (\"users\") AS \"managers\" " + - "ON (\"managers\".\"id\" = \"users\".\"manager_id\")" - AssertSQL(SQL, query) - } - - func test_join_withSubquery_joinsSubquery() { - insertUser("alice", age: 20) - - let maxId = max(id).alias("max_id") - let subquery = users.select(maxId).group(age) - let query = users.join(subquery, on: maxId == id) - - XCTAssertEqual(Int64(1), query.first![maxId]!) - - let SQL = "SELECT * FROM \"users\" " + - "INNER JOIN (SELECT (max(\"id\")) AS \"max_id\" FROM \"users\" GROUP BY \"age\") " + - "ON (\"max_id\" = \"id\") LIMIT 1" - AssertSQL(SQL, query) - } - - func test_join_withAliasedSubquery_joinsSubquery() { - insertUser("alice", age: 20) - - let maxId = max(id).alias("max_id") - let subquery = users.select(maxId).group(age).alias("u") - let query = users.join(subquery, on: subquery[maxId] == id) - - XCTAssertEqual(Int64(1), query.first![subquery[maxId]]!) - - let SQL = "SELECT * FROM \"users\" " + - "INNER JOIN (SELECT (max(\"id\")) AS \"max_id\" FROM (\"users\") AS \"u\" GROUP BY \"age\") AS \"u\" " + - "ON (\"u\".\"max_id\" = \"id\") LIMIT 1" - AssertSQL(SQL, query) - } - - func test_namespacedColumnRowValueAccess() { - let aliceId = users.insert(email <- "alice@example.com")! - let bettyId = users.insert(email <- "betty@example.com", manager_id <- Int64(aliceId))! - - let alice = users.first! - XCTAssertEqual(Int64(aliceId), alice[id]) - - let managers = db["users"].alias("managers") - let query = users.join(managers, on: managers[id] == users[manager_id]) - - let betty = query.first! - XCTAssertEqual(alice[email], betty[managers[email]]) - } - - func test_aliasedColumnRowValueAccess() { - users.insert(email <- "alice@example.com")! - - let alias = email.alias("user_email") - let query = users.select(alias) - let alice = query.first! - - XCTAssertEqual("alice@example.com", alice[alias]) - } - - func test_filter_compilesWhereClause() { - AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(admin == true)) - } - - func test_filter_whenChained_compilesAggregateWhereClause() { - let query = users - .filter(email == "alice@example.com") - .filter(age >= 21) - - let SQL = "SELECT * FROM \"users\" " + - "WHERE ((\"email\" = 'alice@example.com') " + - "AND (\"age\" >= 21))" - AssertSQL(SQL, query) - } - - 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 (\"age\" >= 30)", users.group(age, having: age >= 30)) - } - - func test_group_withExpressionNamesAndHavingBindings_compilesGroupClause() { - 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_withExpressionAndSortDirection_compilesOrderClause() { - AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age.desc, email.asc)) - } - - func test_order_whenChained_overridesOrder() { - 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 10", query.limit(10, offset: 10)) - AssertSQL("SELECT * FROM \"users\"", query.limit(nil)) - } - - func test_alias_compilesAliasInSelectClause() { - AssertSQL("SELECT * FROM (\"users\") AS \"managers\"", users.alias("managers")) - } - - func test_subscript_withExpression_returnsNamespacedExpression() { - AssertSQL("SELECT \"users\".\"admin\" FROM \"users\"", users.select(users[admin])) - AssertSQL("SELECT \"users\".\"salary\" FROM \"users\"", users.select(users[salary])) - AssertSQL("SELECT \"users\".\"age\" FROM \"users\"", users.select(users[age])) - AssertSQL("SELECT \"users\".\"email\" FROM \"users\"", users.select(users[email])) - AssertSQL("SELECT \"users\".* FROM \"users\"", users.select(users[*])) - } - - func test_subscript_withAliasAndExpression_returnsAliasedExpression() { - let managers = users.alias("managers") - AssertSQL("SELECT \"managers\".\"admin\" FROM (\"users\") AS \"managers\"", managers.select(managers[admin])) - AssertSQL("SELECT \"managers\".\"salary\" FROM (\"users\") AS \"managers\"", managers.select(managers[salary])) - AssertSQL("SELECT \"managers\".\"age\" FROM (\"users\") AS \"managers\"", managers.select(managers[age])) - AssertSQL("SELECT \"managers\".\"email\" FROM (\"users\") AS \"managers\"", managers.select(managers[email])) - AssertSQL("SELECT \"managers\".* FROM (\"users\") AS \"managers\"", managers.select(managers[*])) - } - - func test_SQL_compilesProperly() { - var managers = users.alias("managers") - // TODO: automatically namespace in the future? - managers = managers.filter(managers[admin] == true) - - let query = users - .select(users[email], count(users[age])) - .join(.LeftOuter, managers, on: managers[id] == users[manager_id]) - .filter(21..<32 ~= users[age]) - .group(users[age], having: count(users[age]) > 1) - .order(users[email].desc) - .limit(1, offset: 2) - - let SQL = "SELECT \"users\".\"email\", count(\"users\".\"age\") FROM \"users\" " + - "LEFT OUTER JOIN (\"users\") AS \"managers\" " + - "ON ((\"managers\".\"id\" = \"users\".\"manager_id\") AND (\"managers\".\"admin\" = 1)) " + - "WHERE \"users\".\"age\" BETWEEN 21 AND 32 " + - "GROUP BY \"users\".\"age\" HAVING (count(\"users\".\"age\") > 1) " + - "ORDER BY \"users\".\"email\" DESC " + - "LIMIT 1 " + - "OFFSET 2" - AssertSQL(SQL, query) - } - - func test_first_withAnEmptyQuery_returnsNil() { - XCTAssert(users.first == nil) - } - - func test_first_returnsTheFirstRow() { - insertUsers("alice", "betsy") - - XCTAssertEqual(1, users.first![id]) - AssertSQL("SELECT * FROM \"users\" LIMIT 1") - } - - func test_isEmpty_returnsWhetherOrNotTheQueryIsEmpty() { - XCTAssertTrue(users.isEmpty) - - insertUser("alice") - - XCTAssertFalse(users.isEmpty) - - AssertSQL("SELECT * FROM \"users\" LIMIT 1", 2) - } - - func test_insert_insertsRows() { - XCTAssertEqual(1, users.insert(email <- "alice@example.com", age <- 30).rowid!) - - AssertSQL("INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)") - - XCTAssert(users.insert(email <- "alice@example.com", age <- 30).rowid == nil) - } - - func test_insert_withQuery_insertsRows() { - db.execute("CREATE TABLE \"emails\" (\"email\" TEXT)") - let emails = db["emails"] - let admins = users.select(email).filter(admin == true) - - emails.insert(admins)! - AssertSQL("INSERT INTO \"emails\" SELECT \"email\" FROM \"users\" WHERE (\"admin\" = 1)") - } - - func test_insert_insertsDefaultRow() { - db.execute("CREATE TABLE \"timestamps\" (\"id\" INTEGER PRIMARY KEY, \"timestamp\" TEXT DEFAULT CURRENT_DATETIME)") - let table = db["timestamps"] - - XCTAssertEqual(1, table.insert().rowid!) - AssertSQL("INSERT INTO \"timestamps\" DEFAULT VALUES") - } - - func test_replace_replaceRows() { - XCTAssertEqual(1, users.replace(email <- "alice@example.com", age <- 30).rowid!) - AssertSQL("INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)") - - XCTAssertEqual(1, users.replace(id <- 1, email <- "betty@example.com", age <- 30).rowid!) - AssertSQL("INSERT OR REPLACE INTO \"users\" (\"id\", \"email\", \"age\") VALUES (1, 'betty@example.com', 30)") - } - - func test_update_updatesRows() { - insertUsers("alice", "betsy") - insertUser("dolly", admin: true) - - XCTAssertEqual(2, users.filter(!admin).update(age <- 30, admin <- true).changes!) - XCTAssertEqual(0, users.filter(!admin).update(age <- 30, admin <- true).changes!) - } - - func test_delete_deletesRows() { - insertUser("alice", age: 20) - XCTAssertEqual(0, users.filter(email == "betsy@example.com").delete().changes!) - - insertUser("betsy", age: 30) - XCTAssertEqual(2, users.delete().changes!) - XCTAssertEqual(0, users.delete().changes!) - } - - func test_count_returnsCount() { - XCTAssertEqual(0, users.count) - - insertUser("alice") - XCTAssertEqual(1, users.count) - XCTAssertEqual(0, users.filter(age != nil).count) - } - - func test_count_withExpression_returnsCount() { - insertUser("alice", age: 20) - insertUser("betsy", age: 20) - insertUser("cindy") - - XCTAssertEqual(2, users.count(age)) - XCTAssertEqual(1, users.count(distinct: age)) - } - - func test_max_withInt_returnsMaximumInt() { - XCTAssert(users.max(age) == nil) - - insertUser("alice", age: 20) - insertUser("betsy", age: 30) - XCTAssertEqual(30, users.max(age)!) - } - - func test_min_withInt_returnsMinimumInt() { - XCTAssert(users.min(age) == nil) - - insertUser("alice", age: 20) - insertUser("betsy", age: 30) - XCTAssertEqual(20, users.min(age)!) - } - - func test_averageWithInt_returnsDouble() { - XCTAssert(users.average(age) == nil) - - insertUser("alice", age: 20) - insertUser("betsy", age: 50) - insertUser("cindy", age: 50) - XCTAssertEqual(40.0, users.average(age)!) - XCTAssertEqual(35.0, users.average(distinct: age)!) - } - - func test_sum_returnsSum() { - XCTAssert(users.sum(age) == nil) - - insertUser("alice", age: 20) - insertUser("betsy", age: 30) - insertUser("cindy", age: 30) - XCTAssertEqual(80, users.sum(age)!) - XCTAssertEqual(50, users.sum(distinct: age)!) - } - - func test_total_returnsTotal() { - XCTAssertEqual(0.0, users.total(age)) - - insertUser("alice", age: 20) - insertUser("betsy", age: 30) - insertUser("cindy", age: 30) - XCTAssertEqual(80.0, users.total(age)) - XCTAssertEqual(50.0, users.total(distinct: age)) - } - - func test_row_withBoundColumn_returnsValue() { - insertUser("alice", age: 20) - XCTAssertEqual(21, users.select(age + 1).first![age + 1]!) - } - - func test_valueExtension_serializesAndDeserializes() { - let id = Expression("id") - let timestamp = Expression("timestamp") - let touches = db["touches"] - db.create(table: touches) { t in - t.column(id, primaryKey: true) - t.column(timestamp) - } - - let date = NSDate(timeIntervalSince1970: 0) - touches.insert(timestamp <- date)! - XCTAssertEqual(touches.first!.get(timestamp)!, date) - - XCTAssertNil(touches.filter(id == Int64(touches.insert()!)).first!.get(timestamp)) - - XCTAssert(touches.filter(timestamp < NSDate()).first != nil) - } - -} - -private let formatter: NSDateFormatter = { - let formatter = NSDateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") - formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) - return formatter -}() - -extension NSDate: Value { - - public class var declaredDatatype: String { return String.declaredDatatype } - - public class func fromDatatypeValue(datatypeValue: String) -> NSDate { - return formatter.dateFromString(datatypeValue)! - } - - public var datatypeValue: String { - return formatter.stringFromDate(self) - } - -} diff --git a/SQLite Tests/RTreeTests.swift b/SQLite Tests/RTreeTests.swift deleted file mode 100644 index fe5feb57..00000000 --- a/SQLite Tests/RTreeTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import XCTest -import SQLite - -let minX = Expression("minX") -let maxX = Expression("maxX") -let minY = Expression("minY") -let maxY = Expression("maxY") - -class RTreeTests: SQLiteTestCase { - - var index: Query { return db["index"] } - - func test_createVtable_usingRtree_createsVirtualTable() { - db.create(vtable: index, using: rtree(id, minX, maxX, minY, maxY)) - - AssertSQL("CREATE VIRTUAL TABLE \"index\" USING rtree(\"id\", \"minX\", \"maxX\", \"minY\", \"maxY\")") - } - -} diff --git a/SQLite Tests/SchemaTests.swift b/SQLite Tests/SchemaTests.swift deleted file mode 100644 index b65aed0a..00000000 --- a/SQLite Tests/SchemaTests.swift +++ /dev/null @@ -1,372 +0,0 @@ -import XCTest -import SQLite - -class SchemaTests: SQLiteTestCase { - - override func setUp() { - db.foreignKeys = true - - super.setUp() - } - - func test_createTable_createsTable() { - db.create(table: users) { $0.column(age) } - - AssertSQL("CREATE TABLE \"users\" (\"age\" INTEGER)") - } - - func test_createTable_temporary_createsTemporaryTable() { - db.create(table: users, temporary: true) { $0.column(age) } - - AssertSQL("CREATE TEMPORARY TABLE \"users\" (\"age\" INTEGER)") - } - - func test_createTable_ifNotExists_createsTableIfNotExists() { - db.create(table: users, ifNotExists: true) { $0.column(age) } - - AssertSQL("CREATE TABLE IF NOT EXISTS \"users\" (\"age\" INTEGER)") - } - - func test_createTable_column_buildsColumnDefinition() { - db.create(table: users) { $0.column(email) } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT NOT NULL)") - } - - func test_createTable_column_nonIntegerPrimaryKey() { - db.create(table: users) { $0.column(email, primaryKey: true) } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT PRIMARY KEY NOT NULL)") - } - - func test_createTable_column_nonIntegerPrimaryKey_withDefaultValue() { - let uuid = Expression("uuid") - let uuidgen: () -> Expression = db.create(function: "uuidgen") { - return NSUUID().UUIDString - } - db.create(table: users) { $0.column(uuid, primaryKey: true, defaultValue: uuidgen()) } - - AssertSQL("CREATE TABLE \"users\" (\"uuid\" TEXT PRIMARY KEY NOT NULL DEFAULT (\"uuidgen\"()))") - } - - func test_createTable_column_withPrimaryKey_buildsPrimaryKeyClause() { - db.create(table: users) { $0.column(id, primaryKey: true) } - - AssertSQL("CREATE TABLE \"users\" (\"id\" INTEGER PRIMARY KEY NOT NULL)") - } - - func test_createTable_column_withPrimaryKey_buildsPrimaryKeyAutoincrementClause() { - db.create(table: users) { $0.column(id, primaryKey: .Autoincrement) } - - AssertSQL("CREATE TABLE \"users\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)") - } - - func test_createTable_column_withNullFalse_buildsNotNullClause() { - db.create(table: users) { $0 .column(email) } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT NOT NULL)") - } - - func test_createTable_column_withUnique_buildsUniqueClause() { - db.create(table: users) { $0.column(email, unique: true) } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT NOT NULL UNIQUE)") - } - - func test_createTable_column_withCheck_buildsCheckClause() { - db.create(table: users) { $0.column(admin, check: contains([false, true], admin)) } - - AssertSQL("CREATE TABLE \"users\" (\"admin\" INTEGER NOT NULL CHECK ((\"admin\" IN (0, 1))))") - } - - func test_createTable_column_withDefaultValue_buildsDefaultClause() { - db.create(table: users) { $0.column(salary, defaultValue: 0) } - - AssertSQL("CREATE TABLE \"users\" (\"salary\" REAL NOT NULL DEFAULT 0.0)") - } - - func test_createTable_stringColumn_collation_buildsCollateClause() { - db.create(table: users) { $0.column(email, collate: .Nocase) } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT NOT NULL COLLATE NOCASE)") - } - - func test_createTable_intColumn_referencingNamespacedColumn_buildsReferencesClause() { - db.create(table: users) { t in - t.column(id, primaryKey: true) - t.column(manager_id, references: users[id]) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER PRIMARY KEY NOT NULL, " + - "\"manager_id\" INTEGER REFERENCES \"users\"(\"id\"))") - } - - func test_createTable_intColumn_referencingQuery_buildsReferencesClause() { - db.create(table: users) { t in - t.column(id, primaryKey: true) - t.column(manager_id, references: users) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER PRIMARY KEY NOT NULL, " + - "\"manager_id\" INTEGER REFERENCES \"users\")") - } - - func test_createTable_primaryKey_buildsPrimaryKeyTableConstraint() { - db.create(table: users) { t in - t.column(email) - t.primaryKey(email) - } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT NOT NULL, PRIMARY KEY(\"email\"))") - } - - func test_createTable_primaryKey_buildsCompositePrimaryKeyTableConstraint() { - db.create(table: users) { t in - t.column(id) - t.column(email) - t.primaryKey(id, email) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER NOT NULL, \"email\" TEXT NOT NULL, PRIMARY KEY(\"id\", \"email\"))") - } - - func test_createTable_unique_buildsUniqueTableConstraint() { - db.create(table: users) { t in - t.column(email) - t.unique(email) - } - - AssertSQL("CREATE TABLE \"users\" (\"email\" TEXT NOT NULL, UNIQUE(\"email\"))") - } - - func test_createTable_unique_buildsCompositeUniqueTableConstraint() { - db.create(table: users) { t in - t.column(id) - t.column(email) - t.unique(id, email) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER NOT NULL, \"email\" TEXT NOT NULL, UNIQUE(\"id\", \"email\"))") - } - - func test_createTable_check_buildsCheckTableConstraint() { - db.create(table: users) { t in - t.column(admin) - t.check(contains([false, true], admin)) - } - - AssertSQL("CREATE TABLE \"users\" (\"admin\" INTEGER NOT NULL, CHECK ((\"admin\" IN (0, 1))))") - } - - func test_createTable_foreignKey_referencingNamespacedColumn_buildsForeignKeyTableConstraint() { - db.create(table: users) { t in - t.column(id, primaryKey: true) - t.column(manager_id) - t.foreignKey(manager_id, references: users[id]) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER PRIMARY KEY NOT NULL, " + - "\"manager_id\" INTEGER, " + - "FOREIGN KEY(\"manager_id\") REFERENCES \"users\"(\"id\"))") - } - - func test_createTable_foreignKey_withUpdateDependency_buildsUpdateDependency() { - db.create(table: users) { t in - t.column(id, primaryKey: true) - t.column(manager_id) - t.foreignKey(manager_id, references: users[id], update: .Cascade) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER PRIMARY KEY NOT NULL, " + - "\"manager_id\" INTEGER, " + - "FOREIGN KEY(\"manager_id\") REFERENCES \"users\"(\"id\") ON UPDATE CASCADE)") - } - - func test_create_foreignKey_withDeleteDependency_buildsDeleteDependency() { - db.create(table: users) { t in - t.column(id, primaryKey: true) - t.column(manager_id) - t.foreignKey(manager_id, references: users[id], delete: .Cascade) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER PRIMARY KEY NOT NULL, " + - "\"manager_id\" INTEGER, " + - "FOREIGN KEY(\"manager_id\") REFERENCES \"users\"(\"id\") ON DELETE CASCADE)") - } - - func test_createTable_foreignKey_withCompositeKey_buildsForeignKeyTableConstraint() { - let manager_id = Expression("manager_id") // required - - db.create(table: users) { t in - t.column(id, primaryKey: true) - t.column(manager_id) - t.column(email) - t.foreignKey((manager_id, email), references: (users[id], email)) - } - - AssertSQL("CREATE TABLE \"users\" (" + - "\"id\" INTEGER PRIMARY KEY NOT NULL, " + - "\"manager_id\" INTEGER NOT NULL, " + - "\"email\" TEXT NOT NULL, " + - "FOREIGN KEY(\"manager_id\", \"email\") REFERENCES \"users\"(\"id\", \"email\"))") - } - - func test_createTable_withQuery_createsTableWithQuery() { - createUsersTable() - - db.create(table: db["emails"], from: users.select(email)) - AssertSQL("CREATE TABLE \"emails\" AS SELECT \"email\" FROM \"users\"") - - db.create(table: db["emails"], temporary: true, ifNotExists: true, from: users.select(email)) - AssertSQL("CREATE TEMPORARY TABLE IF NOT EXISTS \"emails\" AS SELECT \"email\" FROM \"users\"") - } - - func test_alterTable_renamesTable() { - createUsersTable() - let people = db["people"] - - db.rename(table: "users", to: people) - AssertSQL("ALTER TABLE \"users\" RENAME TO \"people\"") - } - - func test_alterTable_addsNotNullColumn() { - createUsersTable() - let column = Expression("bonus") - - db.alter(table: users, add: column, defaultValue: 0) - AssertSQL("ALTER TABLE \"users\" ADD COLUMN \"bonus\" REAL NOT NULL DEFAULT 0.0") - } - - func test_alterTable_addsRegularColumn() { - createUsersTable() - let column = Expression("bonus") - - db.alter(table: users, add: column) - AssertSQL("ALTER TABLE \"users\" ADD COLUMN \"bonus\" REAL") - } - - func test_alterTable_withDefaultValue_addsRegularColumn() { - createUsersTable() - let column = Expression("bonus") - - db.alter(table: users, add: column, defaultValue: 0) - AssertSQL("ALTER TABLE \"users\" ADD COLUMN \"bonus\" REAL DEFAULT 0.0") - } - - func test_alterTable_withForeignKey_addsRegularColumn() { - createUsersTable() - let column = Expression("parent_id") - - db.alter(table: users, add: column, references: users[id]) - AssertSQL("ALTER TABLE \"users\" ADD COLUMN \"parent_id\" INTEGER REFERENCES \"users\"(\"id\")") - } - - func test_alterTable_stringColumn_collation_buildsCollateClause() { - createUsersTable() - let columnA = Expression("column_a") - let columnB = Expression("column_b") - - db.alter(table: users, add: columnA, defaultValue: "", collate: .Nocase) - AssertSQL("ALTER TABLE \"users\" ADD COLUMN \"column_a\" TEXT NOT NULL DEFAULT '' COLLATE \"NOCASE\"") - - db.alter(table: users, add: columnB, collate: .Nocase) - AssertSQL("ALTER TABLE \"users\" ADD COLUMN \"column_b\" TEXT COLLATE \"NOCASE\"") - } - - func test_dropTable_dropsTable() { - createUsersTable() - - db.drop(table: users) - AssertSQL("DROP TABLE \"users\"") - - db.drop(table: users, ifExists: true) - AssertSQL("DROP TABLE IF EXISTS \"users\"") - } - - func test_index_executesIndexStatement() { - createUsersTable() - - db.create(index: users, on: email) - AssertSQL("CREATE INDEX \"index_users_on_email\" ON \"users\" (\"email\")") - } - - func test_index_withUniqueness_executesUniqueIndexStatement() { - createUsersTable() - - db.create(index: users, unique: true, on: email) - AssertSQL("CREATE UNIQUE INDEX \"index_users_on_email\" ON \"users\" (\"email\")") - } - - func test_index_ifNotExists_executesIndexStatement() { - createUsersTable() - - db.create(index: users, ifNotExists: true, on: email) - AssertSQL("CREATE INDEX IF NOT EXISTS \"index_users_on_email\" ON \"users\" (\"email\")") - } - - func test_index_withMultipleColumns_executesCompoundIndexStatement() { - createUsersTable() - - db.create(index: users, on: age.desc, email) - AssertSQL("CREATE INDEX \"index_users_on_age_email\" ON \"users\" (\"age\" DESC, \"email\")") - } - -// func test_index_withFilter_executesPartialIndexStatementWithWhereClause() { -// if SQLITE_VERSION >= "3.8" { -// CreateUsersTable(db) -// ExpectExecution(db, -// "CREATE INDEX index_users_on_age ON \"users\" (age) WHERE admin", -// db.create(index: users.filter(admin), on: age) -// ) -// } -// } - - func test_dropIndex_dropsIndex() { - createUsersTable() - db.create(index: users, on: email) - - db.drop(index: users, on: email) - AssertSQL("DROP INDEX \"index_users_on_email\"") - - db.drop(index: users, ifExists: true, on: email) - AssertSQL("DROP INDEX IF EXISTS \"index_users_on_email\"") - } - - func test_createView_withQuery_createsViewWithQuery() { - createUsersTable() - - db.create(view: db["emails"], from: users.select(email)) - AssertSQL("CREATE VIEW \"emails\" AS SELECT \"email\" FROM \"users\"") - - db.create(view: db["emails"], temporary: true, ifNotExists: true, from: users.select(email)) - AssertSQL("CREATE TEMPORARY VIEW IF NOT EXISTS \"emails\" AS SELECT \"email\" FROM \"users\"") - } - - func test_dropView_dropsView() { - createUsersTable() - let emails = db["emails"] - db.create(view: emails, from: users.select(email)) - - db.drop(view: emails) - AssertSQL("DROP VIEW \"emails\"") - - db.drop(view: emails, ifExists: true) - AssertSQL("DROP VIEW IF EXISTS \"emails\"") - } - - func test_quotedIdentifiers() { - let table = db["table"] - let column = Expression("My lil' primary key, \"Kiwi\"") - - db.create(table: table) { $0.column(column) } - AssertSQL("CREATE TABLE \"table\" (\"My lil' primary key, \"\"Kiwi\"\"\" INTEGER NOT NULL)") - } - -} diff --git a/SQLite Tests/StatementTests.swift b/SQLite Tests/StatementTests.swift deleted file mode 100644 index 5370aedc..00000000 --- a/SQLite Tests/StatementTests.swift +++ /dev/null @@ -1,160 +0,0 @@ -import XCTest -import SQLite - -class StatementTests: SQLiteTestCase { - - override func setUp() { - createUsersTable() - - super.setUp() - } - - func test_bind_withVariadicParameters_bindsParameters() { - let stmt = db.prepare("SELECT ?, ?, ?, ?") - withBlob { stmt.bind(1, 2.0, "3", $0) } - AssertSQL("SELECT 1, 2.0, '3', x'34'", stmt) - } - - func test_bind_withArrayOfParameters_bindsParameters() { - let stmt = db.prepare("SELECT ?, ?, ?, ?") - withBlob { stmt.bind([1, 2.0, "3", $0]) } - AssertSQL("SELECT 1, 2.0, '3', x'34'", stmt) - } - - func test_bind_withNamedParameters_bindsParameters() { - let stmt = db.prepare("SELECT ?1, ?2, ?3, ?4") - withBlob { stmt.bind(["?1": 1, "?2": 2.0, "?3": "3", "?4": $0]) } - AssertSQL("SELECT 1, 2.0, '3', x'34'", stmt) - } - - func test_bind_withBlob_bindsBlob() { - let stmt = db.prepare("SELECT ?") - withBlob { stmt.bind($0) } - AssertSQL("SELECT x'34'", stmt) - } - - func test_bind_withDouble_bindsDouble() { - AssertSQL("SELECT 2.0", db.prepare("SELECT ?").bind(2.0)) - } - - func test_bind_withInt_bindsInt() { - AssertSQL("SELECT 3", db.prepare("SELECT ?").bind(3)) - } - - func test_bind_withString() { - AssertSQL("SELECT '4'", db.prepare("SELECT ?").bind("4")) - } - - func test_run_withNoParameters() { - db.prepare("INSERT INTO users (email, admin) VALUES ('alice@example.com', 1)").run() - XCTAssertEqual(1, db.totalChanges) - } - - func test_run_withVariadicParameters() { - let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)") - stmt.run("alice@example.com", 1) - XCTAssertEqual(1, db.totalChanges) - } - - func test_run_withArrayOfParameters() { - let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)") - stmt.run(["alice@example.com", 1]) - XCTAssertEqual(1, db.totalChanges) - } - - func test_run_withNamedParameters() { - let stmt = db.prepare( - "INSERT INTO users (email, admin) VALUES ($email, $admin)" - ) - stmt.run(["$email": "alice@example.com", "$admin": 1]) - XCTAssertEqual(1, db.totalChanges) - } - - func test_scalar_withNoParameters() { - let zero = db.prepare("SELECT 0") - XCTAssertEqual(0, zero.scalar() as! Int64) - } - - func test_scalar_withNoParameters_retainsBindings() { - let count = db.prepare("SELECT count(*) FROM users WHERE age >= ?", 21) - XCTAssertEqual(0, count.scalar() as! Int64) - - insertUser("alice", age: 21) - XCTAssertEqual(1, count.scalar() as! Int64) - } - - func test_scalar_withVariadicParameters() { - insertUser( "alice", age: 21) - let count = db.prepare("SELECT count(*) FROM users WHERE age >= ?") - XCTAssertEqual(1, count.scalar(21) as! Int64) - } - - func test_scalar_withArrayOfParameters() { - insertUser( "alice", age: 21) - let count = db.prepare("SELECT count(*) FROM users WHERE age >= ?") - XCTAssertEqual(1, count.scalar([21]) as! Int64) - } - - func test_scalar_withNamedParameters() { - insertUser("alice", age: 21) - let count = db.prepare("SELECT count(*) FROM users WHERE age >= $age") - XCTAssertEqual(1, count.scalar(["$age": 21]) as! Int64) - } - - func test_scalar_withParameters_updatesBindings() { - insertUser("alice", age: 21) - let count = db.prepare("SELECT count(*) FROM users WHERE age >= ?") - XCTAssertEqual(1, count.scalar(21) as! Int64) - XCTAssertEqual(0, count.scalar(22) as! Int64) - } - - func test_scalar_doubleReturnValue() { - XCTAssertEqual(2.0, db.scalar("SELECT 2.0") as! Double) - } - - func test_scalar_intReturnValue() { - XCTAssertEqual(3, db.scalar("SELECT 3") as! Int64) - } - - func test_scalar_stringReturnValue() { - XCTAssertEqual("4", db.scalar("SELECT '4'") as! String) - } - - func test_generate_allowsIteration() { - insertUsers("alice", "betsy", "cindy") - var count = 0 - for row in db.prepare("SELECT id FROM users") { - XCTAssertEqual(1, row.count) - count++ - } - XCTAssertEqual(3, count) - } - - func test_generate_allowsReuse() { - insertUsers("alice", "betsy", "cindy") - var count = 0 - let stmt = db.prepare("SELECT id FROM users") - for row in stmt { count++ } - for row in stmt { count++ } - XCTAssertEqual(6, count) - } - - func test_row_returnsValues() { - insertUser("alice") - let stmt = db.prepare("SELECT id, email FROM users") - stmt.step() - - XCTAssertEqual(1, stmt.row[0] as Int64) - XCTAssertEqual("alice@example.com", stmt.row[1] as String) - } - -} - -func withBlob(block: Blob -> ()) { - let length = 1 - let buflen = Int(length) + 1 - let buffer = UnsafeMutablePointer<()>.alloc(buflen) - memcpy(buffer, "4", length) - block(Blob(bytes: buffer, length: length)) - buffer.dealloc(buflen) -} diff --git a/SQLite Tests/TestHelper.swift b/SQLite Tests/TestHelper.swift deleted file mode 100644 index 9d712c78..00000000 --- a/SQLite Tests/TestHelper.swift +++ /dev/null @@ -1,77 +0,0 @@ -import SQLite -import XCTest - -let id = Expression("id") -let email = Expression("email") -let age = Expression("age") -let salary = Expression("salary") -let admin = Expression("admin") -let manager_id = Expression("manager_id") - -class SQLiteTestCase: XCTestCase { - - var trace = [String: Int]() - - let db = Database() - - var users: Query { return db["users"] } - - override func setUp() { - super.setUp() - - db.trace { SQL in - println(SQL) - self.trace[SQL] = (self.trace[SQL] ?? 0) + 1 - } - } - - func createUsersTable() { - 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...) { - insertUsers(names) - } - - func insertUsers(names: [String]) { - for name in names { insertUser(name) } - } - - func insertUser(name: String, age: Int? = nil, admin: Bool = false) -> Statement { - return db.run( - "INSERT INTO \"users\" (email, age, admin) values (?, ?, ?)", - ["\(name)@example.com", age, admin.datatypeValue] - ) - } - - func AssertSQL(SQL: String, _ executions: Int = 1, _ message: String? = nil, file: String = __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: String = __FILE__, line: UInt = __LINE__) { - 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 } - } - -} diff --git a/SQLite.playground/Contents.swift b/SQLite.playground/Contents.swift new file mode 100644 index 00000000..d893e696 --- /dev/null +++ b/SQLite.playground/Contents.swift @@ -0,0 +1,132 @@ +import SQLite + +/// 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") // non-null +let name = Expression("name") // nullable + +/// 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 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 body = Expression("body") + +/// create the index +try db.run(emails.create(.FTS5( + FTS5Config() + .column(subject) + .column(body) +))) + +/// populate with data +try db.run(emails.insert( + subject <- "Hello, world!", + body <- "This is a hello world message." +)) + +/// run a query +let ftsQuery = try db.prepare(emails.match("hello")) + +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/Documentation/fragment-0.html b/SQLite.playground/Documentation/fragment-0.html deleted file mode 100644 index ef4c0d87..00000000 --- a/SQLite.playground/Documentation/fragment-0.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - -
- -

SQLite.swift

-

- This playground contains sample code to explore SQLite.swift, a Swift wrapper for SQLite3. -

-

- Let’s get started by importing the framework and opening a new in-memory database connection using the Database class. -

-
diff --git a/SQLite.playground/Documentation/fragment-1.html b/SQLite.playground/Documentation/fragment-1.html deleted file mode 100644 index 2d7b55c7..00000000 --- a/SQLite.playground/Documentation/fragment-1.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - -
-

- This implicitly opens a database in ":memory:". To open a database at a specific location, pass the path as a parameter during instantiation, e.g., Database("path/to/database.sqlite3"). Pass nil or an empty string ("") to open a temporary, disk-backed database, instead. -

-

- Once we instantiate a database connection, we can execute SQL statements directly against it. Let’s create a table. -

-
diff --git a/SQLite.playground/Documentation/fragment-10.html b/SQLite.playground/Documentation/fragment-10.html deleted file mode 100644 index 449e18b4..00000000 --- a/SQLite.playground/Documentation/fragment-10.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- The query-building interface is provided via the Query struct. We can access this interface by subscripting our database connection with a table name. -

-
diff --git a/SQLite.playground/Documentation/fragment-11.html b/SQLite.playground/Documentation/fragment-11.html deleted file mode 100644 index f9196504..00000000 --- a/SQLite.playground/Documentation/fragment-11.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- From here, we can build a variety of queries. For example, we can build and run an INSERT statement by calling the query’s insert function. Let’s add a few new rows this way. -

-
diff --git a/SQLite.playground/Documentation/fragment-12.html b/SQLite.playground/Documentation/fragment-12.html deleted file mode 100644 index 4b0633fe..00000000 --- a/SQLite.playground/Documentation/fragment-12.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-

- No room for syntax errors! Try changing an input to the wrong type and see what happens. -

-

- The insert function can return an ID (which will be nil in the case of failure) and the just-run statement. It can also return a Statement object directly, making it easy to run in a transaction. -

-
diff --git a/SQLite.playground/Documentation/fragment-13.html b/SQLite.playground/Documentation/fragment-13.html deleted file mode 100644 index 0341e633..00000000 --- a/SQLite.playground/Documentation/fragment-13.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Query objects can also build SELECT statements. A freshly-subscripted query will select every row (and every column) from a table. Iteration lazily executes the statement. -

-
diff --git a/SQLite.playground/Documentation/fragment-14.html b/SQLite.playground/Documentation/fragment-14.html deleted file mode 100644 index 716f2a76..00000000 --- a/SQLite.playground/Documentation/fragment-14.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-

- You may notice that iteration works a little differently here. Rather than arrays of raw values, we are given Row objects, which can be subscripted with the same expressions we prepared above. This gives us a little more powerful of a mapping to work with and pass around. -

-

- Queries can be used and reused, and can quickly return rows, counts and other aggregate values. -

-
diff --git a/SQLite.playground/Documentation/fragment-15.html b/SQLite.playground/Documentation/fragment-15.html deleted file mode 100644 index 850f2bf8..00000000 --- a/SQLite.playground/Documentation/fragment-15.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - -
- -

- Queries can be refined using a collection of chainable helper functions. Let’s filter our query to the administrator subset. -

-
diff --git a/SQLite.playground/Documentation/fragment-16.html b/SQLite.playground/Documentation/fragment-16.html deleted file mode 100644 index b19d01d4..00000000 --- a/SQLite.playground/Documentation/fragment-16.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Filtered queries will in turn filter their aggregate functions. -

-
diff --git a/SQLite.playground/Documentation/fragment-17.html b/SQLite.playground/Documentation/fragment-17.html deleted file mode 100644 index 9b4e1bde..00000000 --- a/SQLite.playground/Documentation/fragment-17.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Alongside filter, we can use the select, join, group, order, and limit functions to compose rich queries with safety and ease. Let’s say we want to order our results by email, then age, and return no more than three rows. -

-
diff --git a/SQLite.playground/Documentation/fragment-18.html b/SQLite.playground/Documentation/fragment-18.html deleted file mode 100644 index 967cf580..00000000 --- a/SQLite.playground/Documentation/fragment-18.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - -
- -

- We can further filter by chaining additional conditions onto the query. Let’s find administrators that haven’t (yet) provided their ages. -

-
diff --git a/SQLite.playground/Documentation/fragment-19.html b/SQLite.playground/Documentation/fragment-19.html deleted file mode 100644 index c6724957..00000000 --- a/SQLite.playground/Documentation/fragment-19.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Unfortunately, the HR department has ruled that age disclosure is required for administrator responsibilities. We can use our query’s update interface to (temporarily) revoke their privileges while we wait for them to update their profiles. -

-
diff --git a/SQLite.playground/Documentation/fragment-2.html b/SQLite.playground/Documentation/fragment-2.html deleted file mode 100644 index acf1bb08..00000000 --- a/SQLite.playground/Documentation/fragment-2.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-

- The execute function can run multiple SQL statements at once as a convenience and will throw an assertion failure if an error occurs during execution. This is useful for seeding and migrating databases with well-tested statements that are guaranteed to succeed (or where failure can be graceful and silent). -

-

- It’s generally safer to prepare SQL statements individually. Let’s instantiate a Statement object and insert a couple rows. -

-
diff --git a/SQLite.playground/Documentation/fragment-20.html b/SQLite.playground/Documentation/fragment-20.html deleted file mode 100644 index c3150e7b..00000000 --- a/SQLite.playground/Documentation/fragment-20.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-

- If we ever need to remove rows from our database, we can use the delete function, which will be scoped to a query’s filters. Be careful! You may just want to archive the records, instead. -

-

- We don’t archive user data at Acme Inc. (we respect privacy, after all), and unfortunately, Alice has decided to move on. We can carefully, carefully scope a query to match her and delete her record. -

-
diff --git a/SQLite.playground/Documentation/fragment-21.html b/SQLite.playground/Documentation/fragment-21.html deleted file mode 100644 index bea27da9..00000000 --- a/SQLite.playground/Documentation/fragment-21.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - -
-

- And that’s that. -

-

& More…

-

- We’ve only explored the surface to SQLite.swift. Dive into the code to discover more! -

-
diff --git a/SQLite.playground/Documentation/fragment-3.html b/SQLite.playground/Documentation/fragment-3.html deleted file mode 100644 index 8fff27e2..00000000 --- a/SQLite.playground/Documentation/fragment-3.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-

- Prepared statements can bind and escape input values safely. In this case, email and admin columns are bound with different values over two executions. -

-

- The Database class exposes information about recently run queries via several properties: totalChanges returns the total number of changes (inserts, updates, and deletes) since the connection was opened; lastChanges returns the number of changes from the last statement that modified the database; lastID returns the row ID of the last insert. -

-
diff --git a/SQLite.playground/Documentation/fragment-4.html b/SQLite.playground/Documentation/fragment-4.html deleted file mode 100644 index 988c0027..00000000 --- a/SQLite.playground/Documentation/fragment-4.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - -
-

Querying

-

- Statement objects act as both sequences and generators. We can iterate over a select statement’s rows directly using a forin loop. -

-
diff --git a/SQLite.playground/Documentation/fragment-5.html b/SQLite.playground/Documentation/fragment-5.html deleted file mode 100644 index a66a3352..00000000 --- a/SQLite.playground/Documentation/fragment-5.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Single, scalar values can be plucked directly from a statement. -

-
diff --git a/SQLite.playground/Documentation/fragment-6.html b/SQLite.playground/Documentation/fragment-6.html deleted file mode 100644 index 496e7aad..00000000 --- a/SQLite.playground/Documentation/fragment-6.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - -
- -

Transactions & Savepoints

-

- Using the transaction and savepoint functions, we can run a series of statements, commiting the changes to the database if they all succeed. If a single statement fails, we bail out early and roll back. In the following example we prepare two statements: one to insert a manager into the database, and one—given a manager’s row ID—to insert a managed user into the database. -

-
diff --git a/SQLite.playground/Documentation/fragment-7.html b/SQLite.playground/Documentation/fragment-7.html deleted file mode 100644 index 740dbb2b..00000000 --- a/SQLite.playground/Documentation/fragment-7.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Arguments sent to transaction are auto-closures and therefore have access to Database information at the time of execution. In this case, we insert Dolly, a supervisor, and immediately reference her row ID when we insert her assistant, Emery. -

-
diff --git a/SQLite.playground/Documentation/fragment-8.html b/SQLite.playground/Documentation/fragment-8.html deleted file mode 100644 index a5e1c9b2..00000000 --- a/SQLite.playground/Documentation/fragment-8.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
-

- Our database has a uniqueness constraint on email address, so let’s see what happens when we insert Fiona, who also claims to be managing Emery. -

-
diff --git a/SQLite.playground/Documentation/fragment-9.html b/SQLite.playground/Documentation/fragment-9.html deleted file mode 100644 index 2f90332f..00000000 --- a/SQLite.playground/Documentation/fragment-9.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - -
-

- This time, our transaction fails because Emery has already been added to the database. The addition of Fiona has been rolled back, and we’ll need to get to the bottom of this discrepancy (or make some schematic changes to our database to allow for multiple managers per user). -

- -

Query Building

-

- SQLite.swift provides a powerful, type-safe query builder. With only a small amount of boilerplate to map our columns to types, we can ensure the queries we build are valid upon compilation. -

-
diff --git a/SQLite.playground/Documentation/style-1.1.15.css b/SQLite.playground/Documentation/style-1.1.15.css deleted file mode 100644 index 12485e1d..00000000 --- a/SQLite.playground/Documentation/style-1.1.15.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a, -abbr,acronym,address,big,cite,code,del,dfn,em,figure,font,img,ins,kbd,q,s, -samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li, -fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td { - border: 0; - font-size: 100%; - margin: 0; - outline: 0; - padding: 0; - vertical-align: baseline -} -body { - background-color: rgba(255,255,255,0.65); - color: rgba(0,0,0,1); - font-family: Helvetica,sans-serif; - font-size: 62.5%; - margin-left: 15px; -} -section { - padding: 20px 25px 20px 35px -} -h3 { - color: rgba(128,128,128,1); - display: block; - font-size: 2.2em; - font-weight: 100; - margin-bottom: 15px; - margin-top: 20px; -} -p { - color: rgba(65,65,65,1); - font-size: 1.4em; - line-height: 145%; - margin-bottom: 5px; -} -a { - color: rgba(0,136,204,1); - text-decoration: none -} -code { - color: rgba(128,128,128,1); - font-family: Menlo,monospace; - font-size: .9em; - word-wrap: break-word -} -h4 { - color: rgba(128,128,128,1); - font-size: .6em; - font-weight: normal; - letter-spacing: 2px; - margin-bottom: 8px; - text-transform: uppercase -} -aside { - background-color: rgba(249,249,249,1); - border-left: 5px solid rgba(238,238,238,1); - font-size: 1.2em; - margin: 25px 45px 35px 35px; - padding: 15px 15px 7px; - -} -aside p { - font-size: 1em; - margin-bottom: 8px -} diff --git a/SQLite.playground/contents.xcplayground b/SQLite.playground/contents.xcplayground index e16a107d..441c60ef 100644 --- a/SQLite.playground/contents.xcplayground +++ b/SQLite.playground/contents.xcplayground @@ -1,49 +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.playground/section-10.swift b/SQLite.playground/section-10.swift deleted file mode 100644 index 316672a1..00000000 --- a/SQLite.playground/section-10.swift +++ /dev/null @@ -1,3 +0,0 @@ -for row in db.prepare("SELECT id, email FROM users") { - println("id: \(row[0]), email: \(row[1])") -} diff --git a/SQLite.playground/section-12.swift b/SQLite.playground/section-12.swift deleted file mode 100644 index 3f216be3..00000000 --- a/SQLite.playground/section-12.swift +++ /dev/null @@ -1,4 +0,0 @@ -let count = db.prepare("SELECT count(*) FROM users") -count.scalar() - -db.scalar("SELECT email FROM users WHERE id = ?", 1) diff --git a/SQLite.playground/section-14.swift b/SQLite.playground/section-14.swift deleted file mode 100644 index 2d93a99e..00000000 --- a/SQLite.playground/section-14.swift +++ /dev/null @@ -1,2 +0,0 @@ -let sr = db.prepare("INSERT INTO users (email, admin) VALUES (?, 1)") -let jr = db.prepare("INSERT INTO users (email, admin, manager_id) VALUES (?, 0, ?)") diff --git a/SQLite.playground/section-16.swift b/SQLite.playground/section-16.swift deleted file mode 100644 index 5bcfe10d..00000000 --- a/SQLite.playground/section-16.swift +++ /dev/null @@ -1,4 +0,0 @@ -db.transaction() && - sr.run("dolly@acme.com") && - jr.run("emery@acme.com", db.lastId) && - db.commit() || db.rollback() diff --git a/SQLite.playground/section-18.swift b/SQLite.playground/section-18.swift deleted file mode 100644 index 0aff1a5b..00000000 --- a/SQLite.playground/section-18.swift +++ /dev/null @@ -1,10 +0,0 @@ -let txn = db.transaction() && - sr.run("fiona@acme.com") && - jr.run("emery@acme.com", db.lastId) && - db.commit() -txn || db.rollback() - -count.scalar() - -txn.failed -txn.reason diff --git a/SQLite.playground/section-2.swift b/SQLite.playground/section-2.swift deleted file mode 100644 index e033f90b..00000000 --- a/SQLite.playground/section-2.swift +++ /dev/null @@ -1,3 +0,0 @@ -import SQLite - -let db = Database() diff --git a/SQLite.playground/section-20.swift b/SQLite.playground/section-20.swift deleted file mode 100644 index df919ece..00000000 --- a/SQLite.playground/section-20.swift +++ /dev/null @@ -1,5 +0,0 @@ -let id = Expression("id") -let email = Expression("email") -let age = Expression("age") -let admin = Expression("admin") -let manager_id = Expression("manager_id") diff --git a/SQLite.playground/section-22.swift b/SQLite.playground/section-22.swift deleted file mode 100644 index 624a7098..00000000 --- a/SQLite.playground/section-22.swift +++ /dev/null @@ -1 +0,0 @@ -let users = db["users"] diff --git a/SQLite.playground/section-24.swift b/SQLite.playground/section-24.swift deleted file mode 100644 index e4fa4acb..00000000 --- a/SQLite.playground/section-24.swift +++ /dev/null @@ -1,3 +0,0 @@ -users.insert(email <- "giles@acme.com", age <- 42, admin <- true).id -users.insert(email <- "haley@acme.com", age <- 30, admin <- true).id -users.insert(email <- "inigo@acme.com", age <- 24).id diff --git a/SQLite.playground/section-26.swift b/SQLite.playground/section-26.swift deleted file mode 100644 index 58a328f3..00000000 --- a/SQLite.playground/section-26.swift +++ /dev/null @@ -1,4 +0,0 @@ -db.transaction() && - users.insert(email <- "julie@acme.com") && - users.insert(email <- "kelly@acme.com", manager_id <- db.lastId) && - db.commit() || db.rollback() diff --git a/SQLite.playground/section-28.swift b/SQLite.playground/section-28.swift deleted file mode 100644 index 043375a4..00000000 --- a/SQLite.playground/section-28.swift +++ /dev/null @@ -1,4 +0,0 @@ -// SELECT * FROM users -for user in users { - println(user[email]) -} diff --git a/SQLite.playground/section-30.swift b/SQLite.playground/section-30.swift deleted file mode 100644 index 618e1e08..00000000 --- a/SQLite.playground/section-30.swift +++ /dev/null @@ -1,9 +0,0 @@ -// SELECT * FROM users LIMIT 1 -users.first - -// SELECT count(*) FROM users -users.count - -users.min(age) -users.max(age) -users.average(age) diff --git a/SQLite.playground/section-32.swift b/SQLite.playground/section-32.swift deleted file mode 100644 index d6d1597c..00000000 --- a/SQLite.playground/section-32.swift +++ /dev/null @@ -1 +0,0 @@ -let admins = users.filter(admin) diff --git a/SQLite.playground/section-34.swift b/SQLite.playground/section-34.swift deleted file mode 100644 index e6e582f0..00000000 --- a/SQLite.playground/section-34.swift +++ /dev/null @@ -1,2 +0,0 @@ -// SELECT count(*) FROM users WHERE admin -admins.count diff --git a/SQLite.playground/section-36.swift b/SQLite.playground/section-36.swift deleted file mode 100644 index 1dadf623..00000000 --- a/SQLite.playground/section-36.swift +++ /dev/null @@ -1,7 +0,0 @@ -let ordered = admins.order(email.asc, age.asc).limit(3) - -// SELECT * FROM users WHERE admin ORDER BY email ASC, age ASC LIMIT 3 -for admin in ordered { - println(admin[id]) - println(admin[age]) -} diff --git a/SQLite.playground/section-38.swift b/SQLite.playground/section-38.swift deleted file mode 100644 index c7c0ec5e..00000000 --- a/SQLite.playground/section-38.swift +++ /dev/null @@ -1,4 +0,0 @@ -let agelessAdmins = admins.filter(age == nil) - -// SELECT count(*) FROM users WHERE (admin AND age IS NULL) -agelessAdmins.count diff --git a/SQLite.playground/section-4.swift b/SQLite.playground/section-4.swift deleted file mode 100644 index f8807857..00000000 --- a/SQLite.playground/section-4.swift +++ /dev/null @@ -1,10 +0,0 @@ -db.execute( - "CREATE TABLE users (" + - "id INTEGER PRIMARY KEY, " + - "email TEXT NOT NULL UNIQUE, " + - "age INTEGER, " + - "admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), " + - "manager_id INTEGER, " + - "FOREIGN KEY(manager_id) REFERENCES users(id)" + - ")" -) diff --git a/SQLite.playground/section-40.swift b/SQLite.playground/section-40.swift deleted file mode 100644 index d1700c48..00000000 --- a/SQLite.playground/section-40.swift +++ /dev/null @@ -1,2 +0,0 @@ -// UPDATE users SET admin = 0 WHERE (admin AND age IS NULL) -agelessAdmins.update(admin <- false).changes diff --git a/SQLite.playground/section-42.swift b/SQLite.playground/section-42.swift deleted file mode 100644 index fa9b1a10..00000000 --- a/SQLite.playground/section-42.swift +++ /dev/null @@ -1,2 +0,0 @@ -// DELETE FROM users WHERE (email = 'alice@acme.com') -users.filter(email == "alice@acme.com").delete().changes diff --git a/SQLite.playground/section-6.swift b/SQLite.playground/section-6.swift deleted file mode 100644 index b9afc572..00000000 --- a/SQLite.playground/section-6.swift +++ /dev/null @@ -1,4 +0,0 @@ -let stmt = db.prepare("INSERT INTO users (email, admin) VALUES (?, ?)") -for (email, admin) in ["alice@acme.com": 1, "betsy@acme.com": 0] { - stmt.run(email, admin) -} diff --git a/SQLite.playground/section-8.swift b/SQLite.playground/section-8.swift deleted file mode 100644 index 6b363d01..00000000 --- a/SQLite.playground/section-8.swift +++ /dev/null @@ -1,3 +0,0 @@ -db.totalChanges -db.lastChanges -db.lastId diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec new file mode 100644 index 00000000..73c6f705 --- /dev/null +++ b/SQLite.swift.podspec @@ -0,0 +1,78 @@ +Pod::Spec.new do |s| + s.name = "SQLite.swift" + 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 + intent. + DESC + + s.homepage = "https://github.com/stephencelis/SQLite.swift" + s.license = 'MIT' + s.author = { "Stephen Celis" => "stephen@stephencelis.com" } + s.source = { :git => "https://github.com/stephencelis/SQLite.swift.git", :tag => s.version.to_s } + s.social_media_url = 'https://twitter.com/stephencelis' + + s.module_name = 'SQLite' + s.default_subspec = 'standard' + s.swift_versions = ['5'] + + 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.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 = '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 a84363a0..ec0423e9 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -7,973 +7,2093 @@ objects = { /* Begin PBXBuildFile section */ - DC109CE11A0C4D970070988E /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC109CE01A0C4D970070988E /* Schema.swift */; }; - DC109CE41A0C4F5D0070988E /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC109CE31A0C4F5D0070988E /* SchemaTests.swift */; }; - DC2393C81ABE35F8003FF113 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2393C61ABE35F8003FF113 /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DC2393C91ABE35F8003FF113 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2393C61ABE35F8003FF113 /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DC2393CA1ABE35F8003FF113 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2393C71ABE35F8003FF113 /* SQLite-Bridging.m */; }; - DC2393CB1ABE35F8003FF113 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2393C71ABE35F8003FF113 /* SQLite-Bridging.m */; }; - DC2DD6B01ABE437500C2C71A /* libsqlcipher.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC2DD6AC1ABE428E00C2C71A /* libsqlcipher.a */; }; - DC2DD6B31ABE439A00C2C71A /* libsqlcipher.a in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DC2DD6AC1ABE428E00C2C71A /* libsqlcipher.a */; }; - DC3773F919C8CBB3004FCF85 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3773F819C8CBB3004FCF85 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DC3773FF19C8CBB3004FCF85 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC3773F319C8CBB3004FCF85 /* SQLite.framework */; }; - DC37743519C8D626004FCF85 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743419C8D626004FCF85 /* Database.swift */; }; - DC37743819C8D693004FCF85 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743719C8D693004FCF85 /* Value.swift */; }; - DC37743B19C8D6C0004FCF85 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743A19C8D6C0004FCF85 /* Statement.swift */; }; - DC475EA219F219AF00788FBD /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC475E9E19F2199900788FBD /* ExpressionTests.swift */; }; - DC5B12131ABE3298000DA146 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = DC5B12121ABE3298000DA146 /* libsqlite3.dylib */; }; - DC650B9619F0CDC3002FBE91 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC650B9519F0CDC3002FBE91 /* Expression.swift */; }; - DC70AC991AC2331000371524 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = DC37744219C8DC91004FCF85 /* libsqlite3.dylib */; }; - DC70AC9A1AC2331100371524 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = DC37744219C8DC91004FCF85 /* libsqlite3.dylib */; }; - DC9D389C1AAD458500780AE7 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = DC9D389B1AAD458500780AE7 /* fts3_tokenizer.h */; }; - DC9D389D1AAD458500780AE7 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = DC9D389B1AAD458500780AE7 /* fts3_tokenizer.h */; }; - DCAD429719E2E0F1004A51DF /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD429619E2E0F1004A51DF /* Query.swift */; }; - DCAD429A19E2EE50004A51DF /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD429919E2EE50004A51DF /* QueryTests.swift */; }; - DCAFEAD31AABC818000C21A1 /* FTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAFEAD21AABC818000C21A1 /* FTS.swift */; }; - DCAFEAD41AABC818000C21A1 /* FTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAFEAD21AABC818000C21A1 /* FTS.swift */; }; - DCAFEAD71AABEFA7000C21A1 /* FTSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAFEAD61AABEFA7000C21A1 /* FTSTests.swift */; }; - DCBE28411ABDF18F0042A3FC /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBE28401ABDF18F0042A3FC /* RTree.swift */; }; - DCBE28421ABDF18F0042A3FC /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBE28401ABDF18F0042A3FC /* RTree.swift */; }; - DCBE28451ABDF2A80042A3FC /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBE28441ABDF2A80042A3FC /* RTreeTests.swift */; }; - DCC6B36F1A9191C300734B78 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAD429619E2E0F1004A51DF /* Query.swift */; }; - DCC6B3701A9191C300734B78 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743A19C8D6C0004FCF85 /* Statement.swift */; }; - DCC6B3711A9191C300734B78 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC650B9519F0CDC3002FBE91 /* Expression.swift */; }; - DCC6B3721A9191C300734B78 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743419C8D626004FCF85 /* Database.swift */; }; - DCC6B3731A9191C300734B78 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC37743719C8D693004FCF85 /* Value.swift */; }; - DCC6B3741A9191C300734B78 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC109CE01A0C4D970070988E /* Schema.swift */; }; - DCC6B3791A9191C300734B78 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3773F819C8CBB3004FCF85 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DCC6B3A41A9194A800734B78 /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC6B3A31A9194A800734B78 /* Cipher.swift */; }; - DCC6B3A61A9194FB00734B78 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC6B3A51A9194FB00734B78 /* CipherTests.swift */; }; - DCC6B3A71A91974B00734B78 /* FunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3F17121A814F7000C83A2F /* FunctionsTests.swift */; }; - DCC6B3A81A91975700734B78 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3F170F1A8127A300C83A2F /* Functions.swift */; }; - DCC6B3A91A91975C00734B78 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3F170F1A8127A300C83A2F /* Functions.swift */; }; - DCC6B3AC1A91979F00734B78 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC6B3801A9191C300734B78 /* SQLite.framework */; }; - DCC6B3AD1A9197B500734B78 /* TestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF37F8719DDAF79001534AA /* TestHelper.swift */; }; - DCF37F8219DDAC2D001534AA /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF37F8119DDAC2D001534AA /* DatabaseTests.swift */; }; - DCF37F8519DDAF3F001534AA /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF37F8419DDAF3F001534AA /* StatementTests.swift */; }; - DCF37F8819DDAF79001534AA /* TestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF37F8719DDAF79001534AA /* TestHelper.swift */; }; + 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 */; }; + 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 */; }; + 03A65E761C6BB2E60062603F /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + 03A65E771C6BB2E60062603F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + 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 */; }; + 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + 03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + 03A65E7F1C6BB2FB0062603F /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + 03A65E821C6BB2FB0062603F /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + EE247B0C1C3F06E900AE3E12 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + EE247B0E1C3F06E900AE3E12 /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + EE247B111C3F06E900AE3E12 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + EE247B131C3F06E900AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + 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 */; }; + EE247B461C3F3ED000AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */; }; + 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 */; }; + 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 */; }; + EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + EE247B6E1C3F3FEC00AE3E12 /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + EE247B711C3F3FEC00AE3E12 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + 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 */; }; + 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 */ /* Begin PBXContainerItemProxy section */ - DC2393CF1ABE37A5003FF113 /* PBXContainerItemProxy */ = { + 03A65E651C6BB0F60062603F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = DC3773EA19C8CBB3004FCF85 /* Project object */; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; proxyType = 1; - remoteGlobalIDString = DCAE4D141ABE0B3300EFCE7A; - remoteInfo = sqlite3; + remoteGlobalIDString = 03A65E591C6BB0F50062603F; + remoteInfo = "SQLite tvOS"; }; - DC2DD6AB1ABE428E00C2C71A /* PBXContainerItemProxy */ = { + DEB307142B61D07F00F9D46B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = DCC6B3961A91938F00734B78 /* sqlcipher.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D2AAC046055464E500DB518D; - remoteInfo = sqlcipher; - }; - DC2DD6B11ABE438900C2C71A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DCC6B3961A91938F00734B78 /* sqlcipher.xcodeproj */; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; proxyType = 1; - remoteGlobalIDString = D2AAC045055464E500DB518D; - remoteInfo = sqlcipher; + remoteGlobalIDString = DEB306B82B61CEF500F9D46B; + remoteInfo = "SQLite visionOS"; }; - DC37740019C8CBB3004FCF85 /* PBXContainerItemProxy */ = { + EE247ADF1C3F04ED00AE3E12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = DC3773EA19C8CBB3004FCF85 /* Project object */; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; proxyType = 1; - remoteGlobalIDString = DC3773F219C8CBB3004FCF85; + remoteGlobalIDString = EE247AD21C3F04ED00AE3E12; remoteInfo = SQLite; }; - DCAE4D341ABE0C2800EFCE7A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DC3773EA19C8CBB3004FCF85 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DCAE4D141ABE0B3300EFCE7A; - remoteInfo = sqlite3; - }; - DCC6B3AA1A91979100734B78 /* PBXContainerItemProxy */ = { + EE247B471C3F3ED000AE3E12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = DC3773EA19C8CBB3004FCF85 /* Project object */; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; proxyType = 1; - remoteGlobalIDString = DCC6B36D1A9191C300734B78; - remoteInfo = SQLiteCipher; + remoteGlobalIDString = EE247B3B1C3F3ED000AE3E12; + remoteInfo = SQLite; }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - DC2393D21ABE37C4003FF113 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; +/* 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; }; + 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 = ""; }; + 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 = ""; }; + EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + EE247AFB1C3F06E900AE3E12 /* Collation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collation.swift; sourceTree = ""; }; + EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctions.swift; sourceTree = ""; }; + EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = ""; }; + EE247AFE1C3F06E900AE3E12 /* Expression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; + EE247AFF1C3F06E900AE3E12 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; + EE247B001C3F06E900AE3E12 /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 */ + +/* Begin PBXFrameworksBuildPhase section */ + 03A65E561C6BB0F50062603F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; files = ( - DC2DD6B31ABE439A00C2C71A /* libsqlcipher.a in Embed Frameworks */, + 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */, ); - name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - DC109CE01A0C4D970070988E /* Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Schema.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - DC109CE31A0C4F5D0070988E /* SchemaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTests.swift; sourceTree = ""; }; - DC2393C61ABE35F8003FF113 /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQLite-Bridging.h"; sourceTree = ""; }; - DC2393C71ABE35F8003FF113 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SQLite-Bridging.m"; sourceTree = ""; }; - DC3773F319C8CBB3004FCF85 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DC3773F719C8CBB3004FCF85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../SQLite/Info.plist; sourceTree = ""; }; - DC3773F819C8CBB3004FCF85 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SQLite.h; path = ../SQLite/SQLite.h; sourceTree = ""; }; - DC3773FE19C8CBB3004FCF85 /* SQLite Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLite Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - DC37740419C8CBB3004FCF85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = SQLite.xcconfig; sourceTree = ""; }; - DC37743419C8D626004FCF85 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Database.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - DC37743719C8D693004FCF85 /* Value.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Value.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - DC37743A19C8D6C0004FCF85 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Statement.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - DC37744219C8DC91004FCF85 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; - DC37744719C8F50B004FCF85 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - DC3F170F1A8127A300C83A2F /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; - DC3F17121A814F7000C83A2F /* FunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionsTests.swift; sourceTree = ""; }; - DC475E9E19F2199900788FBD /* ExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionTests.swift; sourceTree = ""; }; - DC5B12121ABE3298000DA146 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/libsqlite3.dylib; sourceTree = DEVELOPER_DIR; }; - DC650B9519F0CDC3002FBE91 /* Expression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Expression.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - DC9D389B1AAD458500780AE7 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fts3_tokenizer.h; path = ../Vendor/fts3_tokenizer.h; sourceTree = ""; }; - DCAD429619E2E0F1004A51DF /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Query.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - DCAD429919E2EE50004A51DF /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; - DCAE4D151ABE0B3300EFCE7A /* sqlite3.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = sqlite3.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DCAE4D181ABE0B3300EFCE7A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DCAE4D2E1ABE0B6300EFCE7A /* sqlite3.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = sqlite3.xcconfig; sourceTree = ""; }; - DCAE4D2F1ABE0B8B00EFCE7A /* iphoneos.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = iphoneos.modulemap; sourceTree = ""; }; - DCAE4D301ABE0B8B00EFCE7A /* iphonesimulator.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = iphonesimulator.modulemap; sourceTree = ""; }; - DCAE4D311ABE0B8B00EFCE7A /* macosx.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = macosx.modulemap; sourceTree = ""; }; - DCAFEAD21AABC818000C21A1 /* FTS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS.swift; sourceTree = ""; }; - DCAFEAD61AABEFA7000C21A1 /* FTSTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTSTests.swift; sourceTree = ""; }; - DCBC8C301ABE3CDA002B4631 /* SQLite.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SQLite.playground; sourceTree = ""; }; - DCBE28401ABDF18F0042A3FC /* RTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; - DCBE28441ABDF2A80042A3FC /* RTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTreeTests.swift; sourceTree = ""; }; - DCC6B3801A9191C300734B78 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DCC6B3921A9191D100734B78 /* SQLiteCipher Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteCipher Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - DCC6B3961A91938F00734B78 /* sqlcipher.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = sqlcipher.xcodeproj; path = Vendor/sqlcipher/sqlcipher.xcodeproj; sourceTree = ""; }; - DCC6B3A31A9194A800734B78 /* Cipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; }; - DCC6B3A51A9194FB00734B78 /* CipherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherTests.swift; sourceTree = ""; }; - DCF37F8119DDAC2D001534AA /* DatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = ""; }; - DCF37F8419DDAF3F001534AA /* StatementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementTests.swift; sourceTree = ""; }; - DCF37F8719DDAF79001534AA /* TestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelper.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - DC3773EF19C8CBB3004FCF85 /* Frameworks */ = { + 03A65E601C6BB0F60062603F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC411CA35C79005A31D1 /* Frameworks */ = { + 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 = ( - DC70AC991AC2331000371524 /* libsqlite3.dylib in Frameworks */, + DEB3070B2B61CF9500F9D46B /* SQLite.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - DC3773FB19C8CBB3004FCF85 /* Frameworks */ = { + EE247ACF1C3F04ED00AE3E12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DC3773FF19C8CBB3004FCF85 /* SQLite.framework in Frameworks */, + EE9180941C46EA210038162A /* libsqlite3.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCAE4D111ABE0B3300EFCE7A /* Frameworks */ = { + EE247ADA1C3F04ED00AE3E12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DC5B12131ABE3298000DA146 /* libsqlite3.dylib in Frameworks */, + EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCC6B3761A9191C300734B78 /* Frameworks */ = { + EE247B381C3F3ED000AE3E12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DC2DD6B01ABE437500C2C71A /* libsqlcipher.a in Frameworks */, - DC70AC9A1AC2331100371524 /* libsqlite3.dylib in Frameworks */, + EE9180951C46EBCC0038162A /* libsqlite3.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCC6B38C1A9191D100734B78 /* Frameworks */ = { + EE247B421C3F3ED000AE3E12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DCC6B3AC1A91979F00734B78 /* SQLite.framework in Frameworks */, + EE247B461C3F3ED000AE3E12 /* SQLite.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - DC037C0919F0240100959746 /* Query Building */ = { + 19A1792D261C689FC988A90A /* Schema */ = { isa = PBXGroup; children = ( - DC650B9519F0CDC3002FBE91 /* Expression.swift */, - DC3F170F1A8127A300C83A2F /* Functions.swift */, - DCAD429619E2E0F1004A51DF /* Query.swift */, - DC109CE01A0C4D970070988E /* Schema.swift */, - DCAFEAD21AABC818000C21A1 /* FTS.swift */, - DCBE28401ABDF18F0042A3FC /* RTree.swift */, - ); - name = "Query Building"; + DB58B21028FB864300F8EEA4 /* SchemaReader.swift */, + 19A171B262DDE8718513CFDA /* SchemaChanger.swift */, + 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */, + 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */, + ); + path = Schema; sourceTree = ""; }; - DC10500F19C904DD00D8CA30 /* SQLite Tests */ = { + 19A1798E3459573BEE50FA34 /* Core */ = { isa = PBXGroup; children = ( - DCF37F8719DDAF79001534AA /* TestHelper.swift */, - DCF37F8119DDAC2D001534AA /* DatabaseTests.swift */, - DCF37F8419DDAF3F001534AA /* StatementTests.swift */, - DC475E9E19F2199900788FBD /* ExpressionTests.swift */, - DC3F17121A814F7000C83A2F /* FunctionsTests.swift */, - DCAD429919E2EE50004A51DF /* QueryTests.swift */, - DC109CE31A0C4F5D0070988E /* SchemaTests.swift */, - DCAFEAD61AABEFA7000C21A1 /* FTSTests.swift */, - DCBE28441ABDF2A80042A3FC /* RTreeTests.swift */, - DC37740319C8CBB3004FCF85 /* Supporting Files */, - ); - path = "SQLite Tests"; + 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 = Core; sourceTree = ""; }; - DC2DD6A71ABE428E00C2C71A /* Products */ = { + 19A17AECBF878B1DAE0AE3DD /* Typed */ = { isa = PBXGroup; children = ( - DC2DD6AC1ABE428E00C2C71A /* libsqlcipher.a */, + 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 */, ); - name = Products; + path = Typed; + sourceTree = ""; + }; + 19A17B56FBA20E7245BC8AC0 /* Schema */ = { + isa = PBXGroup; + children = ( + DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */, + 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */, + 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */, + 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */, + 19A171A7714C6524093255C5 /* SchemaTests.swift */, + ); + path = Schema; + sourceTree = ""; + }; + 19A17E470E4492D287C0D12F /* Extensions */ = { + isa = PBXGroup; + children = ( + 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */, + 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */, + 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */, + 19A1787E16C8562C09C076F5 /* CipherTests.swift */, + 19A17162C9861E5C4900455D /* RTreeTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 3D67B3E41DB2469200A4F4C6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */, + ); + name = Frameworks; sourceTree = ""; }; - DC3773E919C8CBB3004FCF85 = { + EE247AC91C3F04ED00AE3E12 = { isa = PBXGroup; children = ( - DC37744719C8F50B004FCF85 /* README.md */, - DCBC8C301ABE3CDA002B4631 /* SQLite.playground */, - DC37742D19C8CC90004FCF85 /* SQLite */, - DC10500F19C904DD00D8CA30 /* SQLite Tests */, - DCC6B3A11A91949C00734B78 /* SQLiteCipher */, - DCC6B3A21A91949C00734B78 /* SQLiteCipher Tests */, - DCAE4D161ABE0B3300EFCE7A /* sqlite3 */, - DCC6B3961A91938F00734B78 /* sqlcipher.xcodeproj */, - DC3773F419C8CBB3004FCF85 /* Products */, + 3D3C3CCB26E5568800759140 /* SQLite.playground */, + EE247AD51C3F04ED00AE3E12 /* SQLite */, + EE247AE11C3F04ED00AE3E12 /* SQLiteTests */, + DEB307132B61D04500F9D46B /* SQLite visionOS.xctestplan */, + EE247B8A1C3F81D000AE3E12 /* Metadata */, + EE247AD41C3F04ED00AE3E12 /* Products */, + 3D67B3E41DB2469200A4F4C6 /* Frameworks */, ); indentWidth = 4; sourceTree = ""; tabWidth = 4; - usesTabs = 0; }; - DC3773F419C8CBB3004FCF85 /* Products */ = { + EE247AD41C3F04ED00AE3E12 /* Products */ = { isa = PBXGroup; children = ( - DC3773F319C8CBB3004FCF85 /* SQLite.framework */, - DC3773FE19C8CBB3004FCF85 /* SQLite Tests.xctest */, - DCC6B3801A9191C300734B78 /* SQLite.framework */, - DCC6B3921A9191D100734B78 /* SQLiteCipher Tests.xctest */, - DCAE4D151ABE0B3300EFCE7A /* sqlite3.framework */, + EE247AD31C3F04ED00AE3E12 /* SQLite.framework */, + EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */, + EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */, + EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */, + 03A65E5A1C6BB0F50062603F /* SQLite.framework */, + 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */, + A121AC451CA35C79005A31D1 /* SQLite.framework */, + DEB306E52B61CEF500F9D46B /* SQLite.framework */, + DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */, ); name = Products; sourceTree = ""; }; - DC37740319C8CBB3004FCF85 /* Supporting Files */ = { + EE247AD51C3F04ED00AE3E12 /* SQLite */ = { isa = PBXGroup; children = ( - DC37740419C8CBB3004FCF85 /* Info.plist */, + EE247AD61C3F04ED00AE3E12 /* SQLite.h */, + EE247AF71C3F06E900AE3E12 /* Foundation.swift */, + EE247AF81C3F06E900AE3E12 /* Helpers.swift */, + EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */, + EE247AD81C3F04ED00AE3E12 /* Info.plist */, + EE247AED1C3F06E900AE3E12 /* Core */, + EE247AF41C3F06E900AE3E12 /* Extensions */, + EE247AF91C3F06E900AE3E12 /* Typed */, + 19A1792D261C689FC988A90A /* Schema */, ); - name = "Supporting Files"; - path = "../SQLite Tests"; + name = SQLite; + path = Sources/SQLite; sourceTree = ""; }; - DC37742D19C8CC90004FCF85 /* SQLite */ = { + EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { isa = PBXGroup; children = ( - DC37743419C8D626004FCF85 /* Database.swift */, - DC37743A19C8D6C0004FCF85 /* Statement.swift */, - DC37743719C8D693004FCF85 /* Value.swift */, - DC037C0919F0240100959746 /* Query Building */, - DC37743319C8CFCE004FCF85 /* Supporting Files */, + 3DF7B79528846FCC005DD8CA /* Resources */, + EE247B161C3F127200AE3E12 /* TestHelpers.swift */, + EE247AE41C3F04ED00AE3E12 /* Info.plist */, + 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */, + 19A17B93B48B5560E6E51791 /* Fixtures.swift */, + 19A17B56FBA20E7245BC8AC0 /* Schema */, + 19A17E470E4492D287C0D12F /* Extensions */, + 19A1798E3459573BEE50FA34 /* Core */, + 19A17AECBF878B1DAE0AE3DD /* Typed */, ); - path = SQLite; + name = SQLiteTests; + path = Tests/SQLiteTests; sourceTree = ""; }; - DC37743319C8CFCE004FCF85 /* Supporting Files */ = { + EE247AED1C3F06E900AE3E12 /* Core */ = { isa = PBXGroup; children = ( - DC3773F819C8CBB3004FCF85 /* SQLite.h */, - DC2393C61ABE35F8003FF113 /* SQLite-Bridging.h */, - DC2393C71ABE35F8003FF113 /* SQLite-Bridging.m */, - DC9D389B1AAD458500780AE7 /* fts3_tokenizer.h */, - DC3773F719C8CBB3004FCF85 /* Info.plist */, - DC37744219C8DC91004FCF85 /* libsqlite3.dylib */, - DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */, - ); - name = "Supporting Files"; + EE247AEE1C3F06E900AE3E12 /* Blob.swift */, + EE247AEF1C3F06E900AE3E12 /* Connection.swift */, + 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 = ""; }; - DCAE4D161ABE0B3300EFCE7A /* sqlite3 */ = { + EE247AF41C3F06E900AE3E12 /* Extensions */ = { isa = PBXGroup; children = ( - DCAE4D171ABE0B3300EFCE7A /* Supporting Files */, + 19A178A39ACA9667A62663CC /* Cipher.swift */, + EE247AF51C3F06E900AE3E12 /* FTS4.swift */, + 19A1730E4390C775C25677D1 /* FTS5.swift */, + EE247AF61C3F06E900AE3E12 /* RTree.swift */, ); - path = sqlite3; + path = Extensions; sourceTree = ""; }; - DCAE4D171ABE0B3300EFCE7A /* Supporting Files */ = { + EE247AF91C3F06E900AE3E12 /* Typed */ = { isa = PBXGroup; children = ( - DCAE4D181ABE0B3300EFCE7A /* Info.plist */, - DC5B12121ABE3298000DA146 /* libsqlite3.dylib */, - DCAE4D2E1ABE0B6300EFCE7A /* sqlite3.xcconfig */, - DCAE4D2F1ABE0B8B00EFCE7A /* iphoneos.modulemap */, - DCAE4D301ABE0B8B00EFCE7A /* iphonesimulator.modulemap */, - DCAE4D311ABE0B8B00EFCE7A /* macosx.modulemap */, - ); - name = "Supporting Files"; + 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 = ""; }; - DCC6B3A11A91949C00734B78 /* SQLiteCipher */ = { + EE247B8A1C3F81D000AE3E12 /* Metadata */ = { isa = PBXGroup; children = ( - DCC6B3A31A9194A800734B78 /* Cipher.swift */, + EE247B771C3F40D700AE3E12 /* README.md */, + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */, + EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, + EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, + 3DFC0B862886C239001C8FC9 /* Package.swift */, + EE247B8D1C3F821200AE3E12 /* Makefile */, + EE9180931C46EA210038162A /* libsqlite3.tbd */, + EE9180911C46E9D30038162A /* libsqlite3.tbd */, + 03A65E961C6BB3210062603F /* libsqlite3.tbd */, + EE247B8E1C3F822500AE3E12 /* Documentation */, ); - path = SQLiteCipher; + name = Metadata; sourceTree = ""; }; - DCC6B3A21A91949C00734B78 /* SQLiteCipher Tests */ = { + EE247B8E1C3F822500AE3E12 /* Documentation */ = { isa = PBXGroup; children = ( - DCC6B3A51A9194FB00734B78 /* CipherTests.swift */, + EE247B8F1C3F822500AE3E12 /* Index.md */, + 3DF7B79B2884C901005DD8CA /* Planning.md */, + EE247B901C3F822500AE3E12 /* Resources */, + 19A17EA3A313F129011B3FA0 /* Release.md */, + 19A1794B7972D14330A65BBD /* Linux.md */, ); - path = "SQLiteCipher Tests"; + path = Documentation; + sourceTree = ""; + }; + EE247B901C3F822500AE3E12 /* Resources */ = { + isa = PBXGroup; + children = ( + EE247B911C3F822500AE3E12 /* installation@2x.png */, + EE247B921C3F822600AE3E12 /* playground@2x.png */, + ); + path = Resources; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - DC3773F019C8CBB3004FCF85 /* Headers */ = { + 03A65E571C6BB0F50062603F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC421CA35C79005A31D1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306B92B61CEF500F9D46B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - DC9D389C1AAD458500780AE7 /* fts3_tokenizer.h in Headers */, - DC3773F919C8CBB3004FCF85 /* SQLite.h in Headers */, - DC2393C81ABE35F8003FF113 /* SQLite-Bridging.h in Headers */, + DEB306BA2B61CEF500F9D46B /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCAE4D121ABE0B3300EFCE7A /* Headers */ = { + EE247AD01C3F04ED00AE3E12 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCC6B3781A9191C300734B78 /* Headers */ = { + EE247B391C3F3ED000AE3E12 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - DC9D389D1AAD458500780AE7 /* fts3_tokenizer.h in Headers */, - DCC6B3791A9191C300734B78 /* SQLite.h in Headers */, - DC2393C91ABE35F8003FF113 /* SQLite-Bridging.h in Headers */, + EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - DC3773F219C8CBB3004FCF85 /* SQLite */ = { + 03A65E591C6BB0F50062603F /* SQLite tvOS */ = { isa = PBXNativeTarget; - buildConfigurationList = DC37740919C8CBB3004FCF85 /* Build configuration list for PBXNativeTarget "SQLite" */; + buildConfigurationList = 03A65E6F1C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLite tvOS" */; buildPhases = ( - DC3773EE19C8CBB3004FCF85 /* Sources */, - DC3773EF19C8CBB3004FCF85 /* Frameworks */, - DC3773F019C8CBB3004FCF85 /* Headers */, - DC3773F119C8CBB3004FCF85 /* Resources */, + 03A65E571C6BB0F50062603F /* Headers */, + 03A65E551C6BB0F50062603F /* Sources */, + 03A65E561C6BB0F50062603F /* Frameworks */, + 03A65E581C6BB0F50062603F /* Resources */, ); buildRules = ( ); dependencies = ( - DCAE4D351ABE0C2800EFCE7A /* PBXTargetDependency */, ); - name = SQLite; + name = "SQLite tvOS"; + productName = "SQLite tvOS"; + productReference = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03A65E701C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLiteTests tvOS" */; + buildPhases = ( + 03A65E5F1C6BB0F60062603F /* Sources */, + 03A65E601C6BB0F60062603F /* Frameworks */, + 03A65E611C6BB0F60062603F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03A65E661C6BB0F60062603F /* PBXTargetDependency */, + ); + name = "SQLiteTests tvOS"; + productName = "SQLite tvOSTests"; + productReference = 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + A121AC441CA35C79005A31D1 /* SQLite watchOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A121AC4C1CA35C79005A31D1 /* Build configuration list for PBXNativeTarget "SQLite watchOS" */; + buildPhases = ( + A121AC421CA35C79005A31D1 /* Headers */, + A121AC401CA35C79005A31D1 /* Sources */, + A121AC411CA35C79005A31D1 /* Frameworks */, + A121AC431CA35C79005A31D1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite watchOS"; + productName = "SQLite watchOS"; + 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 = DC3773F319C8CBB3004FCF85 /* SQLite.framework */; + productReference = DEB306E52B61CEF500F9D46B /* SQLite.framework */; productType = "com.apple.product-type.framework"; }; - DC3773FD19C8CBB3004FCF85 /* SQLite Tests */ = { + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */ = { isa = PBXNativeTarget; - buildConfigurationList = DC37740C19C8CBB3004FCF85 /* Build configuration list for PBXNativeTarget "SQLite Tests" */; + buildConfigurationList = DEB3070E2B61CF9500F9D46B /* Build configuration list for PBXNativeTarget "SQLiteTests visionOS" */; buildPhases = ( - DC3773FA19C8CBB3004FCF85 /* Sources */, - DC3773FB19C8CBB3004FCF85 /* Frameworks */, - DC3773FC19C8CBB3004FCF85 /* Resources */, + DEB306EA2B61CF9500F9D46B /* Sources */, + DEB3070A2B61CF9500F9D46B /* Frameworks */, + DEB3070C2B61CF9500F9D46B /* Resources */, ); buildRules = ( ); dependencies = ( - DC37740119C8CBB3004FCF85 /* PBXTargetDependency */, + DEB307152B61D07F00F9D46B /* PBXTargetDependency */, ); - name = "SQLite Tests"; - productName = "SQLite Tests"; - productReference = DC3773FE19C8CBB3004FCF85 /* SQLite Tests.xctest */; + name = "SQLiteTests visionOS"; + productName = "SQLite tvOSTests"; + productReference = DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - DCAE4D141ABE0B3300EFCE7A /* sqlite3 */ = { + EE247AD21C3F04ED00AE3E12 /* SQLite iOS */ = { isa = PBXNativeTarget; - buildConfigurationList = DCAE4D281ABE0B3400EFCE7A /* Build configuration list for PBXNativeTarget "sqlite3" */; + buildConfigurationList = EE247AE71C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLite iOS" */; buildPhases = ( - DCAE4D101ABE0B3300EFCE7A /* Sources */, - DCAE4D111ABE0B3300EFCE7A /* Frameworks */, - DCAE4D121ABE0B3300EFCE7A /* Headers */, - DCAE4D131ABE0B3300EFCE7A /* Resources */, + EE247AD01C3F04ED00AE3E12 /* Headers */, + EE247ACE1C3F04ED00AE3E12 /* Sources */, + EE247ACF1C3F04ED00AE3E12 /* Frameworks */, + EE247AD11C3F04ED00AE3E12 /* Resources */, ); buildRules = ( ); dependencies = ( ); - name = sqlite3; - productName = sqlite3; - productReference = DCAE4D151ABE0B3300EFCE7A /* sqlite3.framework */; + name = "SQLite iOS"; + productName = SQLite; + productReference = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; productType = "com.apple.product-type.framework"; }; - DCC6B36D1A9191C300734B78 /* SQLiteCipher */ = { + EE247ADC1C3F04ED00AE3E12 /* SQLiteTests iOS */ = { isa = PBXNativeTarget; - buildConfigurationList = DCC6B37D1A9191C300734B78 /* Build configuration list for PBXNativeTarget "SQLiteCipher" */; + buildConfigurationList = EE247AEA1C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests iOS" */; buildPhases = ( - DCC6B36E1A9191C300734B78 /* Sources */, - DCC6B3761A9191C300734B78 /* Frameworks */, - DCC6B3781A9191C300734B78 /* Headers */, - DCC6B37C1A9191C300734B78 /* Resources */, - DC2393D21ABE37C4003FF113 /* Embed Frameworks */, + EE247AD91C3F04ED00AE3E12 /* Sources */, + EE247ADA1C3F04ED00AE3E12 /* Frameworks */, + EE247ADB1C3F04ED00AE3E12 /* Resources */, ); buildRules = ( ); dependencies = ( - DC2DD6B21ABE438900C2C71A /* PBXTargetDependency */, - DC2393D01ABE37A5003FF113 /* PBXTargetDependency */, + EE247AE01C3F04ED00AE3E12 /* PBXTargetDependency */, ); - name = SQLiteCipher; + name = "SQLiteTests iOS"; + productName = SQLiteTests; + productReference = EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EE247B3B1C3F3ED000AE3E12 /* SQLite Mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE247B511C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLite Mac" */; + buildPhases = ( + EE247B391C3F3ED000AE3E12 /* Headers */, + EE247B371C3F3ED000AE3E12 /* Sources */, + EE247B381C3F3ED000AE3E12 /* Frameworks */, + EE247B3A1C3F3ED000AE3E12 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite Mac"; productName = SQLite; - productReference = DCC6B3801A9191C300734B78 /* SQLite.framework */; + productReference = EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */; productType = "com.apple.product-type.framework"; }; - DCC6B3821A9191D100734B78 /* SQLiteCipher Tests */ = { + EE247B441C3F3ED000AE3E12 /* SQLiteTests Mac */ = { isa = PBXNativeTarget; - buildConfigurationList = DCC6B38F1A9191D100734B78 /* Build configuration list for PBXNativeTarget "SQLiteCipher Tests" */; + buildConfigurationList = EE247B521C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests Mac" */; buildPhases = ( - DCC6B3851A9191D100734B78 /* Sources */, - DCC6B38C1A9191D100734B78 /* Frameworks */, - DCC6B38E1A9191D100734B78 /* Resources */, + EE247B411C3F3ED000AE3E12 /* Sources */, + EE247B421C3F3ED000AE3E12 /* Frameworks */, + EE247B431C3F3ED000AE3E12 /* Resources */, ); buildRules = ( ); dependencies = ( - DCC6B3AB1A91979100734B78 /* PBXTargetDependency */, + EE247B481C3F3ED000AE3E12 /* PBXTargetDependency */, ); - name = "SQLiteCipher Tests"; - productName = "SQLite Tests"; - productReference = DCC6B3921A9191D100734B78 /* SQLiteCipher Tests.xctest */; + name = "SQLiteTests Mac"; + productName = SQLiteTests; + productReference = EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - DC3773EA19C8CBB3004FCF85 /* Project object */ = { + EE247ACA1C3F04ED00AE3E12 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0630; - ORGANIZATIONNAME = "Stephen Celis"; + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 1250; TargetAttributes = { - DC3773F219C8CBB3004FCF85 = { - CreatedOnToolsVersion = 6.1; + 03A65E591C6BB0F50062603F = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; - DC3773FD19C8CBB3004FCF85 = { - CreatedOnToolsVersion = 6.1; + 03A65E621C6BB0F60062603F = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; - DCAE4D141ABE0B3300EFCE7A = { - CreatedOnToolsVersion = 6.3; + 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 = DC3773ED19C8CBB3004FCF85 /* Build configuration list for PBXProject "SQLite" */; + buildConfigurationList = EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); - mainGroup = DC3773E919C8CBB3004FCF85; - productRefGroup = DC3773F419C8CBB3004FCF85 /* Products */; + mainGroup = EE247AC91C3F04ED00AE3E12; + productRefGroup = EE247AD41C3F04ED00AE3E12 /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = DC2DD6A71ABE428E00C2C71A /* Products */; - ProjectRef = DCC6B3961A91938F00734B78 /* sqlcipher.xcodeproj */; - }, - ); projectRoot = ""; targets = ( - DC3773F219C8CBB3004FCF85 /* SQLite */, - DC3773FD19C8CBB3004FCF85 /* SQLite Tests */, - DCC6B36D1A9191C300734B78 /* SQLiteCipher */, - DCC6B3821A9191D100734B78 /* SQLiteCipher Tests */, - DCAE4D141ABE0B3300EFCE7A /* sqlite3 */, + EE247AD21C3F04ED00AE3E12 /* SQLite iOS */, + EE247ADC1C3F04ED00AE3E12 /* SQLiteTests iOS */, + EE247B3B1C3F3ED000AE3E12 /* SQLite Mac */, + EE247B441C3F3ED000AE3E12 /* SQLiteTests Mac */, + 03A65E591C6BB0F50062603F /* SQLite tvOS */, + 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */, + A121AC441CA35C79005A31D1 /* SQLite watchOS */, + DEB306B82B61CEF500F9D46B /* SQLite visionOS */, + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */, ); }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - DC2DD6AC1ABE428E00C2C71A /* libsqlcipher.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libsqlcipher.a; - remoteRef = DC2DD6AB1ABE428E00C2C71A /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXResourcesBuildPhase section */ - DC3773F119C8CBB3004FCF85 /* Resources */ = { + 03A65E581C6BB0F50062603F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EAE5A0392B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03A65E611C6BB0F60062603F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DF7B79828846FED005DD8CA /* Resources in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC431CA35C79005A31D1 /* Resources */ = { + 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; }; - DC3773FC19C8CBB3004FCF85 /* Resources */ = { + EE247AD11C3F04ED00AE3E12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0372B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCAE4D131ABE0B3300EFCE7A /* Resources */ = { + EE247ADB1C3F04ED00AE3E12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B79628846FCC005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCC6B37C1A9191C300734B78 /* Resources */ = { + EE247B3A1C3F3ED000AE3E12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0382B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - DCC6B38E1A9191D100734B78 /* Resources */ = { + EE247B431C3F3ED000AE3E12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B79928847055005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - DC3773EE19C8CBB3004FCF85 /* Sources */ = { + 03A65E551C6BB0F50062603F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */, + 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, + 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, + 03A65E7D1C6BB2F70062603F /* RTree.swift 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 */, + 03A65E841C6BB2FB0062603F /* Query.swift in Sources */, + 03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */, + 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; + }; + 03A65E5F1C6BB0F60062603F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E951C6BB3030062603F /* TestHelpers.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; + }; + A121AC401CA35C79005A31D1 /* Sources */ = { + 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 = ( - DC37743519C8D626004FCF85 /* Database.swift in Sources */, - DC2393CA1ABE35F8003FF113 /* SQLite-Bridging.m in Sources */, - DCAFEAD31AABC818000C21A1 /* FTS.swift in Sources */, - DC37743B19C8D6C0004FCF85 /* Statement.swift in Sources */, - DC37743819C8D693004FCF85 /* Value.swift in Sources */, - DC650B9619F0CDC3002FBE91 /* Expression.swift in Sources */, - DCBE28411ABDF18F0042A3FC /* RTree.swift in Sources */, - DCAD429719E2E0F1004A51DF /* Query.swift in Sources */, - DC109CE11A0C4D970070988E /* Schema.swift in Sources */, - DCC6B3A81A91975700734B78 /* Functions.swift in Sources */, + 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; }; - DC3773FA19C8CBB3004FCF85 /* Sources */ = { + EE247ACE1C3F04ED00AE3E12 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DCF37F8819DDAF79001534AA /* TestHelper.swift in Sources */, - DCAFEAD71AABEFA7000C21A1 /* FTSTests.swift in Sources */, - DCF37F8219DDAC2D001534AA /* DatabaseTests.swift in Sources */, - DCF37F8519DDAF3F001534AA /* StatementTests.swift in Sources */, - DC475EA219F219AF00788FBD /* ExpressionTests.swift in Sources */, - DCAD429A19E2EE50004A51DF /* QueryTests.swift in Sources */, - DC109CE41A0C4F5D0070988E /* SchemaTests.swift in Sources */, - DCC6B3A71A91974B00734B78 /* FunctionsTests.swift in Sources */, - DCBE28451ABDF2A80042A3FC /* RTreeTests.swift in Sources */, + 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 */, + 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; }; - DCAE4D101ABE0B3300EFCE7A /* Sources */ = { + EE247AD91C3F04ED00AE3E12 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EE247B171C3F127200AE3E12 /* TestHelpers.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; }; - DCC6B36E1A9191C300734B78 /* Sources */ = { + EE247B371C3F3ED000AE3E12 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DCC6B3721A9191C300734B78 /* Database.swift in Sources */, - DCC6B3701A9191C300734B78 /* Statement.swift in Sources */, - DCC6B3731A9191C300734B78 /* Value.swift in Sources */, - DCC6B3711A9191C300734B78 /* Expression.swift in Sources */, - DCC6B36F1A9191C300734B78 /* Query.swift in Sources */, - DCC6B3741A9191C300734B78 /* Schema.swift in Sources */, - DCC6B3A91A91975C00734B78 /* Functions.swift in Sources */, - DCAFEAD41AABC818000C21A1 /* FTS.swift in Sources */, - DCBE28421ABDF18F0042A3FC /* RTree.swift in Sources */, - DC2393CB1ABE35F8003FF113 /* SQLite-Bridging.m in Sources */, - DCC6B3A41A9194A800734B78 /* Cipher.swift in Sources */, + EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, + EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, + EE247B6C1C3F3FEC00AE3E12 /* RTree.swift 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 */, + EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */, + EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */, + 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; }; - DCC6B3851A9191D100734B78 /* Sources */ = { + EE247B411C3F3ED000AE3E12 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DCC6B3AD1A9197B500734B78 /* TestHelper.swift in Sources */, - DCC6B3A61A9194FB00734B78 /* CipherTests.swift in Sources */, + EE247B611C3F3FC700AE3E12 /* TestHelpers.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; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - DC2393D01ABE37A5003FF113 /* PBXTargetDependency */ = { + 03A65E661C6BB0F60062603F /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = DCAE4D141ABE0B3300EFCE7A /* sqlite3 */; - targetProxy = DC2393CF1ABE37A5003FF113 /* PBXContainerItemProxy */; + target = 03A65E591C6BB0F50062603F /* SQLite tvOS */; + targetProxy = 03A65E651C6BB0F60062603F /* PBXContainerItemProxy */; }; - DC2DD6B21ABE438900C2C71A /* PBXTargetDependency */ = { + DEB307152B61D07F00F9D46B /* PBXTargetDependency */ = { isa = PBXTargetDependency; - name = sqlcipher; - targetProxy = DC2DD6B11ABE438900C2C71A /* PBXContainerItemProxy */; + target = DEB306B82B61CEF500F9D46B /* SQLite visionOS */; + targetProxy = DEB307142B61D07F00F9D46B /* PBXContainerItemProxy */; }; - DC37740119C8CBB3004FCF85 /* PBXTargetDependency */ = { + EE247AE01C3F04ED00AE3E12 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = DC3773F219C8CBB3004FCF85 /* SQLite */; - targetProxy = DC37740019C8CBB3004FCF85 /* PBXContainerItemProxy */; + target = EE247AD21C3F04ED00AE3E12 /* SQLite iOS */; + targetProxy = EE247ADF1C3F04ED00AE3E12 /* PBXContainerItemProxy */; }; - DCAE4D351ABE0C2800EFCE7A /* PBXTargetDependency */ = { + EE247B481C3F3ED000AE3E12 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = DCAE4D141ABE0B3300EFCE7A /* sqlite3 */; - targetProxy = DCAE4D341ABE0C2800EFCE7A /* PBXContainerItemProxy */; - }; - DCC6B3AB1A91979100734B78 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DCC6B36D1A9191C300734B78 /* SQLiteCipher */; - targetProxy = DCC6B3AA1A91979100734B78 /* PBXContainerItemProxy */; + target = EE247B3B1C3F3ED000AE3E12 /* SQLite Mac */; + targetProxy = EE247B471C3F3ED000AE3E12 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - DC37740719C8CBB3004FCF85 /* Debug */ = { + 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"; + 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_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"; + 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_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Release; + }; + 03A65E6D1C6BB0F60062603F /* 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 = appletvos; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Debug; + }; + 03A65E6E1C6BB0F60062603F /* 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 = appletvos; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Release; + }; + A121AC4A1CA35C79005A31D1 /* Debug */ = { + isa = XCBuildConfiguration; + 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"; + 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 = 4.0; + }; + name = Debug; + }; + A121AC4B1CA35C79005A31D1 /* Release */ = { + isa = XCBuildConfiguration; + 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"; + 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 = 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; + }; + EE247AE51C3F04ED00AE3E12 /* Debug */ = { 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"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); - GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + 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; }; - DC37740819C8CBB3004FCF85 /* Release */ = { + EE247AE61C3F04ED00AE3E12 /* Release */ = { 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"; - COPY_PHASE_STRIP = YES; + COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + 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; }; - DC37740A19C8CBB3004FCF85 /* Debug */ = { + EE247AE81C3F04ED00AE3E12 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; 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 = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; - DC37740B19C8CBB3004FCF85 /* Release */ = { + EE247AE91C3F04ED00AE3E12 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; 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 = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; - DC37740D19C8CBB3004FCF85 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; - buildSettings = { - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = "SQLite Tests/Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_WHOLE_MODULE_OPTIMIZATION = NO; - }; - name = Debug; - }; - DC37740E19C8CBB3004FCF85 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; - buildSettings = { - INFOPLIST_FILE = "SQLite Tests/Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_WHOLE_MODULE_OPTIMIZATION = NO; - }; - name = Release; - }; - DCAE4D291ABE0B3400EFCE7A /* Debug */ = { + EE247AEB1C3F04ED00AE3E12 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DCAE4D2E1ABE0B6300EFCE7A /* sqlite3.xcconfig */; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_NO_COMMON_BLOCKS = YES; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = sqlite3/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.3; + 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)"; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; - DCAE4D2A1ABE0B3400EFCE7A /* Release */ = { + EE247AEC1C3F04ED00AE3E12 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DCAE4D2E1ABE0B6300EFCE7A /* sqlite3.xcconfig */; buildSettings = { - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_NO_COMMON_BLOCKS = YES; - INFOPLIST_FILE = sqlite3/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.3; + 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)"; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; - DCC6B37E1A9191C300734B78 /* Debug */ = { + EE247B4D1C3F3ED000AE3E12 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; buildSettings = { - CLANG_ENABLE_MODULES = YES; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "SQLITE_HAS_CODEC=1", - ); - INFOPLIST_FILE = SQLite/Info.plist; + FRAMEWORK_VERSION = A; + 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"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.13; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; + SDKROOT = macosx; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_INCLUDE_PATHS = ""; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; - DCC6B37F1A9191C300734B78 /* Release */ = { + EE247B4E1C3F3ED000AE3E12 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; buildSettings = { - CLANG_ENABLE_MODULES = YES; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "SQLITE_HAS_CODEC=1", - ); - INFOPLIST_FILE = SQLite/Info.plist; + FRAMEWORK_VERSION = A; + 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"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.13; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; + SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = ""; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; - DCC6B3901A9191D100734B78 /* Debug */ = { + EE247B4F1C3F3ED000AE3E12 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; buildSettings = { - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = "SQLite Tests/Info.plist"; - PRODUCT_NAME = "SQLiteCipher Tests"; - SWIFT_WHOLE_MODULE_OPTIMIZATION = NO; + CODE_SIGN_IDENTITY = "-"; + COMBINE_HIDPI_IMAGES = YES; + 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.13; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; - DCC6B3911A9191D100734B78 /* Release */ = { + EE247B501C3F3ED000AE3E12 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DC37742E19C8CE67004FCF85 /* SQLite.xcconfig */; buildSettings = { - INFOPLIST_FILE = "SQLite Tests/Info.plist"; - PRODUCT_NAME = "SQLiteCipher Tests"; - SWIFT_WHOLE_MODULE_OPTIMIZATION = NO; + CODE_SIGN_IDENTITY = "-"; + COMBINE_HIDPI_IMAGES = YES; + 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.13; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - DC3773ED19C8CBB3004FCF85 /* Build configuration list for PBXProject "SQLite" */ = { + 03A65E6F1C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLite tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03A65E6B1C6BB0F60062603F /* Debug */, + 03A65E6C1C6BB0F60062603F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03A65E701C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLiteTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03A65E6D1C6BB0F60062603F /* Debug */, + 03A65E6E1C6BB0F60062603F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A121AC4C1CA35C79005A31D1 /* Build configuration list for PBXNativeTarget "SQLite watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A121AC4A1CA35C79005A31D1 /* Debug */, + A121AC4B1CA35C79005A31D1 /* Release */, + ); + 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 = ( - DC37740719C8CBB3004FCF85 /* Debug */, - DC37740819C8CBB3004FCF85 /* Release */, + DEB3070F2B61CF9500F9D46B /* Debug */, + DEB307102B61CF9500F9D46B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DC37740919C8CBB3004FCF85 /* Build configuration list for PBXNativeTarget "SQLite" */ = { + EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */ = { isa = XCConfigurationList; buildConfigurations = ( - DC37740A19C8CBB3004FCF85 /* Debug */, - DC37740B19C8CBB3004FCF85 /* Release */, + EE247AE51C3F04ED00AE3E12 /* Debug */, + EE247AE61C3F04ED00AE3E12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DC37740C19C8CBB3004FCF85 /* Build configuration list for PBXNativeTarget "SQLite Tests" */ = { + EE247AE71C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLite iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - DC37740D19C8CBB3004FCF85 /* Debug */, - DC37740E19C8CBB3004FCF85 /* Release */, + EE247AE81C3F04ED00AE3E12 /* Debug */, + EE247AE91C3F04ED00AE3E12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DCAE4D281ABE0B3400EFCE7A /* Build configuration list for PBXNativeTarget "sqlite3" */ = { + EE247AEA1C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - DCAE4D291ABE0B3400EFCE7A /* Debug */, - DCAE4D2A1ABE0B3400EFCE7A /* Release */, + EE247AEB1C3F04ED00AE3E12 /* Debug */, + EE247AEC1C3F04ED00AE3E12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DCC6B37D1A9191C300734B78 /* Build configuration list for PBXNativeTarget "SQLiteCipher" */ = { + EE247B511C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLite Mac" */ = { isa = XCConfigurationList; buildConfigurations = ( - DCC6B37E1A9191C300734B78 /* Debug */, - DCC6B37F1A9191C300734B78 /* Release */, + EE247B4D1C3F3ED000AE3E12 /* Debug */, + EE247B4E1C3F3ED000AE3E12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DCC6B38F1A9191D100734B78 /* Build configuration list for PBXNativeTarget "SQLiteCipher Tests" */ = { + EE247B521C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests Mac" */ = { isa = XCConfigurationList; buildConfigurations = ( - DCC6B3901A9191D100734B78 /* Debug */, - DCC6B3911A9191D100734B78 /* Release */, + EE247B4F1C3F3ED000AE3E12 /* Debug */, + EE247B501C3F3ED000AE3E12 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = DC3773EA19C8CBB3004FCF85 /* Project object */; + rootObject = EE247ACA1C3F04ED00AE3E12 /* Project object */; } diff --git a/SQLite.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 71% rename from SQLite.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 08de0be8..18d98100 100644 --- a/SQLite.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -2,7 +2,7 @@ - IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded - + IDEDidComputeMac32BitWarning + diff --git a/SQLite.xcodeproj/project.xcworkspace/xcshareddata/SQLite.xccheckout b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/SQLite.xccheckout deleted file mode 100644 index 60ff51e5..00000000 --- a/SQLite.xcodeproj/project.xcworkspace/xcshareddata/SQLite.xccheckout +++ /dev/null @@ -1,53 +0,0 @@ - - - - - IDESourceControlProjectFavoriteDictionaryKey - - IDESourceControlProjectIdentifier - E427014D-8915-4E3B-859D-5EB72D455321 - IDESourceControlProjectName - SQLite - IDESourceControlProjectOriginsDictionary - - 3AE8ED2E684071AF4FB151FA51BF266B82FF45BD - github.com:stephencelis/SQLite.swift.git - 55011AEBD444630C0E9DF47BD2E18FDBBDCC285D - https://github.com/sqlcipher/sqlcipher.git - - IDESourceControlProjectPath - SQLite.xcodeproj - IDESourceControlProjectRelativeInstallPathDictionary - - 3AE8ED2E684071AF4FB151FA51BF266B82FF45BD - ../.. - 55011AEBD444630C0E9DF47BD2E18FDBBDCC285D - ../../Vendor/sqlcipher - - IDESourceControlProjectURL - github.com:stephencelis/SQLite.swift.git - IDESourceControlProjectVersion - 111 - IDESourceControlProjectWCCIdentifier - 3AE8ED2E684071AF4FB151FA51BF266B82FF45BD - IDESourceControlProjectWCConfigurations - - - IDESourceControlRepositoryExtensionIdentifierKey - public.vcs.git - IDESourceControlWCCIdentifierKey - 55011AEBD444630C0E9DF47BD2E18FDBBDCC285D - IDESourceControlWCCName - sqlcipher - - - IDESourceControlRepositoryExtensionIdentifierKey - public.vcs.git - IDESourceControlWCCIdentifierKey - 3AE8ED2E684071AF4FB151FA51BF266B82FF45BD - IDESourceControlWCCName - SQLite - - - - diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLiteCipher.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme similarity index 78% rename from SQLite.xcodeproj/xcshareddata/xcschemes/SQLiteCipher.xcscheme rename to SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme index b54befa3..a0db21a2 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLiteCipher.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + - - - - - - diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite iOS.xcscheme similarity index 68% rename from SQLite.xcodeproj/xcshareddata/xcschemes/SQLite.xcscheme rename to SQLite.xcodeproj/xcshareddata/xcschemes/SQLite iOS.xcscheme index 0e27d856..6cadc289 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite iOS.xcscheme @@ -1,6 +1,6 @@ - - - - + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + - - - - - - diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme new file mode 100644 index 00000000..01966aa1 --- /dev/null +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite visionOS.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite visionOS.xcscheme new file mode 100644 index 00000000..c1536b66 --- /dev/null +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite visionOS.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Database.swift b/SQLite/Database.swift deleted file mode 100644 index 2e03aa88..00000000 --- a/SQLite/Database.swift +++ /dev/null @@ -1,599 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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 sqlite3 - -/// A connection (handle) to SQLite. -public final class Database { - - internal var handle: COpaquePointer = nil - - /// Whether or not the database was opened in a read-only state. - public var readonly: Bool { return sqlite3_db_readonly(handle, nil) == 1 } - - /// Instantiates a new connection to a database. - /// - /// :param: path The path to the database. Creates a new database if it - /// doesn’t already exist (unless in read-only mode). Pass - /// ":memory:" (or nothing) to open a new, in-memory - /// database. Pass "" (or nil) to open a temporary, - /// file-backed database. Default: ":memory:". - /// - /// :param: readonly Whether or not to open the database in a read-only - /// state. Default: false. - /// - /// :returns: A new database connection. - public init(_ path: String? = ":memory:", readonly: Bool = false) { - let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE - try { sqlite3_open_v2(path ?? "", &self.handle, flags | SQLITE_OPEN_FULLMUTEX, nil) } - } - - deinit { try { sqlite3_close(self.handle) } } // sqlite3_close_v2 in Yosemite/iOS 8? - - // MARK: - - - /// The last rowid inserted into the database via this connection. - public var lastInsertRowid: Int64? { - let rowid = sqlite3_last_insert_rowid(handle) - return rowid == 0 ? nil : rowid - } - - /// 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. - /// - /// :param: SQL A batch of zero or more semicolon-separated SQL statements. - public func execute(SQL: String) { - try { sqlite3_exec(self.handle, SQL, nil, nil, nil) } - } - - // MARK: - Prepare - - /// Prepares a single SQL statement (with optional parameter bindings). - /// - /// :param: statement A single SQL statement. - /// - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: A prepared statement. - public func prepare(statement: String, _ bindings: Binding?...) -> Statement { - if !bindings.isEmpty { return prepare(statement, bindings) } - return Statement(self, statement) - } - - /// Prepares a single SQL statement and binds parameters to it. - /// - /// :param: statement A single SQL statement. - /// - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: A prepared statement. - public func prepare(statement: String, _ bindings: [Binding?]) -> Statement { - return prepare(statement).bind(bindings) - } - - /// Prepares a single SQL statement and binds parameters to it. - /// - /// :param: statement A single SQL statement. - /// - /// :param: bindings A dictionary of named parameters to bind to the - /// statement. - /// - /// :returns: A prepared statement. - public func prepare(statement: String, _ bindings: [String: Binding?]) -> Statement { - return prepare(statement).bind(bindings) - } - - // MARK: - Run - - /// Runs a single SQL statement (with optional parameter bindings). - /// - /// :param: statement A single SQL statement. - /// - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: The statement. - public func run(statement: String, _ bindings: Binding?...) -> Statement { - return run(statement, bindings) - } - - /// Prepares, binds, and runs a single SQL statement. - /// - /// :param: statement A single SQL statement. - /// - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: The statement. - public func run(statement: String, _ bindings: [Binding?]) -> Statement { - return prepare(statement).run(bindings) - } - - /// Prepares, binds, and runs a single SQL statement. - /// - /// :param: statement A single SQL statement. - /// - /// :param: bindings A dictionary of named parameters to bind to the - /// statement. - /// - /// :returns: The statement. - public func run(statement: String, _ bindings: [String: Binding?]) -> Statement { - return prepare(statement).run(bindings) - } - - // MARK: - Scalar - - /// Runs a single SQL statement (with optional parameter bindings), - /// returning the first value of the first row. - /// - /// :param: statement A single SQL statement. - /// - /// :param: 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?...) -> Binding? { - return scalar(statement, bindings) - } - - /// Prepares, binds, and runs a single SQL statement, returning the first - /// value of the first row. - /// - /// :param: statement A single SQL statement. - /// - /// :param: 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?]) -> Binding? { - return prepare(statement).scalar(bindings) - } - - /// Prepares, binds, and runs a single SQL statement, returning the first - /// value of the first row. - /// - /// :param: statement A single SQL statement. - /// - /// :param: 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?]) -> Binding? { - return 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" - - } - - /// The result of a transaction. - public enum TransactionResult: String { - - /// Commits a transaction. - case Commit = "COMMIT TRANSACTION" - - /// Rolls a transaction back. - case Rollback = "ROLLBACK TRANSACTION" - - } - - /// Starts a new transaction with the given mode. - /// - /// :param: mode The mode in which a transaction acquires a lock. (Default: - /// .Deferred.) - /// - /// :returns: The BEGIN TRANSACTION statement. - public func transaction(_ mode: TransactionMode = .Deferred) -> Statement { - return run("BEGIN \(mode.rawValue) TRANSACTION") - } - - /// Runs a transaction with the given savepoint name (if omitted, it will - /// generate a UUID). - /// - /// :param: mode The mode in which a transaction acquires a lock. (Default: - /// .Deferred.) - /// - /// :param: block A closure to run SQL statements within the transaction. - /// Should return a TransactionResult depending on success or - /// failure. - /// - /// :returns: The COMMIT or ROLLBACK statement. - public func transaction(_ mode: TransactionMode = .Deferred, @noescape _ block: (txn: Statement) -> TransactionResult) -> Statement { - return run(block(txn: transaction(mode)).rawValue) - } - - /// Commits the current transaction (or, if a savepoint is open, releases - /// the current savepoint). - /// - /// :param: all Only applicable if a savepoint is open. If true, commits all - /// open savepoints, otherwise releases the current savepoint. - /// (Default: false.) - /// - /// :returns: The COMMIT (or RELEASE) statement. - public func commit(all: Bool = false) -> Statement { - if !savepointStack.isEmpty && !all { - return release() - } - savepointStack.removeAll() - return run(TransactionResult.Commit.rawValue) - } - - /// Rolls back the current transaction (or, if a savepoint is open, the - /// current savepoint). - /// - /// :param: all Only applicable if a savepoint is open. If true, rolls back - /// all open savepoints, otherwise rolls back the current - /// savepoint. (Default: false.) - /// - /// :returns: The ROLLBACK statement. - public func rollback(all: Bool = false) -> Statement { - if !savepointStack.isEmpty && !all { - return rollback(savepointStack.removeLast()) - } - savepointStack.removeAll() - return run(TransactionResult.Rollback.rawValue) - } - - // MARK: - Savepoints - - /// The result of a savepoint. - public enum SavepointResult { - - /// Releases a savepoint. - case Release - - /// Rolls a savepoint back. - case Rollback - - } - - private var savepointStack = [String]() - - /// Starts a new transaction with the given savepoint name. - /// - /// :param: savepointName A unique identifier for the savepoint. - /// - /// :returns: The SAVEPOINT statement. - public func savepoint(_ savepointName: String? = nil) -> Statement { - let name = savepointName ?? NSUUID().UUIDString - savepointStack.append(name) - return run("SAVEPOINT \(quote(literal: name))") - } - - /// Runs a transaction with the given savepoint name (if omitted, it will - /// generate a UUID). - /// - /// :param: savepointName A unique identifier for the savepoint (optional). - /// - /// :param: block A closure to run SQL statements within the - /// transaction. Should return a SavepointResult - /// depending on success or failure. - /// - /// :returns: The RELEASE or ROLLBACK statement. - public func savepoint(_ savepointName: String? = nil, @noescape _ block: (txn: Statement) -> SavepointResult) -> Statement { - switch block(txn: savepoint(savepointName)) { - case .Release: - return release() - case .Rollback: - return rollback() - } - } - - /// Releases a savepoint with the given savepoint name (or the most - /// recently-opened savepoint). - /// - /// :param: savepointName A unique identifier for the savepoint (optional). - /// - /// :returns: The RELEASE SAVEPOINT statement. - public func release(_ savepointName: String? = nil) -> Statement { - let name = savepointName ?? savepointStack.removeLast() - if let idx = find(savepointStack, name) { savepointStack.removeRange(idx.. Statement { - if let idx = find(savepointStack, savepointName) { savepointStack.removeRange(idx.. Bool)?) { - try { - if let callback = callback { - self.busyHandler = { callback(tries: Int($0)) ? 1 : 0 } - } else { - self.busyHandler = nil - } - return _SQLiteBusyHandler(self.handle, self.busyHandler) - } - } - private var busyHandler: _SQLiteBusyHandlerCallback? - - /// Sets a handler to call when a statement is executed with the compiled - /// SQL. - /// - /// :param: callback This block is invoked when a statement is executed with - /// the compiled SQL as its argument. E.g., pass `println` - /// to act as a logger. - public func trace(callback: ((SQL: String) -> ())?) { - if let callback = callback { - trace = { callback(SQL: String.fromCString($0)!) } - } else { - trace = nil - } - _SQLiteTrace(handle, trace) - } - private var trace: _SQLiteTraceCallback? - - /// 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 static func fromRawValue(rawValue: Int32) -> Operation { - switch rawValue { - case SQLITE_INSERT: - return .Insert - case SQLITE_UPDATE: - return .Update - case SQLITE_DELETE: - return .Delete - default: - fatalError("unhandled operation code: \(rawValue)") - } - } - - } - - /// Registers a callback to be invoked whenever a row is inserted, updated, - /// or deleted in a rowid table. - /// - /// :param: 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) -> ())?) { - if let callback = callback { - updateHook = { operation, db, table, rowid in - callback( - operation: .fromRawValue(operation), - db: String.fromCString(db)!, - table: String.fromCString(table)!, - rowid: rowid - ) - } - } else { - updateHook = nil - } - _SQLiteUpdateHook(handle, updateHook) - } - private var updateHook: _SQLiteUpdateHookCallback? - - /// Registers a callback to be invoked whenever a transaction is committed. - /// - /// :param: callback A callback that must return `.Commit` or `.Rollback` to - /// determine whether a transaction should be committed or - /// not. - public func commitHook(callback: (() -> TransactionResult)?) { - if let callback = callback { - commitHook = { callback() == .Commit ? 0 : 1 } - } else { - commitHook = nil - } - _SQLiteCommitHook(handle, commitHook) - } - private var commitHook: _SQLiteCommitHookCallback? - - /// Registers a callback to be invoked whenever a transaction rolls back. - /// - /// :param: callback A callback invoked when a transaction is rolled back. - public func rollbackHook(callback: (() -> ())?) { - rollbackHook = callback.map { $0 } - _SQLiteRollbackHook(handle, rollbackHook) - } - private var rollbackHook: _SQLiteRollbackHookCallback? - - /// Creates or redefines a custom SQL function. - /// - /// :param: function The name of the function to create or redefine. - /// - /// :param: argc The number of arguments that the function takes. - /// If this parameter is -1, then the SQL function may - /// take any number of arguments. (Default: -1.) - /// - /// :param: deterministic Whether or not the function is deterministic. If - /// the function always returns the same result for a - /// given input, SQLite can make optimizations. - /// (Default: false.) - /// - /// :param: 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 create(#function: String, argc: Int = -1, deterministic: Bool = false, _ block: (args: [Binding?]) -> Binding?) { - try { - if self.functions[function] == nil { self.functions[function] = [:] } - self.functions[function]?[argc] = { context, argc, argv in - let arguments: [Binding?] = map(0.. ComparisonResult) { - try { - self.collations[collation] = { lhs, rhs in - return Int32(block(lhs: String.fromCString(lhs)!, rhs: String.fromCString(rhs)!).rawValue) - } - return _SQLiteCreateCollation(self.handle, collation, self.collations[collation]) - } - } - private var collations = [String: _SQLiteCreateCollationCallback]() - - // MARK: - Error Handling - - /// Returns the last error produced on this connection. - public var lastError: String { - return String.fromCString(sqlite3_errmsg(handle))! - } - - internal func try(block: () -> Int32) { - perform { if block() != SQLITE_OK { assertionFailure("\(self.lastError)") } } - } - - // MARK: - Threading - - private let queue = dispatch_queue_create("SQLite.Database", DISPATCH_QUEUE_SERIAL) - - internal func perform(block: () -> ()) { dispatch_sync(queue, block) } - -} - -// MARK: - Printable -extension Database: Printable { - - public var description: String { - return String.fromCString(sqlite3_db_filename(handle, nil))! - } - -} - -internal func quote(#literal: String) -> String { - return quote(literal, "'") -} - -internal func quote(#identifier: String) -> String { - return quote(identifier, "\"") -} - -private func quote(string: String, mark: Character) -> String { - let escaped = reduce(string, "") { string, character in - string + (character == mark ? "\(mark)\(mark)" : "\(character)") - } - return "\(mark)\(escaped)\(mark)" -} diff --git a/SQLite/Expression.swift b/SQLite/Expression.swift deleted file mode 100644 index e076b9af..00000000 --- a/SQLite/Expression.swift +++ /dev/null @@ -1,954 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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. -// - -/// A typed SQL expression that wraps a raw string and bindings. -public struct Expression { - - public let SQL: String - - public let bindings: [Binding?] - - private var ascending: Bool? - - private var original: Expressible? - - /// Builds an SQL expression with a literal string and an optional list of - /// bindings. - /// - /// :param: SQL An SQL string. - /// - /// :param: bindings Values to be bound to the given SQL. - public init(literal SQL: String = "", _ bindings: [Binding?] = []) { - (self.SQL, self.bindings) = (SQL, bindings) - } - - /// Builds an SQL expression with a quoted identifier. - /// - /// :param: identifier An SQL identifier (*e.g.*, a column name). - public init(_ identifier: String) { - self.init(literal: quote(identifier: identifier)) - } - - /// Builds an SQL expression with the given value. - /// - /// :param: value An encodable SQL value. - public init(value: V?) { - self.init(binding: value?.datatypeValue) - } - - /// Builds an SQL expression with the given value. - /// - /// :param: binding A raw SQL value. - internal init(binding: Binding?) { - self.init(literal: "?", [binding]) - } - - /// Returns an ascending sort version of the expression. - public var asc: Expression { - var expression = self - expression.ascending = true - return expression - } - - /// Returns an descending sort version of the expression. - public var desc: Expression { - var expression = self - expression.ascending = false - return expression - } - - /// Returns an aliased version of the expression using AS. The expression is - /// expanded contextually (e.g., in SELECT clauses). - public func alias(alias: String) -> Expression { - return self.alias(Expression(alias)) - } - - private func alias(alias: Expression) -> Expression { - var expression = Expression(alias) - expression.original = self - return expression - } - - internal static func join(separator: String, _ expressions: [Expressible]) -> Expression<()> { - var (SQL, bindings) = ([String](), [Binding?]()) - for expressible in expressions { - let expression = expressible.expression - SQL.append(expression.SQL) - bindings.extend(expression.bindings) - } - return Expression<()>(literal: Swift.join(separator, SQL), bindings) - } - - internal init(_ expression: Expression) { - self.init(literal: expression.SQL, expression.bindings) - (ascending, original) = (expression.ascending, expression.original) - } - - internal var ordered: Expression<()> { - if let ascending = ascending { - return Expression.join(" ", [self, Expression(literal: ascending ? "ASC" : "DESC")]) - } - return Expression<()>(self) - } - - internal var aliased: Expression { - if let aliased = original?.expression { - return Expression(literal: "(\(aliased.SQL)) AS \(SQL)", aliased.bindings + bindings) - } - return self - } - - internal var unaliased: Expression { - return original.map { Expression($0.expression) } ?? self - } - - internal func reverse() -> Expression { - var expression = self - expression.ascending = expression.ascending.map(!) ?? false - return expression - } - - // naïve compiler for statements that can't be bound, e.g., CREATE TABLE - internal func compile() -> String { - var idx = 0 - return reduce(SQL, "") { SQL, character in - let string = String(character) - return SQL + (string == "?" ? transcode(self.bindings[idx++]) : string) - } - } - -} - -public protocol Expressible { - - var expression: Expression<()> { get } - -} - -extension Blob: Expressible { - - public var expression: Expression<()> { - return Expression(binding: self) - } - -} - -extension Bool: Expressible { - - public var expression: Expression<()> { - return Expression(value: self) - } - -} - -extension Double: Expressible { - - public var expression: Expression<()> { - return Expression(binding: self) - } - -} - -extension Int: Expressible { - - public var expression: Expression<()> { - // FIXME: rdar://TODO segfaults during archive // return Expression(value: self) - return Expression(binding: datatypeValue) - } - -} - -extension Int64: Expressible { - - public var expression: Expression<()> { - return Expression(binding: self) - } - -} - -extension String: Expressible { - - public var expression: Expression<()> { - return Expression(binding: self) - } - -} - -extension Expression: Expressible { - - public var expression: Expression<()> { - return Expression<()>(self) - } - -} - -extension Query: Expressible { - - public var expression: Expression<()> { - if tableName.original != nil { - return selectExpression.alias(tableName) - } - return wrap("", selectExpression) - } - -} - -// MARK: - Expressions - -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 lhs + Expression(binding: rhs) } -public func + (lhs: Expression, rhs: String) -> Expression { return lhs + Expression(binding: rhs) } -public func + (lhs: String, rhs: Expression) -> Expression { return Expression(binding: lhs) + rhs } -public func + (lhs: String, rhs: Expression) -> Expression { return Expression(binding: lhs) + rhs } - -public func + (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func + (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func + (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func + (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func + (lhs: Expression, rhs: V) -> Expression { return lhs + Expression(value: rhs) } -public func + (lhs: Expression, rhs: V) -> Expression { return lhs + Expression(value: rhs) } -public func + (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) + rhs } -public func + (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) + rhs } - -public func - (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func - (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func - (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func - (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func - (lhs: Expression, rhs: V) -> Expression { return lhs - Expression(value: rhs) } -public func - (lhs: Expression, rhs: V) -> Expression { return lhs - Expression(value: rhs) } -public func - (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) - rhs } -public func - (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) - rhs } - -public func * (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func * (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func * (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func * (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func * (lhs: Expression, rhs: V) -> Expression { return lhs * Expression(value: rhs) } -public func * (lhs: Expression, rhs: V) -> Expression { return lhs * Expression(value: rhs) } -public func * (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) * rhs } -public func * (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) * rhs } - -public func / (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func / (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func / (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func / (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func / (lhs: Expression, rhs: V) -> Expression { return lhs / Expression(value: rhs) } -public func / (lhs: Expression, rhs: V) -> Expression { return lhs / Expression(value: rhs) } -public func / (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) / rhs } -public func / (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) / rhs } - -public func % (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func % (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func % (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func % (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func % (lhs: Expression, rhs: V) -> Expression { return lhs % Expression(value: rhs) } -public func % (lhs: Expression, rhs: V) -> Expression { return lhs % Expression(value: rhs) } -public func % (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) % rhs } -public func % (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) % rhs } - -public func << (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func << (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func << (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func << (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func << (lhs: Expression, rhs: V) -> Expression { return lhs << Expression(value: rhs) } -public func << (lhs: Expression, rhs: V) -> Expression { return lhs << Expression(value: rhs) } -public func << (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) << rhs } -public func << (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) << rhs } - -public func >> (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func >> (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func >> (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func >> (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func >> (lhs: Expression, rhs: V) -> Expression { return lhs >> Expression(value: rhs) } -public func >> (lhs: Expression, rhs: V) -> Expression { return lhs >> Expression(value: rhs) } -public func >> (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) >> rhs } -public func >> (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) >> rhs } - -public func & (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func & (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func & (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func & (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func & (lhs: Expression, rhs: V) -> Expression { return lhs & Expression(value: rhs) } -public func & (lhs: Expression, rhs: V) -> Expression { return lhs & Expression(value: rhs) } -public func & (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) & rhs } -public func & (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) & rhs } - -public func | (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func | (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func | (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func | (lhs: Expression, rhs: Expression) -> Expression { return infix(__FUNCTION__, lhs, rhs) } -public func | (lhs: Expression, rhs: V) -> Expression { return lhs | Expression(value: rhs) } -public func | (lhs: Expression, rhs: V) -> Expression { return lhs | Expression(value: rhs) } -public func | (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) | rhs } -public func | (lhs: V, rhs: Expression) -> Expression { return Expression(value: 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 ^ Expression(value: rhs) } -public func ^ (lhs: Expression, rhs: V) -> Expression { return lhs ^ Expression(value: rhs) } -public func ^ (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) ^ rhs } -public func ^ (lhs: V, rhs: Expression) -> Expression { return Expression(value: lhs) ^ rhs } - -public prefix func ~ (rhs: Expression) -> Expression { return wrap(__FUNCTION__, rhs) } -public prefix func ~ (rhs: Expression) -> Expression { return wrap(__FUNCTION__, rhs) } - -public enum Collation { - - case Binary - - case Nocase - - case Rtrim - - case Custom(String) - -} - -extension Collation: Printable { - - public var description: String { - switch self { - case Binary: - return "BINARY" - case Nocase: - return "NOCASE" - case Rtrim: - return "RTRIM" - case Custom(let collation): - return collation - } - } - -} - -public func collate(collation: Collation, expression: Expression) -> Expression { - return infix("COLLATE", expression, Expression(collation.description)) -} -public func collate(collation: Collation, expression: Expression) -> Expression { - return infix("COLLATE", expression, Expression(collation.description)) -} - -public func cast(expression: Expression) -> Expression { - return Expression(literal: "CAST (\(expression.SQL) AS \(U.declaredDatatype))", expression.bindings) -} -public func cast(expression: Expression) -> Expression { - return Expression(literal: "CAST (\(expression.SQL) AS \(U.declaredDatatype))", expression.bindings) -} - -// MARK: - Predicates - -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 lhs == Expression(value: rhs) -} -public func == (lhs: Expression, rhs: V?) -> Expression { - if let rhs = rhs { return lhs == Expression(value: rhs) } - return Expression(literal: "\(lhs.SQL) IS ?", lhs.bindings + [nil]) -} -public func == (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) == rhs -} -public func == (lhs: V?, rhs: Expression) -> Expression { - if let lhs = lhs { return Expression(value: lhs) == rhs } - return Expression(literal: "? IS \(rhs.SQL)", [nil] + rhs.bindings) -} - -public func != (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func != (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func != (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func != (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func != (lhs: Expression, rhs: V) -> Expression { - return lhs != Expression(value: rhs) -} -public func != (lhs: Expression, rhs: V?) -> Expression { - if let rhs = rhs { return lhs != Expression(value: rhs) } - return Expression(literal: "\(lhs.SQL) IS NOT ?", lhs.bindings + [nil]) -} -public func != (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) != rhs -} -public func != (lhs: V?, rhs: Expression) -> Expression { - if let lhs = lhs { return Expression(value: lhs) != rhs } - return Expression(literal: "? IS NOT \(rhs.SQL)", [nil] + rhs.bindings) -} - -public func > (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func > (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func > (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func > (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func > (lhs: Expression, rhs: V) -> Expression { - return lhs > Expression(value: rhs) -} -public func > (lhs: Expression, rhs: V) -> Expression { - return lhs > Expression(value: rhs) -} -public func > (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) > rhs -} -public func > (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) > rhs -} - -public func >= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func >= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func >= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func >= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func >= (lhs: Expression, rhs: V) -> Expression { - return lhs >= Expression(value: rhs) -} -public func >= (lhs: Expression, rhs: V) -> Expression { - return lhs >= Expression(value: rhs) -} -public func >= (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) >= rhs -} -public func >= (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) >= rhs -} - -public func < (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func < (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func < (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func < (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func < (lhs: Expression, rhs: V) -> Expression { - return lhs < Expression(value: rhs) -} -public func < (lhs: Expression, rhs: V) -> Expression { - return lhs < Expression(value: rhs) -} -public func < (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) < rhs -} -public func < (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) < rhs -} - -public func <= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func <= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func <= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func <= (lhs: Expression, rhs: Expression) -> Expression { - return infix(__FUNCTION__, lhs, rhs) -} -public func <= (lhs: Expression, rhs: V) -> Expression { - return lhs <= Expression(value: rhs) -} -public func <= (lhs: Expression, rhs: V) -> Expression { - return lhs <= Expression(value: rhs) -} -public func <= (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) <= rhs -} -public func <= (lhs: V, rhs: Expression) -> Expression { - return Expression(value: lhs) <= rhs -} - -public prefix func - (rhs: Expression) -> Expression { return wrap(__FUNCTION__, rhs) } -public prefix func - (rhs: Expression) -> Expression { return wrap(__FUNCTION__, rhs) } - -public func ~= , V.Datatype == I.Bound>(lhs: I, rhs: Expression) -> Expression { - return Expression(literal: "\(rhs.SQL) BETWEEN ? AND ?", rhs.bindings + [lhs.start, lhs.end]) -} -public func ~= , V.Datatype == I.Bound>(lhs: I, rhs: Expression) -> Expression { - return Expression(lhs ~= Expression(rhs)) -} - -// MARK: Operators - -public func like(string: String, expression: Expression) -> Expression { - return infix("LIKE", expression, Expression(binding: string)) -} -public func like(string: String, expression: Expression) -> Expression { - return infix("LIKE", expression, Expression(binding: string)) -} - -public func glob(string: String, expression: Expression) -> Expression { - return infix("GLOB", expression, Expression(binding: string)) -} -public func glob(string: String, expression: Expression) -> Expression { - return infix("GLOB", expression, Expression(binding: string)) -} - -public func match(string: String, expression: Expression) -> Expression { - return infix("MATCH", expression, Expression(binding: string)) -} -public func match(string: String, expression: Expression) -> Expression { - return infix("MATCH", expression, Expression(binding: string)) -} - -// MARK: Compound - -public func && (lhs: Expression, rhs: Expression) -> Expression { return infix("AND", lhs, rhs) } -public func && (lhs: Expression, rhs: Expression) -> Expression { return infix("AND", lhs, rhs) } -public func && (lhs: Expression, rhs: Expression) -> Expression { return infix("AND", lhs, rhs) } -public func && (lhs: Expression, rhs: Expression) -> Expression { return infix("AND", lhs, rhs) } -public func && (lhs: Expression, rhs: Bool) -> Expression { return lhs && Expression(value: rhs) } -public func && (lhs: Expression, rhs: Bool) -> Expression { return lhs && Expression(value: rhs) } -public func && (lhs: Bool, rhs: Expression) -> Expression { return Expression(value: lhs) && rhs } -public func && (lhs: Bool, rhs: Expression) -> Expression { return Expression(value: lhs) && rhs } - -public func || (lhs: Expression, rhs: Expression) -> Expression { return infix("OR", lhs, rhs) } -public func || (lhs: Expression, rhs: Expression) -> Expression { return infix("OR", lhs, rhs) } -public func || (lhs: Expression, rhs: Expression) -> Expression { return infix("OR", lhs, rhs) } -public func || (lhs: Expression, rhs: Expression) -> Expression { return infix("OR", lhs, rhs) } -public func || (lhs: Expression, rhs: Bool) -> Expression { return lhs || Expression(value: rhs) } -public func || (lhs: Expression, rhs: Bool) -> Expression { return lhs || Expression(value: rhs) } -public func || (lhs: Bool, rhs: Expression) -> Expression { return Expression(value: lhs) || rhs } -public func || (lhs: Bool, rhs: Expression) -> Expression { return Expression(value: lhs) || rhs } - -public prefix func ! (rhs: Expression) -> Expression { return wrap("NOT ", rhs) } -public prefix func ! (rhs: Expression) -> Expression { return wrap("NOT ", rhs) } - -// MARK: - Core Functions - -public func abs(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func abs(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -// FIXME: support Expression..., Expression when Swift supports inner variadic signatures -public func coalesce(expressions: Expression...) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", expressions.map { $0 } as [Expressible])) -} - -public func ifnull(expression: Expression, defaultValue: V) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, defaultValue])) -} -public func ifnull(expression: Expression, defaultValue: Expression) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, defaultValue])) -} -public func ifnull(expression: Expression, defaultValue: Expression) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, defaultValue])) -} -public func ?? (expression: Expression, defaultValue: V) -> Expression { - return ifnull(expression, defaultValue) -} -public func ?? (expression: Expression, defaultValue: Expression) -> Expression { - return ifnull(expression, defaultValue) -} -public func ?? (expression: Expression, defaultValue: Expression) -> Expression { - return ifnull(expression, defaultValue) -} - -public func length(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func length(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -public func lower(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func lower(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -public func ltrim(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func ltrim(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -public func ltrim(expression: Expression, characters: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, characters])) -} -public func ltrim(expression: Expression, characters: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, characters])) -} - -public func random() -> Expression { return Expression(literal: __FUNCTION__) } - -public func replace(expression: Expression, match: String, subtitute: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, match, subtitute])) -} -public func replace(expression: Expression, match: String, subtitute: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, match, subtitute])) -} - -public func round(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func round(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func round(expression: Expression, precision: Int) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, precision])) -} -public func round(expression: Expression, precision: Int) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, precision])) -} - -public func rtrim(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func rtrim(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func rtrim(expression: Expression, characters: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, characters])) -} -public func rtrim(expression: Expression, characters: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, characters])) -} - -public func substr(expression: Expression, startIndex: Int) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, startIndex])) -} -public func substr(expression: Expression, startIndex: Int) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, startIndex])) -} - -public func substr(expression: Expression, position: Int, length: Int) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, position, length])) -} -public func substr(expression: Expression, position: Int, length: Int) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, position, length])) -} - -public func substr(expression: Expression, subRange: Range) -> Expression { - return substr(expression, subRange.startIndex, subRange.endIndex - subRange.startIndex) -} -public func substr(expression: Expression, subRange: Range) -> Expression { - return substr(expression, subRange.startIndex, subRange.endIndex - subRange.startIndex) -} - -public func trim(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func trim(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func trim(expression: Expression, characters: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, characters])) -} -public func trim(expression: Expression, characters: String) -> Expression { - return wrap(__FUNCTION__, Expression<()>.join(", ", [expression, characters])) -} - -public func upper(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func upper(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -// MARK: - Aggregate Functions - -public func count(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -public func count(#distinct: Expression) -> Expression { return wrapDistinct("count", distinct) } -public func count(#distinct: Expression) -> Expression { return wrapDistinct("count", distinct) } - -public func count(star: Star) -> Expression { return wrap(__FUNCTION__, star(nil, nil)) } - -public func max(expression: Expression) -> Expression { - return wrap(__FUNCTION__, expression) -} -public func max(expression: Expression) -> Expression { - return wrap(__FUNCTION__, expression) -} - -public func min(expression: Expression) -> Expression { - return wrap(__FUNCTION__, expression) -} -public func min(expression: Expression) -> Expression { - return wrap(__FUNCTION__, expression) -} - -public func average(expression: Expression) -> Expression { return wrap("avg", expression) } -public func average(expression: Expression) -> Expression { return wrap("avg", expression) } - -public func average(#distinct: Expression) -> Expression { return wrapDistinct("avg", distinct) } -public func average(#distinct: Expression) -> Expression { return wrapDistinct("avg", distinct) } - -public func sum(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func sum(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -public func sum(#distinct: Expression) -> Expression { return wrapDistinct("sum", distinct) } -public func sum(#distinct: Expression) -> Expression { return wrapDistinct("sum", distinct) } - -public func total(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } -public func total(expression: Expression) -> Expression { return wrap(__FUNCTION__, expression) } - -public func total(#distinct: Expression) -> Expression { return wrapDistinct("total", distinct) } -public func total(#distinct: Expression) -> Expression { return wrapDistinct("total", distinct) } - -internal func SQLite_count(expression: Expression) -> Expression { return count(expression) } - -internal func SQLite_count(#distinct: Expression) -> Expression { return count(distinct: distinct) } -internal func SQLite_count(#distinct: Expression) -> Expression { return count(distinct: distinct) } - -internal func SQLite_count(star: Star) -> Expression { return count(star) } - -internal func SQLite_max(expression: Expression) -> Expression { - return max(expression) -} -internal func SQLite_max(expression: Expression) -> Expression { - return max(expression) -} - -internal func SQLite_min(expression: Expression) -> Expression { - return min(expression) -} -internal func SQLite_min(expression: Expression) -> Expression { - return min(expression) -} - -internal func SQLite_average(expression: Expression) -> Expression { return average(expression) } -internal func SQLite_average(expression: Expression) -> Expression { return average(expression) } - -internal func SQLite_average(#distinct: Expression) -> Expression { return average(distinct: distinct) } -internal func SQLite_average(#distinct: Expression) -> Expression { return average(distinct: distinct) } - -internal func SQLite_sum(expression: Expression) -> Expression { return sum(expression) } -internal func SQLite_sum(expression: Expression) -> Expression { return sum(expression) } - -internal func SQLite_sum(#distinct: Expression) -> Expression { return sum(distinct: distinct) } -internal func SQLite_sum(#distinct: Expression) -> Expression { return sum(distinct: distinct) } - -internal func SQLite_total(expression: Expression) -> Expression { return total(expression) } -internal func SQLite_total(expression: Expression) -> Expression { return total(expression) } - -internal func SQLite_total(#distinct: Expression) -> Expression { return total(distinct: distinct) } -internal func SQLite_total(#distinct: Expression) -> Expression { return total(distinct: distinct) } - -private func wrapDistinct(function: String, expression: Expression) -> Expression { - return wrap(function, Expression<()>.join(" ", [Expression<()>(literal: "DISTINCT"), expression])) -} - -// MARK: - Helper - -public typealias Star = (Expression?, Expression?) -> Expression<()> - -public func * (Expression?, Expression?) -> Expression<()> { - return Expression(literal: "*") -} -public func contains(values: C, column: Expression) -> Expression { - let templates = join(", ", [String](count: count(values), repeatedValue: "?")) - return infix("IN", column, Expression(literal: "(\(templates))", map(values) { $0.datatypeValue })) -} -public func contains(values: C, column: Expression) -> Expression { - return contains(values, Expression(column)) -} -public func contains(values: Query, column: Expression) -> Expression { - return infix("IN", column, wrap("", values.selectExpression) as Expression<()>) -} - -// MARK: - Modifying - -/// A pair of expressions used to set values in INSERT and UPDATE statements. -public typealias Setter = (Expressible, Expressible) - -/// Returns a setter to be used with INSERT and UPDATE statements. -/// -/// :param: column The column being set. -/// -/// :param: value The value the column is being set to. -/// -/// :returns: A setter that can be used in a Query's insert and update -/// functions. -public func set(column: Expression, value: V) -> Setter { - return (column, Expression<()>(value: value)) -} -public func set(column: Expression, value: V?) -> Setter { - return (column, Expression<()>(value: value)) -} -public func set(column: Expression, value: Expression) -> Setter { return (column, value) } -public func set(column: Expression, value: Expression) -> Setter { return (column, value) } -public func set(column: Expression, value: Expression) -> Setter { return (column, value) } -public func set(column: Expression, value: Expression) -> Setter { return (column, value) } - -infix operator <- { associativity left precedence 140 } -public func <- (column: Expression, value: Expression) -> Setter { return set(column, value) } -public func <- (column: Expression, value: Expression) -> Setter { return set(column, value) } -public func <- (column: Expression, value: Expression) -> Setter { return set(column, value) } -public func <- (column: Expression, value: Expression) -> Setter { return set(column, value) } -public func <- (column: Expression, value: V) -> Setter { return set(column, value) } -public func <- (column: Expression, value: V?) -> Setter { return set(column, value) } - -public func += (column: Expression, value: Expression) -> Setter { return set(column, column + value) } -public func += (column: Expression, value: Expression) -> Setter { return set(column, column + value) } -public func += (column: Expression, value: Expression) -> Setter { return set(column, column + value) } -public func += (column: Expression, value: Expression) -> Setter { return set(column, column + value) } -public func += (column: Expression, value: String) -> Setter { return set(column, column + value) } -public func += (column: Expression, value: String) -> Setter { return set(column, column + value) } - -public func += (column: Expression, value: Expression) -> Setter { - return set(column, column + value) -} -public func += (column: Expression, value: Expression) -> Setter { - return set(column, column + value) -} -public func += (column: Expression, value: Expression) -> Setter { - return set(column, column + value) -} -public func += (column: Expression, value: Expression) -> Setter { - return set(column, column + value) -} -public func += (column: Expression, value: V) -> Setter { return set(column, column + value) } -public func += (column: Expression, value: V) -> Setter { return set(column, column + value) } - -public func -= (column: Expression, value: Expression) -> Setter { - return set(column, column - value) -} -public func -= (column: Expression, value: Expression) -> Setter { - return set(column, column - value) -} -public func -= (column: Expression, value: Expression) -> Setter { - return set(column, column - value) -} -public func -= (column: Expression, value: Expression) -> Setter { - return set(column, column - value) -} -public func -= (column: Expression, value: V) -> Setter { return set(column, column - value) } -public func -= (column: Expression, value: V) -> Setter { return set(column, column - value) } - -public func *= (column: Expression, value: Expression) -> Setter { - return set(column, column * value) -} -public func *= (column: Expression, value: Expression) -> Setter { - return set(column, column * value) -} -public func *= (column: Expression, value: Expression) -> Setter { - return set(column, column * value) -} -public func *= (column: Expression, value: Expression) -> Setter { - return set(column, column * value) -} -public func *= (column: Expression, value: V) -> Setter { return set(column, column * value) } -public func *= (column: Expression, value: V) -> Setter { return set(column, column * value) } - -public func /= (column: Expression, value: Expression) -> Setter { - return set(column, column / value) -} -public func /= (column: Expression, value: Expression) -> Setter { - return set(column, column / value) -} -public func /= (column: Expression, value: Expression) -> Setter { - return set(column, column / value) -} -public func /= (column: Expression, value: Expression) -> Setter { - return set(column, column / value) -} -public func /= (column: Expression, value: V) -> Setter { - return set(column, column / value) -} -public func /= (column: Expression, value: V) -> Setter { - return set(column, column / value) -} - -public func %= (column: Expression, value: Expression) -> Setter { return set(column, column % value) } -public func %= (column: Expression, value: Expression) -> Setter { return set(column, column % value) } -public func %= (column: Expression, value: Expression) -> Setter { return set(column, column % value) } -public func %= (column: Expression, value: Expression) -> Setter { return set(column, column % value) } -public func %= (column: Expression, value: V) -> Setter { return set(column, column % value) } -public func %= (column: Expression, value: V) -> Setter { return set(column, column % value) } - -public func <<= (column: Expression, value: Expression) -> Setter { return set(column, column << value) } -public func <<= (column: Expression, value: Expression) -> Setter { return set(column, column << value) } -public func <<= (column: Expression, value: Expression) -> Setter { return set(column, column << value) } -public func <<= (column: Expression, value: Expression) -> Setter { return set(column, column << value) } -public func <<= (column: Expression, value: V) -> Setter { return set(column, column << value) } -public func <<= (column: Expression, value: V) -> Setter { return set(column, column << value) } - -public func >>= (column: Expression, value: Expression) -> Setter { return set(column, column >> value) } -public func >>= (column: Expression, value: Expression) -> Setter { return set(column, column >> value) } -public func >>= (column: Expression, value: Expression) -> Setter { return set(column, column >> value) } -public func >>= (column: Expression, value: Expression) -> Setter { return set(column, column >> value) } -public func >>= (column: Expression, value: V) -> Setter { return set(column, column >> value) } -public func >>= (column: Expression, value: V) -> Setter { return set(column, column >> value) } - -public func &= (column: Expression, value: Expression) -> Setter { return set(column, column & value) } -public func &= (column: Expression, value: Expression) -> Setter { return set(column, column & value) } -public func &= (column: Expression, value: Expression) -> Setter { return set(column, column & value) } -public func &= (column: Expression, value: Expression) -> Setter { return set(column, column & value) } -public func &= (column: Expression, value: V) -> Setter { return set(column, column & value) } -public func &= (column: Expression, value: V) -> Setter { return set(column, column & value) } - -public func |= (column: Expression, value: Expression) -> Setter { return set(column, column | value) } -public func |= (column: Expression, value: Expression) -> Setter { return set(column, column | value) } -public func |= (column: Expression, value: Expression) -> Setter { return set(column, column | value) } -public func |= (column: Expression, value: Expression) -> Setter { return set(column, column | value) } -public func |= (column: Expression, value: V) -> Setter { return set(column, column | value) } -public func |= (column: Expression, value: V) -> Setter { return set(column, column | value) } - -public func ^= (column: Expression, value: Expression) -> Setter { return set(column, column ^ value) } -public func ^= (column: Expression, value: Expression) -> Setter { return set(column, column ^ value) } -public func ^= (column: Expression, value: Expression) -> Setter { return set(column, column ^ value) } -public func ^= (column: Expression, value: Expression) -> Setter { return set(column, column ^ value) } -public func ^= (column: Expression, value: V) -> Setter { return set(column, column ^ value) } -public func ^= (column: Expression, value: V) -> Setter { return set(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 } - -// MARK: - Internal - -internal let rowid = Expression("ROWID") - -internal func transcode(literal: Binding?) -> String { - if let literal = literal { - if let literal = literal as? Blob { return literal.description } - if let literal = literal as? String { return quote(literal: literal) } - return "\(literal)" - } - return "NULL" -} - -internal func wrap(function: String, expression: Expression) -> Expression { - return Expression(literal: "\(function)\(surround(expression.SQL))", expression.bindings) -} - -internal func infix(function: String, lhs: Expression, rhs: Expression) -> Expression { - return Expression(literal: surround("\(lhs.SQL) \(function) \(rhs.SQL)"), lhs.bindings + rhs.bindings) -} - -private func surround(expression: String) -> String { return "(\(expression))" } diff --git a/SQLite/FTS.swift b/SQLite/FTS.swift deleted file mode 100644 index 35afb07a..00000000 --- a/SQLite/FTS.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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 func fts4(columns: Expression...) -> Expression<()> { - return fts4(columns) -} - -// TODO: matchinfo, compress, uncompress -public func fts4(columns: [Expression], tokenize tokenizer: Tokenizer? = nil) -> Expression<()> { - var options = [String: String]() - options["tokenize"] = tokenizer?.description - return fts("fts4", columns, options) -} - -private func fts(function: String, columns: [Expression], options: [String: String]) -> Expression<()> { - var definitions: [Expressible] = columns.map { $0.expression } - for (key, value) in options { - definitions.append(Expression<()>(literal: "\(key)=\(value)")) - } - return wrap(function, Expression<()>.join(", ", definitions)) -} - -public enum Tokenizer { - - internal static var moduleName = "SQLite.swift" - - case Simple - - case Porter - - case Custom(String) - -} - -extension Tokenizer: Printable { - - public var description: String { - switch self { - case .Simple: - return "simple" - case .Porter: - return "porter" - case .Custom(let tokenizer): - return "\(quote(identifier: Tokenizer.moduleName)) \(quote(literal: tokenizer))" - } - } - -} - -public func match(string: String, expression: Query) -> Expression { - return infix("MATCH", Expression(expression.tableName), Expression(binding: string)) -} - -extension Database { - - public func register(tokenizer submoduleName: String, next: String -> (String, Range)?) { - try { - _SQLiteRegisterTokenizer(self.handle, Tokenizer.moduleName, submoduleName) { input, offset, length in - let string = String.fromCString(input)! - if var (token, range) = next(string) { - let view = string.utf8 - offset.memory += count(string.substringToIndex(range.startIndex).utf8) - length.memory = Int32(distance(range.startIndex.samePositionIn(view), range.endIndex.samePositionIn(view))) - return token - } - return nil - } - } - } - -} diff --git a/SQLite/Functions.swift b/SQLite/Functions.swift deleted file mode 100644 index 148ee031..00000000 --- a/SQLite/Functions.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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 Database { - - // MARK: - Type-Safe Function Creation Shims - - // MARK: 0 Arguments - - /// Creates or redefines a custom SQL function. - /// - /// :param: function The name of the function to create or redefine. - /// - /// :param: 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 create(#function: String, deterministic: Bool = false, _ block: () -> Z) -> (() -> Expression) { - return { self.create(function, 0, deterministic) { _ in return block() }([]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: () -> Z?) -> (() -> Expression) { - return { self.create(function, 0, deterministic) { _ in return block() }([]) } - } - - // MARK: 1 Argument - - public func create(#function: String, deterministic: Bool = false, _ block: A -> Z) -> (Expression -> Expression) { - return { self.create(function, 1, deterministic) { block(asValue($0[0])) }([$0]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: A? -> Z) -> (Expression -> Expression) { - return { self.create(function, 1, deterministic) { block($0[0].map(asValue)) }([$0]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: A -> Z?) -> (Expression -> Expression) { - return { self.create(function, 1, deterministic) { block(asValue($0[0])) }([$0]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: A? -> Z?) -> (Expression -> Expression) { - return { self.create(function, 1, deterministic) { block($0[0].map(asValue)) }([$0]) } - } - - // MARK: 2 Arguments - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A, B) -> Z) -> ((A, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), asValue($0[1])) }([$0, $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A?, B) -> Z) -> ((A?, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), asValue($0[1])) }([Expression(value: $0), $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A, B?) -> Z) -> ((A, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), $0[1].map(asValue)) }([$0, $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z) -> ((A?, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), $0[1].map(asValue)) }([Expression(value: $0), $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A, B) -> Z?) -> ((A, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), asValue($0[1])) }([$0, $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A?, B) -> Z?) -> ((A?, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), asValue($0[1])) }([Expression(value: $0), $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A, B?) -> Z?) -> ((A, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), $0[1].map(asValue)) }([$0, $1]) } - } - - public func create, B: Value>(#function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z?) -> ((A?, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), $0[1].map(asValue)) }([Expression(value: $0), $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A, B) -> Z) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), asValue($0[1])) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A?, B) -> Z) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), asValue($0[1])) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A, B?) -> Z) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), $0[1].map(asValue)) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), $0[1].map(asValue)) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A, B) -> Z?) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), asValue($0[1])) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A?, B) -> Z?) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), asValue($0[1])) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A, B?) -> Z?) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), $0[1].map(asValue)) }([$0, $1]) } - } - - public func create(#function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z?) -> ((Expression, Expression) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), $0[1].map(asValue)) }([$0, $1]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A, B) -> Z) -> ((Expression, B) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), asValue($0[1])) }([$0, $1]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A?, B) -> Z) -> ((Expression, B) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), asValue($0[1])) }([$0, $1]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A, B?) -> Z) -> ((Expression, B?) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), $0[1].map(asValue)) }([$0, Expression(value: $1)]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z) -> ((Expression, B?) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), $0[1].map(asValue)) }([$0, Expression(value: $1)]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A, B) -> Z?) -> ((Expression, B) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), asValue($0[1])) }([$0, $1]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A?, B) -> Z?) -> ((Expression, B) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), asValue($0[1])) }([$0, $1]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A, B?) -> Z?) -> ((Expression, B?) -> Expression) { - return { self.create(function, 2, deterministic) { block(asValue($0[0]), $0[1].map(asValue)) }([$0, Expression(value: $1)]) } - } - - public func create>(#function: String, deterministic: Bool = false, _ block: (A?, B?) -> Z?) -> ((Expression, B?) -> Expression) { - return { self.create(function, 2, deterministic) { block($0[0].map(asValue), $0[1].map(asValue)) }([$0, Expression(value: $1)]) } - } - - // MARK: - - - private func create(function: String, _ argc: Int, _ deterministic: Bool, _ block: [Binding?] -> Z) -> ([Expressible] -> Expression) { - return { Expression(self.create(function, argc, deterministic) { (arguments: [Binding?]) -> Z? in block(arguments) }($0)) } - } - - private func create(function: String, _ argc: Int, _ deterministic: Bool, _ block: [Binding?] -> Z?) -> ([Expressible] -> Expression) { - create(function: function, argc: argc, deterministic: deterministic) { block($0)?.datatypeValue } - return { arguments in wrap(quote(identifier: function), Expression.join(", ", arguments)) } - } - -} - -private func asValue(value: Binding) -> A { - return A.fromDatatypeValue(value as! A.Datatype) as! A -} - -private func asValue(value: Binding?) -> A { - return asValue(value!) -} diff --git a/SQLite/Info.plist b/SQLite/Info.plist deleted file mode 100644 index 9e2c8628..00000000 --- a/SQLite/Info.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - com.stephencelis.$(PRODUCT_NAME:rfc1034identifier) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 0.1 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSHumanReadableCopyright - Copyright © 2014–2015 Stephen Celis. - NSPrincipalClass - - - diff --git a/SQLite/Query.swift b/SQLite/Query.swift deleted file mode 100644 index 9048f3d9..00000000 --- a/SQLite/Query.swift +++ /dev/null @@ -1,882 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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. -// - -/// A query object. Used to build SQL statements with a collection of chainable -/// helper functions. -public struct Query { - - internal var database: Database - - internal init(_ database: Database, _ tableName: String) { - (self.database, self.tableName) = (database, Expression(tableName)) - } - - private init(_ database: Database, _ tableName: Expression<()>) { - (self.database, self.tableName) = (database, tableName) - } - - // MARK: - Keywords - - /// Determines the join operator for a query’s JOIN clause. - public enum JoinType: String { - - /// A CROSS JOIN. - case Cross = "CROSS" - - /// An INNER JOIN. - case Inner = "INNER" - - /// A LEFT OUTER JOIN. - case LeftOuter = "LEFT OUTER" - - } - - private var columns: [Expressible]? - private var distinct: Bool = false - internal var tableName: Expression<()> - private var joins: [(type: JoinType, table: Query, condition: Expression)] = [] - private var filter: Expression? - private var group: Expressible? - private var order = [Expressible]() - private var limit: (to: Int, offset: Int?)? = nil - - public func alias(alias: String) -> Query { - var query = self - query.tableName = query.tableName.alias(alias) - return query - } - - /// Sets the SELECT clause on the query. - /// - /// :param: all A list of expressions to select. - /// - /// :returns: A query with the given SELECT clause applied. - public func select(all: Expressible...) -> Query { - var query = self - (query.distinct, query.columns) = (false, all) - return query - } - - /// Sets the SELECT DISTINCT clause on the query. - /// - /// :param: columns A list of expressions to select. - /// - /// :returns: A query with the given SELECT DISTINCT clause applied. - public func select(distinct columns: Expressible...) -> Query { - var query = self - (query.distinct, query.columns) = (true, columns) - return query - } - - /// Sets the SELECT clause on the query. - /// - /// :param: star A literal *. - /// - /// :returns: A query with SELECT * applied. - public func select(star: Star) -> Query { - var query = self - (query.distinct, query.columns) = (false, nil) - return query - } - - /// Sets the SELECT DISTINCT * clause on the query. - /// - /// :param: star A literal *. - /// - /// :returns: A query with SELECT * applied. - public func select(distinct star: Star) -> Query { - return select(distinct: star(nil, nil)) - } - - /// Adds an INNER JOIN clause to the query. - /// - /// :param: table A query representing the other table. - /// - /// :param: on A boolean expression describing the join condition. - /// - /// :returns: A query with the given INNER JOIN clause applied. - public func join(table: Query, on: Expression) -> Query { - return join(.Inner, table, on: on) - } - - /// Adds an INNER JOIN clause to the query. - /// - /// :param: table A query representing the other table. - /// - /// :param: on A boolean expression describing the join condition. - /// - /// :returns: A query with the given INNER JOIN clause applied. - public func join(table: Query, on: Expression) -> Query { - return join(.Inner, table, on: on) - } - - /// Adds a JOIN clause to the query. - /// - /// :param: type The JOIN operator. - /// - /// :param: table A query representing the other table. - /// - /// :param: on A boolean expression describing the join condition. - /// - /// :returns: A query with the given JOIN clause applied. - public func join(type: JoinType, _ table: Query, on: Expression) -> Query { - var query = self - let join = (type: type, table: table, condition: table.filter.map { on && $0 } ?? on) - query.joins.append(join) - return query - } - - /// Adds a JOIN clause to the query. - /// - /// :param: type The JOIN operator. - /// - /// :param: table A query representing the other table. - /// - /// :param: on A boolean expression describing the join condition. - /// - /// :returns: A query with the given JOIN clause applied. - public func join(type: JoinType, _ table: Query, on: Expression) -> Query { - return join(type, table, on: Expression(on)) - } - - /// Adds a condition to the query’s WHERE clause. - /// - /// :param: condition A boolean expression to filter on. - /// - /// :returns: A query with the given WHERE clause applied. - public func filter(condition: Expression) -> Query { - var query = self - query.filter = filter.map { $0 && condition } ?? condition - return query - } - - /// Adds a condition to the query’s WHERE clause. - /// - /// :param: condition A boolean expression to filter on. - /// - /// :returns: A query with the given WHERE clause applied. - public func filter(condition: Expression) -> Query { - return filter(Expression(condition)) - } - - /// Sets a GROUP BY clause on the query. - /// - /// :param: by A list of columns to group by. - /// - /// :returns: A query with the given GROUP BY clause applied. - public func group(by: Expressible...) -> Query { - return group(by) - } - - /// Sets a GROUP BY clause (with optional HAVING) on the query. - /// - /// :param: by A column to group by. - /// - /// :param: having A condition determining which groups are returned. - /// - /// :returns: A query with the given GROUP BY clause applied. - public func group(by: Expressible, having: Expression) -> Query { - return group([by], having: having) - } - - /// Sets a GROUP BY clause (with optional HAVING) on the query. - /// - /// :param: by A column to group by. - /// - /// :param: having A condition determining which groups are returned. - /// - /// :returns: A query with the given GROUP BY clause applied. - public func group(by: Expressible, having: Expression) -> Query { - return group([by], having: having) - } - - /// Sets a GROUP BY-HAVING clause on the query. - /// - /// :param: by A list of columns to group by. - /// - /// :param: having A condition determining which groups are returned. - /// - /// :returns: A query with the given GROUP BY clause applied. - public func group(by: [Expressible], having: Expression? = nil) -> Query { - var query = self - var group = Expression<()>.join(" ", [Expression<()>(literal: "GROUP BY"), Expression<()>.join(", ", by)]) - if let having = having { group = Expression<()>.join(" ", [group, Expression<()>(literal: "HAVING"), having]) } - query.group = group - return query - } - - /// Sets a GROUP BY-HAVING clause on the query. - /// - /// :param: by A list of columns to group by. - /// - /// :param: having A condition determining which groups are returned. - /// - /// :returns: A query with the given GROUP BY clause applied. - public func group(by: [Expressible], having: Expression) -> Query { - return group(by, having: Expression(having)) - } - - /// Sets an ORDER BY clause on the query. - /// - /// :param: 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...) -> Query { - return order(by) - } - - /// Sets an ORDER BY clause on the query. - /// - /// :param: 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]) -> Query { - var query = self - query.order = by - return query - } - - /// Sets the LIMIT clause (and resets any OFFSET clause) on the query. - /// - /// :param: to The maximum number of rows to return. - /// - /// :returns: A query with the given LIMIT clause applied. - public func limit(to: Int?) -> Query { - return limit(to: to, offset: nil) - } - - /// Sets LIMIT and OFFSET clauses on the query. - /// - /// :param: to The maximum number of rows to return. - /// - /// :param: offset The number of rows to skip. - /// - /// :returns: A query with the given LIMIT and OFFSET clauses applied. - public func limit(to: Int, offset: Int) -> Query { - return limit(to: to, offset: offset) - } - - // prevents limit(nil, offset: 5) - private func limit(#to: Int?, offset: Int? = nil) -> Query { - var query = self - if let to = to { - query.limit = (to, offset) - } else { - query.limit = nil - } - return query - } - - // MARK: - Namespacing - - /// Prefixes a column expression with the query’s table name or alias. - /// - /// :param: column A column expression. - /// - /// :returns: A column expression namespaced with the query’s table name or - /// alias. - public func namespace(column: Expression) -> Expression { - return Expression(.join(".", [tableName, column])) - } - - // FIXME: rdar://18673897 // ... subscript(expression: Expression) -> Expression - - 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 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) } - - /// Prefixes a star with the query’s table name or alias. - /// - /// :param: star A literal *. - /// - /// :returns: A * expression namespaced with the query’s table name or - /// alias. - public subscript(star: Star) -> Expression<()> { - return namespace(star(nil, nil)) - } - - // MARK: - Compiling Statements - - internal var selectStatement: Statement { - let expression = selectExpression - return database.prepare(expression.SQL, expression.bindings) - } - - internal var selectExpression: Expression<()> { - var expressions = [selectClause] - joinClause.map(expressions.append) - whereClause.map(expressions.append) - group.map(expressions.append) - orderClause.map(expressions.append) - limitClause.map(expressions.append) - return Expression<()>.join(" ", expressions) - } - - /// ON CONFLICT resolutions. - public enum OnConflict: String { - - case Replace = "REPLACE" - - case Rollback = "ROLLBACK" - - case Abort = "ABORT" - - case Fail = "FAIL" - - case Ignore = "IGNORE" - - } - - private func insertStatement(values: [Setter], or: OnConflict? = nil) -> Statement { - var insertClause = "INSERT" - if let or = or { insertClause = "\(insertClause) OR \(or.rawValue)" } - var expressions: [Expressible] = [Expression<()>(literal: "\(insertClause) INTO \(tableName.unaliased.SQL)")] - let (c, v) = (Expression<()>.join(", ", values.map { $0.0 }), Expression<()>.join(", ", values.map { $0.1 })) - expressions.append(Expression<()>(literal: "(\(c.SQL)) VALUES (\(v.SQL))", c.bindings + v.bindings)) - whereClause.map(expressions.append) - let expression = Expression<()>.join(" ", expressions) - return database.prepare(expression.SQL, expression.bindings) - } - - private func updateStatement(values: [Setter]) -> Statement { - var expressions: [Expressible] = [Expression<()>(literal: "UPDATE \(tableName.unaliased.SQL) SET")] - expressions.append(Expression<()>.join(", ", values.map { Expression<()>.join(" = ", [$0, $1]) })) - whereClause.map(expressions.append) - let expression = Expression<()>.join(" ", expressions) - return database.prepare(expression.SQL, expression.bindings) - } - - private var deleteStatement: Statement { - var expressions: [Expressible] = [Expression<()>(literal: "DELETE FROM \(tableName.unaliased.SQL)")] - whereClause.map(expressions.append) - let expression = Expression<()>.join(" ", expressions) - return database.prepare(expression.SQL, expression.bindings) - } - - // MARK: - - - private var selectClause: Expressible { - var expressions: [Expressible] = [Expression<()>(literal: "SELECT")] - if distinct { expressions.append(Expression<()>(literal: "DISTINCT")) } - expressions.append(Expression<()>.join(", ", (columns ?? [Expression<()>(literal: "*")]).map { $0.expression.aliased })) - expressions.append(Expression<()>(literal: "FROM \(tableName.aliased.SQL)")) - return Expression<()>.join(" ", expressions) - } - - private var joinClause: Expressible? { - if joins.count == 0 { return nil } - return Expression<()>.join(" ", joins.map { type, table, condition in - let join = (table.columns == nil ? table.tableName : table.expression).aliased - return Expression<()>(literal: "\(type.rawValue) JOIN \(join.SQL) ON \(condition.SQL)", join.bindings + condition.bindings) - }) - } - - internal var whereClause: Expressible? { - if let filter = filter { - return Expression<()>(literal: "WHERE \(filter.SQL)", filter.bindings) - } - return nil - } - - private var orderClause: Expressible? { - if order.count == 0 { return nil } - let clause = Expression<()>.join(", ", order.map { $0.expression.ordered }) - return Expression<()>(literal: "ORDER BY \(clause.SQL)", clause.bindings) - } - - private var limitClause: Expressible? { - if let limit = limit { - var clause = Expression<()>(literal: "LIMIT \(limit.to)") - if let offset = limit.offset { - clause = Expression<()>.join(" ", [clause, Expression<()>(literal: "OFFSET \(offset)")]) - } - return clause - } - return nil - } - - // MARK: - Modifying - - /// Runs an INSERT statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The statement. - public func insert(value: Setter, _ more: Setter...) -> Statement { return insert([value] + more).statement } - - /// Runs an INSERT statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The rowid. - public func insert(value: Setter, _ more: Setter...) -> Int64? { return insert([value] + more).rowid } - - /// Runs an INSERT statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The rowid and statement. - public func insert(value: Setter, _ more: Setter...) -> (rowid: Int64?, statement: Statement) { - return insert([value] + more) - } - - /// Runs an INSERT statement against the query. - /// - /// :param: values An array of values to set. - /// - /// :returns: The rowid. - public func insert(values: [Setter]) -> Int64? { return insert(values).rowid } - - /// Runs an INSERT statement against the query. - /// - /// :param: values An array of values to set. - /// - /// :returns: The rowid and statement. - public func insert(values: [Setter]) -> (rowid: Int64?, statement: Statement) { - let statement = insertStatement(values).run() - return (statement.failed ? nil : database.lastInsertRowid, statement) - } - - public func insert(query: Query) -> Int? { return insert(query).changes } - - public func insert(query: Query) -> Statement { return insert(query).statement } - - public func insert(query: Query) -> (changes: Int?, statement: Statement) { - let expression = query.selectExpression - let statement = database.run("INSERT INTO \(tableName.unaliased.SQL) \(expression.SQL)", expression.bindings) - return (statement.failed ? nil : database.changes, statement) - } - - public func insert() -> Int64? { return insert().rowid } - - public func insert() -> Statement { return insert().statement } - - public func insert() -> (rowid: Int64?, statement: Statement) { - let statement = database.run("INSERT INTO \(tableName.unaliased.SQL) DEFAULT VALUES") - return (statement.failed ? nil : database.lastInsertRowid, statement) - } - - /// Runs a REPLACE statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The statement. - public func replace(values: Setter...) -> Statement { return replace(values).statement } - - /// Runs a REPLACE statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The rowid. - public func replace(values: Setter...) -> Int64? { return replace(values).rowid } - - /// Runs a REPLACE statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The rowid and statement. - public func replace(values: Setter...) -> (rowid: Int64?, statement: Statement) { - return replace(values) - } - - /// Runs a REPLACE statement against the query. - /// - /// :param: values An array of values to set. - /// - /// :returns: The rowid. - public func replace(values: [Setter]) -> Int64? { return replace(values).rowid } - - /// Runs a REPLACE statement against the query. - /// - /// :param: values An array of values to set. - /// - /// :returns: The rowid and statement. - public func replace(values: [Setter]) -> (rowid: Int64?, statement: Statement) { - let statement = insertStatement(values, or: .Replace).run() - return (statement.failed ? nil : database.lastInsertRowid, statement) - } - - /// Runs an UPDATE statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The statement. - public func update(values: Setter...) -> Statement { return update(values).statement } - - /// Runs an UPDATE statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The number of updated rows. - public func update(values: Setter...) -> Int? { return update(values).changes } - - /// Runs an UPDATE statement against the query. - /// - /// :param: values A list of values to set. - /// - /// :returns: The number of updated rows and statement. - public func update(values: Setter...) -> (changes: Int?, statement: Statement) { - return update(values) - } - - /// Runs an UPDATE statement against the query. - /// - /// :param: values An array of of values to set. - /// - /// :returns: The number of updated rows. - public func update(values: [Setter]) -> Int? { return update(values).changes } - - /// Runs an UPDATE statement against the query. - /// - /// :param: values An array of of values to set. - /// - /// :returns: The number of updated rows and statement. - public func update(values: [Setter]) -> (changes: Int?, statement: Statement) { - let statement = updateStatement(values).run() - return (statement.failed ? nil : database.changes, statement) - } - - /// Runs a DELETE statement against the query. - /// - /// :returns: The statement. - public func delete() -> Statement { return delete().statement } - - /// Runs a DELETE statement against the query. - /// - /// :returns: The number of deleted rows. - public func delete() -> Int? { return delete().changes } - - /// Runs a DELETE statement against the query. - /// - /// :returns: The number of deleted rows and statement. - public func delete() -> (changes: Int?, statement: Statement) { - let statement = deleteStatement.run() - return (statement.failed ? nil : database.changes, statement) - } - - // MARK: - Aggregate Functions - - /// Runs count() against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The number of rows matching the given column. - public func count(column: Expression) -> Int { - return calculate(SQLite_count(column))! - } - - /// Runs count() with DISTINCT against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The number of rows matching the given column. - public func count(distinct column: Expression) -> Int { - return calculate(SQLite_count(distinct: column))! - } - - /// Runs count() with DISTINCT against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The number of rows matching the given column. - public func count(distinct column: Expression) -> Int { - return calculate(SQLite_count(distinct: column))! - } - - /// Runs max() against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The largest value of the given column. - public func max(column: Expression) -> V? { - return calculate(SQLite_max(column)) - } - public func max(column: Expression) -> V? { - return calculate(SQLite_max(column)) - } - - /// Runs min() against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The smallest value of the given column. - public func min(column: Expression) -> V? { - return calculate(SQLite_min(column)) - } - public func min(column: Expression) -> V? { - return calculate(SQLite_min(column)) - } - - /// Runs avg() against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The average value of the given column. - public func average(column: Expression) -> Double? { - return calculate(SQLite_average(column)) - } - public func average(column: Expression) -> Double? { - return calculate(SQLite_average(column)) - } - - /// Runs avg() with DISTINCT against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The average value of the given column. - public func average(distinct column: Expression) -> Double? { - return calculate(SQLite_average(distinct: column)) - } - public func average(distinct column: Expression) -> Double? { - return calculate(SQLite_average(distinct: column)) - } - - /// Runs sum() against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The sum of the given column’s values. - public func sum(column: Expression) -> V? { - return calculate(SQLite_sum(column)) - } - public func sum(column: Expression) -> V? { - return calculate(SQLite_sum(column)) - } - - /// Runs sum() with DISTINCT against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The sum of the given column’s values. - public func sum(distinct column: Expression) -> V? { - return calculate(SQLite_sum(distinct: column)) - } - public func sum(distinct column: Expression) -> V? { - return calculate(SQLite_sum(distinct: column)) - } - - /// Runs total() against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The total of the given column’s values. - public func total(column: Expression) -> Double { - return calculate(SQLite_total(column))! - } - public func total(column: Expression) -> Double { - return calculate(SQLite_total(column))! - } - - /// Runs total() with DISTINCT against the query. - /// - /// :param: column The column used for the calculation. - /// - /// :returns: The total of the given column’s values. - public func total(distinct column: Expression) -> Double { - return calculate(SQLite_total(distinct: column))! - } - public func total(distinct column: Expression) -> Double { - return calculate(SQLite_total(distinct: column))! - } - - private func calculate(expression: Expression) -> V? { - return (select(expression).selectStatement.scalar() as? V.Datatype).map(V.fromDatatypeValue) as? V - } - private func calculate(expression: Expression) -> V? { - return (select(expression).selectStatement.scalar() as? V.Datatype).map(V.fromDatatypeValue) as? V - } - - // MARK: - Array - - /// Runs count(*) against the query and returns it. - public var count: Int { return calculate(SQLite_count(*))! } - - /// Returns true if the query has no rows. - public var isEmpty: Bool { return first == nil } - - /// The first row (or nil if the query returns no rows). - public var first: Row? { - var generator = limit(1).generate() - return generator.next() - } - - /// The last row (or nil if the query returns no rows). - public var last: Row? { - return reverse().first - } - - /// A query in reverse order. - public func reverse() -> Query { - return order(order.isEmpty ? [rowid.desc] : order.map { $0.expression.reverse() }) - } - -} - -/// A row object. Returned by iterating over a Query. Provides typed subscript -/// access to a row’s values. -public struct Row { - - private let columnNames: [String: Int] - private let values: [Binding?] - - private init(_ columnNames: [String: Int], _ values: [Binding?]) { - (self.columnNames, self.values) = (columnNames, values) - } - - /// Returns a row’s value for the given column. - /// - /// :param: 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) -> V? { - func valueAtIndex(idx: Int) -> V? { - if let value = values[idx] as? V.Datatype { return (V.fromDatatypeValue(value) as! V) } - return nil - } - - if let idx = columnNames[column.SQL] { return valueAtIndex(idx) } - - let similar = filter(columnNames.keys) { $0.hasSuffix(".\(column.SQL)") } - if similar.count == 1 { return valueAtIndex(columnNames[similar[0]]!) } - - if similar.count > 1 { - fatalError("ambiguous column \(quote(literal: column.SQL)) (please disambiguate: \(similar))") - } - fatalError("no such column \(quote(literal: column.SQL)) in columns: \(sorted(columnNames.keys))") - } - - // FIXME: rdar://18673897 // ... subscript(expression: Expression) -> Expression - - public subscript(column: Expression) -> Blob { return get(column) } - public subscript(column: Expression) -> Blob? { return get(column) } - - public subscript(column: Expression) -> Bool { return get(column) } - public subscript(column: Expression) -> Bool? { return get(column) } - - public subscript(column: Expression) -> Double { return get(column) } - public subscript(column: Expression) -> Double? { return get(column) } - - public subscript(column: Expression) -> Int { return get(column) } - public subscript(column: Expression) -> Int? { return get(column) } - - public subscript(column: Expression) -> Int64 { return get(column) } - public subscript(column: Expression) -> Int64? { return get(column) } - - public subscript(column: Expression) -> String { return get(column) } - public subscript(column: Expression) -> String? { return get(column) } - -} - -// MARK: - SequenceType -extension Query: SequenceType { - - public func generate() -> QueryGenerator { return QueryGenerator(self) } - -} - -// MARK: - GeneratorType -public struct QueryGenerator: GeneratorType { - - private let query: Query - private let statement: Statement - - private lazy var columnNames: [String: Int] = { - var (columnNames, idx) = ([String: Int](), 0) - column: for each in self.query.columns ?? [Expression<()>(literal: "*")] { - let pair = split(each.expression.SQL) { $0 == "." } - let (tableName, column) = (pair.count > 1 ? pair.first : nil, pair.last!) - - func expandGlob(namespace: Bool) -> Query -> () { - return { table in - var query = Query(table.database, table.tableName.unaliased) - if let columns = table.columns { query.columns = columns } - var names = query.selectStatement.columnNames.map { quote(identifier: $0) } - if namespace { names = names.map { "\(table.tableName.SQL).\($0)" } } - for name in names { columnNames[name] = idx++ } - } - } - - if column == "*" { - let tables = [self.query.select(*)] + self.query.joins.map { $0.table } - if let tableName = tableName { - for table in tables { - if table.tableName.SQL == tableName { - expandGlob(true)(table) - continue column - } - } - assertionFailure("no such table: \(tableName)") - } - tables.map(expandGlob(self.query.joins.count > 0)) - continue - } - - columnNames[each.expression.SQL] = idx++ - } - return columnNames - }() - - private init(_ query: Query) { - (self.query, self.statement) = (query, query.selectStatement) - } - - public mutating func next() -> Row? { - return statement.next().map { Row(self.columnNames, $0) } - } - -} - -// MARK: - Printable -extension Query: Printable { - - public var description: String { - return tableName.SQL - } - -} - -extension Database { - - public subscript(tableName: String) -> Query { - return Query(self, tableName) - } - -} diff --git a/SQLite/SQLite-Bridging.h b/SQLite/SQLite-Bridging.h deleted file mode 100644 index 862ebb10..00000000 --- a/SQLite/SQLite-Bridging.h +++ /dev/null @@ -1,53 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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; - -typedef struct SQLiteHandle SQLiteHandle; -typedef struct SQLiteContext SQLiteContext; -typedef struct SQLiteValue SQLiteValue; - -typedef int (^_SQLiteBusyHandlerCallback)(int times); -int _SQLiteBusyHandler(SQLiteHandle * handle, _SQLiteBusyHandlerCallback callback); - -typedef void (^_SQLiteTraceCallback)(const char * SQL); -void _SQLiteTrace(SQLiteHandle * handle, _SQLiteTraceCallback callback); - -typedef void (^_SQLiteUpdateHookCallback)(int operation, const char * db, const char * table, long long rowid); -void _SQLiteUpdateHook(SQLiteHandle * handle, _SQLiteUpdateHookCallback callback); - -typedef int (^_SQLiteCommitHookCallback)(); -void _SQLiteCommitHook(SQLiteHandle * handle, _SQLiteCommitHookCallback callback); - -typedef void (^_SQLiteRollbackHookCallback)(); -void _SQLiteRollbackHook(SQLiteHandle * handle, _SQLiteRollbackHookCallback callback); - -typedef void (^_SQLiteCreateFunctionCallback)(SQLiteContext * context, int argc, SQLiteValue ** argv); -int _SQLiteCreateFunction(SQLiteHandle * handle, const char * name, int argc, int deterministic, _SQLiteCreateFunctionCallback callback); - -typedef int (^_SQLiteCreateCollationCallback)(const char * lhs, const char * rhs); -int _SQLiteCreateCollation(SQLiteHandle * handle, const char * name, _SQLiteCreateCollationCallback callback); - -typedef NSString * (^_SQLiteTokenizerNextCallback)(const char * input, int * inputOffset, int * inputLength); -int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * module, const char * tokenizer, _SQLiteTokenizerNextCallback callback); diff --git a/SQLite/SQLite-Bridging.m b/SQLite/SQLite-Bridging.m deleted file mode 100644 index a473091a..00000000 --- a/SQLite/SQLite-Bridging.m +++ /dev/null @@ -1,219 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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; - -#import "fts3_tokenizer.h" - -static int __SQLiteBusyHandler(void * context, int tries) { - return ((__bridge _SQLiteBusyHandlerCallback)context)(tries); -} - -int _SQLiteBusyHandler(SQLiteHandle * handle, _SQLiteBusyHandlerCallback callback) { - if (callback) { - return sqlite3_busy_handler((sqlite3 *)handle, __SQLiteBusyHandler, (__bridge void *)callback); - } else { - return sqlite3_busy_handler((sqlite3 *)handle, 0, 0); - } -} - -static void __SQLiteTrace(void * context, const char * SQL) { - ((__bridge _SQLiteTraceCallback)context)(SQL); -} - -void _SQLiteTrace(SQLiteHandle * handle, _SQLiteTraceCallback callback) { - if (callback) { - sqlite3_trace((sqlite3 *)handle, __SQLiteTrace, (__bridge void *)callback); - } else { - sqlite3_trace((sqlite3 *)handle, 0, 0); - } -} - -static void __SQLiteUpdateHook(void * context, int operation, const char * db, const char * table, long long rowid) { - ((__bridge _SQLiteUpdateHookCallback)context)(operation, db, table, rowid); -} - -void _SQLiteUpdateHook(SQLiteHandle * handle, _SQLiteUpdateHookCallback callback) { - sqlite3_update_hook((sqlite3 *)handle, __SQLiteUpdateHook, (__bridge void *)callback); -} - -static int __SQLiteCommitHook(void * context) { - return ((__bridge _SQLiteCommitHookCallback)context)(); -} - -void _SQLiteCommitHook(SQLiteHandle * handle, _SQLiteCommitHookCallback callback) { - sqlite3_commit_hook((sqlite3 *)handle, __SQLiteCommitHook, (__bridge void *)callback); -} - -static void __SQLiteRollbackHook(void * context) { - ((__bridge _SQLiteRollbackHookCallback)context)(); -} - -void _SQLiteRollbackHook(SQLiteHandle * handle, _SQLiteRollbackHookCallback callback) { - sqlite3_rollback_hook((sqlite3 *)handle, __SQLiteRollbackHook, (__bridge void *)callback); -} - -static void __SQLiteCreateFunction(sqlite3_context * context, int argc, sqlite3_value ** argv) { - ((__bridge _SQLiteCreateFunctionCallback)sqlite3_user_data(context))((SQLiteContext *)context, argc, (SQLiteValue **)argv); -} - -int _SQLiteCreateFunction(SQLiteHandle * handle, const char * name, int argc, int deterministic, _SQLiteCreateFunctionCallback callback) { - if (callback) { - int flags = SQLITE_UTF8; - if (deterministic) { -#ifdef SQLITE_DETERMINISTIC - flags |= SQLITE_DETERMINISTIC; -#endif - } - return sqlite3_create_function_v2((sqlite3 *)handle, name, -1, flags, (__bridge void *)callback, &__SQLiteCreateFunction, 0, 0, 0); - } else { - return sqlite3_create_function_v2((sqlite3 *)handle, name, 0, 0, 0, 0, 0, 0, 0); - } -} - -static int __SQLiteCreateCollation(void * context, int len_lhs, const void * lhs, int len_rhs, const void * rhs) { - return ((__bridge _SQLiteCreateCollationCallback)context)(lhs, rhs); -} - -int _SQLiteCreateCollation(SQLiteHandle * handle, const char * name, _SQLiteCreateCollationCallback callback) { - if (callback) { - return sqlite3_create_collation_v2((sqlite3 *)handle, name, SQLITE_UTF8, (__bridge void *)callback, &__SQLiteCreateCollation, 0); - } else { - return sqlite3_create_collation_v2((sqlite3 *)handle, name, 0, 0, 0, 0); - } -} - -#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)); // FIXME: needed? - - 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/SQLite.h b/SQLite/SQLite.h deleted file mode 100644 index 418bb477..00000000 --- a/SQLite/SQLite.h +++ /dev/null @@ -1,9 +0,0 @@ -@import Foundation; - -//! Project version number for SQLite. -FOUNDATION_EXPORT double SQLite_VersionNumber; - -//! Project version string for SQLite. -FOUNDATION_EXPORT const unsigned char SQLite_VersionString[]; - -#import diff --git a/SQLite/SQLite.xcconfig b/SQLite/SQLite.xcconfig deleted file mode 100644 index 5e02dd58..00000000 --- a/SQLite/SQLite.xcconfig +++ /dev/null @@ -1,24 +0,0 @@ -SWIFT_WHOLE_MODULE_OPTIMIZATION = YES - -// Universal Framework - -SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx - -VALID_ARCHS[sdk=iphone*] = arm64 armv7 armv7s -VALID_ARCHS[sdk=macosx*] = i386 x86_64 - -LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks -LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks @loader_path/../Frameworks - -FRAMEWORK_SEARCH_PATHS[sdk=iphone*] = $(inherited) $(SDKROOT)/Developer/Library/Frameworks -FRAMEWORK_SEARCH_PATHS[sdk=macosx*] = $(inherited) $(DEVELOPER_FRAMEWORKS_DIR) - -// Platform-specific - -// iOS -CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer -TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 - -// OS X -FRAMEWORK_VERSION[sdk=macosx*] = A -COMBINE_HIDPI_IMAGES[sdk=macosx*] = YES \ No newline at end of file diff --git a/SQLite/Schema.swift b/SQLite/Schema.swift deleted file mode 100644 index 7e19e19d..00000000 --- a/SQLite/Schema.swift +++ /dev/null @@ -1,488 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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 Database { - - public func create( - #table: Query, - temporary: Bool = false, - ifNotExists: Bool = false, - @noescape _ block: SchemaBuilder -> () - ) -> Statement { - var builder = SchemaBuilder(table) - block(builder) - let create = createSQL("TABLE", temporary, false, ifNotExists, table.tableName.unaliased.SQL) - let columns = Expression<()>.join(", ", builder.columns).compile() - return run("\(create) (\(columns))") - } - - public func create(#table: Query, temporary: Bool = false, ifNotExists: Bool = false, from: Query) -> Statement { - let create = createSQL("TABLE", temporary, false, ifNotExists, table.tableName.unaliased.SQL) - let expression = from.selectExpression - return run("\(create) AS \(expression.SQL)", expression.bindings) - } - - public func create(#vtable: Query, ifNotExists: Bool = false, using: Expression<()>) -> Statement { - let create = createSQL("VIRTUAL TABLE", false, false, ifNotExists, vtable.tableName.unaliased.SQL) - return run("\(create) USING \(using.SQL)") - } - - public func rename(table tableName: String, to table: Query) -> Statement { - return run("ALTER TABLE \(quote(identifier: tableName)) RENAME TO \(table.tableName.unaliased.SQL)") - } - - public func alter( - #table: Query, - add column: Expression, - check: Expression? = nil, - defaultValue: V - ) -> Statement { - return alter(table, define(column, nil, false, false, check, Expression(value: defaultValue), nil)) - } - - public func alter( - #table: Query, - add column: Expression, - check: Expression? = nil, - defaultValue: V? = nil - ) -> Statement { - let value = defaultValue.map { Expression(value: $0) } - return alter(table, define(Expression(column), nil, true, false, check, value, nil)) - } - - public func alter( - #table: Query, - add column: Expression, - check: Expression? = nil, - references: Expression - ) -> Statement { - return alter(table, define(Expression(column), nil, true, false, check, nil, [ - Expression<()>(literal: "REFERENCES"), namespace(references) - ])) - } - - public func alter( - #table: Query, - add column: Expression, - check: Expression? = nil, - defaultValue: V, - collate: Collation - ) -> Statement { - return alter(table, define(Expression(column), nil, false, false, check, Expression(value: defaultValue), [ - Expression<()>(literal: "COLLATE"), Expression<()>(collate.description) - ])) - } - - public func alter( - #table: Query, - add column: Expression, - check: Expression? = nil, - defaultValue: V? = nil, - collate: Collation - ) -> Statement { - let value = defaultValue.map { Expression(value: $0) } - return alter(table, define(Expression(column), nil, true, false, check, value, [ - Expression<()>(literal: "COLLATE"), Expression<()>(collate.description) - ])) - } - - private func alter(table: Query, _ definition: Expressible) -> Statement { - return run("ALTER TABLE \(table.tableName.unaliased.SQL) ADD COLUMN \(definition.expression.compile())") - } - - public func drop(#table: Query, ifExists: Bool = false) -> Statement { - return run(dropSQL("TABLE", ifExists, table.tableName.unaliased.SQL)) - } - - public func create( - index table: Query, - unique: Bool = false, - ifNotExists: Bool = false, - on columns: Expressible... - ) -> Statement { - let tableName = table.tableName.unaliased.SQL - let create = createSQL("INDEX", false, unique, ifNotExists, indexName(tableName, on: columns)) - let joined = Expression<()>.join(", ", columns.map { $0.expression.ordered }) - return run("\(create) ON \(tableName) (\(joined.compile()))") - } - - public func drop(index table: Query, ifExists: Bool = false, on columns: Expressible...) -> Statement { - return run(dropSQL("INDEX", ifExists, indexName(table.tableName.unaliased.SQL, on: columns))) - } - - private func indexName(table: String, on columns: [Expressible]) -> String { - let string = join(" ", ["index", table, "on"] + columns.map { $0.expression.SQL }) - return quote(identifier: reduce(string, "") { underscored, character in - if character == "\"" { return underscored } - if "A"..."Z" ~= character || "a"..."z" ~= character { return underscored + String(character) } - return underscored + "_" - }) - } - - public func create(#view: Query, temporary: Bool = false, ifNotExists: Bool = false, from: Query) -> Statement { - let create = createSQL("VIEW", temporary, false, ifNotExists, view.tableName.unaliased.SQL) - let expression = from.selectExpression - return run("\(create) AS \(expression.SQL)", expression.bindings) - } - - public func drop(#view: Query, ifExists: Bool = false) -> Statement { - return run(dropSQL("VIEW", ifExists, view.tableName.unaliased.SQL)) - } - -} - -public final class SchemaBuilder { - - let table: Query - var columns = [Expressible]() - - private init(_ table: Query) { - self.table = table - } - - // MARK: - Column Constraints - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: Expression? - ) { - column(name, nil, false, unique, check, value.map { wrap("", $0) }) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: V? = nil - ) { - column(name, nil, false, unique, check, value.map { Expression(value: $0) }) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: Expression? - ) { - column(Expression(name), nil, true, unique, check, value.map { wrap("", $0) }) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: V? = nil - ) { - column(Expression(name), nil, true, unique, check, value.map { Expression(value: $0) }) - } - - public func column( - name: Expression, - primaryKey: Bool, - check: Expression? = nil, - defaultValue value: Expression? = nil - ) { - column(name, primaryKey ? .Default : nil, false, false, check, value.map { wrap("", $0) }, nil) - } - - // MARK: - INTEGER Columns - - // MARK: PRIMARY KEY - - public enum PrimaryKey { - - case Default - - case Autoincrement - - } - - public func column( - name: Expression, - primaryKey: PrimaryKey?, - check: Expression? = nil - ) { - column(name, primaryKey, false, false, check, nil, nil) - } - - // MARK: REFERENCES - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - references: Expression - ) { - assertForeignKeysEnabled() - let expressions: [Expressible] = [Expression<()>(literal: "REFERENCES"), namespace(references)] - column(name, nil, false, unique, check, nil, expressions) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - references: Query - ) { - return column( - name, - unique: unique, - check: check, - references: Expression(references.tableName) - ) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - references: Expression - ) { - assertForeignKeysEnabled() - let expressions: [Expressible] = [Expression<()>(literal: "REFERENCES"), namespace(references)] - column(Expression(name), nil, true, unique, check, nil, expressions) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - references: Query - ) { - return column( - name, - unique: unique, - check: check, - references: Expression(references.tableName) - ) - } - - // MARK: TEXT Columns (COLLATE) - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: Expression?, - collate: Collation - ) { - let expressions: [Expressible] = [Expression<()>(literal: "COLLATE \(collate)")] - column(name, nil, false, unique, check, value, expressions) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: V? = nil, - collate: Collation - ) { - column(name, unique: unique, check: check, defaultValue: value.map { Expression(value: $0) }, collate: collate) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue value: Expression?, - collate: Collation - ) { - column(Expression(name), unique: unique, check: check, defaultValue: value, collate: collate) - } - - public func column( - name: Expression, - unique: Bool = false, - check: Expression? = nil, - defaultValue: V? = nil, - collate: Collation - ) { - let value = defaultValue.map { Expression(value: $0) } - column(Expression(name), unique: unique, check: check, defaultValue: value, collate: collate) - } - - // MARK: - - - private func column( - name: Expression, - _ primaryKey: PrimaryKey?, - _ null: Bool, - _ unique: Bool, - _ check: Expression?, - _ defaultValue: Expression?, - _ expressions: [Expressible]? = nil - ) { - columns.append(define(name, primaryKey, null, unique, check, defaultValue, expressions)) - } - - // MARK: - Table Constraints - - public func primaryKey(column: Expression) { - primaryKey([column]) - } - - public func primaryKey(columnA: Expression, _ B: Expression) { - primaryKey([columnA, B]) - } - - public func primaryKey(columnA: Expression, _ B: Expression, _ C: Expression) { - primaryKey([columnA, B, C]) - } - - private func primaryKey(composite: [Expressible]) { - let primaryKey = Expression<()>.join(", ", composite) - columns.append(Expression<()>(literal: "PRIMARY KEY(\(primaryKey.SQL))", primaryKey.bindings)) - } - - public func unique(column: Expressible...) { - let unique = Expression<()>.join(", ", column) - columns.append(Expression<()>(literal: "UNIQUE(\(unique.SQL))", unique.bindings)) - } - - public func check(condition: Expression) { - columns.append(Expression<()>(literal: "CHECK (\(condition.SQL))", condition.bindings)) - } - - 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: Expression, - update: Dependency? = nil, - delete: Dependency? = nil - ) { - assertForeignKeysEnabled() - var parts: [Expressible] = [Expression<()>(literal: "FOREIGN KEY(\(column.SQL)) REFERENCES", column.bindings)] - parts.append(namespace(references)) - if let update = update { parts.append(Expression<()>(literal: "ON UPDATE \(update.rawValue)")) } - if let delete = delete { parts.append(Expression<()>(literal: "ON DELETE \(delete.rawValue)")) } - columns.append(Expression<()>.join(" ", parts)) - } - - public func foreignKey( - column: Expression, - references: Expression, - update: Dependency? = nil, - delete: Dependency? = nil - ) { - assertForeignKeysEnabled() - foreignKey(Expression(column), references: references, update: update, delete: delete) - } - - public func foreignKey( - columns: (Expression, Expression), - references: (Expression, Expression), - update: Dependency? = nil, - delete: Dependency? = nil - ) { - let compositeA = Expression(Expression<()>.join(", ", [columns.0, columns.1])) - let compositeB = Expression(Expression<()>.join(", ", [references.0, references.1])) - foreignKey(compositeA, references: compositeB, update: update, delete: delete) - } - - public func foreignKey( - columns: (Expression, Expression, Expression), - references: (Expression, Expression, Expression), - update: Dependency? = nil, - delete: Dependency? = nil - ) { - let compositeA = Expression(Expression<()>.join(", ", [columns.0, columns.1, columns.2])) - let compositeB = Expression(Expression<()>.join(", ", [references.0, references.1, references.2])) - foreignKey(compositeA, references: compositeB, update: update, delete: delete) - } - - private func assertForeignKeysEnabled() { - assert(table.database.foreignKeys, "foreign key constraints are disabled (run `db.foreignKeys = true`)") - } - -} - -private func namespace(column: Expressible) -> Expressible { - let expression = column.expression - if !contains(expression.SQL, ".") { return expression } - let reference = reduce(expression.SQL, "") { SQL, character in - let string = String(character) - return SQL + (string == "." ? "(" : string) - } - return Expression<()>(literal: "\(reference))", expression.bindings) -} - -private func define( - column: Expression, - primaryKey: SchemaBuilder.PrimaryKey?, - null: Bool, - unique: Bool, - check: Expression?, - defaultValue: Expression?, - expressions: [Expressible]? -) -> Expressible { - var parts: [Expressible] = [Expression<()>(column), Expression<()>(literal: V.declaredDatatype)] - if let primaryKey = primaryKey { - parts.append(Expression<()>(literal: "PRIMARY KEY")) - if primaryKey == .Autoincrement { parts.append(Expression<()>(literal: "AUTOINCREMENT")) } - } - if !null { parts.append(Expression<()>(literal: "NOT NULL")) } - if unique { parts.append(Expression<()>(literal: "UNIQUE")) } - if let check = check { parts.append(Expression<()>(literal: "CHECK (\(check.SQL))", check.bindings)) } - if let value = defaultValue { parts.append(Expression<()>(literal: "DEFAULT \(value.SQL)", value.bindings)) } - if let expressions = expressions { parts += expressions } - return Expression<()>.join(" ", parts) -} - -private func createSQL( - type: String, - temporary: Bool, - unique: Bool, - ifNotExists: Bool, - name: String -) -> String { - var parts: [String] = ["CREATE"] - if temporary { parts.append("TEMPORARY") } - if unique { parts.append("UNIQUE") } - parts.append(type) - if ifNotExists { parts.append("IF NOT EXISTS") } - parts.append(name) - return Swift.join(" ", parts) -} - -private func dropSQL(type: String, ifExists: Bool, name: String) -> String { - var parts: [String] = ["DROP \(type)"] - if ifExists { parts.append("IF EXISTS") } - parts.append(name) - return Swift.join(" ", parts) -} diff --git a/SQLite/Statement.swift b/SQLite/Statement.swift deleted file mode 100644 index 3d33fe4e..00000000 --- a/SQLite/Statement.swift +++ /dev/null @@ -1,310 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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 sqlite3 - -internal let SQLITE_STATIC = sqlite3_destructor_type(COpaquePointer(bitPattern: 0)) -internal let SQLITE_TRANSIENT = sqlite3_destructor_type(COpaquePointer(bitPattern: -1)) - -/// A single SQL statement. -public final class Statement { - - private var handle: COpaquePointer = nil - - private let database: Database - - public lazy var row: Cursor = { Cursor(self) }() - - internal init(_ database: Database, _ SQL: String) { - self.database = database - database.try { sqlite3_prepare_v2(database.handle, SQL, -1, &self.handle, nil) } - } - - deinit { sqlite3_finalize(handle) } - - public lazy var columnCount: Int = { Int(sqlite3_column_count(self.handle)) }() - - public lazy var columnNames: [String] = { - (0.. Statement { - return bind(values) - } - - /// Binds a list of parameters to a statement. - /// - /// :param: values A list of parameters to bind to the statement. - /// - /// :returns: The statement object (useful for chaining). - public func bind(values: [Binding?]) -> Statement { - if values.isEmpty { return self } - reset() - assert(values.count == Int(sqlite3_bind_parameter_count(handle)), "\(sqlite3_bind_parameter_count(handle)) values expected, \(values.count) passed") - for idx in 1...values.count { bind(values[idx - 1], atIndex: idx) } - return self - } - - /// Binds a dictionary of named parameters to a statement. - /// - /// :param: values A dictionary of named parameters to bind to the - /// statement. - /// - /// :returns: The statement object (useful for chaining). - public func bind(values: [String: Binding?]) -> Statement { - reset() - for (name, value) in values { - let idx = sqlite3_bind_parameter_index(handle, name) - assert(idx > 0, "parameter not found: \(name)") - bind(value, atIndex: Int(idx)) - } - return self - } - - private func bind(value: Binding?, atIndex idx: Int) { - if value == nil { - try { sqlite3_bind_null(self.handle, Int32(idx)) } - } else if let value = value as? Blob { - try { sqlite3_bind_blob(self.handle, Int32(idx), value.bytes, Int32(value.length), SQLITE_TRANSIENT) } - } else if let value = value as? Double { - try { sqlite3_bind_double(self.handle, Int32(idx), value) } - } else if let value = value as? Int64 { - try { sqlite3_bind_int64(self.handle, Int32(idx), value) } - } else if let value = value as? String { - try { sqlite3_bind_text(self.handle, Int32(idx), value, -1, SQLITE_TRANSIENT) } - } else if let value = value as? Bool { - bind(value.datatypeValue, atIndex: idx) - } else if let value = value as? Int { - bind(value.datatypeValue, atIndex: idx) - } else if let value = value { - fatalError("tried to bind unexpected value \(value)") - } - } - - // MARK: - Run - - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: The statement object (useful for chaining). - public func run(bindings: Binding?...) -> Statement { - if !bindings.isEmpty { return run(bindings) } - reset(clearBindings: false) - while step() {} - return self - } - - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: The statement object (useful for chaining). - public func run(bindings: [Binding?]) -> Statement { - return bind(bindings).run() - } - - /// :param: bindings A dictionary of named parameters to bind to the - /// statement. - /// - /// :returns: The statement object (useful for chaining). - public func run(bindings: [String: Binding?]) -> Statement { - return bind(bindings).run() - } - - // MARK: - Scalar - - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: The first value of the first row returned. - public func scalar(bindings: Binding?...) -> Binding? { - if !bindings.isEmpty { return scalar(bindings) } - reset(clearBindings: false) - step() - return row[0] - } - - /// :param: bindings A list of parameters to bind to the statement. - /// - /// :returns: The first value of the first row returned. - public func scalar(bindings: [Binding?]) -> Binding? { - return bind(bindings).scalar() - } - - /// :param: bindings A dictionary of named parameters to bind to the - /// statement. - /// - /// :returns: The first value of the first row returned. - public func scalar(bindings: [String: Binding?]) -> Binding? { - return bind(bindings).scalar() - } - - // MARK: - - - public func step() -> Bool { - try { sqlite3_step(self.handle) } - return status == SQLITE_ROW - } - - private func reset(clearBindings: Bool = true) { - (status, reason) = (SQLITE_OK, nil) - sqlite3_reset(handle) - if (clearBindings) { sqlite3_clear_bindings(handle) } - } - - // MARK: - Error Handling - - /// :returns: Whether or not a statement has produced an error. - public var failed: Bool { - return !(status == SQLITE_OK || status == SQLITE_ROW || status == SQLITE_DONE) - } - - /// :returns: The reason for an error. - public var reason: String? - - private var status: Int32 = SQLITE_OK - - private func try(block: () -> Int32) { - if failed { return } - database.perform { - self.status = block() - if self.failed { - self.reason = String.fromCString(sqlite3_errmsg(self.database.handle)) - assert(self.status == SQLITE_CONSTRAINT || self.status == SQLITE_INTERRUPT, "\(self.reason!)") - } - } - } - -} - -// MARK: - SequenceType -extension Statement: SequenceType { - - public func generate() -> Statement { - reset(clearBindings: false) - return self - } - -} - -// MARK: - GeneratorType -extension Statement: GeneratorType { - - /// :returns: The next row from the result set (or nil). - public func next() -> [Binding?]? { - return step() ? Array(row) : nil - } - -} - -// MARK: - Printable -extension Statement: Printable { - - public var description: String { - return String.fromCString(sqlite3_sql(handle))! - } - -} - -public func && (lhs: Statement, @autoclosure rhs: () -> Statement) -> Statement { - if lhs.status == SQLITE_OK { lhs.run() } - return lhs.failed ? lhs : rhs() -} - -public func || (lhs: Statement, @autoclosure rhs: () -> Statement) -> Statement { - if lhs.status == SQLITE_OK { lhs.run() } - return lhs.failed ? rhs() : lhs -} - -/// Cursors provide direct access to a statement's current row. -public struct Cursor { - - private let handle: COpaquePointer - - private let columnCount: Int - - private init(_ statement: Statement) { - handle = statement.handle - columnCount = statement.columnCount - } - - public subscript(idx: Int) -> Blob { - let bytes = sqlite3_column_blob(handle, Int32(idx)) - let length = sqlite3_column_bytes(handle, Int32(idx)) - return Blob(bytes: bytes, length: Int(length)) - } - - public subscript(idx: Int) -> Double { - return sqlite3_column_double(handle, Int32(idx)) - } - - public subscript(idx: Int) -> Int64 { - return sqlite3_column_int64(handle, Int32(idx)) - } - - public subscript(idx: Int) -> String { - return String.fromCString(UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) ?? "" - } - - public subscript(idx: Int) -> Bool { - return Bool.fromDatatypeValue(self[idx]) - } - - public subscript(idx: Int) -> Int { - return Int.fromDatatypeValue(self[idx]) - } - -} - -// MARK: - SequenceType -extension Cursor: SequenceType { - - public subscript(idx: Int) -> Binding? { - switch sqlite3_column_type(handle, Int32(idx)) { - case SQLITE_BLOB: - return self[idx] as Blob - case SQLITE_FLOAT: - return self[idx] as Double - case SQLITE_INTEGER: - return self[idx] as Int64 - case SQLITE_NULL: - return nil - case SQLITE_TEXT: - return self[idx] as String - case let type: - fatalError("unsupported column type: \(type)") - } - } - - public func generate() -> GeneratorOf { - var idx = 0 - return GeneratorOf { - idx >= self.columnCount ? Optional.None : self[idx++] - } - } - -} diff --git a/SQLite/Value.swift b/SQLite/Value.swift deleted file mode 100644 index af4383a8..00000000 --- a/SQLite/Value.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright (c) 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 - -/// 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. See the Value protocol, -/// instead. -public protocol Binding {} - -public protocol Number: Binding {} - -public protocol Value { - - typealias ValueType = Self - - typealias Datatype: Binding - - static var declaredDatatype: String { get } - - static func fromDatatypeValue(datatypeValue: Datatype) -> ValueType - - var datatypeValue: Datatype { get } - -} - -public struct Blob { - - private let data: NSData - - public var bytes: UnsafePointer<()> { - return data.bytes - } - - public var length: Int { - return data.length - } - - public init(bytes: UnsafePointer<()>, length: Int) { - data = NSData(bytes: bytes, length: length) - } - -} - -extension Blob: Equatable {} - -public func ==(lhs: Blob, rhs: Blob) -> Bool { - return lhs.data == rhs.data -} - -extension Blob: Binding, Value { - - public static var declaredDatatype = "BLOB" - - public static func fromDatatypeValue(datatypeValue: Blob) -> Blob { - return datatypeValue - } - - public var datatypeValue: Blob { - return self - } - -} - -extension Blob: Printable { - - public var description: String { - let buf = UnsafeBufferPointer(start: UnsafePointer(bytes), count: length) - let hex = join("", map(buf) { String(format: "%02x", $0) }) - return "x'\(hex)'" - } - -} - -extension Double: Number, Value { - - public static var declaredDatatype = "REAL" - - public static func fromDatatypeValue(datatypeValue: Double) -> Double { - return datatypeValue - } - - public var datatypeValue: Double { - return self - } - -} - -extension Int64: Number, Value { - - public static var declaredDatatype = "INTEGER" - - public static func fromDatatypeValue(datatypeValue: Int64) -> Int64 { - return datatypeValue - } - - public var datatypeValue: Int64 { - return self - } - -} - -extension String: Binding, Value { - - public static var declaredDatatype = "TEXT" - - public static func fromDatatypeValue(datatypeValue: String) -> String { - return datatypeValue - } - - public var datatypeValue: String { - return self - } - -} - -// MARK: - - -extension Bool: Binding, Value { - - public static var declaredDatatype = Int64.declaredDatatype - - public static func fromDatatypeValue(datatypeValue: Int64) -> Bool { - return datatypeValue != 0 - } - - public var datatypeValue: Int64 { - return self ? 1 : 0 - } - -} - -extension Int: Binding, Value { - - public static var declaredDatatype = Int64.declaredDatatype - - public static func fromDatatypeValue(datatypeValue: Int64) -> Int { - return Int(datatypeValue) - } - - public var datatypeValue: Int64 { - return Int64(self) - } - -} diff --git a/SQLiteCipher Tests/CipherTests.swift b/SQLiteCipher Tests/CipherTests.swift deleted file mode 100644 index d96d20af..00000000 --- a/SQLiteCipher Tests/CipherTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import XCTest -import SQLite - -class CipherTests: SQLiteTestCase { - - override func setUp() { - db.key("hello") - createUsersTable() - insertUser("alice") - - super.setUp() - } - - func test_key() { - XCTAssertEqual(1, users.count) - } - - func test_rekey() { - db.rekey("world") - XCTAssertEqual(1, users.count) - } - -} \ No newline at end of file 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/RTree.swift b/Sources/SQLite/Core/Blob.swift similarity index 60% rename from SQLite/RTree.swift rename to Sources/SQLite/Core/Blob.swift index 13866f60..a709fb42 100644 --- a/SQLite/RTree.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -1,7 +1,7 @@ // // SQLite.swift // https://github.com/stephencelis/SQLite.swift -// Copyright (c) 2014-2015 Stephen Celis. +// 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 @@ -22,8 +22,35 @@ // THE SOFTWARE. // -public func rtree(primaryKey: Expression, columns: Expression...) -> Expression<()> { - var definitions: [Expressible] = [primaryKey] - definitions.extend(columns.map { $0 }) - return wrap(__FUNCTION__, Expression<()>.join(", ", definitions)) +public struct Blob { + + public let bytes: [UInt8] + + public init(bytes: [UInt8]) { + self.bytes = bytes + } + + 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 { + bytes.map { + ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) + }.joined(separator: "") + } +} + +extension Blob: CustomStringConvertible { + public var description: String { + "x'\(toHex())'" + } +} + +extension Blob: Equatable { +} + +public func ==(lhs: Blob, rhs: Blob) -> Bool { + 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/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift new file mode 100644 index 00000000..82d535b9 --- /dev/null +++ b/Sources/SQLite/Core/Statement.swift @@ -0,0 +1,339 @@ +// +// 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. +// + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +/// A single SQL statement. +public final class Statement { + + fileprivate var handle: OpaquePointer? + + fileprivate let connection: Connection + + init(_ connection: Connection, _ SQL: String) throws { + self.connection = connection + try connection.check(sqlite3_prepare_v2(connection.handle, SQL, -1, &handle, nil)) + } + + deinit { + sqlite3_finalize(handle) + } + + public lazy var columnCount: Int = Int(sqlite3_column_count(handle)) + + public lazy var columnNames: [String] = (0.. Statement { + bind(values) + } + + /// Binds a list of parameters to a 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 { + if values.isEmpty { return self } + reset() + guard values.count == Int(sqlite3_bind_parameter_count(handle)) else { + fatalError("\(sqlite3_bind_parameter_count(handle)) values expected, \(values.count) passed") + } + for idx in 1...values.count { bind(values[idx - 1], atIndex: idx) } + return self + } + + /// Binds a dictionary of named parameters to a statement. + /// + /// - Parameter values: A dictionary of named parameters to bind to the + /// statement. + /// + /// - Returns: The statement object (useful for chaining). + public func bind(_ values: [String: Binding?]) -> Statement { + reset() + for (name, value) in values { + let idx = sqlite3_bind_parameter_index(handle, name) + guard idx > 0 else { + fatalError("parameter not found: \(name)") + } + bind(value, atIndex: Int(idx)) + } + return self + } + + fileprivate func bind(_ value: Binding?, atIndex idx: Int) { + switch value { + case .none: + sqlite3_bind_null(handle, Int32(idx)) + 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) + case let value as Double: + sqlite3_bind_double(handle, Int32(idx), value) + case let value as Int64: + sqlite3_bind_int64(handle, Int32(idx), value) + case let value as String: + sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) + case let value as Int: + self.bind(value.datatypeValue, atIndex: idx) + case let value as Bool: + self.bind(value.datatypeValue, atIndex: idx) + case .some(let value): + fatalError("tried to bind unexpected value \(value)") + } + } + + /// - Parameter bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement object (useful for chaining). + @discardableResult public func run(_ bindings: Binding?...) throws -> Statement { + guard bindings.isEmpty else { + return try run(bindings) + } + + reset(clearBindings: false) + repeat {} while try step() + return self + } + + /// - Parameter bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement object (useful for chaining). + @discardableResult public func run(_ bindings: [Binding?]) throws -> Statement { + try bind(bindings).run() + } + + /// - Parameter bindings: A dictionary of named parameters to bind to the + /// statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement object (useful for chaining). + @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. + public func scalar(_ bindings: Binding?...) throws -> Binding? { + guard bindings.isEmpty else { + return try scalar(bindings) + } + + reset(clearBindings: false) + _ = 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. + 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. + public func scalar(_ bindings: [String: Binding?]) throws -> Binding? { + try bind(bindings).scalar() + } + + public func step() throws -> Bool { + try connection.sync { try connection.check(sqlite3_step(handle)) == SQLITE_ROW } + } + + public func reset() { + reset(clearBindings: true) + } + + fileprivate func reset(clearBindings shouldClear: Bool) { + sqlite3_reset(handle) + if shouldClear { sqlite3_clear_bindings(handle) } + } + +} + +extension Statement: Sequence { + + public func makeIterator() -> Statement { + reset(clearBindings: false) + return self + } + +} + +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 + } +} + +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 { + + public var description: String { + String(cString: sqlite3_sql(handle)) + } + +} + +public struct Cursor { + + fileprivate let handle: OpaquePointer + + fileprivate let columnCount: Int + + fileprivate init(_ statement: Statement) { + handle = statement.handle! + columnCount = statement.columnCount + } + + public subscript(idx: Int) -> Double { + sqlite3_column_double(handle, Int32(idx)) + } + + public subscript(idx: Int) -> Int64 { + sqlite3_column_int64(handle, Int32(idx)) + } + + public subscript(idx: Int) -> String { + String(cString: UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) + } + + public subscript(idx: Int) -> Blob { + 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 { + Bool.fromDatatypeValue(self[idx]) + } + + public subscript(idx: Int) -> Int { + Int.fromDatatypeValue(self[idx]) + } + +} + +/// Cursors provide direct access to a statement’s current row. +extension Cursor: Sequence { + + public subscript(idx: Int) -> Binding? { + switch sqlite3_column_type(handle, Int32(idx)) { + case SQLITE_BLOB: + return self[idx] as Blob + case SQLITE_FLOAT: + return self[idx] as Double + case SQLITE_INTEGER: + return self[idx] as Int64 + case SQLITE_NULL: + return nil + case SQLITE_TEXT: + return self[idx] as String + case let type: + fatalError("unsupported column type: \(type)") + } + } + + public func makeIterator() -> AnyIterator { + var idx = 0 + 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/Sources/SQLite/Core/Value.swift b/Sources/SQLite/Core/Value.swift new file mode 100644 index 00000000..249a7728 --- /dev/null +++ b/Sources/SQLite/Core/Value.swift @@ -0,0 +1,132 @@ +// +// 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. +// + +/// - Warning: `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. See the `Value` +/// protocol, instead. +public protocol Binding {} + +public protocol Number: Binding {} + +public protocol Value: Expressible { // extensions cannot have inheritance clauses + + associatedtype ValueType = Self + + associatedtype Datatype: Binding + + static var declaredDatatype: String { get } + + static func fromDatatypeValue(_ datatypeValue: Datatype) throws -> ValueType + + var datatypeValue: Datatype { get } + +} + +extension Double: Number, Value { + + public static let declaredDatatype = "REAL" + + public static func fromDatatypeValue(_ datatypeValue: Double) -> Double { + datatypeValue + } + + public var datatypeValue: Double { + self + } + +} + +extension Int64: Number, Value { + + public static let declaredDatatype = "INTEGER" + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int64 { + datatypeValue + } + + public var datatypeValue: Int64 { + self + } + +} + +extension String: Binding, Value { + + public static let declaredDatatype = "TEXT" + + public static func fromDatatypeValue(_ datatypeValue: String) -> String { + datatypeValue + } + + public var datatypeValue: String { + self + } + +} + +extension Blob: Binding, Value { + + public static let declaredDatatype = "BLOB" + + public static func fromDatatypeValue(_ datatypeValue: Blob) -> Blob { + datatypeValue + } + + public var datatypeValue: Blob { + self + } + +} + +// MARK: - + +extension Bool: Binding, Value { + + public static var declaredDatatype = Int64.declaredDatatype + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Bool { + datatypeValue != 0 + } + + public var datatypeValue: Int64 { + self ? 1 : 0 + } + +} + +extension Int: Number, Value { + + public static var declaredDatatype = Int64.declaredDatatype + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int { + Int(datatypeValue) + } + + public var datatypeValue: Int64 { + 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/Sources/SQLite/Extensions/FTS4.swift b/Sources/SQLite/Extensions/FTS4.swift new file mode 100644 index 00000000..c02cfdc1 --- /dev/null +++ b/Sources/SQLite/Extensions/FTS4.swift @@ -0,0 +1,326 @@ +// +// 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 Module { + + public static func FTS4(_ column: Expressible, _ more: Expressible...) -> Module { + FTS4([column] + more) + } + + public static func FTS4(_ columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module { + FTS4(FTS4Config().columns(columns).tokenizer(tokenizer)) + } + + public static func FTS4(_ config: FTS4Config) -> Module { + Module(name: "fts4", arguments: config.arguments()) + } +} + +extension VirtualTable { + + /// Builds an expression appended with a `MATCH` query against the given + /// pattern. + /// + /// let emails = VirtualTable("emails") + /// + /// emails.filter(emails.match("Hello")) + /// // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: An expression appended with a `MATCH` query against the given + /// pattern. + public func match(_ pattern: String) -> Expression { + "MATCH".infix(tableName(), pattern) + } + + public func match(_ pattern: Expression) -> Expression { + "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. + /// + /// let emails = VirtualTable("emails") + /// + /// emails.match("Hello") + /// // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A query with the given `WHERE … MATCH` clause applied. + public func match(_ pattern: String) -> QueryType { + filter(match(pattern)) + } + + public func match(_ pattern: Expression) -> QueryType { + 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") + + public static func Unicode61(removeDiacritics: Bool? = nil, + tokenchars: Set = [], + separators: Set = []) -> Tokenizer { + var arguments = [String]() + + if let removeDiacritics { + arguments.append("remove_diacritics=\(removeDiacritics ? 1 : 0)".quote()) + } + + if !tokenchars.isEmpty { + let joined = tokenchars.map { String($0) }.joined(separator: "") + arguments.append("tokenchars=\(joined)".quote()) + } + + if !separators.isEmpty { + let joined = separators.map { String($0) }.joined(separator: "") + arguments.append("separators=\(joined)".quote()) + } + + return Tokenizer("unicode61", arguments) + } + + // 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] + + fileprivate init(_ name: String, _ arguments: [String] = []) { + self.name = name + self.arguments = arguments + } + + fileprivate static let moduleName = "SQLite.swift" + +} + +extension Tokenizer: CustomStringConvertible { + + public var description: String { + ([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. +open class FTSConfig { + public enum ColumnOption { + /// [The notindexed= option](https://www.sqlite.org/fts3.html#section_6_5) + case unindexed + } + + typealias ColumnDefinition = (Expressible, options: [ColumnOption]) + var columnDefinitions = [ColumnDefinition]() + var tokenizer: Tokenizer? + var prefixes = [Int]() + var externalContentSchema: SchemaType? + var isContentless: Bool = false + + /// Adds a column definition + @discardableResult open func column(_ column: Expressible, _ options: [ColumnOption] = []) -> Self { + columnDefinitions.append((column, options)) + return self + } + + @discardableResult open func columns(_ columns: [Expressible]) -> Self { + for column in columns { + self.column(column) + } + return self + } + + /// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer) + @discardableResult open func tokenizer(_ tokenizer: Tokenizer?) -> Self { + self.tokenizer = tokenizer + return self + } + + /// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6) + @discardableResult open func prefix(_ prefix: [Int]) -> Self { + prefixes += prefix + return self + } + + /// [The content= option](https://www.sqlite.org/fts3.html#section_6_2) + @discardableResult open func externalContent(_ schema: SchemaType) -> Self { + externalContentSchema = schema + return self + } + + /// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1) + @discardableResult open func contentless() -> Self { + isContentless = true + return self + } + + func formatColumnDefinitions() -> [Expressible] { + columnDefinitions.map { $0.0 } + } + + func arguments() -> [Expressible] { + options().arguments + } + + func options() -> Options { + var options = Options() + options.append(formatColumnDefinitions()) + if let tokenizer { + options.append("tokenize", value: Expression(literal: tokenizer.description)) + } + options.appendCommaSeparated("prefix", values: prefixes.sorted().map { String($0) }) + if isContentless { + options.append("content", value: "") + } else if let externalContentSchema { + options.append("content", value: externalContentSchema.tableName()) + } + return options + } + + struct Options { + var arguments = [Expressible]() + + @discardableResult mutating func append(_ columns: [Expressible]) -> Options { + arguments.append(contentsOf: columns) + return self + } + + @discardableResult mutating func appendCommaSeparated(_ key: String, values: [String]) -> Options { + if values.isEmpty { + return self + } else { + return append(key, value: values.joined(separator: ",")) + } + } + + @discardableResult mutating func append(_ key: String, value: String) -> Options { + append(key, value: Expression(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. +open class FTS4Config: FTSConfig { + /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) + public enum MatchInfo: String { + case fts3 + } + + /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) + public enum Order: String { + /// Data structures are optimized for returning results in ascending order by docid (default) + case asc + /// FTS4 stores its data in such a way as to optimize returning results in descending order by docid. + case desc + } + + var compressFunction: String? + var uncompressFunction: String? + var languageId: String? + var matchInfo: MatchInfo? + var order: Order? + + override public init() { + } + + /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) + @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) + @discardableResult open func uncompress(_ functionName: String) -> Self { + uncompressFunction = functionName + return self + } + + /// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3) + @discardableResult open func languageId(_ columnName: String) -> Self { + languageId = columnName + return self + } + + /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) + @discardableResult open func matchInfo(_ matchInfo: MatchInfo) -> Self { + self.matchInfo = matchInfo + return self + } + + /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) + @discardableResult open func order(_ order: Order) -> Self { + self.order = order + return self + } + + override func options() -> Options { + var options = super.options() + for (column, _) in (columnDefinitions.filter { $0.options.contains(.unindexed) }) { + options.append("notindexed", value: column) + } + 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/Sources/SQLite/Extensions/FTS5.swift b/Sources/SQLite/Extensions/FTS5.swift new file mode 100644 index 00000000..3e84e171 --- /dev/null +++ b/Sources/SQLite/Extensions/FTS5.swift @@ -0,0 +1,93 @@ +// +// 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 Module { + public static func FTS5(_ config: FTS5Config) -> Module { + Module(name: "fts5", arguments: config.arguments()) + } +} + +/// Configuration for the [FTS5](https://www.sqlite.org/fts5.html) extension. +/// +/// **Note:** this is currently only applicable when using SQLite.swift together with a FTS5-enabled version +/// of SQLite. +open class FTS5Config: FTSConfig { + public enum Detail: String { + /// store rowid, column number, term offset + case full + /// store rowid, column number + case column + /// store rowid + case none + } + + var detail: Detail? + var contentRowId: Expressible? + var columnSize: Int? + + override public init() { + } + + /// [External Content Tables](https://www.sqlite.org/fts5.html#section_4_4_2) + @discardableResult open func contentRowId(_ column: Expressible) -> Self { + contentRowId = column + return self + } + + /// [The Columnsize Option](https://www.sqlite.org/fts5.html#section_4_5) + @discardableResult open func columnSize(_ size: Int) -> Self { + columnSize = size + return self + } + + /// [The Detail Option](https://www.sqlite.org/fts5.html#section_4_6) + @discardableResult open func detail(_ detail: Detail) -> Self { + self.detail = detail + return self + } + + override func options() -> Options { + var options = super.options() + if let contentRowId { + options.append("content_rowid", value: contentRowId) + } + if let columnSize { + options.append("columnsize", value: Expression(value: columnSize)) + } + if let detail { + options.append("detail", value: detail.rawValue) + } + return options + } + + override func formatColumnDefinitions() -> [Expressible] { + columnDefinitions.map { definition in + if definition.options.contains(.unindexed) { + return " ".join([definition.0, Expression(literal: "UNINDEXED")]) + } else { + return definition.0 + } + } + } +} diff --git a/SQLiteCipher/Cipher.swift b/Sources/SQLite/Extensions/RTree.swift similarity index 68% rename from SQLiteCipher/Cipher.swift rename to Sources/SQLite/Extensions/RTree.swift index 3fd2f60c..5ecdf78b 100644 --- a/SQLiteCipher/Cipher.swift +++ b/Sources/SQLite/Extensions/RTree.swift @@ -1,7 +1,7 @@ // // SQLite.swift // https://github.com/stephencelis/SQLite.swift -// Copyright (c) 2014-2015 Stephen Celis. +// 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 @@ -22,14 +22,16 @@ // THE SOFTWARE. // -extension Database { +extension Module { + public static func RTree(_ primaryKey: Expression, + _ pairs: (Expression, Expression)...) + -> Module where T.Datatype == Int64, U.Datatype == Double { + var arguments: [Expressible] = [primaryKey] - public func key(key: String) { - try { sqlite3_key(self.handle, key, Int32(count(key.utf8))) } - } + for pair in pairs { + arguments.append(contentsOf: [pair.0, pair.1] as [Expressible]) + } - public func rekey(key: String) { - try { sqlite3_rekey(self.handle, key, Int32(count(key.utf8))) } + 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/Sources/SQLite/Helpers.swift b/Sources/SQLite/Helpers.swift new file mode 100644 index 00000000..e3c84589 --- /dev/null +++ b/Sources/SQLite/Helpers.swift @@ -0,0 +1,131 @@ +// +// 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. +// + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +public typealias Star = (Expression?, Expression?) -> Expression + +public func *(_: Expression?, _: Expression?) -> Expression { + Expression(literal: "*") +} + +// swiftlint:disable:next type_name +public protocol _OptionalType { + + associatedtype WrappedType + +} + +extension Optional: _OptionalType { + + public typealias WrappedType = Wrapped + +} + +// let SQLITE_STATIC = unsafeBitCast(0, sqlite3_destructor_type.self) +let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +extension String { + func quote(_ mark: Character = "\"") -> String { + var quoted = "" + quoted.append(mark) + for character in self { + quoted.append(character) + if character == mark { + quoted.append(character) + } + } + quoted.append(mark) + return quoted + } + + func join(_ expressions: [Expressible]) -> Expressible { + var (template, bindings) = ([String](), [Binding?]()) + for expressible in expressions { + let expression = expressible.expression + template.append(expression.template) + bindings.append(contentsOf: expression.bindings) + } + return Expression(template.joined(separator: self), bindings) + } + + func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { + infix([lhs, rhs], wrap: wrap) + } + + func infix(_ terms: [Expressible], wrap: Bool = true) -> Expression { + let expression = Expression(" \(self) ".join(terms).expression) + guard wrap else { + return expression + } + return "".wrap(expression) + } + + func prefix(_ expressions: Expressible) -> Expressible { + "\(self) ".wrap(expressions) as Expression + } + + func prefix(_ expressions: [Expressible]) -> Expressible { + "\(self) ".wrap(expressions) as Expression + } + + func wrap(_ expression: Expressible) -> Expression { + Expression("\(self)(\(expression.expression.template))", expression.expression.bindings) + } + + func wrap(_ expressions: [Expressible]) -> Expression { + wrap(", ".join(expressions)) + } + +} + +func transcode(_ literal: Binding?) -> String { + guard let literal else { return "NULL" } + + switch literal { + case let blob as Blob: + return blob.description + case let string as String: + return string.quote("'") + case let binding: + return "\(binding)" + } +} + +// swiftlint:disable force_cast force_try +func value(_ binding: Binding) -> A { + try! A.fromDatatypeValue(binding as! A.Datatype) as! A +} + +func value(_ binding: Binding?) -> A { + value(binding!) +} diff --git a/sqlite3/Info.plist b/Sources/SQLite/Info.plist similarity index 89% rename from sqlite3/Info.plist rename to Sources/SQLite/Info.plist index d9b780a0..ca23c84f 100644 --- a/sqlite3/Info.plist +++ b/Sources/SQLite/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.stephencelis.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + $(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/Sources/SQLite/SQLite.h b/Sources/SQLite/SQLite.h new file mode 100644 index 00000000..21ed899f --- /dev/null +++ b/Sources/SQLite/SQLite.h @@ -0,0 +1,4 @@ +@import Foundation; + +FOUNDATION_EXPORT double SQLiteVersionNumber; +FOUNDATION_EXPORT const unsigned char SQLiteVersionString[]; 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/Sources/SQLite/Typed/AggregateFunctions.swift b/Sources/SQLite/Typed/AggregateFunctions.swift new file mode 100644 index 00000000..17fc4a20 --- /dev/null +++ b/Sources/SQLite/Typed/AggregateFunctions.swift @@ -0,0 +1,264 @@ +// +// 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. +// + +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. + /// + /// let name = Expression("name") + /// name.distinct + /// // DISTINCT "name" + /// + /// - Returns: A copy of the expression prefixed with the `DISTINCT` + /// keyword. + public var distinct: Expression { + Expression("DISTINCT \(template)", bindings) + } + + /// Builds a copy of the expression wrapped with the `count` aggregate + /// function. + /// + /// let name = Expression("name") + /// name.count + /// // count("name") + /// name.distinct.count + /// // count(DISTINCT "name") + /// + /// - Returns: A copy of the expression wrapped with the `count` aggregate + /// function. + public var count: Expression { + Function.count.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value { + + /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. + /// + /// let name = Expression("name") + /// name.distinct + /// // DISTINCT "name" + /// + /// - Returns: A copy of the expression prefixed with the `DISTINCT` + /// keyword. + public var distinct: Expression { + Expression("DISTINCT \(template)", bindings) + } + + /// Builds a copy of the expression wrapped with the `count` aggregate + /// function. + /// + /// let name = Expression("name") + /// name.count + /// // count("name") + /// name.distinct.count + /// // count(DISTINCT "name") + /// + /// - Returns: A copy of the expression wrapped with the `count` aggregate + /// function. + public var count: Expression { + Function.count.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: Comparable { + + /// Builds a copy of the expression wrapped with the `max` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.max + /// // max("age") + /// + /// - Returns: A copy of the expression wrapped with the `max` aggregate + /// function. + public var max: Expression { + Function.max.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `min` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.min + /// // min("age") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var min: Expression { + Function.min.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value, UnderlyingType.WrappedType.Datatype: Comparable { + + /// Builds a copy of the expression wrapped with the `max` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.max + /// // max("age") + /// + /// - Returns: A copy of the expression wrapped with the `max` aggregate + /// function. + public var max: Expression { + Function.max.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `min` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.min + /// // min("age") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var min: Expression { + Function.min.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: Number { + + /// Builds a copy of the expression wrapped with the `avg` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.average + /// // avg("salary") + /// + /// - Returns: A copy of the expression wrapped with the `avg` aggregate + /// function. + public var average: Expression { + Function.avg.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `sum` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.sum + /// // sum("salary") + /// + /// - Returns: A copy of the expression wrapped with the `sum` aggregate + /// function. + public var sum: Expression { + Function.sum.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `total` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.total + /// // total("salary") + /// + /// - Returns: A copy of the expression wrapped with the `total` aggregate + /// function. + public var total: Expression { + Function.total.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value, UnderlyingType.WrappedType.Datatype: Number { + + /// Builds a copy of the expression wrapped with the `avg` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.average + /// // avg("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var average: Expression { + Function.avg.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `sum` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.sum + /// // sum("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var sum: Expression { + Function.sum.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `total` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.total + /// // total("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var total: Expression { + Function.total.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == Int { + + static func count(_ star: Star) -> Expression { + Function.count.wrap(star(nil, nil)) + } + +} + +/// Builds an expression representing `count(*)` (when called with the `*` +/// function literal). +/// +/// count(*) +/// // count(*) +/// +/// - Returns: An expression returning `count(*)` (when called with the `*` +/// function literal). +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/Sources/SQLite/Typed/Collation.swift b/Sources/SQLite/Typed/Collation.swift new file mode 100644 index 00000000..fec66129 --- /dev/null +++ b/Sources/SQLite/Typed/Collation.swift @@ -0,0 +1,69 @@ +// +// 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. +// + +/// A collating function used to compare to strings. +/// +/// - SeeAlso: +public enum Collation { + + /// Compares string by raw data. + case binary + + /// Like binary, but folds uppercase ASCII letters into their lowercase + /// equivalents. + case nocase + + /// Like binary, but strips trailing space. + case rtrim + + /// A custom collating sequence identified by the given string, registered + /// using `Database.create(collation:…)` + case custom(String) + +} + +extension Collation: Expressible { + + public var expression: Expression { + Expression(literal: description) + } + +} + +extension Collation: CustomStringConvertible { + + public var description: String { + switch self { + case .binary: + return "BINARY" + case .nocase: + return "NOCASE" + case .rtrim: + return "RTRIM" + case .custom(let collation): + return collation.quote() + } + } + +} diff --git a/Sources/SQLite/Typed/CoreFunctions.swift b/Sources/SQLite/Typed/CoreFunctions.swift new file mode 100644 index 00000000..c4359d8b --- /dev/null +++ b/Sources/SQLite/Typed/CoreFunctions.swift @@ -0,0 +1,795 @@ +// +// 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 + +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)) + } +} + +extension ExpressionType where UnderlyingType: Number { + + /// Builds a copy of the expression wrapped with the `abs` function. + /// + /// let x = Expression("x") + /// x.absoluteValue + /// // abs("x") + /// + /// - Returns: A copy of the expression wrapped with the `abs` function. + public var absoluteValue: Expression { + Function.abs.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Number { + + /// Builds a copy of the expression wrapped with the `abs` function. + /// + /// let x = Expression("x") + /// x.absoluteValue + /// // abs("x") + /// + /// - Returns: A copy of the expression wrapped with the `abs` function. + public var absoluteValue: Expression { + Function.abs.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == Double { + + /// Builds a copy of the expression wrapped with the `round` function. + /// + /// let salary = Expression("salary") + /// salary.round() + /// // round("salary") + /// salary.round(2) + /// // round("salary", 2) + /// + /// - Returns: A copy of the expression wrapped with the `round` function. + public func round(_ precision: Int? = nil) -> Expression { + guard let precision else { + return Function.round.wrap([self]) + } + return Function.round.wrap([self, Int(precision)]) + } + +} + +extension ExpressionType where UnderlyingType == Double? { + + /// Builds a copy of the expression wrapped with the `round` function. + /// + /// let salary = Expression("salary") + /// salary.round() + /// // round("salary") + /// salary.round(2) + /// // round("salary", 2) + /// + /// - Returns: A copy of the expression wrapped with the `round` function. + public func round(_ precision: Int? = nil) -> Expression { + guard let precision else { + return Function.round.wrap(self) + } + return Function.round.wrap([self, Int(precision)]) + } + +} + +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype == Int64 { + + /// Builds an expression representing the `random` function. + /// + /// Expression.random() + /// // random() + /// + /// - Returns: An expression calling the `random` function. + public static func random() -> Expression { + Function.random.wrap([]) + } + +} + +extension ExpressionType where UnderlyingType == Data { + + /// Builds an expression representing the `randomblob` function. + /// + /// Expression.random(16) + /// // randomblob(16) + /// + /// - Parameter length: Length in bytes. + /// + /// - Returns: An expression calling the `randomblob` function. + public static func random(_ length: Int) -> Expression { + Function.randomblob.wrap([]) + } + + /// Builds an expression representing the `zeroblob` function. + /// + /// Expression.allZeros(16) + /// // zeroblob(16) + /// + /// - Parameter length: Length in bytes. + /// + /// - Returns: An expression calling the `zeroblob` function. + public static func allZeros(_ length: Int) -> Expression { + Function.zeroblob.wrap([]) + } + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let data = Expression("data") + /// data.length + /// // length("data") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + Function.length.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == Data? { + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let data = Expression("data") + /// data.length + /// // length("data") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + Function.length.wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == String { + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let name = Expression("name") + /// name.length + /// // length("name") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + Function.length.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `lower` function. + /// + /// let name = Expression("name") + /// name.lowercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `lower` function. + public var lowercaseString: Expression { + Function.lower.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `upper` function. + /// + /// let name = Expression("name") + /// name.uppercaseString + /// // upper("name") + /// + /// - Returns: A copy of the expression wrapped with the `upper` function. + public var uppercaseString: Expression { + Function.upper.wrap(self) + } + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// email.like("%@example.com") + /// // "email" LIKE '%@example.com' + /// email.like("99\\%@%", escape: "\\") + /// // "email" LIKE '99\%@%' ESCAPE '\' + /// + /// - 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: 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. + /// + /// let path = Expression("path") + /// path.glob("*.png") + /// // "path" GLOB '*.png' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `GLOB` query against + /// the given pattern. + public func glob(_ pattern: String) -> Expression { + Function.glob.infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `MATCH` query against + /// the given pattern. + /// + /// let title = Expression("title") + /// title.match("swift AND programming") + /// // "title" MATCH 'swift AND programming' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `MATCH` query + /// against the given pattern. + public func match(_ pattern: String) -> Expression { + Function.match.infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `REGEXP` query against + /// the given pattern. + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `REGEXP` query + /// against the given pattern. + public func regexp(_ pattern: String) -> Expression { + Function.regexp.infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `COLLATE` clause with + /// the given sequence. + /// + /// let name = Expression("name") + /// name.collate(.Nocase) + /// // "name" COLLATE NOCASE + /// + /// - Parameter collation: A collating sequence. + /// + /// - Returns: A copy of the expression appended with a `COLLATE` clause + /// with the given sequence. + public func collate(_ collation: Collation) -> Expression { + Function.collate.infix(self, collation) + } + + /// Builds a copy of the expression wrapped with the `ltrim` function. + /// + /// let name = Expression("name") + /// name.ltrim() + /// // ltrim("name") + /// name.ltrim([" ", "\t"]) + /// // ltrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `ltrim` function. + public func ltrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.ltrim.wrap(self) + } + return Function.ltrim.wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `rtrim` function. + /// + /// let name = Expression("name") + /// name.rtrim() + /// // rtrim("name") + /// name.rtrim([" ", "\t"]) + /// // rtrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `rtrim` function. + public func rtrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.rtrim.wrap(self) + } + return Function.rtrim.wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `trim` function. + /// + /// let name = Expression("name") + /// name.trim() + /// // trim("name") + /// name.trim([" ", "\t"]) + /// // trim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `trim` function. + public func trim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.trim.wrap([self]) + } + return Function.trim.wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `replace` function. + /// + /// let email = Expression("email") + /// email.replace("@mac.com", with: "@icloud.com") + /// // replace("email", '@mac.com', '@icloud.com') + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - replacement: The replacement string. + /// + /// - Returns: A copy of the expression wrapped with the `replace` function. + public func replace(_ pattern: String, with replacement: String) -> Expression { + Function.replace.wrap([self, pattern, replacement]) + } + + public func substring(_ location: Int, length: Int? = nil) -> Expression { + guard let length else { + return Function.substr.wrap([self, location]) + } + return Function.substr.wrap([self, location, length]) + } + + public subscript(range: Range) -> Expression { + substring(range.lowerBound, length: range.upperBound - range.lowerBound) + } + +} + +extension ExpressionType where UnderlyingType == String? { + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let name = Expression("name") + /// name.length + /// // length("name") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + Function.length.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `lower` function. + /// + /// let name = Expression("name") + /// name.lowercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `lower` function. + public var lowercaseString: Expression { + Function.lower.wrap(self) + } + + /// Builds a copy of the expression wrapped with the `upper` function. + /// + /// let name = Expression("name") + /// name.uppercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `upper` function. + public var uppercaseString: Expression { + Function.upper.wrap(self) + } + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// email.like("%@example.com") + /// // "email" LIKE '%@example.com' + /// email.like("99\\%@%", escape: "\\") + /// // "email" LIKE '99\%@%' ESCAPE '\' + /// + /// - 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: 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. + /// + /// let path = Expression("path") + /// path.glob("*.png") + /// // "path" GLOB '*.png' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `GLOB` query against + /// the given pattern. + public func glob(_ pattern: String) -> Expression { + Function.glob.infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `MATCH` query against + /// the given pattern. + /// + /// let title = Expression("title") + /// title.match("swift AND programming") + /// // "title" MATCH 'swift AND programming' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `MATCH` query + /// against the given pattern. + public func match(_ pattern: String) -> Expression { + Function.match.infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `REGEXP` query against + /// the given pattern. + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `REGEXP` query + /// against the given pattern. + public func regexp(_ pattern: String) -> Expression { + Function.regexp.infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `COLLATE` clause with + /// the given sequence. + /// + /// let name = Expression("name") + /// name.collate(.Nocase) + /// // "name" COLLATE NOCASE + /// + /// - Parameter collation: A collating sequence. + /// + /// - Returns: A copy of the expression appended with a `COLLATE` clause + /// with the given sequence. + public func collate(_ collation: Collation) -> Expression { + Function.collate.infix(self, collation) + } + + /// Builds a copy of the expression wrapped with the `ltrim` function. + /// + /// let name = Expression("name") + /// name.ltrim() + /// // ltrim("name") + /// name.ltrim([" ", "\t"]) + /// // ltrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `ltrim` function. + public func ltrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.ltrim.wrap(self) + } + return Function.ltrim.wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `rtrim` function. + /// + /// let name = Expression("name") + /// name.rtrim() + /// // rtrim("name") + /// name.rtrim([" ", "\t"]) + /// // rtrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `rtrim` function. + public func rtrim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.rtrim.wrap(self) + } + return Function.rtrim.wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `trim` function. + /// + /// let name = Expression("name") + /// name.trim() + /// // trim("name") + /// name.trim([" ", "\t"]) + /// // trim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `trim` function. + public func trim(_ characters: Set? = nil) -> Expression { + guard let characters else { + return Function.trim.wrap(self) + } + return Function.trim.wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `replace` function. + /// + /// let email = Expression("email") + /// email.replace("@mac.com", with: "@icloud.com") + /// // replace("email", '@mac.com', '@icloud.com') + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - replacement: The replacement string. + /// + /// - Returns: A copy of the expression wrapped with the `replace` function. + 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. + /// + /// let title = Expression("title") + /// title.substr(-100) + /// // substr("title", -100) + /// title.substr(0, length: 100) + /// // substr("title", 0, 100) + /// + /// - Parameters: + /// + /// - location: The substring’s start index. + /// + /// - length: An optional substring length. + /// + /// - Returns: A copy of the expression wrapped with the `substr` function. + public func substring(_ location: Int, length: Int? = nil) -> Expression { + guard let length else { + return Function.substr.wrap([self, location]) + } + return Function.substr.wrap([self, location, length]) + } + + /// Builds a copy of the expression wrapped with the `substr` function. + /// + /// let title = Expression("title") + /// title[0..<100] + /// // substr("title", 0, 100) + /// + /// - Parameter range: The character index range of the substring. + /// + /// - Returns: A copy of the expression wrapped with the `substr` function. + public subscript(range: Range) -> Expression { + substring(range.lowerBound, length: range.upperBound - range.lowerBound) + } + +} + +extension Collection where Iterator.Element: Value { + + /// Builds a copy of the expression prepended with an `IN` check against the + /// collection. + /// + /// let name = Expression("name") + /// ["alice", "betty"].contains(name) + /// // "name" IN ('alice', 'betty') + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression prepended with an `IN` check against + /// the collection. + 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 + /// collection. + /// + /// let name = Expression("name") + /// ["alice", "betty"].contains(name) + /// // "name" IN ('alice', 'betty') + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression prepended with an `IN` check against + /// the collection. + 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)]) + } + +} + +/// Builds a copy of the given expressions wrapped with the `ifnull` function. +/// +/// let name = Expression("name") +/// name ?? "An Anonymous Coward" +/// // ifnull("name", 'An Anonymous Coward') +/// +/// - Parameters: +/// +/// - optional: An optional expression. +/// +/// - defaultValue: A fallback value for when the optional expression is +/// `nil`. +/// +/// - Returns: A copy of the given expressions wrapped with the `ifnull` +/// function. +public func ??(optional: Expression, defaultValue: V) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) +} + +/// Builds a copy of the given expressions wrapped with the `ifnull` function. +/// +/// let nick = Expression("nick") +/// let name = Expression("name") +/// nick ?? name +/// // ifnull("nick", "name") +/// +/// - Parameters: +/// +/// - optional: An optional expression. +/// +/// - defaultValue: A fallback expression for when the optional expression is +/// `nil`. +/// +/// - Returns: A copy of the given expressions wrapped with the `ifnull` +/// function. +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) +} + +/// Builds a copy of the given expressions wrapped with the `ifnull` function. +/// +/// let nick = Expression("nick") +/// let name = Expression("name") +/// nick ?? name +/// // ifnull("nick", "name") +/// +/// - Parameters: +/// +/// - optional: An optional expression. +/// +/// - defaultValue: A fallback expression for when the optional expression is +/// `nil`. +/// +/// - Returns: A copy of the given expressions wrapped with the `ifnull` +/// function. +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/Sources/SQLite/Typed/Expression.swift b/Sources/SQLite/Typed/Expression.swift new file mode 100644 index 00000000..dcc44fe4 --- /dev/null +++ b/Sources/SQLite/Typed/Expression.swift @@ -0,0 +1,146 @@ +// +// 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 protocol ExpressionType: Expressible, CustomStringConvertible { // extensions cannot have inheritance clauses + + associatedtype UnderlyingType = Void + + var template: String { get } + var bindings: [Binding?] { get } + + init(_ template: String, _ bindings: [Binding?]) + +} + +extension ExpressionType { + + public init(literal: String) { + self.init(literal, []) + } + + public init(_ identifier: String) { + self.init(literal: identifier.quote()) + } + + 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 typealias UnderlyingType = Datatype + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public protocol Expressible { + + var expression: Expression { get } + +} + +extension Expressible { + + // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE + func asSQL() -> String { + let expressed = expression + return expressed.template.reduce(("", 0)) { memo, character in + let (template, index) = memo + + if character == "?" { + precondition(index < expressed.bindings.count, "not enough bindings for expression") + return (template + transcode(expressed.bindings[index]), index + 1) + } else { + return (template + String(character), index) + } + }.0 + } +} + +extension ExpressionType { + + public var expression: Expression { + Expression(template, bindings) + } + + public var asc: Expressible { + " ".join([self, Expression(literal: "ASC")]) + } + + public var desc: Expressible { + " ".join([self, Expression(literal: "DESC")]) + } + +} + +extension ExpressionType where UnderlyingType: Value { + + public init(value: UnderlyingType) { + self.init("?", [value.datatypeValue]) + } + +} + +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value { + + public static var null: Self { + self.init(value: nil) + } + + public init(value: UnderlyingType.WrappedType?) { + self.init("?", [value?.datatypeValue]) + } + +} + +extension Value { + + public var expression: Expression { + Expression(value: self).expression + } + +} + +public let rowid = Expression("ROWID") + +public func cast(_ expression: Expression) -> Expression { + 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/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift new file mode 100644 index 00000000..6162fcc7 --- /dev/null +++ b/Sources/SQLite/Typed/Query.swift @@ -0,0 +1,1299 @@ +// +// 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 + +public protocol QueryType: Expressible { + + var clauses: QueryClauses { get set } + + init(_ name: String, database: String?) + +} + +public protocol SchemaType: QueryType { + + static var identifier: String { get } + +} + +extension SchemaType { + + /// Builds a copy of the query with the `SELECT` clause applied. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let email = Expression("email") + /// + /// users.select(id, email) + /// // SELECT "id", "email" FROM "users" + /// + /// - 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 { + select(false, [column1] + more) + } + + /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.select(distinct: email) + /// // SELECT DISTINCT "email" FROM "users" + /// + /// - Parameter columns: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT DISTINCT` clause applied. + public func select(distinct column1: Expressible, _ more: Expressible...) -> Self { + select(true, [column1] + more) + } + + /// Builds a copy of the query with the `SELECT` clause applied. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let email = Expression("email") + /// + /// users.select([id, email]) + /// // SELECT "id", "email" FROM "users" + /// + /// - Parameter all: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT` clause applied. + public func select(_ all: [Expressible]) -> Self { + select(false, all) + } + + /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.select(distinct: [email]) + /// // SELECT DISTINCT "email" FROM "users" + /// + /// - Parameter columns: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT DISTINCT` clause applied. + public func select(distinct columns: [Expressible]) -> Self { + select(true, columns) + } + + /// Builds a copy of the query with the `SELECT *` clause applied. + /// + /// let users = Table("users") + /// + /// users.select(*) + /// // SELECT * FROM "users" + /// + /// - Parameter star: A star literal. + /// + /// - Returns: A query with the given `SELECT *` clause applied. + public func select(_ star: Star) -> Self { + select([star(nil, nil)]) + } + + /// Builds a copy of the query with the `SELECT DISTINCT *` clause applied. + /// + /// let users = Table("users") + /// + /// users.select(distinct: *) + /// // SELECT DISTINCT * FROM "users" + /// + /// - Parameter star: A star literal. + /// + /// - Returns: A query with the given `SELECT DISTINCT *` clause applied. + public func select(distinct star: Star) -> Self { + select(distinct: [star(nil, nil)]) + } + + /// Builds a scalar copy of the query with the `SELECT` clause applied. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// + /// users.select(id) + /// // SELECT "id" FROM "users" + /// + /// - Parameter all: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT` clause applied. + public func select(_ column: Expression) -> ScalarQuery { + select(false, [column]) + } + public func select(_ column: Expression) -> ScalarQuery { + select(false, [column]) + } + + /// Builds a scalar copy of the query with the `SELECT DISTINCT` clause + /// applied. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.select(distinct: email) + /// // SELECT DISTINCT "email" FROM "users" + /// + /// - 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 { + select(true, [column]) + } + public func select(distinct column: Expression) -> ScalarQuery { + select(true, [column]) + } + + public var count: ScalarQuery { + select(Expression.count(*)) + } + +} + +extension QueryType { + + 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. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" INNER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - table: A query representing the other table. + /// + /// - 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 { + join(table, on: Expression(condition)) + } + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" INNER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - table: A query representing the other table. + /// + /// - 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 { + join(.inner, table, on: condition) + } + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(.LeftOuter, posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" LEFT OUTER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - type: The `JOIN` operator. + /// + /// - table: A query representing the other table. + /// + /// - 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 { + join(type, table, on: Expression(condition)) + } + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(.LeftOuter, posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" LEFT OUTER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - type: The `JOIN` operator. + /// + /// - table: A query representing the other table. + /// + /// - 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 { + var query = self + query.clauses.join.append((type: type, query: table, + condition: table.clauses.filters.map { condition && $0 } ?? condition as Expressible)) + return query + } + + // MARK: WHERE + + /// Adds a condition to the query’s `WHERE` clause. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// + /// users.filter(id == 1) + /// // SELECT * FROM "users" WHERE ("id" = 1) + /// + /// - Parameter condition: A boolean expression to filter on. + /// + /// - Returns: A query with the given `WHERE` clause applied. + public func filter(_ predicate: Expression) -> Self { + filter(Expression(predicate)) + } + + /// Adds a condition to the query’s `WHERE` clause. + /// + /// let users = Table("users") + /// let age = Expression("age") + /// + /// users.filter(age >= 35) + /// // SELECT * FROM "users" WHERE ("age" >= 35) + /// + /// - Parameter condition: A boolean expression to filter on. + /// + /// - Returns: A query with the given `WHERE` clause applied. + 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. + /// + /// - 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 { + group(by) + } + + /// Sets a `GROUP BY` clause on the query. + /// + /// - 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 { + group(by, nil) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A column to group by. + /// + /// - 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 { + group([by], having: having) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A column to group by. + /// + /// - 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 { + group([by], having: having) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A list of columns to group by. + /// + /// - 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 { + group(by, Expression(having)) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A list of columns to group by. + /// + /// - 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 { + group(by, having) + } + + fileprivate func group(_ by: [Expressible], _ having: Expression?) -> Self { + var query = self + query.clauses.group = (by, having) + return query + } + + // MARK: ORDER BY + + /// Sets an `ORDER BY` clause on the query. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// let name = Expression("name") + /// + /// users.order(email.desc, name.asc) + /// // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC + /// + /// - 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 { + order(by) + } + + /// Sets an `ORDER BY` clause on the query. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// let name = Expression("name") + /// + /// users.order([email.desc, name.asc]) + /// // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC + /// + /// - 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 { + var query = self + query.clauses.order = by + return query + } + + // MARK: LIMIT/OFFSET + + /// Sets the LIMIT clause (and resets any OFFSET clause) on the query. + /// + /// let users = Table("users") + /// + /// users.limit(20) + /// // SELECT * FROM "users" LIMIT 20 + /// + /// - Parameter length: The maximum number of rows to return (or `nil` to + /// return unlimited rows). + /// + /// - Returns: A query with the given LIMIT clause applied. + public func limit(_ length: Int?) -> Self { + limit(length, nil) + } + + /// Sets LIMIT and OFFSET clauses on the query. + /// + /// let users = Table("users") + /// + /// users.limit(20, offset: 20) + /// // SELECT * FROM "users" LIMIT 20 OFFSET 20 + /// + /// - Parameters: + /// + /// - length: The maximum number of rows to return. + /// + /// - 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 { + limit(length, offset) + } + + // prevents limit(nil, offset: 5) + fileprivate func limit(_ length: Int?, _ offset: Int?) -> Self { + var query = self + query.clauses.limit = length.map { ($0, offset) } + return query + } + + // MARK: - Clauses + // + // MARK: SELECT + + // MARK: - + + fileprivate var selectClause: Expressible { + " ".join([ + Expression(literal: + clauses.select.distinct ? "SELECT DISTINCT" : "SELECT"), + ", ".join(clauses.select.columns), + Expression(literal: "FROM"), + tableName(alias: true) + ]) + } + + fileprivate var joinClause: Expressible? { + guard !clauses.join.isEmpty else { + return nil + } + + 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"), + condition + ]) + }) + } + + fileprivate var whereClause: Expressible? { + guard let filters = clauses.filters else { + return nil + } + + return " ".join([ + Expression(literal: "WHERE"), + filters + ]) + } + + fileprivate var groupByClause: Expressible? { + guard let group = clauses.group else { + return nil + } + + let groupByClause = " ".join([ + Expression(literal: "GROUP BY"), + ", ".join(group.by) + ]) + + guard let having = group.having else { + return groupByClause + } + + return " ".join([ + groupByClause, + " ".join([ + Expression(literal: "HAVING"), + having + ]) + ]) + } + + fileprivate var orderClause: Expressible? { + guard !clauses.order.isEmpty else { + return nil + } + + return " ".join([ + Expression(literal: "ORDER BY"), + ", ".join(clauses.order) + ]) + } + + fileprivate var limitOffsetClause: Expressible? { + guard let limit = clauses.limit else { + return nil + } + + let limitClause = Expression(literal: "LIMIT \(limit.length)") + + guard let offset = limit.offset else { + return limitClause + } + + return " ".join([ + limitClause, + Expression(literal: "OFFSET \(offset)") + ]) + } + + 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 { + var query = self + query.clauses.from = (clauses.from.name, aliasName, clauses.from.database) + return query + } + + // MARK: - Operations + // + // MARK: INSERT + + public func insert(_ value: Setter, _ more: Setter...) -> Insert { + insert([value] + more) + } + + public func insert(_ values: [Setter]) -> Insert { + insert(nil, values) + } + + public func insert(or onConflict: OnConflict, _ values: Setter...) -> Insert { + insert(or: onConflict, values) + } + + public func insert(or onConflict: OnConflict, _ values: [Setter]) -> Insert { + insert(onConflict, values) + } + + public func insertMany( _ values: [[Setter]]) -> Insert { + insertMany(nil, values) + } + + 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]) + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + or.map { Expression(literal: "OR \($0.rawValue)") }, + Expression(literal: "INTO"), + tableName(), + "".wrap(insert.columns) as Expression, + Expression(literal: "VALUES"), + "".wrap(insert.values) as Expression, + whereClause + ] + + 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 { + Insert(" ".join([ + Expression(literal: "INSERT INTO"), + tableName(), + Expression(literal: "DEFAULT VALUES") + ]).expression) + } + + /// Runs an `INSERT` statement against the query with the results of another + /// query. + /// + /// - Parameter query: A query to `SELECT` results from. + /// + /// - Returns: The number of updated rows and statement. + public func insert(_ query: QueryType) -> Update { + Update(" ".join([ + Expression(literal: "INSERT INTO"), + tableName(), + query.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 { + update(values) + } + + 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, + orderClause, + limitOffsetClause + ] + + return Update(" ".join(clauses.compactMap { $0 }).expression) + } + + // MARK: DELETE + + public func delete() -> Delete { + let clauses: [Expressible?] = [ + Expression(literal: "DELETE FROM"), + tableName(), + whereClause, + orderClause, + limitOffsetClause + ] + + return Delete(" ".join(clauses.compactMap { $0 }).expression) + } + + // MARK: EXISTS + + public var exists: Select { + Select(" ".join([ + Expression(literal: "SELECT EXISTS"), + "".wrap(expression) as Expression + ]).expression) + } + + // MARK: - + + /// Prefixes a column expression with the query’s table name or alias. + /// + /// - Parameter column: A column expression. + /// + /// - Returns: A column expression namespaced with the query’s table name or + /// alias. + public func namespace(_ column: Expression) -> Expression { + Expression(".".join([tableName(), column]).expression) + } + + public subscript(column: Expression) -> Expression { + namespace(column) + } + + public subscript(column: Expression) -> Expression { + namespace(column) + } + + /// Prefixes a star with the query’s table name or alias. + /// + /// - Parameter star: A literal `*`. + /// + /// - Returns: A `*` expression namespaced with the query’s table name or + /// alias. + public subscript(star: Star) -> Expression { + namespace(star(nil, nil)) + } + + // MARK: - + + // TODO: alias support + func tableName(alias aliased: Bool = false) -> Expressible { + guard let alias = clauses.from.alias, aliased else { + return database(namespace: clauses.from.alias ?? clauses.from.name) + } + + return " ".join([ + database(namespace: clauses.from.name), + Expression(literal: "AS"), + Expression(alias) + ]) + } + + func tableName(qualified: Bool) -> Expressible { + if qualified { + return tableName() + } + return Expression(clauses.from.alias ?? clauses.from.name) + } + + func database(namespace name: String) -> Expressible { + let name = Expression(name) + + guard let database = clauses.from.database else { + return name + } + + return ".".join([Expression(database), name]) + } + + public var expression: Expression { + let clauses: [Expressible?] = [ + withClause, + selectClause, + joinClause, + whereClause, + groupByClause, + unionClause, + orderClause, + limitOffsetClause + ] + + return " ".join(clauses.compactMap { $0 }).expression + } + +} + +// TODO: decide: simplify the below with a boxed type instead + +/// Queries a collection of chainable helper functions and expressions to build +/// executable SQL statements. +public struct Table: SchemaType { + + public static let identifier = "TABLE" + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +public struct View: SchemaType { + + public static let identifier = "VIEW" + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +public struct VirtualTable: SchemaType { + + public static let identifier = "VIRTUAL TABLE" + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +// TODO: make `ScalarQuery` work in `QueryType.select()`, `.filter()`, etc. + +public struct ScalarQuery: QueryType { + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +// TODO: decide: simplify the below with a boxed type instead + +public struct Select: ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public struct Insert: ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public struct Update: ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public struct Delete: ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +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 { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) + + 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 + } + + 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 } + } + } + + 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 + } + + columnNames[each.expression.template] = idx + idx += 1 + } + return columnNames + } + + 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? { + let expression = query.expression + guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } + return try V.fromDatatypeValue(value) + } + + 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? { + let expression = query.expression + guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } + return try V.fromDatatypeValue(value) + } + + public func pluck(_ query: QueryType) throws -> Row? { + try prepareRowIterator(query.limit(1, query.clauses.limit?.offset)).failableNext() + } + + /// Runs an `Insert` query. + /// + /// - 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. + @discardableResult public func run(_ query: Insert) throws -> Int64 { + let expression = query.expression + return try sync { + try self.run(expression.template, expression.bindings) + return lastInsertRowid + } + } + + /// Runs an `Update` query. + /// + /// - SeeAlso: `QueryType.insert(query:)` + /// - SeeAlso: `QueryType.update(values:)` + /// + /// - Parameter query: An update query. + /// + /// - Returns: The number of updated rows. + @discardableResult public func run(_ query: Update) throws -> Int { + let expression = query.expression + return try sync { + try self.run(expression.template, expression.bindings) + return changes + } + } + + /// Runs a `Delete` query. + /// + /// - SeeAlso: `QueryType.delete()` + /// + /// - Parameter query: A delete query. + /// + /// - Returns: The number of deleted rows. + @discardableResult public func run(_ query: Delete) throws -> Int { + let expression = query.expression + return try sync { + try self.run(expression.template, expression.bindings) + return changes + } + } + +} + +public struct Row { + + let columnNames: [String: Int] + + fileprivate let 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) throws -> V { + if let value = try get(Expression(column)) { + return value + } else { + throw QueryError.unexpectedNullValue(name: column.template) + } + } + + 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 try V.fromDatatypeValue(value) as? V + } + + guard let idx = columnNames[column.template] else { + func similar(_ name: String) -> Bool { + return name.hasSuffix(".\(column.template)") + } + + guard let firstIndex = columnNames.firstIndex(where: { similar($0.key) }) else { + throw QueryError.noSuchColumn(name: column.template, columns: columnNames.keys.sorted()) + } + + let secondIndex = columnNames + .suffix(from: columnNames.index(after: firstIndex)) + .firstIndex(where: { similar($0.key) }) + + guard secondIndex == nil else { + throw QueryError.ambiguousColumn( + name: column.template, + similar: columnNames.keys.filter(similar).sorted() + ) + } + return try valueAtIndex(columnNames[firstIndex].value) + } + + return try valueAtIndex(idx) + } + + public subscript(column: Expression) -> T { + // swiftlint:disable:next force_try + try! get(column) + } + + public subscript(column: Expression) -> T? { + // swiftlint:disable:next force_try + try! get(column) + } +} + +/// Determines the join operator for a query’s `JOIN` clause. +public enum JoinType: String { + + /// A `CROSS` join. + case cross = "CROSS" + + /// An `INNER` join. + case inner = "INNER" + + /// A `LEFT OUTER` join. + case leftOuter = "LEFT OUTER" + +} + +/// ON CONFLICT resolutions. +public enum OnConflict: String { + + case replace = "REPLACE" + + case rollback = "ROLLBACK" + + case abort = "ABORT" + + case fail = "FAIL" + + case ignore = "IGNORE" + +} + +// MARK: - Private + +public struct QueryClauses { + + var select = (distinct: false, columns: [Expression(literal: "*") as Expressible]) + + var from: (name: String, alias: String?, database: String?) + + var join = [(type: JoinType, query: QueryType, condition: Expressible)]() + + var filters: Expression? + + var group: (by: [Expressible], having: Expression?)? + + var order = [Expressible]() + + var limit: (length: Int, offset: Int?)? + + 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/Tests/SQLiteTests/Core/ValueTests.swift b/Tests/SQLiteTests/Core/ValueTests.swift new file mode 100644 index 00000000..f880cb34 --- /dev/null +++ b/Tests/SQLiteTests/Core/ValueTests.swift @@ -0,0 +1,6 @@ +import XCTest +import SQLite + +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/Tests/SQLiteTests/Extensions/FTS4Tests.swift b/Tests/SQLiteTests/Extensions/FTS4Tests.swift new file mode 100644 index 00000000..f7258fb5 --- /dev/null +++ b/Tests/SQLiteTests/Extensions/FTS4Tests.swift @@ -0,0 +1,190 @@ +import XCTest +import SQLite + +class FTS4Tests: XCTestCase { + + func test_create_onVirtualTable_withFTS4_compilesCreateVirtualTableExpression() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4()", + virtualTable.create(.FTS4()) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\")", + virtualTable.create(.FTS4(string)) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=simple)", + virtualTable.create(.FTS4(tokenize: .Simple)) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\", tokenize=porter)", + virtualTable.create(.FTS4([string], tokenize: .Porter)) + ) + XCTAssertEqual( + "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 \"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 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) + } + +} + +class FTS4ConfigTests: XCTestCase { + var config: FTS4Config! + + override func setUp() { + super.setUp() + config = FTS4Config() + } + + func test_empty_config() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4()", + sql(config)) + } + + func test_config_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\")", + sql(config.column(string))) + } + + func test_config_columns() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\", \"int\")", + sql(config.columns([string, int]))) + } + + func test_config_unindexed_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\", notindexed=\"string\")", + sql(config.column(string, [.unindexed]))) + } + + func test_external_content_view() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(content=\"view\")", + sql(config.externalContent(_view ))) + } + + func test_external_content_virtual_table() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(content=\"virtual_table\")", + sql(config.externalContent(virtualTable))) + } + + func test_tokenizer_simple() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=simple)", + sql(config.tokenizer(.Simple))) + } + + func test_tokenizer_porter() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=porter)", + sql(config.tokenizer(.Porter))) + } + + func test_tokenizer_unicode61() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61)", + sql(config.tokenizer(.Unicode61()))) + } + + func test_tokenizer_unicode61_with_options() { + XCTAssertEqual( + """ + 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"])))) + } + + func test_content_less() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(content=\"\")", + sql(config.contentless())) + } + + func test_config_matchinfo() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(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))) + } + + func test_config_order_desc() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(order=\"desc\")", + sql(config.order(.desc))) + } + + func test_config_compress() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(compress=\"compress_foo\")", + sql(config.compress("compress_foo"))) + } + + func test_config_uncompress() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(uncompress=\"uncompress_foo\")", + sql(config.uncompress("uncompress_foo"))) + } + + func test_config_languageId() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(languageid=\"lid\")", + sql(config.languageId("lid"))) + } + + 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\") + """.replacingOccurrences(of: "\n", with: ""), + sql(config + .tokenizer(.Porter) + .column(int) + .column(string, [.unindexed]) + .column(date, [.unindexed]) + .externalContent(table) + .matchInfo(.fts3) + .languageId("lid") + .order(.desc) + .prefix([2, 4])) + ) + } + + func sql(_ config: FTS4Config) -> String { + virtualTable.create(.FTS4(config)) + } +} diff --git a/Tests/SQLiteTests/Extensions/FTS5Tests.swift b/Tests/SQLiteTests/Extensions/FTS5Tests.swift new file mode 100644 index 00000000..199ec415 --- /dev/null +++ b/Tests/SQLiteTests/Extensions/FTS5Tests.swift @@ -0,0 +1,137 @@ +import XCTest +import SQLite + +class FTS5Tests: XCTestCase { + var config: FTS5Config! + + override func setUp() { + super.setUp() + config = FTS5Config() + } + + func test_empty_config() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5()", + sql(config)) + } + + func test_config_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"string\")", + sql(config.column(string))) + } + + func test_config_columns() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"string\", \"int\")", + sql(config.columns([string, int]))) + } + + func test_config_unindexed_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"string\" UNINDEXED)", + sql(config.column(string, [.unindexed]))) + } + + func test_external_content_table() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"table\")", + sql(config.externalContent(table))) + } + + func test_external_content_view() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"view\")", + sql(config.externalContent(_view))) + } + + func test_external_content_virtual_table() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"virtual_table\")", + sql(config.externalContent(virtualTable))) + } + + func test_content_less() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"\")", + sql(config.contentless())) + } + + func test_content_rowid() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content_rowid=\"string\")", + sql(config.contentRowId(string))) + } + + func test_tokenizer_porter() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=porter)", + sql(config.tokenizer(.Porter))) + } + + func test_tokenizer_unicode61() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61)", + sql(config.tokenizer(.Unicode61()))) + } + + func test_tokenizer_unicode61_with_options() { + XCTAssertEqual( + "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)", + sql(config.columnSize(1))) + } + + func test_detail_full() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"full\")", + sql(config.detail(.full))) + } + + func test_detail_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"column\")", + sql(config.detail(.column))) + } + + func test_detail_none() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(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\") + """.replacingOccurrences(of: "\n", with: ""), + sql(config + .tokenizer(.Porter) + .column(int) + .column(string, [.unindexed]) + .column(date, [.unindexed]) + .externalContent(table) + .prefix([2, 4])) + ) + } + + 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/Tests/SQLiteTests/Extensions/RTreeTests.swift b/Tests/SQLiteTests/Extensions/RTreeTests.swift new file mode 100644 index 00000000..5525da26 --- /dev/null +++ b/Tests/SQLiteTests/Extensions/RTreeTests.swift @@ -0,0 +1,17 @@ +import XCTest +import SQLite + +class RTreeTests: XCTestCase { + + func test_create_onVirtualTable_withRTree_createVirtualTableExpression() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING rtree(\"int64\", \"double\", \"double\")", + virtualTable.create(.RTree(int64, (double, double))) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING rtree(\"int64\", \"double\", \"double\", \"double\", \"double\")", + virtualTable.create(.RTree(int64, (double, double), (double, double))) + ) + } + +} 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/SQLite Tests/Info.plist b/Tests/SQLiteTests/Info.plist similarity index 90% rename from SQLite Tests/Info.plist rename to Tests/SQLiteTests/Info.plist index 00b831da..ba72822e 100644 --- a/SQLite Tests/Info.plist +++ b/Tests/SQLiteTests/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.stephencelis.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName 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/Tests/SQLiteTests/Schema/SchemaTests.swift b/Tests/SQLiteTests/Schema/SchemaTests.swift new file mode 100644 index 00000000..4f4a49d1 --- /dev/null +++ b/Tests/SQLiteTests/Schema/SchemaTests.swift @@ -0,0 +1,867 @@ +import XCTest +import SQLite + +class SchemaTests: XCTestCase { + + func test_drop_compilesDropTableExpression() { + XCTAssertEqual("DROP TABLE \"table\"", table.drop()) + XCTAssertEqual("DROP TABLE IF EXISTS \"table\"", table.drop(ifExists: true)) + } + + func test_drop_compilesDropVirtualTableExpression() { + XCTAssertEqual("DROP TABLE \"virtual_table\"", virtualTable.drop()) + XCTAssertEqual("DROP TABLE IF EXISTS \"virtual_table\"", virtualTable.drop(ifExists: true)) + } + + func test_drop_compilesDropViewExpression() { + XCTAssertEqual("DROP VIEW \"view\"", _view.drop()) + XCTAssertEqual("DROP VIEW IF EXISTS \"view\"", _view.drop(ifExists: true)) + } + + func test_create_withBuilder_compilesCreateTableExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (" + + "\"blob\" BLOB NOT NULL, " + + "\"blobOptional\" BLOB, " + + "\"double\" REAL NOT NULL, " + + "\"doubleOptional\" REAL, " + + "\"int64\" INTEGER NOT NULL, " + + "\"int64Optional\" INTEGER, " + + "\"string\" TEXT NOT NULL, " + + "\"stringOptional\" TEXT" + + ")", + table.create { t in + t.column(data) + t.column(dataOptional) + t.column(double) + t.column(doubleOptional) + t.column(int64) + t.column(int64Optional) + t.column(string) + t.column(stringOptional) + } + ) + XCTAssertEqual( + "CREATE TEMPORARY TABLE \"table\" (\"int64\" INTEGER NOT NULL)", + table.create(temporary: true) { $0.column(int64) } + ) + XCTAssertEqual( + "CREATE TABLE IF NOT EXISTS \"table\" (\"int64\" INTEGER NOT NULL)", + table.create(ifNotExists: true) { $0.column(int64) } + ) + XCTAssertEqual( + "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() { + XCTAssertEqual( + "CREATE TABLE \"table\" AS SELECT \"int64\" FROM \"view\"", + table.create(_view.select(int64)) + ) + } + + // thoroughness test for ambiguity + // swiftlint:disable:next function_body_length + func test_column_compilesColumnDefinitionExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL)", + table.create { t in t.column(int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE)", + table.create { t in t.column(int64, unique: true) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL DEFAULT (\"int64\"))", + table.create { t in t.column(int64, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL DEFAULT (0))", + table.create { t in t.column(int64, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE DEFAULT (\"int64\"))", + table.create { t in t.column(int64, unique: true, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE DEFAULT (0))", + table.create { t in t.column(int64, unique: true, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, unique: true, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, check: int64Optional > 0, defaultValue: 0) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL DEFAULT (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0, defaultValue: int64) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER)", + table.create { t in t.column(int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE)", + table.create { t in t.column(int64Optional, unique: true) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0))", + table.create { t in t.column(int64Optional, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64Optional, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER DEFAULT (0))", + table.create { t in t.column(int64Optional, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, unique: true, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE DEFAULT (0))", + table.create { t in t.column(int64Optional, unique: true, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, check: int64 > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, defaultValue: 0) } + ) + } + + 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) } + ) + 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) } + ) + 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) } + ) + } + + func test_column_withIntegerExpression_compilesReferentialColumnDefinitionExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, references: qualifiedTable, int64) } + ) + XCTAssertEqual( + "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) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, check: int64Optional > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + "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\"))", + table.create { t in t.column(int64Optional, references: table, int64) } + ) + XCTAssertEqual( + "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) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, references: table, int64) } + ) + XCTAssertEqual( + "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) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE 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) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL 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) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL DEFAULT ('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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + 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) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT COLLATE RTRIM)", + table.create { t in t.column(stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "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 CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: string != "", collate: .rtrim) } + ) + XCTAssertEqual( + "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 DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "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 DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "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 UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", collate: .rtrim) } + ) + XCTAssertEqual( + "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 UNIQUE DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "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 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 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 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 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 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 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 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 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 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 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 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 CHECK (\"stringOptional\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: "string", collate: .rtrim) } + ) + } + + func test_primaryKey_compilesPrimaryKeyExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\"))", + table.create { t in t.primaryKey(int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\"))", + table.create { t in t.primaryKey(int64, string) } + ) + XCTAssertEqual( + "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() { + XCTAssertEqual( + "CREATE TABLE \"table\" (UNIQUE (\"int64\"))", + table.create { t in t.unique(int64) } + ) + } + + func test_check_compilesCheckExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (CHECK ((\"int64\" > 0)))", + table.create { t in t.check(int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (CHECK ((\"int64Optional\" > 0)))", + table.create { t in t.check(int64Optional > 0) } + ) + } + + func test_foreignKey_compilesForeignKeyExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\") REFERENCES \"table\" (\"string\"))", + table.create { t in t.foreignKey(string, references: table, string) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"stringOptional\") REFERENCES \"table\" (\"string\"))", + table.create { t in t.foreignKey(stringOptional, references: table, string) } + ) + + 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) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\", \"string\") REFERENCES \"table\" (\"string\", \"string\"))", + table.create { t in t.foreignKey((string, string), references: table, (string, string)) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\", \"string\", \"string\") REFERENCES \"table\" (\"string\", \"string\", \"string\"))", + table.create { t in t.foreignKey((string, string, string), references: table, (string, string, string)) } + ) + } + + func test_addColumn_compilesAlterTableExpression() { + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL DEFAULT (1)", + table.addColumn(int64, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) DEFAULT (1)", + table.addColumn(int64, check: int64 > 0, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (1)", + table.addColumn(int64, check: int64Optional > 0, defaultValue: 1) + ) + + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER", + table.addColumn(int64Optional) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64\" > 0)", + table.addColumn(int64Optional, check: int64 > 0) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0)", + table.addColumn(int64Optional, check: int64Optional > 0) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER DEFAULT (1)", + table.addColumn(int64Optional, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (1)", + table.addColumn(int64Optional, check: int64 > 0, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (1)", + table.addColumn(int64Optional, check: int64Optional > 0, defaultValue: 1) + ) + } + + func test_addColumn_withIntegerExpression_compilesReferentialAlterTableExpression() { + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL UNIQUE REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, unique: true, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, check: int64Optional > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, unique: true, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, unique: true, check: int64Optional > 0, references: table, int64) + ) + + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER UNIQUE REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, unique: true, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, check: int64Optional > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, unique: true, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, unique: true, check: int64Optional > 0, references: table, int64) + ) + } + + 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) + ) + 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) + ) + 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) + ) + + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT 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) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT 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) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM", + table.addColumn(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .rtrim) + ) + } + + func test_rename_compilesAlterTableRenameToExpression() { + XCTAssertEqual("ALTER TABLE \"old\" RENAME TO \"table\"", Table("old").rename(table)) + } + + func test_createIndex_compilesCreateIndexExpression() { + XCTAssertEqual("CREATE INDEX \"index_table_on_int64\" ON \"table\" (\"int64\")", table.createIndex(int64)) + + XCTAssertEqual( + "CREATE UNIQUE INDEX \"index_table_on_int64\" ON \"table\" (\"int64\")", + table.createIndex(int64, unique: true) + ) + XCTAssertEqual( + "CREATE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", + 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) + ) + XCTAssertEqual( + "CREATE UNIQUE INDEX IF NOT EXISTS \"main\".\"index_table_on_int64\" ON \"table\" (\"int64\")", + 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)) + } + + func test_create_onView_compilesCreateViewExpression() { + XCTAssertEqual( + "CREATE VIEW \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64)) + ) + XCTAssertEqual( + "CREATE TEMPORARY VIEW \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64), temporary: true) + ) + XCTAssertEqual( + "CREATE VIEW IF NOT EXISTS \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64), ifNotExists: true) + ) + XCTAssertEqual( + "CREATE TEMPORARY VIEW IF NOT EXISTS \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64), temporary: true, ifNotExists: true) + ) + } + + func test_create_onVirtualTable_compilesCreateVirtualTableExpression() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING \"custom\"('foo', 'bar')", + virtualTable.create(Module("custom", ["foo", "bar"])) + ) + } + + func test_rename_onVirtualTable_compilesAlterTableRenameToExpression() { + XCTAssertEqual( + "ALTER TABLE \"old\" RENAME TO \"virtual_table\"", + VirtualTable("old").rename(virtualTable) + ) + } + +} 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/Vendor/fts3_tokenizer.h b/Vendor/fts3_tokenizer.h deleted file mode 100644 index 4a40b2b3..00000000 --- a/Vendor/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. -*/ -#include "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/Vendor/sqlcipher b/Vendor/sqlcipher deleted file mode 160000 index fafb0d05..00000000 --- a/Vendor/sqlcipher +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fafb0d05bdae394037ac495fe7261573f5f675eb diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 00000000..3ffba810 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -ev +if [ -n "$BUILD_SCHEME" ]; then + 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 + 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 diff --git a/sqlite3/iphoneos.modulemap b/sqlite3/iphoneos.modulemap deleted file mode 100644 index e1d16f92..00000000 --- a/sqlite3/iphoneos.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module sqlite3 [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/sqlite3/iphonesimulator.modulemap b/sqlite3/iphonesimulator.modulemap deleted file mode 100644 index 2c033c16..00000000 --- a/sqlite3/iphonesimulator.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module sqlite3 [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/sqlite3/macosx.modulemap b/sqlite3/macosx.modulemap deleted file mode 100644 index 2442dd69..00000000 --- a/sqlite3/macosx.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module sqlite3 [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/include/sqlite3.h" - export * -} diff --git a/sqlite3/sqlite3.xcconfig b/sqlite3/sqlite3.xcconfig deleted file mode 100644 index 4b92f889..00000000 --- a/sqlite3/sqlite3.xcconfig +++ /dev/null @@ -1,5 +0,0 @@ -#include "../SQLite/SQLite.xcconfig" - -MODULEMAP_FILE[sdk=iphoneos*] = $(SRCROOT)/sqlite3/iphoneos.modulemap -MODULEMAP_FILE[sdk=iphonesimulator*] = $(SRCROOT)/sqlite3/iphonesimulator.modulemap -MODULEMAP_FILE[sdk=macosx*] = $(SRCROOT)/sqlite3/macosx.modulemap