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/template.md b/.github/issue_template.md similarity index 87% rename from .github/ISSUE_TEMPLATE/template.md rename to .github/issue_template.md index 9304a672..f9622b9b 100644 --- a/.github/ISSUE_TEMPLATE/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 index baa8c2d8..3c6970f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,20 +1,13 @@ name: Build and test on: [push, pull_request] env: - IOS_SIMULATOR: iPhone 12 - IOS_VERSION: "15.0" + IOS_SIMULATOR: "iPhone 16" + IOS_VERSION: "18.4" jobs: build: - runs-on: macos-11 + runs-on: macos-15 steps: - uses: actions/checkout@v2 - - name: Install - run: | - gem update bundler - gem install xcpretty --no-document - brew update - brew outdated carthage || brew upgrade carthage - brew outdated swiftlint || brew upgrade swiftlint - name: "Lint" run: make lint - name: "Run tests (PACKAGE_MANAGER_COMMAND: test)" @@ -65,6 +58,10 @@ jobs: 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: @@ -79,3 +76,9 @@ jobs: 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/.swiftlint.yml b/.swiftlint.yml index 34bb253b..1bb21cd5 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,8 +1,12 @@ +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 @@ -25,8 +29,8 @@ identifier_name: - SQLITE_TRANSIENT type_body_length: - warning: 260 - error: 260 + warning: 350 + error: 350 function_body_length: warning: 60 diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d0218e..b77a8b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,75 @@ +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] ======================================== @@ -102,9 +174,19 @@ [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 @@ -136,9 +218,57 @@ [#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/Documentation/Index.md b/Documentation/Index.md index 52affae4..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) + - [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,45 +40,65 @@ - [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 5 (and -> [Xcode 10.2](https://developer.apple.com/xcode/downloads/)) or greater. - ### Swift Package Manager The [Swift Package Manager][] is a tool for managing the distribution of @@ -81,7 +109,7 @@ process of downloading, compiling, and linking dependencies. ```swift dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.2") + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") ] ``` @@ -102,7 +130,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.13.2 + github "stephencelis/SQLite.swift" ~> 0.15.4 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. @@ -132,7 +160,7 @@ install SQLite.swift with Carthage: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.13.2' + pod 'SQLite.swift', '~> 0.15.4' end ``` @@ -146,7 +174,7 @@ with the OS you can require the `standalone` subspec: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.13.2' + pod 'SQLite.swift/standalone', '~> 0.15.4' end ``` @@ -156,7 +184,7 @@ dependency to sqlite3 or one of its subspecs: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.13.2' + pod 'SQLite.swift/standalone', '~> 0.15.4' pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled end ``` @@ -166,13 +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 # 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.13.2' + pod 'SQLite.swift/SQLCipher', '~> 0.15.4' end ``` @@ -182,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 @@ -279,11 +314,12 @@ 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 +// create parent directory inside application support if it doesn’t exist try FileManager.default.createDirectory( atPath: path, withIntermediateDirectories: true, attributes: nil ) @@ -298,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) ``` @@ -315,6 +351,13 @@ let db = try Connection(path, readonly: true) > 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 @@ -333,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 @@ -371,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. @@ -654,7 +710,7 @@ do { } ``` -Multiple rows can be inserted at once by similarily calling `insertMany` with an array of +Multiple rows can be inserted at once by similarly calling `insertMany` with an array of per-row [setters](#setters). ```swift @@ -806,7 +862,7 @@ let query = try db.prepare(users) for user in query { // 💥 can throw an error here } -```` +``` #### Failable iteration @@ -1086,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 @@ -1170,7 +1285,7 @@ let count = try db.scalar(users.filter(name != nil).count) 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 +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 @@ -1290,13 +1405,43 @@ 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` @@ -1307,6 +1452,24 @@ 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 @@ -1371,13 +1534,66 @@ tables](#creating-a-table). // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" ("id") ``` -### Renaming Columns +### 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") +} +``` -Added in SQLite 3.25.0, not exposed yet. [#1073](https://github.com/stephencelis/SQLite.swift/issues/1073) +#### Renaming/Dropping Tables -### Dropping Columns +```swift +let schemaChanger = SchemaChanger(connection: db) + +try schemaChanger.rename(table: "users", to: "users_new") +try schemaChanger.drop(table: "emails", ifExists: false) +``` + +#### Creating Tables -Added in SQLite 3.35.0, not exposed yet. [#1073](https://github.com/stephencelis/SQLite.swift/issues/1073) +```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 @@ -1434,27 +1650,6 @@ 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 use the convenience property on `Connection` to query and set the @@ -1709,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) @@ -1791,6 +1991,14 @@ 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 @@ -1902,8 +2110,6 @@ try db.run(emails.create(.FTS5(config))) // the last FTS4 query above as: 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 @@ -2002,6 +2208,32 @@ let backup = try db.backup(usingConnection: target) try backup.step() ``` +## Attaching and detaching databases + +We can [ATTACH](https://www3.sqlite.org/lang_attach.html) and [DETACH](https://www3.sqlite.org/lang_detach.html) +databases to an existing connection: + +```swift +let db = try Connection("db.sqlite") + +try db.attach(.uri("external.sqlite", parameters: [.mode(.readOnly)]), as: "external") +// ATTACH DATABASE 'file:external.sqlite?mode=ro' AS 'external' + +let table = Table("table", database: "external") +let count = try db.scalar(table.count) +// SELECT count(*) FROM 'external.table' + +try db.detach("external") +// DETACH DATABASE 'external' +``` + +When compiled for SQLCipher, we can additionally pass a `key` parameter to `attach`: + +```swift +try db.attach(.uri("encrypted.sqlite"), as: "encrypted", key: "secret") +// ATTACH DATABASE 'encrypted.sqlite' AS 'encrypted' KEY 'secret' +``` + ## Logging We can log SQL using the database’s `trace` function. diff --git a/Documentation/Linux.md b/Documentation/Linux.md index 640ced6f..0c88ff5c 100644 --- a/Documentation/Linux.md +++ b/Documentation/Linux.md @@ -2,9 +2,8 @@ ## Limitations -* Custom functions are currently not supported and crash, caused by a bug in Swift. +* 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). -* FTS5 might not work, see [#1007](https://github.com/stephencelis/SQLite.swift/issues/1007) ## Debugging diff --git a/Documentation/Planning.md b/Documentation/Planning.md index 5b50219a..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.13.3 milestone](https://github.com/stephencelis/SQLite.swift/issues?q=is%3Aopen+is%3Aissue+milestone%3A0.13.3) +> 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 diff --git a/Documentation/Release.md b/Documentation/Release.md index 87d715f6..8ec67970 100644 --- a/Documentation/Release.md +++ b/Documentation/Release.md @@ -3,6 +3,7 @@ * [ ] 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` 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 c62da000..52a25a12 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,76 @@ -BUILD_TOOL = xcodebuild +XCODEBUILD = xcodebuild BUILD_SCHEME = SQLite Mac -IOS_SIMULATOR = iPhone 12 -IOS_VERSION = 15.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) -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 -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 +lint-fix: $(SWIFTLINT) + $< --fix clean: - $(BUILD_TOOL) $(BUILD_ARGUMENTS) clean + $(XCODEBUILD) $(BUILD_ARGUMENTS) clean repl: - @$(BUILD_TOOL) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ - swift -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' + @$(XCODEBUILD) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ + swift repl -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' sloc: - @zsh -c "grep -vE '^ *//|^$$' Sources/**/*.{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 clean repl sloc diff --git a/Package.swift b/Package.swift index 24898bca..56925d18 100644 --- a/Package.swift +++ b/Package.swift @@ -1,13 +1,46 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 import PackageDescription +let deps: [Package.Dependency] = [ + .github("swiftlang/swift-toolchain-sqlite", exact: "1.0.4") +] + +let targets: [Target] = [ + .target( + name: "SQLite", + dependencies: [ + .product(name: "SwiftToolchainCSQLite", package: "swift-toolchain-sqlite", condition: .when(platforms: [.linux, .windows, .android])) + ], + exclude: [ + "Info.plist" + ] + ) +] + +let testTargets: [Target] = [ + .testTarget( + name: "SQLiteTests", + dependencies: [ + "SQLite" + ], + path: "Tests/SQLiteTests", + exclude: [ + "Info.plist" + ], + resources: [ + .copy("Resources") + ] + ) +] + let package = Package( name: "SQLite.swift", platforms: [ - .iOS(.v9), - .macOS(.v10_10), - .watchOS(.v3), - .tvOS(.v9) + .iOS(.v12), + .macOS(.v10_13), + .watchOS(.v4), + .tvOS(.v12), + .visionOS(.v1) ], products: [ .library( @@ -15,50 +48,13 @@ let package = Package( targets: ["SQLite"] ) ], - targets: [ - .target( - name: "SQLite", - dependencies: ["SQLiteObjc"], - exclude: [ - "Info.plist" - ] - ), - .target( - name: "SQLiteObjc", - dependencies: [], - exclude: [ - "fts3_tokenizer.h" - ] - ), - .testTarget( - name: "SQLiteTests", - dependencies: [ - "SQLite" - ], - path: "Tests/SQLiteTests", - exclude: [ - "Info.plist" - ], - resources: [ - .copy("fixtures/encrypted-3.x.sqlite"), - .copy("fixtures/encrypted-4.x.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", - dependencies: [.product(name: "CSQLite", package: "CSQLite")], - exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"] - ), - .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests", exclude: [ - "FTSIntegrationTests.swift", - "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 a2b5e537..a52b7ae5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ 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](https://stackoverflow.com/questions/tagged/sqlite.swift), @@ -27,6 +28,7 @@ syntax _and_ intent. [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 @@ -40,9 +42,9 @@ do { let db = try Connection("path/to/db.sqlite3") let users = Table("users") - let id = Expression("id") - let name = Expression("name") - let email = Expression("email") + 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) @@ -81,6 +83,9 @@ do { } ``` +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. @@ -88,7 +93,7 @@ API. // 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) @@ -115,18 +120,8 @@ 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]. - - -[Create a Data Access Layer with SQLite.swift and Swift 2]: https://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html -[SQLiteDataAccessLayer2]: https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master - ## Installation -> _Note:_ Version 0.11.6 and later requires Swift 5 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.2) or greater. Version 0.11.5 requires Swift 4.2 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.1) or greater. - ### Swift Package Manager The [Swift Package Manager][] is a tool for managing the distribution of @@ -136,7 +131,7 @@ Swift code. ```swift dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.2") + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") ] ``` @@ -160,7 +155,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.13.2 + github "stephencelis/SQLite.swift" ~> 0.15.4 ``` 3. Run `carthage update` and @@ -191,7 +186,7 @@ SQLite.swift with CocoaPods: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.13.2' + pod 'SQLite.swift', '~> 0.15.0' end ``` @@ -234,8 +229,6 @@ device: ## 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 @@ -243,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]: 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)) @@ -267,30 +259,24 @@ These projects enhance or use SQLite.swift: - [SQLiteMigrationManager.swift][] (inspired by [FMDBMigrationManager][]) - - [Delta: Math helper](https://apps.apple.com/app/delta-math-helper/id1436506800) - (see [Delta/Utils/Database.swift](https://github.com/GroupeMINASTE/Delta-iOS/blob/master/Delta/Utils/Database.swift) for production implementation example) - ## 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) [Swift]: https://swift.org/ [SQLite3]: https://www.sqlite.org [SQLite.swift]: https://github.com/stephencelis/SQLite.swift -[GitHubActionBadge]: https://img.shields.io/github/workflow/status/stephencelis/SQLite.swift/Build%20and%20test +[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 +[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 +[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 diff --git a/SQLite.playground/Contents.swift b/SQLite.playground/Contents.swift index c089076d..d893e696 100644 --- a/SQLite.playground/Contents.swift +++ b/SQLite.playground/Contents.swift @@ -52,6 +52,7 @@ for user in try Array(rowIterator) { /// also with `map()` let mapRowIterator = try db.prepareRowIterator(users) + let userIds = try mapRowIterator.map { $0[id] } /// using `failableNext()` on `RowIterator` @@ -64,7 +65,7 @@ do { // Handle error } -/// define a virtual tabe for the FTS index +/// define a virtual table for the FTS index let emails = VirtualTable("emails") let subject = Expression("subject") @@ -99,5 +100,33 @@ db.createAggregation("customConcat", initialValue: "users:", reduce: reduce, result: { $0 }) -let result = db.prepare("SELECT customConcat(email) FROM users").scalar() as! String +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.swift.podspec b/SQLite.swift.podspec index 23146eee..73c6f705 100644 --- a/SQLite.swift.podspec +++ b/SQLite.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SQLite.swift" - s.version = "0.13.2" + s.version = "0.15.4" s.summary = "A type-safe, Swift-language layer over SQLite3." s.description = <<-DESC @@ -18,66 +18,61 @@ Pod::Spec.new do |s| s.default_subspec = 'standard' s.swift_versions = ['5'] - ios_deployment_target = '9.0' - tvos_deployment_target = '9.1' - osx_deployment_target = '10.10' - watchos_deployment_target = '3.0' - - s.ios.deployment_target = ios_deployment_target - s.tvos.deployment_target = tvos_deployment_target - s.osx.deployment_target = osx_deployment_target - s.watchos.deployment_target = watchos_deployment_target + 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/fts3_tokenizer.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' - test_spec.ios.deployment_target = ios_deployment_target - test_spec.tvos.deployment_target = tvos_deployment_target - test_spec.osx.deployment_target = osx_deployment_target 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/fts3_tokenizer.h' + ss.resource_bundle = { 'SQLite.swift' => 'Sources/SQLite/PrivacyInfo.xcprivacy' } ss.xcconfig = { 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE', 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_SWIFT_STANDALONE=1' } ss.dependency 'sqlite3' - + ss.test_spec 'tests' do |test_spec| - test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.resources = 'Tests/SQLiteTests/Resources/*' test_spec.source_files = 'Tests/SQLiteTests/*.swift' - test_spec.ios.deployment_target = ios_deployment_target - test_spec.tvos.deployment_target = tvos_deployment_target - test_spec.osx.deployment_target = osx_deployment_target end end s.subspec 'SQLCipher' do |ss| - ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' - ss.private_header_files = 'Sources/SQLiteObjc/fts3_tokenizer.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 SQLITE_SWIFT_SQLCIPHER=1' } 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' - test_spec.ios.deployment_target = ios_deployment_target - test_spec.tvos.deployment_target = tvos_deployment_target - test_spec.osx.deployment_target = osx_deployment_target end end end diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index 26c5e975..ec0423e9 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -17,8 +17,6 @@ 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; 03A65E761C6BB2E60062603F /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; 03A65E771C6BB2E60062603F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; - 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; - 03A65E791C6BB2EF0062603F /* SQLiteObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLiteObjc.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 */; }; @@ -32,74 +30,136 @@ 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 */; }; - 19A171BE056457F13BFBC4C3 /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171FDE4D67879B14ACBDF /* FTSIntegrationTests.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 */; }; - 19A172A9B536D02D4A98AAAD /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.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 */; }; + 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 */; }; - 19A176406BDE9D9C80CC9FA3 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */; }; - 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; - 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.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 */; }; - 19A17C013B00682B70D53DB8 /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171FDE4D67879B14ACBDF /* FTSIntegrationTests.swift */; }; - 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; - 19A17C80076860CF7751A056 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; - 19A17C9407AC0EE104E5CC85 /* FTSIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171FDE4D67879B14ACBDF /* FTSIntegrationTests.swift */; }; - 19A17CB808ACF606726E77A8 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */; }; + 19A17A33EA026C2E2CEBAF36 /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; + 19A17A391BF056E3D729E70A /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171A7714C6524093255C5 /* SchemaTests.swift */; }; + 19A17A52BF29D27C9AA229E7 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + 19A17A7B3E3B7E76364A2AEE /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + 19A17A7DF99B0379FD3396B1 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17F2B5959B212A33C383F /* CustomFunctionsTests.swift */; }; + 19A17A9520802ACF45907970 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + 19A17ABCF0EB4808BDC5B5FF /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A174FE5B47A97937A27276 /* RowTests.swift */; }; + 19A17B0DF1DDB6BBC9C95D64 /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17B1D9B5CEBE9CE09280C /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; + 19A17B36ABC6006AB80F693C /* Connection+SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17CA1DF7D0F7C9A94C51C /* Connection+SchemaTests.swift */; }; + 19A17B62A4125AF4F6014CF5 /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; + 19A17BA13FD35F058787B7D3 /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17BACF4C032513DE1F879 /* Connection+PragmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17C60D3464461057E4D63 /* Connection+PragmaTests.swift */; }; + 19A17C74233AFC2EDAFA23DC /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1745BE8623D8C6808DB3C /* ResultTests.swift */; }; + 19A17CA4D7B63D845428A9C5 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + 19A17CA6ADB78A2E545BF836 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1787E16C8562C09C076F5 /* CipherTests.swift */; }; + 19A17CF65C0196E03BC64519 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17855BD524FF888265B3C /* ConnectionTests.swift */; }; + 19A17D1BEABA610ABF003D67 /* SchemaDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */; }; + 19A17D6EC40BC35A5DC81BA8 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1756EB81E9F7F45B12A78 /* StatementTests.swift */; }; + 19A17D993398B8215B73E1EA /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; + 19A17DAD5975D9367EAA46E2 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17162C9861E5C4900455D /* RTreeTests.swift */; }; 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; + 19A17DD33C2E43DD6EE05A60 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170F141BF21946D159083 /* ExpressionTests.swift */; }; + 19A17DE1FCDB5695702AD24D /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17EC0C43015063945D32E /* SelectTests.swift */; }; + 19A17DE34C477232592A8F6B /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B55F2409B7031443495 /* CoreFunctionsTests.swift */; }; 19A17DF8D4F13A20F5D2269E /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; + 19A17DFE05ED8B1F7C45F7EE /* SchemaChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171B262DDE8718513CFDA /* SchemaChanger.swift */; }; 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17E0ABA6C415F014CD51C /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1709D5BDD2691BA160012 /* SetterTests.swift */; }; + 19A17E1DD976D5CE80018749 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1715904F7B6851FCB5EF6 /* FTS4Tests.swift */; }; 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A17E3F47DA087E2B76D087 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A171ED017645C8B04DF9F2 /* QueryTests.swift */; }; + 19A17E80F736EEE8EE2AA4CE /* SchemaDefinitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A176174862D0F0139B3987 /* SchemaDefinitionsTests.swift */; }; 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A17F0BF02896E1664F4090 /* Connection+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */; }; 19A17F1B3F0A3C96B5ED6D64 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; - 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 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 */; }; - 3717F908221F5D8800B9BD3D /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */; }; - 3717F909221F5D8900B9BD3D /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */; }; - 3717F90A221F5D8900B9BD3D /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */; }; 3D67B3E61DB2469200A4F4C6 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */; }; 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; @@ -119,26 +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 /* SQLiteObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLiteObjc.m */; }; 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 */; }; - 3DDC112F26CDBA0200CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3DDC113626CDBE1900CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3DDC113726CDBE1900CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3DDC113826CDBE1C00CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78B28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B79628846FCC005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79828846FED005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79928847055005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; - D4DB368C20C09CFB00D5A58E /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB368A20C09C9B00D5A58E /* SelectTests.swift */; }; - D4DB368D20C09CFC00D5A58E /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB368A20C09C9B00D5A58E /* SelectTests.swift */; }; - D4DB368E20C09CFD00D5A58E /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB368A20C09C9B00D5A58E /* SelectTests.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 /* SQLiteObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLiteObjc.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 */; }; @@ -155,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 /* SQLiteObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLiteObjc.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 */; }; @@ -217,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 */; @@ -238,30 +375,69 @@ 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 = ""; }; - 19A171FDE4D67879B14ACBDF /* FTSIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTSIntegrationTests.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 = ""; }; + 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 = ""; }; - 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowTests.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 = ""; }; - 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryIntegrationTests.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 = ""; }; - 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAggregationTests.swift; 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; }; - 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SQLiteObjc.h; path = ../SQLiteObjc/include/SQLiteObjc.h; sourceTree = ""; }; + 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; }; - D4DB368A20C09C9B00D5A58E /* SelectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTests.swift; sourceTree = ""; }; + 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 = ""; }; @@ -269,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 /* SQLiteObjc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SQLiteObjc.m; path = ../../SQLiteObjc/SQLiteObjc.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 = ""; }; @@ -287,20 +461,6 @@ 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 = ""; }; @@ -339,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; @@ -374,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 = ( @@ -385,9 +630,10 @@ EE247AC91C3F04ED00AE3E12 = { isa = PBXGroup; children = ( + 3D3C3CCB26E5568800759140 /* SQLite.playground */, EE247AD51C3F04ED00AE3E12 /* SQLite */, EE247AE11C3F04ED00AE3E12 /* SQLiteTests */, - 3D3C3CCB26E5568800759140 /* SQLite.playground */, + DEB307132B61D04500F9D46B /* SQLite visionOS.xctestplan */, EE247B8A1C3F81D000AE3E12 /* Metadata */, EE247AD41C3F04ED00AE3E12 /* Products */, 3D67B3E41DB2469200A4F4C6 /* Frameworks */, @@ -406,6 +652,8 @@ 03A65E5A1C6BB0F50062603F /* SQLite.framework */, 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */, A121AC451CA35C79005A31D1 /* SQLite.framework */, + DEB306E52B61CEF500F9D46B /* SQLite.framework */, + DEB307112B61CF9500F9D46B /* SQLiteTests visionOS.xctest */, ); name = Products; sourceTree = ""; @@ -414,13 +662,14 @@ isa = PBXGroup; children = ( EE247AD61C3F04ED00AE3E12 /* SQLite.h */, - 3DDC112E26CDBA0200CE369F /* SQLiteObjc.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; @@ -429,33 +678,15 @@ EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { isa = PBXGroup; children = ( - 19A17E2695737FAB5D6086E3 /* fixtures */, - EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */, - EE247B1B1C3F137700AE3E12 /* BlobTests.swift */, - EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */, - EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */, - EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */, - 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.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 */, - D4DB368A20C09C9B00D5A58E /* SelectTests.swift */, - 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */, - 19A171FDE4D67879B14ACBDF /* FTSIntegrationTests.swift */, + 19A17B56FBA20E7245BC8AC0 /* Schema */, + 19A17E470E4492D287C0D12F /* Extensions */, + 19A1798E3459573BEE50FA34 /* Core */, + 19A17AECBF878B1DAE0AE3DD /* Typed */, ); name = SQLiteTests; path = Tests/SQLiteTests; @@ -466,14 +697,17 @@ children = ( EE247AEE1C3F06E900AE3E12 /* Blob.swift */, EE247AEF1C3F06E900AE3E12 /* Connection.swift */, - EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */, - EE247AF11C3F06E900AE3E12 /* SQLiteObjc.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 = ""; @@ -481,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 = ""; @@ -493,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 */, @@ -511,8 +747,10 @@ isa = PBXGroup; children = ( EE247B771C3F40D700AE3E12 /* README.md */, + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */, EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, + 3DFC0B862886C239001C8FC9 /* Package.swift */, EE247B8D1C3F821200AE3E12 /* Makefile */, EE9180931C46EA210038162A /* libsqlite3.tbd */, EE9180911C46E9D30038162A /* libsqlite3.tbd */, @@ -526,6 +764,7 @@ isa = PBXGroup; children = ( EE247B8F1C3F822500AE3E12 /* Index.md */, + 3DF7B79B2884C901005DD8CA /* Planning.md */, EE247B901C3F822500AE3E12 /* Resources */, 19A17EA3A313F129011B3FA0 /* Release.md */, 19A1794B7972D14330A65BBD /* Linux.md */, @@ -549,8 +788,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */, - 3DDC113626CDBE1900CE369F /* SQLiteObjc.h in Headers */, 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -560,8 +797,14 @@ buildActionMask = 2147483647; files = ( 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, - 3DDC113726CDBE1900CE369F /* SQLiteObjc.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; }; @@ -569,8 +812,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */, - 3DDC113826CDBE1C00CE369F /* SQLiteObjc.h in Headers */, EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -579,8 +820,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */, - 3DDC112F26CDBA0200CE369F /* SQLiteObjc.h in Headers */, EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -592,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 = ( @@ -628,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 = ( @@ -642,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 = ( @@ -682,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 = ( @@ -773,6 +1048,8 @@ 03A65E591C6BB0F50062603F /* SQLite tvOS */, 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */, A121AC441CA35C79005A31D1 /* SQLite watchOS */, + DEB306B82B61CEF500F9D46B /* SQLite visionOS */, + DEB306E72B61CF9500F9D46B /* SQLiteTests visionOS */, ); }; /* End PBXProject section */ @@ -782,6 +1059,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0392B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -789,7 +1067,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */, + 3DF7B79828846FED005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -797,6 +1075,22 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A03A2B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB306E12B61CEF500F9D46B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEB3070C2B61CF9500F9D46B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEB3070D2B61CF9500F9D46B /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -804,6 +1098,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0372B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -811,7 +1106,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A17FDA323BAFDEC627E76F /* fixtures in Resources */, + 3DF7B79628846FCC005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -819,6 +1114,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAE5A0382B893C43007C7EA4 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -826,7 +1122,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A175DFF47B84757E547C62 /* fixtures in Resources */, + 3DF7B79928847055005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -841,14 +1137,16 @@ 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, - 03A65E791C6BB2EF0062603F /* SQLiteObjc.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 */, @@ -861,8 +1159,16 @@ 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; }; @@ -870,31 +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 */, - 3717F90A221F5D8900B9BD3D /* CustomAggregationTests.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 */, - D4DB368E20C09CFD00D5A58E /* SelectTests.swift in Sources */, - 19A17CB808ACF606726E77A8 /* QueryIntegrationTests.swift in Sources */, - 19A17C9407AC0EE104E5CC85 /* FTSIntegrationTests.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; }; @@ -902,13 +1216,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3D67B3F91DB246E700A4F4C6 /* SQLiteObjc.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 */, @@ -917,6 +1235,7 @@ 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 */, @@ -927,6 +1246,91 @@ 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; }; @@ -938,19 +1342,21 @@ 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 /* SQLiteObjc.m in Sources */, EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */, EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */, 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */, @@ -958,8 +1364,16 @@ 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; }; @@ -967,31 +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 */, - 3717F908221F5D8800B9BD3D /* CustomAggregationTests.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 */, - D4DB368C20C09CFB00D5A58E /* SelectTests.swift in Sources */, - 19A172A9B536D02D4A98AAAD /* QueryIntegrationTests.swift in Sources */, - 19A17C013B00682B70D53DB8 /* FTSIntegrationTests.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; }; @@ -1003,14 +1425,16 @@ 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, - EE247B681C3F3FEC00AE3E12 /* SQLiteObjc.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 */, @@ -1023,8 +1447,16 @@ 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; }; @@ -1032,31 +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 */, - 3717F909221F5D8900B9BD3D /* CustomAggregationTests.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 */, - D4DB368D20C09CFC00D5A58E /* SelectTests.swift in Sources */, - 19A176406BDE9D9C80CC9FA3 /* QueryIntegrationTests.swift in Sources */, - 19A171BE056457F13BFBC4C3 /* FTSIntegrationTests.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; }; @@ -1068,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 */; @@ -1099,6 +1544,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; @@ -1120,6 +1566,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -1133,6 +1580,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; @@ -1146,6 +1594,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -1169,6 +1618,7 @@ SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1192,6 +1642,82 @@ SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 4.0; + }; + name = Release; + }; + DEB306E32B61CEF500F9D46B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = xros; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Debug; + }; + DEB306E42B61CEF500F9D46B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = xros; + SKIP_INSTALL = YES; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Release; + }; + DEB3070F2B61CF9500F9D46B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = xros; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; + }; + name = Debug; + }; + DEB307102B61CF9500F9D46B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = xros; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + TARGETED_DEVICE_FAMILY = 7; }; name = Release; }; @@ -1244,8 +1770,9 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MACOSX_DEPLOYMENT_TARGET = 10.10; + 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 = ""; @@ -1253,10 +1780,10 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; - TVOS_DEPLOYMENT_TARGET = 9.1; + TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 3.0; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; @@ -1303,19 +1830,20 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MACOSX_DEPLOYMENT_TARGET = 10.10; + 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 = 9.1; + TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 3.0; + WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; @@ -1332,8 +1860,8 @@ GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 0.13.2; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; @@ -1355,8 +1883,8 @@ GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 0.13.2; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; @@ -1369,6 +1897,7 @@ buildSettings = { GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1381,6 +1910,7 @@ buildSettings = { GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1403,6 +1933,7 @@ 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.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; @@ -1427,6 +1958,7 @@ 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.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; @@ -1444,6 +1976,7 @@ GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1459,6 +1992,7 @@ GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1496,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/xcshareddata/xcschemes/SQLite visionOS.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite visionOS.xcscheme new file mode 100644 index 00000000..c1536b66 --- /dev/null +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite visionOS.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/SQLite/Core/Backup.swift b/Sources/SQLite/Core/Backup.swift index 84ebf1e0..023acc08 100644 --- a/Sources/SQLite/Core/Backup.swift +++ b/Sources/SQLite/Core/Backup.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 @@ -140,7 +140,7 @@ public final class Backup { /// - 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) diff --git a/Sources/SQLite/Core/Blob.swift b/Sources/SQLite/Core/Blob.swift index cd31483b..a709fb42 100644 --- a/Sources/SQLite/Core/Blob.swift +++ b/Sources/SQLite/Core/Blob.swift @@ -37,22 +37,18 @@ public struct Blob { public func toHex() -> String { bytes.map { - ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) + ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) }.joined(separator: "") } - } extension Blob: CustomStringConvertible { - public var description: String { "x'\(toHex())'" } - } extension Blob: Equatable { - } public func ==(lhs: Blob, rhs: Blob) -> Bool { diff --git a/Sources/SQLite/Core/Connection+Aggregation.swift b/Sources/SQLite/Core/Connection+Aggregation.swift index 4eea76c3..a1abb74a 100644 --- a/Sources/SQLite/Core/Connection+Aggregation.swift +++ b/Sources/SQLite/Core/Connection+Aggregation.swift @@ -3,8 +3,8 @@ import Foundation import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif 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 72e8d857..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. @@ -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) } @@ -152,14 +156,10 @@ public final class Connection { Int(sqlite3_total_changes(handle)) } - /// The user version of the database. - /// See SQLite [PRAGMA user_version](https://sqlite.org/pragma.html#pragma_user_version) - public var userVersion: Int32? { - get { - (try? scalar("PRAGMA user_version") as? Int64).map(Int32.init) - } - set { - _ = try? run("PRAGMA user_version = \(newValue ?? 0)") + /// 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) } } @@ -414,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 @@ -449,7 +449,7 @@ public final class Connection { @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 @@ -458,7 +458,7 @@ public final class Connection { callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) } sqlite3_trace(handle, { (context: UnsafeMutableRawPointer?, SQL: UnsafePointer?) in - if let context = context, let SQL = SQL { + if let context, let SQL { unsafeBitCast(context, to: Trace.self)(SQL) } }, @@ -469,7 +469,7 @@ public final class Connection { @available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) fileprivate func trace_v2(_ callback: ((String) -> Void)?) { - guard let callback = callback else { + 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 @@ -485,7 +485,7 @@ public final class Connection { // 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 = pointer, + if let pointer, let expandedSQL = sqlite3_expanded_sql(OpaquePointer(pointer)) { unsafeBitCast(context, to: Trace.self)(expandedSQL) sqlite3_free(expandedSQL) @@ -507,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 @@ -535,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 @@ -562,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 @@ -650,7 +650,7 @@ public final class Connection { 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 { + if let lhs, let rhs { return unsafeBitCast(callback, to: Collation.self)(lhs, rhs) } else { fatalError("sqlite3_create_collation_v2 callback called with NULL pointer") @@ -727,8 +727,17 @@ 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 } } diff --git a/Sources/SQLite/Core/Result.swift b/Sources/SQLite/Core/Result.swift index 3de4d25d..9a72e4c5 100644 --- a/Sources/SQLite/Core/Result.swift +++ b/Sources/SQLite/Core/Result.swift @@ -2,8 +2,8 @@ import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif @@ -21,11 +21,27 @@ public enum Result: Error { /// - statement: the statement which produced the error case error(message: String, code: Int32, statement: Statement?) + /// Represents a SQLite specific [extended error code] (https://sqlite.org/rescode.html#primary_result_codes_versus_extended_result_codes) + /// + /// - message: English-language text that describes the error + /// + /// - extendedCode: SQLite [extended error code](https://sqlite.org/rescode.html#extended_result_code_list) + /// + /// - statement: the statement which produced the error + case extendedError(message: String, extendedCode: 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) + + guard connection.usesExtendedErrorCodes else { + self = .error(message: message, code: errorCode, statement: statement) + return + } + + let extendedErrorCode = sqlite3_extended_errcode(connection.handle) + self = .extendedError(message: message, extendedCode: extendedErrorCode, statement: statement) } } @@ -35,11 +51,17 @@ extension Result: CustomStringConvertible { public var description: String { switch self { case let .error(message, errorCode, statement): - if let statement = statement { + if let statement { return "\(message) (\(statement)) (code: \(errorCode))" } else { return "\(message) (code: \(errorCode))" } + case let .extendedError(message, extendedCode, statement): + if let statement { + return "\(message) (\(statement)) (extended code: \(extendedCode))" + } else { + return "\(message) (extended code: \(extendedCode))" + } } } } diff --git a/Sources/SQLite/Core/SQLiteFeature.swift b/Sources/SQLite/Core/SQLiteFeature.swift new file mode 100644 index 00000000..50d910a3 --- /dev/null +++ b/Sources/SQLite/Core/SQLiteFeature.swift @@ -0,0 +1,25 @@ +import Foundation + +enum SQLiteFeature { + case partialIntegrityCheck // PRAGMA integrity_check(table) + case sqliteSchemaTable // sqlite_master => 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 1e2489b5..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 @@ -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)") } } @@ -185,7 +188,11 @@ public final class Statement { try connection.sync { try connection.check(sqlite3_step(handle)) == SQLITE_ROW } } - fileprivate func reset(clearBindings shouldClear: Bool = true) { + public func reset() { + reset(clearBindings: true) + } + + fileprivate func reset(clearBindings shouldClear: Bool) { sqlite3_reset(handle) if shouldClear { sqlite3_clear_bindings(handle) } } @@ -228,6 +235,21 @@ extension Statement: FailableIterator { } } +extension Statement { + func prepareRowIterator() -> RowIterator { + RowIterator(statement: self, columnNames: columnNameMap) + } + + var columnNameMap: [String: Int] { + var result = [String: Int]() + for (index, name) in self.columnNames.enumerated() { + result[name.quote()] = index + } + + return result + } +} + extension Statement: CustomStringConvertible { public var description: String { 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 9c463f0c..249a7728 100644 --- a/Sources/SQLite/Core/Value.swift +++ b/Sources/SQLite/Core/Value.swift @@ -39,7 +39,7 @@ public protocol Value: Expressible { // extensions cannot have inheritance claus static var declaredDatatype: String { get } - static func fromDatatypeValue(_ datatypeValue: Datatype) -> ValueType + static func fromDatatypeValue(_ datatypeValue: Datatype) throws -> ValueType var datatypeValue: Datatype { get } diff --git a/Sources/SQLite/Extensions/Cipher.swift b/Sources/SQLite/Extensions/Cipher.swift index b96c2761..03194ef1 100644 --- a/Sources/SQLite/Extensions/Cipher.swift +++ b/Sources/SQLite/Extensions/Cipher.swift @@ -6,6 +6,8 @@ import SQLCipher 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 } @@ -23,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) } @@ -39,6 +43,7 @@ extension Connection { /// 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 @@ -51,11 +56,13 @@ extension Connection { try _key_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count, migrate: true) } - /// Change the key on an open database. If the current database is not encrypted, this routine - /// will encrypt it. + /// 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) } @@ -64,6 +71,17 @@ 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, @@ -75,7 +93,7 @@ extension Connection { // per recommendation of SQLCipher authors let migrateResult = try scalar("PRAGMA cipher_migrate;") if (migrateResult as? String) != "0" { - // "0" is the result of successfull migration + // "0" is the result of successful migration throw Result.error(message: "Error in cipher migration, result \(migrateResult.debugDescription)", code: 1, statement: nil) } } diff --git a/Sources/SQLite/Extensions/FTS4.swift b/Sources/SQLite/Extensions/FTS4.swift index 0e48943a..c02cfdc1 100644 --- a/Sources/SQLite/Extensions/FTS4.swift +++ b/Sources/SQLite/Extensions/FTS4.swift @@ -22,10 +22,6 @@ // THE SOFTWARE. // -#if SWIFT_PACKAGE -import SQLiteObjc -#endif - extension Module { public static func FTS4(_ column: Expressible, _ more: Expressible...) -> Module { @@ -102,8 +98,8 @@ public struct Tokenizer { 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 { @@ -121,7 +117,7 @@ public struct Tokenizer { // https://sqlite.org/fts5.html#the_experimental_trigram_tokenizer public static func Trigram(caseSensitive: Bool = false) -> Tokenizer { - return Tokenizer("trigram", ["case_sensitive", caseSensitive ? "1" : "0"]) + Tokenizer("trigram", ["case_sensitive", caseSensitive ? "1" : "0"]) } public static func Custom(_ name: String) -> Tokenizer { @@ -149,29 +145,6 @@ extension Tokenizer: CustomStringConvertible { } -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.. 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) }) if isContentless { options.append("content", value: "") - } else if let externalContentSchema = externalContentSchema { + } else if let externalContentSchema { options.append("content", value: externalContentSchema.tableName()) } return options @@ -263,18 +236,12 @@ open class FTSConfig { } } - @discardableResult mutating func append(_ key: String, value: CustomStringConvertible?) -> Options { - append(key, value: value?.description) - } - - @discardableResult mutating func append(_ key: String, value: String?) -> Options { - append(key, value: value.map { Expression($0) }) + @discardableResult mutating func append(_ key: String, value: String) -> Options { + append(key, value: Expression(value)) } - @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 } } @@ -283,26 +250,16 @@ open class FTSConfig { /// Configuration for the [FTS4](https://www.sqlite.org/fts3.html) extension. open class FTS4Config: FTSConfig { /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) - public enum MatchInfo: CustomStringConvertible { + public enum MatchInfo: String { case fts3 - public var description: String { - "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? @@ -349,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 f108bbec..3e84e171 100644 --- a/Sources/SQLite/Extensions/FTS5.swift +++ b/Sources/SQLite/Extensions/FTS5.swift @@ -33,21 +33,13 @@ 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 { + 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? @@ -77,11 +69,15 @@ open class FTS5Config: FTSConfig { 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 } diff --git a/Sources/SQLite/Foundation.swift b/Sources/SQLite/Foundation.swift index 2acbc00e..44a31736 100644 --- a/Sources/SQLite/Foundation.swift +++ b/Sources/SQLite/Foundation.swift @@ -84,3 +84,19 @@ extension UUID: Value { } } + +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 d4c6828e..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 @@ -38,6 +38,7 @@ public func *(_: Expression?, _: Expression?) -> Expression 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 { @@ -103,7 +109,7 @@ extension String { } 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: @@ -115,9 +121,9 @@ func transcode(_ literal: Binding?) -> String { } } -// swiftlint:disable force_cast +// swiftlint:disable force_cast force_try func value(_ binding: Binding) -> A { - A.fromDatatypeValue(binding as! A.Datatype) as! A + try! A.fromDatatypeValue(binding as! A.Datatype) as! A } func value(_ binding: Binding?) -> A { 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 fe1e3dfe..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 "SQLiteObjc.h" 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 bf4fb8fc..17fc4a20 100644 --- a/Sources/SQLite/Typed/AggregateFunctions.swift +++ b/Sources/SQLite/Typed/AggregateFunctions.swift @@ -166,7 +166,7 @@ extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: N /// 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 { Function.avg.wrap(self) @@ -179,7 +179,7 @@ extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: N /// 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 { Function.sum.wrap(self) @@ -192,7 +192,7 @@ extension ExpressionType where UnderlyingType: Value, UnderlyingType.Datatype: N /// 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 { Function.total.wrap(self) diff --git a/Sources/SQLite/Typed/Coding.swift b/Sources/SQLite/Typed/Coding.swift index 3dc1e6cf..d0061851 100644 --- a/Sources/SQLite/Typed/Coding.swift +++ b/Sources/SQLite/Typed/Coding.swift @@ -86,12 +86,22 @@ extension QueryType { /// - Returns: An `INSERT` statement for the encodable objects public func insertMany(_ encodables: [Encodable], userInfo: [CodingUserInfoKey: Any] = [:], otherSetters: [Setter] = []) throws -> Insert { - let combinedSetters = try encodables.map { encodable -> [Setter] in - let encoder = SQLiteEncoder(userInfo: userInfo) + let combinedSettersWithoutNils = try encodables.map { encodable -> [Setter] in + let encoder = SQLiteEncoder(userInfo: userInfo, forcingNilValueSetters: false) try encodable.encode(to: encoder) return encoder.setters + otherSetters } - return self.insertMany(combinedSetters) + // 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 @@ -165,9 +175,11 @@ private class SQLiteEncoder: Encoder { 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 { @@ -202,18 +214,72 @@ private class SQLiteEncoder: Encoder { encoder.setters.append(Expression(key.stringValue) <- value) } + func encodeIfPresent(_ value: Int?, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: Bool?, forKey key: Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: Float?, forKey key: Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: Double?, forKey key: Key) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + + func encodeIfPresent(_ value: String?, forKey key: MyKey) throws { + if let value { + try encode(value, forKey: key) + } else if forcingNilValueSetters { + encoder.setters.append(Expression(key.stringValue) <- nil) + } + } + func encode(_ value: T, forKey key: Key) throws where T: Swift.Encodable { - if let data = value as? Data { + switch value { + case let data as Data: encoder.setters.append(Expression(key.stringValue) <- data) - } else if let date = value as? Date { + case let date as Date: encoder.setters.append(Expression(key.stringValue) <- date.datatypeValue) - } else { + case let uuid as UUID: + encoder.setters.append(Expression(key.stringValue) <- uuid.datatypeValue) + default: let encoded = try JSONEncoder().encode(value) let string = String(data: encoded, encoding: .utf8) encoder.setters.append(Expression(key.stringValue) <- string) } } + func encodeIfPresent(_ value: T?, forKey key: Key) throws where T: Swift.Encodable { + guard let value else { + guard forcingNilValueSetters else { + return + } + encoder.setters.append(Expression(key.stringValue) <- nil) + return + } + try encode(value, forKey: key) + } + func encode(_ value: Int8, forKey key: Key) throws { throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "encoding an Int8 is not supported")) @@ -259,7 +325,7 @@ private class SQLiteEncoder: Encoder { } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) - -> KeyedEncodingContainer where NestedKey: CodingKey { + -> KeyedEncodingContainer where NestedKey: CodingKey { fatalError("encoding a nested container is not supported") } @@ -271,9 +337,11 @@ private class SQLiteEncoder: Encoder { 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 { @@ -285,7 +353,7 @@ private class SQLiteEncoder: Encoder { } func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { - KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self)) + KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self, forcingNilValueSetters: forcingNilValueSetters)) } } @@ -379,27 +447,76 @@ private class SQLiteDecoder: Decoder { func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Swift.Decodable { // swiftlint:disable force_cast - if type == Data.self { + switch type { + case is Data.Type: let data = try row.get(Expression(key.stringValue)) return data as! T - } else if type == Date.self { + 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) } - // 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")) + } + + 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 { + -> KeyedDecodingContainer where NestedKey: CodingKey { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "decoding nested containers is not supported")) } diff --git a/Sources/SQLite/Typed/CoreFunctions.swift b/Sources/SQLite/Typed/CoreFunctions.swift index dc6a1044..c4359d8b 100644 --- a/Sources/SQLite/Typed/CoreFunctions.swift +++ b/Sources/SQLite/Typed/CoreFunctions.swift @@ -100,7 +100,7 @@ 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 { + guard let precision else { return Function.round.wrap([self]) } return Function.round.wrap([self, Int(precision)]) @@ -120,7 +120,7 @@ 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 { + guard let precision else { return Function.round.wrap(self) } return Function.round.wrap([self, Int(precision)]) @@ -224,7 +224,7 @@ extension ExpressionType where UnderlyingType == String { /// /// 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 { @@ -250,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)]) @@ -274,7 +274,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: Expression, escape character: Character? = nil) -> Expression { - guard let character = character else { + guard let character else { return Function.like.infix(self, pattern) } let like: Expression = Function.like.infix(self, pattern, wrap: false) @@ -349,7 +349,7 @@ 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 { + guard let characters else { return Function.ltrim.wrap(self) } return Function.ltrim.wrap([self, String(characters)]) @@ -367,7 +367,7 @@ 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 { + guard let characters else { return Function.rtrim.wrap(self) } return Function.rtrim.wrap([self, String(characters)]) @@ -385,7 +385,7 @@ 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 { + guard let characters else { return Function.trim.wrap([self]) } return Function.trim.wrap([self, String(characters)]) @@ -409,7 +409,7 @@ extension ExpressionType where UnderlyingType == String { } public func substring(_ location: Int, length: Int? = nil) -> Expression { - guard let length = length else { + guard let length else { return Function.substr.wrap([self, location]) } return Function.substr.wrap([self, location, length]) @@ -475,7 +475,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 Function.like.infix(self, pattern) } return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) @@ -499,7 +499,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: Expression, escape character: Character? = nil) -> Expression { - guard let character = character else { + guard let character else { return Function.like.infix(self, pattern) } let like: Expression = Function.like.infix(self, pattern, wrap: false) @@ -574,7 +574,7 @@ 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 { + guard let characters else { return Function.ltrim.wrap(self) } return Function.ltrim.wrap([self, String(characters)]) @@ -592,7 +592,7 @@ 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 { + guard let characters else { return Function.rtrim.wrap(self) } return Function.rtrim.wrap([self, String(characters)]) @@ -610,7 +610,7 @@ 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 { + guard let characters else { return Function.trim.wrap(self) } return Function.trim.wrap([self, String(characters)]) @@ -649,7 +649,7 @@ 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 { + guard let length else { return Function.substr.wrap([self, location]) } return Function.substr.wrap([self, location, length]) @@ -726,7 +726,7 @@ 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 { + guard let character else { return Function.like.infix(self, pattern) } let like: Expression = Function.like.infix(self, pattern, wrap: false) diff --git a/Sources/SQLite/Typed/Expression.swift b/Sources/SQLite/Typed/Expression.swift index 95cdd3b9..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 @@ -47,6 +47,9 @@ extension ExpressionType { self.init(expression.template, expression.bindings) } + public var description: String { + asSQL() + } } /// An `Expression` represents a raw SQL fragment and any associated bindings. @@ -73,23 +76,19 @@ public protocol Expressible { extension Expressible { // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE - // FIXME: make internal (0.13.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 { diff --git a/Sources/SQLite/Typed/Operators.swift b/Sources/SQLite/Typed/Operators.swift index 5ffbbceb..1c611cbc 100644 --- a/Sources/SQLite/Typed/Operators.swift +++ b/Sources/SQLite/Typed/Operators.swift @@ -367,14 +367,14 @@ public func ==(lhs: Expression, rhs: V) -> Expression where V 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)) } + guard let rhs else { return "IS".infix(lhs, Expression(value: nil)) } return Operator.eq.infix(lhs, rhs) } public func ==(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { Operator.eq.infix(lhs, rhs) } public func ==(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { - guard let lhs = lhs else { return "IS".infix(Expression(value: nil), rhs) } + guard let lhs else { return "IS".infix(Expression(value: nil), rhs) } return Operator.eq.infix(lhs, rhs) } @@ -394,14 +394,14 @@ public func ===(lhs: Expression, rhs: V) -> Expression where "IS".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)) } + guard let rhs else { return "IS".infix(lhs, Expression(value: nil)) } return "IS".infix(lhs, rhs) } public func ===(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { "IS".infix(lhs, rhs) } public func ===(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { - guard let lhs = lhs else { return "IS".infix(Expression(value: nil), rhs) } + guard let lhs else { return "IS".infix(Expression(value: nil), rhs) } return "IS".infix(lhs, rhs) } @@ -421,14 +421,14 @@ public func !=(lhs: Expression, rhs: V) -> Expression where V Operator.neq.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)) } + guard let rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } return Operator.neq.infix(lhs, rhs) } public func !=(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { Operator.neq.infix(lhs, rhs) } public func !=(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { - guard let lhs = lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } + guard let lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } return Operator.neq.infix(lhs, rhs) } @@ -448,14 +448,14 @@ public func !==(lhs: Expression, rhs: V) -> Expression where "IS NOT".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)) } + guard let rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } return "IS NOT".infix(lhs, rhs) } public func !==(lhs: V, rhs: Expression) -> Expression where V.Datatype: Equatable { "IS NOT".infix(lhs, rhs) } public func !==(lhs: V?, rhs: Expression) -> Expression where V.Datatype: Equatable { - guard let lhs = lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } + guard let lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } return "IS NOT".infix(lhs, 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 2d88db63..6162fcc7 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -193,12 +193,14 @@ 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 } @@ -596,9 +598,9 @@ extension QueryType { 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 ]) }) @@ -856,6 +858,7 @@ extension QueryType { public var expression: Expression { let clauses: [Expressible?] = [ + withClause, selectClause, joinClause, whereClause, @@ -988,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 { @@ -1009,6 +1021,14 @@ 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 { @@ -1016,10 +1036,29 @@ extension Connection { let column = names.removeLast() let namespace = names.joined(separator: ".") + // Return a copy of the input "with" clause stripping all subclauses besides "select", "join", and "with". + func strip(_ with: WithClauses) -> WithClauses { + var stripped = WithClauses() + stripped.recursive = with.recursive + for subclause in with.clauses { + let query = subclause.query + var strippedQuery = type(of: query).init(query.clauses.from.name, database: query.clauses.from.database) + strippedQuery.clauses.select = query.clauses.select + strippedQuery.clauses.join = query.clauses.join + strippedQuery.clauses.with = strip(query.clauses.with) + + var strippedSubclause = WithClauses.Clause(alias: subclause.alias, query: strippedQuery) + strippedSubclause.columns = subclause.columns + stripped.clauses.append(strippedSubclause) + } + return stripped + } + func expandGlob(_ namespace: Bool) -> (QueryType) throws -> Void { - { (queryType: QueryType) throws -> Void in + { (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)" } } @@ -1032,11 +1071,9 @@ extension Connection { 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 - } + for q in queries where q.tableName().expression.template == namespace { + try expandGlob(true)(q) + continue column } throw QueryError.noSuchTable(name: namespace) } @@ -1060,7 +1097,7 @@ extension Connection { 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 { @@ -1068,10 +1105,10 @@ extension Connection { 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? { @@ -1163,25 +1200,34 @@ 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 { @@ -1242,7 +1288,9 @@ public struct QueryClauses { 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?) { from = (name, alias, database) diff --git a/Sources/SQLite/Typed/Schema.swift b/Sources/SQLite/Typed/Schema.swift index 726f3e27..919042ab 100644 --- a/Sources/SQLite/Typed/Schema.swift +++ b/Sources/SQLite/Typed/Schema.swift @@ -136,7 +136,7 @@ extension Table { // 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"), @@ -147,12 +147,20 @@ extension Table { 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 { + public func dropIndex(_ columns: [Expressible], ifExists: Bool = false) -> String { drop("INDEX", indexName(columns), ifExists) } + public func dropIndex(_ columns: Expressible..., ifExists: Bool = false) -> String { + dropIndex(Array(columns), ifExists: ifExists) + } + fileprivate func indexName(_ columns: [Expressible]) -> Expressible { let string = (["index", clauses.from.name, "on"] + columns.map { $0.expression.template }).joined(separator: " ").lowercased() @@ -309,6 +317,26 @@ public final class TableBuilder { column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) } + public func column(_ name: Expression, primaryKey: Bool, check: Expression? = nil, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression? = nil, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, true, false, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression, + references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey ? .default : nil, true, false, check, nil, (table, other), nil) + } + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) diff --git a/Sources/SQLite/Typed/Setter.swift b/Sources/SQLite/Typed/Setter.swift index 7910cab8..8dc8a0e0 100644 --- a/Sources/SQLite/Typed/Setter.swift +++ b/Sources/SQLite/Typed/Setter.swift @@ -75,6 +75,12 @@ extension Setter: Expressible { } +extension Setter: CustomStringConvertible { + public var description: String { + asSQL() + } +} + public func <-(column: Expression, value: Expression) -> Setter { Setter(column: column, value: value) } 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/SQLiteObjc.m b/Sources/SQLiteObjc/SQLiteObjc.m deleted file mode 100644 index 9f2be0bd..00000000 --- a/Sources/SQLiteObjc/SQLiteObjc.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 "SQLiteObjc.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/SQLiteObjc.h b/Sources/SQLiteObjc/include/SQLiteObjc.h deleted file mode 100644 index 610cdf10..00000000 --- a/Sources/SQLiteObjc/include/SQLiteObjc.h +++ /dev/null @@ -1,38 +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; -#if defined(SQLITE_SWIFT_STANDALONE) -@import sqlite3; -#elif defined(SQLITE_SWIFT_SQLCIPHER) -@import SQLCipher; -#else -@import SQLite3; -#endif - -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 index 3537085c..81d6c314 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -6,8 +6,8 @@ disabled_rules: - identifier_name type_body_length: - warning: 800 - error: 800 + warning: 1000 + error: 1000 function_body_length: warning: 200 diff --git a/Tests/Carthage/Makefile b/Tests/Carthage/Makefile index b44d85b9..fe708ab0 100644 --- a/Tests/Carthage/Makefile +++ b/Tests/Carthage/Makefile @@ -1,4 +1,4 @@ -CARTHAGE := /usr/local/bin/carthage +CARTHAGE := $(shell which carthage) CARTHAGE_PLATFORM := iOS CARTHAGE_CONFIGURATION := Release CARTHAGE_DIR := Carthage diff --git a/Tests/SPM/Package.swift b/Tests/SPM/Package.swift index dd1cd45c..a5a4afc4 100644 --- a/Tests/SPM/Package.swift +++ b/Tests/SPM/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,21 +6,18 @@ import PackageDescription let package = Package( name: "test", platforms: [ - .iOS(.v9), - .macOS(.v10_10), - .watchOS(.v3), - .tvOS(.v9) + .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.13.0") + // .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.4") ], targets: [ - .target( - name: "test", - dependencies: [.product(name: "SQLite", package: "SQLite.swift")] - ) + .executableTarget(name: "test", dependencies: [.product(name: "SQLite", package: "SQLite.swift")]) ] ) 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/BlobTests.swift b/Tests/SQLiteTests/BlobTests.swift deleted file mode 100644 index 87eb5709..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(repeating: 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 0995fca0..00000000 --- a/Tests/SQLiteTests/CipherTests.swift +++ /dev/null @@ -1,109 +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 setUpWithError() throws { - // 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')") - - try super.setUpWithError() - } - - 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 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) - } - - 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/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/ConnectionTests.swift b/Tests/SQLiteTests/Core/ConnectionTests.swift similarity index 54% rename from Tests/SQLiteTests/ConnectionTests.swift rename to Tests/SQLiteTests/Core/ConnectionTests.swift index 136271a4..abdb3e1b 100644 --- a/Tests/SQLiteTests/ConnectionTests.swift +++ b/Tests/SQLiteTests/Core/ConnectionTests.swift @@ -7,8 +7,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 @@ -20,39 +20,54 @@ class ConnectionTests: SQLiteTestCase { try createUsersTable() } - func test_init_withInMemory_returnsInMemoryConnection() { - let db = try! Connection(.inMemory) + func test_init_withInMemory_returnsInMemoryConnection() throws { + let db = try Connection(.inMemory) XCTAssertEqual("", db.description) } - func test_init_returnsInMemoryByDefault() { - let db = try! Connection() + func test_init_returnsInMemoryByDefault() throws { + let db = try Connection() XCTAssertEqual("", db.description) } - func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! Connection(.temporary) + func test_init_withTemporary_returnsTemporaryConnection() throws { + let db = try Connection(.temporary) XCTAssertEqual("", db.description) } - func test_init_withURI_returnsURIConnection() { - let db = try! Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + 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() { - let db = try! Connection("\(NSTemporaryDirectory())/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() { - let db = try! Connection(readonly: true) + func test_readonly_returnsTrueOnReadOnlyConnections() throws { + let db = try Connection(readonly: true) XCTAssertTrue(db.readonly) } @@ -60,14 +75,14 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(0, db.changes) } - func test_lastInsertRowid_returnsLastIdAfterInserts() { - try! insertUser("alice") + func test_lastInsertRowid_returnsLastIdAfterInserts() throws { + try insertUser("alice") XCTAssertEqual(1, db.lastInsertRowid) } - func test_lastInsertRowid_doesNotResetAfterError() { + func test_lastInsertRowid_doesNotResetAfterError() throws { XCTAssert(db.lastInsertRowid == 0) - try! insertUser("alice") + try insertUser("alice") XCTAssertEqual(1, db.lastInsertRowid) XCTAssertThrowsError( try db.run("INSERT INTO \"users\" (email, age, admin) values ('invalid@example.com', 12, 'invalid')") @@ -81,73 +96,72 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, db.lastInsertRowid) } - func test_changes_returnsNumberOfChanges() { - try! insertUser("alice") + func test_changes_returnsNumberOfChanges() throws { + try insertUser("alice") XCTAssertEqual(1, db.changes) - try! insertUser("betsy") + try insertUser("betsy") XCTAssertEqual(1, db.changes) } - func test_totalChanges_returnsTotalNumberOfChanges() { + func test_totalChanges_returnsTotalNumberOfChanges() throws { XCTAssertEqual(0, db.totalChanges) - try! insertUser("alice") + try insertUser("alice") XCTAssertEqual(1, db.totalChanges) - try! insertUser("betsy") + try insertUser("betsy") XCTAssertEqual(2, db.totalChanges) } - func test_userVersion() { - db.userVersion = 2 - XCTAssertEqual(2, db.userVersion!) + func test_useExtendedErrorCodes_returnsFalseDefault() throws { + XCTAssertFalse(db.usesExtendedErrorCodes) } - 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_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() { - 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]) + 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() { - try! db.vacuum() + func test_vacuum() throws { + try db.vacuum() } - 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) + 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() { - try! db.run("-- this is a comment\nSELECT 1") + 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() { - try! db.transaction(.deferred) {} + func test_transaction_executesBeginDeferred() throws { + try db.transaction(.deferred) {} assertSQL("BEGIN DEFERRED TRANSACTION") } - func test_transaction_executesBeginImmediate() { - try! db.transaction(.immediate) {} + func test_transaction_executesBeginImmediate() throws { + try db.transaction(.immediate) {} assertSQL("BEGIN IMMEDIATE TRANSACTION") } - func test_transaction_executesBeginExclusive() { - try! db.transaction(.exclusive) {} + func test_transaction_executesBeginExclusive() throws { + try db.transaction(.exclusive) {} assertSQL("BEGIN EXCLUSIVE TRANSACTION") } @@ -164,10 +178,10 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(users.map { $0[0] as? String }, ["alice@example.com", "betsy@example.com"]) } - func test_transaction_beginsAndCommitsTransactions() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + func test_transaction_beginsAndCommitsTransactions() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - try! db.transaction { + try db.transaction { try stmt.run() } @@ -177,8 +191,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("ROLLBACK TRANSACTION", 0) } - func test_transaction_rollsBackTransactionsIfCommitsFail() { - let sqliteVersion = String(describing: try! db.scalar("SELECT sqlite_version()")!) + 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 { @@ -187,9 +201,9 @@ class ConnectionTests: SQLiteTestCase { } // 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) + 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 { @@ -209,14 +223,14 @@ class ConnectionTests: SQLiteTestCase { // 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 { + 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") + func test_transaction_beginsAndRollsTransactionsBack() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { try db.transaction { @@ -232,8 +246,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("COMMIT TRANSACTION", 0) } - func test_savepoint_beginsAndCommitsSavepoints() { - try! db.savepoint("1") { + func test_savepoint_beginsAndCommitsSavepoints() throws { + try db.savepoint("1") { try db.savepoint("2") { try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") } @@ -248,8 +262,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("ROLLBACK TO SAVEPOINT '1'", 0) } - func test_savepoint_beginsAndRollsSavepointsBack() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + func test_savepoint_beginsAndRollsSavepointsBack() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { try db.savepoint("1") { @@ -276,8 +290,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("RELEASE SAVEPOINT '1'", 0) } - func test_updateHook_setsUpdateHook_withInsert() { - async { done in + 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) @@ -285,13 +299,13 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, rowid) done() } - try! insertUser("alice") + try insertUser("alice") } } - func test_updateHook_setsUpdateHook_withUpdate() { - try! insertUser("alice") - async { done in + 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) @@ -299,13 +313,13 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, rowid) done() } - try! db.run("UPDATE users SET email = 'alice@example.com'") + try db.run("UPDATE users SET email = 'alice@example.com'") } } - func test_updateHook_setsUpdateHook_withDelete() { - try! insertUser("alice") - async { done in + 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) @@ -313,24 +327,24 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, rowid) done() } - try! db.run("DELETE FROM users WHERE id = 1") + try db.run("DELETE FROM users WHERE id = 1") } } - func test_commitHook_setsCommitHook() { - async { done in + func test_commitHook_setsCommitHook() throws { + try async { done in db.commitHook { done() } - try! db.transaction { + try db.transaction { try insertUser("alice") } - XCTAssertEqual(1, try! db.scalar("SELECT count(*) FROM users") as? Int64) + XCTAssertEqual(1, try db.scalar("SELECT count(*) FROM users") as? Int64) } } - func test_rollbackHook_setsRollbackHook() { - async { done in + func test_rollbackHook_setsRollbackHook() throws { + try async { done in db.rollbackHook(done) do { try db.transaction { @@ -339,12 +353,12 @@ class ConnectionTests: SQLiteTestCase { } } catch { } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users") as? Int64) } } - func test_commitHook_withRollback_rollsBack() { - async { done in + func test_commitHook_withRollback_rollsBack() throws { + try async { done in db.commitHook { throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) } @@ -355,41 +369,41 @@ class ConnectionTests: SQLiteTestCase { } } catch { } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users") as? Int64) } } // https://github.com/stephencelis/SQLite.swift/issues/1071 - #if !os(Linux) - func test_createFunction_withArrayArguments() { + #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) + XCTAssertEqual("Hello, world!", try db.scalar("SELECT hello('world')") as? String) + XCTAssert(try db.scalar("SELECT hello(NULL)") == nil) } - func test_createFunction_createsQuotableFunction() { + 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) + 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 + 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) + XCTAssertEqual(1, try db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) } - func test_createCollation_createsQuotableCollation() { - try! db.createCollation("NO DIACRITIC") { lhs, rhs in + 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) + XCTAssertEqual(1, try db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) } - func test_interrupt_interruptsLongRunningQuery() { + func XXX_test_interrupt_interruptsLongRunningQuery() throws { let semaphore = DispatchSemaphore(value: 0) db.createFunction("sleep") { _ in DispatchQueue.global(qos: .background).async { @@ -399,7 +413,7 @@ class ConnectionTests: SQLiteTestCase { semaphore.wait() return nil } - let stmt = try! db.prepare("SELECT sleep()") + let stmt = try db.prepare("SELECT sleep()") XCTAssertThrowsError(try stmt.run()) { error in if case Result.error(_, let code, _) = error { XCTAssertEqual(code, SQLITE_INTERRUPT) @@ -410,13 +424,13 @@ class ConnectionTests: SQLiteTestCase { } #endif - func test_concurrent_access_single_connection() { + 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 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 @@ -431,42 +445,3 @@ class ConnectionTests: SQLiteTestCase { semaphores.forEach { $0.wait() } } } - -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(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() { - 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/CoreFunctionsTests.swift b/Tests/SQLiteTests/Core/CoreFunctionsTests.swift similarity index 92% rename from Tests/SQLiteTests/CoreFunctionsTests.swift rename to Tests/SQLiteTests/Core/CoreFunctionsTests.swift index e03e3769..b866c4e9 100644 --- a/Tests/SQLiteTests/CoreFunctionsTests.swift +++ b/Tests/SQLiteTests/Core/CoreFunctionsTests.swift @@ -12,8 +12,8 @@ class CoreFunctionsTests: XCTestCase { } func test_random_generatesExpressionWithRandomFunction() { - assertSQL("random()", Expression.random()) - assertSQL("random()", Expression.random()) + assertSQL("random()", SQLite.Expression.random()) + assertSQL("random()", SQLite.Expression.random()) } func test_length_wrapsStringExpressionWithLengthFunction() { @@ -38,14 +38,14 @@ class CoreFunctionsTests: XCTestCase { 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\")", string.like(SQLite.Expression("a"))) + assertSQL("(\"stringOptional\" LIKE \"a\")", stringOptional.like(SQLite.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\" ESCAPE '\\')", string.like(SQLite.Expression("a"), escape: "\\")) + assertSQL("(\"stringOptional\" LIKE \"a\" ESCAPE '\\')", stringOptional.like(SQLite.Expression("a"), escape: "\\")) - assertSQL("('string' LIKE \"a\")", "string".like(Expression("a"))) - assertSQL("('string' LIKE \"a\" ESCAPE '\\')", "string".like(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() { 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 100% rename from Tests/SQLiteTests/ValueTests.swift rename to Tests/SQLiteTests/Core/ValueTests.swift diff --git a/Tests/SQLiteTests/CustomFunctionsTests.swift b/Tests/SQLiteTests/CustomFunctionsTests.swift deleted file mode 100644 index 3d897f8e..00000000 --- a/Tests/SQLiteTests/CustomFunctionsTests.swift +++ /dev/null @@ -1,151 +0,0 @@ -import XCTest -import SQLite - -// https://github.com/stephencelis/SQLite.swift/issues/1071 -#if !os(Linux) - -class CustomFunctionNoArgsTests: SQLiteTestCase { - typealias FunctionNoOptional = () -> Expression - typealias FunctionResultOptional = () -> Expression - - func testFunctionNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { - "a" - } - let result = try! db.prepare("SELECT test()").scalar() as! String - XCTAssertEqual("a", result) - } - - func testFunctionResultOptional() { - 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 = (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 - "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 - "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 - "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 - "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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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() { - _ = 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/ExpressionTests.swift b/Tests/SQLiteTests/ExpressionTests.swift deleted file mode 100644 index 32100d4d..00000000 --- a/Tests/SQLiteTests/ExpressionTests.swift +++ /dev/null @@ -1,5 +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 76% rename from Tests/SQLiteTests/FTS4Tests.swift rename to Tests/SQLiteTests/Extensions/FTS4Tests.swift index 33be422c..f7258fb5 100644 --- a/Tests/SQLiteTests/FTS4Tests.swift +++ b/Tests/SQLiteTests/Extensions/FTS4Tests.swift @@ -21,12 +21,12 @@ 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\" + 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, @@ -35,9 +35,9 @@ class FTS4Tests: XCTestCase { } 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() { @@ -116,7 +116,7 @@ class FTS4ConfigTests: XCTestCase { func test_tokenizer_unicode61_with_options() { XCTAssertEqual( """ - CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" + 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"])))) @@ -188,40 +188,3 @@ class FTS4ConfigTests: XCTestCase { 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\") - """.replacingOccurrences(of: "\n", with: "")) - - 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 98% rename from Tests/SQLiteTests/FTS5Tests.swift rename to Tests/SQLiteTests/Extensions/FTS5Tests.swift index c271be9d..199ec415 100644 --- a/Tests/SQLiteTests/FTS5Tests.swift +++ b/Tests/SQLiteTests/Extensions/FTS5Tests.swift @@ -77,7 +77,7 @@ 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"])))) } diff --git a/Tests/SQLiteTests/FTSIntegrationTests.swift b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift similarity index 94% rename from Tests/SQLiteTests/FTSIntegrationTests.swift rename to Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift index da9ba8dc..b3b9e617 100644 --- a/Tests/SQLiteTests/FTSIntegrationTests.swift +++ b/Tests/SQLiteTests/Extensions/FTSIntegrationTests.swift @@ -3,15 +3,15 @@ import XCTest import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif @testable import SQLite class FTSIntegrationTests: SQLiteTestCase { - let email = Expression("email") + let email = SQLite.Expression("email") let index = VirtualTable("index") private func createIndex() throws { diff --git a/Tests/SQLiteTests/RTreeTests.swift b/Tests/SQLiteTests/Extensions/RTreeTests.swift similarity index 100% rename from Tests/SQLiteTests/RTreeTests.swift rename to Tests/SQLiteTests/Extensions/RTreeTests.swift 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 cef485fc..453febcd 100644 --- a/Tests/SQLiteTests/FoundationTests.swift +++ b/Tests/SQLiteTests/FoundationTests.swift @@ -25,4 +25,16 @@ class FoundationTests: XCTestCase { 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/QueryIntegrationTests.swift b/Tests/SQLiteTests/QueryIntegrationTests.swift deleted file mode 100644 index 4b9c4bbf..00000000 --- a/Tests/SQLiteTests/QueryIntegrationTests.swift +++ /dev/null @@ -1,220 +0,0 @@ -import XCTest -#if SQLITE_SWIFT_STANDALONE -import sqlite3 -#elseif SQLITE_SWIFT_SQLCIPHER -import SQLCipher -#elseif os(Linux) -import CSQLite -#else -import SQLite3 -#endif -@testable import SQLite - -class QueryIntegrationTests: SQLiteTestCase { - - let id = Expression("id") - let email = Expression("email") - let age = Expression("age") - - override func setUpWithError() throws { - try super.setUpWithError() - try 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("date")) - builder.column(Expression("optional")) - builder.column(Expression("sub")) - }) - - let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, - date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) - let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, - date: Date(timeIntervalSince1970: 5000), 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].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() { - 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_insert_many() { - let id = try! db.run(users.insertMany([[email <- "alice@example.com"], [email <- "geoff@example.com"]])) - XCTAssertEqual(2, id) - } - - 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() { - 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)") - } - } - - // 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(Expression.random()).limit(1))) - XCTAssertEqual(1, result.count) - } -} - -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/fixtures/encrypted-3.x.sqlite b/Tests/SQLiteTests/Resources/encrypted-3.x.sqlite similarity index 100% rename from Tests/SQLiteTests/fixtures/encrypted-3.x.sqlite rename to Tests/SQLiteTests/Resources/encrypted-3.x.sqlite diff --git a/Tests/SQLiteTests/fixtures/encrypted-4.x.sqlite b/Tests/SQLiteTests/Resources/encrypted-4.x.sqlite similarity index 100% rename from Tests/SQLiteTests/fixtures/encrypted-4.x.sqlite rename to Tests/SQLiteTests/Resources/encrypted-4.x.sqlite 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 96% rename from Tests/SQLiteTests/SchemaTests.swift rename to Tests/SQLiteTests/Schema/SchemaTests.swift index 495a5e51..4f4a49d1 100644 --- a/Tests/SQLiteTests/SchemaTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaTests.swift @@ -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) } @@ -329,6 +334,10 @@ class SchemaTests: XCTestCase { "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", table.create { t in t.column(int64, unique: true, check: int64 > 0, references: table, int64) } ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0, references: table, int64) } + ) XCTAssertEqual( """ CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES @@ -336,6 +345,13 @@ class SchemaTests: XCTestCase { """.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\"))", @@ -345,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) } @@ -357,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)", diff --git a/Tests/SQLiteTests/StatementTests.swift b/Tests/SQLiteTests/StatementTests.swift deleted file mode 100644 index b4ad7c28..00000000 --- a/Tests/SQLiteTests/StatementTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -import XCTest -import SQLite - -class StatementTests: SQLiteTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - try 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 4d8f78fa..e62b2a23 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -12,7 +12,7 @@ class SQLiteTestCase: XCTestCase { trace = [String: Int]() db.trace { SQL in - print(SQL) + // print("SQL: \(SQL)") self.trace[SQL, default: 0] += 1 } } @@ -54,8 +54,8 @@ class SQLiteTestCase: XCTestCase { ) } - func assertSQL(_ SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { - try! statement.run() + 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 } } @@ -66,40 +66,55 @@ 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 = Expression("blob") -let dataOptional = Expression("blobOptional") +let data = SQLite.Expression("blob") +let dataOptional = SQLite.Expression("blobOptional") -let date = Expression("date") -let dateOptional = Expression("dateOptional") +let date = SQLite.Expression("date") +let dateOptional = SQLite.Expression("dateOptional") -let double = Expression("double") -let doubleOptional = Expression("doubleOptional") +let double = SQLite.Expression("double") +let doubleOptional = SQLite.Expression("doubleOptional") -let int = Expression("int") -let intOptional = Expression("intOptional") +let int = SQLite.Expression("int") +let intOptional = SQLite.Expression("intOptional") -let int64 = Expression("int64") -let int64Optional = Expression("int64Optional") +let int64 = SQLite.Expression("int64") +let int64Optional = SQLite.Expression("int64Optional") -let string = Expression("string") -let stringOptional = Expression("stringOptional") +let string = SQLite.Expression("string") +let stringOptional = SQLite.Expression("stringOptional") + +let uuid = SQLite.Expression("uuid") +let uuidOptional = SQLite.Expression("uuidOptional") + +let testUUIDValue = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")! func assertSQL(_ expression1: @autoclosure () -> String, _ expression2: @autoclosure () -> Expressible, file: StaticString = #file, line: UInt = #line) { XCTAssertEqual(expression1(), expression2().asSQL(), file: file, line: line) } +func extractAndReplace(_ value: String, regex: String, with replacement: String) -> (String, String) { + // We cannot use `Regex` because it is not available before iOS 16 :( + let regex = try! NSRegularExpression(pattern: regex) + let valueRange = NSRange(location: 0, length: value.utf16.count) + let match = regex.firstMatch(in: value, options: [], range: valueRange)!.range + let range = Range(match, in: value)! + let extractedValue = String(value[range]) + return (value.replacingCharacters(in: range, with: replacement), extractedValue) +} + let table = Table("table") let qualifiedTable = Table("table", database: "main") let virtualTable = VirtualTable("virtual_table") @@ -112,16 +127,18 @@ class TestCodable: Codable, Equatable { let float: Float let double: Double let date: Date + let uuid: UUID let optional: String? let sub: TestCodable? - init(int: Int, string: String, bool: Bool, float: Float, double: Double, date: Date, 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 } @@ -133,7 +150,18 @@ class TestCodable: Codable, Equatable { 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/AggregateFunctionsTests.swift b/Tests/SQLiteTests/Typed/AggregateFunctionsTests.swift similarity index 100% rename from Tests/SQLiteTests/AggregateFunctionsTests.swift rename to Tests/SQLiteTests/Typed/AggregateFunctionsTests.swift diff --git a/Tests/SQLiteTests/CustomAggregationTests.swift b/Tests/SQLiteTests/Typed/CustomAggregationTests.swift similarity index 86% rename from Tests/SQLiteTests/CustomAggregationTests.swift rename to Tests/SQLiteTests/Typed/CustomAggregationTests.swift index 5cbfbbef..b052d236 100644 --- a/Tests/SQLiteTests/CustomAggregationTests.swift +++ b/Tests/SQLiteTests/Typed/CustomAggregationTests.swift @@ -7,14 +7,14 @@ import Dispatch import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif // https://github.com/stephencelis/SQLite.swift/issues/1071 -#if !os(Linux) +#if !(os(Linux) || os(Android)) class CustomAggregationTests: SQLiteTestCase { override func setUpWithError() throws { @@ -25,7 +25,7 @@ class CustomAggregationTests: SQLiteTestCase { try insertUser("Eve", age: 28, admin: false) } - func testUnsafeCustomSum() { + func testUnsafeCustomSum() throws { let step = { (bindings: [Binding?], state: UnsafeMutablePointer) in if let v = bindings[0] as? Int64 { state.pointee += v @@ -43,7 +43,7 @@ class CustomAggregationTests: SQLiteTestCase { v[0] = 0 return v.baseAddress! } - let result = try! db.prepare("SELECT mySUM1(age) AS s FROM users") + 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 @@ -51,7 +51,7 @@ class CustomAggregationTests: SQLiteTestCase { } } - func testUnsafeCustomSumGrouping() { + func testUnsafeCustomSumGrouping() throws { let step = { (bindings: [Binding?], state: UnsafeMutablePointer) in if let v = bindings[0] as? Int64 { state.pointee += v @@ -68,19 +68,19 @@ class CustomAggregationTests: SQLiteTestCase { 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 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() { + 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 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 @@ -88,26 +88,26 @@ class CustomAggregationTests: SQLiteTestCase { } } - func testCustomSumGrouping() { + 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 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() { + 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 result = try db.prepare("SELECT myReduceSUM3(email) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { @@ -116,7 +116,7 @@ class CustomAggregationTests: SQLiteTestCase { } } - func testCustomObjectSum() { + func testCustomObjectSum() throws { { let initial = TestObject(value: 1000) let reduce: (TestObject, [Binding?]) -> TestObject = { (last, bindings) in 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/DateAndTimeFunctionTests.swift b/Tests/SQLiteTests/Typed/DateAndTimeFunctionTests.swift similarity index 100% rename from Tests/SQLiteTests/DateAndTimeFunctionTests.swift rename to Tests/SQLiteTests/Typed/DateAndTimeFunctionTests.swift 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/OperatorsTests.swift b/Tests/SQLiteTests/Typed/OperatorsTests.swift similarity index 99% rename from Tests/SQLiteTests/OperatorsTests.swift rename to Tests/SQLiteTests/Typed/OperatorsTests.swift index 370b910b..7a5d83da 100644 --- a/Tests/SQLiteTests/OperatorsTests.swift +++ b/Tests/SQLiteTests/Typed/OperatorsTests.swift @@ -356,7 +356,7 @@ class OperatorsTests: XCTestCase { } func test_precedencePreserved() { - let n = Expression(value: 1) + 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)) } diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift new file mode 100644 index 00000000..cde5a0c3 --- /dev/null +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -0,0 +1,457 @@ +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 QueryIntegrationTests: SQLiteTestCase { + + let id = SQLite.Expression("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/QueryTests.swift b/Tests/SQLiteTests/Typed/QueryTests.swift similarity index 75% rename from Tests/SQLiteTests/QueryTests.swift rename to Tests/SQLiteTests/Typed/QueryTests.swift index f4942b88..257b5245 100644 --- a/Tests/SQLiteTests/QueryTests.swift +++ b/Tests/SQLiteTests/Typed/QueryTests.swift @@ -3,8 +3,8 @@ import XCTest import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif os(Linux) -import CSQLite +#elseif canImport(SwiftToolchainCSQLite) +import SwiftToolchainCSQLite #else import SQLite3 #endif @@ -13,19 +13,19 @@ import SQLite3 class QueryTests: XCTestCase { let users = Table("users") - let id = Expression("id") - let email = Expression("email") - let age = Expression("age") - let admin = Expression("admin") - let optionalAdmin = Expression("admin") + 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 = Expression("user_id") - let categoryId = Expression("category_id") - let published = Expression("published") + let userId = SQLite.Expression("user_id") + let categoryId = SQLite.Expression("category_id") + let published = SQLite.Expression("published") let categories = Table("categories") - let tag = Expression("tag") + let tag = SQLite.Expression("tag") func test_select_withExpression_compilesSelectClause() { assertSQL("SELECT \"email\" FROM \"users\"", users.select(email)) @@ -59,6 +59,14 @@ class QueryTests: XCTestCase { 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\")", @@ -209,7 +217,7 @@ class QueryTests: XCTestCase { } func test_alias_aliasesTable() { - let managerId = Expression("manager_id") + let managerId = SQLite.Expression("manager_id") let managers = users.alias("managers") @@ -220,6 +228,62 @@ class QueryTests: XCTestCase { ) } + 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)", @@ -279,12 +343,12 @@ class QueryTests: XCTestCase { 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), optional: nil, sub: nil) + 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\") - VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000') + 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 ) @@ -294,22 +358,58 @@ class QueryTests: XCTestCase { 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), optional: nil, sub: nil) + 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), optional: "optional", sub: value1) + 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)! - assertSQL( + + let expectedSQL = """ - INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"optional\", - \"sub\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'optional', '\(encodedJSONString)') - """.replacingOccurrences(of: "\n", with: ""), - insert + 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( """ @@ -322,35 +422,37 @@ class QueryTests: XCTestCase { func test_upsert_encodable() throws { let emails = Table("emails") - let string = Expression("string") + let string = SQLite.Expression("string") let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, - date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + 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\") - VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000') ON CONFLICT (\"string\") + 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\" + \"float\" = \"excluded\".\"float\", \"double\" = \"excluded\".\"double\", \"date\" = \"excluded\".\"date\", + \"uuid\" = \"excluded\".\"uuid\" """.replacingOccurrences(of: "\n", with: ""), insert ) } - func test_insert_many_encodable() throws { + 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), optional: nil, sub: nil) + 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), optional: nil, sub: nil) + 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), optional: nil, sub: nil) + 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\") - VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000'), (2, '3', 1, 3.0, 5.0, '1970-01-01T00:00:00.000'), - (3, '4', 1, 3.0, 6.0, '1970-01-01T00:00:00.000') + 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 ) @@ -373,12 +475,12 @@ class QueryTests: XCTestCase { 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), optional: nil, sub: nil) + 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' + \"date\" = '1970-01-01T00:00:00.000', \"uuid\" = 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F' """.replacingOccurrences(of: "\n", with: ""), update ) @@ -387,9 +489,9 @@ class QueryTests: XCTestCase { 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), optional: nil, sub: nil) + 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), optional: nil, sub: value1) + 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, @@ -398,7 +500,7 @@ class QueryTests: XCTestCase { let expectedPrefix = """ UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, - \"date\" = '1970-01-01T00:00:00.000', \"sub\" = ' + \"date\" = '1970-01-01T00:00:00.000', \"uuid\" = 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', \"sub\" = ' """.replacingOccurrences(of: "\n", with: "") let expectedSuffix = "'" diff --git a/Tests/SQLiteTests/RowTests.swift b/Tests/SQLiteTests/Typed/RowTests.swift similarity index 53% rename from Tests/SQLiteTests/RowTests.swift rename to Tests/SQLiteTests/Typed/RowTests.swift index 36721f80..1aed00a7 100644 --- a/Tests/SQLiteTests/RowTests.swift +++ b/Tests/SQLiteTests/Typed/RowTests.swift @@ -3,51 +3,51 @@ import XCTest 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/SelectTests.swift b/Tests/SQLiteTests/Typed/SelectTests.swift similarity index 63% rename from Tests/SQLiteTests/SelectTests.swift rename to Tests/SQLiteTests/Typed/SelectTests.swift index d1126b66..5fa3cd30 100644 --- a/Tests/SQLiteTests/SelectTests.swift +++ b/Tests/SQLiteTests/Typed/SelectTests.swift @@ -20,23 +20,23 @@ class SelectTests: SQLiteTestCase { ) } - func test_select_columns_from_multiple_tables() { + func test_select_columns_from_multiple_tables() throws { let usersData = Table("users_name") let users = Table("users") - let name = Expression("name") - let id = Expression("id") - let userID = Expression("user_id") - let email = Expression("email") + 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( + 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 { + 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/SetterTests.swift b/Tests/SQLiteTests/Typed/SetterTests.swift similarity index 98% rename from Tests/SQLiteTests/SetterTests.swift rename to Tests/SQLiteTests/Typed/SetterTests.swift index 938dd013..05da57a4 100644 --- a/Tests/SQLiteTests/SetterTests.swift +++ b/Tests/SQLiteTests/Typed/SetterTests.swift @@ -134,4 +134,7 @@ class SetterTests: XCTestCase { 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 49330c12..3ffba810 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -7,15 +7,19 @@ if [ -n "$BUILD_SCHEME" ]; then make test BUILD_SCHEME="$BUILD_SCHEME" fi elif [ -n "$VALIDATOR_SUBSPEC" ]; then - if [ "$VALIDATOR_SUBSPEC" == "none" ]; then - pod lib lint --no-subspecs --fail-fast - else - pod lib lint --subspec="${VALIDATOR_SUBSPEC}" --fail-fast - fi + 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} + cd Tests/SPM && swift "${SPM}" elif [ -n "${PACKAGE_MANAGER_COMMAND}" ]; then swift ${PACKAGE_MANAGER_COMMAND} fi