diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..9304a672 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,20 @@ +> Issues are used to track bugs and feature requests. +> Need help or have a general question? Ask on Stack Overflow (tag sqlite.swift). + +## Build Information + +- Include the SQLite.swift version, commit or branch experiencing the issue. +- Mention Xcode and OS X versions affected. +- How do do you integrate SQLite.swift in your project? + - manual + - CocoaPods + - Carthage + - Swift Package manager + +## General guidelines + +- Be as descriptive as possible. +- Provide as much information needed to _reliably reproduce_ the issue. +- Attach screenshots if possible. +- Better yet: attach GIFs or link to video. +- Even better: link to a sample project exhibiting the issue. diff --git a/.swift-version b/.swift-version index 9f55b2cc..5186d070 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.0 +4.0 diff --git a/.travis.yml b/.travis.yml index 2eb85311..4f17b26f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: objective-c -rvm: 2.2 -osx_image: xcode8.3 +rvm: 2.3 +osx_image: xcode9 env: global: - IOS_SIMULATOR="iPhone 6s" - - IOS_VERSION="10.3.1" + - IOS_VERSION="11.0" matrix: include: - env: BUILD_SCHEME="SQLite iOS" @@ -17,7 +17,7 @@ matrix: - env: CARTHAGE_PLATFORM="Mac" - env: CARTHAGE_PLATFORM="watchOS" - env: CARTHAGE_PLATFORM="tvOS" - - env: PACKAGE_MANAGER_COMMAND="test -Xlinker -lsqlite3" + - env: PACKAGE_MANAGER_COMMAND="test" before_install: - gem update bundler - gem install xcpretty --no-document diff --git a/CHANGELOG.md b/CHANGELOG.md index d6387a56..578b59c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,25 @@ +0.11.4 (30-09-2017), [diff][diff-0.11.4] +======================================== + +* Collate `.nocase` strictly enforces `NOT NULL` even when using Optional ([#697][]) +* Fix transactions not being rolled back when committing fails ([#426][]) +* Add possibility to have expression on right hand side of like ([#591][]) +* Added Date and Time functions ([#142][]) +* Add Swift4 Coding support ([#733][]) +* Preliminary Linux support ([#315][], [#681][]) +* Add `RowIterator` for more safety ([#647][], [#726][]) +* Make `Row.get` throw instead of crash ([#649][]) +* Fix create/drop index functions ([#666][]) +* Revert deployment target to 8.0 ([#625][], [#671][], [#717][]) +* Added support for the union query clause ([#723][]) +* Add support for `ORDER` and `LIMIT` on `UPDATE` and `DELETE` ([#657][], [#722][]) +* Swift 4 support ([#668][]) + 0.11.3 (30-03-2017), [diff][diff-0.11.3] ======================================== * Fix compilation problems when using Carthage ([#615][]) -* Add "WITHOUT ROWID" table option ([#541][]) +* Add `WITHOUT ROWID` table option ([#541][]) * Argument count fixed for binary custom functions ([#481][]) * Documentation updates * Tested with Xcode 8.3 / iOS 10.3 @@ -20,7 +37,7 @@ * Integrate SQLCipher via CocoaPods ([#546][], [#553][]) * Made lastInsertRowid consistent with other SQLite wrappers ([#532][]) -* Fix for ~= operator used with Double ranges +* Fix for `~=` operator used with Double ranges * Various documentation updates 0.11.0 (19-10-2016) @@ -33,10 +50,14 @@ [diff-0.11.1]: https://github.com/stephencelis/SQLite.swift/compare/0.11.0...0.11.1 [diff-0.11.2]: https://github.com/stephencelis/SQLite.swift/compare/0.11.1...0.11.2 [diff-0.11.3]: https://github.com/stephencelis/SQLite.swift/compare/0.11.2...0.11.3 +[diff-0.11.4]: https://github.com/stephencelis/SQLite.swift/compare/0.11.3...0.11.4 -[#481]: https://github.com/stephencelis/SQLit1e.swift/pull/481 -[#532]: https://github.com/stephencelis/SQLit1e.swift/issues/532 -[#541]: https://github.com/stephencelis/SQLit1e.swift/issues/541 +[#142]: https://github.com/stephencelis/SQLite.swift/issues/142 +[#315]: https://github.com/stephencelis/SQLite.swift/issues/315 +[#426]: https://github.com/stephencelis/SQLite.swift/pull/426 +[#481]: https://github.com/stephencelis/SQLite.swift/pull/481 +[#532]: https://github.com/stephencelis/SQLite.swift/issues/532 +[#541]: https://github.com/stephencelis/SQLite.swift/issues/541 [#546]: https://github.com/stephencelis/SQLite.swift/issues/546 [#548]: https://github.com/stephencelis/SQLite.swift/pull/548 [#553]: https://github.com/stephencelis/SQLite.swift/pull/553 @@ -44,4 +65,19 @@ [#560]: https://github.com/stephencelis/SQLite.swift/pull/560 [#561]: https://github.com/stephencelis/SQLite.swift/issues/561 [#571]: https://github.com/stephencelis/SQLite.swift/issues/571 +[#591]: https://github.com/stephencelis/SQLite.swift/pull/591 [#615]: https://github.com/stephencelis/SQLite.swift/pull/615 +[#625]: https://github.com/stephencelis/SQLite.swift/issues/625 +[#647]: https://github.com/stephencelis/SQLite.swift/pull/647 +[#649]: https://github.com/stephencelis/SQLite.swift/pull/649 +[#657]: https://github.com/stephencelis/SQLite.swift/issues/657 +[#666]: https://github.com/stephencelis/SQLite.swift/pull/666 +[#668]: https://github.com/stephencelis/SQLite.swift/pull/668 +[#671]: https://github.com/stephencelis/SQLite.swift/issues/671 +[#681]: https://github.com/stephencelis/SQLite.swift/issues/681 +[#697]: https://github.com/stephencelis/SQLite.swift/issues/697 +[#717]: https://github.com/stephencelis/SQLite.swift/issues/717 +[#722]: https://github.com/stephencelis/SQLite.swift/pull/722 +[#723]: https://github.com/stephencelis/SQLite.swift/pull/723 +[#733]: https://github.com/stephencelis/SQLite.swift/pull/733 +[#726]: https://github.com/stephencelis/SQLite.swift/pull/726 diff --git a/CocoaPods/appletvos/module.modulemap b/CocoaPods/appletvos/module.modulemap deleted file mode 100644 index 637d9935..00000000 --- a/CocoaPods/appletvos/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/appletvsimulator/module.modulemap b/CocoaPods/appletvsimulator/module.modulemap deleted file mode 100644 index f8b9b671..00000000 --- a/CocoaPods/appletvsimulator/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/iphoneos-10.0/module.modulemap b/CocoaPods/iphoneos-10.0/module.modulemap deleted file mode 100644 index 67a6c203..00000000 --- a/CocoaPods/iphoneos-10.0/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.0.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/iphoneos/module.modulemap b/CocoaPods/iphoneos/module.modulemap deleted file mode 100644 index 043db6c4..00000000 --- a/CocoaPods/iphoneos/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/iphonesimulator-10.0/module.modulemap b/CocoaPods/iphonesimulator-10.0/module.modulemap deleted file mode 100644 index c8b84ab8..00000000 --- a/CocoaPods/iphonesimulator-10.0/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.0.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/iphonesimulator/module.modulemap b/CocoaPods/iphonesimulator/module.modulemap deleted file mode 100644 index a7b14cbb..00000000 --- a/CocoaPods/iphonesimulator/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/macosx-10.11/module.modulemap b/CocoaPods/macosx-10.11/module.modulemap deleted file mode 100644 index 9e091297..00000000 --- a/CocoaPods/macosx-10.11/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/macosx-10.12/module.modulemap b/CocoaPods/macosx-10.12/module.modulemap deleted file mode 100644 index 8fc958e6..00000000 --- a/CocoaPods/macosx-10.12/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/macosx/module.modulemap b/CocoaPods/macosx/module.modulemap deleted file mode 100644 index cc8370ec..00000000 --- a/CocoaPods/macosx/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/watchos/module.modulemap b/CocoaPods/watchos/module.modulemap deleted file mode 100644 index 62a6c4ee..00000000 --- a/CocoaPods/watchos/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/usr/include/sqlite3.h" - export * -} diff --git a/CocoaPods/watchsimulator/module.modulemap b/CocoaPods/watchsimulator/module.modulemap deleted file mode 100644 index 086fbab2..00000000 --- a/CocoaPods/watchsimulator/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CSQLite [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/usr/include/sqlite3.h" - export * -} diff --git a/Documentation/Index.md b/Documentation/Index.md index 07bb5f33..c97fe8ca 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -20,6 +20,7 @@ - [Column Constraints](#column-constraints) - [Table Constraints](#table-constraints) - [Inserting Rows](#inserting-rows) + - [Handling SQLite errors](#handling-sqlite-errors) - [Setters](#setters) - [Selecting Rows](#selecting-rows) - [Iterating and Accessing Values](#iterating-and-accessing-values) @@ -49,10 +50,11 @@ - [Custom Types](#custom-types) - [Date-Time Values](#date-time-values) - [Binary Data](#binary-data) - - [Custom Type Caveats](#custom-type-caveats) + - [Codable Types](#codable-types) - [Other Operators](#other-operators) - [Core SQLite Functions](#core-sqlite-functions) - [Aggregate SQLite Functions](#aggregate-sqlite-functions) + - [Date and Time Functions](#date-and-time-functions) - [Custom SQL Functions](#custom-sql-functions) - [Custom Collations](#custom-collations) - [Full-text Search](#full-text-search) @@ -65,20 +67,20 @@ ## Installation -> _Note:_ SQLite.swift requires Swift 3 (and [Xcode 8](https://developer.apple.com/xcode/downloads/)) or greater. +> _Note:_ SQLite.swift requires Swift 4 (and +> [Xcode 9](https://developer.apple.com/xcode/downloads/)) or greater. ### Carthage [Carthage][] is a simple, decentralized dependency manager for Cocoa. To install SQLite.swift with Carthage: - 1. Make sure Carthage is [installed][Carthage Installation]. 2. Update your Cartfile to include the following: - ``` - github "stephencelis/SQLite.swift" ~> 0.11.3 + ```ruby + github "stephencelis/SQLite.swift" ~> 0.11.4 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. @@ -93,48 +95,46 @@ install SQLite.swift with Carthage: [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: - 1. Verify that your copy of Xcode is installed and active in the default location (`/Applications/Xcode.app`). + 1. Make sure CocoaPods is [installed][CocoaPods Installation] (SQLite.swift + requires version 1.0.0 or greater). ```sh - sudo xcode-select --switch /Applications/Xcode.app - ``` - - 2. Make sure CocoaPods is [installed][CocoaPods Installation] (SQLite.swift requires version 1.0.0 or greater). - - ``` sh # Using the default Ruby install will require you to use sudo when # installing and updating gems. [sudo] gem install cocoapods ``` - 3. Update your Podfile to include the following: + 2. Update your Podfile to include the following: - ``` ruby + ```ruby use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.11.3' + pod 'SQLite.swift', '~> 0.11.4' end ``` - 4. Run `pod install --repo-update`. + 3. Run `pod install --repo-update`. #### Requiring a specific version of SQLite - If you want to use a more recent version of SQLite than what is provided with the OS you can require the `standalone` subspec: +If you want to use a more recent version of SQLite than what is provided +with the OS you can require the `standalone` subspec: -``` ruby +```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.11.3' + pod 'SQLite.swift/standalone', '~> 0.11.4' end ``` -By default this will use the most recent version of SQLite without any extras. If you want you can further customize this by adding another dependency to sqlite3 or one of its subspecs: +By default this will use the most recent version of SQLite without any +extras. If you want you can further customize this by adding another +dependency to sqlite3 or one of its subspecs: -``` ruby +```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.11.3' + pod 'SQLite.swift/standalone', '~> 0.11.4' pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled end ``` @@ -143,19 +143,19 @@ 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: +If you want to use [SQLCipher][] with SQLite.swift you can require the +`SQLCipher` subspec in your Podfile: -``` ruby +```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/SQLCipher', '~> 0.11.3' + pod 'SQLite.swift/SQLCipher', '~> 0.11.4' end ``` -This will automatically add a dependency to the SQLCipher pod as well as extend -`Connection` with methods to change the database key: +This will automatically add a dependency to the SQLCipher pod as well as +extend `Connection` with methods to change the database key: -``` swift +```swift import SQLite let db = try Connection("path/to/db.sqlite3") @@ -170,24 +170,25 @@ try db.rekey("another secret") ### Swift Package Manager -The [Swift Package Manager][] is a tool for managing the distribution of Swift code. -It’s integrated with the Swift build system to automate the process of -downloading, compiling, and linking dependencies. +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. It’s integrated with the Swift build system to automate the +process of downloading, compiling, and linking dependencies. -It is the recommended approach for using SQLite.swift in OSX CLI applications. +It is the recommended approach for using SQLite.swift in OSX CLI +applications. 1. Add the following to your `Package.swift` file: - ``` swift + ```swift dependencies: [ - .Package(url: "https://github.com/stephencelis/SQLite.swift.git", majorVersion: 0, minor: 11) + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.11.4") ] ``` 2. Build your project: - ``` sh - $ swift build -Xlinker -lsqlite3 + ```sh + $ swift build ``` [Swift Package Manager]: https://swift.org/package-manager @@ -196,21 +197,28 @@ It is the recommended approach for using SQLite.swift in OSX CLI applications. To install SQLite.swift as an Xcode sub-project: - 1. Drag the **SQLite.xcodeproj** file into your own project. ([Submodule](http://git-scm.com/book/en/Git-Tools-Submodules), clone, or [download](https://github.com/stephencelis/SQLite.swift/archive/master.zip) the project first.) + 1. Drag the **SQLite.xcodeproj** file into your own project. + ([Submodule](http://git-scm.com/book/en/Git-Tools-Submodules), clone, or + [download](https://github.com/stephencelis/SQLite.swift/archive/master.zip) + the project first.) ![Installation Screen Shot](Resources/installation@2x.png) - 2. In your target’s **General** tab, click the **+** button under **Linked Frameworks and Libraries**. + 2. In your target’s **General** tab, click the **+** button under **Linked + Frameworks and Libraries**. 3. Select the appropriate **SQLite.framework** for your platform. 4. **Add**. -You should now be able to `import SQLite` from any of your target’s source files and begin using SQLite.swift. +You should now be able to `import SQLite` from any of your target’s source +files and begin using SQLite.swift. -Some additional steps are required to install the application on an actual device: +Some additional steps are required to install the application on an actual +device: - 5. In the **General** tab, click the **+** button under **Embedded Binaries**. + 5. In the **General** tab, click the **+** button under **Embedded + Binaries**. 6. Select the appropriate **SQLite.framework** for your platform. @@ -218,27 +226,31 @@ Some additional steps are required to install the application on an actual devic ## Getting Started -To use SQLite.swift classes or structures in your target’s source file, first import the `SQLite` module. +To use SQLite.swift classes or structures in your target’s source file, first +import the `SQLite` module. -``` swift +```swift import SQLite ``` ### Connecting to a Database -Database connections are established using the `Connection` class. A connection is initialized with a path to a database. SQLite will attempt to create the database file if it does not already exist. +Database connections are established using the `Connection` class. A +connection is initialized with a path to a database. SQLite will attempt to +create the database file if it does not already exist. -``` swift +```swift let db = try Connection("path/to/db.sqlite3") ``` #### Read-Write Databases -On iOS, you can create a writable database in your app’s **Documents** directory. +On iOS, you can create a writable database in your app’s **Documents** +directory. -``` swift +```swift let path = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true ).first! @@ -248,10 +260,10 @@ let db = try Connection("\(path)/db.sqlite3") On OS X, you can use your app’s **Application Support** directory: -``` swift +```swift var path = NSSearchPathForDirectoriesInDomains( .applicationSupportDirectory, .userDomainMask, true -).first! + Bundle.main.bundleIdentifier! +).first! + "/" + Bundle.main.bundleIdentifier! // create parent directory iff it doesn’t exist try FileManager.default.createDirectoryAtPath( @@ -264,38 +276,53 @@ let db = try Connection("\(path)/db.sqlite3") #### Read-Only Databases -If you bundle a database with your app (_i.e._, you’ve copied a database file into your Xcode project and added it to your application target), you can establish a _read-only_ connection to it. +If you bundle a database with your app (_i.e._, you’ve copied a database file +into your Xcode project and added it to your application target), you can +establish a _read-only_ connection to it. -``` swift +```swift let path = Bundle.main.pathForResource("db", ofType: "sqlite3")! let db = try Connection(path, readonly: true) ``` -> _Note:_ Signed applications cannot modify their bundle resources. If you bundle a database file with your app for the purpose of bootstrapping, copy it to a writable location _before_ establishing a connection (see [Read-Write Databases](#read-write-databases), above, for typical, writable locations). +> _Note:_ Signed applications cannot modify their bundle resources. If you +> bundle a database file with your app for the purpose of bootstrapping, copy +> it to a writable location _before_ establishing a connection (see +> [Read-Write Databases](#read-write-databases), above, for typical, writable +> locations). > -> See these two Stack Overflow questions for more information about iOS apps with SQLite databases: [1](https://stackoverflow.com/questions/34609746/what-different-between-store-database-in-different-locations-in-ios), [2](https://stackoverflow.com/questions/34614968/ios-how-to-copy-pre-seeded-database-at-the-first-running-app-with-sqlite-swift). We welcome sample code to show how to successfully copy and use a bundled "seed" database for writing in an app. +> See these two Stack Overflow questions for more information about iOS apps +> with SQLite databases: [1](https://stackoverflow.com/questions/34609746/what-different-between-store-database-in-different-locations-in-ios), +> [2](https://stackoverflow.com/questions/34614968/ios-how-to-copy-pre-seeded-database-at-the-first-running-app-with-sqlite-swift). +> We welcome sample code to show how to successfully copy and use a bundled "seed" +> database for writing in an app. #### In-Memory Databases -If you omit the path, SQLite.swift will provision an [in-memory database](https://www.sqlite.org/inmemorydb.html). +If you omit the path, SQLite.swift will provision an [in-memory +database](https://www.sqlite.org/inmemorydb.html). -``` swift +```swift let db = try Connection() // equivalent to `Connection(.inMemory)` ``` To create a temporary, disk-backed database, pass an empty file name. -``` swift +```swift let db = try Connection(.temporary) ``` -In-memory databases are automatically deleted when the database connection is closed. +In-memory databases are automatically deleted when the database connection is +closed. #### Thread-Safety -Every Connection comes equipped with its own serial queue for statement execution and can be safely accessed across threads. Threads that open transactions and savepoints will block other threads from executing statements while the transaction is open. +Every Connection comes equipped with its own serial queue for statement +execution and can be safely accessed across threads. Threads that open +transactions and savepoints will block other threads from executing +statements while the transaction is open. If you maintain multiple connections for a single database, consider setting a timeout (in seconds) and/or a busy handler: @@ -310,12 +337,16 @@ db.busyHandler({ tries in }) ``` -> _Note:_ The default timeout is 0, so if you see `database is locked` errors, you may be trying to access the same database simultaneously from multiple connections. +> _Note:_ The default timeout is 0, so if you see `database is locked` +> errors, you may be trying to access the same database simultaneously from +> multiple connections. ## Building Type-Safe SQL -SQLite.swift comes with a typed expression layer that directly maps [Swift types](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/) to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). +SQLite.swift comes with a typed expression layer that directly maps +[Swift types](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/) +to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). | Swift Type | SQLite Type | | --------------- | ----------- | @@ -325,22 +356,32 @@ SQLite.swift comes with a typed expression layer that directly maps [Swift types | `nil` | `NULL` | | `SQLite.Blob`† | `BLOB` | -> *While `Int64` is the basic, raw type (to preserve 64-bit integers on 32-bit platforms), `Int` and `Bool` work transparently. +> *While `Int64` is the basic, raw type (to preserve 64-bit integers on +> 32-bit platforms), `Int` and `Bool` work transparently. > -> †SQLite.swift defines its own `Blob` structure, which safely wraps the underlying bytes. +> †SQLite.swift defines its own `Blob` structure, which safely wraps the +> underlying bytes. > -> See [Custom Types](#custom-types) for more information about extending other classes and structures to work with SQLite.swift. +> See [Custom Types](#custom-types) for more information about extending +> other classes and structures to work with SQLite.swift. > -> See [Executing Arbitrary SQL](#executing-arbitrary-sql) to forego the typed layer and execute raw SQL, instead. +> See [Executing Arbitrary SQL](#executing-arbitrary-sql) to forego the typed +> layer and execute raw SQL, instead. -These expressions (in the form of the structure, [`Expression`](#expressions)) build on one another and, with a query ([`QueryType`](#queries)), can create and execute SQL statements. +These expressions (in the form of the structure, +[`Expression`](#expressions)) build on one another and, with a query +([`QueryType`](#queries)), can create and execute SQL statements. ### Expressions -Expressions are generic structures associated with a type ([built-in](#building-type-safe-sql) or [custom](#custom-types)), raw SQL, and (optionally) values to bind to that SQL. Typically, you will only explicitly create expressions to describe your columns, and typically only once per column. +Expressions are generic structures associated with a type ([built-in +](#building-type-safe-sql) or [custom](#custom-types)), raw SQL, and +(optionally) values to bind to that SQL. Typically, you will only explicitly +create expressions to describe your columns, and typically only once per +column. -``` swift +```swift let id = Expression("id") let email = Expression("email") let balance = Expression("balance") @@ -349,34 +390,48 @@ let verified = Expression("verified") Use optional generics for expressions that can evaluate to `NULL`. -``` swift +```swift let name = Expression("name") ``` -> _Note:_ The default `Expression` initializer is for [quoted identifiers](https://www.sqlite.org/lang_keywords.html) (_i.e._, column names). To build a literal SQL expression, use `init(literal:)`. +> _Note:_ The default `Expression` initializer is for [quoted +> identifiers](https://www.sqlite.org/lang_keywords.html) (_i.e._, column +> names). To build a literal SQL expression, use `init(literal:)`. +> ### Compound Expressions -Expressions can be combined with other expressions and types using [filter operators and functions](#filter-operators-and-functions) (as well as other [non-filter operators](#other-operators) and [functions](#core-sqlite-functions)). These building blocks can create complex SQLite statements. +Expressions can be combined with other expressions and types using +[filter operators and functions](#filter-operators-and-functions) +(as well as other [non-filter operators](#other-operators) and +[functions](#core-sqlite-functions)). These building blocks can create complex SQLite statements. ### Queries -Queries are structures that reference a database and table name, and can be used to build a variety of statements using expressions. We can create a query by initializing a `Table`, `View`, or `VirtualTable`. +Queries are structures that reference a database and table name, and can be +used to build a variety of statements using expressions. We can create a +query by initializing a `Table`, `View`, or `VirtualTable`. -``` swift +```swift let users = Table("users") ``` -Assuming [the table exists](#creating-a-table), we can immediately [insert](#inserting-rows), [select](#selecting-rows), [update](#updating-rows), and [delete](#deleting-rows) rows. +Assuming [the table exists](#creating-a-table), we can immediately [insert +](#inserting-rows), [select](#selecting-rows), [update](#updating-rows), and +[delete](#deleting-rows) rows. ## Creating a Table -We can build [`CREATE TABLE` statements](https://www.sqlite.org/lang_createtable.html) by calling the `create` function on a `Table`. The following is a basic example of SQLite.swift code (using the [expressions](#expressions) and [query](#queries) above) and the corresponding SQL it generates. +We can build [`CREATE TABLE` +statements](https://www.sqlite.org/lang_createtable.html) by calling the +`create` function on a `Table`. The following is a basic example of +SQLite.swift code (using the [expressions](#expressions) and +[query](#queries) above) and the corresponding SQL it generates. -``` swift +```swift try db.run(users.create { t in // CREATE TABLE "users" ( t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL, t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL, @@ -384,34 +439,42 @@ try db.run(users.create { t in // CREATE TABLE "users" ( }) // ) ``` -> _Note:_ `Expression` structures (in this case, the `id` and `email` columns), generate `NOT NULL` constraints automatically, while `Expression` structures (`name`) do not. +> _Note:_ `Expression` structures (in this case, the `id` and `email` +> columns), generate `NOT NULL` constraints automatically, while +> `Expression` structures (`name`) do not. ### Create Table Options The `Table.create` function has several default parameters we can override. - - `temporary` adds a `TEMPORARY` clause to the `CREATE TABLE` statement (to create a temporary table that will automatically drop when the database connection closes). Default: `false`. + - `temporary` adds a `TEMPORARY` clause to the `CREATE TABLE` statement (to + create a temporary table that will automatically drop when the database + connection closes). Default: `false`. - ``` swift + ```swift try db.run(users.create(temporary: true) { t in /* ... */ }) // CREATE TEMPORARY TABLE "users" -- ... ``` - - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` statement (which will bail out gracefully if the table already exists). Default: `false`. + - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` + statement (which will bail out gracefully if the table already exists). + Default: `false`. - ``` swift + ```swift try db.run(users.create(ifNotExists: true) { t in /* ... */ }) // CREATE TABLE "users" IF NOT EXISTS -- ... ``` ### Column Constraints -The `column` function is used for a single column definition. It takes an [expression](#expressions) describing the column name and type, and accepts several parameters that map to various column constraints and clauses. +The `column` function is used for a single column definition. It takes an +[expression](#expressions) describing the column name and type, and accepts +several parameters that map to various column constraints and clauses. - `primaryKey` adds a `PRIMARY KEY` constraint to a single column. - ``` swift + ```swift t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL @@ -419,38 +482,59 @@ The `column` function is used for a single column definition. It takes an [expre // "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ``` - > _Note:_ The `primaryKey` parameter cannot be used alongside `references`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > _Note:_ The `primaryKey` parameter cannot be used alongside + > `references`. If you need to create a column that has a default value + > and is also a primary and/or foreign key, use the `primaryKey` and + > `foreignKey` functions mentioned under + > [Table Constraints](#table-constraints). > > Primary keys cannot be optional (_e.g._, `Expression`). > > Only an `INTEGER PRIMARY KEY` can take `.autoincrement`. - - `unique` adds a `UNIQUE` constraint to the column. (See the `unique` function under [Table Constraints](#table-constraints) for uniqueness over multiple columns). + - `unique` adds a `UNIQUE` constraint to the column. (See the `unique` + function under [Table Constraints](#table-constraints) for uniqueness + over multiple columns). - ``` swift + ```swift t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL ``` - - `check` attaches a `CHECK` constraint to a column definition in the form of a boolean expression (`Expression`). Boolean expressions can be easily built using [filter operators and functions](#filter-operators-and-functions). (See also the `check` function under [Table Constraints](#table-constraints).) + - `check` attaches a `CHECK` constraint to a column definition in the form + of a boolean expression (`Expression`). Boolean expressions can be + easily built using + [filter operators and functions](#filter-operators-and-functions). + (See also the `check` function under + [Table Constraints](#table-constraints).) - ``` swift + ```swift t.column(email, check: email.like("%@%")) // "email" TEXT NOT NULL CHECK ("email" LIKE '%@%') ``` - - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ accepts a value (or expression) matching the column’s type. This value is used if none is explicitly provided during [an `INSERT`](#inserting-rows). + - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ + accepts a value (or expression) matching the column’s type. This value is + used if none is explicitly provided during + [an `INSERT`](#inserting-rows). - ``` swift + ```swift t.column(name, defaultValue: "Anonymous") // "name" TEXT DEFAULT 'Anonymous' ``` - > _Note:_ The `defaultValue` parameter cannot be used alongside `primaryKey` and `references`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > _Note:_ The `defaultValue` parameter cannot be used alongside + > `primaryKey` and `references`. If you need to create a column that has + > a default value and is also a primary and/or foreign key, use the + > `primaryKey` and `foreignKey` functions mentioned under + > [Table Constraints](#table-constraints). - - `collate` adds a `COLLATE` clause to `Expression` (and `Expression`) column definitions with [a collating sequence](https://www.sqlite.org/datatype3.html#collation) defined in the `Collation` enumeration. + - `collate` adds a `COLLATE` clause to `Expression` (and + `Expression`) column definitions with + [a collating sequence](https://www.sqlite.org/datatype3.html#collation) + defined in the `Collation` enumeration. - ``` swift + ```swift t.column(email, collate: .nocase) // "email" TEXT NOT NULL COLLATE "NOCASE" @@ -458,50 +542,66 @@ The `column` function is used for a single column definition. It takes an [expre // "name" TEXT COLLATE "RTRIM" ``` - - `references` adds a `REFERENCES` clause to `Expression` (and `Expression`) column definitions and accepts a table (`SchemaType`) or namespaced column expression. (See the `foreignKey` function under [Table Constraints](#table-constraints) for non-integer foreign key support.) + - `references` adds a `REFERENCES` clause to `Expression` (and + `Expression`) column definitions and accepts a table + (`SchemaType`) or namespaced column expression. (See the `foreignKey` + function under [Table Constraints](#table-constraints) for non-integer + foreign key support.) - ``` swift + ```swift t.column(user_id, references: users, id) // "user_id" INTEGER REFERENCES "users" ("id") - - - > _Note:_ The `references` parameter cannot be used alongside `primaryKey` and `defaultValue`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > _Note:_ The `references` parameter cannot be used alongside + > `primaryKey` and `defaultValue`. If you need to create a column that + > has a default value and is also a primary and/or foreign key, use the + > `primaryKey` and `foreignKey` functions mentioned under + > [Table Constraints](#table-constraints). ### Table Constraints -Additional constraints may be provided outside the scope of a single column using the following functions. +Additional constraints may be provided outside the scope of a single column +using the following functions. - - `primaryKey` adds a `PRIMARY KEY` constraint to the table. Unlike [the column constraint, above](#column-constraints), it supports all SQLite types, [ascending and descending orders](#sorting-rows), and composite (multiple column) keys. + - `primaryKey` adds a `PRIMARY KEY` constraint to the table. Unlike [the + column constraint, above](#column-constraints), it supports all SQLite + types, [ascending and descending orders](#sorting-rows), and composite + (multiple column) keys. - ``` swift + ```swift t.primaryKey(email.asc, name) // PRIMARY KEY("email" ASC, "name") ``` - - `unique` adds a `UNIQUE` constraint to the table. Unlike [the column constraint, above](#column-constraints), it supports composite (multiple column) constraints. + - `unique` adds a `UNIQUE` constraint to the table. Unlike + [the column constraint, above](#column-constraints), it + supports composite (multiplecolumn) constraints. - ``` swift + ```swift t.unique(local, domain) // UNIQUE("local", "domain") ``` - - `check` adds a `CHECK` constraint to the table in the form of a boolean expression (`Expression`). Boolean expressions can be easily built using [filter operators and functions](#filter-operators-and-functions). (See also the `check` parameter under [Column Constraints](#column-constraints).) + - `check` adds a `CHECK` constraint to the table in the form of a boolean + expression (`Expression`). Boolean expressions can be easily built + using [filter operators and functions](#filter-operators-and-functions). + (See also the `check` parameter under + [Column Constraints](#column-constraints).) - ``` swift + ```swift t.check(balance >= 0) // CHECK ("balance" >= 0.0) ``` - - `foreignKey` adds a `FOREIGN KEY` constraint to the table. Unlike [the `references` constraint, above](#column-constraints), it supports all SQLite types, both [`ON UPDATE` and `ON DELETE` actions](https://www.sqlite.org/foreignkeys.html#fk_actions), and composite (multiple column) keys. + - `foreignKey` adds a `FOREIGN KEY` constraint to the table. Unlike [the + `references` constraint, above](#column-constraints), it supports all + SQLite types, both [`ON UPDATE` and `ON DELETE` + actions](https://www.sqlite.org/foreignkeys.html#fk_actions), and + composite (multiple column) keys. - ``` swift + ```swift t.foreignKey(user_id, references: users, id, delete: .setNull) // FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE SET NULL ``` @@ -513,9 +613,12 @@ Additional constraints may be provided outside the scope of a single column usin ## Inserting Rows -We can insert rows into a table by calling a [query’s](#queries) `insert` function with a list of [setters](#setters)—typically [typed column expressions](#expressions) and values (which can also be expressions)—each joined by the `<-` operator. +We can insert rows into a table by calling a [query’s](#queries) `insert` +function with a list of [setters](#setters)—typically [typed column +expressions](#expressions) and values (which can also be expressions)—each +joined by the `<-` operator. -``` swift +```swift try db.run(users.insert(email <- "alice@mac.com", name <- "Alice")) // INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice') @@ -523,9 +626,10 @@ try db.run(users.insert(or: .replace, email <- "alice@mac.com", name <- "Alice B // INSERT OR REPLACE INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice B.') ``` -The `insert` function, when run successfully, returns an `Int64` representing the inserted row’s [`ROWID`][ROWID]. +The `insert` function, when run successfully, returns an `Int64` representing +the inserted row’s [`ROWID`][ROWID]. -``` swift +```swift do { let rowid = try db.run(users.insert(email <- "alice@mac.com")) print("inserted id: \(rowid)") @@ -534,37 +638,61 @@ do { } ``` -The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions follow similar patterns. +The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions +follow similar patterns. -> _Note:_ If `insert` is called without any arguments, the statement will run with a `DEFAULT VALUES` clause. The table must not have any constraints that aren’t fulfilled by default values. +> _Note:_ If `insert` is called without any arguments, the statement will run +> with a `DEFAULT VALUES` clause. The table must not have any constraints +> that aren’t fulfilled by default values. > -> ``` swift +> ```swift > try db.run(timestamps.insert()) > // INSERT INTO "timestamps" DEFAULT VALUES > ``` +### Handling SQLite errors + +You can pattern match on the error to selectively catch SQLite errors. For example, to +specifically handle constraint errors ([SQLITE_CONSTRAINT](https://sqlite.org/rescode.html#constraint)): + +```swift +do { + try db.run(users.insert(email <- "alice@mac.com")) + try db.run(users.insert(email <- "alice@mac.com")) +} catch let Result.error(message, code, statement) where code == SQLITE_CONSTRAINT { + print("constraint failed: \(message), in \(statement)") +} catch let error { + print("insertion failed: \(error)") +} +``` + +The `Result.error` type contains the English-language text that describes the error (`message`), +the error `code` (see [SQLite result code list](https://sqlite.org/rescode.html#primary_result_code_list) +for details) and a optional reference to the `statement` which produced the error. ### Setters -SQLite.swift typically uses the `<-` operator to set values during [inserts](#inserting-rows) and [updates](#updating-rows). +SQLite.swift typically uses the `<-` operator to set values during [inserts +](#inserting-rows) and [updates](#updating-rows). -``` swift +```swift try db.run(counter.update(count <- 0)) // UPDATE "counters" SET "count" = 0 WHERE ("id" = 1) ``` -There are also a number of convenience setters that take the existing value into account using native Swift operators. +There are also a number of convenience setters that take the existing value +into account using native Swift operators. For example, to atomically increment a column, we can use `++`: -``` swift +```swift try db.run(counter.update(count++)) // equivalent to `counter.update(count -> count + 1)` // UPDATE "counters" SET "count" = "count" + 1 WHERE ("id" = 1) ``` To take an amount and “move” it via transaction, we can use `-=` and `+=`: -``` swift +```swift let amount = 100.0 try db.transaction { try db.run(alice.update(balance -= amount)) @@ -605,14 +733,18 @@ try db.transaction { ## Selecting Rows -[Query structures](#queries) are `SELECT` statements waiting to happen. They execute via [iteration](#iterating-and-accessing-values) and [other means](#plucking-values) of sequence access. +[Query structures](#queries) are `SELECT` statements waiting to happen. They +execute via [iteration](#iterating-and-accessing-values) and [other means +](#plucking-values) of sequence access. ### Iterating and Accessing Values -Prepared [queries](#queries) execute lazily upon iteration. Each row is returned as a `Row` object, which can be subscripted with a [column expression](#expressions) matching one of the columns returned. +Prepared [queries](#queries) execute lazily upon iteration. Each row is +returned as a `Row` object, which can be subscripted with a [column +expression](#expressions) matching one of the columns returned. -``` swift +```swift for user in try db.prepare(users) { print("id: \(user[id]), email: \(user[email]), name: \(user[name])") // id: 1, email: alice@mac.com, name: Optional("Alice") @@ -620,21 +752,37 @@ for user in try db.prepare(users) { // SELECT * FROM "users" ``` -`Expression` column values are _automatically unwrapped_ (we’ve made a promise to the compiler that they’ll never be `NULL`), while `Expression` values remain wrapped. +`Expression` column values are _automatically unwrapped_ (we’ve made a +promise to the compiler that they’ll never be `NULL`), while `Expression` +values remain wrapped. +⚠ Column subscripts on `Row` will force try and abort execution in error cases. +If you want to handle this yourself, use `Row.get(_ column: Expression)`: + +```swift +for user in try db.prepare(users) { + do { + print("name: \(try user.get(name))") + } catch { + // handle + } +} +``` ### Plucking Rows -We can pluck the first row by passing a query to the `pluck` function on a database connection. +We can pluck the first row by passing a query to the `pluck` function on a +database connection. -``` swift +```swift if let user = try db.pluck(users) { /* ... */ } // Row // SELECT * FROM "users" LIMIT 1 ``` -To collect all rows into an array, we can simply wrap the sequence (though this is not always the most memory-efficient idea). +To collect all rows into an array, we can simply wrap the sequence (though +this is not always the most memory-efficient idea). -``` swift +```swift let all = Array(try db.prepare(users)) // SELECT * FROM "users" ``` @@ -642,9 +790,12 @@ let all = Array(try db.prepare(users)) ### Building Complex Queries -[Queries](#queries) have a number of chainable functions that can be used (with [expressions](#expressions)) to add and modify [a number of clauses](https://www.sqlite.org/lang_select.html) to the underlying statement. +[Queries](#queries) have a number of chainable functions that can be used +(with [expressions](#expressions)) to add and modify [a number of +clauses](https://www.sqlite.org/lang_select.html) to the underlying +statement. -``` swift +```swift let query = users.select(email) // SELECT "email" FROM "users" .filter(name != nil) // WHERE "name" IS NOT NULL .order(email.desc, name) // ORDER BY "email" DESC, "name" @@ -654,9 +805,11 @@ let query = users.select(email) // SELECT "email" FROM "users" #### Selecting Columns -By default, [queries](#queries) select every column of the result set (using `SELECT *`). We can use the `select` function with a list of [expressions](#expressions) to return specific columns instead. +By default, [queries](#queries) select every column of the result set (using +`SELECT *`). We can use the `select` function with a list of +[expressions](#expressions) to return specific columns instead. -``` swift +```swift for user in try db.prepare(users.select(id, email)) { print("id: \(user[id]), email: \(user[email])") // id: 1, email: alice@mac.com @@ -664,9 +817,10 @@ for user in try db.prepare(users.select(id, email)) { // SELECT "id", "email" FROM "users" ``` -We can access the results of more complex expressions by holding onto a reference of the expression itself. +We can access the results of more complex expressions by holding onto a +reference of the expression itself. -``` swift +```swift let sentence = name + " is " + cast(age) as Expression + " years old!" for user in users.select(sentence) { print(user[sentence]) @@ -680,35 +834,42 @@ for user in users.select(sentence) { We can join tables using a [query’s](#queries) `join` function. -``` swift +```swift users.join(posts, on: user_id == users[id]) // SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id") ``` -The `join` function takes a [query](#queries) object (for the table being joined on), a join condition (`on`), and is prefixed with an optional join type (default: `.inner`). Join conditions can be built using [filter operators and functions](#filter-operators-and-functions), generally require [namespacing](#column-namespacing), and sometimes require [aliasing](#table-aliasing). +The `join` function takes a [query](#queries) object (for the table being +joined on), a join condition (`on`), and is prefixed with an optional join +type (default: `.inner`). Join conditions can be built using [filter +operators and functions](#filter-operators-and-functions), generally require +[namespacing](#column-namespacing), and sometimes require +[aliasing](#table-aliasing). ##### Column Namespacing -When joining tables, column names can become ambiguous. _E.g._, both tables may have an `id` column. +When joining tables, column names can become ambiguous. _E.g._, both tables +may have an `id` column. -``` swift +```swift let query = users.join(posts, on: user_id == id) // assertion failure: ambiguous column 'id' ``` We can disambiguate by namespacing `id`. -``` swift +```swift let query = users.join(posts, on: user_id == users[id]) // SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id") ``` -Namespacing is achieved by subscripting a [query](#queries) with a [column expression](#expressions) (_e.g._, `users[id]` above becomes `users.id`). +Namespacing is achieved by subscripting a [query](#queries) with a [column +expression](#expressions) (_e.g._, `users[id]` above becomes `users.id`). > _Note:_ We can namespace all of a table’s columns using `*`. > -> ``` swift +> ```swift > let query = users.select(users[*]) > // SELECT "users".* FROM "users" > ``` @@ -716,9 +877,11 @@ Namespacing is achieved by subscripting a [query](#queries) with a [column expre ##### Table Aliasing -Occasionally, we need to join a table to itself, in which case we must alias the table with another name. We can achieve this using the [query’s](#queries) `alias` function. +Occasionally, we need to join a table to itself, in which case we must alias +the table with another name. We can achieve this using the +[query’s](#queries) `alias` function. -``` swift +```swift let managers = users.alias("managers") let query = users.join(managers, on: managers[id] == users[managerId]) @@ -726,9 +889,11 @@ let query = users.join(managers, on: managers[id] == users[managerId]) // INNER JOIN ("users") AS "managers" ON ("managers"."id" = "users"."manager_id") ``` -If query results can have ambiguous column names, row values should be accessed with namespaced [column expressions](#expressions). In the above case, `SELECT *` immediately namespaces all columns of the result set. +If query results can have ambiguous column names, row values should be +accessed with namespaced [column expressions](#expressions). In the above +case, `SELECT *` immediately namespaces all columns of the result set. -``` swift +```swift let user = try db.pluck(query) user[id] // fatal error: ambiguous column 'id' // (please disambiguate: ["users"."id", "managers"."id"]) @@ -740,9 +905,10 @@ user[managers[id]] // returns "managers"."id" #### Filtering Rows -SQLite.swift filters rows using a [query’s](#queries) `filter` function with a boolean [expression](#expressions) (`Expression`). +SQLite.swift filters rows using a [query’s](#queries) `filter` function with +a boolean [expression](#expressions) (`Expression`). -``` swift +```swift users.filter(id == 1) // SELECT * FROM "users" WHERE ("id" = 1) @@ -759,18 +925,21 @@ users.filter(verified || balance >= 10_000) // SELECT * FROM "users" WHERE ("verified" OR ("balance" >= 10000.0)) ``` -We can build our own boolean expressions by using one of the many [filter operators and functions](#filter-operators-and-functions). +We can build our own boolean expressions by using one of the many [filter +operators and functions](#filter-operators-and-functions). Instead of `filter` we can also use the `where` function which is an alias: -``` swift +```swift users.where(id == 1) // SELECT * FROM "users" WHERE ("id" = 1) ``` ##### Filter Operators and Functions -SQLite.swift defines a number of operators for building filtering predicates. Operators and functions work together in a type-safe manner, so attempting to equate or compare different types will prevent compilation. +SQLite.swift defines a number of operators for building filtering predicates. +Operators and functions work together in a type-safe manner, so attempting to +equate or compare different types will prevent compilation. ###### Infix Filter Operators @@ -787,7 +956,8 @@ SQLite.swift defines a number of operators for building filtering predicates. Op | `&&` | `Bool -> Bool` | `AND` | | `||` | `Bool -> Bool` | `OR` | -> *When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` accordingly. +> *When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` +> accordingly. ###### Prefix Filter Operators @@ -818,16 +988,18 @@ We can pre-sort returned rows using the [query’s](#queries) `order` function. _E.g._, to return users sorted by `email`, then `name`, in ascending order: -``` swift +```swift users.order(email, name) // SELECT * FROM "users" ORDER BY "email", "name" ``` The `order` function takes a list of [column expressions](#expressions). -`Expression` objects have two computed properties to assist sorting: `asc` and `desc`. These properties append the expression with `ASC` and `DESC` to mark ascending and descending order respectively. +`Expression` objects have two computed properties to assist sorting: `asc` +and `desc`. These properties append the expression with `ASC` and `DESC` to +mark ascending and descending order respectively. -``` swift +```swift users.order(email.desc, name.asc) // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC ``` @@ -835,9 +1007,10 @@ users.order(email.desc, name.asc) #### Limiting and Paging Results -We can limit and skip returned rows using a [query’s](#queries) `limit` function (and its optional `offset` parameter). +We can limit and skip returned rows using a [query’s](#queries) `limit` +function (and its optional `offset` parameter). -``` swift +```swift users.limit(5) // SELECT * FROM "users" LIMIT 5 @@ -848,67 +1021,79 @@ users.limit(5, offset: 5) #### Aggregation -[Queries](#queries) come with a number of functions that quickly return aggregate scalar values from the table. These mirror the [core aggregate functions](#aggregate-sqlite-functions) and are executed immediately against the query. +[Queries](#queries) come with a number of functions that quickly return +aggregate scalar values from the table. These mirror the [core aggregate +functions](#aggregate-sqlite-functions) and are executed immediately against +the query. -``` swift +```swift let count = try db.scalar(users.count) // SELECT count(*) FROM "users" ``` Filtered queries will appropriately filter aggregate values. -``` swift +```swift let count = try db.scalar(users.filter(name != nil).count) // SELECT count(*) FROM "users" WHERE "name" IS NOT NULL ``` - - `count` as a computed property on a query (see examples above) returns the total number of rows matching the query. + - `count` as a computed property on a query (see examples above) returns + the total number of rows matching the query. - `count` as a computed property on a column expression returns the total number of rows where that column is not `NULL`. + `count` as a computed property on a column expression returns the total + number of rows where that column is not `NULL`. - ``` swift + ```swift let count = try db.scalar(users.select(name.count)) // -> Int // SELECT count("name") FROM "users" ``` - - `max` takes a comparable column expression and returns the largest value if any exists. + - `max` takes a comparable column expression and returns the largest value + if any exists. - ``` swift + ```swift let max = try db.scalar(users.select(id.max)) // -> Int64? // SELECT max("id") FROM "users" ``` - - `min` takes a comparable column expression and returns the smallest value if any exists. + - `min` takes a comparable column expression and returns the smallest value + if any exists. - ``` swift + ```swift let min = try db.scalar(users.select(id.min)) // -> Int64? // SELECT min("id") FROM "users" ``` - - `average` takes a numeric column expression and returns the average row value (as a `Double`) if any exists. + - `average` takes a numeric column expression and returns the average row + value (as a `Double`) if any exists. - ``` swift + ```swift let average = try db.scalar(users.select(balance.average)) // -> Double? // SELECT avg("balance") FROM "users" ``` - - `sum` takes a numeric column expression and returns the sum total of all rows if any exist. + - `sum` takes a numeric column expression and returns the sum total of all + rows if any exist. - ``` swift + ```swift let sum = try db.scalar(users.select(balance.sum)) // -> Double? // SELECT sum("balance") FROM "users" ``` - - `total`, like `sum`, takes a numeric column expression and returns the sum total of all rows, but in this case always returns a `Double`, and returns `0.0` for an empty query. + - `total`, like `sum`, takes a numeric column expression and returns the + sum total of all rows, but in this case always returns a `Double`, and + returns `0.0` for an empty query. - ``` swift + ```swift let total = try db.scalar(users.select(balance.total)) // -> Double // SELECT total("balance") FROM "users" ``` -> _Note:_ Expressions can be prefixed with a `DISTINCT` clause by calling the `distinct` computed property. +> _Note:_ Expressions can be prefixed with a `DISTINCT` clause by calling the +> `distinct` computed property. > -> ``` swift +> ```swift > let count = try db.scalar(users.select(name.distinct.count) // -> Int > // SELECT count(DISTINCT "name") FROM "users" > ``` @@ -916,26 +1101,32 @@ let count = try db.scalar(users.filter(name != nil).count) ## Updating Rows -We can update a table’s rows by calling a [query’s](#queries) `update` function with a list of [setters](#setters)—typically [typed column expressions](#expressions) and values (which can also be expressions)—each joined by the `<-` operator. +We can update a table’s rows by calling a [query’s](#queries) `update` +function with a list of [setters](#setters)—typically [typed column +expressions](#expressions) and values (which can also be expressions)—each +joined by the `<-` operator. -When an unscoped query calls `update`, it will update _every_ row in the table. +When an unscoped query calls `update`, it will update _every_ row in the +table. -``` swift +```swift try db.run(users.update(email <- "alice@me.com")) // UPDATE "users" SET "email" = 'alice@me.com' ``` -Be sure to scope `UPDATE` statements beforehand using [the `filter` function](#filtering-rows). +Be sure to scope `UPDATE` statements beforehand using [the `filter` function +](#filtering-rows). -``` swift +```swift let alice = users.filter(id == 1) try db.run(alice.update(email <- "alice@me.com")) // UPDATE "users" SET "email" = 'alice@me.com' WHERE ("id" = 1) ``` -The `update` function returns an `Int` representing the number of updated rows. +The `update` function returns an `Int` representing the number of updated +rows. -``` swift +```swift do { if try db.run(alice.update(email <- "alice@me.com")) > 0 { print("updated alice") @@ -950,26 +1141,30 @@ do { ## Deleting Rows -We can delete rows from a table by calling a [query’s](#queries) `delete` function. +We can delete rows from a table by calling a [query’s](#queries) `delete` +function. -When an unscoped query calls `delete`, it will delete _every_ row in the table. +When an unscoped query calls `delete`, it will delete _every_ row in the +table. -``` swift +```swift try db.run(users.delete()) // DELETE FROM "users" ``` -Be sure to scope `DELETE` statements beforehand using [the `filter` function](#filtering-rows). +Be sure to scope `DELETE` statements beforehand using +[the `filter` function](#filtering-rows). -``` swift +```swift let alice = users.filter(id == 1) try db.run(alice.delete()) // DELETE FROM "users" WHERE ("id" = 1) ``` -The `delete` function returns an `Int` representing the number of deleted rows. +The `delete` function returns an `Int` representing the number of deleted +rows. -``` swift +```swift do { if try db.run(alice.delete()) > 0 { print("deleted alice") @@ -984,9 +1179,11 @@ do { ## Transactions and Savepoints -Using the `transaction` and `savepoint` functions, we can run a series of statements in a transaction. If a single statement fails or the block throws an error, the changes will be rolled back. +Using the `transaction` and `savepoint` functions, we can run a series of +statements in a transaction. If a single statement fails or the block throws +an error, the changes will be rolled back. -``` swift +```swift try db.transaction { let rowid = try db.run(users.insert(email <- "betty@icloud.com")) try db.run(users.insert(email <- "cathy@icloud.com", managerId <- rowid)) @@ -1002,14 +1199,16 @@ try db.transaction { ## Altering the Schema -SQLite.swift comes with several functions (in addition to `Table.create`) for altering a database schema in a type-safe manner. +SQLite.swift comes with several functions (in addition to `Table.create`) for +altering a database schema in a type-safe manner. ### Renaming Tables -We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` function on a `Table` or `VirtualTable`. +We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` +function on a `Table` or `VirtualTable`. -``` swift +```swift try db.run(users.rename(Table("users_old")) // ALTER TABLE "users" RENAME TO "users_old" ``` @@ -1017,9 +1216,12 @@ try db.run(users.rename(Table("users_old")) ### Adding Columns -We can add columns to a table by calling `addColumn` function on a `Table`. SQLite.swift enforces [the same limited subset](https://www.sqlite.org/lang_altertable.html) of `ALTER TABLE` that SQLite supports. +We can add columns to a table by calling `addColumn` function on a `Table`. +SQLite.swift enforces +[the same limited subset](https://www.sqlite.org/lang_altertable.html) of +`ALTER TABLE` that SQLite supports. -``` swift +```swift try db.run(users.addColumn(suffix)) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT ``` @@ -1027,27 +1229,38 @@ try db.run(users.addColumn(suffix)) #### Added Column Constraints -The `addColumn` function shares several of the same [`column` function parameters](#column-constraints) used when [creating tables](#creating-a-table). +The `addColumn` function shares several of the same [`column` function +parameters](#column-constraints) used when [creating +tables](#creating-a-table). - - `check` attaches a `CHECK` constraint to a column definition in the form of a boolean expression (`Expression`). (See also the `check` function under [Table Constraints](#table-constraints).) + - `check` attaches a `CHECK` constraint to a column definition in the form + of a boolean expression (`Expression`). (See also the `check` + function under [Table Constraints](#table-constraints).) - ``` swift + ```swift try db.run(users.addColumn(suffix, check: ["JR", "SR"].contains(suffix))) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT CHECK ("suffix" IN ('JR', 'SR')) ``` - - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ accepts a value matching the column’s type. This value is used if none is explicitly provided during [an `INSERT`](#inserting-rows). + - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ + accepts a value matching the column’s type. This value is used if none is + explicitly provided during [an `INSERT`](#inserting-rows). - ``` swift + ```swift try db.run(users.addColumn(suffix, defaultValue: "SR")) // ALTER TABLE "users" ADD COLUMN "suffix" TEXT DEFAULT 'SR' ``` - > _Note:_ Unlike the [`CREATE TABLE` constraint](#table-constraints), default values may not be expression structures (including `CURRENT_TIME`, `CURRENT_DATE`, or `CURRENT_TIMESTAMP`). + > _Note:_ Unlike the [`CREATE TABLE` constraint](#table-constraints), + > default values may not be expression structures (including + > `CURRENT_TIME`, `CURRENT_DATE`, or `CURRENT_TIMESTAMP`). - - `collate` adds a `COLLATE` clause to `Expression` (and `Expression`) column definitions with [a collating sequence](https://www.sqlite.org/datatype3.html#collation) defined in the `Collation` enumeration. + - `collate` adds a `COLLATE` clause to `Expression` (and + `Expression`) column definitions with [a collating + sequence](https://www.sqlite.org/datatype3.html#collation) defined in the + `Collation` enumeration. - ``` swift + ```swift try db.run(users.addColumn(email, collate: .nocase)) // ALTER TABLE "users" ADD COLUMN "email" TEXT NOT NULL COLLATE "NOCASE" @@ -1055,9 +1268,12 @@ The `addColumn` function shares several of the same [`column` function parameter // ALTER TABLE "users" ADD COLUMN "name" TEXT COLLATE "RTRIM" ``` - - `references` adds a `REFERENCES` clause to `Int64` (and `Int64?`) column definitions and accepts a table or namespaced column expression. (See the `foreignKey` function under [Table Constraints](#table-constraints) for non-integer foreign key support.) + - `references` adds a `REFERENCES` clause to `Int64` (and `Int64?`) column + definitions and accepts a table or namespaced column expression. (See the + `foreignKey` function under [Table Constraints](#table-constraints) for + non-integer foreign key support.) - ``` swift + ```swift try db.run(posts.addColumn(userId, references: users, id) // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" ("id") ``` @@ -1068,27 +1284,32 @@ The `addColumn` function shares several of the same [`column` function parameter #### Creating Indexes -We can build [`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) by calling the `createIndex` function on a `SchemaType`. +We can build +[`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) +by calling the `createIndex` function on a `SchemaType`. -``` swift +```swift try db.run(users.createIndex(email)) // CREATE INDEX "index_users_on_email" ON "users" ("email") ``` -The index name is generated automatically based on the table and column names. +The index name is generated automatically based on the table and column +names. The `createIndex` function has a couple default parameters we can override. - `unique` adds a `UNIQUE` constraint to the index. Default: `false`. - ``` swift + ```swift try db.run(users.createIndex(email, unique: true)) // CREATE UNIQUE INDEX "index_users_on_email" ON "users" ("email") ``` - - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` statement (which will bail out gracefully if the table already exists). Default: `false`. + - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` + statement (which will bail out gracefully if the table already exists). + Default: `false`. - ``` swift + ```swift try db.run(users.createIndex(email, ifNotExists: true)) // CREATE INDEX IF NOT EXISTS "index_users_on_email" ON "users" ("email") ``` @@ -1096,16 +1317,19 @@ The `createIndex` function has a couple default parameters we can override. #### Dropping Indexes -We can build [`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by calling the `dropIndex` function on a `SchemaType`. +We can build +[`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by +calling the `dropIndex` function on a `SchemaType`. -``` swift +```swift try db.run(users.dropIndex(email)) // DROP INDEX "index_users_on_email" ``` -The `dropIndex` function has one additional parameter, `ifExists`, which (when `true`) adds an `IF EXISTS` clause to the statement. +The `dropIndex` function has one additional parameter, `ifExists`, which +(when `true`) adds an `IF EXISTS` clause to the statement. -``` swift +```swift try db.run(users.dropIndex(email, ifExists: true)) // DROP INDEX IF EXISTS "index_users_on_email" ``` @@ -1113,16 +1337,19 @@ try db.run(users.dropIndex(email, ifExists: true)) ### Dropping Tables -We can build [`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) by calling the `dropTable` function on a `SchemaType`. +We can build +[`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) +by calling the `dropTable` function on a `SchemaType`. -``` swift +```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. +The `drop` function has one additional parameter, `ifExists`, which (when +`true`) adds an `IF EXISTS` clause to the statement. -``` swift +```swift try db.run(users.drop(ifExists: true)) // DROP TABLE IF EXISTS "users" ``` @@ -1130,11 +1357,12 @@ try db.run(users.drop(ifExists: true)) ### Migrations and Schema Versioning -You can add a convenience property on `Connection` to query and set the [`PRAGMA user_version`](https://sqlite.org/pragma.html#pragma_user_version). +You can add a convenience property on `Connection` to query and set the +[`PRAGMA user_version`](https://sqlite.org/pragma.html#pragma_user_version). This is a great way to manage your schema’s version over migrations. -``` swift +```swift extension Connection { public var userVersion: Int32 { get { return Int32(try! scalar("PRAGMA user_version") as! Int64)} @@ -1156,85 +1384,48 @@ if db.userVersion == 1 { } ``` -For more complex migration requirements check out the schema management system -[SQLiteMigrationManager.swift][]. +For more complex migration requirements check out the schema management +system [SQLiteMigrationManager.swift][]. ## Custom Types -SQLite.swift supports serializing and deserializing any custom type as long as it conforms to the `Value` protocol. +SQLite.swift supports serializing and deserializing any custom type as long +as it conforms to the `Value` protocol. -> ``` swift -> protocol Value { -> typealias Datatype: Binding -> class var declaredDatatype: String { get } -> class func fromDatatypeValue(datatypeValue: Datatype) -> Self -> var datatypeValue: Datatype { get } -> } -> ``` - -The `Datatype` must be one of the basic Swift types that values are bridged through before serialization and deserialization (see [Building Type-Safe SQL](#building-type-safe-sql) for a list of types). +```swift +protocol Value { + typealias Datatype: Binding + class var declaredDatatype: String { get } + class func fromDatatypeValue(datatypeValue: Datatype) -> Self + var datatypeValue: Datatype { get } +} +``` -> _Note:_ `Binding` is a protocol that SQLite.swift uses internally to directly map SQLite types to Swift types. **Do _not_** conform custom types to the `Binding` protocol. +The `Datatype` must be one of the basic Swift types that values are bridged +through before serialization and deserialization (see [Building Type-Safe SQL +](#building-type-safe-sql) for a list of types). -Once extended, the type can be used [_almost_](#custom-type-caveats) wherever typed expressions can be. +> ⚠ _Note:_ `Binding` is a protocol that SQLite.swift uses internally to +> directly map SQLite types to Swift types. **Do _not_** conform custom types +> to the `Binding` protocol. ### Date-Time Values -In SQLite, `DATETIME` columns can be treated as strings or numbers, so we can transparently bridge `Date` objects through Swift’s `String` or `Int` types. - -To serialize `Date` objects as `TEXT` values (in ISO 8601), we’ll use `String`. - -``` swift -extension Date: Value { - class var declaredDatatype: String { - return String.declaredDatatype - } - class func fromDatatypeValue(stringValue: String) -> Date { - return SQLDateFormatter.dateFromString(stringValue)! - } - var datatypeValue: String { - return SQLDateFormatter.stringFromDate(self) - } -} - -let SQLDateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - formatter.locale = Locale(localeIdentifier: "en_US_POSIX") - formatter.timeZone = TimeZone(forSecondsFromGMT: 0) - return formatter -}() -``` +In SQLite, `DATETIME` columns can be treated as strings or numbers, so we can +transparently bridge `Date` objects through Swift’s `String` types. -We can also treat them as `INTEGER` values using `Int`. +We can use these types directly in SQLite statements. -``` swift -extension Date: Value { - class var declaredDatatype: String { - return Int.declaredDatatype - } - class func fromDatatypeValue(intValue: Int) -> Self { - return self(timeIntervalSince1970: TimeInterval(intValue)) - } - var datatypeValue: Int { - return Int(timeIntervalSince1970) - } -} -``` - -> _Note:_ SQLite’s `CURRENT_DATE`, `CURRENT_TIME`, and `CURRENT_TIMESTAMP` helpers return `TEXT` values. Because of this (and the fact that Unix time is far less human-readable when we’re faced with the raw data), we recommend using the `TEXT` extension. - -Once defined, we can use these types directly in SQLite statements. - -``` swift +```swift let published_at = Expression("published_at") let published = posts.filter(published_at <= Date()) -// extension where Datatype == String: -// SELECT * FROM "posts" WHERE "published_at" <= '2014-11-18 12:45:30' -// extension where Datatype == Int: -// SELECT * FROM "posts" WHERE "published_at" <= 1416314730 +// SELECT * FROM "posts" WHERE "published_at" <= '2014-11-18T12:45:30.000' + +let startDate = Date(timeIntervalSince1970: 0) +let published = posts.filter(startDate...Date() ~= published_at) +// SELECT * FROM "posts" WHERE "published_at" BETWEEN '1970-01-01T00:00:00.000' AND '2014-11-18T12:45:30.000' ``` @@ -1242,7 +1433,7 @@ let published = posts.filter(published_at <= Date()) We can bridge any type that can be initialized from and encoded to `Data`. -``` swift +```swift extension UIImage: Value { public class var declaredDatatype: String { return Blob.declaredDatatype @@ -1257,55 +1448,130 @@ extension UIImage: Value { } ``` -> _Note:_ See the [Archives and Serializations Programming Guide](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html#//apple_ref/doc/uid/10000047i) for more information on encoding and decoding custom types. +> _Note:_ See the [Archives and Serializations Programming Guide][] for more +> information on encoding and decoding custom types. -### Custom Type Caveats +[Archives and Serializations Programming Guide]: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html -Swift does _not_ currently support generic subscripting, which means we cannot, by default, subscript Expressions with custom types to: +## Codable Types - 1. **Namespace expressions**. Use the `namespace` function, instead: +[Codable types][Encoding and Decoding Custom Types] were introduced as a part +of Swift 4 to allow serializing and deserializing types. SQLite.swift supports +the insertion, updating, and retrieval of basic Codable types. - ``` swift - let avatar = Expression("avatar") - users[avatar] // fails to compile - users.namespace(avatar) // "users"."avatar" - ``` +[Encoding and Decoding Custom Types]: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types - 2. **Access column data**. Use the `get` function, instead: +### Inserting Codable Types - ``` swift - let user = users.first! - user[avatar] // fails to compile - user.get(avatar) // UIImage? - ``` +Queries have a method to allow inserting an [Encodable][] type. + +```swift +struct User: Encodable { + let name: String +} +try db.run(users.insert(User(name: "test"))) -We can, of course, write extensions, but they’re rather wordy. +``` -``` swift -extension Query { - subscript(column: Expression) -> Expression { - return namespace(column) - } - subscript(column: Expression) -> Expression { - return namespace(column) - } +There are two other parameters also available to this method: + +- `userInfo` is a dictionary that is passed to the encoder and made available + to encodable types to allow customizing their behavior. + +- `otherSetters` allows you to specify additional setters on top of those + that are generated from the encodable types themselves. + +[Encodable]: https://developer.apple.com/documentation/swift/encodable + +### Updating Codable Types + +Queries have a method to allow updating an Encodable type. + +```swift +try db.run(users.filter(id == userId).update(user)) + +``` + +> ⚠ Unless filtered, using the update method on an instance of a Codable +> type updates all table rows. + +There are two other parameters also available to this method: + +- `userInfo` is a dictionary that is passed to the encoder and made available + to encodable types to allow customizing their behavior. + +- `otherSetters` allows you to specify additional setters on top of those + that are generated from the encodable types themselves. + +### Retrieving Codable Types + +Rows have a method to decode a [Decodable][] type. + +```swift +let loadedUsers: [User] = try db.prepare(users).map { row in + return try row.decode() } +``` -extension Row { - subscript(column: Expression) -> UIImage { - return get(column) - } - subscript(column: Expression) -> UIImage? { - return get(column) +You can also create a decoder to use manually yourself. This can be useful +for example if you are using the +[Facade pattern](https://en.wikipedia.org/wiki/Facade_pattern) to hide +subclasses behind a super class. For example, you may want to encode an Image +type that can be multiple different formats such as PNGImage, JPGImage, or +HEIFIamge. You will need to determine the correct subclass before you know +which type to decode. + +```swift +enum ImageCodingKeys: String, CodingKey { + case kind +} + +enum ImageKind: Int, Codable { + case png, jpg, heif +} + +let loadedImages: [Image] = try db.prepare(images).map { row in + let decoder = row.decoder() + let container = try decoder.container(keyedBy: ImageCodingKeys.self) + switch try container.decode(ImageKind.self, forKey: .kind) { + case .png: + return try PNGImage(from: decoder) + case .jpg: + return try JPGImage(from: decoder) + case .heif: + return try HEIFImage(from: decoder) } } ``` +Both of the above methods also have the following optional parameter: + +- `userInfo` is a dictionary that is passed to the decoder and made available + to decodable types to allow customizing their behavior. + +[Decodable]: https://developer.apple.com/documentation/swift/decodable + +### Restrictions + +There are a few restrictions on using Codable types: + +- The encodable and decodable objects can only use the following types: + - Int, Bool, Float, Double, String + - Nested Codable types that will be encoded as JSON to a single column +- These methods will not handle object relationships for you. You must write + your own Codable and Decodable implementations if you wish to support this. +- The Codable types may not try to access nested containers or nested unkeyed + containers +- The Codable types may not access single value containers or unkeyed + containers +- The Codable types may not access super decoders or encoders ## Other Operators -In addition to [filter operators](#filtering-infix-operators), SQLite.swift defines a number of operators that can modify expression values with arithmetic, bitwise operations, and concatenation. +In addition to [filter operators](#filtering-infix-operators), SQLite.swift +defines a number of operators that can modify expression values with +arithmetic, bitwise operations, and concatenation. ###### Other Infix Operators @@ -1323,7 +1589,8 @@ In addition to [filter operators](#filtering-infix-operators), SQLite.swift defi | `|` | `Int -> Int` | `|` | | `+` | `String -> String` | `||` | -> _Note:_ SQLite.swift also defines a bitwise XOR operator, `^`, which expands the expression `lhs ^ rhs` to `~(lhs & rhs) & (lhs | rhs)`. +> _Note:_ SQLite.swift also defines a bitwise XOR operator, `^`, which +> expands the expression `lhs ^ rhs` to `~(lhs & rhs) & (lhs | rhs)`. ###### Other Prefix Operators @@ -1336,27 +1603,46 @@ In addition to [filter operators](#filtering-infix-operators), SQLite.swift defi ## Core SQLite Functions -Many of SQLite’s [core functions](https://www.sqlite.org/lang_corefunc.html) have been surfaced in and type-audited for SQLite.swift. +Many of SQLite’s [core functions](https://www.sqlite.org/lang_corefunc.html) +have been surfaced in and type-audited for SQLite.swift. > _Note:_ SQLite.swift aliases the `??` operator to the `ifnull` function. > -> ``` swift +> ```swift > name ?? email // ifnull("name", "email") > ``` ## Aggregate SQLite Functions -Most of SQLite’s [aggregate functions](https://www.sqlite.org/lang_aggfunc.html) have been surfaced in and type-audited for SQLite.swift. +Most of SQLite’s +[aggregate functions](https://www.sqlite.org/lang_aggfunc.html) have been +surfaced in and type-audited for SQLite.swift. + +## Date and Time functions + +SQLite's [date and time](https://www.sqlite.org/lang_datefunc.html) +functions are available: +```swift +DateFunctions.date("now") +// date('now') +Date().date +// date('2007-01-09T09:41:00.000') +Expression("date").date +// date("date") +``` ## Custom SQL Functions -We can create custom SQL functions by calling `createFunction` on a database connection. +We can create custom SQL functions by calling `createFunction` on a database +connection. -For example, to give queries access to [`MobileCoreServices.UTTypeConformsTo`](https://developer.apple.com/library/ios/documentation/MobileCoreServices/Reference/UTTypeRef/index.html#//apple_ref/c/func/UTTypeConformsTo), we can write the following: +For example, to give queries access to +[`MobileCoreServices.UTTypeConformsTo`][UTTypeConformsTo], we can +write the following: -``` swift +```swift import MobileCoreServices let typeConformsTo: (Expression, Expression) -> Expression = ( @@ -1366,23 +1652,27 @@ let typeConformsTo: (Expression, Expression) -> Expression ) ``` -> _Note:_ The optional `deterministic` parameter is an optimization that causes the function to be created with [`SQLITE_DETERMINISTIC`](https://www.sqlite.org/c3ref/create_function.html). +> _Note:_ The optional `deterministic` parameter is an optimization that +> causes the function to be created with +> [`SQLITE_DETERMINISTIC`](https://www.sqlite.org/c3ref/c_deterministic.html). Note `typeConformsTo`’s signature: -``` swift +```swift (Expression, Expression) -> Expression ``` -Because of this, `createFunction` expects a block with the following signature: +Because of this, `createFunction` expects a block with the following +signature: -``` swift +```swift (String, String) -> Bool ``` -Once assigned, the closure can be called wherever boolean expressions are accepted. +Once assigned, the closure can be called wherever boolean expressions are +accepted. -``` swift +```swift let attachments = Table("attachments") let UTI = Expression("UTI") @@ -1390,38 +1680,44 @@ let images = attachments.filter(typeConformsTo(UTI, kUTTypeImage)) // SELECT * FROM "attachments" WHERE "typeConformsTo"("UTI", 'public.image') ``` -> _Note:_ The return type of a function must be [a core SQL type](#building-type-safe-sql) or [conform to `Value`](#custom-types). +> _Note:_ The return type of a function must be +> [a core SQL type](#building-type-safe-sql) or [conform to `Value`](#custom-types). -We can create loosely-typed functions by handling an array of raw arguments, instead. +We can create loosely-typed functions by handling an array of raw arguments, +instead. -``` swift +```swift db.createFunction("typeConformsTo", deterministic: true) { args in guard let UTI = args[0] as? String, conformsToUTI = args[1] as? String else { return nil } return UTTypeConformsTo(UTI, conformsToUTI) } ``` -Creating a loosely-typed function cannot return a closure and instead must be wrapped manually or executed [using raw SQL](#executing-arbitrary-sql). +Creating a loosely-typed function cannot return a closure and instead must be +wrapped manually or executed [using raw SQL](#executing-arbitrary-sql). -``` swift +```swift let stmt = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)") for row in stmt.bind(kUTTypeImage) { /* ... */ } ``` +[UTTypeConformsTo]: https://developer.apple.com/documentation/coreservices/1444079-uttypeconformsto ## Custom Collations -We can create custom collating sequences by calling `createCollation` on a database connection. +We can create custom collating sequences by calling `createCollation` on a +database connection. -``` swift +```swift try db.createCollation("NODIACRITIC") { lhs, rhs in return lhs.compare(rhs, options: .diacriticInsensitiveSearch) } ``` -We can reference a custom collation using the `Custom` member of the `Collation` enumeration. +We can reference a custom collation using the `Custom` member of the +`Collation` enumeration. -``` swift +```swift restaurants.order(collate(.custom("NODIACRITIC"), name)) // SELECT * FROM "restaurants" ORDER BY "name" COLLATE "NODIACRITIC" ``` @@ -1429,9 +1725,11 @@ restaurants.order(collate(.custom("NODIACRITIC"), name)) ## Full-text Search -We can create a virtual table using the [FTS4 module](http://www.sqlite.org/fts3.html) by calling `create` on a `VirtualTable`. +We can create a virtual table using the [FTS4 +module](http://www.sqlite.org/fts3.html) by calling `create` on a +`VirtualTable`. -``` swift +```swift let emails = VirtualTable("emails") let subject = Expression("subject") let body = Expression("body") @@ -1442,14 +1740,14 @@ try db.run(emails.create(.FTS4(subject, body))) We can specify a [tokenizer](http://www.sqlite.org/fts3.html#tokenizer) using the `tokenize` parameter. -``` swift +```swift try db.run(emails.create(.FTS4([subject, body], tokenize: .Porter))) // CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", tokenize=porter) ``` We can set the full range of parameters by creating a `FTS4Config` object. -``` swift +```swift let emails = VirtualTable("emails") let subject = Expression("subject") let body = Expression("body") @@ -1463,9 +1761,11 @@ try db.run(emails.create(.FTS4(config)) // CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", notindexed="body", languageid="lid", order="desc") ``` -Once we insert a few rows, we can search using the `match` function, which takes a table or column as its first argument and a query string as its second. +Once we insert a few rows, we can search using the `match` function, which +takes a table or column as its first argument and a query string as its +second. -``` swift +```swift try db.run(emails.insert( subject <- "Just Checking In", body <- "Hey, I was just wondering...did you get my last email?" @@ -1480,8 +1780,9 @@ let replies = emails.filter(subject.match("Re:*")) ### FTS5 -When linking against a version of SQLite with [FTS5](http://www.sqlite.org/fts5.html) enabled we can create the virtual table -in a similar fashion. +When linking against a version of SQLite with +[FTS5](http://www.sqlite.org/fts5.html) enabled we can create the virtual +table in a similar fashion. ```swift let emails = VirtualTable("emails") @@ -1504,45 +1805,53 @@ let replies = emails.filter(emails.match("subject:\"Re:\"*)) ## Executing Arbitrary SQL -Though we recommend you stick with SQLite.swift’s [type-safe system](#building-type-safe-sql) whenever possible, it is possible to simply and safely prepare and execute raw SQL statements via a `Database` connection using the following functions. +Though we recommend you stick with SQLite.swift’s +[type-safe system](#building-type-safe-sql) whenever possible, it is possible +to simply and safely prepare and execute raw SQL statements via a `Database` connection +using the following functions. - `execute` runs an arbitrary number of SQL statements as a convenience. - ``` swift - try db.execute( - "BEGIN TRANSACTION;" + - "CREATE TABLE users (" + - "id INTEGER PRIMARY KEY NOT NULL," + - "email TEXT UNIQUE NOT NULL," + - "name TEXT" + - ");" + - "CREATE TABLE posts (" + - "id INTEGER PRIMARY KEY NOT NULL," + - "title TEXT NOT NULL," + - "body TEXT NOT NULL," + - "published_at DATETIME" + - ");" + - "PRAGMA user_version = 1;" + - "COMMIT TRANSACTION;" + ```swift + try db.execute(""" + BEGIN TRANSACTION; + CREATE TABLE users ( + id INTEGER PRIMARY KEY NOT NULL, + email TEXT UNIQUE NOT NULL, + name TEXT + ); + CREATE TABLE posts ( + id INTEGER PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + published_at DATETIME + ); + PRAGMA user_version = 1; + COMMIT TRANSACTION; + """ ) ``` - - `prepare` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), and returns the statement for deferred execution. + - `prepare` prepares a single `Statement` object from a SQL string, + optionally binds values to it (using the statement’s `bind` function), + and returns the statement for deferred execution. - ``` swift + ```swift let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") ``` - Once prepared, statements may be executed using `run`, binding any unbound parameters. + Once prepared, statements may be executed using `run`, binding any + unbound parameters. - ``` swift + ```swift try stmt.run("alice@mac.com") db.changes // -> {Some 1} ``` - Statements with results may be iterated over, using the columnNames if useful. + Statements with results may be iterated over, using the columnNames if + useful. - ``` swift + ```swift let stmt = try db.prepare("SELECT id, email FROM users") for row in stmt { for (index, name) in stmt.columnNames.enumerate() { @@ -1552,21 +1861,26 @@ Though we recommend you stick with SQLite.swift’s [type-safe system](#building } ``` - - `run` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), executes, and returns the statement. + - `run` prepares a single `Statement` object from a SQL string, optionally + binds values to it (using the statement’s `bind` function), executes, + and returns the statement. - ``` swift + ```swift try db.run("INSERT INTO users (email) VALUES (?)", "alice@mac.com") ``` - - `scalar` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), executes, and returns the first value of the first row. + - `scalar` prepares a single `Statement` object from a SQL string, + optionally binds values to it (using the statement’s `bind` function), + executes, and returns the first value of the first row. - ``` swift + ```swift let count = try db.scalar("SELECT count(*) FROM users") as! Int64 ``` - Statements also have a `scalar` function, which can optionally re-bind values at execution. + Statements also have a `scalar` function, which can optionally re-bind + values at execution. - ``` swift + ```swift let stmt = try db.prepare("SELECT count (*) FROM users") let count = try stmt.scalar() as! Int64 ``` @@ -1576,7 +1890,7 @@ Though we recommend you stick with SQLite.swift’s [type-safe system](#building We can log SQL using the database’s `trace` function. -``` swift +```swift #if DEBUG db.trace { print($0) } #endif diff --git a/Documentation/Planning.md b/Documentation/Planning.md index d814d26b..8a3d5a11 100644 --- a/Documentation/Planning.md +++ b/Documentation/Planning.md @@ -1,7 +1,13 @@ # SQLite.swift Planning -This document captures both near term steps (aka Roadmap) and feature requests. -The goal is to add some visibility and guidance for future additions and Pull Requests, as well as to keep the Issues list clear of enhancement requests so that bugs are more visible. +This document captures both near term steps (aka Roadmap) and feature +requests. The goal is to add some visibility and guidance for future +additions and Pull Requests, as well as to keep the Issues list clear of +enhancement requests so that bugs are more visible. + +> ⚠ This document is currently not actively maintained. See +> the [0.12.0 milestone](https://github.com/stephencelis/SQLite.swift/issues?q=is%3Aopen+is%3Aissue+milestone%3A0.12.0) +> on Github for additional information about planned features for the next release. ## Roadmap @@ -9,16 +15,24 @@ _Lists agreed upon next steps in approximate priority order._ ## Feature Requests -_A gathering point for ideas for new features. In general, the corresponding issue will be closed once it is added here, with the assumption that it will be referred to when it comes time to add the corresponding feature._ +_A gathering point for ideas for new features. In general, the corresponding +issue will be closed once it is added here, with the assumption that it will +be referred to when it comes time to add the corresponding feature._ ### Features - * encapsulate ATTACH DATABASE / DETACH DATABASE as methods, per [#30](https://github.com/stephencelis/SQLite.swift/issues/30) - * provide separate threads for update vs read, so updates don't block reads, per [#236](https://github.com/stephencelis/SQLite.swift/issues/236) - * expose triggers, per [#164](https://github.com/stephencelis/SQLite.swift/issues/164) + * 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 + [#164](https://github.com/stephencelis/SQLite.swift/issues/164) ## Suspended Feature Requests -_Features that are not actively being considered, perhaps because of no clean type-safe way to implement them with the current Swift, or bugs, or just general uncertainty._ +_Features that are not actively being considered, perhaps because of no clean +type-safe way to implement them with the current Swift, or bugs, or just +general uncertainty._ - * provide a mechanism for INSERT INTO multiple values, per [#168](https://github.com/stephencelis/SQLite.swift/issues/168) + * provide a mechanism for INSERT INTO multiple values, per + [#168](https://github.com/stephencelis/SQLite.swift/issues/168) diff --git a/Makefile b/Makefile index adab1d81..ebd38494 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ BUILD_TOOL = xcodebuild BUILD_SCHEME = SQLite Mac IOS_SIMULATOR = iPhone 6s -IOS_VERSION = 10.3 +IOS_VERSION = 11.0 ifeq ($(BUILD_SCHEME),SQLite iOS) BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -destination "platform=iOS Simulator,name=$(IOS_SIMULATOR),OS=$(IOS_VERSION)" else diff --git a/Package.swift b/Package.swift index fc7c5a0e..c008e482 100644 --- a/Package.swift +++ b/Package.swift @@ -1,17 +1,24 @@ +// swift-tools-version:4.0 import PackageDescription let package = Package( - name: "SQLite", + name: "SQLite.swift", + products: [.library(name: "SQLite", targets: ["SQLite"])], targets: [ - Target( - name: "SQLite", - dependencies: [ - .Target(name: "SQLiteObjc") - ]), - Target(name: "SQLiteObjc") + .target(name: "SQLite", dependencies: ["SQLiteObjc"]), + .target(name: "SQLiteObjc"), + .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests") ], - dependencies: [ - .Package(url: "https://github.com/stephencelis/CSQLite.git", majorVersion: 0) - ], - exclude: ["Tests/CocoaPods", "Tests/Carthage"] + swiftLanguageVersions: [4] ) + +#if os(Linux) + package.dependencies = [.package(url: "https://github.com/stephencelis/CSQLite.git", from: "0.0.3")] + package.targets = [ + .target(name: "SQLite", exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"]), + .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests", exclude: [ + "FTS4Tests.swift", + "FTS5Tests.swift" + ]) + ] +#endif diff --git a/README.md b/README.md index e435bfed..3900c0a3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,12 @@ # SQLite.swift -[![Build Status][Badge]][Travis] [![CocoaPods Version](https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png)](http://cocoadocs.org/docsets/SQLite.swift) [![Swift](https://img.shields.io/badge/swift-3-orange.svg?style=flat)](https://developer.apple.com/swift/) [![Platform](https://cocoapod-badges.herokuapp.com/p/SQLite.swift/badge.png)](http://cocoadocs.org/docsets/SQLite.swift) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Join the chat at https://gitter.im/stephencelis/SQLite.swift](https://badges.gitter.im/stephencelis/SQLite.swift.svg)](https://gitter.im/stephencelis/SQLite.swift) +[![Build Status][TravisBadge]][TravisLink] [![CocoaPods Version][CocoaPodsVersionBadge]][CocoaPodsVersionLink] [![Swift4 compatible][Swift4Badge]][Swift4Link] [![Platform][PlatformBadge]][PlatformLink] [![Carthage compatible][CartagheBadge]][CarthageLink] [![Join the chat at https://gitter.im/stephencelis/SQLite.swift][GitterBadge]][GitterLink] A type-safe, [Swift][]-language layer over [SQLite3][]. [SQLite.swift][] provides compile-time confidence in SQL statement syntax _and_ intent. -[Badge]: https://img.shields.io/travis/stephencelis/SQLite.swift/master.svg?style=flat -[Travis]: https://travis-ci.org/stephencelis/SQLite.swift -[Swift]: https://developer.apple.com/swift/ -[SQLite3]: http://www.sqlite.org -[SQLite.swift]: https://github.com/stephencelis/SQLite.swift - - ## Features - A pure-Swift interface @@ -25,16 +18,20 @@ syntax _and_ intent. - [Full-text search][] support - [Well-documented][See Documentation] - Extensively tested - - SQLCipher support via CocoaPods - - Active support at [StackOverflow](http://stackoverflow.com/questions/tagged/sqlite.swift), and [Gitter Chat Room](https://gitter.im/stephencelis/SQLite.swift) (_experimental_) + - [SQLCipher][] support via CocoaPods + - Active support at + [StackOverflow](http://stackoverflow.com/questions/tagged/sqlite.swift), + and [Gitter Chat Room](https://gitter.im/stephencelis/SQLite.swift) + (_experimental_) +[SQLCipher]: https://www.zetetic.net/sqlcipher/ [Full-text search]: Documentation/Index.md#full-text-search [See Documentation]: Documentation/Index.md#sqliteswift-documentation ## Usage -``` swift +```swift import SQLite let db = try Connection("path/to/db.sqlite3") @@ -74,14 +71,14 @@ try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) try db.run(alice.delete()) // DELETE FROM "users" WHERE ("id" = 1) -db.scalar(users.count) // 0 +try db.scalar(users.count) // 0 // SELECT count(*) FROM "users" ``` SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C API. -``` swift +```swift let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") for email in ["betty@icloud.com", "cathy@icloud.com"] { try stmt.run(email) @@ -105,11 +102,17 @@ interactively, from the Xcode project’s playground. ![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) -For a more comprehensive example, see [this article](http://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html) and the [companion repository](https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master). +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]: http://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html +[SQLiteDataAccessLayer2]: https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master ## Installation -> _Note:_ SQLite.swift requires Swift 3 (and [Xcode][] 8). Use the [swift-4][] branch for Xcode 9 Beta. +> _Note:_ SQLite.swift requires Swift 4 (and [Xcode][] 9). ### Carthage @@ -120,11 +123,12 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: - ``` - github "stephencelis/SQLite.swift" ~> 0.11.3 + ```ruby + github "stephencelis/SQLite.swift" ~> 0.11.4 ``` - 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. + 3. Run `carthage update` and + [add the appropriate framework][Carthage Usage]. [Carthage]: https://github.com/Carthage/Carthage @@ -137,51 +141,47 @@ install SQLite.swift with Carthage: [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: - 1. Verify that your copy of Xcode is installed and active in the default location (`/Applications/Xcode.app`). + 1. Make sure CocoaPods is [installed][CocoaPods Installation]. (SQLite.swift + requires version 1.0.0 or greater.) ```sh - sudo xcode-select --switch /Applications/Xcode.app - ``` - - 2. Make sure CocoaPods is [installed][CocoaPods Installation]. (SQLite.swift requires version 1.0.0 or greater.) - - ``` sh # Using the default Ruby install will require you to use sudo when # installing and updating gems. [sudo] gem install cocoapods ``` - 3. Update your Podfile to include the following: + 2. Update your Podfile to include the following: - ``` ruby + ```ruby use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.11.3' + pod 'SQLite.swift', '~> 0.11.4' end ``` - 4. Run `pod install --repo-update`. + 3. Run `pod install --repo-update`. [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started ### Swift Package Manager -The [Swift Package Manager][] is a tool for managing the distribution of Swift code. +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. 1. Add the following to your `Package.swift` file: ```swift dependencies: [ - .Package(url: "https://github.com/stephencelis/SQLite.swift.git", majorVersion: 0, minor: 11) + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.11.4") ] ``` 2. Build your project: - ``` sh - $ swift build -Xlinker -lsqlite3 + ```sh + $ swift build ``` [Swift Package Manager]: https://swift.org/package-manager @@ -202,9 +202,11 @@ To install SQLite.swift as an Xcode sub-project: 4. **Add**. -Some additional steps are required to install the application on an actual device: +Some additional steps are required to install the application on an actual +device: - 5. In the **General** tab, click the **+** button under **Embedded Binaries**. + 5. In the **General** tab, click the **+** button under **Embedded + Binaries**. 6. Select the appropriate **SQLite.framework** for your platform. @@ -249,7 +251,8 @@ file](./LICENSE.txt) for more information. These projects enhance or use SQLite.swift: - - [SQLiteMigrationManager.swift](https://github.com/garriguv/SQLiteMigrationManager.swift) (inspired by [FMDBMigrationManager](https://github.com/layerhq/FMDBMigrationManager)) + - [SQLiteMigrationManager.swift][] (inspired by + [FMDBMigrationManager][]) ## Alternatives @@ -263,5 +266,28 @@ Looking for something else? Try another Swift wrapper (or [FMDB][]): - [SwiftData](https://github.com/ryanfowler/SwiftData) - [SwiftSQLite](https://github.com/chrismsimpson/SwiftSQLite) +[Swift]: https://swift.org/ +[SQLite3]: http://www.sqlite.org +[SQLite.swift]: https://github.com/stephencelis/SQLite.swift + +[TravisBadge]: https://img.shields.io/travis/stephencelis/SQLite.swift/master.svg?style=flat +[TravisLink]: https://travis-ci.org/stephencelis/SQLite.swift + +[CocoaPodsVersionBadge]: https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png +[CocoaPodsVersionLink]: http://cocoadocs.org/docsets/SQLite.swift + +[PlatformBadge]: https://cocoapod-badges.herokuapp.com/p/SQLite.swift/badge.png +[PlatformLink]: http://cocoadocs.org/docsets/SQLite.swift + +[CartagheBadge]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat +[CarthageLink]: https://github.com/Carthage/Carthage + +[GitterBadge]: https://badges.gitter.im/stephencelis/SQLite.swift.svg +[GitterLink]: https://gitter.im/stephencelis/SQLite.swift + +[Swift4Badge]: https://img.shields.io/badge/swift-4-orange.svg?style=flat +[Swift4Link]: https://developer.apple.com/swift/ + +[SQLiteMigrationManager.swift]: https://github.com/garriguv/SQLiteMigrationManager.swift [FMDB]: https://github.com/ccgus/fmdb -[swift-4]: https://github.com/stephencelis/SQLite.swift/tree/swift-4 +[FMDBMigrationManager]: https://github.com/layerhq/FMDBMigrationManager diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec index 06aa043b..4a329338 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.11.3" + s.version = "0.11.4" s.summary = "A type-safe, Swift-language layer over SQLite3 for iOS and OS X." s.description = <<-DESC @@ -15,46 +15,41 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/stephencelis' s.module_name = 'SQLite' - s.ios.deployment_target = "9.0" + s.ios.deployment_target = "8.0" s.tvos.deployment_target = "9.1" s.osx.deployment_target = "10.10" s.watchos.deployment_target = "2.2" s.default_subspec = 'standard' s.pod_target_xcconfig = { - 'SWIFT_VERSION' => '3.0', + 'SWIFT_VERSION' => '4.0', } s.subspec 'standard' do |ss| ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' ss.exclude_files = 'Sources/**/Cipher.swift' ss.private_header_files = 'Sources/SQLiteObjc/*.h' - ss.library = 'sqlite3' - ss.preserve_paths = 'CocoaPods/**/*' - ss.pod_target_xcconfig = { - 'SWIFT_INCLUDE_PATHS[sdk=macosx*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx', - 'SWIFT_INCLUDE_PATHS[sdk=macosx10.11]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx-10.11', - 'SWIFT_INCLUDE_PATHS[sdk=macosx10.12]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx-10.12', - 'SWIFT_INCLUDE_PATHS[sdk=iphoneos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphoneos', - 'SWIFT_INCLUDE_PATHS[sdk=iphoneos10.0]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphoneos-10.0', - 'SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphonesimulator', - 'SWIFT_INCLUDE_PATHS[sdk=iphonesimulator10.0]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphonesimulator-10.0', - 'SWIFT_INCLUDE_PATHS[sdk=appletvos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/appletvos', - 'SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/appletvsimulator', - 'SWIFT_INCLUDE_PATHS[sdk=watchos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/watchos', - 'SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/watchsimulator' - } + + ss.test_spec 'tests' do |test_spec| + test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.source_files = 'Tests/SQLiteTests/*.swift' + end end s.subspec 'standalone' do |ss| ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' ss.exclude_files = 'Sources/**/Cipher.swift' ss.private_header_files = 'Sources/SQLiteObjc/*.h' + ss.xcconfig = { 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE' } + ss.dependency 'sqlite3' - ss.dependency 'sqlite3', '>= 3.14.0' + ss.test_spec 'tests' do |test_spec| + test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.source_files = 'Tests/SQLiteTests/*.swift' + end end s.subspec 'SQLCipher' do |ss| @@ -64,7 +59,11 @@ Pod::Spec.new do |s| 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_SQLCIPHER', 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1' } - ss.dependency 'SQLCipher', '>= 3.4.0' + + ss.test_spec 'tests' do |test_spec| + test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.source_files = 'Tests/SQLiteTests/*.swift' + end end end diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index 78b43249..cb776608 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; - 03A65E711C6BB2CD0062603F /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; @@ -47,27 +46,41 @@ 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; }; 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A171967CC511C4F6F773C9 /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A1720B67ED13E6150C6A3D /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; + 19A172EB202970561E5C4245 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; + 19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; + 19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A17490543609FCED53CACC /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; 19A175DFF47B84757E547C62 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; 19A178072B371489E6A1E839 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; 19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A1785195182AF8731A8BDA /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; + 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 19A179A0C45377CB09BB358C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; + 19A17C80076860CF7751A056 /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; + 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; 19A17FDA323BAFDEC627E76F /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 3D67B3E61DB2469200A4F4C6 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */; }; 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; @@ -88,10 +101,13 @@ 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; - 3D67B3FA1DB2470600A4F4C6 /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + 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 */; }; 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 */; }; @@ -164,10 +180,8 @@ EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; - EE91808C1C46E34A0038162A /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE9180901C46E8980038162A /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE9180941C46EA210038162A /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180931C46EA210038162A /* libsqlite3.tbd */; }; EE9180951C46EBCC0038162A /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180911C46E9D30038162A /* libsqlite3.tbd */; }; /* End PBXBuildFile section */ @@ -200,21 +214,19 @@ 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; }; + 19A1710E73A46D5AC721CDA9 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 19A1721B8984686B9963B45D /* FTS5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tests.swift; sourceTree = ""; }; + 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctionTests.swift; sourceTree = ""; }; 19A1730E4390C775C25677D1 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = ""; }; 19A17399EA9E61235D5D77BF /* CipherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherTests.swift; sourceTree = ""; }; + 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowTests.swift; sourceTree = ""; }; 19A178A39ACA9667A62663CC /* Cipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; }; 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationTests.swift; sourceTree = ""; }; 19A17B93B48B5560E6E51791 /* Fixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; + 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctions.swift; sourceTree = ""; }; 19A17E2695737FAB5D6086E3 /* fixtures */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = folder; path = fixtures; sourceTree = ""; }; - 39548A631CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A651CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A671CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A691CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A6B1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A6D1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - 39548A6F1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; 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; }; + 49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -265,7 +277,6 @@ EE247B911C3F822500AE3E12 /* installation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "installation@2x.png"; sourceTree = ""; }; EE247B921C3F822600AE3E12 /* playground@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playground@2x.png"; sourceTree = ""; }; EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SQLite.swift.podspec; sourceTree = ""; }; - EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = usr/include/sqlite3.h; sourceTree = SDKROOT; }; EE91808D1C46E5230038162A /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SQLite-Bridging.h"; path = "../../SQLiteObjc/include/SQLite-Bridging.h"; sourceTree = ""; }; EE9180911C46E9D30038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; EE9180931C46EA210038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; @@ -331,76 +342,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 39548A611CA63C740003E3B5 /* CocoaPods */ = { - isa = PBXGroup; - children = ( - 39548A621CA63C740003E3B5 /* appletvos */, - 39548A641CA63C740003E3B5 /* appletvsimulator */, - 39548A661CA63C740003E3B5 /* iphoneos */, - 39548A681CA63C740003E3B5 /* iphonesimulator */, - 39548A6A1CA63C740003E3B5 /* macosx */, - 39548A6C1CA63C740003E3B5 /* watchos */, - 39548A6E1CA63C740003E3B5 /* watchsimulator */, - ); - path = CocoaPods; - sourceTree = ""; - }; - 39548A621CA63C740003E3B5 /* appletvos */ = { - isa = PBXGroup; - children = ( - 39548A631CA63C740003E3B5 /* module.modulemap */, - ); - path = appletvos; - sourceTree = ""; - }; - 39548A641CA63C740003E3B5 /* appletvsimulator */ = { - isa = PBXGroup; - children = ( - 39548A651CA63C740003E3B5 /* module.modulemap */, - ); - path = appletvsimulator; - sourceTree = ""; - }; - 39548A661CA63C740003E3B5 /* iphoneos */ = { - isa = PBXGroup; - children = ( - 39548A671CA63C740003E3B5 /* module.modulemap */, - ); - path = iphoneos; - sourceTree = ""; - }; - 39548A681CA63C740003E3B5 /* iphonesimulator */ = { - isa = PBXGroup; - children = ( - 39548A691CA63C740003E3B5 /* module.modulemap */, - ); - path = iphonesimulator; - sourceTree = ""; - }; - 39548A6A1CA63C740003E3B5 /* macosx */ = { - isa = PBXGroup; - children = ( - 39548A6B1CA63C740003E3B5 /* module.modulemap */, - ); - path = macosx; - sourceTree = ""; - }; - 39548A6C1CA63C740003E3B5 /* watchos */ = { - isa = PBXGroup; - children = ( - 39548A6D1CA63C740003E3B5 /* module.modulemap */, - ); - path = watchos; - sourceTree = ""; - }; - 39548A6E1CA63C740003E3B5 /* watchsimulator */ = { - isa = PBXGroup; - children = ( - 39548A6F1CA63C740003E3B5 /* module.modulemap */, - ); - path = watchsimulator; - sourceTree = ""; - }; 3D67B3E41DB2469200A4F4C6 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -439,7 +380,6 @@ EE247AD51C3F04ED00AE3E12 /* SQLite */ = { isa = PBXGroup; children = ( - EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */, EE247AD61C3F04ED00AE3E12 /* SQLite.h */, EE247AF71C3F06E900AE3E12 /* Foundation.swift */, EE247AF81C3F06E900AE3E12 /* Helpers.swift */, @@ -476,6 +416,8 @@ 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */, 19A17399EA9E61235D5D77BF /* CipherTests.swift */, 19A17B93B48B5560E6E51791 /* Fixtures.swift */, + 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */, + 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */, ); name = SQLiteTests; path = Tests/SQLiteTests; @@ -491,6 +433,7 @@ EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */, EE247AF21C3F06E900AE3E12 /* Statement.swift */, EE247AF31C3F06E900AE3E12 /* Value.swift */, + 19A1710E73A46D5AC721CDA9 /* Errors.swift */, ); path = Core; sourceTree = ""; @@ -518,6 +461,8 @@ EE247B001C3F06E900AE3E12 /* Query.swift */, EE247B011C3F06E900AE3E12 /* Schema.swift */, EE247B021C3F06E900AE3E12 /* Setter.swift */, + 49EB68C31F7B3CB400D89D40 /* Coding.swift */, + 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */, ); path = Typed; sourceTree = ""; @@ -525,7 +470,6 @@ EE247B8A1C3F81D000AE3E12 /* Metadata */ = { isa = PBXGroup; children = ( - 39548A611CA63C740003E3B5 /* CocoaPods */, EE247B771C3F40D700AE3E12 /* README.md */, EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, @@ -566,7 +510,6 @@ files = ( 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */, 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */, - 03A65E711C6BB2CD0062603F /* usr/include/sqlite3.h in Headers */, 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -575,7 +518,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3D67B3FA1DB2470600A4F4C6 /* usr/include/sqlite3.h in Headers */, 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */, 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */, @@ -588,7 +530,6 @@ files = ( EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */, EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */, - EE91808C1C46E34A0038162A /* usr/include/sqlite3.h in Headers */, EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -597,7 +538,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE9180901C46E8980038162A /* usr/include/sqlite3.h in Headers */, EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */, EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */, @@ -740,32 +680,35 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0900; TargetAttributes = { 03A65E591C6BB0F50062603F = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; 03A65E621C6BB0F60062603F = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0900; }; A121AC441CA35C79005A31D1 = { CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0900; }; EE247AD21C3F04ED00AE3E12 = { CreatedOnToolsVersion = 7.2; - LastSwiftMigration = 0800; + LastSwiftMigration = 0900; }; EE247ADC1C3F04ED00AE3E12 = { CreatedOnToolsVersion = 7.2; - LastSwiftMigration = 0800; + LastSwiftMigration = 0900; }; EE247B3B1C3F3ED000AE3E12 = { CreatedOnToolsVersion = 7.2; - LastSwiftMigration = 0800; + LastSwiftMigration = 0900; }; EE247B441C3F3ED000AE3E12 = { CreatedOnToolsVersion = 7.2; - LastSwiftMigration = 0800; + LastSwiftMigration = 0900; }; }; }; @@ -853,6 +796,7 @@ buildActionMask = 2147483647; files = ( 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */, + 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */, @@ -872,6 +816,8 @@ 03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */, 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */, 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */, + 19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */, + 19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -898,6 +844,8 @@ 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -906,6 +854,7 @@ buildActionMask = 2147483647; files = ( 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */, + 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */, 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */, 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */, 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */, @@ -925,6 +874,8 @@ 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */, 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */, 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */, + 19A17DC282E36C4F41AA440B /* Errors.swift in Sources */, + 19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -933,6 +884,7 @@ buildActionMask = 2147483647; files = ( EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */, EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */, @@ -952,6 +904,8 @@ EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */, 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */, 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */, + 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */, + 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -978,6 +932,8 @@ 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -986,6 +942,7 @@ buildActionMask = 2147483647; files = ( EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */, @@ -1005,6 +962,8 @@ EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */, 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */, 19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */, + 19A17490543609FCED53CACC /* Errors.swift in Sources */, + 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1031,6 +990,8 @@ 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1073,7 +1034,8 @@ SKIP_INSTALL = YES; "SWIFT_INCLUDE_PATHS[sdk=appletvos*]" = "$(SRCROOT)/CocoaPods/appletvos"; "SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]" = "$(SRCROOT)/CocoaPods/appletvsimulator"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 9.1; }; name = Debug; @@ -1096,7 +1058,8 @@ SKIP_INSTALL = YES; "SWIFT_INCLUDE_PATHS[sdk=appletvos*]" = "$(SRCROOT)/CocoaPods/appletvos"; "SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]" = "$(SRCROOT)/CocoaPods/appletvsimulator"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 9.1; }; name = Release; @@ -1109,7 +1072,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 9.1; }; name = Debug; @@ -1122,7 +1086,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 9.1; }; name = Release; @@ -1146,7 +1111,8 @@ SKIP_INSTALL = YES; "SWIFT_INCLUDE_PATHS[sdk=watchos*]" = "$(SRCROOT)/CocoaPods/watchos"; "SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]" = "$(SRCROOT)/CocoaPods/watchsimulator"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 4; WATCHOS_DEPLOYMENT_TARGET = 2.2; }; @@ -1171,7 +1137,8 @@ SKIP_INSTALL = YES; "SWIFT_INCLUDE_PATHS[sdk=watchos*]" = "$(SRCROOT)/CocoaPods/watchos"; "SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]" = "$(SRCROOT)/CocoaPods/watchsimulator"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 4; WATCHOS_DEPLOYMENT_TARGET = 2.2; }; @@ -1185,14 +1152,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -1237,14 +1210,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -1287,7 +1266,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; @@ -1297,7 +1276,8 @@ "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]" = "$(SRCROOT)/CocoaPods/iphonesimulator"; "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator10.0]" = "$(SRCROOT)/CocoaPods/iphonesimulator-10.0"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1313,7 +1293,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; @@ -1322,7 +1302,8 @@ "SWIFT_INCLUDE_PATHS[sdk=iphoneos10.0]" = "$(SRCROOT)/CocoaPods/iphoneos-10.0"; "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]" = "$(SRCROOT)/CocoaPods/iphonesimulator"; "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator10.0]" = "$(SRCROOT)/CocoaPods/iphonesimulator-10.0"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1334,7 +1315,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1346,7 +1328,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1373,7 +1356,8 @@ "SWIFT_INCLUDE_PATHS[sdk=macosx*]" = "$(SRCROOT)/CocoaPods/macosx"; "SWIFT_INCLUDE_PATHS[sdk=macosx10.11]" = "$(SRCROOT)/CocoaPods/macosx-10.11"; "SWIFT_INCLUDE_PATHS[sdk=macosx10.12]" = "$(SRCROOT)/CocoaPods/macosx-10.12"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1400,7 +1384,8 @@ "SWIFT_INCLUDE_PATHS[sdk=macosx*]" = "$(SRCROOT)/CocoaPods/macosx"; "SWIFT_INCLUDE_PATHS[sdk=macosx10.11]" = "$(SRCROOT)/CocoaPods/macosx-10.11"; "SWIFT_INCLUDE_PATHS[sdk=macosx10.12]" = "$(SRCROOT)/CocoaPods/macosx-10.12"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1415,7 +1400,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1430,7 +1416,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 4.0; }; name = Release; }; diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme index 606b5a10..2c3c431f 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme @@ -1,6 +1,6 @@ @@ -56,6 +57,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme index 71ba5f16..cfa4e0fa 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite tvOS.xcscheme @@ -1,6 +1,6 @@ Void) throws { + public func transaction(_ mode: TransactionMode = .deferred, block: () throws -> Void) throws { try transaction("BEGIN \(mode.rawValue) TRANSACTION", block, "COMMIT TRANSACTION", or: "ROLLBACK TRANSACTION") } @@ -348,23 +350,23 @@ public final class Connection { /// The block must throw to roll the savepoint back. /// /// - Throws: `SQLite.Result.Error`, and rethrows. - public func savepoint(_ name: String = UUID().uuidString, block: @escaping () throws -> Void) throws { + public func savepoint(_ name: String = UUID().uuidString, block: () throws -> Void) throws { let name = name.quote("'") let savepoint = "SAVEPOINT \(name)" try transaction(savepoint, block, "RELEASE \(savepoint)", or: "ROLLBACK TO \(savepoint)") } - fileprivate func transaction(_ begin: String, _ block: @escaping () throws -> Void, _ commit: String, or rollback: String) throws { + fileprivate func transaction(_ begin: String, _ block: () throws -> Void, _ commit: String, or rollback: String) throws { return try sync { try self.run(begin) do { try block() + try self.run(commit) } catch { try self.run(rollback) throw error } - try self.run(commit) } } @@ -413,7 +415,7 @@ public final class Connection { /// /// db.trace { SQL in print(SQL) } public func trace(_ callback: ((String) -> Void)?) { - #if SQLITE_SWIFT_SQLCIPHER + #if SQLITE_SWIFT_SQLCIPHER || os(Linux) trace_v1(callback) #else if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { @@ -583,9 +585,11 @@ public final class Connection { } } var flags = SQLITE_UTF8 + #if !os(Linux) if deterministic { flags |= SQLITE_DETERMINISTIC } + #endif sqlite3_create_function_v2(handle, function, Int32(argc), flags, unsafeBitCast(box, to: UnsafeMutableRawPointer.self), { context, argc, value in let function = unsafeBitCast(sqlite3_user_data(context), to: Function.self) function(context, argc, value) @@ -626,29 +630,12 @@ public final class Connection { // MARK: - Error Handling - func sync(_ block: @escaping () throws -> T) rethrows -> T { - var success: T? - var failure: Error? - - let box: () -> Void = { - do { - success = try block() - } catch { - failure = error - } - } - + func sync(_ block: () throws -> T) rethrows -> T { if DispatchQueue.getSpecific(key: Connection.queueKey) == queueContext { - box() + return try block() } else { - queue.sync(execute: box) // FIXME: rdar://problem/21389236 + return try queue.sync(execute: block) } - - if let failure = failure { - try { () -> Void in throw failure }() - } - - return success! } @discardableResult func check(_ resultCode: Int32, statement: Statement? = nil) throws -> Int32 { @@ -694,6 +681,13 @@ public enum Result : Error { fileprivate static let successCodes: Set = [SQLITE_OK, SQLITE_ROW, SQLITE_DONE] + /// Represents a SQLite specific [error code](https://sqlite.org/rescode.html) + /// + /// - message: English-language text that describes the error + /// + /// - code: SQLite [error code](https://sqlite.org/rescode.html#primary_result_code_list) + /// + /// - statement: the statement which produced the error case error(message: String, code: Int32, statement: Statement?) init?(errorCode: Int32, connection: Connection, statement: Statement? = nil) { @@ -719,7 +713,7 @@ extension Result : CustomStringConvertible { } } -#if !SQLITE_SWIFT_SQLCIPHER +#if !SQLITE_SWIFT_SQLCIPHER && !os(Linux) @available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) extension Connection { fileprivate func trace_v2(_ callback: ((String) -> Void)?) { diff --git a/Sources/SQLite/Core/Errors.swift b/Sources/SQLite/Core/Errors.swift new file mode 100644 index 00000000..3cd7ae9f --- /dev/null +++ b/Sources/SQLite/Core/Errors.swift @@ -0,0 +1,21 @@ +import Foundation + +public enum QueryError: Error, CustomStringConvertible { + case noSuchTable(name: String) + case noSuchColumn(name: String, columns: [String]) + case ambiguousColumn(name: String, similar: [String]) + case unexpectedNullValue(name: String) + + public var description: String { + switch self { + case .noSuchTable(let name): + return "No such table: \(name)" + case .noSuchColumn(let name, let columns): + return "No such column `\(name)` in columns \(columns)" + case .ambiguousColumn(let name, let similar): + return "Ambiguous column `\(name)` (please disambiguate: \(similar))" + case .unexpectedNullValue(let name): + return "Unexpected null value for column `\(name)`" + } + } +} diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index a9232bba..dc91d3d8 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -26,8 +26,10 @@ import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif SWIFT_PACKAGE || COCOAPODS +#elseif os(Linux) import CSQLite +#else +import SQLite3 #endif /// A single SQL statement. @@ -200,12 +202,30 @@ extension Statement : Sequence { } -extension Statement : IteratorProtocol { +public protocol FailableIterator : IteratorProtocol { + func failableNext() throws -> Self.Element? +} - public func next() -> [Binding?]? { - return try! step() ? Array(row) : nil +extension FailableIterator { + public func next() -> Element? { + return try! failableNext() } +} +extension Array { + public init(_ failableIterator: I) throws where I.Element == Element { + self.init() + while let row = try failableIterator.failableNext() { + append(row) + } + } +} + +extension Statement : FailableIterator { + public typealias Element = [Binding?] + public func failableNext() throws -> [Binding?]? { + return try step() ? Array(row) : nil + } } extension Statement : CustomStringConvertible { diff --git a/Sources/SQLite/Extensions/FTS4.swift b/Sources/SQLite/Extensions/FTS4.swift index 3f28d33b..5ef84dd7 100644 --- a/Sources/SQLite/Extensions/FTS4.swift +++ b/Sources/SQLite/Extensions/FTS4.swift @@ -151,13 +151,18 @@ extension Connection { guard let (token, range) = next(string) else { return nil } - let view = string.utf8 - offset.pointee += Int32(string.substring(to: range.lowerBound).utf8.count) - length.pointee = Int32(view.distance(from: range.lowerBound.samePosition(in: view), to: range.upperBound.samePosition(in: view))) - return token + 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..… - -extension QueryType { - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - -} - -extension Row { - - public subscript(column: Expression) -> Data { - return get(column) - } - public subscript(column: Expression) -> Data? { - return get(column) - } - - public subscript(column: Expression) -> Date { - return get(column) - } - public subscript(column: Expression) -> Date? { - return get(column) - } - -} diff --git a/Sources/SQLite/Helpers.swift b/Sources/SQLite/Helpers.swift index 50b21d0d..64e5eca8 100644 --- a/Sources/SQLite/Helpers.swift +++ b/Sources/SQLite/Helpers.swift @@ -26,8 +26,10 @@ import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif SWIFT_PACKAGE || COCOAPODS +#elseif os(Linux) import CSQLite +#else +import SQLite3 #endif public typealias Star = (Expression?, Expression?) -> Expression diff --git a/Sources/SQLite/Info.plist b/Sources/SQLite/Info.plist index 8c55becd..7347d842 100644 --- a/Sources/SQLite/Info.plist +++ b/Sources/SQLite/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.11.3 + 0.11.4 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/SQLite/Typed/Coding.swift b/Sources/SQLite/Typed/Coding.swift new file mode 100644 index 00000000..dd6a4ec2 --- /dev/null +++ b/Sources/SQLite/Typed/Coding.swift @@ -0,0 +1,340 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension QueryType { + /// Creates an `INSERT` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `INSERT` statement fort the encodable object + public func insert(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.insert(encoder.setters + otherSetters) + } + + /// Creates an `UPDATE` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `UPDATE` statement fort the encodable object + public func update(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Update { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.update(encoder.setters + otherSetters) + } +} + +extension Row { + /// Decode an object from this row + /// This method expects any custom nested types to be in the form of JSON data and does not handle + /// any sort of object relationships. If you want to support relationships between objects you will + /// have to provide your own Decodable implementations that decodes the correct columns. + /// + /// - Parameter: userInfo + /// + /// - Returns: a decoded object from this row + public func decode(userInfo: [CodingUserInfoKey: Any] = [:]) throws -> V { + return try V(from: self.decoder(userInfo: userInfo)) + } + + public func decoder(userInfo: [CodingUserInfoKey: Any] = [:]) -> Decoder { + return SQLiteDecoder(row: self, userInfo: userInfo) + } +} + +/// Generates a list of settings for an Encodable object +fileprivate class SQLiteEncoder: Encoder { + class SQLiteKeyedEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = MyKey + + let encoder: SQLiteEncoder + let codingPath: [CodingKey] = [] + + init(encoder: SQLiteEncoder) { + self.encoder = encoder + } + + func superEncoder() -> Swift.Encoder { + fatalError("SQLiteEncoding does not support super encoders") + } + + func superEncoder(forKey key: Key) -> Swift.Encoder { + fatalError("SQLiteEncoding does not support super encoders") + } + + func encodeNil(forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- nil) + } + + func encode(_ value: Int, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: Bool, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: Float, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- Double(value)) + } + + func encode(_ value: Double, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: String, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: T, forKey key: Key) throws where T : Swift.Encodable { + if let data = value as? Data { + self.encoder.setters.append(Expression(key.stringValue) <- data) + } + else { + let encoded = try JSONEncoder().encode(value) + let string = String(data: encoded, encoding: .utf8) + self.encoder.setters.append(Expression(key.stringValue) <- string) + } + } + + func encode(_ value: Int8, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int8 is not supported")) + } + + func encode(_ value: Int16, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int16 is not supported")) + } + + func encode(_ value: Int32, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int32 is not supported")) + } + + func encode(_ value: Int64, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int64 is not supported")) + } + + func encode(_ value: UInt, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt is not supported")) + } + + func encode(_ value: UInt8, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt8 is not supported")) + } + + func encode(_ value: UInt16, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt16 is not supported")) + } + + func encode(_ value: UInt32, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt32 is not supported")) + } + + func encode(_ value: UInt64, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt64 is not supported")) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError("encoding a nested container is not supported") + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + fatalError("encoding nested values is not supported") + } + } + + fileprivate var setters: [SQLite.Setter] = [] + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey: Any] + + init(userInfo: [CodingUserInfoKey: Any]) { + self.userInfo = userInfo + } + + func singleValueContainer() -> SingleValueEncodingContainer { + fatalError("not supported") + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("not supported") + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + return KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self)) + } +} + +fileprivate class SQLiteDecoder : Decoder { + class SQLiteKeyedDecodingContainer : KeyedDecodingContainerProtocol { + typealias Key = MyKey + + let codingPath: [CodingKey] = [] + let row: Row + + init(row: Row) { + self.row = row + } + + var allKeys: [Key] { + return self.row.columnNames.keys.flatMap({Key(stringValue: $0)}) + } + + func contains(_ key: Key) -> Bool { + return self.row.hasValue(for: key.stringValue) + } + + func decodeNil(forKey key: Key) throws -> Bool { + return !self.contains(key) + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int8 is not supported")) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int16 is not supported")) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int32 is not supported")) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt64 is not supported")) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt is not supported")) + + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt8 is not supported")) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt16 is not supported")) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt32 is not supported")) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt64 is not supported")) + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + return Float(try self.row.get(Expression(key.stringValue))) + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Swift.Decodable { + if type == Data.self { + let data = try self.row.get(Expression(key.stringValue)) + return data as! T + } + guard let JSONString = try self.row.get(Expression(key.stringValue)) else { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "an unsupported type was found")) + } + guard let data = JSONString.data(using: .utf8) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "invalid utf8 data found")) + } + return try JSONDecoder().decode(type, from: data) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding nested containers is not supported")) + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding unkeyed containers is not supported")) + } + + func superDecoder() throws -> Swift.Decoder { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding super encoders containers is not supported")) + } + + func superDecoder(forKey key: Key) throws -> Swift.Decoder { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding super decoders is not supported")) + } + } + + let row: Row + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey: Any] + + init(row: Row, userInfo: [CodingUserInfoKey: Any]) { + self.row = row + self.userInfo = userInfo + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + return KeyedDecodingContainer(SQLiteKeyedDecodingContainer(row: self.row)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an unkeyed container is not supported")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding a single value container is not supported")) + } +} + diff --git a/Sources/SQLite/Typed/CoreFunctions.swift b/Sources/SQLite/Typed/CoreFunctions.swift index 9d17a326..1afe4e87 100644 --- a/Sources/SQLite/Typed/CoreFunctions.swift +++ b/Sources/SQLite/Typed/CoreFunctions.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -import Foundation.NSData +import Foundation extension ExpressionType where UnderlyingType : Number { @@ -223,6 +223,31 @@ extension ExpressionType where UnderlyingType == String { return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) } + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// let pattern = Expression("pattern") + /// email.like(pattern) + /// // "email" LIKE "pattern" + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: Expression, escape character: Character? = nil) -> Expression { + guard let character = character else { + return "LIKE".infix(self, pattern) + } + let like: Expression = "LIKE".infix(self, pattern, wrap: false) + return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) + } + /// Builds a copy of the expression appended with a `GLOB` query against the /// given pattern. /// @@ -422,6 +447,31 @@ extension ExpressionType where UnderlyingType == String? { } return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) } + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// let pattern = Expression("pattern") + /// email.like(pattern) + /// // "email" LIKE "pattern" + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: Expression, escape character: Character? = nil) -> Expression { + guard let character = character else { + return "LIKE".infix(self, pattern) + } + let like: Expression = "LIKE".infix(self, pattern, wrap: false) + return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) + } /// Builds a copy of the expression appended with a `GLOB` query against the /// given pattern. @@ -623,6 +673,35 @@ extension Collection where Iterator.Element : Value, IndexDistance == Int { } +extension String { + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = "some@thing.com" + /// let pattern = Expression("pattern") + /// email.like(pattern) + /// // 'some@thing.com' LIKE "pattern" + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: Expression, escape character: Character? = nil) -> Expression { + guard let character = character else { + return "LIKE".infix(self, pattern) + } + let like: Expression = "LIKE".infix(self, pattern, wrap: false) + return Expression("(\(like.template) ESCAPE ?)", like.bindings + [String(character)]) + } + +} + /// Builds a copy of the given expressions wrapped with the `ifnull` function. /// /// let name = Expression("name") diff --git a/Sources/SQLite/Typed/DateAndTimeFunctions.swift b/Sources/SQLite/Typed/DateAndTimeFunctions.swift new file mode 100644 index 00000000..0b9a497f --- /dev/null +++ b/Sources/SQLite/Typed/DateAndTimeFunctions.swift @@ -0,0 +1,106 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// All five date and time functions take a time string as an argument. +/// The time string is followed by zero or more modifiers. +/// The strftime() function also takes a format string as its first argument. +/// +/// https://www.sqlite.org/lang_datefunc.html +public class DateFunctions { + /// The date() function returns the date in this format: YYYY-MM-DD. + public static func date(_ timestring: String, _ modifiers: String...) -> Expression { + return timefunction("date", timestring: timestring, modifiers: modifiers) + } + + /// The time() function returns the time as HH:MM:SS. + public static func time(_ timestring: String, _ modifiers: String...) -> Expression { + return timefunction("time", timestring: timestring, modifiers: modifiers) + } + + /// The datetime() function returns "YYYY-MM-DD HH:MM:SS". + public static func datetime(_ timestring: String, _ modifiers: String...) -> Expression { + return timefunction("datetime", timestring: timestring, modifiers: modifiers) + } + + /// The julianday() function returns the Julian day - + /// the number of days since noon in Greenwich on November 24, 4714 B.C. + public static func julianday(_ timestring: String, _ modifiers: String...) -> Expression { + return timefunction("julianday", timestring: timestring, modifiers: modifiers) + } + + /// The strftime() routine returns the date formatted according to the format string specified as the first argument. + public static func strftime(_ format: String, _ timestring: String, _ modifiers: String...) -> Expression { + if !modifiers.isEmpty { + let templates = [String](repeating: "?", count: modifiers.count).joined(separator: ", ") + return Expression("strftime(?, ?, \(templates))", [format, timestring] + modifiers) + } + return Expression("strftime(?, ?)", [format, timestring]) + } + + private static func timefunction(_ name: String, timestring: String, modifiers: [String]) -> Expression { + if !modifiers.isEmpty { + let templates = [String](repeating: "?", count: modifiers.count).joined(separator: ", ") + return Expression("\(name)(?, \(templates))", [timestring] + modifiers) + } + return Expression("\(name)(?)", [timestring]) + } +} + +extension Date { + public var date: Expression { + return DateFunctions.date(dateFormatter.string(from: self)) + } + + public var time: Expression { + return DateFunctions.time(dateFormatter.string(from: self)) + } + + public var datetime: Expression { + return DateFunctions.datetime(dateFormatter.string(from: self)) + } + + public var julianday: Expression { + return DateFunctions.julianday(dateFormatter.string(from: self)) + } +} + +extension Expression where UnderlyingType == Date { + public var date: Expression { + return Expression("date(\(template))", bindings) + } + + public var time: Expression { + return Expression("time(\(template))", bindings) + } + + public var datetime: Expression { + return Expression("datetime(\(template))", bindings) + } + + public var julianday: Expression { + return Expression("julianday(\(template))", bindings) + } +} diff --git a/Sources/SQLite/Typed/Expression.swift b/Sources/SQLite/Typed/Expression.swift index 3198901c..33329b73 100644 --- a/Sources/SQLite/Typed/Expression.swift +++ b/Sources/SQLite/Typed/Expression.swift @@ -73,7 +73,7 @@ public protocol Expressible { extension Expressible { // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE - // FIXME: use @testable and make internal + // FIXME: make internal (0.12.0) public func asSQL() -> String { let expressed = expression var idx = 0 diff --git a/Sources/SQLite/Typed/Operators.swift b/Sources/SQLite/Typed/Operators.swift index fdd293be..d97e52b9 100644 --- a/Sources/SQLite/Typed/Operators.swift +++ b/Sources/SQLite/Typed/Operators.swift @@ -474,11 +474,44 @@ public func <=(lhs: V, rhs: Expression) -> Expression wher return infix(lhs, rhs) } -public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Binding & Comparable { - return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound as? Binding, lhs.upperBound as? Binding]) +public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) } -public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Binding & Comparable { - return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound as? Binding, lhs.upperBound as? Binding]) + +public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: Range, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) >= ? AND \(rhs.template) < ?", rhs.bindings + [lhs.lowerBound.datatypeValue] + rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: Range, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) >= ? AND \(rhs.template) < ?", rhs.bindings + [lhs.lowerBound.datatypeValue] + rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) <= ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) <= ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeUpTo, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) < ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeUpTo, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) < ?", rhs.bindings + [lhs.upperBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) >= ?", rhs.bindings + [lhs.lowerBound.datatypeValue]) +} + +public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype : Comparable & Value { + return Expression("\(rhs.template) >= ?", rhs.bindings + [lhs.lowerBound.datatypeValue]) } // MARK: - diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index c9d2ea9c..17ec715a 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -22,6 +22,8 @@ // THE SOFTWARE. // +import Foundation + public protocol QueryType : Expressible { var clauses: QueryClauses { get set } @@ -180,6 +182,27 @@ extension QueryType { return query } + // MARK: UNION + + /// Adds a `UNION` clause to the query. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.filter(email == "alice@example.com").union(users.filter(email == "sally@example.com")) + /// // SELECT * FROM "users" WHERE email = 'alice@example.com' UNION SELECT * FROM "users" WHERE email = 'sally@example.com' + /// + /// - Parameters: + /// + /// - table: A query representing the other table. + /// + /// - Returns: A query with the given `UNION` clause applied. + public func union(_ table: QueryType) -> Self { + var query = self + query.clauses.union.append(table) + return query + } + // MARK: JOIN /// Adds a `JOIN` clause to the query. @@ -494,8 +517,9 @@ extension QueryType { return nil } - return " ".join(clauses.join.map { type, query, condition in - " ".join([ + return " ".join(clauses.join.map { arg in + let (type, query, condition) = arg + return " ".join([ Expression(literal: "\(type.rawValue) JOIN"), query.tableName(alias: true), Expression(literal: "ON"), @@ -565,6 +589,19 @@ extension QueryType { Expression(literal: "OFFSET \(offset)") ]) } + + fileprivate var unionClause: Expressible? { + guard !clauses.union.isEmpty else { + return nil + } + + return " ".join(clauses.union.map { query in + " ".join([ + Expression(literal: "UNION"), + query + ]) + }) + } // MARK: - @@ -648,7 +685,9 @@ extension QueryType { tableName(), Expression(literal: "SET"), ", ".join(values.map { " = ".join([$0.column, $0.value]) }), - whereClause + whereClause, + orderClause, + limitOffsetClause ] return Update(" ".join(clauses.flatMap { $0 }).expression) @@ -660,7 +699,9 @@ extension QueryType { let clauses: [Expressible?] = [ Expression(literal: "DELETE FROM"), tableName(), - whereClause + whereClause, + orderClause, + limitOffsetClause ] return Delete(" ".join(clauses.flatMap { $0 }).expression) @@ -687,47 +728,11 @@ extension QueryType { return Expression(".".join([tableName(), column]).expression) } - // FIXME: rdar://problem/18673897 // subscript… - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { + public subscript(column: Expression) -> Expression { return namespace(column) } - public subscript(column: Expression) -> Expression { - return namespace(column) - } - public subscript(column: Expression) -> Expression { + public subscript(column: Expression) -> Expression { return namespace(column) } @@ -779,6 +784,7 @@ extension QueryType { joinClause, whereClause, groupByClause, + unionClause, orderClause, limitOffsetClause ] @@ -890,58 +896,87 @@ public struct Delete : ExpressionType { } + +public struct RowIterator: FailableIterator { + public typealias Element = Row + let statement: Statement + let columnNames: [String: Int] + + public func failableNext() throws -> Row? { + return try statement.failableNext().flatMap { Row(columnNames, $0) } + } + + public func map(_ transform: (Element) throws -> T) throws -> [T] { + var elements = [T]() + while let row = try failableNext() { + elements.append(try transform(row)) + } + return elements + } +} + extension Connection { public func prepare(_ query: QueryType) throws -> AnySequence { let expression = query.expression let statement = try prepare(expression.template, expression.bindings) - let columnNames: [String: Int] = try { - var (columnNames, idx) = ([String: Int](), 0) - column: for each in query.clauses.select.columns { - var names = each.expression.template.characters.split { $0 == "." }.map(String.init) - let column = names.removeLast() - let namespace = names.joined(separator: ".") - - func expandGlob(_ namespace: Bool) -> ((QueryType) throws -> Void) { - return { (query: QueryType) throws -> (Void) in - var q = type(of: query).init(query.clauses.from.name, database: query.clauses.from.database) - q.clauses.select = query.clauses.select - let e = q.expression - var names = try self.prepare(e.template, e.bindings).columnNames.map { $0.quote() } - if namespace { names = names.map { "\(query.tableName().expression.template).\($0)" } } - for name in names { columnNames[name] = idx; idx += 1 } - } - } + let columnNames = try columnNamesForQuery(query) - if column == "*" { - var select = query - select.clauses.select = (false, [Expression(literal: "*") as Expressible]) - let queries = [select] + query.clauses.join.map { $0.query } - if !namespace.isEmpty { - for q in queries { - if q.tableName().expression.template == namespace { - try expandGlob(true)(q) - continue column - } - } - fatalError("no such table: \(namespace)") - } + return AnySequence { + AnyIterator { statement.next().map { Row(columnNames, $0) } } + } + } + + + public func prepareRowIterator(_ query: QueryType) throws -> RowIterator { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) + return RowIterator(statement: statement, columnNames: try columnNamesForQuery(query)) + } + + private func columnNamesForQuery(_ query: QueryType) throws -> [String: Int] { + var (columnNames, idx) = ([String: Int](), 0) + column: for each in query.clauses.select.columns { + var names = each.expression.template.characters.split { $0 == "." }.map(String.init) + let column = names.removeLast() + let namespace = names.joined(separator: ".") + + func expandGlob(_ namespace: Bool) -> ((QueryType) throws -> Void) { + return { (query: QueryType) throws -> (Void) in + var q = type(of: query).init(query.clauses.from.name, database: query.clauses.from.database) + q.clauses.select = query.clauses.select + let e = q.expression + var names = try self.prepare(e.template, e.bindings).columnNames.map { $0.quote() } + if namespace { names = names.map { "\(query.tableName().expression.template).\($0)" } } + for name in names { columnNames[name] = idx; idx += 1 } + } + } + + if column == "*" { + var select = query + select.clauses.select = (false, [Expression(literal: "*") as Expressible]) + let queries = [select] + query.clauses.join.map { $0.query } + if !namespace.isEmpty { for q in queries { - try expandGlob(query.clauses.join.count > 0)(q) + if q.tableName().expression.template == namespace { + try expandGlob(true)(q) + continue column + } + throw QueryError.noSuchTable(name: namespace) } - continue + throw QueryError.noSuchTable(name: namespace) } - - columnNames[each.expression.template] = idx - idx += 1 + for q in queries { + try expandGlob(query.clauses.join.count > 0)(q) + } + continue } - return columnNames - }() - - return AnySequence { - AnyIterator { statement.next().map { Row(columnNames, $0) } } + + columnNames[each.expression.template] = idx + idx += 1 } + return columnNames } public func scalar(_ query: ScalarQuery) throws -> V { @@ -967,7 +1002,7 @@ extension Connection { } public func pluck(_ query: QueryType) throws -> Row? { - return try prepare(query.limit(1, query.clauses.limit?.offset)).makeIterator().next() + return try prepareRowIterator(query.limit(1, query.clauses.limit?.offset)).failableNext() } /// Runs an `Insert` query. @@ -1023,27 +1058,39 @@ extension Connection { public struct Row { - fileprivate let columnNames: [String: Int] + let columnNames: [String: Int] fileprivate let values: [Binding?] - fileprivate init(_ columnNames: [String: Int], _ values: [Binding?]) { + internal init(_ columnNames: [String: Int], _ values: [Binding?]) { self.columnNames = columnNames self.values = values } + func hasValue(for column: String) -> Bool { + guard let idx = columnNames[column.quote()] else { + return false + } + return values[idx] != nil + } + /// Returns a row’s value for the given column. /// /// - Parameter column: An expression representing a column selected in a Query. /// /// - Returns: The value for the given column. - public func get(_ column: Expression) -> V { - return get(Expression(column))! + public func get(_ column: Expression) throws -> V { + if let value = try get(Expression(column)) { + return value + } else { + throw QueryError.unexpectedNullValue(name: column.template) + } } - public func get(_ column: Expression) -> V? { + + public func get(_ column: Expression) throws -> V? { func valueAtIndex(_ idx: Int) -> V? { guard let value = values[idx] as? V.Datatype else { return nil } - return (V.fromDatatypeValue(value) as? V)! + return V.fromDatatypeValue(value) as? V } guard let idx = columnNames[column.template] else { @@ -1051,61 +1098,24 @@ public struct Row { switch similar.count { case 0: - fatalError("no such column '\(column.template)' in columns: \(columnNames.keys.sorted())") + throw QueryError.noSuchColumn(name: column.template, columns: columnNames.keys.sorted()) case 1: return valueAtIndex(columnNames[similar[0]]!) default: - fatalError("ambiguous column '\(column.template)' (please disambiguate: \(similar))") + throw QueryError.ambiguousColumn(name: column.template, similar: similar) } } return valueAtIndex(idx) } - // FIXME: rdar://problem/18673897 // subscript… - - public subscript(column: Expression) -> Blob { - return get(column) - } - public subscript(column: Expression) -> Blob? { - return get(column) - } - - public subscript(column: Expression) -> Bool { - return get(column) - } - public subscript(column: Expression) -> Bool? { - return get(column) + public subscript(column: Expression) -> T { + return try! get(column) } - public subscript(column: Expression) -> Double { - return get(column) + public subscript(column: Expression) -> T? { + return try! get(column) } - public subscript(column: Expression) -> Double? { - return get(column) - } - - public subscript(column: Expression) -> Int { - return get(column) - } - public subscript(column: Expression) -> Int? { - return get(column) - } - - public subscript(column: Expression) -> Int64 { - return get(column) - } - public subscript(column: Expression) -> Int64? { - return get(column) - } - - public subscript(column: Expression) -> String { - return get(column) - } - public subscript(column: Expression) -> String? { - return get(column) - } - } /// Determines the join operator for a query’s `JOIN` clause. @@ -1154,9 +1164,12 @@ public struct QueryClauses { var order = [Expressible]() var limit: (length: Int, offset: Int?)? + + var union = [QueryType]() fileprivate init(_ name: String, alias: String?, database: String?) { self.from = (name, alias, database) } } + diff --git a/Sources/SQLite/Typed/Schema.swift b/Sources/SQLite/Typed/Schema.swift index 7bf70fe4..46a1f87a 100644 --- a/Sources/SQLite/Typed/Schema.swift +++ b/Sources/SQLite/Typed/Schema.swift @@ -127,11 +127,7 @@ extension Table { // MARK: - CREATE INDEX - public func createIndex(_ columns: Expressible...) -> String { - return createIndex(columns) - } - - 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"), @@ -144,11 +140,8 @@ extension Table { // MARK: - DROP INDEX - public func dropIndex(_ columns: Expressible...) -> String { - return dropIndex(columns) - } - public func dropIndex(_ columns: [Expressible], ifExists: Bool = false) -> String { + public func dropIndex(_ columns: Expressible..., ifExists: Bool = false) -> String { return drop("INDEX", indexName(columns), ifExists) } @@ -307,27 +300,27 @@ public final class TableBuilder { } 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) + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) } public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression, collate: Collation) where V.Datatype == String { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) } public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V, collate: Collation) where V.Datatype == String { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) } public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) } public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression, collate: Collation) where V.Datatype == String { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) } public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: V, collate: Collation) where V.Datatype == String { - column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, collate) } fileprivate func column(_ name: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) { diff --git a/Sources/SQLiteObjc/SQLite-Bridging.m b/Sources/SQLiteObjc/SQLite-Bridging.m index d8fe6b68..e00a7315 100644 --- a/Sources/SQLiteObjc/SQLite-Bridging.m +++ b/Sources/SQLiteObjc/SQLite-Bridging.m @@ -112,14 +112,14 @@ static int __SQLiteTokenizerNext(sqlite3_tokenizer_cursor * pCursor, const char __SQLiteTokenizerNext }; -int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * moduleName, const char * submoduleName, _SQLiteTokenizerNextCallback callback) { +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((sqlite3 *)db, "SELECT fts3_tokenizer(?, ?)", -1, &stmt, 0); + int status = sqlite3_prepare_v2(db, "SELECT fts3_tokenizer(?, ?)", -1, &stmt, 0); if (status != SQLITE_OK ){ return status; } diff --git a/Sources/SQLiteObjc/include/SQLite-Bridging.h b/Sources/SQLiteObjc/include/SQLite-Bridging.h index d15e8d56..5b356593 100644 --- a/Sources/SQLiteObjc/include/SQLite-Bridging.h +++ b/Sources/SQLiteObjc/include/SQLite-Bridging.h @@ -24,14 +24,10 @@ @import Foundation; -#ifndef COCOAPODS #import "sqlite3.h" -#endif - -typedef struct SQLiteHandle SQLiteHandle; // CocoaPods workaround NS_ASSUME_NONNULL_BEGIN -typedef NSString * _Nullable (^_SQLiteTokenizerNextCallback)(const char * input, int * inputOffset, int * inputLength); -int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * module, const char * tokenizer, _Nullable _SQLiteTokenizerNextCallback callback); +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/CocoaPods/Gemfile b/Tests/CocoaPods/Gemfile index e40770e8..04f0155a 100644 --- a/Tests/CocoaPods/Gemfile +++ b/Tests/CocoaPods/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -gem 'cocoapods', '~> 1.2.0' +gem 'cocoapods', '~> 1.3.1' gem 'minitest' diff --git a/Tests/CocoaPods/Gemfile.lock b/Tests/CocoaPods/Gemfile.lock index 869ae3dc..47a2db58 100644 --- a/Tests/CocoaPods/Gemfile.lock +++ b/Tests/CocoaPods/Gemfile.lock @@ -2,33 +2,33 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (2.3.5) - activesupport (4.2.8) + activesupport (4.2.9) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - claide (1.0.1) - cocoapods (1.2.0) + claide (1.0.2) + cocoapods (1.3.1) activesupport (>= 4.0.2, < 5) - claide (>= 1.0.1, < 2.0) - cocoapods-core (= 1.2.0) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.3.1) cocoapods-deintegrate (>= 1.0.1, < 2.0) cocoapods-downloader (>= 1.1.3, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.1.2, < 2.0) + cocoapods-trunk (>= 1.2.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) - colored (~> 1.2) + colored2 (~> 3.1) escape (~> 0.0.4) fourflusher (~> 2.0.1) gh_inspector (~> 1.0) - molinillo (~> 0.5.5) + molinillo (~> 0.5.7) nap (~> 1.0) - ruby-macho (~> 0.2.5) - xcodeproj (>= 1.4.1, < 2.0) - cocoapods-core (1.2.0) - activesupport (>= 4.0.2, < 5) + ruby-macho (~> 1.1) + xcodeproj (>= 1.5.1, < 2.0) + cocoapods-core (1.3.1) + activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) cocoapods-deintegrate (1.0.1) @@ -37,37 +37,36 @@ GEM nap cocoapods-search (1.0.0) cocoapods-stats (1.0.0) - cocoapods-trunk (1.1.2) + cocoapods-trunk (1.2.0) nap (>= 0.8, < 2.0) netrc (= 0.7.8) cocoapods-try (1.1.0) - colored (1.2) + colored2 (3.1.2) escape (0.0.4) fourflusher (2.0.1) fuzzy_match (2.0.4) gh_inspector (1.0.3) - i18n (0.8.1) + i18n (0.8.6) minitest (5.10.1) molinillo (0.5.7) nanaimo (0.2.3) nap (1.1.0) netrc (0.7.8) - ruby-macho (0.2.6) + ruby-macho (1.1.0) thread_safe (0.3.6) tzinfo (1.2.3) thread_safe (~> 0.1) - xcodeproj (1.4.2) + xcodeproj (1.5.1) CFPropertyList (~> 2.3.3) - activesupport (>= 3) - claide (>= 1.0.1, < 2.0) - colored (~> 1.2) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) nanaimo (~> 0.2.3) PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.2.0) + cocoapods (~> 1.3.1) minitest BUNDLED WITH diff --git a/Tests/CocoaPods/integration_test.rb b/Tests/CocoaPods/integration_test.rb index 192aff90..9792b570 100755 --- a/Tests/CocoaPods/integration_test.rb +++ b/Tests/CocoaPods/integration_test.rb @@ -1,7 +1,8 @@ #!/usr/bin/env ruby +require 'cocoapods' +require 'cocoapods/validator' require 'minitest/autorun' -require_relative 'test_running_validator' class IntegrationTest < Minitest::Test @@ -12,9 +13,7 @@ def test_validate_project private def validator - @validator ||= TestRunningValidator.new(podspec, []).tap do |validator| - validator.test_files = Dir["#{project_test_dir}/**/*.swift"] - validator.test_resources = Dir["#{project_test_dir}/fixtures"] + @validator ||= CustomValidator.new(podspec, ['https://github.com/CocoaPods/Specs.git']).tap do |validator| validator.config.verbose = true validator.no_clean = true validator.use_frameworks = true @@ -27,9 +26,6 @@ def validator else validator.only_subspec = subspec end - if ENV['IOS_SIMULATOR'] - validator.ios_simulator = ENV['IOS_SIMULATOR'] - end end end @@ -37,7 +33,11 @@ def podspec File.expand_path(File.dirname(__FILE__) + '/../../SQLite.swift.podspec') end - def project_test_dir - File.expand_path(File.dirname(__FILE__) + '/../SQLiteTests') + + class CustomValidator < Pod::Validator + def test_pod + # https://github.com/CocoaPods/CocoaPods/issues/7009 + super unless consumer.platform_name == :watchos + end end end diff --git a/Tests/CocoaPods/test_running_validator.rb b/Tests/CocoaPods/test_running_validator.rb deleted file mode 100644 index fcfd3c28..00000000 --- a/Tests/CocoaPods/test_running_validator.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'cocoapods' -require 'cocoapods/validator' -require 'fileutils' - -class TestRunningValidator < Pod::Validator - APP_TARGET = 'App' - TEST_TARGET = 'Tests' - - attr_accessor :test_files - attr_accessor :test_resources - attr_accessor :ios_simulator - attr_accessor :tvos_simulator - attr_accessor :watchos_simulator - - def initialize(spec_or_path, source_urls) - super(spec_or_path, source_urls) - self.test_files = [] - self.test_resources = [] - self.ios_simulator = :oldest - self.tvos_simulator = :oldest - self.watchos_simulator = :oldest - end - - def create_app_project - super - project = Xcodeproj::Project.open(validation_dir + "#{APP_TARGET}.xcodeproj") - create_test_target(project) - project.save - end - - def add_app_project_import - super - project = Xcodeproj::Project.open(validation_dir + 'App.xcodeproj') - group = project.new_group(TEST_TARGET) - test_target = project.targets.last - test_target.add_resources(test_resources.map { |resource| group.new_file(resource) }) - test_target.add_file_references(test_files.map { |file| group.new_file(file) }) - add_swift_version(test_target) - project.save - end - - def install_pod - super - if local? - FileUtils.ln_s file.dirname, validation_dir + "Pods/#{spec.name}" - end - end - - def podfile_from_spec(*args) - super(*args).tap do |pod_file| - add_test_target(pod_file) - end - end - - def build_pod - super - Pod::UI.message "\Testing with xcodebuild.\n".yellow do - run_tests - end - end - - private - def create_test_target(project) - test_target = project.new_target(:unit_test_bundle, TEST_TARGET, consumer.platform_name, deployment_target) - create_test_scheme(project, test_target) - end - - def create_test_scheme(project, test_target) - project.recreate_user_schemes - test_scheme = Xcodeproj::XCScheme.new(test_scheme_path(project)) - test_scheme.add_test_target(test_target) - test_scheme.save! - end - - def test_scheme_path(project) - Xcodeproj::XCScheme.user_data_dir(project.path) + "#{TEST_TARGET}.xcscheme" - end - - def add_test_target(pod_file) - app_target = pod_file.target_definitions[APP_TARGET] - Pod::Podfile::TargetDefinition.new(TEST_TARGET, app_target) - end - - def run_tests - command = [ - 'clean', 'build', 'build-for-testing', 'test-without-building', - '-workspace', File.join(validation_dir, "#{APP_TARGET}.xcworkspace"), - '-scheme', TEST_TARGET, - '-configuration', 'Debug' - ] - case consumer.platform_name - when :ios - command += %w(CODE_SIGN_IDENTITY=- -sdk iphonesimulator) - command += Fourflusher::SimControl.new.destination(ios_simulator, 'iOS', deployment_target) - when :osx - command += %w(LD_RUNPATH_SEARCH_PATHS=@loader_path/../Frameworks) - when :tvos - command += %w(CODE_SIGN_IDENTITY=- -sdk appletvsimulator) - command += Fourflusher::SimControl.new.destination(tvos_simulator, 'tvOS', deployment_target) - when :watchos - # there's no XCTest on watchOS (https://openradar.appspot.com/21760513) - return - else - return - end - - output, status = _xcodebuild(command) - - unless status.success? - message = 'Returned an unsuccessful exit code.' - if config.verbose? - message += "\nXcode output: \n#{output}\n" - else - message += ' You can use `--verbose` for more information.' - end - error('xcodebuild', message) - end - output - end -end diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 00000000..59796fde --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,6 @@ +import XCTest +@testable import SQLiteTests + +XCTMain([ +testCase([ +])]) diff --git a/Tests/SQLiteTests/ConnectionTests.swift b/Tests/SQLiteTests/ConnectionTests.swift index d05c5ece..03f00719 100644 --- a/Tests/SQLiteTests/ConnectionTests.swift +++ b/Tests/SQLiteTests/ConnectionTests.swift @@ -1,12 +1,16 @@ import XCTest +import Foundation +import Dispatch @testable import SQLite #if SQLITE_SWIFT_STANDALONE import sqlite3 #elseif SQLITE_SWIFT_SQLCIPHER import SQLCipher -#elseif SWIFT_PACKAGE || COCOAPODS +#elseif os(Linux) import CSQLite +#else +import SQLite3 #endif class ConnectionTests : SQLiteTestCase { @@ -151,6 +155,44 @@ class ConnectionTests : SQLiteTestCase { AssertSQL("ROLLBACK TRANSACTION", 0) } + func test_transaction_rollsBackTransactionsIfCommitsFail() { + let sqliteVersion = String(describing: try! db.scalar("SELECT sqlite_version()")!) + .split(separator: ".").flatMap { Int($0) } + // PRAGMA defer_foreign_keys only supported in SQLite >= 3.8.0 + guard sqliteVersion[0] == 3 && sqliteVersion[1] >= 8 else { + NSLog("skipping test for SQLite version \(sqliteVersion)") + return + } + // This test case needs to emulate an environment where the individual statements succeed, but committing the + // transaction fails. Using deferred foreign keys is one option to achieve this. + try! db.execute("PRAGMA foreign_keys = ON;") + try! db.execute("PRAGMA defer_foreign_keys = ON;") + let stmt = try! db.prepare("INSERT INTO users (email, manager_id) VALUES (?, ?)", "alice@example.com", 100) + + do { + try db.transaction { + try stmt.run() + } + XCTFail("expected error") + } catch let Result.error(_, code, _) { + XCTAssertEqual(SQLITE_CONSTRAINT, code) + } catch let error { + XCTFail("unexpected error: \(error)") + } + + AssertSQL("BEGIN DEFERRED TRANSACTION") + AssertSQL("INSERT INTO users (email, manager_id) VALUES ('alice@example.com', 100)") + AssertSQL("COMMIT TRANSACTION") + AssertSQL("ROLLBACK TRANSACTION") + + // Run another transaction to ensure that a subsequent transaction does not fail with an "cannot start a + // transaction within a transaction" error. + let stmt2 = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + try! db.transaction { + try stmt2.run() + } + } + func test_transaction_beginsAndRollsTransactionsBack() { let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") @@ -169,7 +211,7 @@ class ConnectionTests : SQLiteTestCase { } func test_savepoint_beginsAndCommitsSavepoints() { - let db = self.db + let db:Connection = self.db try! db.savepoint("1") { try db.savepoint("2") { @@ -187,7 +229,7 @@ class ConnectionTests : SQLiteTestCase { } func test_savepoint_beginsAndRollsSavepointsBack() { - let db = self.db + let db:Connection = self.db let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { @@ -336,11 +378,32 @@ class ConnectionTests : SQLiteTestCase { let stmt = try! db.prepare("SELECT *, sleep(?) FROM users", 0.1) try! stmt.run() - let deadline = DispatchTime.now() + Double(Int64(10 * NSEC_PER_MSEC)) / Double(NSEC_PER_SEC) + let deadline = DispatchTime.now() + 0.01 _ = DispatchQueue(label: "queue", qos: .background).asyncAfter(deadline: deadline, execute: db.interrupt) AssertThrows(try stmt.run()) } + func test_concurrent_access_single_connection() { + let conn = try! Connection("\(NSTemporaryDirectory())/\(UUID().uuidString)") + try! conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") + try! conn.run("INSERT INTO test(value) VALUES(?)", 0) + let queue = DispatchQueue(label: "Readers", attributes: [.concurrent]) + let nReaders = 5 + var reads = Array(repeating: 0, count: nReaders) + var finished = false + for index in 0.. 500) } + } + } } diff --git a/Tests/SQLiteTests/CoreFunctionsTests.swift b/Tests/SQLiteTests/CoreFunctionsTests.swift index db37ff7f..e7402de3 100644 --- a/Tests/SQLiteTests/CoreFunctionsTests.swift +++ b/Tests/SQLiteTests/CoreFunctionsTests.swift @@ -1,5 +1,5 @@ import XCTest -import SQLite +@testable import SQLite class CoreFunctionsTests : XCTestCase { @@ -37,6 +37,15 @@ 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\" ESCAPE '\\')", string.like(Expression("a"), escape: "\\")) + AssertSQL("(\"stringOptional\" LIKE \"a\" ESCAPE '\\')", stringOptional.like(Expression("a"), escape: "\\")) + + AssertSQL("('string' LIKE \"a\")", "string".like(Expression("a"))) + AssertSQL("('string' LIKE \"a\" ESCAPE '\\')", "string".like(Expression("a"), escape: "\\")) } func test_glob_buildsExpressionWithGlobOperator() { diff --git a/Tests/SQLiteTests/DateAndTimeFunctionTests.swift b/Tests/SQLiteTests/DateAndTimeFunctionTests.swift new file mode 100644 index 00000000..628b5910 --- /dev/null +++ b/Tests/SQLiteTests/DateAndTimeFunctionTests.swift @@ -0,0 +1,66 @@ +import XCTest +@testable import SQLite + +class DateAndTimeFunctionsTests : XCTestCase { + + func test_date() { + AssertSQL("date('now')", DateFunctions.date("now")) + AssertSQL("date('now', 'localtime')", DateFunctions.date("now", "localtime")) + } + + func test_time() { + AssertSQL("time('now')", DateFunctions.time("now")) + AssertSQL("time('now', 'localtime')", DateFunctions.time("now", "localtime")) + } + + func test_datetime() { + AssertSQL("datetime('now')", DateFunctions.datetime("now")) + AssertSQL("datetime('now', 'localtime')", DateFunctions.datetime("now", "localtime")) + } + + func test_julianday() { + AssertSQL("julianday('now')", DateFunctions.julianday("now")) + AssertSQL("julianday('now', 'localtime')", DateFunctions.julianday("now", "localtime")) + } + + func test_strftime() { + AssertSQL("strftime('%Y-%m-%d', 'now')", DateFunctions.strftime("%Y-%m-%d", "now")) + AssertSQL("strftime('%Y-%m-%d', 'now', 'localtime')", DateFunctions.strftime("%Y-%m-%d", "now", "localtime")) + } +} + +class DateExtensionTests : XCTestCase { + func test_time() { + AssertSQL("time('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).time) + } + + func test_date() { + AssertSQL("date('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).date) + } + + func test_datetime() { + AssertSQL("datetime('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).datetime) + } + + func test_julianday() { + AssertSQL("julianday('1970-01-01T00:00:00.000')", Date(timeIntervalSince1970: 0).julianday) + } +} + +class DateExpressionTests : XCTestCase { + func test_date() { + AssertSQL("date(\"date\")", date.date) + } + + func test_time() { + AssertSQL("time(\"date\")", date.time) + } + + func test_datetime() { + AssertSQL("datetime(\"date\")", date.datetime) + } + + func test_julianday() { + AssertSQL("julianday(\"date\")", date.julianday) + } +} diff --git a/Tests/SQLiteTests/Fixtures.swift b/Tests/SQLiteTests/Fixtures.swift index 13f83f77..d0683130 100644 --- a/Tests/SQLiteTests/Fixtures.swift +++ b/Tests/SQLiteTests/Fixtures.swift @@ -3,6 +3,6 @@ import Foundation func fixture(_ name: String, withExtension: String?) -> String { let testBundle = Bundle(for: SQLiteTestCase.self) return testBundle.url( - forResource: URL(string: "fixtures")?.appendingPathComponent(name).path, + forResource: name, withExtension: withExtension)!.path } diff --git a/Tests/SQLiteTests/OperatorsTests.swift b/Tests/SQLiteTests/OperatorsTests.swift index f0e585cd..948eb0a4 100644 --- a/Tests/SQLiteTests/OperatorsTests.swift +++ b/Tests/SQLiteTests/OperatorsTests.swift @@ -255,7 +255,27 @@ class OperatorsTests : XCTestCase { AssertSQL("\"doubleOptional\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= doubleOptional) } - func test_patternMatchingOperator_withomparableClosedRangeString_buildsBetweenBooleanExpression() { + func test_patternMatchingOperator_withComparableRange_buildsBooleanExpression() { + AssertSQL("\"double\" >= 1.2 AND \"double\" < 4.5", 1.2..<4.5 ~= double) + AssertSQL("\"doubleOptional\" >= 1.2 AND \"doubleOptional\" < 4.5", 1.2..<4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparablePartialRangeThrough_buildsBooleanExpression() { + AssertSQL("\"double\" <= 4.5", ...4.5 ~= double) + AssertSQL("\"doubleOptional\" <= 4.5", ...4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparablePartialRangeUpTo_buildsBooleanExpression() { + AssertSQL("\"double\" < 4.5", ..<4.5 ~= double) + AssertSQL("\"doubleOptional\" < 4.5", ..<4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparablePartialRangeFrom_buildsBooleanExpression() { + AssertSQL("\"double\" >= 4.5", 4.5... ~= double) + AssertSQL("\"doubleOptional\" >= 4.5", 4.5... ~= doubleOptional) + } + + func test_patternMatchingOperator_withComparableClosedRangeString_buildsBetweenBooleanExpression() { AssertSQL("\"string\" BETWEEN 'a' AND 'b'", "a"..."b" ~= string) AssertSQL("\"stringOptional\" BETWEEN 'a' AND 'b'", "a"..."b" ~= stringOptional) } @@ -293,4 +313,30 @@ class OperatorsTests : XCTestCase { AssertSQL("((1 = 1) AND ((1 = 1) OR (1 = 1)))", n == n && (n == n || n == n)) } + func test_dateExpressionLessGreater() { + let begin = Date(timeIntervalSince1970: 0) + AssertSQL("(\"date\" < '1970-01-01T00:00:00.000')", date < begin) + AssertSQL("(\"date\" > '1970-01-01T00:00:00.000')", date > begin) + AssertSQL("(\"date\" >= '1970-01-01T00:00:00.000')", date >= begin) + AssertSQL("(\"date\" <= '1970-01-01T00:00:00.000')", date <= begin) + } + + func test_dateExpressionRange() { + let begin = Date(timeIntervalSince1970: 0) + let end = Date(timeIntervalSince1970: 5000) + AssertSQL( + "\"date\" >= '1970-01-01T00:00:00.000' AND \"date\" < '1970-01-01T01:23:20.000'", + (begin..("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]] } + } - let managerId = Expression("manager_id") + 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")) @@ -334,6 +430,41 @@ class QueryIntegrationTests : SQLiteTestCase { } } + func test_select_codable() throws { + let table = Table("codable") + try db.run(table.create { builder in + builder.column(Expression("int")) + builder.column(Expression("string")) + builder.column(Expression("bool")) + builder.column(Expression("float")) + builder.column(Expression("double")) + builder.column(Expression("optional")) + builder.column(Expression("sub")) + }) + + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, optional: "optional", sub: value1) + + try db.run(table.insert(value)) + + let rows = try db.prepare(table) + let values: [TestCodable] = try rows.map({ try $0.decode() }) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].int, 5) + XCTAssertEqual(values[0].string, "6") + XCTAssertEqual(values[0].bool, true) + XCTAssertEqual(values[0].float, 7) + XCTAssertEqual(values[0].double, 8) + XCTAssertEqual(values[0].optional, "optional") + XCTAssertEqual(values[0].sub?.int, 1) + XCTAssertEqual(values[0].sub?.string, "2") + XCTAssertEqual(values[0].sub?.bool, true) + XCTAssertEqual(values[0].sub?.float, 3) + XCTAssertEqual(values[0].sub?.double, 4) + XCTAssertNil(values[0].sub?.optional) + XCTAssertNil(values[0].sub?.sub) + } + func test_scalar() { XCTAssertEqual(0, try! db.scalar(users.count)) XCTAssertEqual(false, try! db.scalar(users.exists)) @@ -361,5 +492,51 @@ class QueryIntegrationTests : SQLiteTestCase { let changes = try! db.run(users.delete()) XCTAssertEqual(0, changes) } + + func test_union() throws { + let expectedIDs = [ + try db.run(users.insert(email <- "alice@example.com")), + try db.run(users.insert(email <- "sally@example.com")) + ] + + let query1 = users.filter(email == "alice@example.com") + let query2 = users.filter(email == "sally@example.com") + + let actualIDs = try db.prepare(query1.union(query2)).map { $0[id] } + XCTAssertEqual(expectedIDs, actualIDs) + + let query3 = users.select(users[*], Expression(literal: "1 AS weight")).filter(email == "sally@example.com") + let query4 = users.select(users[*], Expression(literal: "2 AS weight")).filter(email == "alice@example.com") + + print(query3.union(query4).order(Expression(literal: "weight")).asSQL()) + + let orderedIDs = try db.prepare(query3.union(query4).order(Expression(literal: "weight"), email)).map { $0[id] } + XCTAssertEqual(Array(expectedIDs.reversed()), orderedIDs) + } + + func test_no_such_column() throws { + let doesNotExist = Expression("doesNotExist") + try! InsertUser("alice") + let row = try! db.pluck(users.filter(email == "alice@example.com"))! + + XCTAssertThrowsError(try row.get(doesNotExist)) { error in + if case QueryError.noSuchColumn(let name, _) = error { + XCTAssertEqual("\"doesNotExist\"", name) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + func test_catchConstraintError() { + try! db.run(users.insert(email <- "alice@example.com")) + do { + try db.run(users.insert(email <- "alice@example.com")) + XCTFail("expected error") + } catch let Result.error(_, code, _) where code == SQLITE_CONSTRAINT { + // expected + } catch let error { + XCTFail("unexpected error: \(error)") + } + } } diff --git a/Tests/SQLiteTests/RowTests.swift b/Tests/SQLiteTests/RowTests.swift new file mode 100644 index 00000000..17873e71 --- /dev/null +++ b/Tests/SQLiteTests/RowTests.swift @@ -0,0 +1,88 @@ +import XCTest +@testable import SQLite + +class RowTests : XCTestCase { + + public func test_get_value() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = try! row.get(Expression("foo")) + + XCTAssertEqual("value", result) + } + + public func test_get_value_subscript() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = row[Expression("foo")] + + XCTAssertEqual("value", result) + } + + public func test_get_value_optional() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = try! row.get(Expression("foo")) + + XCTAssertEqual("value", result) + } + + public func test_get_value_optional_subscript() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = row[Expression("foo")] + + XCTAssertEqual("value", result) + } + + public func test_get_value_optional_nil() { + let row = Row(["\"foo\"": 0], [nil]) + let result = try! row.get(Expression("foo")) + + XCTAssertNil(result) + } + + public func test_get_value_optional_nil_subscript() { + let row = Row(["\"foo\"": 0], [nil]) + let result = row[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 + if case QueryError.unexpectedNullValue(let name) = error { + XCTAssertEqual("\"foo\"", name) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + public func test_get_type_mismatch_optional_returns_nil() { + let row = Row(["\"foo\"": 0], ["value"]) + let result = try! row.get(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 + if case QueryError.noSuchColumn(let name, let columns) = error { + XCTAssertEqual("\"bar\"", name) + XCTAssertEqual(["\"foo\""], columns) + } else { + XCTFail("unexpected error: \(error)") + } + } + } + + public func test_get_ambiguous_column_throws() { + let row = Row(["table1.\"foo\"": 0, "table2.\"foo\"": 1], ["value"]) + XCTAssertThrowsError(try row.get(Expression("foo"))) { error in + if case QueryError.ambiguousColumn(let name, let columns) = error { + XCTAssertEqual("\"foo\"", name) + XCTAssertEqual(["table1.\"foo\"", "table2.\"foo\""], columns.sorted()) + } else { + XCTFail("unexpected error: \(error)") + } + } + } +} diff --git a/Tests/SQLiteTests/SchemaTests.swift b/Tests/SQLiteTests/SchemaTests.swift index e59b569b..b9a08881 100644 --- a/Tests/SQLiteTests/SchemaTests.swift +++ b/Tests/SQLiteTests/SchemaTests.swift @@ -435,99 +435,99 @@ class SchemaTests : XCTestCase { ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT COLLATE RTRIM)", table.create { t in t.column(stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: string != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: stringOptional != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT (\"string\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT DEFAULT (\"string\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT (\"stringOptional\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT DEFAULT (\"stringOptional\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT DEFAULT ('string') COLLATE RTRIM)", table.create { t in t.column(stringOptional, defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: string != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT (\"stringOptional\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE DEFAULT (\"stringOptional\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT ('string') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE DEFAULT ('string') COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: string != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: string, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: string != "", defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: stringOptional, collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: string != "", defaultValue: "string", collate: .rtrim) } ) XCTAssertEqual( - "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", + "CREATE TABLE \"table\" (\"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } ) } @@ -727,25 +727,25 @@ class SchemaTests : XCTestCase { XCTAssertEqual( "CREATE UNIQUE INDEX \"index_table_on_int64\" ON \"table\" (\"int64\")", - table.createIndex([int64], unique: true) + table.createIndex(int64, unique: true) ) XCTAssertEqual( "CREATE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", - table.createIndex([int64], ifNotExists: true) + table.createIndex(int64, ifNotExists: true) ) XCTAssertEqual( "CREATE UNIQUE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", - table.createIndex([int64], unique: true, ifNotExists: true) + table.createIndex(int64, unique: true, ifNotExists: true) ) XCTAssertEqual( "CREATE UNIQUE INDEX IF NOT EXISTS \"main\".\"index_table_on_int64\" ON \"table\" (\"int64\")", - qualifiedTable.createIndex([int64], unique: true, ifNotExists: true) + qualifiedTable.createIndex(int64, unique: true, ifNotExists: true) ) } func test_dropIndex_compilesCreateIndexExpression() { XCTAssertEqual("DROP INDEX \"index_table_on_int64\"", table.dropIndex(int64)) - XCTAssertEqual("DROP INDEX IF EXISTS \"index_table_on_int64\"", table.dropIndex([int64], ifExists: true)) + XCTAssertEqual("DROP INDEX IF EXISTS \"index_table_on_int64\"", table.dropIndex(int64, ifExists: true)) } func test_create_onView_compilesCreateViewExpression() { diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index 8c33bf6a..8d60362c 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -1,34 +1,34 @@ import XCTest -import SQLite +@testable import SQLite class SQLiteTestCase : XCTestCase { - - var trace = [String: Int]() - - let db = try! Connection() - + private var trace:[String: Int]! + var db:Connection! let users = Table("users") override func setUp() { super.setUp() + db = try! Connection() + trace = [String:Int]() db.trace { SQL in print(SQL) - self.trace[SQL] = (self.trace[SQL] ?? 0) + 1 + self.trace[SQL, default: 0] += 1 } } func CreateUsersTable() { - try! db.execute( - "CREATE TABLE \"users\" (" + - "id INTEGER PRIMARY KEY, " + - "email TEXT NOT NULL UNIQUE, " + - "age INTEGER, " + - "salary REAL, " + - "admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), " + - "manager_id INTEGER, " + - "FOREIGN KEY(manager_id) REFERENCES users(id)" + - ")" + try! db.execute(""" + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + age INTEGER, + salary REAL, + admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), + manager_id INTEGER, + FOREIGN KEY(manager_id) REFERENCES users(id) + ) + """ ) } @@ -69,7 +69,7 @@ class SQLiteTestCase : XCTestCase { func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) -> Void) { let expectation = self.expectation(description: description) - block(expectation.fulfill) + block({ expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) } @@ -113,3 +113,23 @@ let table = Table("table") let qualifiedTable = Table("table", database: "main") let virtualTable = VirtualTable("virtual_table") let _view = View("view") // avoid Mac XCTestCase collision + +class TestCodable: Codable { + let int: Int + let string: String + let bool: Bool + let float: Float + let double: Double + let optional: String? + let sub: TestCodable? + + init(int: Int, string: String, bool: Bool, float: Float, double: Double, optional: String?, sub: TestCodable?) { + self.int = int + self.string = string + self.bool = bool + self.float = float + self.double = double + self.optional = optional + self.sub = sub + } +}