diff --git a/.cocoadocs.yml b/.cocoadocs.yml deleted file mode 100644 index 27840032..00000000 --- a/.cocoadocs.yml +++ /dev/null @@ -1,2 +0,0 @@ -additional_guides: - - Documentation/Index.md 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 similarity index 87% rename from .github/ISSUE_TEMPLATE.md rename to .github/issue_template.md index 9304a672..f9622b9b 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/issue_template.md @@ -1,5 +1,5 @@ > Issues are used to track bugs and feature requests. -> Need help or have a general question? Ask on Stack Overflow (tag sqlite.swift). +> Need help or have a general question? Ask on Stack Overflow (tag sqlite.swift). ## Build Information 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 5882e0cb..e7b2ad4d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,10 @@ DerivedData # Carthage /Carthage/ +# Makefile +bin/ + # Swift Package Manager .build Packages/ +.swiftpm/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29b..00000000 diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 5186d070..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.0 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..1bb21cd5 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,47 @@ +opt_in_rules: + - shorthand_optional_binding +disabled_rules: # rule identifiers to exclude from running + - todo + - operator_whitespace + - large_tuple + - closure_parameter_position + - inclusive_language # sqlite_master etc. + - blanket_disable_command +included: # paths to include during linting. `--path` is ignored if present. takes precendence over `excluded`. + - Sources + - Tests +excluded: # paths to ignore during linting. overridden by `included`. + +identifier_name: + excluded: + - db + - in + - to + - by + - or + - eq + - gt + - lt + - fn + - a + - b + - q + - SQLITE_TRANSIENT + +type_body_length: + warning: 350 + error: 350 + +function_body_length: + warning: 60 + error: 60 + +line_length: + warning: 150 + error: 150 + ignores_comments: true + +file_length: + warning: 500 + error: 500 + ignore_comment_only_lines: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4f17b26f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: objective-c -rvm: 2.3 -osx_image: xcode9 -env: - global: - - IOS_SIMULATOR="iPhone 6s" - - IOS_VERSION="11.0" -matrix: - include: - - env: BUILD_SCHEME="SQLite iOS" - - env: BUILD_SCHEME="SQLite Mac" - - env: VALIDATOR_SUBSPEC="none" - - env: VALIDATOR_SUBSPEC="standard" - - env: VALIDATOR_SUBSPEC="standalone" - - env: VALIDATOR_SUBSPEC="SQLCipher" - - env: CARTHAGE_PLATFORM="iOS" - - env: CARTHAGE_PLATFORM="Mac" - - env: CARTHAGE_PLATFORM="watchOS" - - env: CARTHAGE_PLATFORM="tvOS" - - env: PACKAGE_MANAGER_COMMAND="test" -before_install: - - gem update bundler - - gem install xcpretty --no-document - - brew update - - brew outdated carthage || brew upgrade carthage -script: - - ./run-tests.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 578b59c2..b77a8b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,119 @@ +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] ======================================== @@ -51,9 +167,26 @@ [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 @@ -81,3 +214,61 @@ [#723]: https://github.com/stephencelis/SQLite.swift/pull/723 [#733]: https://github.com/stephencelis/SQLite.swift/pull/733 [#726]: https://github.com/stephencelis/SQLite.swift/pull/726 +[#797]: https://github.com/stephencelis/SQLite.swift/pull/797 +[#866]: https://github.com/stephencelis/SQLite.swift/pull/866 +[#881]: https://github.com/stephencelis/SQLite.swift/pull/881 +[#919]: https://github.com/stephencelis/SQLite.swift/pull/919 +[#1073]: https://github.com/stephencelis/SQLite.swift/issues/1073 +[#1074]: https://github.com/stephencelis/SQLite.swift/issues/1074 +[#1075]: https://github.com/stephencelis/SQLite.swift/pull/1075 +[#1077]: https://github.com/stephencelis/SQLite.swift/issues/1077 +[#1094]: https://github.com/stephencelis/SQLite.swift/pull/1094 +[#1095]: https://github.com/stephencelis/SQLite.swift/pull/1095 +[#1098]: https://github.com/stephencelis/SQLite.swift/issues/1098 +[#1100]: https://github.com/stephencelis/SQLite.swift/pull/1100 +[#1101]: https://github.com/stephencelis/SQLite.swift/issues/1101 +[#1104]: https://github.com/stephencelis/SQLite.swift/issues/1104 +[#1105]: https://github.com/stephencelis/SQLite.swift/pull/1105 +[#1109]: https://github.com/stephencelis/SQLite.swift/issues/1109 +[#1110]: https://github.com/stephencelis/SQLite.swift/pull/1110 +[#1112]: https://github.com/stephencelis/SQLite.swift/pull/1112 +[#1115]: https://github.com/stephencelis/SQLite.swift/pull/1115 +[#1119]: https://github.com/stephencelis/SQLite.swift/pull/1119 +[#1121]: https://github.com/stephencelis/SQLite.swift/pull/1121 +[#1128]: https://github.com/stephencelis/SQLite.swift/issues/1128 +[#1130]: https://github.com/stephencelis/SQLite.swift/issues/1130 +[#1131]: https://github.com/stephencelis/SQLite.swift/pull/1131 +[#1132]: https://github.com/stephencelis/SQLite.swift/pull/1132 +[#1137]: https://github.com/stephencelis/SQLite.swift/pull/1137 +[#1138]: https://github.com/stephencelis/SQLite.swift/pull/1138 +[#1139]: https://github.com/stephencelis/SQLite.swift/pull/1139 +[#1141]: https://github.com/stephencelis/SQLite.swift/pull/1141 +[#1142]: https://github.com/stephencelis/SQLite.swift/pull/1142 +[#1144]: https://github.com/stephencelis/SQLite.swift/pull/1144 +[#1146]: https://github.com/stephencelis/SQLite.swift/pull/1146 +[#1148]: https://github.com/stephencelis/SQLite.swift/pull/1148 +[#1167]: https://github.com/stephencelis/SQLite.swift/pull/1167 +[#1179]: https://github.com/stephencelis/SQLite.swift/pull/1179 +[#1178]: https://github.com/stephencelis/SQLite.swift/pull/1178 +[#1182]: https://github.com/stephencelis/SQLite.swift/pull/1182 +[#1192]: https://github.com/stephencelis/SQLite.swift/pull/1192 +[#1196]: https://github.com/stephencelis/SQLite.swift/pull/1196 +[#1200]: https://github.com/stephencelis/SQLite.swift/pull/1200 +[#1206]: https://github.com/stephencelis/SQLite.swift/pull/1206 +[#1217]: https://github.com/stephencelis/SQLite.swift/pull/1217 +[#1224]: https://github.com/stephencelis/SQLite.swift/pull/1224 +[#1242]: https://github.com/stephencelis/SQLite.swift/pull/1242 +[#1228]: https://github.com/stephencelis/SQLite.swift/pull/1228 +[#1218]: https://github.com/stephencelis/SQLite.swift/pull/1218 +[#1210]: https://github.com/stephencelis/SQLite.swift/pull/1210 +[#1245]: https://github.com/stephencelis/SQLite.swift/pull/1245 +[#1249]: https://github.com/stephencelis/SQLite.swift/pull/1249 +[#1248]: https://github.com/stephencelis/SQLite.swift/pull/1248 +[#1255]: https://github.com/stephencelis/SQLite.swift/pull/1255 +[#1237]: https://github.com/stephencelis/SQLite.swift/pull/1237 +[#1260]: https://github.com/stephencelis/SQLite.swift/pull/1260 +[#1265]: https://github.com/stephencelis/SQLite.swift/pull/1265 +[#1279]: https://github.com/stephencelis/SQLite.swift/pull/1279 +[#1280]: https://github.com/stephencelis/SQLite.swift/pull/1280 +[#1315]: https://github.com/stephencelis/SQLite.swift/pull/1315 +[#1317]: https://github.com/stephencelis/SQLite.swift/pull/1317 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60c18370..6969a705 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ addresses everything. If it doesn’t, continue the conversation there. If your searches return empty, see the [bug](#bugs) or [feature request](#feature-requests) guidelines below. -[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Ask on Stack Overflow]: https://stackoverflow.com/questions/tagged/sqlite.swift [Search]: https://github.com/stephencelis/SQLite.swift/search?type=Issues @@ -74,7 +74,7 @@ Made it through everything above and still having trouble? Sorry! - Even better: link to a sample project exhibiting the issue. - Include the SQLite.swift commit or branch experiencing the issue. - Include devices and operating systems affected. - - Include build information: the Xcode and OS X versions affected. + - Include build information: the Xcode and macOS versions affected. [installation instructions]: Documentation/Index.md#installation [See Documentation]: Documentation/Index.md#sqliteswift-documentation diff --git a/Documentation/Index.md b/Documentation/Index.md index efb90ae6..0e8261f2 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -1,19 +1,24 @@ # SQLite.swift Documentation +- [SQLite.swift Documentation](#sqliteswift-documentation) - [Installation](#installation) + - [Swift Package Manager](#swift-package-manager) - [Carthage](#carthage) - [CocoaPods](#cocoapods) - - [Swift Package Manager](#swift-package-manager) + - [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) + - [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) @@ -22,8 +27,11 @@ - [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) @@ -32,44 +40,86 @@ - [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) - [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) - - [Date and Time Functions](#date-and-time-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 4 (and -> [Xcode 9](https://developer.apple.com/xcode/downloads/)) or greater. +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. It’s integrated with the Swift build system to automate the +process of downloading, compiling, and linking dependencies. + + 1. Add the following to your `Package.swift` file: + + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ] + ``` + + 2. Build your project: + + ```sh + $ swift build + ``` +[Swift Package Manager]: https://swift.org/package-manager ### Carthage @@ -80,7 +130,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.11.4 + github "stephencelis/SQLite.swift" ~> 0.15.4 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. @@ -96,7 +146,7 @@ install SQLite.swift with Carthage: [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: 1. Make sure CocoaPods is [installed][CocoaPods Installation] (SQLite.swift - requires version 1.0.0 or greater). + requires version 1.6.1 or greater). ```sh # Using the default Ruby install will require you to use sudo when @@ -110,7 +160,7 @@ install SQLite.swift with Carthage: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.11.4' + pod 'SQLite.swift', '~> 0.15.4' end ``` @@ -124,7 +174,7 @@ with the OS you can require the `standalone` subspec: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.11.4' + pod 'SQLite.swift/standalone', '~> 0.15.4' end ``` @@ -134,7 +184,7 @@ dependency to sqlite3 or one of its subspecs: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.11.4' + pod 'SQLite.swift/standalone', '~> 0.15.4' pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled end ``` @@ -144,11 +194,13 @@ 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: +`SQLCipher` subspec in your Podfile (SPM is not supported yet, see [#1084](https://github.com/stephencelis/SQLite.swift/issues/1084)): ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/SQLCipher', '~> 0.11.4' + # 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 ``` @@ -158,9 +210,16 @@ extend `Connection` with methods to change the database key: ```swift import SQLite -let db = try Connection("path/to/db.sqlite3") +let db = try Connection("path/to/encrypted.sqlite3") try db.key("secret") -try db.rekey("another 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 @@ -168,31 +227,6 @@ try db.rekey("another secret") [sqlite3pod]: https://github.com/clemensg/sqlite3pod [SQLCipher]: https://www.zetetic.net/sqlcipher/ -### Swift Package Manager - -The [Swift Package Manager][] is a tool for managing the distribution of -Swift code. It’s integrated with the Swift build system to automate the -process of downloading, compiling, and linking dependencies. - -It is the recommended approach for using SQLite.swift in OSX CLI -applications. - - 1. Add the following to your `Package.swift` file: - - ```swift - dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.11.4") - ] - ``` - - 2. Build your project: - - ```sh - $ swift build - ``` - -[Swift Package Manager]: https://swift.org/package-manager - ### Manual To install SQLite.swift as an Xcode sub-project: @@ -258,22 +292,41 @@ let path = NSSearchPathForDirectoriesInDomains( let db = try Connection("\(path)/db.sqlite3") ``` -On OS X, you can use your app’s **Application Support** directory: +If you have bundled it in your application, you can use FileManager to copy it to the Documents directory: ```swift +func copyDatabaseIfNeeded(sourcePath: String) -> Bool { + let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let destinationPath = documents + "/db.sqlite3" + let exists = FileManager.default.fileExists(atPath: destinationPath) + guard !exists else { return false } + do { + try FileManager.default.copyItem(atPath: sourcePath, toPath: destinationPath) + return true + } catch { + print("error during file copy: \(error)") + return false + } +} +``` + +On macOS, you can use your app’s **Application Support** directory: + + +```swift +// set the path corresponding to application support var path = NSSearchPathForDirectoriesInDomains( .applicationSupportDirectory, .userDomainMask, true ).first! + "/" + Bundle.main.bundleIdentifier! -// create parent directory iff it doesn’t exist -try FileManager.default.createDirectoryAtPath( - path, withIntermediateDirectories: true, attributes: nil +// create parent directory inside application support if it doesn’t exist +try FileManager.default.createDirectory( +atPath: path, withIntermediateDirectories: true, attributes: nil ) let db = try Connection("\(path)/db.sqlite3") ``` - #### Read-Only Databases If you bundle a database with your app (_i.e._, you’ve copied a database file @@ -281,7 +334,7 @@ into your Xcode project and added it to your application target), you can establish a _read-only_ connection to it. ```swift -let path = Bundle.main.pathForResource("db", ofType: "sqlite3")! +let path = Bundle.main.path(forResource: "db", ofType: "sqlite3")! let db = try Connection(path, readonly: true) ``` @@ -295,9 +348,16 @@ let db = try Connection(path, readonly: true) > See these two Stack Overflow questions for more information about iOS apps > with SQLite databases: [1](https://stackoverflow.com/questions/34609746/what-different-between-store-database-in-different-locations-in-ios), > [2](https://stackoverflow.com/questions/34614968/ios-how-to-copy-pre-seeded-database-at-the-first-running-app-with-sqlite-swift). -> We welcome sample code to show how to successfully copy and use a bundled "seed" +> 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 @@ -316,6 +376,16 @@ 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)])) +``` + +See [Uniform Resource Identifiers](https://www.sqlite.org/uri.html#recognized_query_parameters) for more details. #### Thread-Safety @@ -324,16 +394,15 @@ execution and can be safely accessed across threads. Threads that open transactions and savepoints will block other threads from executing statements while the transaction is open. -If you maintain multiple connections for a single database, consider setting a timeout (in seconds) and/or a busy handler: +If you maintain multiple connections for a single database, consider setting a timeout +(in seconds) *or* a busy handler. There can only be one active at a time, so setting a busy +handler will effectively override `busyTimeout`. ```swift -db.busyTimeout = 5 +db.busyTimeout = 5 // error after 5 seconds (does multiple retries) db.busyHandler({ tries in - if tries >= 3 { - return false - } - return true + tries < 3 // error after 3 tries }) ``` @@ -355,6 +424,9 @@ to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). | `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. @@ -638,6 +710,19 @@ do { } ``` +Multiple rows can be inserted at once by similarly calling `insertMany` with an array of +per-row [setters](#setters). + +```swift +do { + let lastRowid = try db.run(users.insertMany([mail <- "alice@mac.com"], [email <- "geoff@mac.com"])) + print("last inserted id: \(lastRowid)") +} catch { + print("insertion failed: \(error)") +} +``` + + The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions follow similar patterns. @@ -718,7 +803,7 @@ try db.transaction { | `<<=` | `Int -> Int` | | `>>=` | `Int -> Int` | | `&=` | `Int -> Int` | -| `||=` | `Int -> Int` | +| `\|\|=` | `Int -> Int` | | `^=` | `Int -> Int` | | `+=` | `String -> String` | @@ -769,6 +854,42 @@ for user in try db.prepare(users) { } ``` +Note that the iterator can throw *undeclared* database errors at any point during +iteration: + +```swift +let query = try db.prepare(users) +for user in query { + // 💥 can throw an error here +} +``` + +#### Failable iteration + +It is therefore recommended using the `RowIterator` API instead, +which has explicit error handling: + +```swift +// option 1: convert results into an Array of rows +let rowIterator = try db.prepareRowIterator(users) +for user in try Array(rowIterator) { + print("id: \(user[id]), email: \(user[email])") +} + +/// option 2: transform results using `map()` +let mapRowIterator = try db.prepareRowIterator(users) +let userIds = try mapRowIterator.map { $0[id] } + +/// option 3: handle each row individually with `failableNext()` +do { + while let row = try rowIterator.failableNext() { + // Handle row + } +} catch { + // Handle error +} +``` + ### Plucking Rows We can pluck the first row by passing a query to the `pluck` function on a @@ -954,9 +1075,11 @@ equate or compare different types will prevent compilation. | `<=` | `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` +> * When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` > accordingly. @@ -1019,6 +1142,65 @@ users.limit(5, offset: 5) ``` +#### Recursive and Hierarchical Queries + +We can perform a recursive or hierarchical query using a [query's](#queries) +[`WITH`](https://sqlite.org/lang_with.html) function. + +```swift +// Get the management chain for the manager with id == 8 + +let chain = Table("chain") +let id = Expression("id") +let managerId = Expression("manager_id") + +let query = managers + .where(id == 8) + .union(chain.join(managers, on: chain[managerId] == managers[id]) + +chain.with(chain, recursive: true, as: query) +// WITH RECURSIVE +// "chain" AS ( +// SELECT * FROM "managers" WHERE "id" = 8 +// UNION +// SELECT * from "chain" +// JOIN "managers" ON "chain"."manager_id" = "managers"."id" +// ) +// SELECT * FROM "chain" +``` + +Column names and a materialization hint can optionally be provided. + +```swift +// Add a "level" column to the query representing manager's position in the chain +let level = Expression("level") + +let queryWithLevel = + managers + .select(id, managerId, 0) + .where(id == 8) + .union( + chain + .select(managers[id], managers[manager_id], level + 1) + .join(managers, on: chain[managerId] == managers[id]) + ) + +chain.with(chain, + columns: [id, managerId, level], + recursive: true, + hint: .materialize, + as: queryWithLevel) +// WITH RECURSIVE +// "chain" ("id", "manager_id", "level") AS MATERIALIZED ( +// SELECT ("id", "manager_id", 0) FROM "managers" WHERE "id" = 8 +// UNION +// SELECT ("manager"."id", "manager"."manager_id", "level" + 1) FROM "chain" +// JOIN "managers" ON "chain"."manager_id" = "managers"."id" +// ) +// SELECT * FROM "chain" +``` + + #### Aggregation [Queries](#queries) come with a number of functions that quickly return @@ -1098,6 +1280,33 @@ let count = try db.scalar(users.filter(name != nil).count) > // 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 @@ -1196,23 +1405,71 @@ try db.transaction { > _Note:_ Transactions run in a serial queue. +## Querying the Schema + +We can obtain generic information about objects in the current schema with a `SchemaReader`: + +```swift +let schema = db.schema +``` + +To query the data: + +```swift +let indexes = try schema.objectDefinitions(type: .index) +let tables = try schema.objectDefinitions(type: .table) +let triggers = try schema.objectDefinitions(type: .trigger) +``` + +### Indexes and Columns + +Specialized methods are available to get more detailed information: + +```swift +let indexes = try schema.indexDefinitions("users") +let columns = try schema.columnDefinitions("users") + +for index in indexes { + print("\(index.name) columns:\(index.columns))") +} +for column in columns { + print("\(column.name) pk:\(column.primaryKey) nullable: \(column.nullable)") +} +``` ## Altering the Schema SQLite.swift comes with several functions (in addition to `Table.create`) for altering a database schema in a type-safe manner. - ### Renaming Tables We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` function on a `Table` or `VirtualTable`. ```swift -try db.run(users.rename(Table("users_old")) +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 @@ -1226,7 +1483,6 @@ try db.run(users.addColumn(suffix)) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT ``` - #### Added Column Constraints The `addColumn` function shares several of the same [`column` function @@ -1278,6 +1534,66 @@ tables](#creating-a-table). // 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 @@ -1334,44 +1650,13 @@ try db.run(users.dropIndex(email, ifExists: true)) // DROP INDEX IF EXISTS "index_users_on_email" ``` - -### 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" -``` - - ### Migrations and Schema Versioning -You can add a convenience property on `Connection` to query and set the +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. - -```swift -extension Connection { - public var userVersion: Int32 { - get { return Int32(try! scalar("PRAGMA user_version") as! Int64)} - set { try! run("PRAGMA user_version = \(newValue)") } - } -} -``` - -Then you can conditionally run your migrations along the lines of: +You can conditionally run your migrations along the lines of: ```swift if db.userVersion == 0 { @@ -1557,7 +1842,7 @@ Both of the above methods also have the following optional parameter: 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 + - 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. @@ -1586,8 +1871,8 @@ arithmetic, bitwise operations, and concatenation. | `<<` | `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)`. @@ -1619,6 +1904,11 @@ 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) @@ -1701,8 +1991,29 @@ let stmt = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ? for row in stmt.bind(kUTTypeImage) { /* ... */ } ``` +> _Note:_ Prepared queries can be reused, and long lived prepared queries should be `reset()` after each use. Otherwise, the transaction (either [implicit or explicit](https://www.sqlite.org/lang_transaction.html#implicit_versus_explicit_transactions)) will be held open until the query is reset or finalized. This can affect performance. Statements are reset automatically during `deinit`. +> +> ```swift +> someObj.statement = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)") +> for row in someObj.statement.bind(kUTTypeImage) { /* ... */ } +> someObj.statement.reset() +> ``` + [UTTypeConformsTo]: https://developer.apple.com/documentation/coreservices/1444079-uttypeconformsto +## Custom Aggregations + +We can create custom aggregation functions by calling `createAggregation`: + +```swift +let reduce: (String, [Binding?]) -> String = { (last, bindings) in + last + " " + (bindings.first as? String ?? "") +} + +db.createAggregation("customConcat", initialValue: "", reduce: reduce, result: { $0 }) +let result = db.prepare("SELECT customConcat(email) FROM users").scalar() as! String +``` + ## Custom Collations We can create custom collating sequences by calling `createCollation` on a @@ -1792,15 +2103,13 @@ let config = FTS5Config() .column(subject) .column(body, [.unindexed]) -try db.run(emails.create(.FTS5(config)) +try db.run(emails.create(.FTS5(config))) // CREATE VIRTUAL TABLE "emails" USING fts5("subject", "body" UNINDEXED) // Note that FTS5 uses a different syntax to select columns, so we need to rewrite // the last FTS4 query above as: -let replies = emails.filter(emails.match("subject:\"Re:\"*)) +let replies = emails.filter(emails.match("subject:\"Re:\"*")) // SELECT * FROM "emails" WHERE "emails" MATCH 'subject:"Re:"*' - -// https://www.sqlite.org/fts5.html#_changes_to_select_statements_ ``` ## Executing Arbitrary SQL @@ -1854,8 +2163,8 @@ using the following functions. ```swift let stmt = try db.prepare("SELECT id, email FROM users") for row in stmt { - for (index, name) in stmt.columnNames.enumerate() { - print ("\(name)=\(row[index]!)") + for (index, name) in stmt.columnNames.enumerated() { + print ("\(name):\(row[index]!)") // id: Optional(1), email: Optional("alice@mac.com") } } @@ -1885,6 +2194,45 @@ using the following functions. 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 @@ -1896,6 +2244,14 @@ We can log SQL using the database’s `trace` function. #endif ``` +## Vacuum + +To run the [vacuum](https://www.sqlite.org/lang_vacuum.html) command: + +```swift +try db.vacuum() +``` + [ROWID]: https://sqlite.org/lang_createtable.html#rowid [SQLiteMigrationManager.swift]: https://github.com/garriguv/SQLiteMigrationManager.swift diff --git a/Documentation/Linux.md b/Documentation/Linux.md new file mode 100644 index 00000000..0c88ff5c --- /dev/null +++ b/Documentation/Linux.md @@ -0,0 +1,29 @@ +# Linux + +## Limitations + +* Custom functions/aggregations are currently not supported and crash, caused by a bug in Swift. +See [#1071](https://github.com/stephencelis/SQLite.swift/issues/1071). + +## Debugging + +### Create and launch docker container + +```shell +$ docker container create swift:focal +$ docker run --cap-add=SYS_PTRACE \ + --security-opt seccomp=unconfined \ + --security-opt apparmor=unconfined \ + -i -t swift:focal bash +``` + +### Compile and run tests in debugger + +```shell +$ apt-get update && apt-get install libsqlite3-dev +$ git clone https://github.com/stephencelis/SQLite.swift.git +$ swift test +$ lldb .build/x86_64-unknown-linux-gnu/debug/SQLite.swiftPackageTests.xctest +(lldb) target create ".build/x86_64-unknown-linux-gnu/debug/SQLite.swiftPackageTests.xctest" +(lldb) run +``` diff --git a/Documentation/Planning.md b/Documentation/Planning.md index 8a3d5a11..cdfca9c4 100644 --- a/Documentation/Planning.md +++ b/Documentation/Planning.md @@ -6,7 +6,7 @@ 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.12.0 milestone](https://github.com/stephencelis/SQLite.swift/issues?q=is%3Aopen+is%3Aissue+milestone%3A0.12.0) +> 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 @@ -21,8 +21,6 @@ be referred to when it comes time to add the corresponding feature._ ### Features - * encapsulate ATTACH DATABASE / DETACH DATABASE as methods, per - [#30](https://github.com/stephencelis/SQLite.swift/issues/30) * provide separate threads for update vs read, so updates don't block reads, per [#236](https://github.com/stephencelis/SQLite.swift/issues/236) * expose triggers, per @@ -33,6 +31,3 @@ be referred to when it comes time to add the corresponding feature._ _Features that are not actively being considered, perhaps because of no clean type-safe way to implement them with the current Swift, or bugs, or just general uncertainty._ - - * provide a mechanism for INSERT INTO multiple values, per - [#168](https://github.com/stephencelis/SQLite.swift/issues/168) diff --git a/Documentation/Release.md b/Documentation/Release.md new file mode 100644 index 00000000..8ec67970 --- /dev/null +++ b/Documentation/Release.md @@ -0,0 +1,13 @@ +# SQLite.swift Release checklist + +* [ ] Make sure current master branch has a green build +* [ ] Make sure `SQLite.playground` runs without errors +* [ ] Make sure `CHANGELOG.md` is up-to-date +* [ ] Add content to `Documentation/Upgrading.md` if needed +* [ ] Update the version number in `SQLite.swift.podspec` +* [ ] Run `pod lib lint` locally +* [ ] Update the version numbers mentioned in `README.md`, `Documentation/Index.md` +* [ ] Update `MARKETING_VERSION` in `SQLite.xcodeproj/project.pbxproj` +* [ ] Create a tag with the version number (`x.y.z`) +* [ ] Publish to CocoaPods: `pod trunk push` +* [ ] Update the release information on GitHub diff --git a/Documentation/Resources/playground@2x.png b/Documentation/Resources/playground@2x.png index 32646d6a..da132718 100644 Binary files a/Documentation/Resources/playground@2x.png and b/Documentation/Resources/playground@2x.png differ diff --git a/Documentation/Upgrading.md b/Documentation/Upgrading.md new file mode 100644 index 00000000..0e12aacf --- /dev/null +++ b/Documentation/Upgrading.md @@ -0,0 +1,11 @@ +# Upgrading + +## 0.13 → 0.14 + +- `Expression.asSQL()` is no longer available. Expressions now implement `CustomStringConvertible`, + where `description` returns the SQL. +- `Statement.prepareRowIterator()` is no longer available. Instead, use the methods + of the same name on `Connection`. +- `Connection.registerTokenizer` is no longer available to register custom FTS4 tokenizers. +- `Setter.asSQL()` is no longer available. Instead, Setter now implement `CustomStringConvertible`, + where `description` returns the SQL. diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..2770e85d --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# https://github.com/CocoaPods/CocoaPods/pull/12816 +gem 'cocoapods', :git => 'https://github.com/jberkel/CocoaPods.git', branch: 'watchos-fourflusher' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..7dac2c18 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,122 @@ +GIT + remote: https://github.com/jberkel/CocoaPods.git + revision: 32a90c184bc5dc9ec8b7b9b8ad08e98b7253dec2 + branch: watchos-fourflusher + specs: + cocoapods (1.16.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.16.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (~> 4.1.0) + xcodeproj (>= 1.27.0, < 2.0) + +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.2.2.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) + claide (1.1.0) + cocoapods-core (1.16.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + drb (2.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.12.0) + logger (1.7.0) + minitest (5.25.5) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + netrc (0.11.0) + nkf (0.2.0) + public_suffix (4.0.7) + rexml (3.4.1) + ruby-macho (4.1.0) + securerandom (0.4.1) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + cocoapods! + +BUNDLED WITH + 2.6.9 diff --git a/Makefile b/Makefile index ebd38494..52a25a12 100644 --- a/Makefile +++ b/Makefile @@ -1,60 +1,76 @@ -BUILD_TOOL = xcodebuild +XCODEBUILD = xcodebuild BUILD_SCHEME = SQLite Mac -IOS_SIMULATOR = iPhone 6s -IOS_VERSION = 11.0 +IOS_SIMULATOR = iPhone 14 +IOS_VERSION = 16.4 + +# tool settings +SWIFTLINT_VERSION=0.52.2 +SWIFTLINT=bin/swiftlint-$(SWIFTLINT_VERSION) +SWIFTLINT_URL=https://github.com/realm/SwiftLint/releases/download/$(SWIFTLINT_VERSION)/portable_swiftlint.zip +XCBEAUTIFY_VERSION=0.20.0 +XCBEAUTIFY=bin/xcbeautify-$(XCBEAUTIFY_VERSION) +ifeq ($(shell uname), Linux) + XCBEAUTIFY_PLATFORM=x86_64-unknown-linux-gnu.tar.xz +else + XCBEAUTIFY_PLATFORM=universal-apple-macosx.zip +endif +XCBEAUTIFY_URL=https://github.com/tuist/xcbeautify/releases/download/$(XCBEAUTIFY_VERSION)/xcbeautify-$(XCBEAUTIFY_VERSION)-$(XCBEAUTIFY_PLATFORM) +CURL_OPTS=--fail --silent -L --retry 3 + ifeq ($(BUILD_SCHEME),SQLite iOS) BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -destination "platform=iOS Simulator,name=$(IOS_SIMULATOR),OS=$(IOS_VERSION)" else BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" endif -XCPRETTY := $(shell command -v xcpretty) -SWIFTCOV := $(shell command -v swiftcov) -GCOVR := $(shell command -v gcovr) -TEST_ACTIONS := clean build build-for-testing test-without-building +test: $(XCBEAUTIFY) + set -o pipefail; \ + $(XCODEBUILD) $(BUILD_ARGUMENTS) test | $(XCBEAUTIFY) -default: test +build: $(XCBEAUTIFY) + set -o pipefail; \ + $(XCODEBUILD) $(BUILD_ARGUMENTS) | $(XCBEAUTIFY) -build: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) +lint: $(SWIFTLINT) + $< --strict -test: -ifdef XCPRETTY - @set -o pipefail && $(BUILD_TOOL) $(BUILD_ARGUMENTS) $(TEST_ACTIONS) | $(XCPRETTY) -c -else - $(BUILD_TOOL) $(BUILD_ARGUMENTS) $(TEST_ACTIONS) -endif - -coverage: -ifdef SWIFTCOV - $(SWIFTCOV) generate --output coverage \ - $(BUILD_TOOL) $(BUILD_ARGUMENTS) -configuration Release test \ - -- ./SQLite/*.swift -ifdef GCOVR - $(GCOVR) \ - --root . \ - --use-gcov-files \ - --html \ - --html-details \ - --output coverage/index.html \ - --keep -else - @echo gcovr must be installed for HTML output: https://github.com/gcovr/gcovr -endif -else - @echo swiftcov must be installed for coverage: https://github.com/realm/SwiftCov - @exit 1 -endif +lint-fix: $(SWIFTLINT) + $< --fix clean: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) clean - rm -r coverage + $(XCODEBUILD) $(BUILD_ARGUMENTS) clean repl: - @$(BUILD_TOOL) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ - swift -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' + @$(XCODEBUILD) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ + swift repl -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' sloc: - @zsh -c "grep -vE '^ *//|^$$' SQLite/*/*.{swift,h,m} | wc -l" + @zsh -c "grep -vE '^ *//|^$$' Sources/**/*.{swift,h} | wc -l" + +$(SWIFTLINT): + set -e ; \ + curl $(CURL_OPTS) $(SWIFTLINT_URL) -o swiftlint.zip; \ + unzip -o swiftlint.zip swiftlint; \ + mkdir -p bin; \ + mv swiftlint $@ && rm -f swiftlint.zip + +$(XCBEAUTIFY): + set -e; \ + FILE=$(XCBEAUTIFY_PLATFORM); \ + curl $(CURL_OPTS) $(XCBEAUTIFY_URL) -o $$FILE; \ + case "$${FILE#*.}" in \ + "zip") \ + unzip -o $$FILE xcbeautify; \ + ;; \ + "tar.xz") \ + tar -xvf $$FILE xcbeautify; \ + ;; \ + *) \ + echo "unknown extension $${FILE#*.}!"; \ + exit 1; \ + ;; \ + esac; \ + mkdir -p bin; \ + mv xcbeautify $@ && rm -f $$FILE; -.PHONY: test coverage clean repl sloc +.PHONY: test clean repl sloc diff --git a/Package.swift b/Package.swift index c008e482..56925d18 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,60 @@ -// swift-tools-version:4.0 +// 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", - products: [.library(name: "SQLite", targets: ["SQLite"])], - targets: [ - .target(name: "SQLite", dependencies: ["SQLiteObjc"]), - .target(name: "SQLiteObjc"), - .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests") + platforms: [ + .iOS(.v12), + .macOS(.v10_13), + .watchOS(.v4), + .tvOS(.v12), + .visionOS(.v1) ], - swiftLanguageVersions: [4] + products: [ + .library( + name: "SQLite", + targets: ["SQLite"] + ) + ], + dependencies: deps, + targets: targets + testTargets ) -#if os(Linux) - package.dependencies = [.package(url: "https://github.com/stephencelis/CSQLite.git", from: "0.0.3")] - package.targets = [ - .target(name: "SQLite", exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"]), - .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests", exclude: [ - "FTS4Tests.swift", - "FTS5Tests.swift" - ]) - ] -#endif +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 3900c0a3..a52b7ae5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SQLite.swift -[![Build Status][TravisBadge]][TravisLink] [![CocoaPods Version][CocoaPodsVersionBadge]][CocoaPodsVersionLink] [![Swift4 compatible][Swift4Badge]][Swift4Link] [![Platform][PlatformBadge]][PlatformLink] [![Carthage compatible][CartagheBadge]][CarthageLink] [![Join the chat at https://gitter.im/stephencelis/SQLite.swift][GitterBadge]][GitterLink] +![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][]. @@ -19,13 +19,16 @@ syntax _and_ intent. - [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](http://stackoverflow.com/questions/tagged/sqlite.swift), + [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 @@ -34,67 +37,82 @@ syntax _and_ intent. ```swift import SQLite -let db = try Connection("path/to/db.sqlite3") - -let users = Table("users") -let id = Expression("id") -let name = Expression("name") -let email = Expression("email") - -try db.run(users.create { t in - t.column(id, primaryKey: true) - t.column(name) - t.column(email, unique: true) -}) -// CREATE TABLE "users" ( -// "id" INTEGER PRIMARY KEY NOT NULL, -// "name" TEXT, -// "email" TEXT NOT NULL UNIQUE -// ) - -let insert = users.insert(name <- "Alice", email <- "alice@mac.com") -let rowid = try db.run(insert) -// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') - -for user in try db.prepare(users) { - print("id: \(user[id]), name: \(user[name]), email: \(user[email])") - // id: 1, name: Optional("Alice"), email: alice@mac.com +// Wrap everything in a do...catch to handle errors +do { + let db = try Connection("path/to/db.sqlite3") + + let users = Table("users") + let id = SQLite.Expression("id") + let name = SQLite.Expression("name") + let email = SQLite.Expression("email") + + try db.run(users.create { t in + t.column(id, primaryKey: true) + t.column(name) + t.column(email, unique: true) + }) + // CREATE TABLE "users" ( + // "id" INTEGER PRIMARY KEY NOT NULL, + // "name" TEXT, + // "email" TEXT NOT NULL UNIQUE + // ) + + let insert = users.insert(name <- "Alice", email <- "alice@mac.com") + let rowid = try db.run(insert) + // INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') + + for user in try db.prepare(users) { + print("id: \(user[id]), name: \(user[name]), email: \(user[email])") + // id: 1, name: Optional("Alice"), email: alice@mac.com + } + // SELECT * FROM "users" + + let alice = users.filter(id == rowid) + + try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) + // UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') + // WHERE ("id" = 1) + + try db.run(alice.delete()) + // DELETE FROM "users" WHERE ("id" = 1) + + try db.scalar(users.count) // 0 + // SELECT count(*) FROM "users" +} catch { + print (error) } -// SELECT * FROM "users" - -let alice = users.filter(id == rowid) - -try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) -// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') -// WHERE ("id" = 1) - -try db.run(alice.delete()) -// DELETE FROM "users" WHERE ("id" = 1) - -try db.scalar(users.count) // 0 -// SELECT count(*) FROM "users" ``` +Note that `Expression` should be written as `SQLite.Expression` to avoid +conflicts with the `SwiftUI.Expression` if you are using SwiftUI too. + SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C API. ```swift -let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") -for email in ["betty@icloud.com", "cathy@icloud.com"] { - try stmt.run(email) +// Wrap everything in a do...catch to handle errors +do { + // ... + + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") + for email in ["betty@icloud.com", "cathy@icloud.com"] { + try stmt.run(email) + } + + db.totalChanges // 3 + db.changes // 1 + db.lastInsertRowid // 3 + + for row in try db.prepare("SELECT id, email FROM users") { + print("id: \(row[0]), email: \(row[1])") + // id: Optional(2), email: Optional("betty@icloud.com") + // id: Optional(3), email: Optional("cathy@icloud.com") + } + + try db.scalar("SELECT count(*) FROM users") // 2 +} catch { + print (error) } - -db.totalChanges // 3 -db.changes // 1 -db.lastInsertRowid // 3 - -for row in try db.prepare("SELECT id, email FROM users") { - print("id: \(row[0]), email: \(row[1])") - // id: Optional(2), email: Optional("betty@icloud.com") - // id: Optional(3), email: Optional("cathy@icloud.com") -} - -try db.scalar("SELECT count(*) FROM users") // 2 ``` [Read the documentation][See Documentation] or explore more, @@ -102,17 +120,30 @@ interactively, from the Xcode project’s playground. ![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) -For a more comprehensive example, see -[this article][Create a Data Access Layer with SQLite.swift and Swift 2] -and the [companion repository][SQLiteDataAccessLayer2]. +## Installation +### Swift Package Manager -[Create a Data Access Layer with SQLite.swift and Swift 2]: http://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html -[SQLiteDataAccessLayer2]: https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. -## Installation +1. Add the following to your `Package.swift` file: + + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") + ] + ``` + +2. Build your project: + + ```sh + $ swift build + ``` + +See the [Tests/SPM](https://github.com/stephencelis/SQLite.swift/tree/master/Tests/SPM) folder for a small demo project which uses SPM. -> _Note:_ SQLite.swift requires Swift 4 (and [Xcode][] 9). +[Swift Package Manager]: https://swift.org/package-manager ### Carthage @@ -124,7 +155,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.11.4 + github "stephencelis/SQLite.swift" ~> 0.15.4 ``` 3. Run `carthage update` and @@ -141,8 +172,7 @@ install SQLite.swift with Carthage: [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: - 1. Make sure CocoaPods is [installed][CocoaPods Installation]. (SQLite.swift - requires version 1.0.0 or greater.) + 1. Make sure CocoaPods is [installed][CocoaPods Installation]. ```sh # Using the default Ruby install will require you to use sudo when @@ -156,7 +186,7 @@ SQLite.swift with CocoaPods: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.11.4' + pod 'SQLite.swift', '~> 0.15.0' end ``` @@ -165,27 +195,6 @@ SQLite.swift with CocoaPods: [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started -### Swift Package Manager - -The [Swift Package Manager][] is a tool for managing the distribution of -Swift code. - -1. Add the following to your `Package.swift` file: - - ```swift - dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.11.4") - ] - ``` - -2. Build your project: - - ```sh - $ swift build - ``` - -[Swift Package Manager]: https://swift.org/package-manager - ### Manual To install SQLite.swift as an Xcode sub-project: @@ -214,14 +223,12 @@ device: [Xcode]: https://developer.apple.com/xcode/downloads/ -[Submodule]: http://git-scm.com/book/en/Git-Tools-Submodules +[Submodule]: https://git-scm.com/book/en/Git-Tools-Submodules [download]: https://github.com/stephencelis/SQLite.swift/archive/master.zip ## Communication -[See the planning document] for a roadmap and existing feature requests. - [Read the contributing guidelines][]. The _TL;DR_ (but please; _R_): - Need **help** or have a **general question**? [Ask on Stack @@ -229,14 +236,13 @@ device: - Found a **bug** or have a **feature request**? [Open an issue][]. - Want to **contribute**? [Submit a pull request][]. -[See the planning document]: /Documentation/Planning.md [Read the contributing guidelines]: ./CONTRIBUTING.md#contributing -[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Ask on Stack Overflow]: https://stackoverflow.com/questions/tagged/sqlite.swift [Open an issue]: https://github.com/stephencelis/SQLite.swift/issues/new [Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork -## Author +## Original author - [Stephen Celis](mailto:stephen@stephencelis.com) ([@stephencelis](https://twitter.com/stephencelis)) @@ -254,30 +260,24 @@ 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) - [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]: http://www.sqlite.org +[SQLite3]: https://www.sqlite.org [SQLite.swift]: https://github.com/stephencelis/SQLite.swift -[TravisBadge]: https://img.shields.io/travis/stephencelis/SQLite.swift/master.svg?style=flat -[TravisLink]: https://travis-ci.org/stephencelis/SQLite.swift +[GitHubActionBadge]: https://img.shields.io/github/actions/workflow/status/stephencelis/SQLite.swift/build.yml?branch=master -[CocoaPodsVersionBadge]: https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png -[CocoaPodsVersionLink]: http://cocoadocs.org/docsets/SQLite.swift +[CocoaPodsVersionBadge]: https://img.shields.io/cocoapods/v/SQLite.swift.svg?style=flat +[CocoaPodsVersionLink]: https://cocoapods.org/pods/SQLite.swift -[PlatformBadge]: https://cocoapod-badges.herokuapp.com/p/SQLite.swift/badge.png -[PlatformLink]: http://cocoadocs.org/docsets/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 @@ -285,8 +285,8 @@ Looking for something else? Try another Swift wrapper (or [FMDB][]): [GitterBadge]: https://badges.gitter.im/stephencelis/SQLite.swift.svg [GitterLink]: https://gitter.im/stephencelis/SQLite.swift -[Swift4Badge]: https://img.shields.io/badge/swift-4-orange.svg?style=flat -[Swift4Link]: https://developer.apple.com/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 diff --git a/SQLite.playground/Contents.swift b/SQLite.playground/Contents.swift index 11e13139..d893e696 100644 --- a/SQLite.playground/Contents.swift +++ b/SQLite.playground/Contents.swift @@ -1,43 +1,132 @@ import SQLite -let db = try! Connection() +/// Create an in-memory database +let db = try Connection(.inMemory) +/// enable statement logging db.trace { print($0) } +/// define a "users" table with some fields let users = Table("users") let id = Expression("id") -let email = Expression("email") -let name = Expression("name") +let email = Expression("email") // non-null +let name = Expression("name") // nullable -try! db.run(users.create { t in +/// prepare the query +let statement = users.create { t in t.column(id, primaryKey: true) t.column(email, unique: true, check: email.like("%@%")) t.column(name) -}) +} + +/// …and run it +try db.run(statement) + +/// insert "alice" +let rowid = try db.run(users.insert(email <- "alice@mac.com")) + +/// insert multiple rows using `insertMany` +let lastRowid = try db.run(users.insertMany([ + [email <- "bob@mac.com"], + [email <- "mallory@evil.com"] +])) -let rowid = try! db.run(users.insert(email <- "alice@mac.com")) -let alice = users.filter(id == rowid) -for user in try! db.prepare(users) { +let query = try db.prepare(users) +for user in query { + print("id: \(user[id]), email: \(user[email])") +} + +// re-requery just rowid of Alice +let alice = try db.prepare(users.filter(id == rowid)) +for user in alice { print("id: \(user[id]), email: \(user[email])") } +/// using the `RowIterator` API +let rowIterator = try db.prepareRowIterator(users) +for user in try Array(rowIterator) { + print("id: \(user[id]), email: \(user[email])") +} + +/// also with `map()` +let mapRowIterator = try db.prepareRowIterator(users) + +let userIds = try mapRowIterator.map { $0[id] } + +/// using `failableNext()` on `RowIterator` +let iterator = try db.prepareRowIterator(users) +do { + while let row = try rowIterator.failableNext() { + print(row) + } +} catch { + // Handle error +} + +/// define a virtual table for the FTS index let emails = VirtualTable("emails") -let subject = Expression("subject") +let subject = Expression("subject") let body = Expression("body") -try! db.run(emails.create(.FTS4(subject, body))) +/// create the index +try db.run(emails.create(.FTS5( + FTS5Config() + .column(subject) + .column(body) +))) -try! db.run(emails.insert( +/// populate with data +try db.run(emails.insert( subject <- "Hello, world!", body <- "This is a hello world message." )) -let row = try! db.pluck(emails.match("hello")) +/// run a query +let ftsQuery = try db.prepare(emails.match("hello")) -let query = try! db.prepare(emails.match("hello")) -for row in query { +for row in ftsQuery { print(row[subject]) } + +/// custom aggregations +let reduce: (String, [Binding?]) -> String = { (last, bindings) in + last + " " + (bindings.first as? String ?? "") +} + +db.createAggregation("customConcat", + initialValue: "users:", + reduce: reduce, + result: { $0 }) +let result = try db.prepare("SELECT customConcat(email) FROM users").scalar() as! String +print(result) + +/// schema queries +let schema = db.schema +let objects = try schema.objectDefinitions() +print(objects) + +let columns = try schema.columnDefinitions(table: "users") +print(columns) + +/// schema alteration + +let schemaChanger = SchemaChanger(connection: db) +try schemaChanger.alter(table: "users") { table in + table.add(column: ColumnDefinition(name: "age", type: .INTEGER)) + table.rename(column: "email", to: "electronic_mail") + table.drop(column: "name") +} + +let changedColumns = try schema.columnDefinitions(table: "users") +print(changedColumns) + +let age = Expression("age") +let electronicMail = Expression("electronic_mail") + +let newRowid = try db.run(users.insert( + electronicMail <- "carol@mac.com", + age <- 33 +)) diff --git a/SQLite.playground/contents.xcplayground b/SQLite.playground/contents.xcplayground index fd676d5b..441c60ef 100644 --- a/SQLite.playground/contents.xcplayground +++ b/SQLite.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/SQLite.playground/playground.xcworkspace/contents.xcworkspacedata b/SQLite.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SQLite.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec index 4a329338..73c6f705 100644 --- a/SQLite.swift.podspec +++ b/SQLite.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SQLite.swift" - s.version = "0.11.4" - s.summary = "A type-safe, Swift-language layer over SQLite3 for iOS and OS X." + s.version = "0.15.4" + s.summary = "A type-safe, Swift-language layer over SQLite3." s.description = <<-DESC SQLite.swift provides compile-time confidence in SQL statement syntax and @@ -15,54 +15,63 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/stephencelis' s.module_name = 'SQLite' - s.ios.deployment_target = "8.0" - s.tvos.deployment_target = "9.1" - s.osx.deployment_target = "10.10" - s.watchos.deployment_target = "2.2" s.default_subspec = 'standard' - s.pod_target_xcconfig = { - 'SWIFT_VERSION' => '4.0', - } + 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,SQLiteObjc}/**/*.{c,h,m,swift}' + ss.source_files = 'Sources/SQLite/**/*.{c,h,m,swift}' ss.exclude_files = 'Sources/**/Cipher.swift' - ss.private_header_files = 'Sources/SQLiteObjc/*.h' ss.library = 'sqlite3' - + ss.resource_bundle = { 'SQLite.swift' => 'Sources/SQLite/PrivacyInfo.xcprivacy' } + ss.test_spec 'tests' do |test_spec| - test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + 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,SQLiteObjc}/**/*.{c,h,m,swift}' + ss.source_files = 'Sources/SQLite/**/*.{c,h,m,swift}' ss.exclude_files = 'Sources/**/Cipher.swift' - ss.private_header_files = 'Sources/SQLiteObjc/*.h' + ss.resource_bundle = { 'SQLite.swift' => 'Sources/SQLite/PrivacyInfo.xcprivacy' } ss.xcconfig = { - 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE' + '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/fixtures/*' + test_spec.resources = 'Tests/SQLiteTests/Resources/*' test_spec.source_files = 'Tests/SQLiteTests/*.swift' end end s.subspec 'SQLCipher' do |ss| - ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' - ss.private_header_files = 'Sources/SQLiteObjc/*.h' + # 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' + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1 SQLITE_SWIFT_SQLCIPHER=1' } - ss.dependency 'SQLCipher', '>= 3.4.0' - + ss.dependency 'SQLCipher', '>= 4.0.0' + ss.test_spec 'tests' do |test_spec| - test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.resources = 'Tests/SQLiteTests/Resources/*' test_spec.source_files = 'Tests/SQLiteTests/*.swift' end end diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index df603862..ec0423e9 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -7,15 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 02A43A9822738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + 02A43A9922738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + 02A43A9A22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + 02A43A9B22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; 03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; 03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; - 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E761C6BB2E60062603F /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; 03A65E771C6BB2E60062603F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; 03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; @@ -29,57 +30,135 @@ 03A65E841C6BB2FB0062603F /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; - 03A65E871C6BB3030062603F /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; - 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; - 03A65E891C6BB3030062603F /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; - 03A65E8A1C6BB3030062603F /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; - 03A65E8B1C6BB3030062603F /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; - 03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; - 03A65E8D1C6BB3030062603F /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; - 03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; - 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; - 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; - 03A65E911C6BB3030062603F /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; - 03A65E921C6BB3030062603F /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; - 03A65E931C6BB3030062603F /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; - 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; }; + 19A17018F250343BD0F9F4B0 /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + 19A17021286A4D8D6C2EF12D /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + 19A17026DCDCDA405B09A229 /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; + 19A17073552293CA063BEA66 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + 19A1708D3D58D7BC1168E55F /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A170ACC97B19730FB7BA4D /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; + 19A170AEBAA56DC3355A73B3 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */; }; + 19A170C56745F9D722A73D77 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */; }; + 19A170D938343E30119EDFB3 /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + 19A1714F7CF964D568AB14E0 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A2ED4E2640F197F48C /* BlobTests.swift */; }; 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A1716BF8E15F91A6B5CB7A /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; - 19A171967CC511C4F6F773C9 /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; - 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; + 19A17188B4D96636F9C0C209 /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; - 19A1720B67ED13E6150C6A3D /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; - 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; - 19A172EB202970561E5C4245 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.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 */; }; - 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; - 19A175DFF47B84757E547C62 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; - 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; - 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.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 */; }; - 19A1785195182AF8731A8BDA /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.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 */; }; - 19A179A0C45377CB09BB358C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.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 */; }; - 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; - 19A17C80076860CF7751A056 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.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 */; }; - 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 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 */; }; - 19A17FDA323BAFDEC627E76F /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 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 */; }; @@ -100,20 +179,126 @@ 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 */; }; - 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; - 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78B28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B79628846FCC005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79828846FED005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79928847055005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 64A8EE432B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE442B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE452B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64A8EE462B095FBB00F583F7 /* WindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */; }; + 64B8E1702B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 64B8E1712B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 64B8E1722B09748000545AFB /* WindowFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */; }; + 997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + 997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + 997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + 997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + DB58B21128FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21228FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21328FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21428FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DB58B21628FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB58B21728FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB58B21828FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB58B21928FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DB7C5DA628D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DB7C5DA728D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DB7C5DA828D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DB7C5DA928D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DBB93D5A2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DBB93D5B2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DBB93D5C2A22A373009BB96E /* SchemaReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB93D592A22A373009BB96E /* SchemaReaderTests.swift */; }; + DEB306BA2B61CEF500F9D46B /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DEB306BC2B61CEF500F9D46B /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + DEB306BD2B61CEF500F9D46B /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + DEB306BE2B61CEF500F9D46B /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + DEB306BF2B61CEF500F9D46B /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + DEB306C02B61CEF500F9D46B /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + DEB306C12B61CEF500F9D46B /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + DEB306C22B61CEF500F9D46B /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + DEB306C32B61CEF500F9D46B /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + DEB306C42B61CEF500F9D46B /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + DEB306C52B61CEF500F9D46B /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + DEB306C62B61CEF500F9D46B /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + DEB306C72B61CEF500F9D46B /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + DEB306C82B61CEF500F9D46B /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + DEB306C92B61CEF500F9D46B /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + DEB306CA2B61CEF500F9D46B /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + DEB306CB2B61CEF500F9D46B /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + DEB306CC2B61CEF500F9D46B /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + DEB306CD2B61CEF500F9D46B /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + DEB306CE2B61CEF500F9D46B /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + DEB306CF2B61CEF500F9D46B /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + DEB306D02B61CEF500F9D46B /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + DEB306D12B61CEF500F9D46B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + DEB306D22B61CEF500F9D46B /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; }; + DEB306D32B61CEF500F9D46B /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; + DEB306D42B61CEF500F9D46B /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + DEB306D52B61CEF500F9D46B /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; }; + DEB306D62B61CEF500F9D46B /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + DEB306D72B61CEF500F9D46B /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; }; + DEB306D82B61CEF500F9D46B /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; + DEB306D92B61CEF500F9D46B /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; }; + DEB306DA2B61CEF500F9D46B /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; + DEB306DB2B61CEF500F9D46B /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + DEB306DC2B61CEF500F9D46B /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + DEB306DD2B61CEF500F9D46B /* Connection+Pragmas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */; }; + DEB306DE2B61CEF500F9D46B /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; }; + DEB306E02B61CEF500F9D46B /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180931C46EA210038162A /* libsqlite3.tbd */; }; + DEB306EB2B61CF9500F9D46B /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; + DEB306EC2B61CF9500F9D46B /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + DEB306ED2B61CF9500F9D46B /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + DEB306EE2B61CF9500F9D46B /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; + DEB306EF2B61CF9500F9D46B /* SchemaChangerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17FDB0B0CFB8987906FD0 /* SchemaChangerTests.swift */; }; + DEB306F02B61CF9500F9D46B /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; + DEB306F12B61CF9500F9D46B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B96EBD42C878E609CDC /* FTS5Tests.swift */; }; + DEB306F22B61CF9500F9D46B /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BE1CC4AD4036BAB8EE0 /* FTSIntegrationTests.swift */; }; + DEB306F32B61CF9500F9D46B /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */; }; + DEB306F42B61CF9500F9D46B /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170F141BF21946D159083 /* ExpressionTests.swift */; }; + DEB306F52B61CF9500F9D46B /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + DEB306F62B61CF9500F9D46B /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + DEB306F72B61CF9500F9D46B /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + DEB306F82B61CF9500F9D46B /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A2ED4E2640F197F48C /* BlobTests.swift */; }; + DEB306F92B61CF9500F9D46B /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + DEB306FA2B61CF9500F9D46B /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; + DEB306FB2B61CF9500F9D46B /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */; }; + DEB306FC2B61CF9500F9D46B /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; + DEB306FD2B61CF9500F9D46B /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17475DCA068453F787613 /* OperatorsTests.swift */; }; + DEB306FE2B61CF9500F9D46B /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1745BE8623D8C6808DB3C /* ResultTests.swift */; }; + DEB306FF2B61CF9500F9D46B /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + DEB307002B61CF9500F9D46B /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A7714C6524093255C5 /* SchemaTests.swift */; }; + DEB307012B61CF9500F9D46B /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17EC0C43015063945D32E /* SelectTests.swift */; }; + DEB307022B61CF9500F9D46B /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17AE284BB1DF31D1B753E /* ValueTests.swift */; }; + DEB307032B61CF9500F9D46B /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */; }; + DEB307042B61CF9500F9D46B /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */; }; + DEB307052B61CF9500F9D46B /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */; }; + DEB307062B61CF9500F9D46B /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1709D5BDD2691BA160012 /* SetterTests.swift */; }; + DEB307072B61CF9500F9D46B /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + DEB307082B61CF9500F9D46B /* Connection+PragmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */; }; + DEB307092B61CF9500F9D46B /* Connection+AttachTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170C08525D3D27CB5F83C /* Connection+AttachTests.swift */; }; + DEB3070B2B61CF9500F9D46B /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; + DEB3070D2B61CF9500F9D46B /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + EAE5A0372B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; + EAE5A0382B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; + EAE5A0392B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; + EAE5A03A2B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */; }; EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; }; EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; EE247B081C3F06E900AE3E12 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; @@ -130,43 +315,13 @@ EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; EE247B171C3F127200AE3E12 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; - EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; - EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; - EE247B231C3F137700AE3E12 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; - EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; - EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; - EE247B271C3F137700AE3E12 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; - EE247B281C3F137700AE3E12 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; - EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; - EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; - EE247B2F1C3F141E00AE3E12 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; - EE247B301C3F141E00AE3E12 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; - EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; - EE247B341C3F142E00AE3E12 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; - EE247B351C3F142E00AE3E12 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; EE247B461C3F3ED000AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */; }; - EE247B531C3F3FC700AE3E12 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; - EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; - EE247B551C3F3FC700AE3E12 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; - EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; - EE247B571C3F3FC700AE3E12 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; - EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; - EE247B591C3F3FC700AE3E12 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; - EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; - EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; - EE247B5C1C3F3FC700AE3E12 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; - EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; - EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; - EE247B5F1C3F3FC700AE3E12 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; - EE247B601C3F3FC700AE3E12 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; EE247B611C3F3FC700AE3E12 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; @@ -180,8 +335,6 @@ EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; - EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */ @@ -194,6 +347,13 @@ remoteGlobalIDString = 03A65E591C6BB0F50062603F; remoteInfo = "SQLite tvOS"; }; + DEB307142B61D07F00F9D46B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEB306B82B61CEF500F9D46B; + remoteInfo = "SQLite visionOS"; + }; EE247ADF1C3F04ED00AE3E12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; @@ -211,23 +371,73 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02A43A9722738CF100FEC494 /* Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backup.swift; sourceTree = ""; }; 03A65E5A1C6BB0F50062603F /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 03A65E961C6BB3210062603F /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + 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 = ""; }; - 19A1721B8984686B9963B45D /* FTS5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tests.swift; sourceTree = ""; }; - 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctionTests.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 = ""; }; - 19A17399EA9E61235D5D77BF /* CipherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherTests.swift; sourceTree = ""; }; - 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowTests.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 = ""; }; - 19A17E2695737FAB5D6086E3 /* fixtures */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = folder; path = fixtures; 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 = ""; }; @@ -235,8 +445,6 @@ EE247AE41C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EE247AEE1C3F06E900AE3E12 /* Blob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blob.swift; sourceTree = ""; }; EE247AEF1C3F06E900AE3E12 /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; - EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fts3_tokenizer.h; path = ../../SQLiteObjc/fts3_tokenizer.h; sourceTree = ""; }; - EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SQLite-Bridging.m"; path = "../../SQLiteObjc/SQLite-Bridging.m"; sourceTree = ""; }; EE247AF21C3F06E900AE3E12 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Statement.swift; sourceTree = ""; }; EE247AF31C3F06E900AE3E12 /* Value.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Value.swift; sourceTree = ""; }; EE247AF51C3F06E900AE3E12 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = ""; }; @@ -253,32 +461,16 @@ EE247B011C3F06E900AE3E12 /* Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Schema.swift; sourceTree = ""; }; EE247B021C3F06E900AE3E12 /* Setter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Setter.swift; sourceTree = ""; }; EE247B161C3F127200AE3E12 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; - EE247B181C3F134A00AE3E12 /* SetterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetterTests.swift; sourceTree = ""; }; - EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctionsTests.swift; sourceTree = ""; }; - EE247B1B1C3F137700AE3E12 /* BlobTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobTests.swift; sourceTree = ""; }; - EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTests.swift; sourceTree = ""; }; - EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctionsTests.swift; sourceTree = ""; }; - EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctionsTests.swift; sourceTree = ""; }; - EE247B201C3F137700AE3E12 /* ExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionTests.swift; sourceTree = ""; }; - EE247B211C3F137700AE3E12 /* FTS4Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4Tests.swift; sourceTree = ""; }; - EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorsTests.swift; sourceTree = ""; }; - EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; - EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTreeTests.swift; sourceTree = ""; }; - EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTests.swift; sourceTree = ""; }; - EE247B321C3F142E00AE3E12 /* StatementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementTests.swift; sourceTree = ""; }; - EE247B331C3F142E00AE3E12 /* ValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueTests.swift; sourceTree = ""; }; EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests Mac.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; EE247B771C3F40D700AE3E12 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; - EE247B8C1C3F821200AE3E12 /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = ""; }; EE247B8D1C3F821200AE3E12 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; EE247B8F1C3F822500AE3E12 /* Index.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Index.md; sourceTree = ""; }; EE247B911C3F822500AE3E12 /* installation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "installation@2x.png"; sourceTree = ""; }; EE247B921C3F822600AE3E12 /* playground@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playground@2x.png"; sourceTree = ""; }; EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SQLite.swift.podspec; sourceTree = ""; }; - EE91808D1C46E5230038162A /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SQLite-Bridging.h"; path = "../../SQLiteObjc/include/SQLite-Bridging.h"; sourceTree = ""; }; - EE9180911C46E9D30038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + EE9180911C46E9D30038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; EE9180931C46EA210038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -307,6 +499,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DEB306DF2B61CEF500F9D46B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306E02B61CEF500F9D46B /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB3070A2B61CF9500F9D46B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB3070B2B61CF9500F9D46B /* SQLite.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; EE247ACF1C3F04ED00AE3E12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -342,6 +550,75 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 19A1792D261C689FC988A90A /* Schema */ = { + isa = PBXGroup; + children = ( + DB58B21028FB864300F8EEA4 /* SchemaReader.swift */, + 19A171B262DDE8718513CFDA /* SchemaChanger.swift */, + 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */, + 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */, + ); + path = Schema; + sourceTree = ""; + }; + 19A1798E3459573BEE50FA34 /* Core */ = { + isa = PBXGroup; + children = ( + 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 = ""; + }; + 19A17AECBF878B1DAE0AE3DD /* Typed */ = { + isa = PBXGroup; + children = ( + 19A170F141BF21946D159083 /* ExpressionTests.swift */, + 19A171ED017645C8B04DF9F2 /* QueryTests.swift */, + 19A178AEF32ABC8BF2993FB5 /* DateAndTimeFunctionTests.swift */, + 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */, + 19A17475DCA068453F787613 /* OperatorsTests.swift */, + 19A17EC0C43015063945D32E /* SelectTests.swift */, + 19A17CAE60446607E99C22A6 /* QueryIntegrationTests.swift */, + 19A17613F00A3F4D4A257A04 /* AggregateFunctionsTests.swift */, + 19A177EF5E2D91BA86DA4480 /* CustomAggregationTests.swift */, + 19A1709D5BDD2691BA160012 /* SetterTests.swift */, + 19A174FE5B47A97937A27276 /* RowTests.swift */, + 64B8E16F2B09748000545AFB /* WindowFunctionsTests.swift */, + ); + path = 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 = ( @@ -353,8 +630,10 @@ EE247AC91C3F04ED00AE3E12 = { isa = PBXGroup; children = ( + 3D3C3CCB26E5568800759140 /* SQLite.playground */, EE247AD51C3F04ED00AE3E12 /* SQLite */, EE247AE11C3F04ED00AE3E12 /* SQLiteTests */, + DEB307132B61D04500F9D46B /* SQLite visionOS.xctestplan */, EE247B8A1C3F81D000AE3E12 /* Metadata */, EE247AD41C3F04ED00AE3E12 /* Products */, 3D67B3E41DB2469200A4F4C6 /* Frameworks */, @@ -373,6 +652,8 @@ 03A65E5A1C6BB0F50062603F /* SQLite.framework */, 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */, A121AC451CA35C79005A31D1 /* SQLite.framework */, + DEB306E52B61CEF500F9D46B /* SQLite.framework */, + DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */, ); name = Products; sourceTree = ""; @@ -383,10 +664,12 @@ EE247AD61C3F04ED00AE3E12 /* SQLite.h */, EE247AF71C3F06E900AE3E12 /* Foundation.swift */, EE247AF81C3F06E900AE3E12 /* Helpers.swift */, + EAE5A0362B893C43007C7EA4 /* PrivacyInfo.xcprivacy */, EE247AD81C3F04ED00AE3E12 /* Info.plist */, EE247AED1C3F06E900AE3E12 /* Core */, EE247AF41C3F06E900AE3E12 /* Extensions */, EE247AF91C3F06E900AE3E12 /* Typed */, + 19A1792D261C689FC988A90A /* Schema */, ); name = SQLite; path = Sources/SQLite; @@ -395,29 +678,15 @@ EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { isa = PBXGroup; children = ( - 19A17E2695737FAB5D6086E3 /* fixtures */, - EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */, - EE247B1B1C3F137700AE3E12 /* BlobTests.swift */, - EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */, - EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */, - EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */, - EE247B201C3F137700AE3E12 /* ExpressionTests.swift */, - EE247B211C3F137700AE3E12 /* FTS4Tests.swift */, - EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */, - EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */, - EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */, - EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */, - EE247B181C3F134A00AE3E12 /* SetterTests.swift */, - EE247B321C3F142E00AE3E12 /* StatementTests.swift */, - EE247B331C3F142E00AE3E12 /* ValueTests.swift */, + 3DF7B79528846FCC005DD8CA /* Resources */, EE247B161C3F127200AE3E12 /* TestHelpers.swift */, EE247AE41C3F04ED00AE3E12 /* Info.plist */, - 19A1721B8984686B9963B45D /* FTS5Tests.swift */, 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */, - 19A17399EA9E61235D5D77BF /* CipherTests.swift */, 19A17B93B48B5560E6E51791 /* Fixtures.swift */, - 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */, - 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */, + 19A17B56FBA20E7245BC8AC0 /* Schema */, + 19A17E470E4492D287C0D12F /* Extensions */, + 19A1798E3459573BEE50FA34 /* Core */, + 19A17AECBF878B1DAE0AE3DD /* Typed */, ); name = SQLiteTests; path = Tests/SQLiteTests; @@ -426,14 +695,19 @@ EE247AED1C3F06E900AE3E12 /* Core */ = { isa = PBXGroup; children = ( - EE91808D1C46E5230038162A /* SQLite-Bridging.h */, EE247AEE1C3F06E900AE3E12 /* Blob.swift */, EE247AEF1C3F06E900AE3E12 /* Connection.swift */, - EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */, - EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */, EE247AF21C3F06E900AE3E12 /* Statement.swift */, EE247AF31C3F06E900AE3E12 /* Value.swift */, 19A1710E73A46D5AC721CDA9 /* Errors.swift */, + 02A43A9722738CF100FEC494 /* Backup.swift */, + 19A17E723300E5ED3771DCB5 /* Result.swift */, + 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */, + 3DF7B78728842972005DD8CA /* Connection+Attach.swift */, + 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */, + DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */, + DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */, + 19A17F285B767BFACD96714B /* Connection+Pragmas.swift */, ); path = Core; sourceTree = ""; @@ -441,10 +715,10 @@ EE247AF41C3F06E900AE3E12 /* Extensions */ = { isa = PBXGroup; children = ( + 19A178A39ACA9667A62663CC /* Cipher.swift */, EE247AF51C3F06E900AE3E12 /* FTS4.swift */, - EE247AF61C3F06E900AE3E12 /* RTree.swift */, 19A1730E4390C775C25677D1 /* FTS5.swift */, - 19A178A39ACA9667A62663CC /* Cipher.swift */, + EE247AF61C3F06E900AE3E12 /* RTree.swift */, ); path = Extensions; sourceTree = ""; @@ -453,12 +727,14 @@ isa = PBXGroup; children = ( EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */, + 64A8EE422B095FBB00F583F7 /* WindowFunctions.swift */, EE247AFB1C3F06E900AE3E12 /* Collation.swift */, EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */, EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */, EE247AFE1C3F06E900AE3E12 /* Expression.swift */, EE247AFF1C3F06E900AE3E12 /* Operators.swift */, EE247B001C3F06E900AE3E12 /* Query.swift */, + 997DF2AD287FC06D00F8DF95 /* Query+with.swift */, EE247B011C3F06E900AE3E12 /* Schema.swift */, EE247B021C3F06E900AE3E12 /* Setter.swift */, 49EB68C31F7B3CB400D89D40 /* Coding.swift */, @@ -471,9 +747,10 @@ isa = PBXGroup; children = ( EE247B771C3F40D700AE3E12 /* README.md */, + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */, EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, - EE247B8C1C3F821200AE3E12 /* .travis.yml */, + 3DFC0B862886C239001C8FC9 /* Package.swift */, EE247B8D1C3F821200AE3E12 /* Makefile */, EE9180931C46EA210038162A /* libsqlite3.tbd */, EE9180911C46E9D30038162A /* libsqlite3.tbd */, @@ -487,7 +764,10 @@ isa = PBXGroup; children = ( EE247B8F1C3F822500AE3E12 /* Index.md */, + 3DF7B79B2884C901005DD8CA /* Planning.md */, EE247B901C3F822500AE3E12 /* Resources */, + 19A17EA3A313F129011B3FA0 /* Release.md */, + 19A1794B7972D14330A65BBD /* Linux.md */, ); path = Documentation; sourceTree = ""; @@ -508,8 +788,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */, - 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */, 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -518,9 +796,15 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */, 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, - 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306B92B61CEF500F9D46B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306BA2B61CEF500F9D46B /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -528,8 +812,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */, - EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */, EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -538,9 +820,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */, EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, - EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -551,9 +831,9 @@ isa = PBXNativeTarget; buildConfigurationList = 03A65E6F1C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLite tvOS" */; buildPhases = ( + 03A65E571C6BB0F50062603F /* Headers */, 03A65E551C6BB0F50062603F /* Sources */, 03A65E561C6BB0F50062603F /* Frameworks */, - 03A65E571C6BB0F50062603F /* Headers */, 03A65E581C6BB0F50062603F /* Resources */, ); buildRules = ( @@ -587,9 +867,9 @@ isa = PBXNativeTarget; buildConfigurationList = A121AC4C1CA35C79005A31D1 /* Build configuration list for PBXNativeTarget "SQLite watchOS" */; buildPhases = ( + A121AC421CA35C79005A31D1 /* Headers */, A121AC401CA35C79005A31D1 /* Sources */, A121AC411CA35C79005A31D1 /* Frameworks */, - A121AC421CA35C79005A31D1 /* Headers */, A121AC431CA35C79005A31D1 /* Resources */, ); buildRules = ( @@ -601,13 +881,49 @@ productReference = A121AC451CA35C79005A31D1 /* SQLite.framework */; productType = "com.apple.product-type.framework"; }; + DEB306B82B61CEF500F9D46B /* SQLite visionOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEB306E22B61CEF500F9D46B /* Build configuration list for PBXNativeTarget "SQLite visionOS" */; + buildPhases = ( + DEB306B92B61CEF500F9D46B /* Headers */, + DEB306BB2B61CEF500F9D46B /* Sources */, + DEB306DF2B61CEF500F9D46B /* Frameworks */, + DEB306E12B61CEF500F9D46B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite visionOS"; + productName = SQLite; + productReference = DEB306E52B61CEF500F9D46B /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEB3070E2B61CF9500F9D46B /* Build configuration list for PBXNativeTarget "SQLiteTests visionOS" */; + buildPhases = ( + DEB306EA2B61CF9500F9D46B /* Sources */, + DEB3070A2B61CF9500F9D46B /* Frameworks */, + DEB3070C2B61CF9500F9D46B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEB307152B61D07F00F9D46B /* PBXTargetDependency */, + ); + name = "SQLiteTests visionOS"; + productName = "SQLite tvOSTests"; + productReference = DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; EE247AD21C3F04ED00AE3E12 /* SQLite iOS */ = { isa = PBXNativeTarget; buildConfigurationList = EE247AE71C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLite iOS" */; buildPhases = ( + EE247AD01C3F04ED00AE3E12 /* Headers */, EE247ACE1C3F04ED00AE3E12 /* Sources */, EE247ACF1C3F04ED00AE3E12 /* Frameworks */, - EE247AD01C3F04ED00AE3E12 /* Headers */, EE247AD11C3F04ED00AE3E12 /* Resources */, ); buildRules = ( @@ -641,9 +957,9 @@ isa = PBXNativeTarget; buildConfigurationList = EE247B511C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLite Mac" */; buildPhases = ( + EE247B391C3F3ED000AE3E12 /* Headers */, EE247B371C3F3ED000AE3E12 /* Sources */, EE247B381C3F3ED000AE3E12 /* Frameworks */, - EE247B391C3F3ED000AE3E12 /* Headers */, EE247B3A1C3F3ED000AE3E12 /* Resources */, ); buildRules = ( @@ -680,7 +996,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1250; TargetAttributes = { 03A65E591C6BB0F50062603F = { CreatedOnToolsVersion = 7.2; @@ -700,7 +1016,7 @@ }; EE247ADC1C3F04ED00AE3E12 = { CreatedOnToolsVersion = 7.2; - LastSwiftMigration = 0900; + LastSwiftMigration = 1020; }; EE247B3B1C3F3ED000AE3E12 = { CreatedOnToolsVersion = 7.2; @@ -714,10 +1030,11 @@ }; buildConfigurationList = EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = EE247AC91C3F04ED00AE3E12; productRefGroup = EE247AD41C3F04ED00AE3E12 /* Products */; @@ -731,6 +1048,8 @@ 03A65E591C6BB0F50062603F /* SQLite tvOS */, 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */, A121AC441CA35C79005A31D1 /* SQLite watchOS */, + DEB306B82B61CEF500F9D46B /* SQLite visionOS */, + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */, ); }; /* End PBXProject section */ @@ -740,6 +1059,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0392B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -747,21 +1067,38 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */, + 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; + }; EE247AD11C3F04ED00AE3E12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0372B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -769,7 +1106,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A17FDA323BAFDEC627E76F /* fixtures in Resources */, + 3DF7B79628846FCC005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -777,6 +1114,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0382B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -784,7 +1122,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A175DFF47B84757E547C62 /* fixtures in Resources */, + 3DF7B79928847055005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -799,14 +1137,16 @@ 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, - 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */, + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */, 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */, 03A65E821C6BB2FB0062603F /* Expression.swift in Sources */, 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */, 03A65E7F1C6BB2FB0062603F /* Collation.swift in Sources */, 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */, + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */, 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */, 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */, + 64A8EE452B095FBB00F583F7 /* WindowFunctions.swift in Sources */, 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */, 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */, 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */, @@ -816,8 +1156,19 @@ 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; }; @@ -825,27 +1176,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */, - 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */, - 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */, - 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */, - 03A65E8B1C6BB3030062603F /* CustomFunctionsTests.swift in Sources */, - 03A65E871C6BB3030062603F /* AggregateFunctionsTests.swift in Sources */, - 03A65E921C6BB3030062603F /* SetterTests.swift in Sources */, - 03A65E891C6BB3030062603F /* ConnectionTests.swift in Sources */, - 03A65E8A1C6BB3030062603F /* CoreFunctionsTests.swift in Sources */, - 03A65E931C6BB3030062603F /* StatementTests.swift in Sources */, - 03A65E911C6BB3030062603F /* SchemaTests.swift in Sources */, - 03A65E8D1C6BB3030062603F /* FTS4Tests.swift in Sources */, - 03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */, - 03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */, 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */, - 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */, 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */, - 19A179A0C45377CB09BB358C /* CipherTests.swift in Sources */, 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */, - 19A1785195182AF8731A8BDA /* RowTests.swift in Sources */, - 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.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; }; @@ -853,13 +1216,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */, + 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 */, @@ -868,14 +1235,102 @@ 3D67B3F11DB246D100A4F4C6 /* CustomFunctions.swift in Sources */, 3D67B3F21DB246D100A4F4C6 /* Expression.swift in Sources */, 3D67B3F31DB246D100A4F4C6 /* Operators.swift in Sources */, + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */, 3D67B3F41DB246D100A4F4C6 /* Query.swift in Sources */, 3D67B3F51DB246D100A4F4C6 /* Schema.swift in Sources */, 3D67B3F61DB246D100A4F4C6 /* Setter.swift in Sources */, 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */, 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */, 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */, + 02A43A9B22738CF100FEC494 /* Backup.swift in Sources */, 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */, 19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */, + 19A17DF8D4F13A20F5D2269E /* Result.swift in Sources */, + 64A8EE462B095FBB00F583F7 /* WindowFunctions.swift in Sources */, + 19A17DFE05ED8B1F7C45F7EE /* SchemaChanger.swift in Sources */, + 19A17D1BEABA610ABF003D67 /* SchemaDefinitions.swift in Sources */, + 19A17A33EA026C2E2CEBAF36 /* Connection+Schema.swift in Sources */, + 19A178A8B2A34FB6B565DEDA /* Connection+Pragmas.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306BB2B61CEF500F9D46B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306BC2B61CEF500F9D46B /* CoreFunctions.swift in Sources */, + DEB306BD2B61CEF500F9D46B /* Coding.swift in Sources */, + DEB306BE2B61CEF500F9D46B /* RTree.swift in Sources */, + DEB306BF2B61CEF500F9D46B /* Blob.swift in Sources */, + DEB306C02B61CEF500F9D46B /* URIQueryParameter.swift in Sources */, + DEB306C12B61CEF500F9D46B /* Foundation.swift in Sources */, + DEB306C22B61CEF500F9D46B /* Connection.swift in Sources */, + DEB306C32B61CEF500F9D46B /* Expression.swift in Sources */, + DEB306C42B61CEF500F9D46B /* Helpers.swift in Sources */, + DEB306C52B61CEF500F9D46B /* Collation.swift in Sources */, + DEB306C62B61CEF500F9D46B /* Setter.swift in Sources */, + DEB306C72B61CEF500F9D46B /* Connection+Attach.swift in Sources */, + DEB306C82B61CEF500F9D46B /* CustomFunctions.swift in Sources */, + DEB306C92B61CEF500F9D46B /* FTS4.swift in Sources */, + DEB306CA2B61CEF500F9D46B /* Value.swift in Sources */, + DEB306CB2B61CEF500F9D46B /* Operators.swift in Sources */, + DEB306CC2B61CEF500F9D46B /* Schema.swift in Sources */, + DEB306CD2B61CEF500F9D46B /* Query.swift in Sources */, + DEB306CE2B61CEF500F9D46B /* Statement.swift in Sources */, + DEB306CF2B61CEF500F9D46B /* AggregateFunctions.swift in Sources */, + DEB306D02B61CEF500F9D46B /* FTS5.swift in Sources */, + DEB306D12B61CEF500F9D46B /* Cipher.swift in Sources */, + DEB306D22B61CEF500F9D46B /* Backup.swift in Sources */, + DEB306D32B61CEF500F9D46B /* Errors.swift in Sources */, + DEB306D42B61CEF500F9D46B /* DateAndTimeFunctions.swift in Sources */, + DEB306D52B61CEF500F9D46B /* SQLiteVersion.swift in Sources */, + DEB306D62B61CEF500F9D46B /* Result.swift in Sources */, + DEB306D72B61CEF500F9D46B /* Query+with.swift in Sources */, + DEB306D82B61CEF500F9D46B /* Connection+Aggregation.swift in Sources */, + DEB306D92B61CEF500F9D46B /* SQLiteFeature.swift in Sources */, + DEB306DA2B61CEF500F9D46B /* SchemaChanger.swift in Sources */, + DEB306DB2B61CEF500F9D46B /* SchemaDefinitions.swift in Sources */, + DEB306DC2B61CEF500F9D46B /* Connection+Schema.swift in Sources */, + DEB306DD2B61CEF500F9D46B /* Connection+Pragmas.swift in Sources */, + DEB306DE2B61CEF500F9D46B /* SchemaReader.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306EA2B61CF9500F9D46B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB306EB2B61CF9500F9D46B /* TestHelpers.swift in Sources */, + DEB306EC2B61CF9500F9D46B /* FoundationTests.swift in Sources */, + DEB306ED2B61CF9500F9D46B /* Fixtures.swift in Sources */, + DEB306EE2B61CF9500F9D46B /* SchemaDefinitionsTests.swift in Sources */, + DEB306EF2B61CF9500F9D46B /* SchemaChangerTests.swift in Sources */, + DEB306F02B61CF9500F9D46B /* Connection+SchemaTests.swift in Sources */, + DEB306F12B61CF9500F9D46B /* FTS5Tests.swift in Sources */, + DEB306F22B61CF9500F9D46B /* FTSIntegrationTests.swift in Sources */, + DEB306F32B61CF9500F9D46B /* FTS4Tests.swift in Sources */, + DEB306F42B61CF9500F9D46B /* ExpressionTests.swift in Sources */, + DEB306F52B61CF9500F9D46B /* StatementTests.swift in Sources */, + DEB306F62B61CF9500F9D46B /* QueryTests.swift in Sources */, + DEB306F72B61CF9500F9D46B /* CipherTests.swift in Sources */, + DEB306F82B61CF9500F9D46B /* BlobTests.swift in Sources */, + DEB306F92B61CF9500F9D46B /* ConnectionTests.swift in Sources */, + DEB306FA2B61CF9500F9D46B /* CoreFunctionsTests.swift in Sources */, + DEB306FB2B61CF9500F9D46B /* DateAndTimeFunctionTests.swift in Sources */, + DEB306FC2B61CF9500F9D46B /* CustomFunctionsTests.swift in Sources */, + DEB306FD2B61CF9500F9D46B /* OperatorsTests.swift in Sources */, + DEB306FE2B61CF9500F9D46B /* ResultTests.swift in Sources */, + DEB306FF2B61CF9500F9D46B /* RTreeTests.swift in Sources */, + DEB307002B61CF9500F9D46B /* SchemaTests.swift in Sources */, + DEB307012B61CF9500F9D46B /* SelectTests.swift in Sources */, + DEB307022B61CF9500F9D46B /* ValueTests.swift in Sources */, + DEB307032B61CF9500F9D46B /* QueryIntegrationTests.swift in Sources */, + DEB307042B61CF9500F9D46B /* AggregateFunctionsTests.swift in Sources */, + DEB307052B61CF9500F9D46B /* CustomAggregationTests.swift in Sources */, + DEB307062B61CF9500F9D46B /* SetterTests.swift in Sources */, + DEB307072B61CF9500F9D46B /* RowTests.swift in Sources */, + DEB307082B61CF9500F9D46B /* Connection+PragmaTests.swift in Sources */, + DEB307092B61CF9500F9D46B /* Connection+AttachTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -887,25 +1342,38 @@ 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */, + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */, EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */, EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */, EE247B111C3F06E900AE3E12 /* Expression.swift in Sources */, EE247B0C1C3F06E900AE3E12 /* Helpers.swift in Sources */, EE247B0E1C3F06E900AE3E12 /* Collation.swift in Sources */, EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */, + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */, + 64A8EE432B095FBB00F583F7 /* WindowFunctions.swift in Sources */, EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */, EE247B081C3F06E900AE3E12 /* Value.swift in Sources */, EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */, EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */, EE247B131C3F06E900AE3E12 /* Query.swift in Sources */, - EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */, EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */, EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */, 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */, 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */, + 02A43A9822738CF100FEC494 /* Backup.swift in Sources */, 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */, 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */, + DB7C5DA628D7C9B6006395CF /* SQLiteVersion.swift in Sources */, + 19A173EFEF0B3BD0B3ED406C /* Result.swift in Sources */, + 997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */, + 19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */, + DB58B21628FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */, + 19A1773A335CAB9D0AE14E8E /* SchemaChanger.swift in Sources */, + 19A17BA13FD35F058787B7D3 /* SchemaDefinitions.swift in Sources */, + 19A174506543905D71BF0518 /* Connection+Schema.swift in Sources */, + 19A17018F250343BD0F9F4B0 /* Connection+Pragmas.swift in Sources */, + DB58B21128FB864300F8EEA4 /* SchemaReader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -913,27 +1381,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */, - EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */, - EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */, - EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */, EE247B171C3F127200AE3E12 /* TestHelpers.swift in Sources */, - EE247B281C3F137700AE3E12 /* ExpressionTests.swift in Sources */, - EE247B271C3F137700AE3E12 /* CustomFunctionsTests.swift in Sources */, - EE247B341C3F142E00AE3E12 /* StatementTests.swift in Sources */, - EE247B301C3F141E00AE3E12 /* RTreeTests.swift in Sources */, - EE247B231C3F137700AE3E12 /* BlobTests.swift in Sources */, - EE247B351C3F142E00AE3E12 /* ValueTests.swift in Sources */, - EE247B2F1C3F141E00AE3E12 /* QueryTests.swift in Sources */, - EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */, - EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */, - EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */, - 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */, 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */, - 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */, 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */, - 19A1720B67ED13E6150C6A3D /* RowTests.swift in Sources */, - 19A17C80076860CF7751A056 /* DateAndTimeFunctionTests.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; }; @@ -945,14 +1425,16 @@ 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, - EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */, + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */, EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */, EE247B711C3F3FEC00AE3E12 /* Expression.swift in Sources */, EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */, EE247B6E1C3F3FEC00AE3E12 /* Collation.swift in Sources */, EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */, + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */, EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */, + 64A8EE442B095FBB00F583F7 /* WindowFunctions.swift in Sources */, EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */, EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */, EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */, @@ -962,8 +1444,19 @@ 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; }; @@ -971,27 +1464,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */, - EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */, - EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */, - EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */, - EE247B591C3F3FC700AE3E12 /* FTS4Tests.swift in Sources */, - EE247B531C3F3FC700AE3E12 /* AggregateFunctionsTests.swift in Sources */, - EE247B5F1C3F3FC700AE3E12 /* StatementTests.swift in Sources */, - EE247B5C1C3F3FC700AE3E12 /* RTreeTests.swift in Sources */, - EE247B571C3F3FC700AE3E12 /* CustomFunctionsTests.swift in Sources */, - EE247B601C3F3FC700AE3E12 /* ValueTests.swift in Sources */, - EE247B551C3F3FC700AE3E12 /* ConnectionTests.swift in Sources */, EE247B611C3F3FC700AE3E12 /* TestHelpers.swift in Sources */, - EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */, - EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */, - EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */, - 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */, 19A178072B371489E6A1E839 /* FoundationTests.swift in Sources */, - 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */, 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */, - 19A171967CC511C4F6F773C9 /* RowTests.swift in Sources */, - 19A172EB202970561E5C4245 /* DateAndTimeFunctionTests.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; }; @@ -1003,6 +1508,11 @@ target = 03A65E591C6BB0F50062603F /* SQLite tvOS */; targetProxy = 03A65E651C6BB0F60062603F /* PBXContainerItemProxy */; }; + DEB307152B61D07F00F9D46B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEB306B82B61CEF500F9D46B /* SQLite visionOS */; + targetProxy = DEB307142B61D07F00F9D46B /* PBXContainerItemProxy */; + }; EE247AE01C3F04ED00AE3E12 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EE247AD21C3F04ED00AE3E12 /* SQLite iOS */; @@ -1025,6 +1535,7 @@ 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"; @@ -1032,9 +1543,8 @@ PRODUCT_NAME = SQLite; SDKROOT = appletvos; SKIP_INSTALL = YES; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; @@ -1047,6 +1557,7 @@ 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"; @@ -1054,37 +1565,36 @@ PRODUCT_NAME = SQLite; SDKROOT = appletvos; SKIP_INSTALL = YES; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; 03A65E6D1C6BB0F60062603F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - INFOPLIST_FILE = Tests/SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; 03A65E6E1C6BB0F60062603F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - INFOPLIST_FILE = Tests/SQLite/Info.plist; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; - TVOS_DEPLOYMENT_TARGET = 9.1; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -1098,6 +1608,7 @@ 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"; @@ -1105,10 +1616,9 @@ PRODUCT_NAME = SQLite; SDKROOT = watchos; SKIP_INSTALL = YES; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.2; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1122,6 +1632,7 @@ 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"; @@ -1129,10 +1640,84 @@ PRODUCT_NAME = SQLite; SDKROOT = watchos; SKIP_INSTALL = YES; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.2; + WATCHOS_DEPLOYMENT_TARGET = 4.0; + }; + name = Release; + }; + DEB306E32B61CEF500F9D46B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = xros; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Debug; + }; + DEB306E42B61CEF500F9D46B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = xros; + SKIP_INSTALL = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Release; + }; + DEB3070F2B61CF9500F9D46B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = xros; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Debug; + }; + DEB307102B61CF9500F9D46B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = xros; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; }; name = Release; }; @@ -1140,6 +1725,7 @@ 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; @@ -1148,14 +1734,17 @@ 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; @@ -1181,16 +1770,20 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MARKETING_VERSION = 0.15.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = ""; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1198,6 +1791,7 @@ 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; @@ -1206,14 +1800,17 @@ 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; @@ -1233,16 +1830,20 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MARKETING_VERSION = 0.15.4; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = ""; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; @@ -1256,16 +1857,16 @@ 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"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; @@ -1279,41 +1880,41 @@ 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"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; EE247AEB1C3F04ED00AE3E12 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; EE247AEC1C3F04ED00AE3E12 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; @@ -1328,17 +1929,17 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; 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"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_INCLUDE_PATHS = ""; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; @@ -1353,17 +1954,17 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; 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"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_INCLUDE_PATHS = ""; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; @@ -1372,14 +1973,14 @@ buildSettings = { 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.10; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Debug; }; @@ -1388,14 +1989,14 @@ buildSettings = { 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.10; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; }; name = Release; }; @@ -1429,6 +2030,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DEB306E22B61CEF500F9D46B /* Build configuration list for PBXNativeTarget "SQLite visionOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEB306E32B61CEF500F9D46B /* Debug */, + DEB306E42B61CEF500F9D46B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEB3070E2B61CF9500F9D46B /* Build configuration list for PBXNativeTarget "SQLiteTests visionOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEB3070F2B61CF9500F9D46B /* Debug */, + DEB307102B61CF9500F9D46B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SQLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme index f1fa216c..a0db21a2 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -40,23 +48,11 @@ - - - - - - - - + + + + @@ -40,23 +48,11 @@ - - - - - - - - + + + + @@ -40,23 +48,11 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme index 92cc9126..be1ef0ee 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite watchOS.xcscheme @@ -1,6 +1,6 @@ - - - - +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/Sources/SQLite/Core/Blob.swift b/Sources/SQLite/Core/Blob.swift index 2f5d2a14..a709fb42 100644 --- a/Sources/SQLite/Core/Blob.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -36,25 +36,21 @@ public struct Blob { } public func toHex() -> String { - return bytes.map { - ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) + bytes.map { + ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) }.joined(separator: "") } - } -extension Blob : CustomStringConvertible { - +extension Blob: CustomStringConvertible { public var description: String { - return "x'\(toHex())'" + "x'\(toHex())'" } - } -extension Blob : Equatable { - +extension Blob: Equatable { } public func ==(lhs: Blob, rhs: Blob) -> Bool { - return lhs.bytes == rhs.bytes + lhs.bytes == rhs.bytes } diff --git a/Sources/SQLite/Core/Connection+Aggregation.swift b/Sources/SQLite/Core/Connection+Aggregation.swift new file mode 100644 index 00000000..a1abb74a --- /dev/null +++ b/Sources/SQLite/Core/Connection+Aggregation.swift @@ -0,0 +1,155 @@ +import Foundation +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +extension Connection { + private typealias Aggregate = @convention(block) (Int, Context, Int32, Argv) -> Void + + /// Creates or redefines a custom SQL aggregate. + /// + /// - Parameters: + /// + /// - aggregate: The name of the aggregate to create or redefine. + /// + /// - argumentCount: The number of arguments that the aggregate takes. If + /// `nil`, the aggregate may take any number of arguments. + /// + /// Default: `nil` + /// + /// - deterministic: Whether or not the aggregate is deterministic (_i.e._ + /// the aggregate always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - step: A block of code to run for each row of an aggregation group. + /// The block is called with an array of raw SQL values mapped to the + /// aggregate’s parameters, and an UnsafeMutablePointer to a state + /// variable. + /// + /// - final: A block of code to run after each row of an aggregation group + /// is processed. The block is called with an UnsafeMutablePointer to a + /// state variable, and should return a raw SQL value (or nil). + /// + /// - state: A block of code to run to produce a fresh state variable for + /// each aggregation group. The block should return an + /// UnsafeMutablePointer to the fresh state variable. + public func createAggregation( + _ functionName: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + step: @escaping ([Binding?], UnsafeMutablePointer) -> Void, + final: @escaping (UnsafeMutablePointer) -> Binding?, + state: @escaping () -> UnsafeMutablePointer) { + + let argc = argumentCount.map { Int($0) } ?? -1 + let box: Aggregate = { (stepFlag: Int, context: Context, argc: Int32, argv: Argv) in + let nBytes = Int32(MemoryLayout>.size) + guard let aggregateContext = sqlite3_aggregate_context(context, nBytes) else { + fatalError("Could not get aggregate context") + } + let mutablePointer = aggregateContext.assumingMemoryBound(to: UnsafeMutableRawPointer.self) + if stepFlag > 0 { + let arguments = argv.getBindings(argc: argc) + if aggregateContext.assumingMemoryBound(to: Int64.self).pointee == 0 { + mutablePointer.pointee = UnsafeMutableRawPointer(mutating: state()) + } + step(arguments, mutablePointer.pointee.assumingMemoryBound(to: T.self)) + } else { + let result = final(mutablePointer.pointee.assumingMemoryBound(to: T.self)) + context.set(result: result) + } + } + + func xStep(context: Context, argc: Int32, value: Argv) { + unsafeBitCast(sqlite3_user_data(context), to: Aggregate.self)(1, context, argc, value) + } + + func xFinal(context: Context) { + unsafeBitCast(sqlite3_user_data(context), to: Aggregate.self)(0, context, 0, nil) + } + + let flags = SQLITE_UTF8 | (deterministic ? SQLITE_DETERMINISTIC : 0) + let resultCode = sqlite3_create_function_v2( + handle, + functionName, + Int32(argc), + flags, + /* pApp */ unsafeBitCast(box, to: UnsafeMutableRawPointer.self), + /* xFunc */ nil, xStep, xFinal, /* xDestroy */ nil + ) + if let result = Result(errorCode: resultCode, connection: self) { + fatalError("Error creating function: \(result)") + } + register(functionName, argc: argc, value: box) + } + + public func createAggregation( + _ aggregate: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + initialValue: T, + reduce: @escaping (T, [Binding?]) -> T, + result: @escaping (T) -> Binding? + ) { + let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, ptr) in + let pointer = ptr.pointee.assumingMemoryBound(to: T.self) + let current = Unmanaged.fromOpaque(pointer).takeRetainedValue() + let next = reduce(current, bindings) + ptr.pointee = Unmanaged.passRetained(next).toOpaque() + } + + let final: (UnsafeMutablePointer) -> Binding? = { ptr in + let pointer = ptr.pointee.assumingMemoryBound(to: T.self) + let obj = Unmanaged.fromOpaque(pointer).takeRetainedValue() + let value = result(obj) + ptr.deallocate() + return value + } + + let state: () -> UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = Unmanaged.passRetained(initialValue).toOpaque() + return pointer + } + + createAggregation(aggregate, step: step, final: final, state: state) + } + + public func createAggregation( + _ aggregate: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + initialValue: T, + reduce: @escaping (T, [Binding?]) -> T, + result: @escaping (T) -> Binding? + ) { + + let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, pointer) in + let current = pointer.pointee + let next = reduce(current, bindings) + pointer.pointee = next + } + + let final: (UnsafeMutablePointer) -> Binding? = { pointer in + let value = result(pointer.pointee) + pointer.deallocate() + return value + } + + let state: () -> UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: initialValue) + return pointer + } + + createAggregation(aggregate, step: step, final: final, state: state) + } + +} diff --git a/Sources/SQLite/Core/Connection+Attach.swift b/Sources/SQLite/Core/Connection+Attach.swift new file mode 100644 index 00000000..0c674ee6 --- /dev/null +++ b/Sources/SQLite/Core/Connection+Attach.swift @@ -0,0 +1,33 @@ +import Foundation +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite +#else +import SQLite3 +#endif + +extension Connection { + #if SQLITE_SWIFT_SQLCIPHER + /// See https://www.zetetic.net/sqlcipher/sqlcipher-api/#attach + public func attach(_ location: Location, as schemaName: String, key: String? = nil) throws { + if let key { + try run("ATTACH DATABASE ? AS ? KEY ?", location.description, schemaName, key) + } else { + try run("ATTACH DATABASE ? AS ?", location.description, schemaName) + } + } + #else + /// See https://www3.sqlite.org/lang_attach.html + public func attach(_ location: Location, as schemaName: String) throws { + try run("ATTACH DATABASE ? AS ?", location.description, schemaName) + } + #endif + + /// See https://www3.sqlite.org/lang_detach.html + public func detach(_ schemaName: String) throws { + try run("DETACH DATABASE ?", schemaName) + } +} diff --git a/Sources/SQLite/Core/Connection+Pragmas.swift b/Sources/SQLite/Core/Connection+Pragmas.swift new file mode 100644 index 00000000..2c4f0efb --- /dev/null +++ b/Sources/SQLite/Core/Connection+Pragmas.swift @@ -0,0 +1,51 @@ +import Foundation + +public typealias UserVersion = Int32 + +public extension Connection { + /// The user version of the database. + /// See SQLite [PRAGMA user_version](https://sqlite.org/pragma.html#pragma_user_version) + var userVersion: UserVersion? { + get { + (try? scalar("PRAGMA user_version") as? Int64)?.map(Int32.init) + } + set { + _ = try? run("PRAGMA user_version = \(newValue ?? 0)") + } + } + + /// The version of SQLite. + /// See SQLite [sqlite_version()](https://sqlite.org/lang_corefunc.html#sqlite_version) + var sqliteVersion: SQLiteVersion { + guard let version = (try? scalar("SELECT sqlite_version()")) as? String, + let splits = .some(version.split(separator: ".", maxSplits: 3)), splits.count == 3, + let major = Int(splits[0]), let minor = Int(splits[1]), let point = Int(splits[2]) else { + return .zero + } + return .init(major: major, minor: minor, point: point) + } + + // Changing the foreign_keys setting affects the execution of all statements prepared using the database + // connection, including those prepared before the setting was changed. + // + // https://sqlite.org/pragma.html#pragma_foreign_keys + var foreignKeys: Bool { + get { getBoolPragma("foreign_keys") } + set { setBoolPragma("foreign_keys", newValue) } + } + + var deferForeignKeys: Bool { + get { getBoolPragma("defer_foreign_keys") } + set { setBoolPragma("defer_foreign_keys", newValue) } + } + + private func getBoolPragma(_ key: String) -> Bool { + guard let binding = try? scalar("PRAGMA \(key)"), + let intBinding = binding as? Int64 else { return false } + return intBinding == 1 + } + + private func setBoolPragma(_ key: String, _ newValue: Bool) { + _ = try? run("PRAGMA \(key) = \(newValue ? "1" : "0")") + } +} diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift index 1bbf7f73..57521f83 100644 --- a/Sources/SQLite/Core/Connection.swift +++ b/Sources/SQLite/Core/Connection.swift @@ -28,8 +28,8 @@ import Dispatch import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif @@ -55,7 +55,8 @@ public final class Connection { /// See: /// /// - Parameter filename: A URI filename - case uri(String) + /// - Parameter parameters: optional query parameters + case uri(String, parameters: [URIQueryParameter] = []) } /// An SQL operation passed to update callbacks. @@ -70,7 +71,7 @@ public final class Connection { /// A DELETE operation. case delete - fileprivate init(rawValue:Int32) { + fileprivate init(rawValue: Int32) { switch rawValue { case SQLITE_INSERT: self = .insert @@ -84,9 +85,9 @@ public final class Connection { } } - public var handle: OpaquePointer { return _handle! } + public var handle: OpaquePointer { _handle! } - fileprivate var _handle: OpaquePointer? = nil + fileprivate var _handle: OpaquePointer? /// Initializes a new SQLite connection. /// @@ -103,8 +104,11 @@ public final class Connection { /// /// - Returns: A new database connection. public init(_ location: Location = .inMemory, readonly: Bool = false) throws { - let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE - try check(sqlite3_open_v2(location.description, &_handle, flags | SQLITE_OPEN_FULLMUTEX, nil)) + 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) } @@ -133,23 +137,30 @@ public final class Connection { // MARK: - /// Whether or not the database was opened in a read-only state. - public var readonly: Bool { return sqlite3_db_readonly(handle, nil) == 1 } + public var readonly: Bool { sqlite3_db_readonly(handle, nil) == 1 } /// The last rowid inserted into the database via this connection. public var lastInsertRowid: Int64 { - return sqlite3_last_insert_rowid(handle) + 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 { - return Int(sqlite3_changes(handle)) + 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)) + 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 @@ -161,7 +172,7 @@ public final class Connection { /// /// - Throws: `Result.Error` if query execution fails. public func execute(_ SQL: String) throws { - _ = try sync { try self.check(sqlite3_exec(self.handle, SQL, nil, nil, nil)) } + _ = try sync { try check(sqlite3_exec(handle, SQL, nil, nil, nil)) } } // MARK: - Prepare @@ -190,7 +201,7 @@ public final class Connection { /// /// - Returns: A prepared statement. public func prepare(_ statement: String, _ bindings: [Binding?]) throws -> Statement { - return try prepare(statement).bind(bindings) + try prepare(statement).bind(bindings) } /// Prepares a single SQL statement and binds parameters to it. @@ -203,7 +214,7 @@ public final class Connection { /// /// - Returns: A prepared statement. public func prepare(_ statement: String, _ bindings: [String: Binding?]) throws -> Statement { - return try prepare(statement).bind(bindings) + try prepare(statement).bind(bindings) } // MARK: - Run @@ -220,7 +231,7 @@ public final class Connection { /// /// - Returns: The statement. @discardableResult public func run(_ statement: String, _ bindings: Binding?...) throws -> Statement { - return try run(statement, bindings) + try run(statement, bindings) } /// Prepares, binds, and runs a single SQL statement. @@ -235,7 +246,7 @@ public final class Connection { /// /// - Returns: The statement. @discardableResult public func run(_ statement: String, _ bindings: [Binding?]) throws -> Statement { - return try prepare(statement).run(bindings) + try prepare(statement).run(bindings) } /// Prepares, binds, and runs a single SQL statement. @@ -250,7 +261,18 @@ public final class Connection { /// /// - Returns: The statement. @discardableResult public func run(_ statement: String, _ bindings: [String: Binding?]) throws -> Statement { - return try prepare(statement).run(bindings) + 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 @@ -266,7 +288,7 @@ public final class Connection { /// /// - Returns: The first value of the first row returned. public func scalar(_ statement: String, _ bindings: Binding?...) throws -> Binding? { - return try scalar(statement, bindings) + try scalar(statement, bindings) } /// Runs a single SQL statement (with optional parameter bindings), @@ -280,7 +302,7 @@ public final class Connection { /// /// - Returns: The first value of the first row returned. public func scalar(_ statement: String, _ bindings: [Binding?]) throws -> Binding? { - return try prepare(statement).scalar(bindings) + try prepare(statement).scalar(bindings) } /// Runs a single SQL statement (with optional parameter bindings), @@ -294,13 +316,13 @@ public final class Connection { /// /// - Returns: The first value of the first row returned. public func scalar(_ statement: String, _ bindings: [String: Binding?]) throws -> Binding? { - return try prepare(statement).scalar(bindings) + try prepare(statement).scalar(bindings) } // MARK: - Transactions /// The mode in which a transaction acquires a lock. - public enum TransactionMode : String { + public enum TransactionMode: String { /// Defers locking the database till the first read/write executes. case deferred = "DEFERRED" @@ -392,7 +414,7 @@ public final class Connection { /// times it’s been called for this lock. If it returns `true`, it will /// try again. If it returns `false`, no further attempts will be made. public func busyHandler(_ callback: ((_ tries: Int) -> Bool)?) { - guard let callback = callback else { + guard let callback else { sqlite3_busy_handler(handle, nil, nil) busyHandler = nil return @@ -415,19 +437,19 @@ public final class Connection { /// /// db.trace { SQL in print(SQL) } public func trace(_ callback: ((String) -> Void)?) { - #if SQLITE_SWIFT_SQLCIPHER || os(Linux) + if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { + trace_v2(callback) + } else { trace_v1(callback) - #else - if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { - trace_v2(callback) - } else { - trace_v1(callback) - } - #endif + } } + @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 = callback else { + guard let callback else { sqlite3_trace(handle, nil /* xCallback */, nil /* pCtx */) trace = nil return @@ -435,11 +457,9 @@ public final class Connection { let box: Trace = { (pointer: UnsafeRawPointer) in callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) } - sqlite3_trace(handle, - { - (C: UnsafeMutableRawPointer?, SQL: UnsafePointer?) in - if let C = C, let SQL = SQL { - unsafeBitCast(C, to: Trace.self)(SQL) + sqlite3_trace(handle, { (context: UnsafeMutableRawPointer?, SQL: UnsafePointer?) in + if let context, let SQL { + unsafeBitCast(context, to: Trace.self)(SQL) } }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self) @@ -447,8 +467,35 @@ public final class Connection { 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? @@ -460,7 +507,7 @@ public final class Connection { /// `.Insert`, `.Update`, or `.Delete`), database name, table name, and /// rowid. public func updateHook(_ callback: ((_ operation: Operation, _ db: String, _ table: String, _ rowid: Int64) -> Void)?) { - guard let callback = callback else { + guard let callback else { sqlite3_update_hook(handle, nil, nil) updateHook = nil return @@ -488,7 +535,7 @@ public final class Connection { /// committed. If this callback throws, the transaction will be rolled /// back. public func commitHook(_ callback: (() throws -> Void)?) { - guard let callback = callback else { + guard let callback else { sqlite3_commit_hook(handle, nil, nil) commitHook = nil return @@ -515,7 +562,7 @@ public final class Connection { /// - Parameter callback: A callback invoked when a transaction is rolled /// back. public func rollbackHook(_ callback: (() -> Void)?) { - guard let callback = callback else { + guard let callback else { sqlite3_rollback_hook(handle, nil, nil) rollbackHook = nil return @@ -549,56 +596,42 @@ public final class Connection { /// - block: A block of code to run when the function is called. The block /// is called with an array of raw SQL values mapped to the function’s /// parameters and should return a raw SQL value (or nil). - public func createFunction(_ function: String, argumentCount: UInt? = nil, deterministic: Bool = false, _ block: @escaping (_ args: [Binding?]) -> Binding?) { + 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, argc, argv in - let arguments: [Binding?] = (0..?) -> Void - fileprivate var functions = [String: [Int: Function]]() + + fileprivate typealias Function = @convention(block) (Context, Int32, Argv) -> Void + fileprivate var functions = [String: [Int: Any]]() /// Defines a new collating sequence. /// @@ -615,9 +648,9 @@ public final class Connection { 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 = lhs, let rhs = rhs { + 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") @@ -628,6 +661,30 @@ public final class Connection { 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 { @@ -654,15 +711,15 @@ public final class Connection { } -extension Connection : CustomStringConvertible { +extension Connection: CustomStringConvertible { public var description: String { - return String(cString: sqlite3_db_filename(handle, nil)) + String(cString: sqlite3_db_filename(handle, nil)) } } -extension Connection.Location : CustomStringConvertible { +extension Connection.Location: CustomStringConvertible { public var description: String { switch self { @@ -670,81 +727,61 @@ extension Connection.Location : CustomStringConvertible { return ":memory:" case .temporary: return "" - case .uri(let URI): - return URI + 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 } } } -public enum Result : Error { - - fileprivate static let successCodes: Set = [SQLITE_OK, SQLITE_ROW, SQLITE_DONE] - - /// Represents a SQLite specific [error code](https://sqlite.org/rescode.html) - /// - /// - message: English-language text that describes the error - /// - /// - code: SQLite [error code](https://sqlite.org/rescode.html#primary_result_code_list) - /// - /// - statement: the statement which produced the error - case error(message: String, code: Int32, statement: Statement?) - - init?(errorCode: Int32, connection: Connection, statement: Statement? = nil) { - guard !Result.successCodes.contains(errorCode) else { return nil } - - let message = String(cString: sqlite3_errmsg(connection.handle)) - self = .error(message: message, code: errorCode, statement: statement) - } - -} - -extension Result : CustomStringConvertible { - - public var description: String { - switch self { - case let .error(message, errorCode, statement): - if let statement = statement { - return "\(message) (\(statement)) (code: \(errorCode))" - } else { - return "\(message) (code: \(errorCode))" - } +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))") } } } -#if !SQLITE_SWIFT_SQLCIPHER && !os(Linux) -@available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) -extension Connection { - fileprivate func trace_v2(_ callback: ((String) -> Void)?) { - guard let callback = 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))) +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 index dc91d3d8..82d535b9 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -26,8 +26,8 @@ import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif @@ -35,7 +35,7 @@ import SQLite3 /// A single SQL statement. public final class Statement { - fileprivate var handle: OpaquePointer? = nil + fileprivate var handle: OpaquePointer? fileprivate let connection: Connection @@ -48,10 +48,10 @@ public final class Statement { sqlite3_finalize(handle) } - public lazy var columnCount: Int = Int(sqlite3_column_count(self.handle)) + public lazy var columnCount: Int = Int(sqlite3_column_count(handle)) - public lazy var columnNames: [String] = (0.. Statement { - return bind(values) + bind(values) } /// Binds a list of parameters to a statement. @@ -100,21 +100,24 @@ public final class Statement { } fileprivate func bind(_ value: Binding?, atIndex idx: Int) { - if value == nil { + switch value { + case .none: sqlite3_bind_null(handle, Int32(idx)) - } else if let value = value as? Blob { + case let value as Blob where value.bytes.count == 0: + sqlite3_bind_zeroblob(handle, Int32(idx), 0) + case let value as Blob: sqlite3_bind_blob(handle, Int32(idx), value.bytes, Int32(value.bytes.count), SQLITE_TRANSIENT) - } else if let value = value as? Double { + case let value as Double: sqlite3_bind_double(handle, Int32(idx), value) - } else if let value = value as? Int64 { + case let value as Int64: sqlite3_bind_int64(handle, Int32(idx), value) - } else if let value = value as? String { + case let value as String: sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) - } else if let value = value as? Int { + case let value as Int: self.bind(value.datatypeValue, atIndex: idx) - } else if let value = value as? Bool { + case let value as Bool: self.bind(value.datatypeValue, atIndex: idx) - } else if let value = value { + case .some(let value): fatalError("tried to bind unexpected value \(value)") } } @@ -140,7 +143,7 @@ public final class Statement { /// /// - Returns: The statement object (useful for chaining). @discardableResult public func run(_ bindings: [Binding?]) throws -> Statement { - return try bind(bindings).run() + try bind(bindings).run() } /// - Parameter bindings: A dictionary of named parameters to bind to the @@ -150,7 +153,7 @@ public final class Statement { /// /// - Returns: The statement object (useful for chaining). @discardableResult public func run(_ bindings: [String: Binding?]) throws -> Statement { - return try bind(bindings).run() + try bind(bindings).run() } /// - Parameter bindings: A list of parameters to bind to the statement. @@ -170,30 +173,33 @@ public final class Statement { /// /// - Returns: The first value of the first row returned. public func scalar(_ bindings: [Binding?]) throws -> Binding? { - return try bind(bindings).scalar() + 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? { - return try bind(bindings).scalar() + try bind(bindings).scalar() } public func step() throws -> Bool { - return try connection.sync { try self.connection.check(sqlite3_step(self.handle)) == SQLITE_ROW } + try connection.sync { try connection.check(sqlite3_step(handle)) == SQLITE_ROW } + } + + public func reset() { + reset(clearBindings: true) } - fileprivate func reset(clearBindings shouldClear: Bool = true) { + fileprivate func reset(clearBindings shouldClear: Bool) { sqlite3_reset(handle) - if (shouldClear) { sqlite3_clear_bindings(handle) } + if shouldClear { sqlite3_clear_bindings(handle) } } } -extension Statement : Sequence { +extension Statement: Sequence { public func makeIterator() -> Statement { reset(clearBindings: false) @@ -202,13 +208,14 @@ extension Statement : Sequence { } -public protocol FailableIterator : IteratorProtocol { +public protocol FailableIterator: IteratorProtocol { func failableNext() throws -> Self.Element? } extension FailableIterator { public func next() -> Element? { - return try! failableNext() + // swiftlint:disable:next force_try + try! failableNext() } } @@ -221,17 +228,32 @@ extension Array { } } -extension Statement : FailableIterator { +extension Statement: FailableIterator { public typealias Element = [Binding?] public func failableNext() throws -> [Binding?]? { - return try step() ? Array(row) : nil + try step() ? Array(row) : nil + } +} + +extension Statement { + func prepareRowIterator() -> RowIterator { + RowIterator(statement: self, columnNames: columnNameMap) + } + + var columnNameMap: [String: Int] { + var result = [String: Int]() + for (index, name) in self.columnNames.enumerated() { + result[name.quote()] = index + } + + return result } } -extension Statement : CustomStringConvertible { +extension Statement: CustomStringConvertible { public var description: String { - return String(cString: sqlite3_sql(handle)) + String(cString: sqlite3_sql(handle)) } } @@ -248,15 +270,15 @@ public struct Cursor { } public subscript(idx: Int) -> Double { - return sqlite3_column_double(handle, Int32(idx)) + sqlite3_column_double(handle, Int32(idx)) } public subscript(idx: Int) -> Int64 { - return sqlite3_column_int64(handle, Int32(idx)) + sqlite3_column_int64(handle, Int32(idx)) } public subscript(idx: Int) -> String { - return String(cString: UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) + String(cString: UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) } public subscript(idx: Int) -> Blob { @@ -273,17 +295,17 @@ public struct Cursor { // MARK: - public subscript(idx: Int) -> Bool { - return Bool.fromDatatypeValue(self[idx]) + Bool.fromDatatypeValue(self[idx]) } public subscript(idx: Int) -> Int { - return Int.fromDatatypeValue(self[idx]) + Int.fromDatatypeValue(self[idx]) } } /// Cursors provide direct access to a statement’s current row. -extension Cursor : Sequence { +extension Cursor: Sequence { public subscript(idx: Int) -> Binding? { switch sqlite3_column_type(handle, Int32(idx)) { @@ -305,8 +327,8 @@ extension Cursor : Sequence { public func makeIterator() -> AnyIterator { var idx = 0 return AnyIterator { - if idx >= self.columnCount { - return Optional.none + 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 index 608f0ce6..249a7728 100644 --- a/Sources/SQLite/Core/Value.swift +++ b/Sources/SQLite/Core/Value.swift @@ -29,104 +29,104 @@ /// protocol, instead. public protocol Binding {} -public protocol Number : Binding {} +public protocol Number: Binding {} -public protocol Value : Expressible { // extensions cannot have inheritance clauses +public protocol Value: Expressible { // extensions cannot have inheritance clauses associatedtype ValueType = Self - associatedtype Datatype : Binding + associatedtype Datatype: Binding static var declaredDatatype: String { get } - static func fromDatatypeValue(_ datatypeValue: Datatype) -> ValueType + static func fromDatatypeValue(_ datatypeValue: Datatype) throws -> ValueType var datatypeValue: Datatype { get } } -extension Double : Number, Value { +extension Double: Number, Value { public static let declaredDatatype = "REAL" public static func fromDatatypeValue(_ datatypeValue: Double) -> Double { - return datatypeValue + datatypeValue } public var datatypeValue: Double { - return self + self } } -extension Int64 : Number, Value { +extension Int64: Number, Value { public static let declaredDatatype = "INTEGER" public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int64 { - return datatypeValue + datatypeValue } public var datatypeValue: Int64 { - return self + self } } -extension String : Binding, Value { +extension String: Binding, Value { public static let declaredDatatype = "TEXT" public static func fromDatatypeValue(_ datatypeValue: String) -> String { - return datatypeValue + datatypeValue } public var datatypeValue: String { - return self + self } } -extension Blob : Binding, Value { +extension Blob: Binding, Value { public static let declaredDatatype = "BLOB" public static func fromDatatypeValue(_ datatypeValue: Blob) -> Blob { - return datatypeValue + datatypeValue } public var datatypeValue: Blob { - return self + self } } // MARK: - -extension Bool : Binding, Value { +extension Bool: Binding, Value { public static var declaredDatatype = Int64.declaredDatatype public static func fromDatatypeValue(_ datatypeValue: Int64) -> Bool { - return datatypeValue != 0 + datatypeValue != 0 } public var datatypeValue: Int64 { - return self ? 1 : 0 + self ? 1 : 0 } } -extension Int : Number, Value { +extension Int: Number, Value { public static var declaredDatatype = Int64.declaredDatatype public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int { - return Int(datatypeValue) + Int(datatypeValue) } public var datatypeValue: Int64 { - return Int64(self) + Int64(self) } } diff --git a/Sources/SQLite/Extensions/Cipher.swift b/Sources/SQLite/Extensions/Cipher.swift index 6c0d4657..03194ef1 100644 --- a/Sources/SQLite/Extensions/Cipher.swift +++ b/Sources/SQLite/Extensions/Cipher.swift @@ -1,11 +1,17 @@ #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(). /// @@ -19,6 +25,8 @@ extension Connection { /// 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) } @@ -27,12 +35,34 @@ extension Connection { 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) + } - /// Change the key on an open database. If the current database is not encrypted, this routine - /// will encrypt it. + /// 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) } @@ -41,9 +71,32 @@ extension Connection { 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) throws { + 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() } @@ -55,7 +108,7 @@ extension Connection { // 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;") + _ = try scalar("SELECT count(*) FROM sqlite_master;") } } #endif diff --git a/Sources/SQLite/Extensions/FTS4.swift b/Sources/SQLite/Extensions/FTS4.swift index 5ef84dd7..c02cfdc1 100644 --- a/Sources/SQLite/Extensions/FTS4.swift +++ b/Sources/SQLite/Extensions/FTS4.swift @@ -22,22 +22,18 @@ // THE SOFTWARE. // -#if SWIFT_PACKAGE -import SQLiteObjc -#endif - extension Module { public static func FTS4(_ column: Expressible, _ more: Expressible...) -> Module { - return FTS4([column] + more) + FTS4([column] + more) } public static func FTS4(_ columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module { - return FTS4(FTS4Config().columns(columns).tokenizer(tokenizer)) + FTS4(FTS4Config().columns(columns).tokenizer(tokenizer)) } public static func FTS4(_ config: FTS4Config) -> Module { - return Module(name: "fts4", arguments: config.arguments()) + Module(name: "fts4", arguments: config.arguments()) } } @@ -56,15 +52,15 @@ extension VirtualTable { /// - Returns: An expression appended with a `MATCH` query against the given /// pattern. public func match(_ pattern: String) -> Expression { - return "MATCH".infix(tableName(), pattern) + "MATCH".infix(tableName(), pattern) } public func match(_ pattern: Expression) -> Expression { - return "MATCH".infix(tableName(), pattern) + "MATCH".infix(tableName(), pattern) } public func match(_ pattern: Expression) -> Expression { - return "MATCH".infix(tableName(), pattern) + "MATCH".infix(tableName(), pattern) } /// Builds a copy of the query with a `WHERE … MATCH` clause. @@ -78,30 +74,32 @@ extension VirtualTable { /// /// - Returns: A query with the given `WHERE … MATCH` clause applied. public func match(_ pattern: String) -> QueryType { - return filter(match(pattern)) + filter(match(pattern)) } public func match(_ pattern: Expression) -> QueryType { - return filter(match(pattern)) + filter(match(pattern)) } public func match(_ pattern: Expression) -> QueryType { - return filter(match(pattern)) + 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 { + public static func Unicode61(removeDiacritics: Bool? = nil, + tokenchars: Set = [], + separators: Set = []) -> Tokenizer { var arguments = [String]() - if let removeDiacritics = removeDiacritics { - arguments.append("removeDiacritics=\(removeDiacritics ? 1 : 0)".quote()) + if let removeDiacritics { + arguments.append("remove_diacritics=\(removeDiacritics ? 1 : 0)".quote()) } if !tokenchars.isEmpty { @@ -117,8 +115,13 @@ public struct Tokenizer { 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 { - return Tokenizer(Tokenizer.moduleName.quote(), [name.quote()]) + Tokenizer(Tokenizer.moduleName.quote(), [name.quote()]) } public let name: String @@ -134,37 +137,14 @@ public struct Tokenizer { } -extension Tokenizer : CustomStringConvertible { +extension Tokenizer: CustomStringConvertible { public var description: String { - return ([name] + arguments).joined(separator: " ") + ([name] + arguments).joined(separator: " ") } } -extension Connection { - - public func registerTokenizer(_ submoduleName: String, next: @escaping (String) -> (String, Range)?) throws { - try check(_SQLiteRegisterTokenizer(handle, Tokenizer.moduleName, submoduleName) { ( - input: UnsafePointer, offset: UnsafeMutablePointer, length: UnsafeMutablePointer) in - let string = String(cString: input) - - guard let (token, range) = next(string) else { return nil } - - let view:String.UTF8View = string.utf8 - - if let from = range.lowerBound.samePosition(in: view), - let to = range.upperBound.samePosition(in: view) { - offset.pointee += Int32(string[string.startIndex.. Self { - self.columnDefinitions.append((column, options)) + columnDefinitions.append((column, options)) return self } @@ -194,47 +174,47 @@ open class FTSConfig { } /// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer) - open func tokenizer(_ tokenizer: Tokenizer?) -> Self { + @discardableResult open func tokenizer(_ tokenizer: Tokenizer?) -> Self { self.tokenizer = tokenizer return self } /// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6) - open func prefix(_ prefix: [Int]) -> Self { - self.prefixes += prefix + @discardableResult open func prefix(_ prefix: [Int]) -> Self { + prefixes += prefix return self } /// [The content= option](https://www.sqlite.org/fts3.html#section_6_2) - open func externalContent(_ schema: SchemaType) -> Self { - self.externalContentSchema = schema + @discardableResult open func externalContent(_ schema: SchemaType) -> Self { + externalContentSchema = schema return self } /// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1) - open func contentless() -> Self { - self.isContentless = true + @discardableResult open func contentless() -> Self { + isContentless = true return self } func formatColumnDefinitions() -> [Expressible] { - return columnDefinitions.map { $0.0 } + columnDefinitions.map { $0.0 } } func arguments() -> [Expressible] { - return options().arguments + options().arguments } func options() -> Options { var options = Options() options.append(formatColumnDefinitions()) - if let tokenizer = tokenizer { + if let tokenizer { options.append("tokenize", value: Expression(literal: tokenizer.description)) } - options.appendCommaSeparated("prefix", values:prefixes.sorted().map { String($0) }) + options.appendCommaSeparated("prefix", values: prefixes.sorted().map { String($0) }) if isContentless { options.append("content", value: "") - } else if let externalContentSchema = externalContentSchema { + } else if let externalContentSchema { options.append("content", value: externalContentSchema.tableName()) } return options @@ -256,46 +236,30 @@ open class FTSConfig { } } - @discardableResult mutating func append(_ key: String, value: CustomStringConvertible?) -> Options { - return append(key, value: value?.description) + @discardableResult mutating func append(_ key: String, value: String) -> Options { + append(key, value: Expression(value)) } - @discardableResult mutating func append(_ key: String, value: String?) -> Options { - return append(key, value: value.map { Expression($0) }) - } - - @discardableResult mutating func append(_ key: String, value: Expressible?) -> Options { - if let value = value { - arguments.append("=".join([Expression(literal: key), value])) - } + @discardableResult mutating func append(_ key: String, value: Expressible) -> Options { + arguments.append("=".join([Expression(literal: key), value])) return self } } } /// Configuration for the [FTS4](https://www.sqlite.org/fts3.html) extension. -open class FTS4Config : FTSConfig { +open class FTS4Config: FTSConfig { /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) - public enum MatchInfo : CustomStringConvertible { + public enum MatchInfo: String { case fts3 - public var description: String { - return "fts3" - } } /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) - public enum Order : CustomStringConvertible { + public enum Order: String { /// Data structures are optimized for returning results in ascending order by docid (default) case asc /// FTS4 stores its data in such a way as to optimize returning results in descending order by docid. case desc - - public var description: String { - switch self { - case .asc: return "asc" - case .desc: return "desc" - } - } } var compressFunction: String? @@ -308,31 +272,31 @@ open class FTS4Config : FTSConfig { } /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) - open func compress(_ functionName: String) -> Self { - self.compressFunction = functionName + @discardableResult open func compress(_ functionName: String) -> Self { + compressFunction = functionName return self } /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) - open func uncompress(_ functionName: String) -> Self { - self.uncompressFunction = functionName + @discardableResult open func uncompress(_ functionName: String) -> Self { + uncompressFunction = functionName return self } /// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3) - open func languageId(_ columnName: String) -> Self { - self.languageId = columnName + @discardableResult open func languageId(_ columnName: String) -> Self { + languageId = columnName return self } /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) - open func matchInfo(_ matchInfo: MatchInfo) -> Self { + @discardableResult open func matchInfo(_ matchInfo: MatchInfo) -> Self { self.matchInfo = matchInfo return self } /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) - open func order(_ order: Order) -> Self { + @discardableResult open func order(_ order: Order) -> Self { self.order = order return self } @@ -342,11 +306,21 @@ open class FTS4Config : FTSConfig { for (column, _) in (columnDefinitions.filter { $0.options.contains(.unindexed) }) { options.append("notindexed", value: column) } - options.append("languageid", value: languageId) - options.append("compress", value: compressFunction) - options.append("uncompress", value: uncompressFunction) - options.append("matchinfo", value: matchInfo) - options.append("order", value: order) + if let languageId { + options.append("languageid", value: languageId) + } + if let compressFunction { + options.append("compress", value: compressFunction) + } + if let uncompressFunction { + options.append("uncompress", value: uncompressFunction) + } + if let matchInfo { + options.append("matchinfo", value: matchInfo.rawValue) + } + if let order { + options.append("order", value: order.rawValue) + } return options } } diff --git a/Sources/SQLite/Extensions/FTS5.swift b/Sources/SQLite/Extensions/FTS5.swift index 763927ff..3e84e171 100644 --- a/Sources/SQLite/Extensions/FTS5.swift +++ b/Sources/SQLite/Extensions/FTS5.swift @@ -24,7 +24,7 @@ extension Module { public static func FTS5(_ config: FTS5Config) -> Module { - return Module(name: "fts5", arguments: config.arguments()) + Module(name: "fts5", arguments: config.arguments()) } } @@ -32,22 +32,14 @@ extension Module { /// /// **Note:** this is currently only applicable when using SQLite.swift together with a FTS5-enabled version /// of SQLite. -open class FTS5Config : FTSConfig { - public enum Detail : CustomStringConvertible { +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 - - public var description: String { - switch self { - case .full: return "full" - case .column: return "column" - case .none: return "none" - } - } } var detail: Detail? @@ -58,35 +50,39 @@ open class FTS5Config : FTSConfig { } /// [External Content Tables](https://www.sqlite.org/fts5.html#section_4_4_2) - open func contentRowId(_ column: Expressible) -> Self { - self.contentRowId = column + @discardableResult open func contentRowId(_ column: Expressible) -> Self { + contentRowId = column return self } /// [The Columnsize Option](https://www.sqlite.org/fts5.html#section_4_5) - open func columnSize(_ size: Int) -> Self { - self.columnSize = size + @discardableResult open func columnSize(_ size: Int) -> Self { + columnSize = size return self } /// [The Detail Option](https://www.sqlite.org/fts5.html#section_4_6) - open func detail(_ detail: Detail) -> Self { + @discardableResult open func detail(_ detail: Detail) -> Self { self.detail = detail return self } override func options() -> Options { var options = super.options() - options.append("content_rowid", value: contentRowId) - if let columnSize = columnSize { + if let contentRowId { + options.append("content_rowid", value: contentRowId) + } + if let columnSize { options.append("columnsize", value: Expression(value: columnSize)) } - options.append("detail", value: detail) + if let detail { + options.append("detail", value: detail.rawValue) + } return options } override func formatColumnDefinitions() -> [Expressible] { - return columnDefinitions.map { definition in + columnDefinitions.map { definition in if definition.options.contains(.unindexed) { return " ".join([definition.0, Expression(literal: "UNINDEXED")]) } else { diff --git a/Sources/SQLite/Extensions/RTree.swift b/Sources/SQLite/Extensions/RTree.swift index 4fc1a235..5ecdf78b 100644 --- a/Sources/SQLite/Extensions/RTree.swift +++ b/Sources/SQLite/Extensions/RTree.swift @@ -23,8 +23,9 @@ // extension Module { - - public static func RTree(_ primaryKey: Expression, _ pairs: (Expression, Expression)...) -> Module where T.Datatype == Int64, U.Datatype == Double { + public static func RTree(_ primaryKey: Expression, + _ pairs: (Expression, Expression)...) + -> Module where T.Datatype == Int64, U.Datatype == Double { var arguments: [Expressible] = [primaryKey] for pair in pairs { @@ -33,5 +34,4 @@ extension Module { return Module(name: "rtree", arguments: arguments) } - } diff --git a/Sources/SQLite/Foundation.swift b/Sources/SQLite/Foundation.swift index 5638bc5b..44a31736 100644 --- a/Sources/SQLite/Foundation.swift +++ b/Sources/SQLite/Foundation.swift @@ -24,36 +24,36 @@ import Foundation -extension Data : Value { +extension Data: Value { public static var declaredDatatype: String { - return Blob.declaredDatatype + Blob.declaredDatatype } public static func fromDatatypeValue(_ dataValue: Blob) -> Data { - return Data(bytes: dataValue.bytes) + Data(dataValue.bytes) } public var datatypeValue: Blob { - return withUnsafeBytes { (pointer: UnsafePointer) -> Blob in - return Blob(bytes: pointer, length: count) + withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> Blob in + Blob(bytes: pointer.baseAddress!, length: count) } } } -extension Date : Value { +extension Date: Value { public static var declaredDatatype: String { - return String.declaredDatatype + String.declaredDatatype } public static func fromDatatypeValue(_ stringValue: String) -> Date { - return dateFormatter.date(from: stringValue)! + dateFormatter.date(from: stringValue)! } public var datatypeValue: String { - return dateFormatter.string(from: self) + dateFormatter.string(from: self) } } @@ -68,3 +68,35 @@ public var dateFormatter: DateFormatter = { 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 index ac831667..e3c84589 100644 --- a/Sources/SQLite/Helpers.swift +++ b/Sources/SQLite/Helpers.swift @@ -26,8 +26,8 @@ import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif @@ -35,16 +35,17 @@ import SQLite3 public typealias Star = (Expression?, Expression?) -> Expression public func *(_: Expression?, _: Expression?) -> Expression { - return Expression(literal: "*") + Expression(literal: "*") } +// swiftlint:disable:next type_name public protocol _OptionalType { associatedtype WrappedType } -extension Optional : _OptionalType { +extension Optional: _OptionalType { public typealias WrappedType = Wrapped @@ -54,12 +55,17 @@ extension Optional : _OptionalType { let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) extension String { - func quote(_ mark: Character = "\"") -> String { - let escaped = reduce("") { string, character in - string + (character == mark ? "\(mark)\(mark)" : "\(character)") + var quoted = "" + quoted.append(mark) + for character in self { + quoted.append(character) + if character == mark { + quoted.append(character) + } } - return "\(mark)\(escaped)\(mark)" + quoted.append(mark) + return quoted } func join(_ expressions: [Expressible]) -> Expressible { @@ -73,7 +79,11 @@ extension String { } func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { - let expression = Expression(" \(self) ".join([lhs, rhs]).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 } @@ -81,37 +91,25 @@ extension String { } func prefix(_ expressions: Expressible) -> Expressible { - return "\(self) ".wrap(expressions) as Expression + "\(self) ".wrap(expressions) as Expression } func prefix(_ expressions: [Expressible]) -> Expressible { - return "\(self) ".wrap(expressions) as Expression + "\(self) ".wrap(expressions) as Expression } func wrap(_ expression: Expressible) -> Expression { - return Expression("\(self)(\(expression.expression.template))", expression.expression.bindings) + Expression("\(self)(\(expression.expression.template))", expression.expression.bindings) } func wrap(_ expressions: [Expressible]) -> Expression { - return wrap(", ".join(expressions)) + wrap(", ".join(expressions)) } } -func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true, function: String = #function) -> Expression { - return function.infix(lhs, rhs, wrap: wrap) -} - -func wrap(_ expression: Expressible, function: String = #function) -> Expression { - return function.wrap(expression) -} - -func wrap(_ expressions: [Expressible], function: String = #function) -> Expression { - return function.wrap(", ".join(expressions)) -} - func transcode(_ literal: Binding?) -> String { - guard let literal = literal else { return "NULL" } + guard let literal else { return "NULL" } switch literal { case let blob as Blob: @@ -123,10 +121,11 @@ func transcode(_ literal: Binding?) -> String { } } -func value(_ v: Binding) -> A { - return A.fromDatatypeValue(v as! A.Datatype) as! A +// swiftlint:disable force_cast force_try +func value(_ binding: Binding) -> A { + try! A.fromDatatypeValue(binding as! A.Datatype) as! A } -func value(_ v: Binding?) -> A { - return value(v!) +func value(_ binding: Binding?) -> A { + value(binding!) } diff --git a/Sources/SQLite/Info.plist b/Sources/SQLite/Info.plist index 7347d842..ca23c84f 100644 --- a/Sources/SQLite/Info.plist +++ b/Sources/SQLite/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.11.4 + $(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 index 693ce323..21ed899f 100644 --- a/Sources/SQLite/SQLite.h +++ b/Sources/SQLite/SQLite.h @@ -2,5 +2,3 @@ FOUNDATION_EXPORT double SQLiteVersionNumber; FOUNDATION_EXPORT const unsigned char SQLiteVersionString[]; - -#import diff --git a/Sources/SQLite/Schema/Connection+Schema.swift b/Sources/SQLite/Schema/Connection+Schema.swift new file mode 100644 index 00000000..2977af17 --- /dev/null +++ b/Sources/SQLite/Schema/Connection+Schema.swift @@ -0,0 +1,35 @@ +import Foundation + +public extension Connection { + var schema: SchemaReader { SchemaReader(connection: self) } + + // There are four columns in each result row. + // The first column is the name of the table that + // contains the REFERENCES clause. + // The second column is the rowid of the row that contains the + // invalid REFERENCES clause, or NULL if the child table is a WITHOUT ROWID table. + // The third column is the name of the table that is referred to. + // The fourth column is the index of the specific foreign key constraint that failed. + // + // https://sqlite.org/pragma.html#pragma_foreign_key_check + func foreignKeyCheck(table: String? = nil) throws -> [ForeignKeyError] { + try run("PRAGMA foreign_key_check" + (table.map { "(\($0.quote()))" } ?? "")) + .compactMap { (row: [Binding?]) -> ForeignKeyError? in + guard let table = row[0] as? String, + let rowId = row[1] as? Int64, + let target = row[2] as? String else { return nil } + + return ForeignKeyError(from: table, rowId: rowId, to: target) + } + } + + // This pragma does a low-level formatting and consistency check of the database. + // https://sqlite.org/pragma.html#pragma_integrity_check + func integrityCheck(table: String? = nil) throws -> [String] { + precondition(table == nil || supports(.partialIntegrityCheck), "partial integrity check not supported") + + return try run("PRAGMA integrity_check" + (table.map { "(\($0.quote()))" } ?? "")) + .compactMap { $0[0] as? String } + .filter { $0 != "ok" } + } +} diff --git a/Sources/SQLite/Schema/SchemaChanger.swift b/Sources/SQLite/Schema/SchemaChanger.swift new file mode 100644 index 00000000..6fae532a --- /dev/null +++ b/Sources/SQLite/Schema/SchemaChanger.swift @@ -0,0 +1,372 @@ +import Foundation + +/* + https://www.sqlite.org/lang_altertable.html + + The only schema altering commands directly supported by SQLite are the "rename table" and "add column" + commands shown above. + + (SQLite 3.25.0: RENAME COLUMN) + (SQLite 3.35.0: DROP COLUMN) + + However, applications can make other arbitrary changes to the format of a table using a + simple sequence of operations. The steps to make arbitrary changes to the schema design of some table X are as follows: + + 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF. + 2. Start a transaction. + 3. Remember the format of all indexes and triggers associated with table X + (SELECT sql FROM sqlite_master WHERE tbl_name='X' AND type='index') + 4. Use CREATE TABLE to construct a new table "new_X" that is in the desired revised format of table X. + 5. Transfer content from X into new_X using a statement like: INSERT INTO new_X SELECT ... FROM X. + 6. Drop the old table X: DROP TABLE X. + 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X. + 8. Use CREATE INDEX and CREATE TRIGGER to reconstruct indexes and triggers associated with table X. + 9. If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW + 10. If foreign key constraints were originally enabled then run PRAGMA foreign_key_check + 11. Commit the transaction started in step 2. + 12. If foreign keys constraints were originally enabled, reenable them now. +*/ +public class SchemaChanger: CustomStringConvertible { + public enum Error: LocalizedError { + case invalidColumnDefinition(String) + case foreignKeyError([ForeignKeyError]) + + public var errorDescription: String? { + switch self { + case .foreignKeyError(let errors): + return "Foreign key errors: \(errors)" + case .invalidColumnDefinition(let message): + return "Invalid column definition: \(message)" + } + } + } + + public enum Operation { + case addColumn(ColumnDefinition) + case addIndex(IndexDefinition, ifNotExists: Bool) + case dropColumn(String) + case dropIndex(String, ifExists: Bool) + case renameColumn(String, String) + case renameTable(String) + case createTable(columns: [ColumnDefinition], ifNotExists: Bool) + + /// Returns non-nil if the operation can be executed with a simple SQL statement + func toSQL(_ table: String, version: SQLiteVersion) -> String? { + switch self { + case .addColumn(let definition): + return "ALTER TABLE \(table.quote()) ADD COLUMN \(definition.toSQL())" + case .addIndex(let definition, let ifNotExists): + return definition.toSQL(ifNotExists: ifNotExists) + case .renameColumn(let from, let to) where SQLiteFeature.renameColumn.isSupported(by: version): + return "ALTER TABLE \(table.quote()) RENAME COLUMN \(from.quote()) TO \(to.quote())" + case .dropColumn(let column) where SQLiteFeature.dropColumn.isSupported(by: version): + return "ALTER TABLE \(table.quote()) DROP COLUMN \(column.quote())" + case .dropIndex(let name, let ifExists): + return "DROP INDEX \(ifExists ? " IF EXISTS " : "") \(name.quote())" + case .createTable(let columns, let ifNotExists): + return "CREATE TABLE \(ifNotExists ? " IF NOT EXISTS " : "") \(table.quote()) (" + + columns.map { $0.toSQL() }.joined(separator: ", ") + + ")" + default: return nil + } + } + + func validate() throws { + switch self { + case .addColumn(let definition): + // The new column may take any of the forms permissible in a CREATE TABLE statement, with the following restrictions: + // - The column may not have a PRIMARY KEY or UNIQUE constraint. + // - The column may not have a default value of CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP, or an expression in parentheses + // - If a NOT NULL constraint is specified, then the column must have a default value other than NULL. + guard definition.primaryKey == nil else { + throw Error.invalidColumnDefinition("can not add primary key column") + } + let invalidValues: [LiteralValue] = [.CURRENT_TIME, .CURRENT_DATE, .CURRENT_TIMESTAMP] + if invalidValues.contains(definition.defaultValue) { + throw Error.invalidColumnDefinition("Invalid default value") + } + if !definition.nullable && definition.defaultValue == .NULL { + throw Error.invalidColumnDefinition("NOT NULL columns must have a default value other than NULL") + } + case .dropColumn: + // The DROP COLUMN command only works if the column is not referenced by any other parts of the schema + // and is not a PRIMARY KEY and does not have a UNIQUE constraint + break + default: break + } + } + } + + public class AlterTableDefinition { + fileprivate var operations: [Operation] = [] + + public let name: String + + init(name: String) { + self.name = name + } + + public func add(column: ColumnDefinition) { + operations.append(.addColumn(column)) + } + + public func add(index: IndexDefinition, ifNotExists: Bool = false) { + operations.append(.addIndex(index, ifNotExists: ifNotExists)) + } + + public func drop(column: String) { + operations.append(.dropColumn(column)) + } + + public func drop(index: String, ifExists: Bool = false) { + operations.append(.dropIndex(index, ifExists: ifExists)) + } + + public func rename(column: String, to: String) { + operations.append(.renameColumn(column, to)) + } + } + + public class CreateTableDefinition { + fileprivate var columnDefinitions: [ColumnDefinition] = [] + fileprivate var indexDefinitions: [IndexDefinition] = [] + + let name: String + let ifNotExists: Bool + + init(name: String, ifNotExists: Bool) { + self.name = name + self.ifNotExists = ifNotExists + } + + public func add(column: ColumnDefinition) { + columnDefinitions.append(column) + } + + public func add(expression: Expression) where T: Value { + add(column: .init(name: columnName(for: expression), type: .init(expression: expression), nullable: false)) + } + + public func add(expression: Expression) where T: Value { + add(column: .init(name: columnName(for: expression), type: .init(expression: expression), nullable: true)) + } + + public func add(index: IndexDefinition) { + indexDefinitions.append(index) + } + + var operations: [Operation] { + precondition(!columnDefinitions.isEmpty) + return [ + .createTable(columns: columnDefinitions, ifNotExists: ifNotExists) + ] + indexDefinitions.map { .addIndex($0, ifNotExists: ifNotExists) } + } + + private func columnName(for expression: Expression) -> String { + switch LiteralValue(expression.template) { + case .stringLiteral(let string): return string + default: fatalError("expression is not a literal string value") + } + } + } + + private let connection: Connection + private let schemaReader: SchemaReader + private let version: SQLiteVersion + static let tempPrefix = "tmp_" + typealias Block = () throws -> Void + public typealias AlterTableDefinitionBlock = (AlterTableDefinition) -> Void + public typealias CreateTableDefinitionBlock = (CreateTableDefinition) -> Void + + struct Options: OptionSet { + let rawValue: Int + static let `default`: Options = [] + static let temp = Options(rawValue: 1) + } + + public convenience init(connection: Connection) { + self.init(connection: connection, + version: connection.sqliteVersion) + } + + init(connection: Connection, version: SQLiteVersion) { + self.connection = connection + schemaReader = connection.schema + self.version = version + } + + public func alter(table: String, block: AlterTableDefinitionBlock) throws { + let alterTableDefinition = AlterTableDefinition(name: table) + block(alterTableDefinition) + + for operation in alterTableDefinition.operations { + try run(table: table, operation: operation) + } + } + + public func create(table: String, ifNotExists: Bool = false, block: CreateTableDefinitionBlock) throws { + let createTableDefinition = CreateTableDefinition(name: table, ifNotExists: ifNotExists) + block(createTableDefinition) + + for operation in createTableDefinition.operations { + try run(table: table, operation: operation) + } + } + + public func drop(table: String, ifExists: Bool = true) throws { + try dropTable(table, ifExists: ifExists) + } + + // Beginning with release 3.25.0 (2018-09-15), references to the table within trigger bodies and + // view definitions are also renamed. + public func rename(table: String, to: String) throws { + try connection.run("ALTER TABLE \(table.quote()) RENAME TO \(to.quote())") + } + + // Runs arbitrary SQL. Should only be used if no predefined operations exist. + @discardableResult + public func run(_ sql: String, _ bindings: Binding?...) throws -> Statement { + return try connection.run(sql, bindings) + } + + private func run(table: String, operation: Operation) throws { + try operation.validate() + + if let sql = operation.toSQL(table, version: version) { + try connection.run(sql) + } else { + try doTheTableDance(table: table, operation: operation) + } + } + + private func doTheTableDance(table: String, operation: Operation) throws { + try connection.transaction { + try disableRefIntegrity { + let tempTable = "\(SchemaChanger.tempPrefix)\(table)" + try moveTable(from: table, to: tempTable, options: [.temp], operation: operation) + try rename(table: tempTable, to: table) + let foreignKeyErrors = try connection.foreignKeyCheck() + if foreignKeyErrors.count > 0 { + throw Error.foreignKeyError(foreignKeyErrors) + } + } + } + } + + private func disableRefIntegrity(block: Block) throws { + let oldForeignKeys = connection.foreignKeys + let oldDeferForeignKeys = connection.deferForeignKeys + + connection.deferForeignKeys = true + connection.foreignKeys = false + + defer { + connection.deferForeignKeys = oldDeferForeignKeys + connection.foreignKeys = oldForeignKeys + } + + try block() + } + + private func moveTable(from: String, to: String, options: Options = .default, operation: Operation? = nil) throws { + try copyTable(from: from, to: to, options: options, operation: operation) + try dropTable(from, ifExists: true) + } + + private func copyTable(from: String, to: String, options: Options = .default, operation: Operation?) throws { + let fromDefinition = TableDefinition( + name: from, + columns: try schemaReader.columnDefinitions(table: from), + indexes: try schemaReader.indexDefinitions(table: from) + ) + let toDefinition = fromDefinition + .apply(.renameTable(to)) + .apply(operation) + + try createTable(definition: toDefinition, options: options) + try createTableIndexes(definition: toDefinition) + if case .dropColumn = operation { + try copyTableContents(from: fromDefinition.apply(operation), to: toDefinition) + } else { + try copyTableContents(from: fromDefinition, to: toDefinition) + } + } + + private func createTable(definition: TableDefinition, options: Options) throws { + try connection.run(definition.toSQL(temporary: options.contains(.temp))) + } + + private func createTableIndexes(definition: TableDefinition) throws { + for index in definition.indexes { + try index.validate() + try connection.run(index.toSQL()) + } + } + + private func dropTable(_ table: String, ifExists: Bool) throws { + try connection.run("DROP TABLE \(ifExists ? "IF EXISTS" : "") \(table.quote())") + } + + private func copyTableContents(from: TableDefinition, to: TableDefinition) throws { + try connection.run(from.copySQL(to: to)) + } + + public var description: String { + "SQLiteSchemaChanger: \(connection.description)" + } +} + +extension IndexDefinition { + func renameTable(to: String) -> IndexDefinition { + func indexName() -> String { + if to.starts(with: SchemaChanger.tempPrefix) { + return "\(SchemaChanger.tempPrefix)\(name)" + } else if table.starts(with: SchemaChanger.tempPrefix) { + return name.replacingOccurrences(of: SchemaChanger.tempPrefix, with: "") + } else { + return name + } + } + return IndexDefinition(table: to, name: indexName(), unique: unique, columns: columns, where: `where`, orders: orders) + } + + func renameColumn(from: String, to: String) -> IndexDefinition { + IndexDefinition(table: table, name: name, unique: unique, columns: columns.map { + $0 == from ? to : $0 + }, where: `where`, orders: orders) + } +} + +extension TableDefinition { + func apply(_ operation: SchemaChanger.Operation?) -> TableDefinition { + switch operation { + case .none: return self + case .createTable, .addIndex, .dropIndex: fatalError() + case .addColumn: fatalError("Use 'ALTER TABLE ADD COLUMN (...)'") + + case .dropColumn(let column): + return TableDefinition(name: name, + columns: columns.filter { $0.name != column }, + indexes: indexes.filter { !$0.columns.contains(column) } + ) + case .renameColumn(let from, let to): + return TableDefinition( + name: name, + columns: columns.map { $0.rename(from: from, to: to) }, + indexes: indexes.map { $0.renameColumn(from: from, to: to) } + ) + case .renameTable(let to): + return TableDefinition(name: to, columns: columns, indexes: indexes.map { $0.renameTable(to: to) }) + } + } +} + +extension ColumnDefinition.Affinity { + init(expression: Expression) where T: Value { + self.init(T.declaredDatatype) + } + + init(expression: Expression) where T: Value { + self.init(T.declaredDatatype) + } +} diff --git a/Sources/SQLite/Schema/SchemaDefinitions.swift b/Sources/SQLite/Schema/SchemaDefinitions.swift new file mode 100644 index 00000000..d7803999 --- /dev/null +++ b/Sources/SQLite/Schema/SchemaDefinitions.swift @@ -0,0 +1,433 @@ +import Foundation + +struct TableDefinition: Equatable { + let name: String + let columns: [ColumnDefinition] + let indexes: [IndexDefinition] + + var quotedColumnList: String { + columns.map { $0.name.quote() }.joined(separator: ", ") + } +} + +// https://sqlite.org/schematab.html#interpretation_of_the_schema_table +public struct ObjectDefinition: Equatable { + public enum ObjectType: String { + case table, index, view, trigger + } + public let type: ObjectType + + // The name of the object + public let name: String + + // The name of a table or view that the object is associated with. + // * For a table or view, a copy of the name column. + // * For an index, the name of the table that is indexed + // * For a trigger, the column stores the name of the table or view that causes the trigger to fire. + public let tableName: String + + // The page number of the root b-tree page for tables and indexes, otherwise 0 or NULL + public let rootpage: Int64 + + // SQL text that describes the object (NULL for the internal indexes) + public let sql: String? + + public var isInternal: Bool { + name.starts(with: "sqlite_") || sql == nil + } +} + +// https://sqlite.org/syntax/column-def.html +// column-name -> type-name -> column-constraint* +public struct ColumnDefinition: Equatable { + + // The type affinity of a column is the recommended type for data stored in that column. + // The important idea here is that the type is recommended, not required. Any column can still + // store any type of data. It is just that some columns, given the choice, will prefer to use one + // storage class over another. The preferred storage class for a column is called its "affinity". + public enum Affinity: String, CustomStringConvertible, CaseIterable { + case INTEGER + case NUMERIC + case REAL + case TEXT + case BLOB + + public var description: String { + rawValue + } + + init(_ string: String) { + let test = string.uppercased() + // https://sqlite.org/datatype3.html#determination_of_column_affinity + if test.contains("INT") { // Rule 1 + self = .INTEGER + } else if ["CHAR", "CLOB", "TEXT"].first(where: {test.contains($0)}) != nil { // Rule 2 + self = .TEXT + } else if string.contains("BLOB") { // Rule 3 + self = .BLOB + } else if ["REAL", "FLOA", "DOUB"].first(where: {test.contains($0)}) != nil { // Rule 4 + self = .REAL + } else { // Rule 5 + self = .NUMERIC + } + } + } + + public enum OnConflict: String, CaseIterable { + case ROLLBACK + case ABORT + case FAIL + case IGNORE + case REPLACE + + init?(_ string: String) { + guard let value = (OnConflict.allCases.first { $0.rawValue == string }) else { return nil } + self = value + } + } + + public struct PrimaryKey: Equatable { + let autoIncrement: Bool + let onConflict: OnConflict? + + // swiftlint:disable:next force_try + static let pattern = try! NSRegularExpression(pattern: "PRIMARY KEY\\s*(?:ASC|DESC)?\\s*(?:ON CONFLICT (\\w+)?)?\\s*(AUTOINCREMENT)?") + + public init(autoIncrement: Bool = true, onConflict: OnConflict? = nil) { + self.autoIncrement = autoIncrement + self.onConflict = onConflict + } + + init?(sql: String) { + guard let match = PrimaryKey.pattern.firstMatch( + in: sql, + range: NSRange(location: 0, length: sql.count)) else { + return nil + } + let conflict = match.range(at: 1) + let onConflict: ColumnDefinition.OnConflict? + if conflict.location != NSNotFound { + onConflict = OnConflict((sql as NSString).substring(with: conflict)) + } else { + onConflict = nil + } + let autoIncrement = match.range(at: 2).location != NSNotFound + self.init(autoIncrement: autoIncrement, onConflict: onConflict) + } + } + + public struct ForeignKey: Equatable { + let fromColumn: String + let toTable: String + // when null, use primary key of "toTable" + let toColumn: String? + let onUpdate: String? + let onDelete: String? + + public init(toTable: String, toColumn: String? = nil, onUpdate: String? = nil, onDelete: String? = nil) { + self.init(fromColumn: "", toTable: toTable, toColumn: toColumn, onUpdate: onUpdate, onDelete: onDelete) + } + + public init(fromColumn: String, toTable: String, toColumn: String? = nil, onUpdate: String? = nil, onDelete: String? = nil) { + self.fromColumn = fromColumn + self.toTable = toTable + self.toColumn = toColumn + self.onUpdate = onUpdate + self.onDelete = onDelete + } + } + + public let name: String + public let primaryKey: PrimaryKey? + public let type: Affinity + public let nullable: Bool + public let unique: Bool + public let defaultValue: LiteralValue + public let references: ForeignKey? + + public init(name: String, + primaryKey: PrimaryKey? = nil, + type: Affinity, + nullable: Bool = true, + unique: Bool = false, + defaultValue: LiteralValue = .NULL, + references: ForeignKey? = nil) { + self.name = name + self.primaryKey = primaryKey + self.type = type + self.nullable = nullable + self.unique = unique + self.defaultValue = defaultValue + self.references = references + } + + func rename(from: String, to: String) -> ColumnDefinition { + guard from == name else { return self } + return ColumnDefinition(name: to, primaryKey: primaryKey, type: type, nullable: nullable, defaultValue: defaultValue, references: references) + } +} + +public enum LiteralValue: Equatable, CustomStringConvertible { + // swiftlint:disable force_try + private static let singleQuote = try! NSRegularExpression(pattern: "^'(.*)'$") + private static let doubleQuote = try! NSRegularExpression(pattern: "^\"(.*)\"$") + private static let blob = try! NSRegularExpression(pattern: "^[xX]\'(.*)\'$") + // swiftlint:enable force_try + + case numericLiteral(String) + case stringLiteral(String) + // BLOB literals are string literals containing hexadecimal data and preceded by a single "x" or "X" + // character. Example: X'53514C697465' + case blobLiteral(String) + + // If there is no explicit DEFAULT clause attached to a column definition, then the default value of the + // column is NULL + case NULL + + // Beginning with SQLite 3.23.0 (2018-04-02), SQLite recognizes the identifiers "TRUE" and + // "FALSE" as boolean literals, if and only if those identifiers are not already used for some other + // meaning. + // + // The boolean identifiers TRUE and FALSE are usually just aliases for the integer values 1 and 0, respectively. + case TRUE + case FALSE + // swiftlint:disable identifier_name + case CURRENT_TIME + case CURRENT_DATE + case CURRENT_TIMESTAMP + // swiftlint:enable identifier_name + + init(_ string: String?) { + guard let string else { + self = .NULL + return + } + switch string { + case "NULL": self = .NULL + case "TRUE": self = .TRUE + case "FALSE": self = .FALSE + case "CURRENT_TIME": self = .CURRENT_TIME + case "CURRENT_TIMESTAMP": self = .CURRENT_TIMESTAMP + case "CURRENT_DATE": self = .CURRENT_DATE + default: self = LiteralValue.parse(string) + } + } + + public var description: String { + switch self { + case .NULL: return "NULL" + case .TRUE: return "TRUE" + case .FALSE: return "FALSE" + case .CURRENT_TIMESTAMP: return "CURRENT_TIMESTAMP" + case .CURRENT_TIME: return "CURRENT_TIME" + case .CURRENT_DATE: return "CURRENT_DATE" + case .stringLiteral(let value): return value.quote("'") + case .blobLiteral(let value): return "X\(value.quote("'"))" + case .numericLiteral(let value): return value + } + } + + func map(block: (LiteralValue) -> U) -> U? { + if self == .NULL { + return nil + } else { + return block(self) + } + } + private static func parse(_ string: String) -> LiteralValue { + if let match = LiteralValue.singleQuote.firstMatch(in: string, range: NSRange(location: 0, length: string.count)) { + return .stringLiteral((string as NSString).substring(with: match.range(at: 1)).replacingOccurrences(of: "''", with: "'")) + } else if let match = LiteralValue.doubleQuote.firstMatch(in: string, range: NSRange(location: 0, length: string.count)) { + return .stringLiteral((string as NSString).substring(with: match.range(at: 1)).replacingOccurrences(of: "\"\"", with: "\"")) + } else if let match = LiteralValue.blob.firstMatch(in: string, range: NSRange(location: 0, length: string.count)) { + return .blobLiteral((string as NSString).substring(with: match.range(at: 1))) + } else { + return .numericLiteral(string) + } + } +} + +// https://sqlite.org/lang_createindex.html +// schema-name.index-name ON table-name ( indexed-column+ ) WHERE expr +public struct IndexDefinition: Equatable { + // SQLite supports index names up to 64 characters. + static let maxIndexLength = 64 + + // swiftlint:disable force_try + static let whereRe = try! NSRegularExpression(pattern: "\\sWHERE\\s+(.+)$") + static let orderRe = try! NSRegularExpression(pattern: "\"?(\\w+)\"? DESC") + // swiftlint:enable force_try + + public enum Order: String { case ASC, DESC } + + public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil, + orders: [String: Order]? = nil, origin: Origin? = nil) { + self.table = table + self.name = name + self.unique = unique + self.columns = columns + self.where = `where` + self.orders = orders + self.origin = origin + } + + init (table: String, name: String, unique: Bool, columns: [String], indexSQL: String?, origin: Origin? = nil) { + func wherePart(sql: String) -> String? { + IndexDefinition.whereRe.firstMatch(in: sql, options: [], range: NSRange(location: 0, length: sql.count)).map { + (sql as NSString).substring(with: $0.range(at: 1)) + } + } + + func orders(sql: String) -> [String: IndexDefinition.Order] { + IndexDefinition.orderRe + .matches(in: sql, range: NSRange(location: 0, length: sql.count)) + .reduce([String: IndexDefinition.Order]()) { (memo, result) in + var memo2 = memo + let column = (sql as NSString).substring(with: result.range(at: 1)) + memo2[column] = .DESC + return memo2 + } + } + + let orders = indexSQL.flatMap(orders) + + self.init(table: table, + name: name, + unique: unique, + columns: columns, + where: indexSQL.flatMap(wherePart), + orders: (orders?.isEmpty ?? false) ? nil : orders, + origin: origin) + } + + public let table: String + public let name: String + public let unique: Bool + public let columns: [String] + public let `where`: String? + public let orders: [String: Order]? + public let origin: Origin? + + public enum Origin: String { + case uniqueConstraint = "u" // index created from a "CREATE TABLE (... UNIQUE)" column constraint + case createIndex = "c" // index created explicitly via "CREATE INDEX ..." + case primaryKey = "pk" // index created from a "CREATE TABLE PRIMARY KEY" column constraint + } + + enum IndexError: LocalizedError { + case tooLong(String, String) + + var errorDescription: String? { + switch self { + case .tooLong(let name, let table): + return "Index name '\(name)' on table '\(table)' is too long; the limit is " + + "\(IndexDefinition.maxIndexLength) characters" + } + } + } + + // Indices with names of the form "sqlite_autoindex_TABLE_N" that are used to implement UNIQUE and PRIMARY KEY + // constraints on ordinary tables. + // https://sqlite.org/fileformat2.html#intschema + var isInternal: Bool { + name.starts(with: "sqlite_autoindex_") + } + + func validate() throws { + if name.count > IndexDefinition.maxIndexLength { + throw IndexError.tooLong(name, table) + } + } +} + +public struct ForeignKeyError: CustomStringConvertible { + public let from: String + public let rowId: Int64 + public let to: String + + public var description: String { + "\(from) [\(rowId)] => \(to)" + } +} + +extension TableDefinition { + func toSQL(temporary: Bool = false) -> String { + precondition(columns.count > 0, "no columns to create") + + return ([ + "CREATE", + temporary ? "TEMPORARY" : nil, + "TABLE", + name, + "(", + columns.map { $0.toSQL() }.joined(separator: ",\n"), + ")" + ] as [String?]).compactMap { $0 } + .joined(separator: " ") + } + + func copySQL(to: TableDefinition) -> String { + precondition(columns.count > 0) + precondition(columns.count == to.columns.count, "column counts don't match") + return "INSERT INTO \(to.name.quote()) (\(to.quotedColumnList)) SELECT \(quotedColumnList) FROM \(name.quote())" + } +} + +extension ColumnDefinition { + func toSQL() -> String { + [ + name.quote(), + type.rawValue, + defaultValue.map { "DEFAULT \($0)" }, + primaryKey.map { $0.toSQL() }, + nullable ? nil : "NOT NULL", + unique ? "UNIQUE" : nil, + references.map { $0.toSQL() } + ].compactMap { $0 } + .joined(separator: " ") + } +} + +extension IndexDefinition { + public func toSQL(ifNotExists: Bool = false) -> String { + let commaSeparatedColumns = columns.map { (column: String) -> String in + column.quote() + (orders?[column].map { " \($0.rawValue)" } ?? "") + }.joined(separator: ", ") + + return ([ + "CREATE", + unique ? "UNIQUE" : nil, + "INDEX", + ifNotExists ? "IF NOT EXISTS" : nil, + name.quote(), + "ON", + table.quote(), + "(\(commaSeparatedColumns))", + `where`.map { "WHERE \($0)" } + ] as [String?]).compactMap { $0 } + .joined(separator: " ") + } +} + +extension ColumnDefinition.ForeignKey { + func toSQL() -> String { + ([ + "REFERENCES", + toTable.quote(), + toColumn.map { "(\($0.quote()))" }, + onUpdate.map { "ON UPDATE \($0)" }, + onDelete.map { "ON DELETE \($0)" } + ] as [String?]).compactMap { $0 } + .joined(separator: " ") + } +} + +extension ColumnDefinition.PrimaryKey { + func toSQL() -> String { + [ + "PRIMARY KEY", + autoIncrement ? "AUTOINCREMENT" : nil, + onConflict.map { "ON CONFLICT \($0.rawValue)" } + ].compactMap { $0 }.joined(separator: " ") + } +} diff --git a/Sources/SQLite/Schema/SchemaReader.swift b/Sources/SQLite/Schema/SchemaReader.swift new file mode 100644 index 00000000..995cd4d7 --- /dev/null +++ b/Sources/SQLite/Schema/SchemaReader.swift @@ -0,0 +1,220 @@ +import Foundation + +public class SchemaReader { + private let connection: Connection + + init(connection: Connection) { + self.connection = connection + } + + // https://sqlite.org/pragma.html#pragma_table_info + // + // This pragma returns one row for each column in the named table. Columns in the result set include the + // column name, data type, whether or not the column can be NULL, and the default value for the column. The + // "pk" column in the result set is zero for columns that are not part of the primary key, and is the + // index of the column in the primary key for columns that are part of the primary key. + public func columnDefinitions(table: String) throws -> [ColumnDefinition] { + func parsePrimaryKey(column: String) throws -> ColumnDefinition.PrimaryKey? { + try createTableSQL(name: table).flatMap { .init(sql: $0) } + } + + let foreignKeys: [String: [ColumnDefinition.ForeignKey]] = + Dictionary(grouping: try foreignKeys(table: table), by: { $0.fromColumn }) + + let columnDefinitions = try connection.prepareRowIterator("PRAGMA table_info(\(table.quote()))") + .map { (row: Row) -> ColumnDefinition in + ColumnDefinition( + name: row[TableInfoTable.nameColumn], + primaryKey: (row[TableInfoTable.primaryKeyColumn] ?? 0) > 0 ? + try parsePrimaryKey(column: row[TableInfoTable.nameColumn]) : nil, + type: ColumnDefinition.Affinity(row[TableInfoTable.typeColumn]), + nullable: row[TableInfoTable.notNullColumn] == 0, + unique: false, + defaultValue: LiteralValue(row[TableInfoTable.defaultValueColumn]), + references: foreignKeys[row[TableInfoTable.nameColumn]]?.first + ) + } + + let internalIndexes = try indexDefinitions(table: table).filter { $0.isInternal } + return columnDefinitions.map { definition in + if let index = internalIndexes.first(where: { $0.columns.contains(definition.name) }), index.origin == .uniqueConstraint { + + ColumnDefinition(name: definition.name, + primaryKey: definition.primaryKey, + type: definition.type, + nullable: definition.nullable, + unique: true, + defaultValue: definition.defaultValue, + references: definition.references) + } else { + definition + } + } + } + + public func objectDefinitions(name: String? = nil, + type: ObjectDefinition.ObjectType? = nil, + temp: Bool = false) throws -> [ObjectDefinition] { + var query: QueryType = SchemaTable.get(for: connection, temp: temp) + if let name { + query = query.where(SchemaTable.nameColumn == name) + } + if let type { + query = query.where(SchemaTable.typeColumn == type.rawValue) + } + return try connection.prepare(query).map { row -> ObjectDefinition in + guard let type = ObjectDefinition.ObjectType(rawValue: row[SchemaTable.typeColumn]) else { + fatalError("unexpected type") + } + return ObjectDefinition( + type: type, + name: row[SchemaTable.nameColumn], + tableName: row[SchemaTable.tableNameColumn], + rootpage: row[SchemaTable.rootPageColumn] ?? 0, + sql: row[SchemaTable.sqlColumn] + ) + } + } + + public func indexDefinitions(table: String) throws -> [IndexDefinition] { + func indexSQL(name: String) throws -> String? { + try objectDefinitions(name: name, type: .index) + .compactMap(\.sql) + .first + } + + func indexInfos(name: String) throws -> [IndexInfo] { + try connection.prepareRowIterator("PRAGMA index_info(\(name.quote()))") + .compactMap { row in + IndexInfo(name: row[IndexInfoTable.nameColumn], + columnRank: row[IndexInfoTable.seqnoColumn], + columnRankWithinTable: row[IndexInfoTable.cidColumn]) + + } + } + + return try connection.prepareRowIterator("PRAGMA index_list(\(table.quote()))") + .compactMap { row -> IndexDefinition? in + let name = row[IndexListTable.nameColumn] + return IndexDefinition( + table: table, + name: name, + unique: row[IndexListTable.uniqueColumn] == 1, + columns: try indexInfos(name: name).compactMap { $0.name }, + indexSQL: try indexSQL(name: name), + origin: IndexDefinition.Origin(rawValue: row[IndexListTable.originColumn]) + ) + } + } + + func foreignKeys(table: String) throws -> [ColumnDefinition.ForeignKey] { + try connection.prepareRowIterator("PRAGMA foreign_key_list(\(table.quote()))") + .map { row in + ColumnDefinition.ForeignKey( + fromColumn: row[ForeignKeyListTable.fromColumn], + toTable: row[ForeignKeyListTable.tableColumn], + toColumn: row[ForeignKeyListTable.toColumn], + onUpdate: row[ForeignKeyListTable.onUpdateColumn] == TableBuilder.Dependency.noAction.rawValue + ? nil : row[ForeignKeyListTable.onUpdateColumn], + onDelete: row[ForeignKeyListTable.onDeleteColumn] == TableBuilder.Dependency.noAction.rawValue + ? nil : row[ForeignKeyListTable.onDeleteColumn] + ) + } + } + + func tableDefinitions() throws -> [TableDefinition] { + try objectDefinitions(type: .table) + .map { table in + TableDefinition( + name: table.name, + columns: try columnDefinitions(table: table.name), + indexes: try indexDefinitions(table: table.name) + ) + } + } + + private func createTableSQL(name: String) throws -> String? { + try ( + objectDefinitions(name: name, type: .table) + + objectDefinitions(name: name, type: .table, temp: true) + ).compactMap(\.sql).first + } + + struct IndexInfo { + let name: String? + // The rank of the column within the index. (0 means left-most.) + let columnRank: Int + // The rank of the column within the table being indexed. + // A value of -1 means rowid and a value of -2 means that an expression is being used + let columnRankWithinTable: Int + } +} + +private enum SchemaTable { + private static let name = Table("sqlite_schema", database: "main") + private static let tempName = Table("sqlite_schema", database: "temp") + // legacy names (< 3.33.0) + private static let masterName = Table("sqlite_master") + private static let tempMasterName = Table("sqlite_temp_master") + + static func get(for connection: Connection, temp: Bool = false) -> Table { + if connection.supports(.sqliteSchemaTable) { + return temp ? SchemaTable.tempName : SchemaTable.name + } else { + return temp ? SchemaTable.tempMasterName : SchemaTable.masterName + } + } + + // columns + static let typeColumn = Expression("type") + static let nameColumn = Expression("name") + static let tableNameColumn = Expression("tbl_name") + static let rootPageColumn = Expression("rootpage") + static let sqlColumn = Expression("sql") +} + +private enum TableInfoTable { + static let idColumn = Expression("cid") + static let nameColumn = Expression("name") + static let typeColumn = Expression("type") + static let notNullColumn = Expression("notnull") + static let defaultValueColumn = Expression("dflt_value") + static let primaryKeyColumn = Expression("pk") +} + +private enum IndexInfoTable { + // The rank of the column within the index. (0 means left-most.) + static let seqnoColumn = Expression("seqno") + // The rank of the column within the table being indexed. + // A value of -1 means rowid and a value of -2 means that an expression is being used. + static let cidColumn = Expression("cid") + // The name of the column being indexed. + // This columns is NULL if the column is the rowid or an expression. + static let nameColumn = Expression("name") +} + +private enum IndexListTable { + // A sequence number assigned to each index for internal tracking purposes. + static let seqColumn = Expression("seq") + // The name of the index + static let nameColumn = Expression("name") + // "1" if the index is UNIQUE and "0" if not. + static let uniqueColumn = Expression("unique") + // "c" if the index was created by a CREATE INDEX statement, + // "u" if the index was created by a UNIQUE constraint, or + // "pk" if the index was created by a PRIMARY KEY constraint. + static let originColumn = Expression("origin") + // "1" if the index is a partial index and "0" if not. + static let partialColumn = Expression("partial") +} + +private enum ForeignKeyListTable { + static let idColumn = Expression("id") + static let seqColumn = Expression("seq") + static let tableColumn = Expression("table") + static let fromColumn = Expression("from") + static let toColumn = Expression("to") // when null, use primary key + static let onUpdateColumn = Expression("on_update") + static let onDeleteColumn = Expression("on_delete") + static let matchColumn = Expression("match") +} diff --git a/Sources/SQLite/Typed/AggregateFunctions.swift b/Sources/SQLite/Typed/AggregateFunctions.swift index 249bbe60..17fc4a20 100644 --- a/Sources/SQLite/Typed/AggregateFunctions.swift +++ b/Sources/SQLite/Typed/AggregateFunctions.swift @@ -22,7 +22,20 @@ // THE SOFTWARE. // -extension ExpressionType where UnderlyingType : Value { +private enum Function: String { + case count + case max + case min + case avg + case sum + case total + + func wrap(_ expression: Expressible) -> Expression { + self.rawValue.wrap(expression) + } +} + +extension ExpressionType where UnderlyingType: Value { /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. /// @@ -33,7 +46,7 @@ extension ExpressionType where UnderlyingType : Value { /// - Returns: A copy of the expression prefixed with the `DISTINCT` /// keyword. public var distinct: Expression { - return Expression("DISTINCT \(template)", bindings) + Expression("DISTINCT \(template)", bindings) } /// Builds a copy of the expression wrapped with the `count` aggregate @@ -48,12 +61,12 @@ extension ExpressionType where UnderlyingType : Value { /// - Returns: A copy of the expression wrapped with the `count` aggregate /// function. public var count: Expression { - return wrap(self) + Function.count.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value { /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. /// @@ -64,7 +77,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression prefixed with the `DISTINCT` /// keyword. public var distinct: Expression { - return Expression("DISTINCT \(template)", bindings) + Expression("DISTINCT \(template)", bindings) } /// Builds a copy of the expression wrapped with the `count` aggregate @@ -79,12 +92,12 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `count` aggregate /// function. public var count: Expression { - return wrap(self) + Function.count.wrap(self) } } -extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : Comparable { +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: Comparable { /// Builds a copy of the expression wrapped with the `max` aggregate /// function. @@ -96,7 +109,7 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// - Returns: A copy of the expression wrapped with the `max` aggregate /// function. public var max: Expression { - return wrap(self) + Function.max.wrap(self) } /// Builds a copy of the expression wrapped with the `min` aggregate @@ -109,12 +122,12 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var min: Expression { - return wrap(self) + Function.min.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value, UnderlyingType.WrappedType.Datatype : Comparable { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value, UnderlyingType.WrappedType.Datatype: Comparable { /// Builds a copy of the expression wrapped with the `max` aggregate /// function. @@ -126,7 +139,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `max` aggregate /// function. public var max: Expression { - return wrap(self) + Function.max.wrap(self) } /// Builds a copy of the expression wrapped with the `min` aggregate @@ -139,12 +152,12 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var min: Expression { - return wrap(self) + Function.min.wrap(self) } } -extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : Number { +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: Number { /// Builds a copy of the expression wrapped with the `avg` aggregate /// function. @@ -153,10 +166,10 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// salary.average /// // avg("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `avg` aggregate /// function. public var average: Expression { - return "avg".wrap(self) + Function.avg.wrap(self) } /// Builds a copy of the expression wrapped with the `sum` aggregate @@ -166,10 +179,10 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// salary.sum /// // sum("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `sum` aggregate /// function. public var sum: Expression { - return wrap(self) + Function.sum.wrap(self) } /// Builds a copy of the expression wrapped with the `total` aggregate @@ -179,15 +192,15 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : /// salary.total /// // total("salary") /// - /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// - Returns: A copy of the expression wrapped with the `total` aggregate /// function. public var total: Expression { - return wrap(self) + Function.total.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value, UnderlyingType.WrappedType.Datatype : Number { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value, UnderlyingType.WrappedType.Datatype: Number { /// Builds a copy of the expression wrapped with the `avg` aggregate /// function. @@ -199,7 +212,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var average: Expression { - return "avg".wrap(self) + Function.avg.wrap(self) } /// Builds a copy of the expression wrapped with the `sum` aggregate @@ -212,7 +225,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var sum: Expression { - return wrap(self) + Function.sum.wrap(self) } /// Builds a copy of the expression wrapped with the `total` aggregate @@ -225,7 +238,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// - Returns: A copy of the expression wrapped with the `min` aggregate /// function. public var total: Expression { - return wrap(self) + Function.total.wrap(self) } } @@ -233,7 +246,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr extension ExpressionType where UnderlyingType == Int { static func count(_ star: Star) -> Expression { - return wrap(star(nil, nil)) + Function.count.wrap(star(nil, nil)) } } @@ -247,5 +260,5 @@ extension ExpressionType where UnderlyingType == Int { /// - Returns: An expression returning `count(*)` (when called with the `*` /// function literal). public func count(_ star: Star) -> Expression { - return Expression.count(star) + Expression.count(star) } diff --git a/Sources/SQLite/Typed/Coding.swift b/Sources/SQLite/Typed/Coding.swift index dd6a4ec2..d0061851 100644 --- a/Sources/SQLite/Typed/Coding.swift +++ b/Sources/SQLite/Typed/Coding.swift @@ -38,13 +38,95 @@ extension QueryType { /// /// - otherSetters: Any other setters to include in the insert /// - /// - Returns: An `INSERT` statement fort the encodable object - public func insert(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> 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 @@ -59,7 +141,8 @@ extension QueryType { /// - 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 { + 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) @@ -76,24 +159,27 @@ extension Row { /// /// - Returns: a decoded object from this row public func decode(userInfo: [CodingUserInfoKey: Any] = [:]) throws -> V { - return try V(from: self.decoder(userInfo: userInfo)) + try V(from: decoder(userInfo: userInfo)) } public func decoder(userInfo: [CodingUserInfoKey: Any] = [:]) -> Decoder { - return SQLiteDecoder(row: self, userInfo: userInfo) + SQLiteDecoder(row: self, userInfo: userInfo) } } /// Generates a list of settings for an Encodable object -fileprivate class SQLiteEncoder: Encoder { +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) { + init(encoder: SQLiteEncoder, forcingNilValueSetters: Bool = false) { self.encoder = encoder + self.forcingNilValueSetters = forcingNilValueSetters } func superEncoder() -> Swift.Encoder { @@ -105,77 +191,141 @@ fileprivate class SQLiteEncoder: Encoder { } func encodeNil(forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { - self.encoder.setters.append(Expression(key.stringValue) <- nil) + encoder.setters.append(Expression(key.stringValue) <- nil) } func encode(_ value: Int, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { - self.encoder.setters.append(Expression(key.stringValue) <- value) + encoder.setters.append(Expression(key.stringValue) <- value) } func encode(_ value: Bool, forKey key: Key) throws { - self.encoder.setters.append(Expression(key.stringValue) <- value) + encoder.setters.append(Expression(key.stringValue) <- value) } func encode(_ value: Float, forKey key: Key) throws { - self.encoder.setters.append(Expression(key.stringValue) <- Double(value)) + encoder.setters.append(Expression(key.stringValue) <- Double(value)) } func encode(_ value: Double, forKey key: Key) throws { - self.encoder.setters.append(Expression(key.stringValue) <- value) + encoder.setters.append(Expression(key.stringValue) <- value) } func encode(_ value: String, forKey key: Key) throws { - self.encoder.setters.append(Expression(key.stringValue) <- value) + 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 encode(_ value: T, forKey key: Key) throws where T : Swift.Encodable { - if let data = value as? Data { - self.encoder.setters.append(Expression(key.stringValue) <- data) + 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) } - else { + } + + 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) - self.encoder.setters.append(Expression(key.stringValue) <- string) + 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: self.codingPath, debugDescription: "encoding an Int8 is not supported")) + 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: self.codingPath, debugDescription: "encoding an Int16 is not supported")) + 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: self.codingPath, debugDescription: "encoding an Int32 is not supported")) + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, + debugDescription: "encoding an Int32 is not supported")) } func encode(_ value: Int64, forKey key: Key) throws { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int64 is not supported")) + encoder.setters.append(Expression(key.stringValue) <- value) } func encode(_ value: UInt, forKey key: Key) throws { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt is not supported")) + 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: self.codingPath, debugDescription: "encoding an UInt8 is not supported")) + 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: self.codingPath, debugDescription: "encoding an UInt16 is not supported")) + 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: self.codingPath, debugDescription: "encoding an UInt32 is not supported")) + 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: self.codingPath, debugDescription: "encoding an UInt64 is not supported")) + 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 { + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) + -> KeyedEncodingContainer where NestedKey: CodingKey { fatalError("encoding a nested container is not supported") } @@ -184,12 +334,14 @@ fileprivate class SQLiteEncoder: Encoder { } } - fileprivate var setters: [SQLite.Setter] = [] + fileprivate var setters: [Setter] = [] let codingPath: [CodingKey] = [] let userInfo: [CodingUserInfoKey: Any] + let forcingNilValueSetters: Bool - init(userInfo: [CodingUserInfoKey: Any]) { + init(userInfo: [CodingUserInfoKey: Any], forcingNilValueSetters: Bool = false) { self.userInfo = userInfo + self.forcingNilValueSetters = forcingNilValueSetters } func singleValueContainer() -> SingleValueEncodingContainer { @@ -200,13 +352,13 @@ fileprivate class SQLiteEncoder: Encoder { fatalError("not supported") } - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - return KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self)) + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self, forcingNilValueSetters: forcingNilValueSetters)) } } -fileprivate class SQLiteDecoder : Decoder { - class SQLiteKeyedDecodingContainer : KeyedDecodingContainerProtocol { +private class SQLiteDecoder: Decoder { + class SQLiteKeyedDecodingContainer: KeyedDecodingContainerProtocol { typealias Key = MyKey let codingPath: [CodingKey] = [] @@ -217,102 +369,171 @@ fileprivate class SQLiteDecoder : Decoder { } var allKeys: [Key] { - return self.row.columnNames.keys.flatMap({Key(stringValue: $0)}) + row.columnNames.keys.compactMap({ Key(stringValue: $0) }) } func contains(_ key: Key) -> Bool { - return self.row.hasValue(for: key.stringValue) + row.hasValue(for: key.stringValue) } func decodeNil(forKey key: Key) throws -> Bool { - return !self.contains(key) + !contains(key) } func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { - return try self.row.get(Expression(key.stringValue)) + try row.get(Expression(key.stringValue)) } func decode(_ type: Int.Type, forKey key: Key) throws -> Int { - return try self.row.get(Expression(key.stringValue)) + try row.get(Expression(key.stringValue)) } func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int8 is not supported")) + 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: self.codingPath, debugDescription: "decoding an Int16 is not supported")) + 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: self.codingPath, debugDescription: "decoding an Int32 is not supported")) + 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 { - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt64 is not supported")) + try row.get(Expression(key.stringValue)) } func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt is not supported")) + 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: self.codingPath, debugDescription: "decoding an UInt8 is not supported")) + 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: self.codingPath, debugDescription: "decoding an UInt16 is not supported")) + 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: self.codingPath, debugDescription: "decoding an UInt32 is not supported")) + 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: self.codingPath, debugDescription: "decoding an UInt64 is not supported")) + 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 { - return Float(try self.row.get(Expression(key.stringValue))) + Float(try row.get(Expression(key.stringValue))) } func decode(_ type: Double.Type, forKey key: Key) throws -> Double { - return try self.row.get(Expression(key.stringValue)) + try row.get(Expression(key.stringValue)) } func decode(_ type: String.Type, forKey key: Key) throws -> String { - return try self.row.get(Expression(key.stringValue)) + try row.get(Expression(key.stringValue)) } func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Swift.Decodable { - if type == Data.self { - let data = try self.row.get(Expression(key.stringValue)) + // 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) } - guard let JSONString = try self.row.get(Expression(key.stringValue)) else { - throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "an unsupported type was found")) - } - guard let data = JSONString.data(using: .utf8) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "invalid utf8 data found")) + } + + 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) } - 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: self.codingPath, debugDescription: "decoding nested containers is not supported")) + 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: self.codingPath, debugDescription: "decoding unkeyed containers is not supported")) + 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: self.codingPath, debugDescription: "decoding super encoders containers is not supported")) + 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: self.codingPath, debugDescription: "decoding super decoders is not supported")) + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "decoding super decoders is not supported")) } } @@ -325,16 +546,17 @@ fileprivate class SQLiteDecoder : Decoder { self.userInfo = userInfo } - func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { - return KeyedDecodingContainer(SQLiteKeyedDecodingContainer(row: self.row)) + 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: self.codingPath, debugDescription: "decoding an unkeyed container is not supported")) + 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: self.codingPath, debugDescription: "decoding a single value container is not supported")) + 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 index e2ff9d10..fec66129 100644 --- a/Sources/SQLite/Typed/Collation.swift +++ b/Sources/SQLite/Typed/Collation.swift @@ -43,17 +43,17 @@ public enum Collation { } -extension Collation : Expressible { +extension Collation: Expressible { public var expression: Expression { - return Expression(literal: description) + Expression(literal: description) } } -extension Collation : CustomStringConvertible { +extension Collation: CustomStringConvertible { - public var description : String { + public var description: String { switch self { case .binary: return "BINARY" diff --git a/Sources/SQLite/Typed/CoreFunctions.swift b/Sources/SQLite/Typed/CoreFunctions.swift index 1afe4e87..c4359d8b 100644 --- a/Sources/SQLite/Typed/CoreFunctions.swift +++ b/Sources/SQLite/Typed/CoreFunctions.swift @@ -21,11 +21,44 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // - import Foundation +private enum Function: String { + case abs + case round + case random + case randomblob + case zeroblob + case length + case lower + case upper + case ltrim + case rtrim + case trim + case replace + case substr + case like = "LIKE" + case `in` = "IN" + case glob = "GLOB" + case match = "MATCH" + case regexp = "REGEXP" + case collate = "COLLATE" + case ifnull + + func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { + self.rawValue.infix(lhs, rhs, wrap: wrap) + } + + func wrap(_ expression: Expressible) -> Expression { + self.rawValue.wrap(expression) + } + + func wrap(_ expressions: [Expressible]) -> Expression { + self.rawValue.wrap(", ".join(expressions)) + } +} -extension ExpressionType where UnderlyingType : Number { +extension ExpressionType where UnderlyingType: Number { /// Builds a copy of the expression wrapped with the `abs` function. /// @@ -34,13 +67,13 @@ extension ExpressionType where UnderlyingType : Number { /// // abs("x") /// /// - Returns: A copy of the expression wrapped with the `abs` function. - public var absoluteValue : Expression { - return "abs".wrap(self) + public var absoluteValue: Expression { + Function.abs.wrap(self) } } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Number { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Number { /// Builds a copy of the expression wrapped with the `abs` function. /// @@ -49,8 +82,8 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr /// // abs("x") /// /// - Returns: A copy of the expression wrapped with the `abs` function. - public var absoluteValue : Expression { - return "abs".wrap(self) + public var absoluteValue: Expression { + Function.abs.wrap(self) } } @@ -67,10 +100,10 @@ extension ExpressionType where UnderlyingType == Double { /// /// - Returns: A copy of the expression wrapped with the `round` function. public func round(_ precision: Int? = nil) -> Expression { - guard let precision = precision else { - return wrap([self]) + guard let precision else { + return Function.round.wrap([self]) } - return wrap([self, Int(precision)]) + return Function.round.wrap([self, Int(precision)]) } } @@ -87,15 +120,15 @@ extension ExpressionType where UnderlyingType == Double? { /// /// - Returns: A copy of the expression wrapped with the `round` function. public func round(_ precision: Int? = nil) -> Expression { - guard let precision = precision else { - return wrap(self) + guard let precision else { + return Function.round.wrap(self) } - return wrap([self, Int(precision)]) + return Function.round.wrap([self, Int(precision)]) } } -extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype == Int64 { +extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype == Int64 { /// Builds an expression representing the `random` function. /// @@ -104,7 +137,7 @@ extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype = /// /// - Returns: An expression calling the `random` function. public static func random() -> Expression { - return "random".wrap([]) + Function.random.wrap([]) } } @@ -120,7 +153,7 @@ extension ExpressionType where UnderlyingType == Data { /// /// - Returns: An expression calling the `randomblob` function. public static func random(_ length: Int) -> Expression { - return "randomblob".wrap([]) + Function.randomblob.wrap([]) } /// Builds an expression representing the `zeroblob` function. @@ -132,7 +165,7 @@ extension ExpressionType where UnderlyingType == Data { /// /// - Returns: An expression calling the `zeroblob` function. public static func allZeros(_ length: Int) -> Expression { - return "zeroblob".wrap([]) + Function.zeroblob.wrap([]) } /// Builds a copy of the expression wrapped with the `length` function. @@ -143,7 +176,7 @@ extension ExpressionType where UnderlyingType == Data { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } } @@ -158,7 +191,7 @@ extension ExpressionType where UnderlyingType == Data? { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } } @@ -173,7 +206,7 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } /// Builds a copy of the expression wrapped with the `lower` function. @@ -184,18 +217,18 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `lower` function. public var lowercaseString: Expression { - return "lower".wrap(self) + Function.lower.wrap(self) } /// Builds a copy of the expression wrapped with the `upper` function. /// /// let name = Expression("name") /// name.uppercaseString - /// // lower("name") + /// // upper("name") /// /// - Returns: A copy of the expression wrapped with the `upper` function. public var uppercaseString: Expression { - return "upper".wrap(self) + Function.upper.wrap(self) } /// Builds a copy of the expression appended with a `LIKE` query against the @@ -217,7 +250,7 @@ extension ExpressionType where UnderlyingType == String { /// - 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 = character else { + guard let character else { return "LIKE".infix(self, pattern) } return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) @@ -241,10 +274,10 @@ extension ExpressionType where UnderlyingType == String { /// - 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 = character else { - return "LIKE".infix(self, pattern) + guard let character else { + return Function.like.infix(self, pattern) } - let like: Expression = "LIKE".infix(self, pattern, wrap: false) + let like: Expression = Function.like.infix(self, pattern, wrap: false) return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) } @@ -260,7 +293,7 @@ extension ExpressionType where UnderlyingType == String { /// - Returns: A copy of the expression appended with a `GLOB` query against /// the given pattern. public func glob(_ pattern: String) -> Expression { - return "GLOB".infix(self, pattern) + Function.glob.infix(self, pattern) } /// Builds a copy of the expression appended with a `MATCH` query against @@ -275,7 +308,7 @@ extension ExpressionType where UnderlyingType == String { /// - Returns: A copy of the expression appended with a `MATCH` query /// against the given pattern. public func match(_ pattern: String) -> Expression { - return "MATCH".infix(self, pattern) + Function.match.infix(self, pattern) } /// Builds a copy of the expression appended with a `REGEXP` query against @@ -286,7 +319,7 @@ extension ExpressionType where UnderlyingType == String { /// - Returns: A copy of the expression appended with a `REGEXP` query /// against the given pattern. public func regexp(_ pattern: String) -> Expression { - return "REGEXP".infix(self, pattern) + Function.regexp.infix(self, pattern) } /// Builds a copy of the expression appended with a `COLLATE` clause with @@ -301,7 +334,7 @@ extension ExpressionType where UnderlyingType == String { /// - Returns: A copy of the expression appended with a `COLLATE` clause /// with the given sequence. public func collate(_ collation: Collation) -> Expression { - return "COLLATE".infix(self, collation) + Function.collate.infix(self, collation) } /// Builds a copy of the expression wrapped with the `ltrim` function. @@ -316,10 +349,10 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `ltrim` function. public func ltrim(_ characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + guard let characters else { + return Function.ltrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.ltrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `rtrim` function. @@ -334,10 +367,10 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `rtrim` function. public func rtrim(_ characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + guard let characters else { + return Function.rtrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.rtrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `trim` function. @@ -352,10 +385,10 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `trim` function. public func trim(_ characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap([self]) + guard let characters else { + return Function.trim.wrap([self]) } - return wrap([self, String(characters)]) + return Function.trim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `replace` function. @@ -372,18 +405,18 @@ extension ExpressionType where UnderlyingType == String { /// /// - Returns: A copy of the expression wrapped with the `replace` function. public func replace(_ pattern: String, with replacement: String) -> Expression { - return "replace".wrap([self, pattern, replacement]) + Function.replace.wrap([self, pattern, replacement]) } public func substring(_ location: Int, length: Int? = nil) -> Expression { - guard let length = length else { - return "substr".wrap([self, location]) + guard let length else { + return Function.substr.wrap([self, location]) } - return "substr".wrap([self, location, length]) + return Function.substr.wrap([self, location, length]) } public subscript(range: Range) -> Expression { - return substring(range.lowerBound, length: range.upperBound - range.lowerBound) + substring(range.lowerBound, length: range.upperBound - range.lowerBound) } } @@ -398,7 +431,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `length` function. public var length: Expression { - return wrap(self) + Function.length.wrap(self) } /// Builds a copy of the expression wrapped with the `lower` function. @@ -409,7 +442,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `lower` function. public var lowercaseString: Expression { - return "lower".wrap(self) + Function.lower.wrap(self) } /// Builds a copy of the expression wrapped with the `upper` function. @@ -420,7 +453,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `upper` function. public var uppercaseString: Expression { - return "upper".wrap(self) + Function.upper.wrap(self) } /// Builds a copy of the expression appended with a `LIKE` query against the @@ -442,12 +475,12 @@ extension ExpressionType where UnderlyingType == String? { /// - 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 = character else { - return "LIKE".infix(self, pattern) + 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. /// @@ -466,10 +499,10 @@ extension ExpressionType where UnderlyingType == String? { /// - 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 = character else { - return "LIKE".infix(self, pattern) + guard let character else { + return Function.like.infix(self, pattern) } - let like: Expression = "LIKE".infix(self, pattern, wrap: false) + let like: Expression = Function.like.infix(self, pattern, wrap: false) return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) } @@ -485,7 +518,7 @@ extension ExpressionType where UnderlyingType == String? { /// - Returns: A copy of the expression appended with a `GLOB` query against /// the given pattern. public func glob(_ pattern: String) -> Expression { - return "GLOB".infix(self, pattern) + Function.glob.infix(self, pattern) } /// Builds a copy of the expression appended with a `MATCH` query against @@ -500,7 +533,7 @@ extension ExpressionType where UnderlyingType == String? { /// - Returns: A copy of the expression appended with a `MATCH` query /// against the given pattern. public func match(_ pattern: String) -> Expression { - return "MATCH".infix(self, pattern) + Function.match.infix(self, pattern) } /// Builds a copy of the expression appended with a `REGEXP` query against @@ -511,7 +544,7 @@ extension ExpressionType where UnderlyingType == String? { /// - Returns: A copy of the expression appended with a `REGEXP` query /// against the given pattern. public func regexp(_ pattern: String) -> Expression { - return "REGEXP".infix(self, pattern) + Function.regexp.infix(self, pattern) } /// Builds a copy of the expression appended with a `COLLATE` clause with @@ -526,7 +559,7 @@ extension ExpressionType where UnderlyingType == String? { /// - Returns: A copy of the expression appended with a `COLLATE` clause /// with the given sequence. public func collate(_ collation: Collation) -> Expression { - return "COLLATE".infix(self, collation) + Function.collate.infix(self, collation) } /// Builds a copy of the expression wrapped with the `ltrim` function. @@ -541,10 +574,10 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `ltrim` function. public func ltrim(_ characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + guard let characters else { + return Function.ltrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.ltrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `rtrim` function. @@ -559,10 +592,10 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `rtrim` function. public func rtrim(_ characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + guard let characters else { + return Function.rtrim.wrap(self) } - return wrap([self, String(characters)]) + return Function.rtrim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `trim` function. @@ -577,10 +610,10 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `trim` function. public func trim(_ characters: Set? = nil) -> Expression { - guard let characters = characters else { - return wrap(self) + guard let characters else { + return Function.trim.wrap(self) } - return wrap([self, String(characters)]) + return Function.trim.wrap([self, String(characters)]) } /// Builds a copy of the expression wrapped with the `replace` function. @@ -597,7 +630,7 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `replace` function. public func replace(_ pattern: String, with replacement: String) -> Expression { - return "replace".wrap([self, pattern, replacement]) + Function.replace.wrap([self, pattern, replacement]) } /// Builds a copy of the expression wrapped with the `substr` function. @@ -616,10 +649,10 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `substr` function. public func substring(_ location: Int, length: Int? = nil) -> Expression { - guard let length = length else { - return "substr".wrap([self, location]) + guard let length else { + return Function.substr.wrap([self, location]) } - return "substr".wrap([self, location, length]) + return Function.substr.wrap([self, location, length]) } /// Builds a copy of the expression wrapped with the `substr` function. @@ -632,12 +665,12 @@ extension ExpressionType where UnderlyingType == String? { /// /// - Returns: A copy of the expression wrapped with the `substr` function. public subscript(range: Range) -> Expression { - return substring(range.lowerBound, length: range.upperBound - range.lowerBound) + substring(range.lowerBound, length: range.upperBound - range.lowerBound) } } -extension Collection where Iterator.Element : Value, IndexDistance == Int { +extension Collection where Iterator.Element: Value { /// Builds a copy of the expression prepended with an `IN` check against the /// collection. @@ -652,7 +685,7 @@ extension Collection where Iterator.Element : Value, IndexDistance == Int { /// the collection. public func contains(_ expression: Expression) -> Expression { let templates = [String](repeating: "?", count: count).joined(separator: ", ") - return "IN".infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + return Function.in.infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) } /// Builds a copy of the expression prepended with an `IN` check against the @@ -668,13 +701,13 @@ extension Collection where Iterator.Element : Value, IndexDistance == Int { /// the collection. public func contains(_ expression: Expression) -> Expression { let templates = [String](repeating: "?", count: count).joined(separator: ", ") - return "IN".infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + 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. /// @@ -693,10 +726,10 @@ extension String { /// - 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 = character else { - return "LIKE".infix(self, pattern) + guard let character else { + return Function.like.infix(self, pattern) } - let like: Expression = "LIKE".infix(self, pattern, wrap: false) + let like: Expression = Function.like.infix(self, pattern, wrap: false) return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) } @@ -717,8 +750,8 @@ extension String { /// /// - Returns: A copy of the given expressions wrapped with the `ifnull` /// function. -public func ??(optional: Expression, defaultValue: V) -> Expression { - return "ifnull".wrap([optional, defaultValue]) +public func ??(optional: Expression, defaultValue: V) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) } /// Builds a copy of the given expressions wrapped with the `ifnull` function. @@ -737,8 +770,8 @@ public func ??(optional: Expression, defaultValue: V) -> Expressi /// /// - Returns: A copy of the given expressions wrapped with the `ifnull` /// function. -public func ??(optional: Expression, defaultValue: Expression) -> Expression { - return "ifnull".wrap([optional, defaultValue]) +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) } /// Builds a copy of the given expressions wrapped with the `ifnull` function. @@ -757,6 +790,6 @@ public func ??(optional: Expression, defaultValue: Expression) /// /// - Returns: A copy of the given expressions wrapped with the `ifnull` /// function. -public func ??(optional: Expression, defaultValue: Expression) -> Expression { - return "ifnull".wrap([optional, defaultValue]) +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + Function.ifnull.wrap([optional, defaultValue]) } diff --git a/Sources/SQLite/Typed/CustomFunctions.swift b/Sources/SQLite/Typed/CustomFunctions.swift index 2389901f..0eaa8cf3 100644 --- a/Sources/SQLite/Typed/CustomFunctions.swift +++ b/Sources/SQLite/Typed/CustomFunctions.swift @@ -39,83 +39,107 @@ public extension Connection { /// The assigned types must be explicit. /// /// - Returns: A closure returning an SQL expression to call the function. - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z) throws -> (() -> Expression) { + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z) throws + -> () -> Expression { let fn = try createFunction(function, 0, deterministic) { _ in block() } return { fn([]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z?) throws -> (() -> Expression) { + func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z?) throws + -> () -> Expression { let fn = try createFunction(function, 0, deterministic) { _ in block() } return { fn([]) } } // MARK: - - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A) -> Z) throws -> ((Expression) -> Expression) { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?) -> Z) throws -> ((Expression) -> Expression) { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A) -> Z?) throws -> ((Expression) -> Expression) { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?) -> Z?) throws -> ((Expression) -> Expression) { + 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: - - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B) -> Z) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B) -> Z) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B?) -> Z) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B) -> Z?) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B?) -> Z) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B) -> Z?) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B?) -> Z?) throws -> (Expression, Expression) -> Expression { + 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]) } } - public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B?) -> Z?) throws -> (Expression, Expression) -> Expression { + 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) { + 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 } @@ -124,7 +148,9 @@ public extension Connection { } } - fileprivate func createFunction(_ function: String, _ argumentCount: UInt, _ deterministic: Bool, _ block: @escaping ([Binding?]) -> Z?) throws -> (([Expressible]) -> Expression) { + 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 } diff --git a/Sources/SQLite/Typed/DateAndTimeFunctions.swift b/Sources/SQLite/Typed/DateAndTimeFunctions.swift index 0b9a497f..b4382194 100644 --- a/Sources/SQLite/Typed/DateAndTimeFunctions.swift +++ b/Sources/SQLite/Typed/DateAndTimeFunctions.swift @@ -32,23 +32,23 @@ import Foundation public class DateFunctions { /// The date() function returns the date in this format: YYYY-MM-DD. public static func date(_ timestring: String, _ modifiers: String...) -> Expression { - return timefunction("date", timestring: timestring, modifiers: modifiers) + 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 { - return timefunction("time", timestring: timestring, modifiers: modifiers) + 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 { - return timefunction("datetime", timestring: timestring, modifiers: modifiers) + 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 { - return timefunction("julianday", timestring: timestring, modifiers: modifiers) + timefunction("julianday", timestring: timestring, modifiers: modifiers) } /// The strftime() routine returns the date formatted according to the format string specified as the first argument. @@ -71,36 +71,36 @@ public class DateFunctions { extension Date { public var date: Expression { - return DateFunctions.date(dateFormatter.string(from: self)) + DateFunctions.date(dateFormatter.string(from: self)) } public var time: Expression { - return DateFunctions.time(dateFormatter.string(from: self)) + DateFunctions.time(dateFormatter.string(from: self)) } public var datetime: Expression { - return DateFunctions.datetime(dateFormatter.string(from: self)) + DateFunctions.datetime(dateFormatter.string(from: self)) } public var julianday: Expression { - return DateFunctions.julianday(dateFormatter.string(from: self)) + DateFunctions.julianday(dateFormatter.string(from: self)) } } extension Expression where UnderlyingType == Date { public var date: Expression { - return Expression("date(\(template))", bindings) + Expression("date(\(template))", bindings) } public var time: Expression { - return Expression("time(\(template))", bindings) + Expression("time(\(template))", bindings) } public var datetime: Expression { - return Expression("datetime(\(template))", bindings) + Expression("datetime(\(template))", bindings) } public var julianday: Expression { - return Expression("julianday(\(template))", bindings) + Expression("julianday(\(template))", bindings) } } diff --git a/Sources/SQLite/Typed/Expression.swift b/Sources/SQLite/Typed/Expression.swift index d89ee6cc..dcc44fe4 100644 --- a/Sources/SQLite/Typed/Expression.swift +++ b/Sources/SQLite/Typed/Expression.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public protocol ExpressionType : Expressible { // extensions cannot have inheritance clauses +public protocol ExpressionType: Expressible, CustomStringConvertible { // extensions cannot have inheritance clauses associatedtype UnderlyingType = Void @@ -43,14 +43,17 @@ extension ExpressionType { self.init(literal: identifier.quote()) } - public init(_ expression: U) { + public init(_ expression: U) { self.init(expression.template, expression.bindings) } + public var description: String { + asSQL() + } } /// An `Expression` represents a raw SQL fragment and any associated bindings. -public struct Expression : ExpressionType { +public struct Expression: ExpressionType { public typealias UnderlyingType = Datatype @@ -73,42 +76,38 @@ public protocol Expressible { extension Expressible { // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE - // FIXME: make internal (0.12.0) - public func asSQL() -> String { + func asSQL() -> String { let expressed = expression - var idx = 0 - return expressed.template.reduce("") { template, character in - let transcoded: String - + return expressed.template.reduce(("", 0)) { memo, character in + let (template, index) = memo + if character == "?" { - transcoded = transcode(expressed.bindings[idx]) - idx += 1 + precondition(index < expressed.bindings.count, "not enough bindings for expression") + return (template + transcode(expressed.bindings[index]), index + 1) } else { - transcoded = String(character) + return (template + String(character), index) } - return template + transcoded - } + }.0 } - } extension ExpressionType { public var expression: Expression { - return Expression(template, bindings) + Expression(template, bindings) } public var asc: Expressible { - return " ".join([self, Expression(literal: "ASC")]) + " ".join([self, Expression(literal: "ASC")]) } public var desc: Expressible { - return " ".join([self, Expression(literal: "DESC")]) + " ".join([self, Expression(literal: "DESC")]) } } -extension ExpressionType where UnderlyingType : Value { +extension ExpressionType where UnderlyingType: Value { public init(value: UnderlyingType) { self.init("?", [value.datatypeValue]) @@ -116,10 +115,10 @@ extension ExpressionType where UnderlyingType : Value { } -extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value { +extension ExpressionType where UnderlyingType: _OptionalType, UnderlyingType.WrappedType: Value { public static var null: Self { - return self.init(value: nil) + self.init(value: nil) } public init(value: UnderlyingType.WrappedType?) { @@ -131,7 +130,7 @@ extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.Wr extension Value { public var expression: Expression { - return Expression(value: self).expression + Expression(value: self).expression } } @@ -139,9 +138,9 @@ extension Value { public let rowid = Expression("ROWID") public func cast(_ expression: Expression) -> Expression { - return Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) + Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) } public func cast(_ expression: Expression) -> Expression { - return Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) + Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) } diff --git a/Sources/SQLite/Typed/Operators.swift b/Sources/SQLite/Typed/Operators.swift index d97e52b9..1c611cbc 100644 --- a/Sources/SQLite/Typed/Operators.swift +++ b/Sources/SQLite/Typed/Operators.swift @@ -24,551 +24,651 @@ // 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 { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: Expression, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: Expression, rhs: String) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: Expression, rhs: String) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: String, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } public func +(lhs: String, rhs: Expression) -> Expression { - return "||".infix(lhs, rhs) + Operator.concatenate.infix(lhs, rhs) } // MARK: - -public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return wrap(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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return (~(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 { - return (~(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 { - return (~(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 { - return (~(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 { - return (~(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 { - return (~(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 { - return (~(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 { - return (~(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 { - return wrap(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 { - return 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 { - return "=".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 { - return "=".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 { - return "=".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 { - return "=".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 { - return "=".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 = rhs else { return "IS".infix(lhs, Expression(value: nil)) } - return "=".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 { - return "=".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 = lhs else { return "IS".infix(Expression(value: nil), rhs) } - return "=".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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 = rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } - return 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 { - return 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 = lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 : Comparable { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return infix(lhs, rhs) +public func >=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Comparable { + Operator.gte.infix(lhs, rhs) } -public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { - return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) +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: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { - return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) +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 { - return 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: Range, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { - return 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 { - return 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: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { - return 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 { - return 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 { - return 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 { - return 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]) } -public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { - return 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 { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Expression, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Expression, rhs: Bool) -> Expression { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Expression, rhs: Bool) -> Expression { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Bool, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) + Operator.and.infix(lhs, rhs) } public func &&(lhs: Bool, rhs: Expression) -> Expression { - return "AND".infix(lhs, rhs) + 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 { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Expression, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Expression, rhs: Bool) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Expression, rhs: Bool) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Bool, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public func ||(lhs: Bool, rhs: Expression) -> Expression { - return "OR".infix(lhs, rhs) + Operator.or.infix(lhs, rhs) } public prefix func !(rhs: Expression) -> Expression { - return "NOT ".wrap(rhs) + Operator.not.wrap(rhs) } + public prefix func !(rhs: Expression) -> Expression { - return "NOT ".wrap(rhs) + 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 index 1d04b797..6162fcc7 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -21,10 +21,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // - import Foundation -public protocol QueryType : Expressible { +public protocol QueryType: Expressible { var clauses: QueryClauses { get set } @@ -32,7 +31,7 @@ public protocol QueryType : Expressible { } -public protocol SchemaType : QueryType { +public protocol SchemaType: QueryType { static var identifier: String { get } @@ -53,7 +52,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT` clause applied. public func select(_ column1: Expressible, _ more: Expressible...) -> Self { - return select(false, [column1] + more) + select(false, [column1] + more) } /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. @@ -68,7 +67,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT DISTINCT` clause applied. public func select(distinct column1: Expressible, _ more: Expressible...) -> Self { - return select(true, [column1] + more) + select(true, [column1] + more) } /// Builds a copy of the query with the `SELECT` clause applied. @@ -84,7 +83,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT` clause applied. public func select(_ all: [Expressible]) -> Self { - return select(false, all) + select(false, all) } /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. @@ -99,7 +98,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT DISTINCT` clause applied. public func select(distinct columns: [Expressible]) -> Self { - return select(true, columns) + select(true, columns) } /// Builds a copy of the query with the `SELECT *` clause applied. @@ -113,7 +112,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT *` clause applied. public func select(_ star: Star) -> Self { - return select([star(nil, nil)]) + select([star(nil, nil)]) } /// Builds a copy of the query with the `SELECT DISTINCT *` clause applied. @@ -127,7 +126,7 @@ extension SchemaType { /// /// - Returns: A query with the given `SELECT DISTINCT *` clause applied. public func select(distinct star: Star) -> Self { - return select(distinct: [star(nil, nil)]) + select(distinct: [star(nil, nil)]) } /// Builds a scalar copy of the query with the `SELECT` clause applied. @@ -141,11 +140,11 @@ extension SchemaType { /// - Parameter all: A list of expressions to select. /// /// - Returns: A query with the given `SELECT` clause applied. - public func select(_ column: Expression) -> ScalarQuery { - return select(false, [column]) + public func select(_ column: Expression) -> ScalarQuery { + select(false, [column]) } - public func select(_ column: Expression) -> ScalarQuery { - return select(false, [column]) + public func select(_ column: Expression) -> ScalarQuery { + select(false, [column]) } /// Builds a scalar copy of the query with the `SELECT DISTINCT` clause @@ -160,22 +159,22 @@ extension SchemaType { /// - Parameter column: A list of expressions to select. /// /// - Returns: A query with the given `SELECT DISTINCT` clause applied. - public func select(distinct column: Expression) -> ScalarQuery { - return select(true, [column]) + public func select(distinct column: Expression) -> ScalarQuery { + select(true, [column]) } - public func select(distinct column: Expression) -> ScalarQuery { - return select(true, [column]) + public func select(distinct column: Expression) -> ScalarQuery { + select(true, [column]) } public var count: ScalarQuery { - return select(Expression.count(*)) + select(Expression.count(*)) } } extension QueryType { - fileprivate func select(_ distinct: Bool, _ columns: [Expressible]) -> Q { + fileprivate func select(_ distinct: Bool, _ columns: [Expressible]) -> Q { var query = Q.init(clauses.from.name, database: clauses.from.database) query.clauses = clauses query.clauses.select = (distinct, columns) @@ -183,7 +182,7 @@ extension QueryType { } // MARK: UNION - + /// Adds a `UNION` clause to the query. /// /// let users = Table("users") @@ -194,15 +193,17 @@ extension QueryType { /// /// - 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(_ table: QueryType) -> Self { + public func union(all: Bool = false, _ table: QueryType) -> Self { var query = self - query.clauses.union.append(table) + query.clauses.union.append((all, table)) return query } - + // MARK: JOIN /// Adds a `JOIN` clause to the query. @@ -223,7 +224,7 @@ extension QueryType { /// /// - Returns: A query with the given `JOIN` clause applied. public func join(_ table: QueryType, on condition: Expression) -> Self { - return join(table, on: Expression(condition)) + join(table, on: Expression(condition)) } /// Adds a `JOIN` clause to the query. @@ -244,7 +245,7 @@ extension QueryType { /// /// - Returns: A query with the given `JOIN` clause applied. public func join(_ table: QueryType, on condition: Expression) -> Self { - return join(.inner, table, on: condition) + join(.inner, table, on: condition) } /// Adds a `JOIN` clause to the query. @@ -267,7 +268,7 @@ extension QueryType { /// /// - Returns: A query with the given `JOIN` clause applied. public func join(_ type: JoinType, _ table: QueryType, on condition: Expression) -> Self { - return join(type, table, on: Expression(condition)) + join(type, table, on: Expression(condition)) } /// Adds a `JOIN` clause to the query. @@ -291,7 +292,8 @@ extension QueryType { /// - 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)) + query.clauses.join.append((type: type, query: table, + condition: table.clauses.filters.map { condition && $0 } ?? condition as Expressible)) return query } @@ -309,7 +311,7 @@ extension QueryType { /// /// - Returns: A query with the given `WHERE` clause applied. public func filter(_ predicate: Expression) -> Self { - return filter(Expression(predicate)) + filter(Expression(predicate)) } /// Adds a condition to the query’s `WHERE` clause. @@ -332,13 +334,13 @@ extension QueryType { /// Adds a condition to the query’s `WHERE` clause. /// This is an alias for `filter(predicate)` public func `where`(_ predicate: Expression) -> Self { - return `where`(Expression(predicate)) + `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 { - return filter(predicate) + filter(predicate) } // MARK: GROUP BY @@ -349,7 +351,7 @@ extension QueryType { /// /// - Returns: A query with the given `GROUP BY` clause applied. public func group(_ by: Expressible...) -> Self { - return group(by) + group(by) } /// Sets a `GROUP BY` clause on the query. @@ -358,7 +360,7 @@ extension QueryType { /// /// - Returns: A query with the given `GROUP BY` clause applied. public func group(_ by: [Expressible]) -> Self { - return group(by, nil) + group(by, nil) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -371,7 +373,7 @@ extension QueryType { /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. public func group(_ by: Expressible, having: Expression) -> Self { - return group([by], having: having) + group([by], having: having) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -384,7 +386,7 @@ extension QueryType { /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. public func group(_ by: Expressible, having: Expression) -> Self { - return group([by], having: having) + group([by], having: having) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -397,7 +399,7 @@ extension QueryType { /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. public func group(_ by: [Expressible], having: Expression) -> Self { - return group(by, Expression(having)) + group(by, Expression(having)) } /// Sets a `GROUP BY`-`HAVING` clause on the query. @@ -410,7 +412,7 @@ extension QueryType { /// /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. public func group(_ by: [Expressible], having: Expression) -> Self { - return group(by, having) + group(by, having) } fileprivate func group(_ by: [Expressible], _ having: Expression?) -> Self { @@ -434,7 +436,7 @@ extension QueryType { /// /// - Returns: A query with the given `ORDER BY` clause applied. public func order(_ by: Expressible...) -> Self { - return order(by) + order(by) } /// Sets an `ORDER BY` clause on the query. @@ -469,7 +471,7 @@ extension QueryType { /// /// - Returns: A query with the given LIMIT clause applied. public func limit(_ length: Int?) -> Self { - return limit(length, nil) + limit(length, nil) } /// Sets LIMIT and OFFSET clauses on the query. @@ -487,7 +489,7 @@ extension QueryType { /// /// - Returns: A query with the given LIMIT and OFFSET clauses applied. public func limit(_ length: Int, offset: Int) -> Self { - return limit(length, offset) + limit(length, offset) } // prevents limit(nil, offset: 5) @@ -504,12 +506,13 @@ extension QueryType { // MARK: - fileprivate var selectClause: Expressible { - return " ".join([ - Expression(literal: clauses.select.distinct ? "SELECT DISTINCT" : "SELECT"), - ", ".join(clauses.select.columns), - Expression(literal: "FROM"), - tableName(alias: true) - ]) + " ".join([ + Expression(literal: + clauses.select.distinct ? "SELECT DISTINCT" : "SELECT"), + ", ".join(clauses.select.columns), + Expression(literal: "FROM"), + tableName(alias: true) + ]) } fileprivate var joinClause: Expressible? { @@ -589,15 +592,15 @@ extension QueryType { Expression(literal: "OFFSET \(offset)") ]) } - + fileprivate var unionClause: Expressible? { guard !clauses.union.isEmpty else { return nil } - - return " ".join(clauses.union.map { query in + + return " ".join(clauses.union.map { (all, query) in " ".join([ - Expression(literal: "UNION"), + Expression(literal: all ? "UNION ALL" : "UNION"), query ]) }) @@ -616,19 +619,31 @@ extension QueryType { // MARK: INSERT public func insert(_ value: Setter, _ more: Setter...) -> Insert { - return insert([value] + more) + insert([value] + more) } public func insert(_ values: [Setter]) -> Insert { - return insert(nil, values) + insert(nil, values) } public func insert(or onConflict: OnConflict, _ values: Setter...) -> Insert { - return insert(or: onConflict, values) + insert(or: onConflict, values) } public func insert(or onConflict: OnConflict, _ values: [Setter]) -> Insert { - return insert(onConflict, values) + insert(onConflict, values) + } + + public func insertMany( _ values: [[Setter]]) -> Insert { + insertMany(nil, values) + } + + 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 { @@ -647,12 +662,37 @@ extension QueryType { whereClause ] - return Insert(" ".join(clauses.flatMap { $0 }).expression) + return Insert(" ".join(clauses.compactMap { $0 }).expression) + } + + fileprivate func insertMany(_ or: OnConflict?, _ values: [[Setter]]) -> Insert { + guard let firstInsert = values.first else { + // must be at least 1 object or else we don't know columns. Default to default inserts. + return insert() + } + let columns = firstInsert.map { $0.column } + let insertValues = values.map { rowValues in + rowValues.reduce([Expressible]()) { insert, setter in + insert + [setter.value] + } + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + or.map { Expression(literal: "OR \($0.rawValue)") }, + Expression(literal: "INTO"), + tableName(), + "".wrap(columns) as Expression, + Expression(literal: "VALUES"), + ", ".join(insertValues.map({ "".wrap($0) as Expression })), + whereClause + ] + return Insert(" ".join(clauses.compactMap { $0 }).expression) } /// Runs an `INSERT` statement against the query with `DEFAULT VALUES`. public func insert() -> Insert { - return Insert(" ".join([ + Insert(" ".join([ Expression(literal: "INSERT INTO"), tableName(), Expression(literal: "DEFAULT VALUES") @@ -666,17 +706,55 @@ extension QueryType { /// /// - Returns: The number of updated rows and statement. public func insert(_ query: QueryType) -> Update { - return Update(" ".join([ + Update(" ".join([ Expression(literal: "INSERT INTO"), tableName(), query.expression - ]).expression) + ]).expression) + } + + // MARK: UPSERT + + public func upsert(_ insertValues: Setter..., onConflictOf conflicting: Expressible) -> Insert { + upsert(insertValues, onConflictOf: conflicting) + } + + public func upsert(_ insertValues: [Setter], onConflictOf conflicting: Expressible) -> Insert { + let setValues = insertValues.filter { $0.column.asSQL() != conflicting.asSQL() } + .map { Setter(excluded: $0.column) } + return upsert(insertValues, onConflictOf: conflicting, set: setValues) + } + + public func upsert(_ insertValues: Setter..., onConflictOf conflicting: Expressible, set setValues: [Setter]) -> Insert { + upsert(insertValues, onConflictOf: conflicting, set: setValues) + } + + public func upsert(_ insertValues: [Setter], onConflictOf conflicting: Expressible, set setValues: [Setter]) -> Insert { + let insert = insertValues.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in + (insert.columns + [setter.column], insert.values + [setter.value]) + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + Expression(literal: "INTO"), + tableName(), + "".wrap(insert.columns) as Expression, + Expression(literal: "VALUES"), + "".wrap(insert.values) as Expression, + whereClause, + Expression(literal: "ON CONFLICT"), + "".wrap(conflicting) as Expression, + Expression(literal: "DO UPDATE SET"), + ", ".join(setValues.map { $0.expression }) + ] + + return Insert(" ".join(clauses.compactMap { $0 }).expression) } // MARK: UPDATE public func update(_ values: Setter...) -> Update { - return update(values) + update(values) } public func update(_ values: [Setter]) -> Update { @@ -690,7 +768,7 @@ extension QueryType { limitOffsetClause ] - return Update(" ".join(clauses.flatMap { $0 }).expression) + return Update(" ".join(clauses.compactMap { $0 }).expression) } // MARK: DELETE @@ -704,13 +782,13 @@ extension QueryType { limitOffsetClause ] - return Delete(" ".join(clauses.flatMap { $0 }).expression) + return Delete(" ".join(clauses.compactMap { $0 }).expression) } // MARK: EXISTS public var exists: Select { - return Select(" ".join([ + Select(" ".join([ Expression(literal: "SELECT EXISTS"), "".wrap(expression) as Expression ]).expression) @@ -725,15 +803,15 @@ extension QueryType { /// - Returns: A column expression namespaced with the query’s table name or /// alias. public func namespace(_ column: Expression) -> Expression { - return Expression(".".join([tableName(), column]).expression) + Expression(".".join([tableName(), column]).expression) } public subscript(column: Expression) -> Expression { - return namespace(column) + namespace(column) } public subscript(column: Expression) -> Expression { - return namespace(column) + namespace(column) } /// Prefixes a star with the query’s table name or alias. @@ -743,14 +821,14 @@ extension QueryType { /// - Returns: A `*` expression namespaced with the query’s table name or /// alias. public subscript(star: Star) -> Expression { - return namespace(star(nil, nil)) + namespace(star(nil, nil)) } // MARK: - // TODO: alias support func tableName(alias aliased: Bool = false) -> Expressible { - guard let alias = clauses.from.alias , aliased else { + guard let alias = clauses.from.alias, aliased else { return database(namespace: clauses.from.alias ?? clauses.from.name) } @@ -780,6 +858,7 @@ extension QueryType { public var expression: Expression { let clauses: [Expressible?] = [ + withClause, selectClause, joinClause, whereClause, @@ -789,7 +868,7 @@ extension QueryType { limitOffsetClause ] - return " ".join(clauses.flatMap { $0 }).expression + return " ".join(clauses.compactMap { $0 }).expression } } @@ -798,7 +877,7 @@ extension QueryType { /// Queries a collection of chainable helper functions and expressions to build /// executable SQL statements. -public struct Table : SchemaType { +public struct Table: SchemaType { public static let identifier = "TABLE" @@ -810,7 +889,7 @@ public struct Table : SchemaType { } -public struct View : SchemaType { +public struct View: SchemaType { public static let identifier = "VIEW" @@ -822,7 +901,7 @@ public struct View : SchemaType { } -public struct VirtualTable : SchemaType { +public struct VirtualTable: SchemaType { public static let identifier = "VIRTUAL TABLE" @@ -836,7 +915,7 @@ public struct VirtualTable : SchemaType { // TODO: make `ScalarQuery` work in `QueryType.select()`, `.filter()`, etc. -public struct ScalarQuery : QueryType { +public struct ScalarQuery: QueryType { public var clauses: QueryClauses @@ -848,7 +927,7 @@ public struct ScalarQuery : QueryType { // TODO: decide: simplify the below with a boxed type instead -public struct Select : ExpressionType { +public struct Select: ExpressionType { public var template: String public var bindings: [Binding?] @@ -860,7 +939,7 @@ public struct Select : ExpressionType { } -public struct Insert : ExpressionType { +public struct Insert: ExpressionType { public var template: String public var bindings: [Binding?] @@ -872,7 +951,7 @@ public struct Insert : ExpressionType { } -public struct Update : ExpressionType { +public struct Update: ExpressionType { public var template: String public var bindings: [Binding?] @@ -884,7 +963,7 @@ public struct Update : ExpressionType { } -public struct Delete : ExpressionType { +public struct Delete: ExpressionType { public var template: String public var bindings: [Binding?] @@ -896,14 +975,13 @@ public struct Delete : ExpressionType { } - public struct RowIterator: FailableIterator { public typealias Element = Row let statement: Statement let columnNames: [String: Int] public func failableNext() throws -> Row? { - return try statement.failableNext().flatMap { Row(columnNames, $0) } + try statement.failableNext().flatMap { Row(columnNames, $0) } } public func map(_ transform: (Element) throws -> T) throws -> [T] { @@ -913,6 +991,15 @@ public struct RowIterator: FailableIterator { } 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 { @@ -927,7 +1014,6 @@ extension Connection { AnyIterator { statement.next().map { Row(columnNames, $0) } } } } - public func prepareRowIterator(_ query: QueryType) throws -> RowIterator { let expression = query.expression @@ -935,35 +1021,59 @@ extension Connection { 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: ".") - - func expandGlob(_ namespace: Bool) -> ((QueryType) throws -> Void) { - return { (query: QueryType) throws -> (Void) in - var q = type(of: query).init(query.clauses.from.name, database: query.clauses.from.database) - q.clauses.select = query.clauses.select - let e = q.expression - var names = try self.prepare(e.template, e.bindings).columnNames.map { $0.quote() } - if namespace { names = names.map { "\(query.tableName().expression.template).\($0)" } } + + // 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 { - if q.tableName().expression.template == namespace { - try expandGlob(true)(q) - continue column - } - throw QueryError.noSuchTable(name: namespace) + for q in queries where q.tableName().expression.template == namespace { + try expandGlob(true)(q) + continue column } throw QueryError.noSuchTable(name: namespace) } @@ -972,37 +1082,37 @@ extension Connection { } continue } - + columnNames[each.expression.template] = idx idx += 1 } return columnNames } - public func scalar(_ query: ScalarQuery) throws -> V { + public func scalar(_ query: ScalarQuery) throws -> V { let expression = query.expression return value(try scalar(expression.template, expression.bindings)) } - public func scalar(_ query: ScalarQuery) throws -> V.ValueType? { + public func scalar(_ query: ScalarQuery) throws -> V.ValueType? { let expression = query.expression guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) + return try V.fromDatatypeValue(value) } - public func scalar(_ query: Select) throws -> V { + public func scalar(_ query: Select) throws -> V { let expression = query.expression return value(try scalar(expression.template, expression.bindings)) } - public func scalar(_ query: Select) throws -> V.ValueType? { + public func scalar(_ query: Select) throws -> V.ValueType? { let expression = query.expression guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) + return try V.fromDatatypeValue(value) } public func pluck(_ query: QueryType) throws -> Row? { - return try prepareRowIterator(query.limit(1, query.clauses.limit?.offset)).failableNext() + try prepareRowIterator(query.limit(1, query.clauses.limit?.offset)).failableNext() } /// Runs an `Insert` query. @@ -1010,6 +1120,8 @@ extension Connection { /// - SeeAlso: `QueryType.insert(value:_:)` /// - SeeAlso: `QueryType.insert(values:)` /// - SeeAlso: `QueryType.insert(or:_:)` + /// - SeeAlso: `QueryType.insertMany(values:)` + /// - SeeAlso: `QueryType.insertMany(or:_:)` /// - SeeAlso: `QueryType.insert()` /// /// - Parameter query: An insert query. @@ -1019,7 +1131,7 @@ extension Connection { let expression = query.expression return try sync { try self.run(expression.template, expression.bindings) - return self.lastInsertRowid + return lastInsertRowid } } @@ -1035,7 +1147,7 @@ extension Connection { let expression = query.expression return try sync { try self.run(expression.template, expression.bindings) - return self.changes + return changes } } @@ -1050,7 +1162,7 @@ extension Connection { let expression = query.expression return try sync { try self.run(expression.template, expression.bindings) - return self.changes + return changes } } @@ -1088,38 +1200,49 @@ public struct Row { } public func get(_ column: Expression) throws -> V? { - func valueAtIndex(_ idx: Int) -> V? { + func valueAtIndex(_ idx: Int) throws -> V? { guard let value = values[idx] as? V.Datatype else { return nil } - return V.fromDatatypeValue(value) as? V + return try V.fromDatatypeValue(value) as? V } guard let idx = columnNames[column.template] else { - let similar = Array(columnNames.keys).filter { $0.hasSuffix(".\(column.template)") } + func similar(_ name: String) -> Bool { + return name.hasSuffix(".\(column.template)") + } - switch similar.count { - case 0: + guard let firstIndex = columnNames.firstIndex(where: { similar($0.key) }) else { throw QueryError.noSuchColumn(name: column.template, columns: columnNames.keys.sorted()) - case 1: - return valueAtIndex(columnNames[similar[0]]!) - default: - throw QueryError.ambiguousColumn(name: column.template, similar: similar) } + + 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 valueAtIndex(idx) + return try valueAtIndex(idx) } - public subscript(column: Expression) -> T { - return try! get(column) + public subscript(column: Expression) -> T { + // swiftlint:disable:next force_try + try! get(column) } - public subscript(column: Expression) -> T? { - return 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 { +public enum JoinType: String { /// A `CROSS` join. case cross = "CROSS" @@ -1164,12 +1287,13 @@ public struct QueryClauses { var order = [Expressible]() var limit: (length: Int, offset: Int?)? - - var union = [QueryType]() + + var union = [(all: Bool, table: QueryType)]() + + var with = WithClauses() fileprivate init(_ name: String, alias: String?, database: String?) { - self.from = (name, alias, database) + from = (name, alias, database) } } - diff --git a/Sources/SQLite/Typed/Schema.swift b/Sources/SQLite/Typed/Schema.swift index 60b7be8a..919042ab 100644 --- a/Sources/SQLite/Typed/Schema.swift +++ b/Sources/SQLite/Typed/Schema.swift @@ -27,7 +27,7 @@ extension SchemaType { // MARK: - DROP TABLE / VIEW / VIRTUAL TABLE public func drop(ifExists: Bool = false) -> String { - return drop("TABLE", tableName(), ifExists) + drop("TABLE", tableName(), ifExists) } } @@ -36,7 +36,8 @@ extension Table { // MARK: - CREATE TABLE - public func create(temporary: Bool = false, ifNotExists: Bool = false, withoutRowid: Bool = false, block: (TableBuilder) -> Void) -> String { + public func create(temporary: Bool = false, ifNotExists: Bool = false, withoutRowid: Bool = false, + block: (TableBuilder) -> Void) -> String { let builder = TableBuilder() block(builder) @@ -47,7 +48,7 @@ extension Table { withoutRowid ? Expression(literal: "WITHOUT ROWID") : nil ] - return " ".join(clauses.flatMap { $0 }).asSQL() + return " ".join(clauses.compactMap { $0 }).asSQL() } public func create(_ query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { @@ -57,61 +58,69 @@ extension Table { query ] - return " ".join(clauses.flatMap { $0 }).asSQL() + return " ".join(clauses.compactMap { $0 }).asSQL() } // MARK: - ALTER TABLE … ADD COLUMN - public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V) -> String { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) + public func addColumn(_ name: Expression, check: Expression? = 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 { - return 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 { - return addColumn(definition(name, V.declaredDatatype, nil, true, 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 { - return 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 { - return addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression? = nil, + references table: QueryType, _ other: Expression) -> String 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 { - return addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression, + references table: QueryType, _ other: Expression) -> String 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 { - return addColumn(definition(name, V.declaredDatatype, nil, true, 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 { - return addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression, + references table: QueryType, _ other: Expression) -> String 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 { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) + 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 { - return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) + public func addColumn(_ name: Expression, check: Expression, defaultValue: V, + collate: Collation) -> String 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 { - return addColumn(definition(name, V.declaredDatatype, nil, true, 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 { - return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) + public func addColumn(_ name: Expression, check: Expression, defaultValue: V? = nil, + collate: Collation) -> String where V.Datatype == String { + addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) } fileprivate func addColumn(_ expression: Expressible) -> String { - return " ".join([ + " ".join([ Expression(literal: "ALTER TABLE"), tableName(), Expression(literal: "ADD COLUMN"), @@ -122,12 +131,12 @@ extension Table { // MARK: - ALTER TABLE … RENAME TO public func rename(_ to: Table) -> String { - return rename(to: to) + rename(to: to) } // MARK: - CREATE INDEX - public func createIndex(_ columns: Expressible..., unique: Bool = false, ifNotExists: Bool = false) -> String { + 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"), @@ -135,14 +144,21 @@ extension Table { "".wrap(columns) as Expression ] - return " ".join(clauses.flatMap { $0 }).asSQL() + 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 { - return drop("INDEX", indexName(columns), ifExists) + dropIndex(Array(columns), ifExists: ifExists) } fileprivate func indexName(_ columns: [Expressible]) -> Expressible { @@ -174,13 +190,13 @@ extension View { query ] - return " ".join(clauses.flatMap { $0 }).asSQL() + return " ".join(clauses.compactMap { $0 }).asSQL() } // MARK: - DROP VIEW public func drop(ifExists: Bool = false) -> String { - return drop("VIEW", tableName(), ifExists) + drop("VIEW", tableName(), ifExists) } } @@ -196,13 +212,13 @@ extension VirtualTable { using ] - return " ".join(clauses.flatMap { $0 }).asSQL() + return " ".join(clauses.compactMap { $0 }).asSQL() } // MARK: - ALTER TABLE … RENAME TO public func rename(_ to: VirtualTable) -> String { - return rename(to: to) + rename(to: to) } } @@ -211,134 +227,195 @@ public final class TableBuilder { fileprivate var definitions = [Expressible]() - public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) } - fileprivate func column(_ name: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) { + // 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) { + public func primaryKey(_ column: Expression) { primaryKey([column]) } - public func primaryKey(_ compositeA: Expression, _ b: Expression) { - primaryKey([compositeA, b]) + 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, _ b: Expression, _ c: Expression) { - primaryKey([compositeA, b, c]) + public func primaryKey(_ compositeA: Expression, + _ expr1: Expression, + _ expr2: Expression, + _ expr3: Expression) { + primaryKey([compositeA, expr1, expr2, expr3]) } fileprivate func primaryKey(_ composite: [Expressible]) { @@ -375,29 +452,37 @@ public final class TableBuilder { } - public func foreignKey(_ column: Expression, references table: QueryType, _ other: Expression, update: Dependency? = nil, delete: Dependency? = nil) { + 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) { + 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) { + 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) { + 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?) { + fileprivate func foreignKey(_ column: Expressible, _ references: (QueryType, Expressible), + _ update: Dependency?, _ delete: Dependency?) { let clauses: [Expressible?] = [ "FOREIGN KEY".prefix(column), reference(references), @@ -405,7 +490,7 @@ public final class TableBuilder { delete.map { Expression(literal: "ON DELETE \($0.rawValue)") } ] - definitions.append(" ".join(clauses.flatMap { $0 })) + definitions.append(" ".join(clauses.compactMap { $0 })) } } @@ -435,10 +520,10 @@ public struct Module { } -extension Module : Expressible { +extension Module: Expressible { public var expression: Expression { - return name.wrap(arguments) + name.wrap(arguments) } } @@ -456,11 +541,11 @@ private extension QueryType { name ] - return " ".join(clauses.flatMap { $0 }) + return " ".join(clauses.compactMap { $0 }) } func rename(to: Self) -> String { - return " ".join([ + " ".join([ Expression(literal: "ALTER TABLE"), tableName(), Expression(literal: "RENAME TO"), @@ -475,12 +560,15 @@ private extension QueryType { name ] - return " ".join(clauses.flatMap { $0 }).asSQL() + return " ".join(clauses.compactMap { $0 }).asSQL() } } -private func definition(_ column: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) -> Expressible { +// 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), @@ -493,18 +581,18 @@ private func definition(_ column: Expressible, _ datatype: String, _ primaryKey: collate.map { " ".join([Expression(literal: "COLLATE"), $0]) } ] - return " ".join(clauses.flatMap { $0 }) + return " ".join(clauses.compactMap { $0 }) } private func reference(_ primary: (QueryType, Expressible)) -> Expressible { - return " ".join([ + " ".join([ Expression(literal: "REFERENCES"), primary.0.tableName(qualified: false), "".wrap(primary.1) as Expression ]) } -private enum Modifier : String { +private enum Modifier: String { case unique = "UNIQUE" diff --git a/Sources/SQLite/Typed/Setter.swift b/Sources/SQLite/Typed/Setter.swift index 86f16fca..8dc8a0e0 100644 --- a/Sources/SQLite/Typed/Setter.swift +++ b/Sources/SQLite/Typed/Setter.swift @@ -35,243 +35,254 @@ public struct Setter { let column: Expressible let value: Expressible - fileprivate init(column: Expression, value: Expression) { + fileprivate init(column: Expression, value: Expression) { self.column = column self.value = value } - fileprivate init(column: Expression, value: V) { + fileprivate init(column: Expression, value: V) { self.column = column self.value = value } - fileprivate init(column: Expression, value: Expression) { + fileprivate init(column: Expression, value: Expression) { self.column = column self.value = value } - fileprivate init(column: Expression, value: Expression) { + fileprivate init(column: Expression, value: Expression) { self.column = column self.value = value } - fileprivate init(column: Expression, value: V?) { + 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 { +extension Setter: Expressible { public var expression: Expression { - return "=".infix(column, value, wrap: false) + "=".infix(column, value, wrap: false) } } -public func <-(column: Expression, value: Expression) -> Setter { - return Setter(column: column, value: value) +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 { - return Setter(column: column, value: value) +public func <-(column: Expression, value: V) -> Setter { + Setter(column: column, value: value) } -public func <-(column: Expression, value: Expression) -> Setter { - return Setter(column: column, value: value) +public func <-(column: Expression, value: Expression) -> Setter { + Setter(column: column, value: value) } -public func <-(column: Expression, value: Expression) -> Setter { - return Setter(column: column, value: value) +public func <-(column: Expression, value: Expression) -> Setter { + Setter(column: column, value: value) } -public func <-(column: Expression, value: V?) -> Setter { - return Setter(column: column, value: value) +public func <-(column: Expression, value: V?) -> Setter { + Setter(column: column, value: value) } public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value + column <- column + value } public func +=(column: Expression, value: String) -> Setter { - return column <- column + value + column <- column + value } public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value + column <- column + value } public func +=(column: Expression, value: Expression) -> Setter { - return column <- column + value + column <- column + value } public func +=(column: Expression, value: String) -> Setter { - return column <- column + value + column <- column + value } -public func +=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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 { - return 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/Sources/SQLiteObjc/SQLite-Bridging.m b/Sources/SQLiteObjc/SQLite-Bridging.m deleted file mode 100644 index e00a7315..00000000 --- a/Sources/SQLiteObjc/SQLite-Bridging.m +++ /dev/null @@ -1,138 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -#import "SQLite-Bridging.h" -#import "fts3_tokenizer.h" - -#pragma mark - FTS - -typedef struct __SQLiteTokenizer { - sqlite3_tokenizer base; - __unsafe_unretained _SQLiteTokenizerNextCallback callback; -} __SQLiteTokenizer; - -typedef struct __SQLiteTokenizerCursor { - void * base; - const char * input; - int inputOffset; - int inputLength; - int idx; -} __SQLiteTokenizerCursor; - -static NSMutableDictionary * __SQLiteTokenizerMap; - -static int __SQLiteTokenizerCreate(int argc, const char * const * argv, sqlite3_tokenizer ** ppTokenizer) { - __SQLiteTokenizer * tokenizer = (__SQLiteTokenizer *)sqlite3_malloc(sizeof(__SQLiteTokenizer)); - if (!tokenizer) { - return SQLITE_NOMEM; - } - memset(tokenizer, 0, sizeof(* tokenizer)); - - NSString * key = [NSString stringWithUTF8String:argv[0]]; - tokenizer->callback = [__SQLiteTokenizerMap objectForKey:key]; - if (!tokenizer->callback) { - return SQLITE_ERROR; - } - - *ppTokenizer = &tokenizer->base; - return SQLITE_OK; -} - -static int __SQLiteTokenizerDestroy(sqlite3_tokenizer * pTokenizer) { - sqlite3_free(pTokenizer); - return SQLITE_OK; -} - -static int __SQLiteTokenizerOpen(sqlite3_tokenizer * pTokenizer, const char * pInput, int nBytes, sqlite3_tokenizer_cursor ** ppCursor) { - __SQLiteTokenizerCursor * cursor = (__SQLiteTokenizerCursor *)sqlite3_malloc(sizeof(__SQLiteTokenizerCursor)); - if (!cursor) { - return SQLITE_NOMEM; - } - - cursor->input = pInput; - cursor->inputOffset = 0; - cursor->inputLength = 0; - cursor->idx = 0; - - *ppCursor = (sqlite3_tokenizer_cursor *)cursor; - return SQLITE_OK; -} - -static int __SQLiteTokenizerClose(sqlite3_tokenizer_cursor * pCursor) { - sqlite3_free(pCursor); - return SQLITE_OK; -} - -static int __SQLiteTokenizerNext(sqlite3_tokenizer_cursor * pCursor, const char ** ppToken, int * pnBytes, int * piStartOffset, int * piEndOffset, int * piPosition) { - __SQLiteTokenizerCursor * cursor = (__SQLiteTokenizerCursor *)pCursor; - __SQLiteTokenizer * tokenizer = (__SQLiteTokenizer *)cursor->base; - - cursor->inputOffset += cursor->inputLength; - const char * input = cursor->input + cursor->inputOffset; - const char * token = [tokenizer->callback(input, &cursor->inputOffset, &cursor->inputLength) cStringUsingEncoding:NSUTF8StringEncoding]; - if (!token) { - return SQLITE_DONE; - } - - *ppToken = token; - *pnBytes = (int)strlen(token); - *piStartOffset = cursor->inputOffset; - *piEndOffset = cursor->inputOffset + cursor->inputLength; - *piPosition = cursor->idx++; - return SQLITE_OK; -} - -static const sqlite3_tokenizer_module __SQLiteTokenizerModule = { - 0, - __SQLiteTokenizerCreate, - __SQLiteTokenizerDestroy, - __SQLiteTokenizerOpen, - __SQLiteTokenizerClose, - __SQLiteTokenizerNext -}; - -int _SQLiteRegisterTokenizer(sqlite3 *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(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/Sources/SQLiteObjc/fts3_tokenizer.h b/Sources/SQLiteObjc/fts3_tokenizer.h deleted file mode 100644 index d8a1e44b..00000000 --- a/Sources/SQLiteObjc/fts3_tokenizer.h +++ /dev/null @@ -1,161 +0,0 @@ -/* -** 2006 July 10 -** -** The author disclaims copyright to this source code. -** -************************************************************************* -** Defines the interface to tokenizers used by fulltext-search. There -** are three basic components: -** -** sqlite3_tokenizer_module is a singleton defining the tokenizer -** interface functions. This is essentially the class structure for -** tokenizers. -** -** sqlite3_tokenizer is used to define a particular tokenizer, perhaps -** including customization information defined at creation time. -** -** sqlite3_tokenizer_cursor is generated by a tokenizer to generate -** tokens from a particular input. -*/ -#ifndef _FTS3_TOKENIZER_H_ -#define _FTS3_TOKENIZER_H_ - -/* TODO(shess) Only used for SQLITE_OK and SQLITE_DONE at this time. -** If tokenizers are to be allowed to call sqlite3_*() functions, then -** we will need a way to register the API consistently. -*/ -#import "sqlite3.h" - -/* -** Structures used by the tokenizer interface. When a new tokenizer -** implementation is registered, the caller provides a pointer to -** an sqlite3_tokenizer_module containing pointers to the callback -** functions that make up an implementation. -** -** When an fts3 table is created, it passes any arguments passed to -** the tokenizer clause of the CREATE VIRTUAL TABLE statement to the -** sqlite3_tokenizer_module.xCreate() function of the requested tokenizer -** implementation. The xCreate() function in turn returns an -** sqlite3_tokenizer structure representing the specific tokenizer to -** be used for the fts3 table (customized by the tokenizer clause arguments). -** -** To tokenize an input buffer, the sqlite3_tokenizer_module.xOpen() -** method is called. It returns an sqlite3_tokenizer_cursor object -** that may be used to tokenize a specific input buffer based on -** the tokenization rules supplied by a specific sqlite3_tokenizer -** object. -*/ -typedef struct sqlite3_tokenizer_module sqlite3_tokenizer_module; -typedef struct sqlite3_tokenizer sqlite3_tokenizer; -typedef struct sqlite3_tokenizer_cursor sqlite3_tokenizer_cursor; - -struct sqlite3_tokenizer_module { - - /* - ** Structure version. Should always be set to 0 or 1. - */ - int iVersion; - - /* - ** Create a new tokenizer. The values in the argv[] array are the - ** arguments passed to the "tokenizer" clause of the CREATE VIRTUAL - ** TABLE statement that created the fts3 table. For example, if - ** the following SQL is executed: - ** - ** CREATE .. USING fts3( ... , tokenizer arg1 arg2) - ** - ** then argc is set to 2, and the argv[] array contains pointers - ** to the strings "arg1" and "arg2". - ** - ** This method should return either SQLITE_OK (0), or an SQLite error - ** code. If SQLITE_OK is returned, then *ppTokenizer should be set - ** to point at the newly created tokenizer structure. The generic - ** sqlite3_tokenizer.pModule variable should not be initialized by - ** this callback. The caller will do so. - */ - int (*xCreate)( - int argc, /* Size of argv array */ - const char *const*argv, /* Tokenizer argument strings */ - sqlite3_tokenizer **ppTokenizer /* OUT: Created tokenizer */ - ); - - /* - ** Destroy an existing tokenizer. The fts3 module calls this method - ** exactly once for each successful call to xCreate(). - */ - int (*xDestroy)(sqlite3_tokenizer *pTokenizer); - - /* - ** Create a tokenizer cursor to tokenize an input buffer. The caller - ** is responsible for ensuring that the input buffer remains valid - ** until the cursor is closed (using the xClose() method). - */ - int (*xOpen)( - sqlite3_tokenizer *pTokenizer, /* Tokenizer object */ - const char *pInput, int nBytes, /* Input buffer */ - sqlite3_tokenizer_cursor **ppCursor /* OUT: Created tokenizer cursor */ - ); - - /* - ** Destroy an existing tokenizer cursor. The fts3 module calls this - ** method exactly once for each successful call to xOpen(). - */ - int (*xClose)(sqlite3_tokenizer_cursor *pCursor); - - /* - ** Retrieve the next token from the tokenizer cursor pCursor. This - ** method should either return SQLITE_OK and set the values of the - ** "OUT" variables identified below, or SQLITE_DONE to indicate that - ** the end of the buffer has been reached, or an SQLite error code. - ** - ** *ppToken should be set to point at a buffer containing the - ** normalized version of the token (i.e. after any case-folding and/or - ** stemming has been performed). *pnBytes should be set to the length - ** of this buffer in bytes. The input text that generated the token is - ** identified by the byte offsets returned in *piStartOffset and - ** *piEndOffset. *piStartOffset should be set to the index of the first - ** byte of the token in the input buffer. *piEndOffset should be set - ** to the index of the first byte just past the end of the token in - ** the input buffer. - ** - ** The buffer *ppToken is set to point at is managed by the tokenizer - ** implementation. It is only required to be valid until the next call - ** to xNext() or xClose(). - */ - /* TODO(shess) current implementation requires pInput to be - ** nul-terminated. This should either be fixed, or pInput/nBytes - ** should be converted to zInput. - */ - int (*xNext)( - sqlite3_tokenizer_cursor *pCursor, /* Tokenizer cursor */ - const char **ppToken, int *pnBytes, /* OUT: Normalized text for token */ - int *piStartOffset, /* OUT: Byte offset of token in input buffer */ - int *piEndOffset, /* OUT: Byte offset of end of token in input buffer */ - int *piPosition /* OUT: Number of tokens returned before this one */ - ); - - /*********************************************************************** - ** Methods below this point are only available if iVersion>=1. - */ - - /* - ** Configure the language id of a tokenizer cursor. - */ - int (*xLanguageid)(sqlite3_tokenizer_cursor *pCsr, int iLangid); -}; - -struct sqlite3_tokenizer { - const sqlite3_tokenizer_module *pModule; /* The module for this tokenizer */ - /* Tokenizer implementations will typically add additional fields */ -}; - -struct sqlite3_tokenizer_cursor { - sqlite3_tokenizer *pTokenizer; /* Tokenizer for this cursor. */ - /* Tokenizer implementations will typically add additional fields */ -}; - -int fts3_global_term_cnt(int iTerm, int iCol); -int fts3_term_cnt(int iTerm, int iCol); - - -#endif /* _FTS3_TOKENIZER_H_ */ diff --git a/Sources/SQLiteObjc/include/SQLite-Bridging.h b/Sources/SQLiteObjc/include/SQLite-Bridging.h deleted file mode 100644 index 5b356593..00000000 --- a/Sources/SQLiteObjc/include/SQLite-Bridging.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// SQLite.swift -// https://github.com/stephencelis/SQLite.swift -// Copyright © 2014-2015 Stephen Celis. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -@import Foundation; - -#import "sqlite3.h" - -NS_ASSUME_NONNULL_BEGIN -typedef NSString * _Nullable (^_SQLiteTokenizerNextCallback)(const char *input, int *inputOffset, int *inputLength); -int _SQLiteRegisterTokenizer(sqlite3 *db, const char *module, const char *tokenizer, _Nullable _SQLiteTokenizerNextCallback callback); -NS_ASSUME_NONNULL_END - 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/Makefile b/Tests/Carthage/Makefile index f28eb25b..fe708ab0 100644 --- a/Tests/Carthage/Makefile +++ b/Tests/Carthage/Makefile @@ -1,16 +1,17 @@ -CARTHAGE := /usr/local/bin/carthage +CARTHAGE := $(shell which carthage) CARTHAGE_PLATFORM := iOS CARTHAGE_CONFIGURATION := Release CARTHAGE_DIR := Carthage -CARTHAGE_ARGS := --no-use-binaries -CARTHAGE_TOOLCHAIN := com.apple.dt.toolchain.Swift_3_0 +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 "$(TRAVIS_BUILD_DIR)" "HEAD"' > $@ + echo 'git "$(GITHUB_WORKSPACE)" "HEAD"' > $@ clean: @rm -f Cartfile Cartfile.resolved diff --git a/Tests/CocoaPods/.gitignore b/Tests/CocoaPods/.gitignore deleted file mode 100644 index 4cf24de2..00000000 --- a/Tests/CocoaPods/.gitignore +++ /dev/null @@ -1 +0,0 @@ -gems/ diff --git a/Tests/CocoaPods/Gemfile b/Tests/CocoaPods/Gemfile deleted file mode 100644 index 04f0155a..00000000 --- a/Tests/CocoaPods/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source 'https://rubygems.org' - -gem 'cocoapods', '~> 1.3.1' -gem 'minitest' diff --git a/Tests/CocoaPods/Gemfile.lock b/Tests/CocoaPods/Gemfile.lock deleted file mode 100644 index 47a2db58..00000000 --- a/Tests/CocoaPods/Gemfile.lock +++ /dev/null @@ -1,73 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (2.3.5) - activesupport (4.2.9) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - claide (1.0.2) - cocoapods (1.3.1) - activesupport (>= 4.0.2, < 5) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.3.1) - cocoapods-deintegrate (>= 1.0.1, < 2.0) - cocoapods-downloader (>= 1.1.3, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.2.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (~> 2.0.1) - gh_inspector (~> 1.0) - molinillo (~> 0.5.7) - nap (~> 1.0) - ruby-macho (~> 1.1) - xcodeproj (>= 1.5.1, < 2.0) - cocoapods-core (1.3.1) - activesupport (>= 4.0.2, < 6) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - cocoapods-deintegrate (1.0.1) - cocoapods-downloader (1.1.3) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.0) - cocoapods-stats (1.0.0) - cocoapods-trunk (1.2.0) - nap (>= 0.8, < 2.0) - netrc (= 0.7.8) - cocoapods-try (1.1.0) - colored2 (3.1.2) - escape (0.0.4) - fourflusher (2.0.1) - fuzzy_match (2.0.4) - gh_inspector (1.0.3) - i18n (0.8.6) - minitest (5.10.1) - molinillo (0.5.7) - nanaimo (0.2.3) - nap (1.1.0) - netrc (0.7.8) - ruby-macho (1.1.0) - thread_safe (0.3.6) - tzinfo (1.2.3) - thread_safe (~> 0.1) - xcodeproj (1.5.1) - CFPropertyList (~> 2.3.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.2.3) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods (~> 1.3.1) - minitest - -BUNDLED WITH - 1.13.6 diff --git a/Tests/CocoaPods/Makefile b/Tests/CocoaPods/Makefile deleted file mode 100644 index 26163fdb..00000000 --- a/Tests/CocoaPods/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -test: install repo_update - @set -e; \ - for test in *_test.rb; do \ - bundle exec ./$$test; \ - done - -repo_update: - @bundle exec pod repo update --silent - -install: - @bundle install --path gems - -.PHONY: test install diff --git a/Tests/CocoaPods/integration_test.rb b/Tests/CocoaPods/integration_test.rb deleted file mode 100755 index 9792b570..00000000 --- a/Tests/CocoaPods/integration_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env ruby - -require 'cocoapods' -require 'cocoapods/validator' -require 'minitest/autorun' - -class IntegrationTest < Minitest::Test - - def test_validate_project - assert validator.validate, "validation failed: #{validator.failure_reason}" - end - - private - - def validator - @validator ||= CustomValidator.new(podspec, ['https://github.com/CocoaPods/Specs.git']).tap do |validator| - validator.config.verbose = true - validator.no_clean = true - validator.use_frameworks = true - validator.fail_fast = true - validator.local = true - validator.allow_warnings = true - subspec = ENV['VALIDATOR_SUBSPEC'] - if subspec == 'none' - validator.no_subspecs = true - else - validator.only_subspec = subspec - end - end - end - - def podspec - File.expand_path(File.dirname(__FILE__) + '/../../SQLite.swift.podspec') - end - - - class CustomValidator < Pod::Validator - def test_pod - # https://github.com/CocoaPods/CocoaPods/issues/7009 - super unless consumer.platform_name == :watchos - end - end -end diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 59796fde..00000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -@testable import SQLiteTests - -XCTMain([ -testCase([ -])]) 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/AggregateFunctionsTests.swift b/Tests/SQLiteTests/AggregateFunctionsTests.swift deleted file mode 100644 index 6b583ccf..00000000 --- a/Tests/SQLiteTests/AggregateFunctionsTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -import XCTest -import SQLite - -class AggregateFunctionsTests : XCTestCase { - - func test_distinct_prependsExpressionsWithDistinctKeyword() { - AssertSQL("DISTINCT \"int\"", int.distinct) - AssertSQL("DISTINCT \"intOptional\"", intOptional.distinct) - AssertSQL("DISTINCT \"double\"", double.distinct) - AssertSQL("DISTINCT \"doubleOptional\"", doubleOptional.distinct) - AssertSQL("DISTINCT \"string\"", string.distinct) - AssertSQL("DISTINCT \"stringOptional\"", stringOptional.distinct) - } - - func test_count_wrapsOptionalExpressionsWithCountFunction() { - AssertSQL("count(\"intOptional\")", intOptional.count) - AssertSQL("count(\"doubleOptional\")", doubleOptional.count) - AssertSQL("count(\"stringOptional\")", stringOptional.count) - } - - func test_max_wrapsComparableExpressionsWithMaxFunction() { - AssertSQL("max(\"int\")", int.max) - AssertSQL("max(\"intOptional\")", intOptional.max) - AssertSQL("max(\"double\")", double.max) - AssertSQL("max(\"doubleOptional\")", doubleOptional.max) - AssertSQL("max(\"string\")", string.max) - AssertSQL("max(\"stringOptional\")", stringOptional.max) - AssertSQL("max(\"date\")", date.max) - AssertSQL("max(\"dateOptional\")", dateOptional.max) - } - - func test_min_wrapsComparableExpressionsWithMinFunction() { - AssertSQL("min(\"int\")", int.min) - AssertSQL("min(\"intOptional\")", intOptional.min) - AssertSQL("min(\"double\")", double.min) - AssertSQL("min(\"doubleOptional\")", doubleOptional.min) - AssertSQL("min(\"string\")", string.min) - AssertSQL("min(\"stringOptional\")", stringOptional.min) - AssertSQL("min(\"date\")", date.min) - AssertSQL("min(\"dateOptional\")", dateOptional.min) - } - - func test_average_wrapsNumericExpressionsWithAvgFunction() { - AssertSQL("avg(\"int\")", int.average) - AssertSQL("avg(\"intOptional\")", intOptional.average) - AssertSQL("avg(\"double\")", double.average) - AssertSQL("avg(\"doubleOptional\")", doubleOptional.average) - } - - func test_sum_wrapsNumericExpressionsWithSumFunction() { - AssertSQL("sum(\"int\")", int.sum) - AssertSQL("sum(\"intOptional\")", intOptional.sum) - AssertSQL("sum(\"double\")", double.sum) - AssertSQL("sum(\"doubleOptional\")", doubleOptional.sum) - } - - func test_total_wrapsNumericExpressionsWithTotalFunction() { - AssertSQL("total(\"int\")", int.total) - AssertSQL("total(\"intOptional\")", intOptional.total) - AssertSQL("total(\"double\")", double.total) - AssertSQL("total(\"doubleOptional\")", doubleOptional.total) - } - - func test_count_withStar_wrapsStarWithCountFunction() { - AssertSQL("count(*)", count(*)) - } - -} diff --git a/Tests/SQLiteTests/BlobTests.swift b/Tests/SQLiteTests/BlobTests.swift deleted file mode 100644 index fbcca9bc..00000000 --- a/Tests/SQLiteTests/BlobTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import XCTest -import SQLite - -class BlobTests : XCTestCase { - - func test_toHex() { - let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) - - XCTAssertEqual(blob.toHex(), "000a141e28323c46505a6496faff") - } - - func test_init_array() { - let blob = Blob(bytes: [42, 42, 42]) - XCTAssertEqual(blob.bytes, [42, 42, 42]) - } - - func test_init_unsafeRawPointer() { - let pointer = UnsafeMutablePointer.allocate(capacity: 3) - pointer.initialize(to: 42, count: 3) - let blob = Blob(bytes: pointer, length: 3) - XCTAssertEqual(blob.bytes, [42, 42, 42]) - } -} diff --git a/Tests/SQLiteTests/CipherTests.swift b/Tests/SQLiteTests/CipherTests.swift deleted file mode 100644 index 3ee0b135..00000000 --- a/Tests/SQLiteTests/CipherTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -#if SQLITE_SWIFT_SQLCIPHER -import XCTest -import SQLite -import SQLCipher - -class CipherTests: XCTestCase { - - let db1 = try! Connection() - let db2 = try! Connection() - - override func setUp() { - // db - - 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')") - - super.setUp() - } - - func test_key() { - XCTAssertEqual(1, try! db1.scalar("SELECT count(*) FROM foo") as? Int64) - } - - func test_key_blob_literal() { - let db = try! Connection() - try! db.key("x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'") - } - - func test_rekey() { - try! db1.rekey("goodbye") - XCTAssertEqual(1, try! db1.scalar("SELECT count(*) FROM foo") as? Int64) - } - - func test_data_key() { - XCTAssertEqual(1, try! db2.scalar("SELECT count(*) FROM foo") as? Int64) - } - - func test_data_rekey() { - 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() { - 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() { - // $ sqlcipher SQLiteTests/fixtures/encrypted.sqlite - // sqlite> pragma key = 'sqlcipher-test'; - // sqlite> CREATE TABLE foo (bar TEXT); - // sqlite> INSERT INTO foo (bar) VALUES ('world'); - let encryptedFile = fixture("encrypted", withExtension: "sqlite") - - try! FileManager.default.setAttributes([FileAttributeKey.immutable : 1], ofItemAtPath: encryptedFile) - XCTAssertFalse(FileManager.default.isWritableFile(atPath: encryptedFile)) - - let conn = try! Connection(encryptedFile) - try! conn.key("sqlcipher-test") - XCTAssertEqual(1, try! conn.scalar("SELECT count(*) FROM foo") as? Int64) - } - - private func keyData(length: Int = 64) -> NSMutableData { - let keyData = NSMutableData(length: length)! - let result = SecRandomCopyBytes(kSecRandomDefault, length, - keyData.mutableBytes.assumingMemoryBound(to: UInt8.self)) - XCTAssertEqual(0, result) - return keyData - } -} -#endif diff --git a/Tests/SQLiteTests/ConnectionTests.swift b/Tests/SQLiteTests/ConnectionTests.swift deleted file mode 100644 index 7bb86e41..00000000 --- a/Tests/SQLiteTests/ConnectionTests.swift +++ /dev/null @@ -1,447 +0,0 @@ -import XCTest -import Foundation -import Dispatch -@testable import SQLite - -#if SQLITE_SWIFT_STANDALONE -import sqlite3 -#elseif SQLITE_SWIFT_SQLCIPHER -import SQLCipher -#elseif os(Linux) -import CSQLite -#else -import SQLite3 -#endif - -class ConnectionTests : SQLiteTestCase { - - override func setUp() { - super.setUp() - - CreateUsersTable() - } - - func test_init_withInMemory_returnsInMemoryConnection() { - let db = try! Connection(.inMemory) - XCTAssertEqual("", db.description) - } - - func test_init_returnsInMemoryByDefault() { - let db = try! Connection() - XCTAssertEqual("", db.description) - } - - func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! Connection(.temporary) - XCTAssertEqual("", db.description) - } - - func test_init_withURI_returnsURIConnection() { - let db = try! Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) - XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) - } - - func test_init_withString_returnsURIConnection() { - let db = try! Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") - XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) - } - - func test_readonly_returnsFalseOnReadWriteConnections() { - XCTAssertFalse(db.readonly) - } - - func test_readonly_returnsTrueOnReadOnlyConnections() { - let db = try! Connection(readonly: true) - XCTAssertTrue(db.readonly) - } - - func test_changes_returnsZeroOnNewConnections() { - XCTAssertEqual(0, db.changes) - } - - func test_lastInsertRowid_returnsLastIdAfterInserts() { - try! InsertUser("alice") - XCTAssertEqual(1, db.lastInsertRowid) - } - - func test_lastInsertRowid_doesNotResetAfterError() { - 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() { - try! InsertUser("alice") - XCTAssertEqual(1, db.changes) - try! InsertUser("betsy") - XCTAssertEqual(1, db.changes) - } - - func test_totalChanges_returnsTotalNumberOfChanges() { - XCTAssertEqual(0, db.totalChanges) - try! InsertUser("alice") - XCTAssertEqual(1, db.totalChanges) - try! InsertUser("betsy") - XCTAssertEqual(2, db.totalChanges) - } - - func test_prepare_preparesAndReturnsStatements() { - _ = try! db.prepare("SELECT * FROM users WHERE admin = 0") - _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", 0) - _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", [0]) - _ = try! db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) - } - - func test_run_preparesRunsAndReturnsStatements() { - try! db.run("SELECT * FROM users WHERE admin = 0") - try! db.run("SELECT * FROM users WHERE admin = ?", 0) - try! db.run("SELECT * FROM users WHERE admin = ?", [0]) - try! db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) - AssertSQL("SELECT * FROM users WHERE admin = 0", 4) - } - - func test_scalar_preparesRunsAndReturnsScalarValues() { - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = $admin", ["$admin": 0]) as? Int64) - AssertSQL("SELECT count(*) FROM users WHERE admin = 0", 4) - } - - func test_execute_comment() { - try! db.run("-- this is a comment\nSELECT 1") - AssertSQL("-- this is a comment", 0) - AssertSQL("SELECT 1", 0) - } - - func test_transaction_executesBeginDeferred() { - try! db.transaction(.deferred) {} - - AssertSQL("BEGIN DEFERRED TRANSACTION") - } - - func test_transaction_executesBeginImmediate() { - try! db.transaction(.immediate) {} - - AssertSQL("BEGIN IMMEDIATE TRANSACTION") - } - - func test_transaction_executesBeginExclusive() { - try! db.transaction(.exclusive) {} - - AssertSQL("BEGIN EXCLUSIVE TRANSACTION") - } - - func test_transaction_beginsAndCommitsTransactions() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - - try! db.transaction { - try stmt.run() - } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") - AssertSQL("COMMIT TRANSACTION") - AssertSQL("ROLLBACK TRANSACTION", 0) - } - - func test_transaction_rollsBackTransactionsIfCommitsFail() { - let sqliteVersion = String(describing: try! db.scalar("SELECT sqlite_version()")!) - .split(separator: ".").flatMap { 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() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - - do { - try db.transaction { - try stmt.run() - try stmt.run() - } - } catch { - } - - AssertSQL("BEGIN DEFERRED TRANSACTION") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) - AssertSQL("ROLLBACK TRANSACTION") - AssertSQL("COMMIT TRANSACTION", 0) - } - - func test_savepoint_beginsAndCommitsSavepoints() { - let db:Connection = self.db - - try! db.savepoint("1") { - try db.savepoint("2") { - try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") - } - } - - AssertSQL("SAVEPOINT '1'") - AssertSQL("SAVEPOINT '2'") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") - AssertSQL("RELEASE SAVEPOINT '2'") - AssertSQL("RELEASE SAVEPOINT '1'") - AssertSQL("ROLLBACK TO SAVEPOINT '2'", 0) - AssertSQL("ROLLBACK TO SAVEPOINT '1'", 0) - } - - func test_savepoint_beginsAndRollsSavepointsBack() { - let db:Connection = self.db - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - - do { - try db.savepoint("1") { - try db.savepoint("2") { - try stmt.run() - try stmt.run() - try stmt.run() - } - try db.savepoint("2") { - try stmt.run() - try stmt.run() - try stmt.run() - } - } - } catch { - } - - AssertSQL("SAVEPOINT '1'") - AssertSQL("SAVEPOINT '2'") - AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) - AssertSQL("ROLLBACK TO SAVEPOINT '2'") - AssertSQL("ROLLBACK TO SAVEPOINT '1'") - AssertSQL("RELEASE SAVEPOINT '2'", 0) - AssertSQL("RELEASE SAVEPOINT '1'", 0) - } - - func test_updateHook_setsUpdateHook_withInsert() { - async { done in - db.updateHook { operation, db, table, rowid in - XCTAssertEqual(Connection.Operation.insert, operation) - XCTAssertEqual("main", db) - XCTAssertEqual("users", table) - XCTAssertEqual(1, rowid) - done() - } - try! InsertUser("alice") - } - } - - func test_updateHook_setsUpdateHook_withUpdate() { - try! InsertUser("alice") - async { done in - db.updateHook { operation, db, table, rowid in - XCTAssertEqual(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() { - try! InsertUser("alice") - 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() { - async { done in - db.commitHook { - done() - } - try! db.transaction { - try self.InsertUser("alice") - } - XCTAssertEqual(1, try! db.scalar("SELECT count(*) FROM users") as? Int64) - } - } - - func test_rollbackHook_setsRollbackHook() { - async { done in - db.rollbackHook(done) - do { - try db.transaction { - try self.InsertUser("alice") - try self.InsertUser("alice") // throw - } - } catch { - } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) - } - } - - func test_commitHook_withRollback_rollsBack() { - async { done in - db.commitHook { - throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) - } - db.rollbackHook(done) - do { - try db.transaction { - try self.InsertUser("alice") - } - } catch { - } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) - } - } - - func test_createFunction_withArrayArguments() { - db.createFunction("hello") { $0[0].map { "Hello, \($0)!" } } - - XCTAssertEqual("Hello, world!", try! db.scalar("SELECT hello('world')") as? String) - XCTAssert(try! db.scalar("SELECT hello(NULL)") == nil) - } - - func test_createFunction_createsQuotableFunction() { - db.createFunction("hello world") { $0[0].map { "Hello, \($0)!" } } - - XCTAssertEqual("Hello, world!", try! db.scalar("SELECT \"hello world\"('world')") as? String) - XCTAssert(try! db.scalar("SELECT \"hello world\"(NULL)") == nil) - } - - func test_createCollation_createsCollation() { - try! db.createCollation("NODIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .diacriticInsensitive) - } - XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) - } - - func test_createCollation_createsQuotableCollation() { - try! db.createCollation("NO DIACRITIC") { lhs, rhs in - return lhs.compare(rhs, options: .diacriticInsensitive) - } - XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) - } - - func test_interrupt_interruptsLongRunningQuery() { - try! InsertUsers("abcdefghijklmnopqrstuvwxyz".map { String($0) }) - db.createFunction("sleep") { args in - usleep(UInt32((args[0] as? Double ?? Double(args[0] as? Int64 ?? 1)) * 1_000_000)) - return nil - } - - let stmt = try! db.prepare("SELECT *, sleep(?) FROM users", 0.1) - try! stmt.run() - - let deadline = DispatchTime.now() + 0.01 - _ = DispatchQueue(label: "queue", qos: .background).asyncAfter(deadline: deadline, execute: db.interrupt) - AssertThrows(try stmt.run()) - } - - func test_concurrent_access_single_connection() { - 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 - var reads = Array(repeating: 0, count: nReaders) - var finished = false - for index in 0.. 500) } - } - } -} - - -class ResultTests : XCTestCase { - let 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(self.connection === connection) - } else { - XCTFail() - } - } - - 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() { - let statement = try! Statement(connection, "SELECT 1") - XCTAssertEqual("not an error (SELECT 1) (code: 21)", - Result(errorCode: SQLITE_MISUSE, connection: connection, statement: statement)?.description) - } -} 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/ValueTests.swift b/Tests/SQLiteTests/Core/ValueTests.swift similarity index 50% rename from Tests/SQLiteTests/ValueTests.swift rename to Tests/SQLiteTests/Core/ValueTests.swift index bda2b4b3..f880cb34 100644 --- a/Tests/SQLiteTests/ValueTests.swift +++ b/Tests/SQLiteTests/Core/ValueTests.swift @@ -1,6 +1,6 @@ import XCTest import SQLite -class ValueTests : XCTestCase { +class ValueTests: XCTestCase { } diff --git a/Tests/SQLiteTests/CoreFunctionsTests.swift b/Tests/SQLiteTests/CoreFunctionsTests.swift deleted file mode 100644 index e7402de3..00000000 --- a/Tests/SQLiteTests/CoreFunctionsTests.swift +++ /dev/null @@ -1,145 +0,0 @@ -import XCTest -@testable import SQLite - -class CoreFunctionsTests : XCTestCase { - - func test_round_wrapsDoubleExpressionsWithRoundFunction() { - AssertSQL("round(\"double\")", double.round()) - AssertSQL("round(\"doubleOptional\")", doubleOptional.round()) - - AssertSQL("round(\"double\", 1)", double.round(1)) - AssertSQL("round(\"doubleOptional\", 2)", doubleOptional.round(2)) - } - - func test_random_generatesExpressionWithRandomFunction() { - AssertSQL("random()", Expression.random()) - AssertSQL("random()", Expression.random()) - } - - func test_length_wrapsStringExpressionWithLengthFunction() { - AssertSQL("length(\"string\")", string.length) - AssertSQL("length(\"stringOptional\")", stringOptional.length) - } - - func test_lowercaseString_wrapsStringExpressionWithLowerFunction() { - AssertSQL("lower(\"string\")", string.lowercaseString) - AssertSQL("lower(\"stringOptional\")", stringOptional.lowercaseString) - } - - func test_uppercaseString_wrapsStringExpressionWithUpperFunction() { - AssertSQL("upper(\"string\")", string.uppercaseString) - AssertSQL("upper(\"stringOptional\")", stringOptional.uppercaseString) - } - - func test_like_buildsExpressionWithLikeOperator() { - AssertSQL("(\"string\" LIKE 'a%')", string.like("a%")) - AssertSQL("(\"stringOptional\" LIKE 'b%')", stringOptional.like("b%")) - - AssertSQL("(\"string\" LIKE '%\\%' ESCAPE '\\')", string.like("%\\%", escape: "\\")) - AssertSQL("(\"stringOptional\" LIKE '_\\_' ESCAPE '\\')", stringOptional.like("_\\_", escape: "\\")) - - AssertSQL("(\"string\" LIKE \"a\")", string.like(Expression("a"))) - AssertSQL("(\"stringOptional\" LIKE \"a\")", stringOptional.like(Expression("a"))) - - AssertSQL("(\"string\" LIKE \"a\" ESCAPE '\\')", string.like(Expression("a"), escape: "\\")) - AssertSQL("(\"stringOptional\" LIKE \"a\" ESCAPE '\\')", stringOptional.like(Expression("a"), escape: "\\")) - - AssertSQL("('string' LIKE \"a\")", "string".like(Expression("a"))) - AssertSQL("('string' LIKE \"a\" ESCAPE '\\')", "string".like(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/CustomFunctionsTests.swift b/Tests/SQLiteTests/CustomFunctionsTests.swift deleted file mode 100644 index 919986b6..00000000 --- a/Tests/SQLiteTests/CustomFunctionsTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -import XCTest -import SQLite - -class CustomFunctionNoArgsTests : SQLiteTestCase { - typealias FunctionNoOptional = () -> Expression - typealias FunctionResultOptional = () -> Expression - - func testFunctionNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { - return "a" - } - let result = try! db.prepare("SELECT test()").scalar() as! String - XCTAssertEqual("a", result) - } - - func testFunctionResultOptional() { - let _: FunctionResultOptional = try! db.createFunction("test", deterministic: true) { - return "a" - } - let result = try! db.prepare("SELECT test()").scalar() as! String? - XCTAssertEqual("a", result) - } -} - -class CustomFunctionWithOneArgTests : SQLiteTestCase { - typealias FunctionNoOptional = (Expression) -> Expression - typealias FunctionLeftOptional = (Expression) -> Expression - typealias FunctionResultOptional = (Expression) -> Expression - typealias FunctionLeftResultOptional = (Expression) -> Expression - - func testFunctionNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { a in - return "b"+a - } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String - XCTAssertEqual("ba", result) - } - - func testFunctionLeftOptional() { - let _: FunctionLeftOptional = try! db.createFunction("test", deterministic: true) { a in - return "b"+a! - } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String - XCTAssertEqual("ba", result) - } - - func testFunctionResultOptional() { - let _: FunctionResultOptional = try! db.createFunction("test", deterministic: true) { a in - return "b"+a - } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String - XCTAssertEqual("ba", result) - } - - func testFunctionLeftResultOptional() { - let _: FunctionLeftResultOptional = try! db.createFunction("test", deterministic: true) { (a:String?) -> String? in - return "b"+a! - } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String - XCTAssertEqual("ba", result) - } -} - -class CustomFunctionWithTwoArgsTests : SQLiteTestCase { - typealias FunctionNoOptional = (Expression, Expression) -> Expression - typealias FunctionLeftOptional = (Expression, Expression) -> Expression - typealias FunctionRightOptional = (Expression, Expression) -> Expression - typealias FunctionResultOptional = (Expression, Expression) -> Expression - typealias FunctionLeftRightOptional = (Expression, Expression) -> Expression - typealias FunctionLeftResultOptional = (Expression, Expression) -> Expression - typealias FunctionRightResultOptional = (Expression, Expression) -> Expression - typealias FunctionLeftRightResultOptional = (Expression, Expression) -> Expression - - func testNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a+b - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String - XCTAssertEqual("ab", result) - } - - func testLeftOptional() { - let _: FunctionLeftOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a!+b - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String - XCTAssertEqual("ab", result) - } - - func testRightOptional() { - let _: FunctionRightOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a+b! - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String - XCTAssertEqual("ab", result) - } - - func testResultOptional() { - let _: FunctionResultOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a+b - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? - XCTAssertEqual("ab", result) - } - - func testFunctionLeftRightOptional() { - let _: FunctionLeftRightOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a!+b! - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String - XCTAssertEqual("ab", result) - } - - func testFunctionLeftResultOptional() { - let _: FunctionLeftResultOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a!+b - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? - XCTAssertEqual("ab", result) - } - - func testFunctionRightResultOptional() { - let _: FunctionRightResultOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a+b! - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? - XCTAssertEqual("ab", result) - } - - func testFunctionLeftRightResultOptional() { - let _: FunctionLeftRightResultOptional = try! db.createFunction("test", deterministic: true) { a, b in - return a!+b! - } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? - XCTAssertEqual("ab", result) - } -} diff --git a/Tests/SQLiteTests/DateAndTimeFunctionTests.swift b/Tests/SQLiteTests/DateAndTimeFunctionTests.swift deleted file mode 100644 index 628b5910..00000000 --- a/Tests/SQLiteTests/DateAndTimeFunctionTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -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/ExpressionTests.swift b/Tests/SQLiteTests/ExpressionTests.swift deleted file mode 100644 index 036e10ce..00000000 --- a/Tests/SQLiteTests/ExpressionTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -import SQLite - -class ExpressionTests : 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/FTS4Tests.swift b/Tests/SQLiteTests/Extensions/FTS4Tests.swift similarity index 61% rename from Tests/SQLiteTests/FTS4Tests.swift rename to Tests/SQLiteTests/Extensions/FTS4Tests.swift index 4373bf8b..f7258fb5 100644 --- a/Tests/SQLiteTests/FTS4Tests.swift +++ b/Tests/SQLiteTests/Extensions/FTS4Tests.swift @@ -1,7 +1,7 @@ import XCTest import SQLite -class FTS4Tests : XCTestCase { +class FTS4Tests: XCTestCase { func test_create_onVirtualTable_withFTS4_compilesCreateVirtualTableExpression() { XCTAssertEqual( @@ -21,30 +21,37 @@ class FTS4Tests : XCTestCase { virtualTable.create(.FTS4([string], tokenize: .Porter)) ) XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=0\")", + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"remove_diacritics=0\")", virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: false))) ) XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", - virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"]))) + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"remove_diacritics=1\" + \"tokenchars=.\" \"separators=X\") + """.replacingOccurrences(of: "\n", with: ""), + virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: true, + tokenchars: ["."], separators: ["X"]))) ) } func test_match_onVirtualTableAsExpression_compilesMatchExpression() { - AssertSQL("(\"virtual_table\" MATCH 'string')", virtualTable.match("string") as Expression) - AssertSQL("(\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as Expression) - AssertSQL("(\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as Expression) + assertSQL("(\"virtual_table\" MATCH 'string')", virtualTable.match("string") as SQLite.Expression) + assertSQL("(\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as SQLite.Expression) + assertSQL("(\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as SQLite.Expression) } func test_match_onVirtualTableAsQueryType_compilesMatchExpression() { - AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH 'string')", virtualTable.match("string") as QueryType) - AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as QueryType) - AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as QueryType) + assertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH 'string')", + virtualTable.match("string") as QueryType) + assertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"string\")", + virtualTable.match(string) as QueryType) + assertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"stringOptional\")", + virtualTable.match(stringOptional) as QueryType) } } -class FTS4ConfigTests : XCTestCase { +class FTS4ConfigTests: XCTestCase { var config: FTS4Config! override func setUp() { @@ -108,7 +115,10 @@ class FTS4ConfigTests : XCTestCase { func test_tokenizer_unicode61_with_options() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"remove_diacritics=1\" + \"tokenchars=.\" \"separators=X\") + """.replacingOccurrences(of: "\n", with: ""), sql(config.tokenizer(.Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"])))) } @@ -156,7 +166,11 @@ class FTS4ConfigTests : XCTestCase { func test_config_all() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"int\", \"string\", \"date\", tokenize=porter, prefix=\"2,4\", content=\"table\", notindexed=\"string\", notindexed=\"date\", languageid=\"lid\", matchinfo=\"fts3\", order=\"desc\")", + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"int\", \"string\", \"date\", + tokenize=porter, prefix=\"2,4\", content=\"table\", notindexed=\"string\", notindexed=\"date\", + languageid=\"lid\", matchinfo=\"fts3\", order=\"desc\") + """.replacingOccurrences(of: "\n", with: ""), sql(config .tokenizer(.Porter) .column(int) @@ -171,38 +185,6 @@ class FTS4ConfigTests : XCTestCase { } func sql(_ config: FTS4Config) -> String { - return virtualTable.create(.FTS4(config)) + virtualTable.create(.FTS4(config)) } } - -class FTS4IntegrationTests : SQLiteTestCase { -#if !SQLITE_SWIFT_STANDALONE && !SQLITE_SWIFT_SQLCIPHER - func test_registerTokenizer_registersTokenizer() { - let emails = VirtualTable("emails") - let subject = Expression("subject") - let body = Expression("body") - - let locale = CFLocaleCopyCurrent() - let tokenizerName = "tokenizer" - let tokenizer = CFStringTokenizerCreate(nil, "" as CFString!, CFRangeMake(0, 0), UInt(kCFStringTokenizerUnitWord), locale) - try! db.registerTokenizer(tokenizerName) { string in - CFStringTokenizerSetString(tokenizer, string as CFString, CFRangeMake(0, CFStringGetLength(string as CFString))) - if CFStringTokenizerAdvanceToNextToken(tokenizer).isEmpty { - return nil - } - let range = CFStringTokenizerGetCurrentTokenRange(tokenizer) - let input = CFStringCreateWithSubstring(kCFAllocatorDefault, string as CFString, range)! - let token = CFStringCreateMutableCopy(nil, range.length, input)! - CFStringLowercase(token, locale) - CFStringTransform(token, nil, kCFStringTransformStripDiacritics, false) - return (token as String, string.range(of: input as String)!) - } - - try! db.run(emails.create(.FTS4([subject, body], tokenize: .Custom(tokenizerName)))) - AssertSQL("CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=\"SQLite.swift\" \"tokenizer\")") - - try! _ = db.run(emails.insert(subject <- "Aún más cáfe!")) - XCTAssertEqual(1, try! db.scalar(emails.filter(emails.match("aun")).count)) - } -#endif -} diff --git a/Tests/SQLiteTests/FTS5Tests.swift b/Tests/SQLiteTests/Extensions/FTS5Tests.swift similarity index 82% rename from Tests/SQLiteTests/FTS5Tests.swift rename to Tests/SQLiteTests/Extensions/FTS5Tests.swift index 63d8dc40..199ec415 100644 --- a/Tests/SQLiteTests/FTS5Tests.swift +++ b/Tests/SQLiteTests/Extensions/FTS5Tests.swift @@ -77,10 +77,20 @@ class FTS5Tests: XCTestCase { func test_tokenizer_unicode61_with_options() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61 \"remove_diacritics=1\" \"tokenchars=.\" \"separators=X\")", sql(config.tokenizer(.Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"])))) } + func test_tokenizer_trigram() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=trigram case_sensitive 0)", + sql(config.tokenizer(.Trigram()))) + + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=trigram case_sensitive 1)", + sql(config.tokenizer(.Trigram(caseSensitive: true)))) + } + func test_column_size() { XCTAssertEqual( "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(columnsize=1)", @@ -107,7 +117,10 @@ class FTS5Tests: XCTestCase { func test_fts5_config_all() { XCTAssertEqual( - "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"int\", \"string\" UNINDEXED, \"date\" UNINDEXED, tokenize=porter, prefix=\"2,4\", content=\"table\")", + """ + CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"int\", \"string\" UNINDEXED, \"date\" UNINDEXED, + tokenize=porter, prefix=\"2,4\", content=\"table\") + """.replacingOccurrences(of: "\n", with: ""), sql(config .tokenizer(.Porter) .column(int) @@ -119,6 +132,6 @@ class FTS5Tests: XCTestCase { } func sql(_ config: FTS5Config) -> String { - return virtualTable.create(.FTS5(config)) + 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/RTreeTests.swift b/Tests/SQLiteTests/Extensions/RTreeTests.swift similarity index 94% rename from Tests/SQLiteTests/RTreeTests.swift rename to Tests/SQLiteTests/Extensions/RTreeTests.swift index 7147533e..5525da26 100644 --- a/Tests/SQLiteTests/RTreeTests.swift +++ b/Tests/SQLiteTests/Extensions/RTreeTests.swift @@ -1,7 +1,7 @@ import XCTest import SQLite -class RTreeTests : XCTestCase { +class RTreeTests: XCTestCase { func test_create_onVirtualTable_withRTree_createVirtualTableExpression() { XCTAssertEqual( @@ -14,4 +14,4 @@ class RTreeTests : XCTestCase { ) } -} \ No newline at end of file +} diff --git a/Tests/SQLiteTests/Fixtures.swift b/Tests/SQLiteTests/Fixtures.swift index d0683130..bd261d2c 100644 --- a/Tests/SQLiteTests/Fixtures.swift +++ b/Tests/SQLiteTests/Fixtures.swift @@ -1,8 +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) - return testBundle.url( - forResource: name, - withExtension: withExtension)!.path + #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 index 0df746d9..453febcd 100644 --- a/Tests/SQLiteTests/FoundationTests.swift +++ b/Tests/SQLiteTests/FoundationTests.swift @@ -1,9 +1,9 @@ import XCTest import SQLite -class FoundationTests : XCTestCase { +class FoundationTests: XCTestCase { func testDataFromBlob() { - let data = Data(bytes: [1, 2, 3]) + let data = Data([1, 2, 3]) let blob = data.datatypeValue XCTAssertEqual([1, 2, 3], blob.bytes) } @@ -11,6 +11,30 @@ class FoundationTests : XCTestCase { func testBlobToData() { let blob = Blob(bytes: [1, 2, 3]) let data = Data.fromDatatypeValue(blob) - XCTAssertEqual(Data(bytes: [1, 2, 3]), data) + 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/Tests/SQLiteTests/OperatorsTests.swift b/Tests/SQLiteTests/OperatorsTests.swift deleted file mode 100644 index 948eb0a4..00000000 --- a/Tests/SQLiteTests/OperatorsTests.swift +++ /dev/null @@ -1,342 +0,0 @@ -import XCTest -import SQLite - -class OperatorsTests : XCTestCase { - - func test_stringExpressionPlusStringExpression_buildsConcatenatingStringExpression() { - AssertSQL("(\"string\" || \"string\")", string + string) - AssertSQL("(\"string\" || \"stringOptional\")", string + stringOptional) - AssertSQL("(\"stringOptional\" || \"string\")", stringOptional + string) - AssertSQL("(\"stringOptional\" || \"stringOptional\")", stringOptional + stringOptional) - AssertSQL("(\"string\" || 'literal')", string + "literal") - AssertSQL("(\"stringOptional\" || 'literal')", stringOptional + "literal") - AssertSQL("('literal' || \"string\")", "literal" + string) - AssertSQL("('literal' || \"stringOptional\")", "literal" + stringOptional) - } - - func test_numberExpression_plusNumberExpression_buildsAdditiveNumberExpression() { - AssertSQL("(\"int\" + \"int\")", int + int) - AssertSQL("(\"int\" + \"intOptional\")", int + intOptional) - AssertSQL("(\"intOptional\" + \"int\")", intOptional + int) - AssertSQL("(\"intOptional\" + \"intOptional\")", intOptional + intOptional) - AssertSQL("(\"int\" + 1)", int + 1) - AssertSQL("(\"intOptional\" + 1)", intOptional + 1) - AssertSQL("(1 + \"int\")", 1 + int) - AssertSQL("(1 + \"intOptional\")", 1 + intOptional) - - AssertSQL("(\"double\" + \"double\")", double + double) - AssertSQL("(\"double\" + \"doubleOptional\")", double + doubleOptional) - AssertSQL("(\"doubleOptional\" + \"double\")", doubleOptional + double) - AssertSQL("(\"doubleOptional\" + \"doubleOptional\")", doubleOptional + doubleOptional) - AssertSQL("(\"double\" + 1.0)", double + 1) - AssertSQL("(\"doubleOptional\" + 1.0)", doubleOptional + 1) - AssertSQL("(1.0 + \"double\")", 1 + double) - AssertSQL("(1.0 + \"doubleOptional\")", 1 + doubleOptional) - } - - func test_numberExpression_minusNumberExpression_buildsSubtractiveNumberExpression() { - AssertSQL("(\"int\" - \"int\")", int - int) - AssertSQL("(\"int\" - \"intOptional\")", int - intOptional) - AssertSQL("(\"intOptional\" - \"int\")", intOptional - int) - AssertSQL("(\"intOptional\" - \"intOptional\")", intOptional - intOptional) - AssertSQL("(\"int\" - 1)", int - 1) - AssertSQL("(\"intOptional\" - 1)", intOptional - 1) - AssertSQL("(1 - \"int\")", 1 - int) - AssertSQL("(1 - \"intOptional\")", 1 - intOptional) - - AssertSQL("(\"double\" - \"double\")", double - double) - AssertSQL("(\"double\" - \"doubleOptional\")", double - doubleOptional) - AssertSQL("(\"doubleOptional\" - \"double\")", doubleOptional - double) - AssertSQL("(\"doubleOptional\" - \"doubleOptional\")", doubleOptional - doubleOptional) - AssertSQL("(\"double\" - 1.0)", double - 1) - AssertSQL("(\"doubleOptional\" - 1.0)", doubleOptional - 1) - AssertSQL("(1.0 - \"double\")", 1 - double) - AssertSQL("(1.0 - \"doubleOptional\")", 1 - doubleOptional) - } - - func test_numberExpression_timesNumberExpression_buildsMultiplicativeNumberExpression() { - AssertSQL("(\"int\" * \"int\")", int * int) - AssertSQL("(\"int\" * \"intOptional\")", int * intOptional) - AssertSQL("(\"intOptional\" * \"int\")", intOptional * int) - AssertSQL("(\"intOptional\" * \"intOptional\")", intOptional * intOptional) - AssertSQL("(\"int\" * 1)", int * 1) - AssertSQL("(\"intOptional\" * 1)", intOptional * 1) - AssertSQL("(1 * \"int\")", 1 * int) - AssertSQL("(1 * \"intOptional\")", 1 * intOptional) - - AssertSQL("(\"double\" * \"double\")", double * double) - AssertSQL("(\"double\" * \"doubleOptional\")", double * doubleOptional) - AssertSQL("(\"doubleOptional\" * \"double\")", doubleOptional * double) - AssertSQL("(\"doubleOptional\" * \"doubleOptional\")", doubleOptional * doubleOptional) - AssertSQL("(\"double\" * 1.0)", double * 1) - AssertSQL("(\"doubleOptional\" * 1.0)", doubleOptional * 1) - AssertSQL("(1.0 * \"double\")", 1 * double) - AssertSQL("(1.0 * \"doubleOptional\")", 1 * doubleOptional) - } - - func test_numberExpression_dividedByNumberExpression_buildsDivisiveNumberExpression() { - AssertSQL("(\"int\" / \"int\")", int / int) - AssertSQL("(\"int\" / \"intOptional\")", int / intOptional) - AssertSQL("(\"intOptional\" / \"int\")", intOptional / int) - AssertSQL("(\"intOptional\" / \"intOptional\")", intOptional / intOptional) - AssertSQL("(\"int\" / 1)", int / 1) - AssertSQL("(\"intOptional\" / 1)", intOptional / 1) - AssertSQL("(1 / \"int\")", 1 / int) - AssertSQL("(1 / \"intOptional\")", 1 / intOptional) - - AssertSQL("(\"double\" / \"double\")", double / double) - AssertSQL("(\"double\" / \"doubleOptional\")", double / doubleOptional) - AssertSQL("(\"doubleOptional\" / \"double\")", doubleOptional / double) - AssertSQL("(\"doubleOptional\" / \"doubleOptional\")", doubleOptional / doubleOptional) - AssertSQL("(\"double\" / 1.0)", double / 1) - AssertSQL("(\"doubleOptional\" / 1.0)", doubleOptional / 1) - AssertSQL("(1.0 / \"double\")", 1 / double) - AssertSQL("(1.0 / \"doubleOptional\")", 1 / doubleOptional) - } - - func test_numberExpression_prefixedWithMinus_buildsInvertedNumberExpression() { - AssertSQL("-(\"int\")", -int) - AssertSQL("-(\"intOptional\")", -intOptional) - - AssertSQL("-(\"double\")", -double) - AssertSQL("-(\"doubleOptional\")", -doubleOptional) - } - - func test_integerExpression_moduloIntegerExpression_buildsModuloIntegerExpression() { - AssertSQL("(\"int\" % \"int\")", int % int) - AssertSQL("(\"int\" % \"intOptional\")", int % intOptional) - AssertSQL("(\"intOptional\" % \"int\")", intOptional % int) - AssertSQL("(\"intOptional\" % \"intOptional\")", intOptional % intOptional) - AssertSQL("(\"int\" % 1)", int % 1) - AssertSQL("(\"intOptional\" % 1)", intOptional % 1) - AssertSQL("(1 % \"int\")", 1 % int) - AssertSQL("(1 % \"intOptional\")", 1 % intOptional) - } - - func test_integerExpression_bitShiftLeftIntegerExpression_buildsLeftShiftedIntegerExpression() { - AssertSQL("(\"int\" << \"int\")", int << int) - AssertSQL("(\"int\" << \"intOptional\")", int << intOptional) - AssertSQL("(\"intOptional\" << \"int\")", intOptional << int) - AssertSQL("(\"intOptional\" << \"intOptional\")", intOptional << intOptional) - AssertSQL("(\"int\" << 1)", int << 1) - AssertSQL("(\"intOptional\" << 1)", intOptional << 1) - AssertSQL("(1 << \"int\")", 1 << int) - AssertSQL("(1 << \"intOptional\")", 1 << intOptional) - } - - func test_integerExpression_bitShiftRightIntegerExpression_buildsRightShiftedIntegerExpression() { - AssertSQL("(\"int\" >> \"int\")", int >> int) - AssertSQL("(\"int\" >> \"intOptional\")", int >> intOptional) - AssertSQL("(\"intOptional\" >> \"int\")", intOptional >> int) - AssertSQL("(\"intOptional\" >> \"intOptional\")", intOptional >> intOptional) - AssertSQL("(\"int\" >> 1)", int >> 1) - AssertSQL("(\"intOptional\" >> 1)", intOptional >> 1) - AssertSQL("(1 >> \"int\")", 1 >> int) - AssertSQL("(1 >> \"intOptional\")", 1 >> intOptional) - } - - func test_integerExpression_bitwiseAndIntegerExpression_buildsAndedIntegerExpression() { - AssertSQL("(\"int\" & \"int\")", int & int) - AssertSQL("(\"int\" & \"intOptional\")", int & intOptional) - AssertSQL("(\"intOptional\" & \"int\")", intOptional & int) - AssertSQL("(\"intOptional\" & \"intOptional\")", intOptional & intOptional) - AssertSQL("(\"int\" & 1)", int & 1) - AssertSQL("(\"intOptional\" & 1)", intOptional & 1) - AssertSQL("(1 & \"int\")", 1 & int) - AssertSQL("(1 & \"intOptional\")", 1 & intOptional) - } - - func test_integerExpression_bitwiseOrIntegerExpression_buildsOredIntegerExpression() { - AssertSQL("(\"int\" | \"int\")", int | int) - AssertSQL("(\"int\" | \"intOptional\")", int | intOptional) - AssertSQL("(\"intOptional\" | \"int\")", intOptional | int) - AssertSQL("(\"intOptional\" | \"intOptional\")", intOptional | intOptional) - AssertSQL("(\"int\" | 1)", int | 1) - AssertSQL("(\"intOptional\" | 1)", intOptional | 1) - AssertSQL("(1 | \"int\")", 1 | int) - AssertSQL("(1 | \"intOptional\")", 1 | intOptional) - } - - func test_integerExpression_bitwiseExclusiveOrIntegerExpression_buildsOredIntegerExpression() { - AssertSQL("(~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^ int) - AssertSQL("(~((\"int\" & \"intOptional\")) & (\"int\" | \"intOptional\"))", int ^ intOptional) - AssertSQL("(~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^ int) - AssertSQL("(~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^ intOptional) - AssertSQL("(~((\"int\" & 1)) & (\"int\" | 1))", int ^ 1) - AssertSQL("(~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^ 1) - AssertSQL("(~((1 & \"int\")) & (1 | \"int\"))", 1 ^ int) - AssertSQL("(~((1 & \"intOptional\")) & (1 | \"intOptional\"))", 1 ^ intOptional) - } - - func test_bitwiseNot_integerExpression_buildsComplementIntegerExpression() { - AssertSQL("~(\"int\")", ~int) - AssertSQL("~(\"intOptional\")", ~intOptional) - } - - func test_equalityOperator_withEquatableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" = \"bool\")", bool == bool) - AssertSQL("(\"bool\" = \"boolOptional\")", bool == boolOptional) - AssertSQL("(\"boolOptional\" = \"bool\")", boolOptional == bool) - AssertSQL("(\"boolOptional\" = \"boolOptional\")", boolOptional == boolOptional) - AssertSQL("(\"bool\" = 1)", bool == true) - AssertSQL("(\"boolOptional\" = 1)", boolOptional == true) - AssertSQL("(1 = \"bool\")", true == bool) - AssertSQL("(1 = \"boolOptional\")", true == boolOptional) - - AssertSQL("(\"boolOptional\" IS NULL)", boolOptional == nil) - AssertSQL("(NULL IS \"boolOptional\")", nil == boolOptional) - } - - func test_inequalityOperator_withEquatableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" != \"bool\")", bool != bool) - AssertSQL("(\"bool\" != \"boolOptional\")", bool != boolOptional) - AssertSQL("(\"boolOptional\" != \"bool\")", boolOptional != bool) - AssertSQL("(\"boolOptional\" != \"boolOptional\")", boolOptional != boolOptional) - AssertSQL("(\"bool\" != 1)", bool != true) - AssertSQL("(\"boolOptional\" != 1)", boolOptional != true) - AssertSQL("(1 != \"bool\")", true != bool) - AssertSQL("(1 != \"boolOptional\")", true != boolOptional) - - AssertSQL("(\"boolOptional\" IS NOT NULL)", boolOptional != nil) - AssertSQL("(NULL IS NOT \"boolOptional\")", nil != boolOptional) - } - - func test_greaterThanOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" > \"bool\")", bool > bool) - AssertSQL("(\"bool\" > \"boolOptional\")", bool > boolOptional) - AssertSQL("(\"boolOptional\" > \"bool\")", boolOptional > bool) - AssertSQL("(\"boolOptional\" > \"boolOptional\")", boolOptional > boolOptional) - AssertSQL("(\"bool\" > 1)", bool > true) - AssertSQL("(\"boolOptional\" > 1)", boolOptional > true) - AssertSQL("(1 > \"bool\")", true > bool) - AssertSQL("(1 > \"boolOptional\")", true > boolOptional) - } - - func test_greaterThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" >= \"bool\")", bool >= bool) - AssertSQL("(\"bool\" >= \"boolOptional\")", bool >= boolOptional) - AssertSQL("(\"boolOptional\" >= \"bool\")", boolOptional >= bool) - AssertSQL("(\"boolOptional\" >= \"boolOptional\")", boolOptional >= boolOptional) - AssertSQL("(\"bool\" >= 1)", bool >= true) - AssertSQL("(\"boolOptional\" >= 1)", boolOptional >= true) - AssertSQL("(1 >= \"bool\")", true >= bool) - AssertSQL("(1 >= \"boolOptional\")", true >= boolOptional) - } - - func test_lessThanOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" < \"bool\")", bool < bool) - AssertSQL("(\"bool\" < \"boolOptional\")", bool < boolOptional) - AssertSQL("(\"boolOptional\" < \"bool\")", boolOptional < bool) - AssertSQL("(\"boolOptional\" < \"boolOptional\")", boolOptional < boolOptional) - AssertSQL("(\"bool\" < 1)", bool < true) - AssertSQL("(\"boolOptional\" < 1)", boolOptional < true) - AssertSQL("(1 < \"bool\")", true < bool) - AssertSQL("(1 < \"boolOptional\")", true < boolOptional) - } - - func test_lessThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { - AssertSQL("(\"bool\" <= \"bool\")", bool <= bool) - AssertSQL("(\"bool\" <= \"boolOptional\")", bool <= boolOptional) - AssertSQL("(\"boolOptional\" <= \"bool\")", boolOptional <= bool) - AssertSQL("(\"boolOptional\" <= \"boolOptional\")", boolOptional <= boolOptional) - AssertSQL("(\"bool\" <= 1)", bool <= true) - AssertSQL("(\"boolOptional\" <= 1)", boolOptional <= true) - AssertSQL("(1 <= \"bool\")", true <= bool) - AssertSQL("(1 <= \"boolOptional\")", true <= boolOptional) - } - - func test_patternMatchingOperator_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_doubleOrOperator_withBooleanExpressions_buildsCompoundExpression() { - AssertSQL("(\"bool\" OR \"bool\")", bool || bool) - AssertSQL("(\"bool\" OR \"boolOptional\")", bool || boolOptional) - AssertSQL("(\"boolOptional\" OR \"bool\")", boolOptional || bool) - AssertSQL("(\"boolOptional\" OR \"boolOptional\")", boolOptional || boolOptional) - AssertSQL("(\"bool\" OR 1)", bool || true) - AssertSQL("(\"boolOptional\" OR 1)", boolOptional || true) - AssertSQL("(1 OR \"bool\")", true || bool) - AssertSQL("(1 OR \"boolOptional\")", true || boolOptional) - } - - func test_unaryNotOperator_withBooleanExpressions_buildsNotExpression() { - AssertSQL("NOT (\"bool\")", !bool) - AssertSQL("NOT (\"boolOptional\")", !boolOptional) - } - - func test_precedencePreserved() { - let n = Expression(value: 1) - AssertSQL("(((1 = 1) AND (1 = 1)) OR (1 = 1))", (n == n && n == n) || n == n) - AssertSQL("((1 = 1) AND ((1 = 1) OR (1 = 1)))", n == n && (n == n || n == n)) - } - - 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 = Expression("email") - let age = Expression("age") - let admin = Expression("admin") - let optionalAdmin = Expression("admin") - - let posts = Table("posts") - let userId = Expression("user_id") - let categoryId = Expression("category_id") - let published = Expression("published") - - let categories = Table("categories") - let tag = Expression("tag") - - func test_select_withExpression_compilesSelectClause() { - AssertSQL("SELECT \"email\" FROM \"users\"", users.select(email)) - } - - func test_select_withStarExpression_compilesSelectClause() { - AssertSQL("SELECT * FROM \"users\"", users.select(*)) - } - - func test_select_withNamespacedStarExpression_compilesSelectClause() { - AssertSQL("SELECT \"users\".* FROM \"users\"", users.select(users[*])) - } - - func test_select_withVariadicExpressions_compilesSelectClause() { - AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select(email, count(*))) - } - - func test_select_withExpressions_compilesSelectClause() { - AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select([email, count(*)])) - } - - func test_selectDistinct_withExpression_compilesSelectClause() { - AssertSQL("SELECT DISTINCT \"age\" FROM \"users\"", users.select(distinct: age)) - } - - func test_selectDistinct_withExpressions_compilesSelectClause() { - AssertSQL("SELECT DISTINCT \"age\", \"admin\" FROM \"users\"", users.select(distinct: [age, admin])) - } - - func test_selectDistinct_withStar_compilesSelectClause() { - AssertSQL("SELECT DISTINCT * FROM \"users\"", users.select(distinct: *)) - } - - func test_join_compilesJoinClause() { - AssertSQL( - "SELECT * FROM \"users\" INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", - users.join(posts, on: posts[userId] == users[id]) - ) - } - - func test_join_withExplicitType_compilesJoinClauseWithType() { - AssertSQL( - "SELECT * FROM \"users\" LEFT OUTER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", - users.join(.leftOuter, posts, on: posts[userId] == users[id]) - ) - - AssertSQL( - "SELECT * FROM \"users\" CROSS JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", - users.join(.cross, posts, on: posts[userId] == users[id]) - ) - } - - func test_join_withTableCondition_compilesJoinClauseWithTableCondition() { - AssertSQL( - "SELECT * FROM \"users\" INNER JOIN \"posts\" ON ((\"posts\".\"user_id\" = \"users\".\"id\") AND \"published\")", - users.join(posts.filter(published), on: posts[userId] == users[id]) - ) - } - - func test_join_whenChained_compilesAggregateJoinClause() { - AssertSQL( - "SELECT * FROM \"users\" " + - "INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\") " + - "INNER JOIN \"categories\" ON (\"categories\".\"id\" = \"posts\".\"category_id\")", - users.join(posts, on: posts[userId] == users[id]).join(categories, on: categories[id] == posts[categoryId]) - ) - } - - func test_filter_compilesWhereClause() { - AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(admin == true)) - } - - func test_filter_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 = Expression("manager_id") - - let managers = users.alias("managers") - - AssertSQL( - "SELECT * FROM \"users\" " + - "INNER JOIN \"users\" AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\")", - users.join(managers, on: managers[id] == users[managerId]) - ) - } - - func test_insert_compilesInsertExpression() { - AssertSQL( - "INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", - users.insert(email <- "alice@example.com", age <- 30) - ) - } - - func test_insert_withOnConflict_compilesInsertOrOnConflictExpression() { - AssertSQL( - "INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", - users.insert(or: .replace, email <- "alice@example.com", age <- 30) - ) - } - - func test_insert_compilesInsertExpressionWithDefaultValues() { - AssertSQL("INSERT INTO \"users\" DEFAULT VALUES", users.insert()) - } - - func test_insert_withQuery_compilesInsertExpressionWithSelectStatement() { - let emails = Table("emails") - - AssertSQL( - "INSERT INTO \"emails\" SELECT \"email\" FROM \"users\" WHERE \"admin\"", - emails.insert(users.select(email).filter(admin)) - ) - } - - func test_insert_encodable() throws { - let emails = Table("emails") - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) - let insert = try emails.insert(value) - AssertSQL( - "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\") VALUES (1, '2', 1, 3.0, 4.0)", - insert - ) - } - - 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, optional: nil, sub: nil) - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: "optional", sub: value1) - let insert = try emails.insert(value) - let encodedJSON = try JSONEncoder().encode(value1) - let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! - AssertSQL( - "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"optional\", \"sub\") VALUES (1, '2', 1, 3.0, 4.0, 'optional', '\(encodedJSONString)')", - 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, 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", - 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, optional: nil, sub: nil) - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: value1) - let update = try emails.update(value) - let encodedJSON = try JSONEncoder().encode(value1) - let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! - AssertSQL( - "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, \"sub\" = '\(encodedJSONString)'", - update - ) - } - - 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) - } - -} - -class QueryIntegrationTests : SQLiteTestCase { - - let id = Expression("id") - let email = Expression("email") - - override func setUp() { - super.setUp() - - CreateUsersTable() - } - - // MARK: - - - func test_select() { - let managerId = Expression("manager_id") - let managers = users.alias("managers") - - let alice = try! db.run(users.insert(email <- "alice@example.com")) - _ = try! db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) - - for user in try! db.prepare(users.join(managers, on: managers[id] == users[managerId])) { - _ = user[users[managerId]] - } - } - - func test_prepareRowIterator() { - let names = ["a", "b", "c"] - try! InsertUsers(names) - - let emailColumn = Expression("email") - let emails = try! db.prepareRowIterator(users).map { $0[emailColumn] } - - XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) - } - - func test_ambiguousMap() { - 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() { - let managerId = Expression("manager_id") - let managers = users.alias("managers") - - let alice = try! db.run(users.insert(email <- "alice@example.com")) - _ = try! db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) - - for user in try! db.prepare(users.join(managers, on: managers[id] == users[managerId])) { - _ = user[users[managerId]] - } - } - - func test_select_codable() throws { - let table = Table("codable") - try db.run(table.create { builder in - builder.column(Expression("int")) - builder.column(Expression("string")) - builder.column(Expression("bool")) - builder.column(Expression("float")) - builder.column(Expression("double")) - builder.column(Expression("optional")) - builder.column(Expression("sub")) - }) - - let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) - let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, 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].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) - XCTAssertNil(values[0].sub?.optional) - XCTAssertNil(values[0].sub?.sub) - } - - func test_scalar() { - XCTAssertEqual(0, try! db.scalar(users.count)) - XCTAssertEqual(false, try! db.scalar(users.exists)) - - try! InsertUsers("alice") - XCTAssertEqual(1, try! db.scalar(users.select(id.average))) - } - - func test_pluck() { - let rowid = try! db.run(users.insert(email <- "alice@example.com")) - XCTAssertEqual(rowid, try! db.pluck(users)![id]) - } - - func test_insert() { - let id = try! db.run(users.insert(email <- "alice@example.com")) - XCTAssertEqual(1, id) - } - - func test_update() { - let changes = try! db.run(users.update(email <- "alice@example.com")) - XCTAssertEqual(0, changes) - } - - func test_delete() { - let changes = try! db.run(users.delete()) - XCTAssertEqual(0, changes) - } - - 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[*], Expression(literal: "1 AS weight")).filter(email == "sally@example.com") - let query4 = users.select(users[*], Expression(literal: "2 AS weight")).filter(email == "alice@example.com") - - print(query3.union(query4).order(Expression(literal: "weight")).asSQL()) - - let orderedIDs = try db.prepare(query3.union(query4).order(Expression(literal: "weight"), email)).map { $0[id] } - XCTAssertEqual(Array(expectedIDs.reversed()), orderedIDs) - } - - func test_no_such_column() throws { - let doesNotExist = 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() { - 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)") - } - } -} diff --git a/Tests/SQLiteTests/fixtures/encrypted.sqlite b/Tests/SQLiteTests/Resources/encrypted-3.x.sqlite similarity index 100% rename from Tests/SQLiteTests/fixtures/encrypted.sqlite rename to Tests/SQLiteTests/Resources/encrypted-3.x.sqlite 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/SchemaTests.swift b/Tests/SQLiteTests/Schema/SchemaTests.swift similarity index 86% rename from Tests/SQLiteTests/SchemaTests.swift rename to Tests/SQLiteTests/Schema/SchemaTests.swift index b9a08881..4f4a49d1 100644 --- a/Tests/SQLiteTests/SchemaTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaTests.swift @@ -1,7 +1,7 @@ import XCTest import SQLite -class SchemaTests : XCTestCase { +class SchemaTests: XCTestCase { func test_drop_compilesDropTableExpression() { XCTAssertEqual("DROP TABLE \"table\"", table.drop()) @@ -72,6 +72,7 @@ class SchemaTests : XCTestCase { } // thoroughness test for ambiguity + // swiftlint:disable:next function_body_length func test_column_compilesColumnDefinitionExpression() { XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL)", @@ -317,6 +318,10 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64, unique: true, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64, check: int64 > 0, references: table, int64) } @@ -330,9 +335,23 @@ class SchemaTests : XCTestCase { table.create { t in t.column(int64, unique: true, check: int64 > 0, references: table, int64) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + """ + CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES + \"table\" (\"int64\")) + """.replacingOccurrences(of: "\n", with: ""), table.create { t in t.column(int64, unique: true, check: int64Optional > 0, references: table, int64) } ) + XCTAssertEqual( + """ + CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0) REFERENCES + \"table\" (\"int64\")) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64Optional\" INTEGER REFERENCES \"table\" (\"int64\"))", @@ -342,6 +361,10 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, unique: true, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER PRIMARY KEY REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, primaryKey: true, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, check: int64 > 0, references: table, int64) } @@ -354,12 +377,21 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER PRIMARY KEY CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, primaryKey: true, check: int64 > 0, references: table, int64) } + ) XCTAssertEqual( "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER PRIMARY KEY CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, primaryKey: true, check: int64Optional > 0, references: table, int64) } + ) } + // swiftlint:disable:next function_body_length func test_column_withStringExpression_compilesCollatedColumnDefinitionExpression() { XCTAssertEqual( "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL COLLATE RTRIM)", @@ -487,48 +519,95 @@ class SchemaTests : XCTestCase { 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)", - table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: stringOptional, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, + unique: true, + check: string != "", + defaultValue: stringOptional, + collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: string, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') + DEFAULT (\"string\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", + defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: stringOptional, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", + defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: "string", collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: string != "", + defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", + defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", defaultValue: string, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') + DEFAULT (\"string\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: string != "", + defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: string, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') + DEFAULT (\"string\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", defaultValue: stringOptional, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: string != "", + defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: stringOptional, collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') + DEFAULT (\"stringOptional\") COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: string != "", defaultValue: "string", collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: string != "", + defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", - table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } + """ + CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') + DEFAULT ('string') COLLATE RTRIM) + """.replacingOccurrences(of: "\n", with: ""), + table.create { t in t.column(stringOptional, check: stringOptional != "", + defaultValue: "string", collate: .rtrim) } ) } @@ -545,6 +624,10 @@ class SchemaTests : XCTestCase { "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\", \"double\"))", table.create { t in t.primaryKey(int64, string, double) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\", \"double\", \"date\"))", + table.create { t in t.primaryKey(int64, string, double, date) } + ) } func test_unique_compilesUniqueExpression() { diff --git a/Tests/SQLiteTests/SetterTests.swift b/Tests/SQLiteTests/SetterTests.swift deleted file mode 100644 index d4f189d7..00000000 --- a/Tests/SQLiteTests/SetterTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -import XCTest -import SQLite - -class SetterTests : XCTestCase { - - func test_setterAssignmentOperator_buildsSetter() { - AssertSQL("\"int\" = \"int\"", int <- int) - AssertSQL("\"int\" = 1", int <- 1) - AssertSQL("\"intOptional\" = \"int\"", intOptional <- int) - AssertSQL("\"intOptional\" = \"intOptional\"", intOptional <- intOptional) - AssertSQL("\"intOptional\" = 1", intOptional <- 1) - AssertSQL("\"intOptional\" = NULL", intOptional <- nil) - } - - func test_plusEquals_withStringExpression_buildsSetter() { - AssertSQL("\"string\" = (\"string\" || \"string\")", string += string) - AssertSQL("\"string\" = (\"string\" || 'literal')", string += "literal") - AssertSQL("\"stringOptional\" = (\"stringOptional\" || \"string\")", stringOptional += string) - AssertSQL("\"stringOptional\" = (\"stringOptional\" || \"stringOptional\")", stringOptional += stringOptional) - AssertSQL("\"stringOptional\" = (\"stringOptional\" || 'literal')", stringOptional += "literal") - } - - func test_plusEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" + \"int\")", int += int) - AssertSQL("\"int\" = (\"int\" + 1)", int += 1) - AssertSQL("\"intOptional\" = (\"intOptional\" + \"int\")", intOptional += int) - AssertSQL("\"intOptional\" = (\"intOptional\" + \"intOptional\")", intOptional += intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional += 1) - - AssertSQL("\"double\" = (\"double\" + \"double\")", double += double) - AssertSQL("\"double\" = (\"double\" + 1.0)", double += 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"double\")", doubleOptional += double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"doubleOptional\")", doubleOptional += doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + 1.0)", doubleOptional += 1) - } - - func test_minusEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" - \"int\")", int -= int) - AssertSQL("\"int\" = (\"int\" - 1)", int -= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" - \"int\")", intOptional -= int) - AssertSQL("\"intOptional\" = (\"intOptional\" - \"intOptional\")", intOptional -= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional -= 1) - - AssertSQL("\"double\" = (\"double\" - \"double\")", double -= double) - AssertSQL("\"double\" = (\"double\" - 1.0)", double -= 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"double\")", doubleOptional -= double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"doubleOptional\")", doubleOptional -= doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - 1.0)", doubleOptional -= 1) - } - - func test_timesEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" * \"int\")", int *= int) - AssertSQL("\"int\" = (\"int\" * 1)", int *= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" * \"int\")", intOptional *= int) - AssertSQL("\"intOptional\" = (\"intOptional\" * \"intOptional\")", intOptional *= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" * 1)", intOptional *= 1) - - AssertSQL("\"double\" = (\"double\" * \"double\")", double *= double) - AssertSQL("\"double\" = (\"double\" * 1.0)", double *= 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"double\")", doubleOptional *= double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"doubleOptional\")", doubleOptional *= doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * 1.0)", doubleOptional *= 1) - } - - func test_dividedByEquals_withNumberExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" / \"int\")", int /= int) - AssertSQL("\"int\" = (\"int\" / 1)", int /= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" / \"int\")", intOptional /= int) - AssertSQL("\"intOptional\" = (\"intOptional\" / \"intOptional\")", intOptional /= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" / 1)", intOptional /= 1) - - AssertSQL("\"double\" = (\"double\" / \"double\")", double /= double) - AssertSQL("\"double\" = (\"double\" / 1.0)", double /= 1) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"double\")", doubleOptional /= double) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"doubleOptional\")", doubleOptional /= doubleOptional) - AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / 1.0)", doubleOptional /= 1) - } - - func test_moduloEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" % \"int\")", int %= int) - AssertSQL("\"int\" = (\"int\" % 1)", int %= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" % \"int\")", intOptional %= int) - AssertSQL("\"intOptional\" = (\"intOptional\" % \"intOptional\")", intOptional %= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" % 1)", intOptional %= 1) - } - - func test_leftShiftEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" << \"int\")", int <<= int) - AssertSQL("\"int\" = (\"int\" << 1)", int <<= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" << \"int\")", intOptional <<= int) - AssertSQL("\"intOptional\" = (\"intOptional\" << \"intOptional\")", intOptional <<= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" << 1)", intOptional <<= 1) - } - - func test_rightShiftEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" >> \"int\")", int >>= int) - AssertSQL("\"int\" = (\"int\" >> 1)", int >>= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" >> \"int\")", intOptional >>= int) - AssertSQL("\"intOptional\" = (\"intOptional\" >> \"intOptional\")", intOptional >>= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" >> 1)", intOptional >>= 1) - } - - func test_bitwiseAndEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" & \"int\")", int &= int) - AssertSQL("\"int\" = (\"int\" & 1)", int &= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" & \"int\")", intOptional &= int) - AssertSQL("\"intOptional\" = (\"intOptional\" & \"intOptional\")", intOptional &= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" & 1)", intOptional &= 1) - } - - func test_bitwiseOrEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (\"int\" | \"int\")", int |= int) - AssertSQL("\"int\" = (\"int\" | 1)", int |= 1) - AssertSQL("\"intOptional\" = (\"intOptional\" | \"int\")", intOptional |= int) - AssertSQL("\"intOptional\" = (\"intOptional\" | \"intOptional\")", intOptional |= intOptional) - AssertSQL("\"intOptional\" = (\"intOptional\" | 1)", intOptional |= 1) - } - - func test_bitwiseExclusiveOrEquals_withIntegerExpression_buildsSetter() { - AssertSQL("\"int\" = (~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^= int) - AssertSQL("\"int\" = (~((\"int\" & 1)) & (\"int\" | 1))", int ^= 1) - AssertSQL("\"intOptional\" = (~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^= int) - AssertSQL("\"intOptional\" = (~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^= intOptional) - AssertSQL("\"intOptional\" = (~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^= 1) - } - - func test_postfixPlus_withIntegerValue_buildsSetter() { - AssertSQL("\"int\" = (\"int\" + 1)", int++) - AssertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional++) - } - - func test_postfixMinus_withIntegerValue_buildsSetter() { - AssertSQL("\"int\" = (\"int\" - 1)", int--) - AssertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional--) - } - -} diff --git a/Tests/SQLiteTests/StatementTests.swift b/Tests/SQLiteTests/StatementTests.swift deleted file mode 100644 index 326259b2..00000000 --- a/Tests/SQLiteTests/StatementTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -import XCTest -import SQLite - -class StatementTests : SQLiteTestCase { - override func setUp() { - super.setUp() - CreateUsersTable() - } - - func test_cursor_to_blob() { - 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() { - let blobs = Table("blobs") - let blobColumn = 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) - } -} diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index 8d60362c..e62b2a23 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -1,24 +1,24 @@ import XCTest @testable import SQLite -class SQLiteTestCase : XCTestCase { - private var trace:[String: Int]! - var db:Connection! +class SQLiteTestCase: XCTestCase { + private var trace: [String: Int]! + var db: Connection! let users = Table("users") - override func setUp() { - super.setUp() - db = try! Connection() - trace = [String:Int]() + override func setUpWithError() throws { + try super.setUpWithError() + db = try Connection() + trace = [String: Int]() db.trace { SQL in - print(SQL) + // print("SQL: \(SQL)") self.trace[SQL, default: 0] += 1 } } - func CreateUsersTable() { - try! db.execute(""" + func createUsersTable() throws { + try db.execute(""" CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE, @@ -26,28 +26,27 @@ class SQLiteTestCase : XCTestCase { 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 { + try insertUsers(names) } - func InsertUsers(_ names: [String]) throws { - for name in names { try InsertUser(name) } + 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 { - return try db.run( - "INSERT INTO \"users\" (email, age, admin) values (?, ?, ?)", - "\(name)@example.com", age?.datatypeValue, admin.datatypeValue - ) + @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) { + func assertSQL(_ SQL: String, _ executions: Int = 1, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { XCTAssertEqual( executions, trace[SQL] ?? 0, message ?? SQL, @@ -55,9 +54,9 @@ class SQLiteTestCase : XCTestCase { ) } - func AssertSQL(_ SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { - try! statement.run() - AssertSQL(SQL, 1, message, file: file, line: line) + 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 } } @@ -67,46 +66,53 @@ class SQLiteTestCase : XCTestCase { // if let count = trace[SQL] { trace[SQL] = count - 1 } // } - func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) -> Void) { + func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) throws -> Void) throws { let expectation = self.expectation(description: description) - block({ expectation.fulfill() }) + try block({ expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) } } -let bool = Expression("bool") -let boolOptional = Expression("boolOptional") +let bool = SQLite.Expression("bool") +let boolOptional = SQLite.Expression("boolOptional") + +let data = SQLite.Expression("blob") +let dataOptional = SQLite.Expression("blobOptional") -let data = Expression("blob") -let dataOptional = Expression("blobOptional") +let date = SQLite.Expression("date") +let dateOptional = SQLite.Expression("dateOptional") -let date = Expression("date") -let dateOptional = Expression("dateOptional") +let double = SQLite.Expression("double") +let doubleOptional = SQLite.Expression("doubleOptional") -let double = Expression("double") -let doubleOptional = Expression("doubleOptional") +let int = SQLite.Expression("int") +let intOptional = SQLite.Expression("intOptional") -let int = Expression("int") -let intOptional = Expression("intOptional") +let int64 = SQLite.Expression("int64") +let int64Optional = SQLite.Expression("int64Optional") -let int64 = Expression("int64") -let int64Optional = Expression("int64Optional") +let string = SQLite.Expression("string") +let stringOptional = SQLite.Expression("stringOptional") -let string = Expression("string") -let stringOptional = Expression("stringOptional") +let uuid = SQLite.Expression("uuid") +let uuidOptional = SQLite.Expression("uuidOptional") -func AssertSQL(_ expression1: @autoclosure () -> String, _ expression2: @autoclosure () -> Expressible, file: StaticString = #file, line: UInt = #line) { +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 AssertThrows(_ expression: @autoclosure () throws -> T, file: StaticString = #file, line: UInt = #line) { - do { - _ = try expression() - XCTFail("expression expected to throw", file: file, line: line) - } catch { - XCTAssert(true, file: file, line: line) - } +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") @@ -114,22 +120,48 @@ let qualifiedTable = Table("table", database: "main") let virtualTable = VirtualTable("virtual_table") let _view = View("view") // avoid Mac XCTestCase collision -class TestCodable: Codable { +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, optional: String?, 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/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift similarity index 52% rename from Tests/SQLiteTests/RowTests.swift rename to Tests/SQLiteTests/Typed/RowTests.swift index 17873e71..1aed00a7 100644 --- a/Tests/SQLiteTests/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -1,53 +1,53 @@ import XCTest @testable import SQLite -class RowTests : XCTestCase { +class RowTests: XCTestCase { - public func test_get_value() { + public func test_get_value() throws { let row = Row(["\"foo\"": 0], ["value"]) - let result = try! row.get(Expression("foo")) + 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[Expression("foo")] + let result = row[SQLite.Expression("foo")] XCTAssertEqual("value", result) } - public func test_get_value_optional() { + public func test_get_value_optional() throws { let row = Row(["\"foo\"": 0], ["value"]) - let result = try! row.get(Expression("foo")) + 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[Expression("foo")] + let result = row[SQLite.Expression("foo")] XCTAssertEqual("value", result) } - public func test_get_value_optional_nil() { + public func test_get_value_optional_nil() throws { let row = Row(["\"foo\"": 0], [nil]) - let result = try! row.get(Expression("foo")) + 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[Expression("foo")] + 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(Expression("foo"))) { error in + XCTAssertThrowsError(try row.get(SQLite.Expression("foo"))) { error in if case QueryError.unexpectedNullValue(let name) = error { XCTAssertEqual("\"foo\"", name) } else { @@ -56,15 +56,15 @@ class RowTests : XCTestCase { } } - public func test_get_type_mismatch_optional_returns_nil() { + public func test_get_type_mismatch_optional_returns_nil() throws { let row = Row(["\"foo\"": 0], ["value"]) - let result = try! row.get(Expression("foo")) + 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(Expression("bar"))) { error in + XCTAssertThrowsError(try row.get(SQLite.Expression("bar"))) { error in if case QueryError.noSuchColumn(let name, let columns) = error { XCTAssertEqual("\"bar\"", name) XCTAssertEqual(["\"foo\""], columns) @@ -76,7 +76,7 @@ class RowTests : XCTestCase { public func test_get_ambiguous_column_throws() { let row = Row(["table1.\"foo\"": 0, "table2.\"foo\"": 1], ["value"]) - XCTAssertThrowsError(try row.get(Expression("foo"))) { error in + 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()) @@ -85,4 +85,34 @@ class RowTests : XCTestCase { } } } + + public func test_get_datatype_throws() { + // swiftlint:disable nesting + struct MyType: Value { + enum MyError: Error { + case failed + } + + public static var declaredDatatype: String { + Blob.declaredDatatype + } + + public static func fromDatatypeValue(_ dataValue: Blob) throws -> Data { + throw MyError.failed + } + + public var datatypeValue: Blob { + return Blob(bytes: []) + } + } + + let row = Row(["\"foo\"": 0], [Blob(bytes: [])]) + XCTAssertThrowsError(try row.get(SQLite.Expression("foo"))) { error in + if case MyType.MyError.failed = error { + XCTAssertTrue(true) + } else { + XCTFail("unexpected error: \(error)") + } + } + } } diff --git a/Tests/SQLiteTests/Typed/SelectTests.swift b/Tests/SQLiteTests/Typed/SelectTests.swift new file mode 100644 index 00000000..5fa3cd30 --- /dev/null +++ b/Tests/SQLiteTests/Typed/SelectTests.swift @@ -0,0 +1,44 @@ +import XCTest +@testable import SQLite + +class SelectTests: SQLiteTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + try createUsersTable() + try createUsersDataTable() + } + + func createUsersDataTable() throws { + try db.execute(""" + CREATE TABLE users_name ( + id INTEGER, + user_id INTEGER REFERENCES users(id), + name TEXT + ) + """ + ) + } + + func test_select_columns_from_multiple_tables() throws { + let usersData = Table("users_name") + let users = Table("users") + + let name = SQLite.Expression("name") + let id = SQLite.Expression("id") + let userID = SQLite.Expression("user_id") + let email = SQLite.Expression("email") + + try insertUser("Joey") + try db.run(usersData.insert( + id <- 1, + userID <- 1, + name <- "Joey" + )) + + try db.prepare(users.select(name, email).join(usersData, on: userID == users[id])).forEach { + XCTAssertEqual($0[name], "Joey") + XCTAssertEqual($0[email], "Joey@example.com") + } + } +} diff --git a/Tests/SQLiteTests/Typed/SetterTests.swift b/Tests/SQLiteTests/Typed/SetterTests.swift new file mode 100644 index 00000000..05da57a4 --- /dev/null +++ b/Tests/SQLiteTests/Typed/SetterTests.swift @@ -0,0 +1,140 @@ +import XCTest +import SQLite + +class SetterTests: XCTestCase { + + func test_setterAssignmentOperator_buildsSetter() { + assertSQL("\"int\" = \"int\"", int <- int) + assertSQL("\"int\" = 1", int <- 1) + assertSQL("\"intOptional\" = \"int\"", intOptional <- int) + assertSQL("\"intOptional\" = \"intOptional\"", intOptional <- intOptional) + assertSQL("\"intOptional\" = 1", intOptional <- 1) + assertSQL("\"intOptional\" = NULL", intOptional <- nil) + } + + func test_plusEquals_withStringExpression_buildsSetter() { + assertSQL("\"string\" = (\"string\" || \"string\")", string += string) + assertSQL("\"string\" = (\"string\" || 'literal')", string += "literal") + assertSQL("\"stringOptional\" = (\"stringOptional\" || \"string\")", stringOptional += string) + assertSQL("\"stringOptional\" = (\"stringOptional\" || \"stringOptional\")", stringOptional += stringOptional) + assertSQL("\"stringOptional\" = (\"stringOptional\" || 'literal')", stringOptional += "literal") + } + + func test_plusEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" + \"int\")", int += int) + assertSQL("\"int\" = (\"int\" + 1)", int += 1) + assertSQL("\"intOptional\" = (\"intOptional\" + \"int\")", intOptional += int) + assertSQL("\"intOptional\" = (\"intOptional\" + \"intOptional\")", intOptional += intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional += 1) + + assertSQL("\"double\" = (\"double\" + \"double\")", double += double) + assertSQL("\"double\" = (\"double\" + 1.0)", double += 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"double\")", doubleOptional += double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"doubleOptional\")", doubleOptional += doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" + 1.0)", doubleOptional += 1) + } + + func test_minusEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" - \"int\")", int -= int) + assertSQL("\"int\" = (\"int\" - 1)", int -= 1) + assertSQL("\"intOptional\" = (\"intOptional\" - \"int\")", intOptional -= int) + assertSQL("\"intOptional\" = (\"intOptional\" - \"intOptional\")", intOptional -= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional -= 1) + + assertSQL("\"double\" = (\"double\" - \"double\")", double -= double) + assertSQL("\"double\" = (\"double\" - 1.0)", double -= 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"double\")", doubleOptional -= double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"doubleOptional\")", doubleOptional -= doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" - 1.0)", doubleOptional -= 1) + } + + func test_timesEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" * \"int\")", int *= int) + assertSQL("\"int\" = (\"int\" * 1)", int *= 1) + assertSQL("\"intOptional\" = (\"intOptional\" * \"int\")", intOptional *= int) + assertSQL("\"intOptional\" = (\"intOptional\" * \"intOptional\")", intOptional *= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" * 1)", intOptional *= 1) + + assertSQL("\"double\" = (\"double\" * \"double\")", double *= double) + assertSQL("\"double\" = (\"double\" * 1.0)", double *= 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"double\")", doubleOptional *= double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"doubleOptional\")", doubleOptional *= doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" * 1.0)", doubleOptional *= 1) + } + + func test_dividedByEquals_withNumberExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" / \"int\")", int /= int) + assertSQL("\"int\" = (\"int\" / 1)", int /= 1) + assertSQL("\"intOptional\" = (\"intOptional\" / \"int\")", intOptional /= int) + assertSQL("\"intOptional\" = (\"intOptional\" / \"intOptional\")", intOptional /= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" / 1)", intOptional /= 1) + + assertSQL("\"double\" = (\"double\" / \"double\")", double /= double) + assertSQL("\"double\" = (\"double\" / 1.0)", double /= 1) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"double\")", doubleOptional /= double) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"doubleOptional\")", doubleOptional /= doubleOptional) + assertSQL("\"doubleOptional\" = (\"doubleOptional\" / 1.0)", doubleOptional /= 1) + } + + func test_moduloEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" % \"int\")", int %= int) + assertSQL("\"int\" = (\"int\" % 1)", int %= 1) + assertSQL("\"intOptional\" = (\"intOptional\" % \"int\")", intOptional %= int) + assertSQL("\"intOptional\" = (\"intOptional\" % \"intOptional\")", intOptional %= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" % 1)", intOptional %= 1) + } + + func test_leftShiftEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" << \"int\")", int <<= int) + assertSQL("\"int\" = (\"int\" << 1)", int <<= 1) + assertSQL("\"intOptional\" = (\"intOptional\" << \"int\")", intOptional <<= int) + assertSQL("\"intOptional\" = (\"intOptional\" << \"intOptional\")", intOptional <<= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" << 1)", intOptional <<= 1) + } + + func test_rightShiftEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" >> \"int\")", int >>= int) + assertSQL("\"int\" = (\"int\" >> 1)", int >>= 1) + assertSQL("\"intOptional\" = (\"intOptional\" >> \"int\")", intOptional >>= int) + assertSQL("\"intOptional\" = (\"intOptional\" >> \"intOptional\")", intOptional >>= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" >> 1)", intOptional >>= 1) + } + + func test_bitwiseAndEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" & \"int\")", int &= int) + assertSQL("\"int\" = (\"int\" & 1)", int &= 1) + assertSQL("\"intOptional\" = (\"intOptional\" & \"int\")", intOptional &= int) + assertSQL("\"intOptional\" = (\"intOptional\" & \"intOptional\")", intOptional &= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" & 1)", intOptional &= 1) + } + + func test_bitwiseOrEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (\"int\" | \"int\")", int |= int) + assertSQL("\"int\" = (\"int\" | 1)", int |= 1) + assertSQL("\"intOptional\" = (\"intOptional\" | \"int\")", intOptional |= int) + assertSQL("\"intOptional\" = (\"intOptional\" | \"intOptional\")", intOptional |= intOptional) + assertSQL("\"intOptional\" = (\"intOptional\" | 1)", intOptional |= 1) + } + + func test_bitwiseExclusiveOrEquals_withIntegerExpression_buildsSetter() { + assertSQL("\"int\" = (~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^= int) + assertSQL("\"int\" = (~((\"int\" & 1)) & (\"int\" | 1))", int ^= 1) + assertSQL("\"intOptional\" = (~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^= int) + assertSQL("\"intOptional\" = (~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^= intOptional) + assertSQL("\"intOptional\" = (~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^= 1) + } + + func test_postfixPlus_withIntegerValue_buildsSetter() { + assertSQL("\"int\" = (\"int\" + 1)", int++) + assertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional++) + } + + func test_postfixMinus_withIntegerValue_buildsSetter() { + assertSQL("\"int\" = (\"int\" - 1)", int--) + assertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional--) + } + + func test_setter_custom_string_convertible() { + XCTAssertEqual("\"int\" = \"int\"", (int <- int).description) + } +} diff --git a/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift b/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift new file mode 100644 index 00000000..01e88297 --- /dev/null +++ b/Tests/SQLiteTests/Typed/WindowFunctionsTests.swift @@ -0,0 +1,58 @@ +import XCTest +import SQLite + +class WindowFunctionsTests: XCTestCase { + + func test_ntile_wrapsExpressionWithOverClause() { + assertSQL("ntile(1) OVER (ORDER BY \"int\" DESC)", ntile(1, int.desc)) + assertSQL("ntile(20) OVER (ORDER BY \"intOptional\" ASC)", ntile(20, intOptional.asc)) + assertSQL("ntile(20) OVER (ORDER BY \"double\" ASC)", ntile(20, double.asc)) + assertSQL("ntile(1) OVER (ORDER BY \"doubleOptional\" ASC)", ntile(1, doubleOptional.asc)) + assertSQL("ntile(1) OVER (ORDER BY \"int\" DESC)", ntile(1, int.desc)) + } + + func test_row_number_wrapsExpressionWithOverClause() { + assertSQL("row_number() OVER (ORDER BY \"int\" DESC)", rowNumber(int.desc)) + } + + func test_rank_wrapsExpressionWithOverClause() { + assertSQL("rank() OVER (ORDER BY \"int\" DESC)", rank(int.desc)) + } + + func test_dense_rank_wrapsExpressionWithOverClause() { + assertSQL("dense_rank() OVER (ORDER BY \"int\" DESC)", denseRank(int.desc)) + } + + func test_percent_rank_wrapsExpressionWithOverClause() { + assertSQL("percent_rank() OVER (ORDER BY \"int\" DESC)", percentRank(int.desc)) + } + + func test_cume_dist_wrapsExpressionWithOverClause() { + assertSQL("cume_dist() OVER (ORDER BY \"int\" DESC)", cumeDist(int.desc)) + } + + func test_lag_wrapsExpressionWithOverClause() { + assertSQL("lag(\"int\", 0) OVER (ORDER BY \"int\" DESC)", int.lag(int.desc)) + assertSQL("lag(\"int\", 7) OVER (ORDER BY \"int\" DESC)", int.lag(offset: 7, int.desc)) + assertSQL("lag(\"int\", 1, 3) OVER (ORDER BY \"int\" DESC)", int.lag(offset: 1, default: SQLite.Expression(value: 3), int.desc)) + } + + func test_lead_wrapsExpressionWithOverClause() { + assertSQL("lead(\"int\", 0) OVER (ORDER BY \"int\" DESC)", int.lead(int.desc)) + assertSQL("lead(\"int\", 7) OVER (ORDER BY \"int\" DESC)", int.lead(offset: 7, int.desc)) + assertSQL("lead(\"int\", 1, 3) OVER (ORDER BY \"int\" DESC)", int.lead(offset: 1, default: SQLite.Expression(value: 3), int.desc)) + } + + func test_firstValue_wrapsExpressionWithOverClause() { + assertSQL("first_value(\"int\") OVER (ORDER BY \"int\" DESC)", int.firstValue(int.desc)) + assertSQL("first_value(\"double\") OVER (ORDER BY \"int\" DESC)", double.firstValue(int.desc)) + } + + func test_lastValue_wrapsExpressionWithOverClause() { + assertSQL("last_value(\"int\") OVER (ORDER BY \"int\" DESC)", int.lastValue(int.desc)) + } + + func test_nth_value_wrapsExpressionWithOverClause() { + assertSQL("nth_value(\"int\", 3) OVER (ORDER BY \"int\" DESC)", int.value(3, int.desc)) + } +} diff --git a/run-tests.sh b/run-tests.sh index 0a105c41..3ffba810 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -7,9 +7,19 @@ if [ -n "$BUILD_SCHEME" ]; then make test BUILD_SCHEME="$BUILD_SCHEME" fi elif [ -n "$VALIDATOR_SUBSPEC" ]; then - cd Tests/CocoaPods && make test + bundle install + case "$VALIDATOR_SUBSPEC" in + none) + bundle exec pod lib lint --no-subspecs --fail-fast + ;; + *) + bundle exec pod lib lint --subspec="${VALIDATOR_SUBSPEC}" --fail-fast + ;; + esac elif [ -n "$CARTHAGE_PLATFORM" ]; then cd Tests/Carthage && make test CARTHAGE_PLATFORM="$CARTHAGE_PLATFORM" +elif [ -n "$SPM" ]; then + cd Tests/SPM && swift "${SPM}" elif [ -n "${PACKAGE_MANAGER_COMMAND}" ]; then swift ${PACKAGE_MANAGER_COMMAND} fi