diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/template.md similarity index 100% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/template.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..1c47e3a6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,60 @@ +name: Build and test +on: [push, pull_request] +env: + IOS_SIMULATOR: iPhone 12 + IOS_VERSION: 14.4 +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: | + gem update bundler + gem install xcpretty --no-document + brew update + brew outdated carthage || brew upgrade carthage + - name: "Run tests (BUILD_SCHEME: SQLite iOS)" + env: + BUILD_SCHEME: SQLite iOS + run: ./run-tests.sh + - name: "Run tests (BUILD_SCHEME: SQLite Mac)" + env: + BUILD_SCHEME: SQLite Mac + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: none)" + env: + VALIDATOR_SUBSPEC: none + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: standard)" + env: + VALIDATOR_SUBSPEC: standard + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: standalone)" + env: + VALIDATOR_SUBSPEC: standalone + run: ./run-tests.sh + - name: "Run tests (VALIDATOR_SUBSPEC: SQLCipher)" + env: + VALIDATOR_SUBSPEC: SQLCipher + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: iOS)" + env: + CARTHAGE_PLATFORM: iOS + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: Mac)" + env: + CARTHAGE_PLATFORM: Mac + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: watchOS)" + env: + CARTHAGE_PLATFORM: watchOS + run: ./run-tests.sh + - name: "Run tests (CARTHAGE_PLATFORM: tvOS)" + env: + CARTHAGE_PLATFORM: tvOS + run: ./run-tests.sh + - name: "Run tests (PACKAGE_MANAGER_COMMAND: test)" + env: + PACKAGE_MANAGER_COMMAND: test + run: ./run-tests.sh diff --git a/.swift-version b/.swift-version deleted file mode 100644 index bf77d549..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c8fc5feb..00000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: objective-c -rvm: 2.3 -# https://docs.travis-ci.com/user/reference/osx -osx_image: xcode10.2 -env: - global: - - IOS_SIMULATOR="iPhone XS" - - IOS_VERSION="12.2" -matrix: - include: - - env: BUILD_SCHEME="SQLite iOS" - - env: BUILD_SCHEME="SQLite Mac" - - env: VALIDATOR_SUBSPEC="none" - - env: VALIDATOR_SUBSPEC="standard" - - env: VALIDATOR_SUBSPEC="standalone" - - env: VALIDATOR_SUBSPEC="SQLCipher" - - env: CARTHAGE_PLATFORM="iOS" - - env: CARTHAGE_PLATFORM="Mac" - - env: CARTHAGE_PLATFORM="watchOS" - - env: CARTHAGE_PLATFORM="tvOS" - - env: PACKAGE_MANAGER_COMMAND="test" -before_install: - - gem update bundler - - gem install xcpretty --no-document - - brew update - - brew outdated carthage || brew upgrade carthage -script: -# Workaround for Xcode 10.2/tvOS 9.1 bug -# See https://stackoverflow.com/questions/55389080/xcode-10-2-failed-to-run-app-on-simulator-with-ios-10 - - sudo mkdir /Library/Developer/CoreSimulator/Profiles/Runtimes/tvOS\ 9.1.simruntime/Contents/Resources/RuntimeRoot/usr/lib/swift - - ./run-tests.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 027611ee..465438bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,22 @@ -0.11.6 (xxx), [diff][diff-0.11.6] +0.13.0 (22-08-2021), [diff][diff-0.13.0] +======================================== + +* Swift 5.3 support +* Xcode 12.5 support +* Bumps minimum deployment versions +* Fixes up Package.swift to build SQLiteObjc module + +0.12.1, 0.12.2 (21-06-2019) [diff][diff-0.12.2] +======================================== + +* CocoaPods modular headers support + +0.12.0 (24-04-2019) [diff][diff-0.12.0] +======================================== + +* Version with Swift 5 Support + +0.11.6 (19-04-2019), [diff][diff-0.11.6] ======================================== * Swift 4.2, SQLCipher 4.x ([#866][]) @@ -63,6 +81,9 @@ [diff-0.11.4]: https://github.com/stephencelis/SQLite.swift/compare/0.11.3...0.11.4 [diff-0.11.5]: https://github.com/stephencelis/SQLite.swift/compare/0.11.4...0.11.5 [diff-0.11.6]: https://github.com/stephencelis/SQLite.swift/compare/0.11.5...0.11.6 +[diff-0.12.0]: https://github.com/stephencelis/SQLite.swift/compare/0.11.6...0.12.0 +[diff-0.12.2]: https://github.com/stephencelis/SQLite.swift/compare/0.12.0...0.12.2 +[diff-0.13.0]: https://github.com/stephencelis/SQLite.swift/compare/0.12.2...0.13.0 [#142]: https://github.com/stephencelis/SQLite.swift/issues/142 [#315]: https://github.com/stephencelis/SQLite.swift/issues/315 diff --git a/Documentation/Index.md b/Documentation/Index.md index 70d67c2d..21b1c995 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -1,9 +1,9 @@ # SQLite.swift Documentation - [Installation](#installation) + - [Swift Package Manager](#swift-package-manager) - [Carthage](#carthage) - [CocoaPods](#cocoapods) - - [Swift Package Manager](#swift-package-manager) - [Manual](#manual) - [Getting Started](#getting-started) - [Connecting to a Database](#connecting-to-a-database) @@ -35,6 +35,7 @@ - [Sorting Rows](#sorting-rows) - [Limiting and Paging Results](#limiting-and-paging-results) - [Aggregation](#aggregation) + - [Upserting Rows](#upserting-rows) - [Updating Rows](#updating-rows) - [Deleting Rows](#deleting-rows) - [Transactions and Savepoints](#transactions-and-savepoints) @@ -70,6 +71,30 @@ > _Note:_ SQLite.swift requires Swift 5 (and > [Xcode 10.2](https://developer.apple.com/xcode/downloads/)) or greater. +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. It’s integrated with the Swift build system to automate the +process of downloading, compiling, and linking dependencies. + +It is the recommended approach for using SQLite.swift in OSX CLI +applications. + + 1. Add the following to your `Package.swift` file: + + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0") + ] + ``` + + 2. Build your project: + + ```sh + $ swift build + ``` + +[Swift Package Manager]: https://swift.org/package-manager ### Carthage @@ -80,7 +105,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.12.0 + github "stephencelis/SQLite.swift" ~> 0.13.0 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. @@ -110,7 +135,7 @@ install SQLite.swift with Carthage: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.12.0' + pod 'SQLite.swift', '~> 0.13.0' end ``` @@ -124,7 +149,7 @@ with the OS you can require the `standalone` subspec: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.12.0' + pod 'SQLite.swift/standalone', '~> 0.13.0' end ``` @@ -134,7 +159,7 @@ dependency to sqlite3 or one of its subspecs: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.12.0' + pod 'SQLite.swift/standalone', '~> 0.13.0' pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled end ``` @@ -148,7 +173,7 @@ If you want to use [SQLCipher][] with SQLite.swift you can require the ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/SQLCipher', '~> 0.12.0' + pod 'SQLite.swift/SQLCipher', '~> 0.13.0' end ``` @@ -168,31 +193,6 @@ try db.rekey("another secret") [sqlite3pod]: https://github.com/clemensg/sqlite3pod [SQLCipher]: https://www.zetetic.net/sqlcipher/ -### Swift Package Manager - -The [Swift Package Manager][] is a tool for managing the distribution of -Swift code. It’s integrated with the Swift build system to automate the -process of downloading, compiling, and linking dependencies. - -It is the recommended approach for using SQLite.swift in OSX CLI -applications. - - 1. Add the following to your `Package.swift` file: - - ```swift - dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.12.0") - ] - ``` - - 2. Build your project: - - ```sh - $ swift build - ``` - -[Swift Package Manager]: https://swift.org/package-manager - ### Manual To install SQLite.swift as an Xcode sub-project: @@ -266,8 +266,8 @@ var path = NSSearchPathForDirectoriesInDomains( ).first! + "/" + Bundle.main.bundleIdentifier! // create parent directory iff it doesn’t exist -try FileManager.default.createDirectoryAtPath( - path, withIntermediateDirectories: true, attributes: nil +try FileManager.default.createDirectory( +atPath: path, withIntermediateDirectories: true, attributes: nil ) let db = try Connection("\(path)/db.sqlite3") @@ -638,6 +638,18 @@ do { } ``` +Multiple rows can be inserted at once by similarily calling `insertMany` with an array of per-row [setters](#setters). + +```swift +do { + let rowid = try db.run(users.insertMany([mail <- "alice@mac.com"], [email <- "geoff@mac.com"])) + print("inserted id: \(rowid)") +} catch { + print("insertion failed: \(error)") +} +``` + + The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions follow similar patterns. @@ -955,8 +967,10 @@ equate or compare different types will prevent compilation. | `~=` | `(Interval, Comparable) -> Bool` | `BETWEEN` | | `&&` | `Bool -> Bool` | `AND` | | `\|\|`| `Bool -> Bool` | `OR` | +| `===` | `Equatable -> Bool` | `IS` | +| `!==` | `Equatable -> Bool` | `IS NOT` | -> *When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` +> * When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` > accordingly. @@ -1098,6 +1112,33 @@ let count = try db.scalar(users.filter(name != nil).count) > // SELECT count(DISTINCT "name") FROM "users" > ``` +## Upserting Rows + +We can upsert rows into a table by calling a [query’s](#queries) `upsert` +function with a list of [setters](#setters)—typically [typed column +expressions](#expressions) and values (which can also be expressions)—each +joined by the `<-` operator. Upserting is like inserting, except if there is a +conflict on the specified column value, SQLite will perform an update on the row instead. + +```swift +try db.run(users.upsert(email <- "alice@mac.com", name <- "Alice"), onConflictOf: email) +// INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice') ON CONFLICT (\"email\") DO UPDATE SET \"name\" = \"excluded\".\"name\" +``` + +The `upsert` function, when run successfully, returns an `Int64` representing +the inserted row’s [`ROWID`][ROWID]. + +```swift +do { + let rowid = try db.run(users.upsert(email <- "alice@mac.com", name <- "Alice", onConflictOf: email)) + print("inserted id: \(rowid)") +} catch { + print("insertion failed: \(error)") +} +``` + +The [`insert`](#inserting-rows), [`update`](#updating-rows), and [`delete`](#deleting-rows) functions +follow similar patterns. ## Updating Rows @@ -1557,7 +1598,7 @@ Both of the above methods also have the following optional parameter: There are a few restrictions on using Codable types: - The encodable and decodable objects can only use the following types: - - Int, Bool, Float, Double, String + - Int, Bool, Float, Double, String, Date - Nested Codable types that will be encoded as JSON to a single column - These methods will not handle object relationships for you. You must write your own Codable and Decodable implementations if you wish to support this. @@ -1792,12 +1833,12 @@ let config = FTS5Config() .column(subject) .column(body, [.unindexed]) -try db.run(emails.create(.FTS5(config)) +try db.run(emails.create(.FTS5(config))) // CREATE VIRTUAL TABLE "emails" USING fts5("subject", "body" UNINDEXED) // Note that FTS5 uses a different syntax to select columns, so we need to rewrite // the last FTS4 query above as: -let replies = emails.filter(emails.match("subject:\"Re:\"*)) +let replies = emails.filter(emails.match("subject:\"Re:\"*")) // SELECT * FROM "emails" WHERE "emails" MATCH 'subject:"Re:"*' // https://www.sqlite.org/fts5.html#_changes_to_select_statements_ diff --git a/Documentation/Planning.md b/Documentation/Planning.md index 5f885de8..62df1f24 100644 --- a/Documentation/Planning.md +++ b/Documentation/Planning.md @@ -33,6 +33,3 @@ be referred to when it comes time to add the corresponding feature._ _Features that are not actively being considered, perhaps because of no clean type-safe way to implement them with the current Swift, or bugs, or just general uncertainty._ - - * provide a mechanism for INSERT INTO multiple values, per - [#168](https://github.com/stephencelis/SQLite.swift/issues/168) diff --git a/Makefile b/Makefile index 50d07148..f3f355ab 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ BUILD_TOOL = xcodebuild BUILD_SCHEME = SQLite Mac -IOS_SIMULATOR = iPhone XS -IOS_VERSION = 12.2 +IOS_SIMULATOR = iPhone 12 +IOS_VERSION = 14.4 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 430ae6f2..7522fa45 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,53 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.3 import PackageDescription let package = Package( name: "SQLite.swift", - products: [.library(name: "SQLite", targets: ["SQLite"])], - targets: [ - .target(name: "SQLite", dependencies: ["SQLiteObjc"]), - .target(name: "SQLiteObjc"), - .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests") + products: [ + .library( + name: "SQLite", + targets: ["SQLite"] + ) ], - swiftLanguageVersions: [4, 5] + targets: [ + .target( + name: "SQLite", + dependencies: ["SQLiteObjc"], + exclude: [ + "Info.plist" + ] + ), + .target( + name: "SQLiteObjc", + dependencies: [], + exclude: [ + "fts3_tokenizer.h" + ] + ), + .testTarget( + name: "SQLiteTests", + dependencies: [ + "SQLite" + ], + path: "Tests/SQLiteTests", + exclude: [ + "Info.plist" + ], + resources: [ + .copy("fixtures/encrypted-3.x.sqlite"), + .copy("fixtures/encrypted-4.x.sqlite") + ] + ) + ] ) #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" - ]) - ] +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 b7a18e0b..98a625c4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SQLite.swift -[![Build Status][TravisBadge]][TravisLink] [![CocoaPods Version][CocoaPodsVersionBadge]][CocoaPodsVersionLink] [![Swift5 compatible][Swift5Badge]][Swift5Link] [![Platform][PlatformBadge]][PlatformLink] [![Carthage compatible][CartagheBadge]][CarthageLink] [![Join the chat at https://gitter.im/stephencelis/SQLite.swift][GitterBadge]][GitterLink] +![Build Status][GitHubActionBadge] [![CocoaPods Version][CocoaPodsVersionBadge]][CocoaPodsVersionLink] [![Swift5 compatible][Swift5Badge]][Swift5Link] [![Platform][PlatformBadge]][PlatformLink] [![Carthage compatible][CartagheBadge]][CarthageLink] [![Join the chat at https://gitter.im/stephencelis/SQLite.swift][GitterBadge]][GitterLink] A type-safe, [Swift][]-language layer over [SQLite3][]. @@ -34,67 +34,79 @@ syntax _and_ intent. ```swift import SQLite -let db = try Connection("path/to/db.sqlite3") - -let users = Table("users") -let id = Expression("id") -let name = Expression("name") -let email = Expression("email") - -try db.run(users.create { t in - t.column(id, primaryKey: true) - t.column(name) - t.column(email, unique: true) -}) -// CREATE TABLE "users" ( -// "id" INTEGER PRIMARY KEY NOT NULL, -// "name" TEXT, -// "email" TEXT NOT NULL UNIQUE -// ) - -let insert = users.insert(name <- "Alice", email <- "alice@mac.com") -let rowid = try db.run(insert) -// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') - -for user in try db.prepare(users) { - print("id: \(user[id]), name: \(user[name]), email: \(user[email])") - // id: 1, name: Optional("Alice"), email: alice@mac.com +// Wrap everything in a do...catch to handle errors +do { + let db = try Connection("path/to/db.sqlite3") + + let users = Table("users") + let id = Expression("id") + let name = Expression("name") + let email = Expression("email") + + try db.run(users.create { t in + t.column(id, primaryKey: true) + t.column(name) + t.column(email, unique: true) + }) + // CREATE TABLE "users" ( + // "id" INTEGER PRIMARY KEY NOT NULL, + // "name" TEXT, + // "email" TEXT NOT NULL UNIQUE + // ) + + let insert = users.insert(name <- "Alice", email <- "alice@mac.com") + let rowid = try db.run(insert) + // INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') + + for user in try db.prepare(users) { + print("id: \(user[id]), name: \(user[name]), email: \(user[email])") + // id: 1, name: Optional("Alice"), email: alice@mac.com + } + // SELECT * FROM "users" + + let alice = users.filter(id == rowid) + + try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) + // UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') + // WHERE ("id" = 1) + + try db.run(alice.delete()) + // DELETE FROM "users" WHERE ("id" = 1) + + try db.scalar(users.count) // 0 + // SELECT count(*) FROM "users" +} catch { + print (error) } -// SELECT * FROM "users" - -let alice = users.filter(id == rowid) - -try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) -// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') -// WHERE ("id" = 1) - -try db.run(alice.delete()) -// DELETE FROM "users" WHERE ("id" = 1) - -try db.scalar(users.count) // 0 -// SELECT count(*) FROM "users" ``` SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C API. ```swift -let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") -for email in ["betty@icloud.com", "cathy@icloud.com"] { - try stmt.run(email) -} - -db.totalChanges // 3 -db.changes // 1 -db.lastInsertRowid // 3 - -for row in try db.prepare("SELECT id, email FROM users") { - print("id: \(row[0]), email: \(row[1])") - // id: Optional(2), email: Optional("betty@icloud.com") - // id: Optional(3), email: Optional("cathy@icloud.com") +// Wrap everything in a do...catch to handle errors +do { + // ... + + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") + for email in ["betty@icloud.com", "cathy@icloud.com"] { + try stmt.run(email) + } + + db.totalChanges // 3 + db.changes // 1 + db.lastInsertRowid // 3 + + for row in try db.prepare("SELECT id, email FROM users") { + print("id: \(row[0]), email: \(row[1])") + // id: Optional(2), email: Optional("betty@icloud.com") + // id: Optional(3), email: Optional("cathy@icloud.com") + } + + try db.scalar("SELECT count(*) FROM users") // 2 +} catch { + print (error) } - -try db.scalar("SELECT count(*) FROM users") // 2 ``` [Read the documentation][See Documentation] or explore more, @@ -112,7 +124,28 @@ and the [companion repository][SQLiteDataAccessLayer2]. ## Installation -> _Note:_ Version 0.12 requires Swift 5 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.2) or greater. Version 0.11.6 requires Swift 4.2 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.1) or greater. +> _Note:_ Version 0.11.6 and later requires Swift 5 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.2) or greater. Version 0.11.5 requires Swift 4.2 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.1) or greater. + +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of +Swift code. + +1. Add the following to your `Package.swift` file: + + ```swift + dependencies: [ + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0") + ] + ``` + +2. Build your project: + + ```sh + $ swift build + ``` + +[Swift Package Manager]: https://swift.org/package-manager ### Carthage @@ -124,7 +157,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.12.0 + github "stephencelis/SQLite.swift" ~> 0.13.0 ``` 3. Run `carthage update` and @@ -156,7 +189,7 @@ SQLite.swift with CocoaPods: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.12.0' + pod 'SQLite.swift', '~> 0.13.0' end ``` @@ -165,27 +198,6 @@ SQLite.swift with CocoaPods: [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started -### Swift Package Manager - -The [Swift Package Manager][] is a tool for managing the distribution of -Swift code. - -1. Add the following to your `Package.swift` file: - - ```swift - dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.12.0") - ] - ``` - -2. Build your project: - - ```sh - $ swift build - ``` - -[Swift Package Manager]: https://swift.org/package-manager - ### Manual To install SQLite.swift as an Xcode sub-project: @@ -253,6 +265,7 @@ These projects enhance or use SQLite.swift: - [SQLiteMigrationManager.swift][] (inspired by [FMDBMigrationManager][]) + - [Delta: Math helper](https://apps.apple.com/app/delta-math-helper/id1436506800) (see [Delta/Utils/Database.swift](https://github.com/GroupeMINASTE/Delta-iOS/blob/master/Delta/Utils/Database.swift) for production implementation example) ## Alternatives @@ -270,8 +283,7 @@ Looking for something else? Try another Swift wrapper (or [FMDB][]): [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 +[GitHubActionBadge]: https://img.shields.io/github/workflow/status/stephencelis/SQLite.swift/Build%20and%20test [CocoaPodsVersionBadge]: https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png [CocoaPodsVersionLink]: http://cocoadocs.org/docsets/SQLite.swift diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec index 2341cfc4..499012c1 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.12.2" + s.version = "0.13.0" s.summary = "A type-safe, Swift-language layer over SQLite3 for iOS and macOS." s.description = <<-DESC @@ -16,13 +16,13 @@ Pod::Spec.new do |s| s.module_name = 'SQLite' s.default_subspec = 'standard' - s.swift_versions = ['4.2', '5'] + s.swift_versions = ['5'] - ios_deployment_target = '8.0' + ios_deployment_target = '9.0' tvos_deployment_target = '9.1' - osx_deployment_target = '10.10' - watchos_deployment_target = '2.2' + osx_deployment_target = '10.15' + watchos_deployment_target = '3.0' s.ios.deployment_target = ios_deployment_target s.tvos.deployment_target = tvos_deployment_target @@ -32,7 +32,7 @@ Pod::Spec.new do |s| 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.private_header_files = 'Sources/SQLiteObjc/fts3_tokenizer.h' ss.library = 'sqlite3' ss.test_spec 'tests' do |test_spec| @@ -47,10 +47,11 @@ Pod::Spec.new do |s| 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.private_header_files = 'Sources/SQLiteObjc/fts3_tokenizer.h' ss.xcconfig = { - 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE' + 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_SWIFT_STANDALONE=1' } ss.dependency 'sqlite3' @@ -65,12 +66,12 @@ Pod::Spec.new do |s| s.subspec 'SQLCipher' do |ss| ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' - ss.private_header_files = 'Sources/SQLiteObjc/*.h' + ss.private_header_files = 'Sources/SQLiteObjc/fts3_tokenizer.h' ss.xcconfig = { 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_SQLCIPHER', - 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1' + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1 SQLITE_SWIFT_SQLCIPHER=1' } - ss.dependency 'SQLCipher', '>= 3.4.0' + ss.dependency 'SQLCipher', '>= 4.0.0' ss.test_spec 'tests' do |test_spec| test_spec.resources = 'Tests/SQLiteTests/fixtures/*' diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index 30a40214..3056bd74 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; - 03A65E751C6BB2DF0062603F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A65E761C6BB2E60062603F /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; 03A65E771C6BB2E60062603F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; @@ -101,13 +100,19 @@ 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; 3D67B3F91DB246E700A4F4C6 /* SQLiteObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLiteObjc.m */; }; - 3D67B3FB1DB2470600A4F4C6 /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLiteObjc.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 */; }; + 3DDC112F26CDBA0200CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DDC113626CDBE1900CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DDC113726CDBE1900CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DDC113826CDBE1C00CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + D4DB368C20C09CFB00D5A58E /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB368A20C09C9B00D5A58E /* SelectTests.swift */; }; + D4DB368D20C09CFC00D5A58E /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB368A20C09C9B00D5A58E /* SelectTests.swift */; }; + D4DB368E20C09CFD00D5A58E /* SelectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB368A20C09C9B00D5A58E /* SelectTests.swift */; }; 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 */; }; @@ -180,8 +185,6 @@ EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; - EE91808E1C46E5230038162A /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE91808F1C46E76D0038162A /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLiteObjc.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 */ @@ -226,8 +229,10 @@ 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 = ""; }; 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS3.0.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SQLiteObjc.h; path = ../SQLiteObjc/include/SQLiteObjc.h; sourceTree = ""; }; 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; }; + D4DB368A20C09C9B00D5A58E /* SelectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTests.swift; sourceTree = ""; }; EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = ""; }; EE247AD81C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -271,13 +276,11 @@ EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests Mac.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; EE247B771C3F40D700AE3E12 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; - EE247B8C1C3F821200AE3E12 /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = ""; }; EE247B8D1C3F821200AE3E12 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; EE247B8F1C3F822500AE3E12 /* Index.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Index.md; sourceTree = ""; }; EE247B911C3F822500AE3E12 /* installation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "installation@2x.png"; sourceTree = ""; }; EE247B921C3F822600AE3E12 /* playground@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playground@2x.png"; sourceTree = ""; }; EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SQLite.swift.podspec; sourceTree = ""; }; - EE91808D1C46E5230038162A /* SQLiteObjc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SQLiteObjc.h; path = ../../SQLiteObjc/include/SQLiteObjc.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; }; /* End PBXFileReference section */ @@ -381,6 +384,7 @@ isa = PBXGroup; children = ( EE247AD61C3F04ED00AE3E12 /* SQLite.h */, + 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */, EE247AF71C3F06E900AE3E12 /* Foundation.swift */, EE247AF81C3F06E900AE3E12 /* Helpers.swift */, EE247AD81C3F04ED00AE3E12 /* Info.plist */, @@ -418,6 +422,7 @@ 19A17B93B48B5560E6E51791 /* Fixtures.swift */, 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */, 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */, + D4DB368A20C09C9B00D5A58E /* SelectTests.swift */, ); name = SQLiteTests; path = Tests/SQLiteTests; @@ -426,7 +431,6 @@ EE247AED1C3F06E900AE3E12 /* Core */ = { isa = PBXGroup; children = ( - EE91808D1C46E5230038162A /* SQLiteObjc.h */, EE247AEE1C3F06E900AE3E12 /* Blob.swift */, EE247AEF1C3F06E900AE3E12 /* Connection.swift */, EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */, @@ -473,7 +477,6 @@ EE247B771C3F40D700AE3E12 /* README.md */, EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, - EE247B8C1C3F821200AE3E12 /* .travis.yml */, EE247B8D1C3F821200AE3E12 /* Makefile */, EE9180931C46EA210038162A /* libsqlite3.tbd */, EE9180911C46E9D30038162A /* libsqlite3.tbd */, @@ -509,7 +512,7 @@ buildActionMask = 2147483647; files = ( 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */, - 03A65E751C6BB2DF0062603F /* SQLiteObjc.h in Headers */, + 3DDC113626CDBE1900CE369F /* SQLiteObjc.h in Headers */, 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -518,8 +521,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3D67B3FB1DB2470600A4F4C6 /* SQLiteObjc.h in Headers */, 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, + 3DDC113726CDBE1900CE369F /* SQLiteObjc.h in Headers */, 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -528,8 +531,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - EE91808E1C46E5230038162A /* SQLiteObjc.h in Headers */, EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */, + 3DDC113826CDBE1C00CE369F /* SQLiteObjc.h in Headers */, EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -539,8 +542,8 @@ buildActionMask = 2147483647; files = ( EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */, + 3DDC112F26CDBA0200CE369F /* SQLiteObjc.h in Headers */, EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, - EE91808F1C46E76D0038162A /* SQLiteObjc.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -680,7 +683,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1250; TargetAttributes = { 03A65E591C6BB0F50062603F = { CreatedOnToolsVersion = 7.2; @@ -847,6 +850,7 @@ 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */, 19A1785195182AF8731A8BDA /* RowTests.swift in Sources */, 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.swift in Sources */, + D4DB368E20C09CFD00D5A58E /* SelectTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -935,6 +939,7 @@ 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */, 19A1720B67ED13E6150C6A3D /* RowTests.swift in Sources */, 19A17C80076860CF7751A056 /* DateAndTimeFunctionTests.swift in Sources */, + D4DB368C20C09CFB00D5A58E /* SelectTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -993,6 +998,7 @@ 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */, 19A171967CC511C4F6F773C9 /* RowTests.swift in Sources */, 19A172EB202970561E5C4245 /* DateAndTimeFunctionTests.swift in Sources */, + D4DB368D20C09CFC00D5A58E /* SelectTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1033,7 +1039,7 @@ PRODUCT_NAME = SQLite; SDKROOT = appletvos; SKIP_INSTALL = YES; - TVOS_DEPLOYMENT_TARGET = 9.1; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; @@ -1053,7 +1059,7 @@ PRODUCT_NAME = SQLite; SDKROOT = appletvos; SKIP_INSTALL = YES; - TVOS_DEPLOYMENT_TARGET = 9.1; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -1065,7 +1071,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - TVOS_DEPLOYMENT_TARGET = 9.1; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; @@ -1077,7 +1083,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; - TVOS_DEPLOYMENT_TARGET = 9.1; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -1099,7 +1105,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.2; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; @@ -1121,7 +1127,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.2; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; @@ -1148,6 +1154,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -1173,8 +1180,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = ""; @@ -1210,6 +1217,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -1229,8 +1237,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MACOSX_DEPLOYMENT_TARGET = 10.9; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = ""; SDKROOT = iphoneos; @@ -1255,8 +1263,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 0.13.0; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; @@ -1276,8 +1285,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 0.13.0; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; @@ -1288,7 +1298,6 @@ isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1299,7 +1308,6 @@ isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1320,7 +1328,6 @@ INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; @@ -1343,7 +1350,6 @@ INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SDKROOT = macosx; @@ -1359,7 +1365,6 @@ COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1373,7 +1378,6 @@ COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme index 4e94e80a..a0db21a2 100644 --- a/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme +++ b/SQLite.xcodeproj/xcshareddata/xcschemes/SQLite Mac.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -39,17 +48,6 @@ - - - - - - - - + + + + @@ -39,17 +48,6 @@ - - - - - - - - + + + + @@ -39,17 +48,6 @@ - - - - - - - - - - - - Statement { return try prepare(statement).run(bindings) } + + // MARK: - VACUUM + + /// Run a vacuum on the database + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func vacuum() throws -> Statement { + return try run("VACUUM") + } // MARK: - Scalar @@ -415,17 +426,17 @@ public final class Connection { /// /// db.trace { SQL in print(SQL) } public func trace(_ callback: ((String) -> Void)?) { - #if SQLITE_SWIFT_SQLCIPHER || os(Linux) + if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { + trace_v2(callback) + } else { trace_v1(callback) - #else - if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { - trace_v2(callback) - } else { - trace_v1(callback) - } - #endif + } } + @available(OSX, deprecated: 10.2) + @available(iOS, deprecated: 10.0) + @available(watchOS, deprecated: 3.0) + @available(tvOS, deprecated: 10.0) fileprivate func trace_v1(_ callback: ((String) -> Void)?) { guard let callback = callback else { sqlite3_trace(handle, nil /* xCallback */, nil /* pCtx */) @@ -447,8 +458,36 @@ public final class Connection { trace = box } + @available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) + fileprivate func trace_v2(_ callback: ((String) -> Void)?) { + guard let callback = callback else { + // If the X callback is NULL or if the M mask is zero, then tracing is disabled. + sqlite3_trace_v2(handle, 0 /* mask */, nil /* xCallback */, nil /* pCtx */) + trace = nil + return + } - + let box: Trace = { (pointer: UnsafeRawPointer) in + callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) + } + sqlite3_trace_v2(handle, UInt32(SQLITE_TRACE_STMT) /* mask */, + { + // A trace callback is invoked with four arguments: callback(T,C,P,X). + // The T argument is one of the SQLITE_TRACE constants to indicate why the + // callback was invoked. The C argument is a copy of the context pointer. + // The P and X arguments are pointers whose meanings depend on T. + (T: UInt32, C: UnsafeMutableRawPointer?, P: UnsafeMutableRawPointer?, X: UnsafeMutableRawPointer?) in + if let P = P, + let expandedSQL = sqlite3_expanded_sql(OpaquePointer(P)) { + unsafeBitCast(C, to: Trace.self)(expandedSQL) + sqlite3_free(expandedSQL) + } + return Int32(0) // currently ignored + }, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self) /* pCtx */ + ) + trace = box + } fileprivate typealias Trace = @convention(block) (UnsafeRawPointer) -> Void fileprivate var trace: Trace? @@ -712,39 +751,3 @@ extension Result : CustomStringConvertible { } } } - -#if !SQLITE_SWIFT_SQLCIPHER && !os(Linux) -@available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) -extension Connection { - fileprivate func trace_v2(_ callback: ((String) -> Void)?) { - guard let callback = callback else { - // If the X callback is NULL or if the M mask is zero, then tracing is disabled. - sqlite3_trace_v2(handle, 0 /* mask */, nil /* xCallback */, nil /* pCtx */) - trace = nil - return - } - - let box: Trace = { (pointer: UnsafeRawPointer) in - callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) - } - sqlite3_trace_v2(handle, - UInt32(SQLITE_TRACE_STMT) /* mask */, - { - // A trace callback is invoked with four arguments: callback(T,C,P,X). - // The T argument is one of the SQLITE_TRACE constants to indicate why the - // callback was invoked. The C argument is a copy of the context pointer. - // The P and X arguments are pointers whose meanings depend on T. - (T: UInt32, C: UnsafeMutableRawPointer?, P: UnsafeMutableRawPointer?, X: UnsafeMutableRawPointer?) in - if let P = P, - let expandedSQL = sqlite3_expanded_sql(OpaquePointer(P)) { - unsafeBitCast(C, to: Trace.self)(expandedSQL) - sqlite3_free(expandedSQL) - } - return Int32(0) // currently ignored - }, - unsafeBitCast(box, to: UnsafeMutableRawPointer.self) /* pCtx */ - ) - trace = box - } -} -#endif diff --git a/Sources/SQLite/Core/Statement.swift b/Sources/SQLite/Core/Statement.swift index dc91d3d8..237d4812 100644 --- a/Sources/SQLite/Core/Statement.swift +++ b/Sources/SQLite/Core/Statement.swift @@ -208,7 +208,7 @@ public protocol FailableIterator : IteratorProtocol { extension FailableIterator { public func next() -> Element? { - return try! failableNext() + return try? failableNext() } } diff --git a/Sources/SQLite/Extensions/FTS4.swift b/Sources/SQLite/Extensions/FTS4.swift index 5ef84dd7..f0184565 100644 --- a/Sources/SQLite/Extensions/FTS4.swift +++ b/Sources/SQLite/Extensions/FTS4.swift @@ -194,25 +194,25 @@ open class FTSConfig { } /// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer) - open func tokenizer(_ tokenizer: Tokenizer?) -> Self { + @discardableResult open func tokenizer(_ tokenizer: Tokenizer?) -> Self { self.tokenizer = tokenizer return self } /// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6) - open func prefix(_ prefix: [Int]) -> Self { + @discardableResult open func prefix(_ prefix: [Int]) -> Self { self.prefixes += prefix return self } /// [The content= option](https://www.sqlite.org/fts3.html#section_6_2) - open func externalContent(_ schema: SchemaType) -> Self { + @discardableResult open func externalContent(_ schema: SchemaType) -> Self { self.externalContentSchema = schema return self } /// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1) - open func contentless() -> Self { + @discardableResult open func contentless() -> Self { self.isContentless = true return self } @@ -308,31 +308,31 @@ open class FTS4Config : FTSConfig { } /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) - open func compress(_ functionName: String) -> Self { + @discardableResult open func compress(_ functionName: String) -> Self { self.compressFunction = functionName return self } /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) - open func uncompress(_ functionName: String) -> Self { + @discardableResult open func uncompress(_ functionName: String) -> Self { self.uncompressFunction = functionName return self } /// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3) - open func languageId(_ columnName: String) -> Self { + @discardableResult open func languageId(_ columnName: String) -> Self { self.languageId = columnName return self } /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) - open func matchInfo(_ matchInfo: MatchInfo) -> Self { + @discardableResult open func matchInfo(_ matchInfo: MatchInfo) -> Self { self.matchInfo = matchInfo return self } /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) - open func order(_ order: Order) -> Self { + @discardableResult open func order(_ order: Order) -> Self { self.order = order return self } diff --git a/Sources/SQLite/Extensions/FTS5.swift b/Sources/SQLite/Extensions/FTS5.swift index 763927ff..cf13f3d8 100644 --- a/Sources/SQLite/Extensions/FTS5.swift +++ b/Sources/SQLite/Extensions/FTS5.swift @@ -58,19 +58,19 @@ open class FTS5Config : FTSConfig { } /// [External Content Tables](https://www.sqlite.org/fts5.html#section_4_4_2) - open func contentRowId(_ column: Expressible) -> Self { + @discardableResult open func contentRowId(_ column: Expressible) -> Self { self.contentRowId = column return self } /// [The Columnsize Option](https://www.sqlite.org/fts5.html#section_4_5) - open func columnSize(_ size: Int) -> Self { + @discardableResult open func columnSize(_ size: Int) -> Self { self.columnSize = size return self } /// [The Detail Option](https://www.sqlite.org/fts5.html#section_4_6) - open func detail(_ detail: Detail) -> Self { + @discardableResult open func detail(_ detail: Detail) -> Self { self.detail = detail return self } diff --git a/Sources/SQLite/Foundation.swift b/Sources/SQLite/Foundation.swift index cfb79bec..9986f581 100644 --- a/Sources/SQLite/Foundation.swift +++ b/Sources/SQLite/Foundation.swift @@ -68,3 +68,19 @@ public var dateFormatter: DateFormatter = { formatter.timeZone = TimeZone(secondsFromGMT: 0) return formatter }() + +extension UUID : Value { + + public static var declaredDatatype: String { + return String.declaredDatatype + } + + public static func fromDatatypeValue(_ stringValue: String) -> UUID { + return UUID(uuidString: stringValue)! + } + + public var datatypeValue: String { + return self.uuidString + } + +} diff --git a/Sources/SQLite/Helpers.swift b/Sources/SQLite/Helpers.swift index 115ea5c6..44aaec19 100644 --- a/Sources/SQLite/Helpers.swift +++ b/Sources/SQLite/Helpers.swift @@ -73,7 +73,11 @@ extension String { } func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { - let expression = Expression(" \(self) ".join([lhs, rhs]).expression) + return infix([lhs, rhs], wrap: wrap) + } + + func infix(_ terms: [Expressible], wrap: Bool = true) -> Expression { + let expression = Expression(" \(self) ".join(terms).expression) guard wrap else { return expression } diff --git a/Sources/SQLite/Info.plist b/Sources/SQLite/Info.plist index 2d956da2..ca23c84f 100644 --- a/Sources/SQLite/Info.plist +++ b/Sources/SQLite/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.12.2 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/Sources/SQLite/Typed/Coding.swift b/Sources/SQLite/Typed/Coding.swift index c3fb931b..7044bc37 100644 --- a/Sources/SQLite/Typed/Coding.swift +++ b/Sources/SQLite/Typed/Coding.swift @@ -38,12 +38,81 @@ extension QueryType { /// /// - otherSetters: Any other setters to include in the insert /// - /// - Returns: An `INSERT` statement fort the encodable object + /// - Returns: An `INSERT` statement for the encodable object public func insert(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert { let encoder = SQLiteEncoder(userInfo: userInfo) try encodable.encode(to: encoder) return self.insert(encoder.setters + otherSetters) } + + /// Creates an `INSERT` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// The onConflict will be passed to the actual insert function to define what should happen + /// when an error occurs during the insert operation. + /// + /// - Parameters: + /// + /// - onConlict: Define what happens when an insert operation fails + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `INSERT` statement fort the encodable object + public func insert(or onConflict: OnConflict, encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.insert(or: onConflict, encoder.setters + otherSetters) + } + + /// Creates a batch `INSERT` statement by encoding the array of given objects + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodables: Encodable objects to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the inserts, per row/object. + /// + /// - Returns: An `INSERT` statement for the encodable objects + public func insertMany(_ encodables: [Encodable], userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert { + let combinedSetters = try encodables.map { encodable -> [Setter] in + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return encoder.setters + otherSetters + } + return self.insertMany(combinedSetters) + } + + /// Creates an `INSERT ON CONFLICT DO UPDATE` statement, aka upsert, by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - onConflictOf: The column that if conflicts should trigger an update instead of insert. + /// + /// - Returns: An `INSERT` statement fort the encodable object + public func upsert(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = [], onConflictOf conflicting: Expressible) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.upsert(encoder.setters + otherSetters, onConflictOf: conflicting) + } /// Creates an `UPDATE` statement by encoding the given object /// This method converts any custom nested types to JSON data and does not handle any sort @@ -132,6 +201,9 @@ fileprivate class SQLiteEncoder: Encoder { if let data = value as? Data { self.encoder.setters.append(Expression(key.stringValue) <- data) } + else if let date = value as? Date { + self.encoder.setters.append(Expression(key.stringValue) <- date.datatypeValue) + } else { let encoded = try JSONEncoder().encode(value) let string = String(data: encoded, encoding: .utf8) @@ -152,7 +224,7 @@ fileprivate class SQLiteEncoder: Encoder { } func encode(_ value: Int64, forKey key: Key) throws { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int64 is not supported")) + self.encoder.setters.append(Expression(key.stringValue) <- value) } func encode(_ value: UInt, forKey key: Key) throws { @@ -249,7 +321,7 @@ fileprivate class SQLiteDecoder : Decoder { } 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")) + return try self.row.get(Expression(key.stringValue)) } func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { @@ -290,6 +362,10 @@ fileprivate class SQLiteDecoder : Decoder { let data = try self.row.get(Expression(key.stringValue)) return data as! T } + else if type == Date.self { + let date = try self.row.get(Expression(key.stringValue)) + return date 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")) } diff --git a/Sources/SQLite/Typed/Operators.swift b/Sources/SQLite/Typed/Operators.swift index b5637ea3..954a4af4 100644 --- a/Sources/SQLite/Typed/Operators.swift +++ b/Sources/SQLite/Typed/Operators.swift @@ -378,6 +378,33 @@ public func ==(lhs: V?, rhs: Expression) -> Expression whe return Operator.eq.infix(lhs, rhs) } +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: V) -> Expression where V.Datatype : Equatable { + return "IS".infix(lhs, rhs) +} +public func ===(lhs: Expression, rhs: V?) -> Expression where V.Datatype : Equatable { + guard let rhs = rhs else { return "IS".infix(lhs, Expression(value: nil)) } + return "IS".infix(lhs, rhs) +} +public func ===(lhs: V, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS".infix(lhs, rhs) +} +public func ===(lhs: V?, rhs: Expression) -> Expression where V.Datatype : Equatable { + guard let lhs = lhs else { return "IS".infix(Expression(value: nil), rhs) } + return "IS".infix(lhs, rhs) +} + public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { return Operator.neq.infix(lhs, rhs) } @@ -405,6 +432,34 @@ public func !=(lhs: V?, rhs: Expression) -> Expression whe return Operator.neq.infix(lhs, rhs) } +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: V) -> Expression where V.Datatype : Equatable { + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: Expression, rhs: V?) -> Expression where V.Datatype : Equatable { + guard let rhs = rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: V, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "IS NOT".infix(lhs, rhs) +} +public func !==(lhs: V?, rhs: Expression) -> Expression where V.Datatype : Equatable { + guard let lhs = lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } + return "IS NOT".infix(lhs, rhs) +} + + public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { return Operator.gt.infix(lhs, rhs) } @@ -547,6 +602,12 @@ public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expr // MARK: - +public func and(_ terms: Expression...) -> Expression { + return "AND".infix(terms) +} +public func and(_ terms: [Expression]) -> Expression { + return "AND".infix(terms) +} public func &&(lhs: Expression, rhs: Expression) -> Expression { return Operator.and.infix(lhs, rhs) } @@ -572,6 +633,12 @@ public func &&(lhs: Bool, rhs: Expression) -> Expression { return Operator.and.infix(lhs, rhs) } +public func or(_ terms: Expression...) -> Expression { + return "OR".infix(terms) +} +public func or(_ terms: [Expression]) -> Expression { + return "OR".infix(terms) +} public func ||(lhs: Expression, rhs: Expression) -> Expression { return Operator.or.infix(lhs, rhs) } diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index f6ef6df8..669001d3 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -631,6 +631,18 @@ extension QueryType { return insert(onConflict, values) } + public func insertMany( _ values: [[Setter]]) -> Insert { + return insertMany(nil, values) + } + + public func insertMany(or onConflict: OnConflict, _ values: [[Setter]]) -> Insert { + return insertMany(onConflict, values) + } + + public func insertMany(or onConflict: OnConflict, _ values: [Setter]...) -> Insert { + return insertMany(onConflict, values) + } + fileprivate func insert(_ or: OnConflict?, _ values: [Setter]) -> Insert { let insert = values.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in (insert.columns + [setter.column], insert.values + [setter.value]) @@ -650,6 +662,31 @@ extension QueryType { return Insert(" ".join(clauses.compactMap { $0 }).expression) } + fileprivate func insertMany(_ or: OnConflict?, _ values: [[Setter]]) -> Insert { + guard let firstInsert = values.first else { + // must be at least 1 object or else we don't know columns. Default to default inserts. + return insert() + } + let columns = firstInsert.map { $0.column } + let insertValues = values.map { rowValues in + rowValues.reduce([Expressible]()) { insert, setter in + insert + [setter.value] + } + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + or.map { Expression(literal: "OR \($0.rawValue)") }, + Expression(literal: "INTO"), + tableName(), + "".wrap(columns) as Expression, + Expression(literal: "VALUES"), + ", ".join(insertValues.map({ "".wrap($0) as Expression })), + whereClause + ] + return Insert(" ".join(clauses.compactMap { $0 }).expression) + } + /// Runs an `INSERT` statement against the query with `DEFAULT VALUES`. public func insert() -> Insert { return Insert(" ".join([ @@ -672,6 +709,45 @@ extension QueryType { query.expression ]).expression) } + + // MARK: UPSERT + + public func upsert(_ insertValues: Setter..., onConflictOf conflicting: Expressible) -> Insert { + return upsert(insertValues, onConflictOf: conflicting) + } + + public func upsert(_ insertValues: [Setter], onConflictOf conflicting: Expressible) -> Insert { + let setValues = insertValues.filter { $0.column.asSQL() != conflicting.asSQL() } + .map { Setter(excluded: $0.column) } + return upsert(insertValues, onConflictOf: conflicting, set: setValues) + } + + public func upsert(_ insertValues: Setter..., onConflictOf conflicting: Expressible, set setValues: [Setter]) -> Insert { + return upsert(insertValues, onConflictOf: conflicting, set: setValues) + } + + public func upsert(_ insertValues: [Setter], onConflictOf conflicting: Expressible, set setValues: [Setter]) -> Insert { + let insert = insertValues.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in + (insert.columns + [setter.column], insert.values + [setter.value]) + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + Expression(literal: "INTO"), + tableName(), + "".wrap(insert.columns) as Expression, + Expression(literal: "VALUES"), + "".wrap(insert.values) as Expression, + whereClause, + Expression(literal: "ON CONFLICT"), + "".wrap(conflicting) as Expression, + Expression(literal: "DO UPDATE SET"), + ", ".join(setValues.map { $0.expression }) + ] + + return Insert(" ".join(clauses.compactMap { $0 }).expression) + } + // MARK: UPDATE @@ -963,7 +1039,6 @@ extension Connection { try expandGlob(true)(q) continue column } - throw QueryError.noSuchTable(name: namespace) } throw QueryError.noSuchTable(name: namespace) } @@ -1010,6 +1085,8 @@ extension Connection { /// - SeeAlso: `QueryType.insert(value:_:)` /// - SeeAlso: `QueryType.insert(values:)` /// - SeeAlso: `QueryType.insert(or:_:)` + /// - SeeAlso: `QueryType.insertMany(values:)` + /// - SeeAlso: `QueryType.insertMany(or:_:)` /// - SeeAlso: `QueryType.insert()` /// /// - Parameter query: An insert query. diff --git a/Sources/SQLite/Typed/Setter.swift b/Sources/SQLite/Typed/Setter.swift index 86f16fca..3d3170f6 100644 --- a/Sources/SQLite/Typed/Setter.swift +++ b/Sources/SQLite/Typed/Setter.swift @@ -60,6 +60,11 @@ public struct Setter { self.value = Expression(value: value) } + init(excluded column: Expressible) { + let excluded = Expression("excluded") + self.column = column + self.value = ".".join([excluded, column.expression]) + } } extension Setter : Expressible { diff --git a/Sources/SQLiteObjc/include/SQLiteObjc.h b/Sources/SQLiteObjc/include/SQLiteObjc.h index f8c2a3b3..610cdf10 100644 --- a/Sources/SQLiteObjc/include/SQLiteObjc.h +++ b/Sources/SQLiteObjc/include/SQLiteObjc.h @@ -23,7 +23,13 @@ // @import Foundation; +#if defined(SQLITE_SWIFT_STANDALONE) +@import sqlite3; +#elif defined(SQLITE_SWIFT_SQLCIPHER) +@import SQLCipher; +#else @import SQLite3; +#endif NS_ASSUME_NONNULL_BEGIN typedef NSString * _Nullable (^_SQLiteTokenizerNextCallback)(const char *input, int *inputOffset, int *inputLength); diff --git a/Tests/Carthage/Makefile b/Tests/Carthage/Makefile index f28eb25b..b44d85b9 100644 --- a/Tests/Carthage/Makefile +++ b/Tests/Carthage/Makefile @@ -2,15 +2,16 @@ CARTHAGE := /usr/local/bin/carthage CARTHAGE_PLATFORM := iOS CARTHAGE_CONFIGURATION := Release CARTHAGE_DIR := Carthage -CARTHAGE_ARGS := --no-use-binaries -CARTHAGE_TOOLCHAIN := com.apple.dt.toolchain.Swift_3_0 +CARTHAGE_ARGS := --no-use-binaries --use-xcframeworks +# CARTHAGE_TOOLCHAIN := com.apple.dt.toolchain.Swift_3_0 +CARTHAGE_TOOLCHAIN := com.apple.dt.toolchain.XcodeDefault CARTHAGE_CMDLINE := --configuration $(CARTHAGE_CONFIGURATION) --platform $(CARTHAGE_PLATFORM) --toolchain $(CARTHAGE_TOOLCHAIN) $(CARTHAGE_ARGS) test: $(CARTHAGE) Cartfile $< bootstrap $(CARTHAGE_CMDLINE) Cartfile: - echo 'git "$(TRAVIS_BUILD_DIR)" "HEAD"' > $@ + echo 'git "$(GITHUB_WORKSPACE)" "HEAD"' > $@ clean: @rm -f Cartfile Cartfile.resolved diff --git a/Tests/CocoaPods/Gemfile b/Tests/CocoaPods/Gemfile index 77d90eec..da52ec89 100644 --- a/Tests/CocoaPods/Gemfile +++ b/Tests/CocoaPods/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -gem 'cocoapods', '~> 1.6.1' +gem 'cocoapods', '~> 1.10.2' gem 'minitest' diff --git a/Tests/CocoaPods/Gemfile.lock b/Tests/CocoaPods/Gemfile.lock index b5172144..e0383a67 100644 --- a/Tests/CocoaPods/Gemfile.lock +++ b/Tests/CocoaPods/Gemfile.lock @@ -1,77 +1,96 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.0) - activesupport (4.2.11) - i18n (~> 0.7) + CFPropertyList (3.0.3) + activesupport (5.2.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) atomos (0.1.3) - claide (1.0.2) - cocoapods (1.6.0.beta.2) - activesupport (>= 4.0.2, < 5) + claide (1.0.3) + cocoapods (1.10.2) + addressable (~> 2.6) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.6.0.beta.2) - cocoapods-deintegrate (>= 1.0.2, < 2.0) - cocoapods-downloader (>= 1.2.2, < 2.0) + cocoapods-core (= 1.10.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 1.4.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.3.1, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) - fourflusher (~> 2.0.1) + fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) molinillo (~> 0.6.6) nap (~> 1.0) - ruby-macho (~> 1.3, >= 1.3.1) - xcodeproj (>= 1.7.0, < 2.0) - cocoapods-core (1.6.0.beta.2) - activesupport (>= 4.0.2, < 6) + ruby-macho (~> 1.4) + xcodeproj (>= 1.19.0, < 2.0) + cocoapods-core (1.10.2) + activesupport (> 5.0, < 6) + addressable (~> 2.6) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) - cocoapods-deintegrate (1.0.2) - cocoapods-downloader (1.2.2) + netrc (~> 0.11) + public_suffix + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (1.4.0) cocoapods-plugins (1.0.0) nap - cocoapods-search (1.0.0) - cocoapods-stats (1.0.0) - cocoapods-trunk (1.3.1) + cocoapods-search (1.0.1) + cocoapods-trunk (1.5.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) - cocoapods-try (1.1.0) + cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.1.4) + concurrent-ruby (1.1.9) escape (0.0.4) - fourflusher (2.0.1) + ethon (0.14.0) + ffi (>= 1.15.0) + ffi (1.15.3) + fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - i18n (0.9.5) + httpclient (2.8.3) + i18n (1.8.10) concurrent-ruby (~> 1.0) + json (2.5.1) minitest (5.11.3) molinillo (0.6.6) - nanaimo (0.2.6) + nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - ruby-macho (1.3.1) + public_suffix (4.0.6) + rexml (3.2.5) + ruby-macho (1.4.0) thread_safe (0.3.6) - tzinfo (1.2.5) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.9) thread_safe (~> 0.1) - xcodeproj (1.7.0) + xcodeproj (1.21.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.6) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.6.0beta2) + cocoapods (~> 1.10.2) minitest BUNDLED WITH - 1.17.1 + 2.2.22 diff --git a/Tests/CocoaPods/integration_test.rb b/Tests/CocoaPods/integration_test.rb index 98a539b5..67b13385 100755 --- a/Tests/CocoaPods/integration_test.rb +++ b/Tests/CocoaPods/integration_test.rb @@ -13,7 +13,7 @@ def test_validate_project private def validator - @validator ||= CustomValidator.new(podspec, ['https://github.com/CocoaPods/Specs.git']).tap do |validator| + @validator ||= Pod::Validator.new(podspec, ['https://github.com/CocoaPods/Specs.git']).tap do |validator| validator.config.verbose = true validator.no_clean = true validator.use_frameworks = true @@ -32,39 +32,4 @@ def validator def podspec File.expand_path(File.dirname(__FILE__) + '/../../SQLite.swift.podspec') end - - - class CustomValidator < Pod::Validator - def test_pod - # https://github.com/CocoaPods/CocoaPods/issues/7009 - super unless consumer.platform_name == :watchos - end - - def xcodebuild(action, scheme, configuration) - require 'fourflusher' - command = %W(#{action} -workspace #{File.join(validation_dir, 'App.xcworkspace')} -scheme #{scheme} -configuration #{configuration}) - case consumer.platform_name - when :osx, :macos - command += %w(CODE_SIGN_IDENTITY=) - when :ios - command += %w(CODE_SIGN_IDENTITY=- -sdk iphonesimulator) - command += Fourflusher::SimControl.new.destination(nil, 'iOS', deployment_target) - when :watchos - command += %w(CODE_SIGN_IDENTITY=- -sdk watchsimulator) - command += Fourflusher::SimControl.new.destination(:oldest, 'watchOS', deployment_target) - when :tvos - command += %w(CODE_SIGN_IDENTITY=- -sdk appletvsimulator) - command += Fourflusher::SimControl.new.destination(:oldest, 'tvOS', deployment_target) - end - - begin - _xcodebuild(command, true) - rescue => e - message = 'Returned an unsuccessful exit code.' - message += ' You can use `--verbose` for more information.' unless config.verbose? - error('xcodebuild', message) - e.message - end - end - end end diff --git a/Tests/SQLiteTests/CipherTests.swift b/Tests/SQLiteTests/CipherTests.swift index abecffba..a27ac17d 100644 --- a/Tests/SQLiteTests/CipherTests.swift +++ b/Tests/SQLiteTests/CipherTests.swift @@ -88,6 +88,11 @@ class CipherTests: XCTestCase { try! FileManager.default.setAttributes([FileAttributeKey.immutable : 1], ofItemAtPath: encryptedFile) XCTAssertFalse(FileManager.default.isWritableFile(atPath: encryptedFile)) + defer { + // ensure file can be cleaned up afterwards + try! FileManager.default.setAttributes([FileAttributeKey.immutable : 0], ofItemAtPath: encryptedFile) + } + let conn = try! Connection(encryptedFile) try! conn.key("sqlcipher-test") XCTAssertEqual(1, try! conn.scalar("SELECT count(*) FROM foo") as? Int64) diff --git a/Tests/SQLiteTests/ConnectionTests.swift b/Tests/SQLiteTests/ConnectionTests.swift index eab3cf00..a05cceb5 100644 --- a/Tests/SQLiteTests/ConnectionTests.swift +++ b/Tests/SQLiteTests/ConnectionTests.swift @@ -38,12 +38,14 @@ class ConnectionTests : SQLiteTestCase { func test_init_withURI_returnsURIConnection() { let db = try! Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) - XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) + let url = URL(fileURLWithPath: db.description) + XCTAssertEqual(url.lastPathComponent, "SQLite.swift Tests.sqlite3") } func test_init_withString_returnsURIConnection() { let db = try! Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") - XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) + let url = URL(fileURLWithPath: db.description) + XCTAssertEqual(url.lastPathComponent, "SQLite.swift Tests.sqlite3") } func test_readonly_returnsFalseOnReadWriteConnections() { @@ -109,6 +111,10 @@ class ConnectionTests : SQLiteTestCase { try! db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) AssertSQL("SELECT * FROM users WHERE admin = 0", 4) } + + func test_vacuum() { + try! db.vacuum() + } func test_scalar_preparesRunsAndReturnsScalarValues() { XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) diff --git a/Tests/SQLiteTests/FoundationTests.swift b/Tests/SQLiteTests/FoundationTests.swift index dd80afc1..ba9685b7 100644 --- a/Tests/SQLiteTests/FoundationTests.swift +++ b/Tests/SQLiteTests/FoundationTests.swift @@ -13,4 +13,16 @@ class FoundationTests : XCTestCase { let data = Data.fromDatatypeValue(blob) XCTAssertEqual(Data([1, 2, 3]), data) } + + func testStringFromUUID() { + let uuid = UUID(uuidString: "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3")! + let string = uuid.datatypeValue + XCTAssertEqual("4ABE10C9-FF12-4CD4-90C1-4B429001BAD3", string) + } + + func testUUIDFromString() { + let string = "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3" + let uuid = UUID.fromDatatypeValue(string) + XCTAssertEqual(UUID(uuidString: "4ABE10C9-FF12-4CD4-90C1-4B429001BAD3"), uuid) + } } diff --git a/Tests/SQLiteTests/OperatorsTests.swift b/Tests/SQLiteTests/OperatorsTests.swift index 948eb0a4..c9665671 100644 --- a/Tests/SQLiteTests/OperatorsTests.swift +++ b/Tests/SQLiteTests/OperatorsTests.swift @@ -187,6 +187,34 @@ class OperatorsTests : XCTestCase { AssertSQL("(NULL IS \"boolOptional\")", nil == boolOptional) } + func test_isOperator_withEquatableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" IS \"bool\")", bool === bool) + AssertSQL("(\"bool\" IS \"boolOptional\")", bool === boolOptional) + AssertSQL("(\"boolOptional\" IS \"bool\")", boolOptional === bool) + AssertSQL("(\"boolOptional\" IS \"boolOptional\")", boolOptional === boolOptional) + AssertSQL("(\"bool\" IS 1)", bool === true) + AssertSQL("(\"boolOptional\" IS 1)", boolOptional === true) + AssertSQL("(1 IS \"bool\")", true === bool) + AssertSQL("(1 IS \"boolOptional\")", true === boolOptional) + + AssertSQL("(\"boolOptional\" IS NULL)", boolOptional === nil) + AssertSQL("(NULL IS \"boolOptional\")", nil === boolOptional) + } + + func test_isNotOperator_withEquatableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" IS NOT \"bool\")", bool !== bool) + AssertSQL("(\"bool\" IS NOT \"boolOptional\")", bool !== boolOptional) + AssertSQL("(\"boolOptional\" IS NOT \"bool\")", boolOptional !== bool) + AssertSQL("(\"boolOptional\" IS NOT \"boolOptional\")", boolOptional !== boolOptional) + AssertSQL("(\"bool\" IS NOT 1)", bool !== true) + AssertSQL("(\"boolOptional\" IS NOT 1)", boolOptional !== true) + AssertSQL("(1 IS NOT \"bool\")", true !== bool) + AssertSQL("(1 IS NOT \"boolOptional\")", true !== boolOptional) + + AssertSQL("(\"boolOptional\" IS NOT NULL)", boolOptional !== nil) + AssertSQL("(NULL IS NOT \"boolOptional\")", nil !== boolOptional) + } + func test_inequalityOperator_withEquatableExpressions_buildsBooleanExpression() { AssertSQL("(\"bool\" != \"bool\")", bool != bool) AssertSQL("(\"bool\" != \"boolOptional\")", bool != boolOptional) @@ -290,6 +318,16 @@ class OperatorsTests : XCTestCase { AssertSQL("(1 AND \"bool\")", true && bool) AssertSQL("(1 AND \"boolOptional\")", true && boolOptional) } + + func test_andFunction_withBooleanExpressions_buildsCompoundExpression() { + AssertSQL("(\"bool\" AND \"bool\" AND \"bool\")", and([bool, bool, bool])) + AssertSQL("(\"bool\" AND \"bool\")", and([bool, bool])) + AssertSQL("(\"bool\")", and([bool])) + + AssertSQL("(\"bool\" AND \"bool\" AND \"bool\")", and(bool, bool, bool)) + AssertSQL("(\"bool\" AND \"bool\")", and(bool, bool)) + AssertSQL("(\"bool\")", and(bool)) + } func test_doubleOrOperator_withBooleanExpressions_buildsCompoundExpression() { AssertSQL("(\"bool\" OR \"bool\")", bool || bool) @@ -301,6 +339,16 @@ class OperatorsTests : XCTestCase { AssertSQL("(1 OR \"bool\")", true || bool) AssertSQL("(1 OR \"boolOptional\")", true || boolOptional) } + + func test_orFunction_withBooleanExpressions_buildsCompoundExpression() { + AssertSQL("(\"bool\" OR \"bool\" OR \"bool\")", or([bool, bool, bool])) + AssertSQL("(\"bool\" OR \"bool\")", or([bool, bool])) + AssertSQL("(\"bool\")", or([bool])) + + AssertSQL("(\"bool\" OR \"bool\" OR \"bool\")", or(bool, bool, bool)) + AssertSQL("(\"bool\" OR \"bool\")", or(bool, bool)) + AssertSQL("(\"bool\")", or(bool)) + } func test_unaryNotOperator_withBooleanExpressions_buildsNotExpression() { AssertSQL("NOT (\"bool\")", !bool) diff --git a/Tests/SQLiteTests/QueryTests.swift b/Tests/SQLiteTests/QueryTests.swift index 2a9e4ecb..041b7d0c 100644 --- a/Tests/SQLiteTests/QueryTests.swift +++ b/Tests/SQLiteTests/QueryTests.swift @@ -247,25 +247,75 @@ class QueryTests : XCTestCase { ) } + func test_insert_many_compilesInsertManyExpression() { + AssertSQL( + "INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30), ('geoff@example.com', 32), ('alex@example.com', 83)", + users.insertMany([[email <- "alice@example.com", age <- 30], [email <- "geoff@example.com", age <- 32], [email <- "alex@example.com", age <- 83]]) + ) + } + func test_insert_many_compilesInsertManyNoneExpression() { + AssertSQL( + "INSERT INTO \"users\" DEFAULT VALUES", + users.insertMany([]) + ) + } + + func test_insert_many_withOnConflict_compilesInsertManyOrOnConflictExpression() { + AssertSQL( + "INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30), ('geoff@example.com', 32), ('alex@example.com', 83)", + users.insertMany(or: .replace, [[email <- "alice@example.com", age <- 30], [email <- "geoff@example.com", age <- 32], [email <- "alex@example.com", age <- 83]]) + ) + } + func test_insert_encodable() throws { let emails = Table("emails") - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) let insert = try emails.insert(value) AssertSQL( - "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\") VALUES (1, '2', 1, 3.0, 4.0)", + "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000')", insert ) } func test_insert_encodable_with_nested_encodable() throws { let emails = Table("emails") - let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: "optional", sub: value1) + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: "optional", sub: value1) let insert = try emails.insert(value) let encodedJSON = try JSONEncoder().encode(value1) let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! AssertSQL( - "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"optional\", \"sub\") VALUES (1, '2', 1, 3.0, 4.0, 'optional', '\(encodedJSONString)')", + "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"optional\", \"sub\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'optional', '\(encodedJSONString)')", + insert + ) + } + + func test_upsert_withOnConflict_compilesInsertOrOnConflictExpression() { + AssertSQL( + "INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30) ON CONFLICT (\"email\") DO UPDATE SET \"age\" = \"excluded\".\"age\"", + users.upsert(email <- "alice@example.com", age <- 30, onConflictOf: email) + ) + } + + func test_upsert_encodable() throws { + let emails = Table("emails") + let string = Expression("string") + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let insert = try emails.upsert(value, onConflictOf: string) + AssertSQL( + "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000') ON CONFLICT (\"string\") DO UPDATE SET \"int\" = \"excluded\".\"int\", \"bool\" = \"excluded\".\"bool\", \"float\" = \"excluded\".\"float\", \"double\" = \"excluded\".\"double\", \"date\" = \"excluded\".\"date\"", + insert + ) + } + + func test_insert_many_encodable() throws { + let emails = Table("emails") + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let value2 = TestCodable(int: 2, string: "3", bool: true, float: 3, double: 5, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let value3 = TestCodable(int: 3, string: "4", bool: true, float: 3, double: 6, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let insert = try emails.insertMany([value1, value2, value3]) + AssertSQL( + "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000'), (2, '3', 1, 3.0, 5.0, '1970-01-01T00:00:00.000'), (3, '4', 1, 3.0, 6.0, '1970-01-01T00:00:00.000')", insert ) } @@ -286,23 +336,23 @@ class QueryTests : XCTestCase { func test_update_encodable() throws { let emails = Table("emails") - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) let update = try emails.update(value) AssertSQL( - "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0", + "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, \"date\" = '1970-01-01T00:00:00.000'", update ) } func test_update_encodable_with_nested_encodable() throws { let emails = Table("emails") - let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) - let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: value1) + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: value1) let update = try emails.update(value) let encodedJSON = try JSONEncoder().encode(value1) let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! AssertSQL( - "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, \"sub\" = '\(encodedJSONString)'", + "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, \"date\" = '1970-01-01T00:00:00.000', \"sub\" = '\(encodedJSONString)'", update ) } @@ -378,6 +428,7 @@ class QueryIntegrationTests : SQLiteTestCase { let id = Expression("id") let email = Expression("email") + let age = Expression("age") override func setUp() { super.setUp() @@ -438,12 +489,13 @@ class QueryIntegrationTests : SQLiteTestCase { builder.column(Expression("bool")) builder.column(Expression("float")) builder.column(Expression("double")) + builder.column(Expression("date")) builder.column(Expression("optional")) builder.column(Expression("sub")) }) - let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) - let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, optional: "optional", sub: value1) + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil) + let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, date: Date(timeIntervalSince1970: 5000), optional: "optional", sub: value1) try db.run(table.insert(value)) @@ -455,12 +507,14 @@ class QueryIntegrationTests : SQLiteTestCase { XCTAssertEqual(values[0].bool, true) XCTAssertEqual(values[0].float, 7) XCTAssertEqual(values[0].double, 8) + XCTAssertEqual(values[0].date, Date(timeIntervalSince1970: 5000)) XCTAssertEqual(values[0].optional, "optional") XCTAssertEqual(values[0].sub?.int, 1) XCTAssertEqual(values[0].sub?.string, "2") XCTAssertEqual(values[0].sub?.bool, true) XCTAssertEqual(values[0].sub?.float, 3) XCTAssertEqual(values[0].sub?.double, 4) + XCTAssertEqual(values[0].sub?.date, Date(timeIntervalSince1970: 0)) XCTAssertNil(values[0].sub?.optional) XCTAssertNil(values[0].sub?.sub) } @@ -483,6 +537,26 @@ class QueryIntegrationTests : SQLiteTestCase { XCTAssertEqual(1, id) } + func test_insert_many() { + let id = try! db.run(users.insertMany([[email <- "alice@example.com"], [email <- "geoff@example.com"]])) + XCTAssertEqual(2, id) + } + + func test_upsert() throws { + guard db.satisfiesMinimumVersion(minor: 24) else { return } + let fetchAge = { () throws -> Int? in + try self.db.pluck(self.users.filter(self.email == "alice@example.com")).flatMap { $0[self.age] } + } + + let id = try db.run(users.upsert(email <- "alice@example.com", age <- 30, onConflictOf: email)) + XCTAssertEqual(1, id) + XCTAssertEqual(30, try fetchAge()) + + let nextId = try db.run(users.upsert(email <- "alice@example.com", age <- 42, onConflictOf: email)) + XCTAssertEqual(1, nextId) + XCTAssertEqual(42, try fetchAge()) + } + func test_update() { let changes = try! db.run(users.update(email <- "alice@example.com")) XCTAssertEqual(0, changes) @@ -492,24 +566,24 @@ 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) } @@ -540,3 +614,13 @@ class QueryIntegrationTests : SQLiteTestCase { } } } + +private extension Connection { + func satisfiesMinimumVersion(minor: Int, patch: Int = 0) -> Bool { + guard let version = try? scalar("SELECT sqlite_version()") as? String else { return false } + let components = version.split(separator: ".", maxSplits: 3).compactMap { Int($0) } + guard components.count == 3 else { return false } + + return components[1] >= minor && components[2] >= patch + } +} diff --git a/Tests/SQLiteTests/SelectTests.swift b/Tests/SQLiteTests/SelectTests.swift new file mode 100644 index 00000000..bca01092 --- /dev/null +++ b/Tests/SQLiteTests/SelectTests.swift @@ -0,0 +1,45 @@ +import XCTest +@testable import SQLite + +class SelectTests: SQLiteTestCase { + + override func setUp() { + super.setUp() + CreateUsersTable() + CreateUsersDataTable() + } + + func CreateUsersDataTable() { + try! db.execute(""" + CREATE TABLE users_name ( + id INTEGER, + user_id INTEGER REFERENCES users(id), + name TEXT + ) + """ + ) + } + + func test_select_columns_from_multiple_tables() { + let usersData = Table("users_name") + let users = Table("users") + + let name = Expression("name") + let id = Expression("id") + let userID = Expression("user_id") + let email = Expression("email") + + try! InsertUser("Joey") + try! db.run(usersData.insert( + id <- 1, + userID <- 1, + name <- "Joey" + )) + + try! db.prepare(users.select(name, email).join(usersData, on: userID == users[id])).forEach { + XCTAssertEqual($0[name], "Joey") + XCTAssertEqual($0[email], "Joey@example.com") + } + } + +} diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index 2e491b01..247013b4 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -26,6 +26,7 @@ class SQLiteTestCase : XCTestCase { salary REAL, admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), manager_id INTEGER, + created_at DATETIME, FOREIGN KEY(manager_id) REFERENCES users(id) ) """ @@ -111,15 +112,17 @@ class TestCodable: Codable { let bool: Bool let float: Float let double: Double + let date: Date let optional: String? let sub: TestCodable? - init(int: Int, string: String, bool: Bool, float: Float, double: Double, optional: String?, sub: TestCodable?) { + init(int: Int, string: String, bool: Bool, float: Float, double: Double, date: Date, optional: String?, sub: TestCodable?) { self.int = int self.string = string self.bool = bool self.float = float self.double = double + self.date = date self.optional = optional self.sub = sub }