diff --git a/CHANGELOG.md b/CHANGELOG.md index 49590fbe..2783a67d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ -0.14.0 (tbd), [diff][diff-0.14.0] +0.14.0 (27-10-2022), [diff][diff-0.14.0] ======================================== +For breaking changes, see [Upgrading.md](Documentation/Upgrading.md). * Support more complex schema changes and queries ([#1073][], [#1146][] [#1148][]) * Support `ATTACH`/`DETACH` ([#30][], [#1142][]) +* Expose connection flags (via `URIQueryParameter`) to open db ([#1074][])) * Support `WITH` clause ([#1139][]) * Add `Value` conformance for `NSURL` ([#1110][], [#1141][]) * Add decoding for `UUID` ([#1137][]) @@ -164,6 +166,7 @@ [#881]: https://github.com/stephencelis/SQLite.swift/pull/881 [#919]: https://github.com/stephencelis/SQLite.swift/pull/919 [#1073]: https://github.com/stephencelis/SQLite.swift/issues/1073 +[#1074]: https://github.com/stephencelis/SQLite.swift/issues/1074 [#1075]: https://github.com/stephencelis/SQLite.swift/pull/1075 [#1077]: https://github.com/stephencelis/SQLite.swift/issues/1077 [#1094]: https://github.com/stephencelis/SQLite.swift/pull/1094 diff --git a/Documentation/Index.md b/Documentation/Index.md index fa91f7b7..363443d6 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -9,6 +9,7 @@ - [Connecting to a Database](#connecting-to-a-database) - [Read-Write Databases](#read-write-databases) - [Read-Only Databases](#read-only-databases) + - [In a Shared Group Container](#in-a-shared-group-container) - [In-Memory Databases](#in-memory-databases) - [URI parameters](#uri-parameters) - [Thread-Safety](#thread-safety) @@ -41,14 +42,19 @@ - [Updating Rows](#updating-rows) - [Deleting Rows](#deleting-rows) - [Transactions and Savepoints](#transactions-and-savepoints) + - [Querying the Schema](#querying-the-schema) - [Altering the Schema](#altering-the-schema) - [Renaming Tables](#renaming-tables) + - [Dropping Tables](#dropping-tables) - [Adding Columns](#adding-columns) - [Added Column Constraints](#added-column-constraints) + - [Schema Changer](#schemachanger) + - [Renaming Columns](#renaming-columns) + - [Dropping Columns](#dropping-columns) + - [Renaming/dropping Tables](#renamingdropping-tables) - [Indexes](#indexes) - [Creating Indexes](#creating-indexes) - [Dropping Indexes](#dropping-indexes) - - [Dropping Tables](#dropping-tables) - [Migrations and Schema Versioning](#migrations-and-schema-versioning) - [Custom Types](#custom-types) - [Date-Time Values](#date-time-values) @@ -83,7 +89,7 @@ process of downloading, compiling, and linking dependencies. ```swift dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.3") + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.0") ] ``` @@ -104,7 +110,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.13.3 + github "stephencelis/SQLite.swift" ~> 0.14.0 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. @@ -134,7 +140,7 @@ install SQLite.swift with Carthage: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.13.3' + pod 'SQLite.swift', '~> 0.14.0' end ``` @@ -148,7 +154,7 @@ with the OS you can require the `standalone` subspec: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.13.3' + pod 'SQLite.swift/standalone', '~> 0.14.0' end ``` @@ -158,7 +164,7 @@ dependency to sqlite3 or one of its subspecs: ```ruby target 'YourAppTargetName' do - pod 'SQLite.swift/standalone', '~> 0.13.3' + pod 'SQLite.swift/standalone', '~> 0.14.0' pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled end ``` @@ -168,13 +174,13 @@ See the [sqlite3 podspec][sqlite3pod] for more details. #### Using SQLite.swift with SQLCipher If you want to use [SQLCipher][] with SQLite.swift you can require the -`SQLCipher` subspec in your Podfile: +`SQLCipher` subspec in your Podfile (SPM is not supported yet, see [#1084](https://github.com/stephencelis/SQLite.swift/issues/1084)): ```ruby target 'YourAppTargetName' do # Make sure you only require the subspec, otherwise you app might link against # the system SQLite, which means the SQLCipher-specific methods won't work. - pod 'SQLite.swift/SQLCipher', '~> 0.13.3' + pod 'SQLite.swift/SQLCipher', '~> 0.14.0' end ``` @@ -325,6 +331,13 @@ let db = try Connection(path, readonly: true) > We welcome changes to the above sample code to show how to successfully copy and use a bundled "seed" > database for writing in an app. +#### In a shared group container + +It is not recommend to store databases in a [shared group container], +some users have reported crashes ([#1042](https://github.com/stephencelis/SQLite.swift/issues/1042)). + +[shared group container]: https://developer.apple.com/documentation/foundation/filemanager/1412643-containerurl# + #### In-Memory Databases If you omit the path, SQLite.swift will provision an [in-memory @@ -1409,7 +1422,6 @@ for column in columns { SQLite.swift comes with several functions (in addition to `Table.create`) for altering a database schema in a type-safe manner. - ### Renaming Tables We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` @@ -1420,6 +1432,24 @@ try db.run(users.rename(Table("users_old"))) // ALTER TABLE "users" RENAME TO "users_old" ``` +### Dropping Tables + +We can build +[`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) +by calling the `dropTable` function on a `SchemaType`. + +```swift +try db.run(users.drop()) +// DROP TABLE "users" +``` + +The `drop` function has one additional parameter, `ifExists`, which (when +`true`) adds an `IF EXISTS` clause to the statement. + +```swift +try db.run(users.drop(ifExists: true)) +// DROP TABLE IF EXISTS "users" +``` ### Adding Columns @@ -1484,57 +1514,54 @@ tables](#creating-a-table). // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" ("id") ``` -### Renaming Columns +### SchemaChanger + +Version 0.14.0 introduces `SchemaChanger`, an alternative API to perform more complex +migrations such as renaming columns. These operations work with all versions of +SQLite but use SQL statements such as `ALTER TABLE RENAME COLUMN` when available. -We can rename columns with the help of the `SchemaChanger` class: +#### Adding Columns ```swift +let newColumn = ColumnDefinition( + name: "new_text_column", + type: .TEXT, + nullable: true, + defaultValue: .stringLiteral("foo") +) + let schemaChanger = SchemaChanger(connection: db) + try schemaChanger.alter(table: "users") { table in - table.rename(column: "old_name", to: "new_name") + table.add(newColumn) } ``` -### Dropping Columns +#### Renaming Columns ```swift let schemaChanger = SchemaChanger(connection: db) try schemaChanger.alter(table: "users") { table in - table.drop(column: "email") + table.rename(column: "old_name", to: "new_name") } ``` -These operations will work with all versions of SQLite and use modern SQL -operations such as `DROP COLUMN` when available. - -### Adding Columns (SchemaChanger) - -The `SchemaChanger` provides an alternative API to add new columns: +#### Dropping Columns ```swift -let newColumn = ColumnDefinition( - name: "new_text_column", - type: .TEXT, - nullable: true, - defaultValue: .stringLiteral("foo") -) - let schemaChanger = SchemaChanger(connection: db) - try schemaChanger.alter(table: "users") { table in - table.add(newColumn) + table.drop(column: "email") } ``` -### Renaming/dropping Tables (SchemaChanger) - -The `SchemaChanger` provides an alternative API to rename and drop tables: +#### Renaming/dropping Tables ```swift let schemaChanger = SchemaChanger(connection: db) try schemaChanger.rename(table: "users", to: "users_new") -try schemaChanger.drop(table: "emails") +try schemaChanger.drop(table: "emails", ifExists: false) ``` ### Indexes @@ -1592,25 +1619,6 @@ try db.run(users.dropIndex(email, ifExists: true)) // DROP INDEX IF EXISTS "index_users_on_email" ``` -### Dropping Tables - -We can build -[`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) -by calling the `dropTable` function on a `SchemaType`. - -```swift -try db.run(users.drop()) -// DROP TABLE "users" -``` - -The `drop` function has one additional parameter, `ifExists`, which (when -`true`) adds an `IF EXISTS` clause to the statement. - -```swift -try db.run(users.drop(ifExists: true)) -// DROP TABLE IF EXISTS "users" -``` - ### Migrations and Schema Versioning You can use the convenience property on `Connection` to query and set the @@ -2183,7 +2191,7 @@ try db.detach("external") // DETACH DATABASE 'external' ``` -When compiled for SQLCipher, you can additionally pass a `key` parameter to `attach`: +When compiled for SQLCipher, we can additionally pass a `key` parameter to `attach`: ```swift try db.attach(.uri("encrypted.sqlite"), as: "encrypted", key: "secret") diff --git a/Documentation/Release.md b/Documentation/Release.md index 87d715f6..8ec67970 100644 --- a/Documentation/Release.md +++ b/Documentation/Release.md @@ -3,6 +3,7 @@ * [ ] Make sure current master branch has a green build * [ ] Make sure `SQLite.playground` runs without errors * [ ] Make sure `CHANGELOG.md` is up-to-date +* [ ] Add content to `Documentation/Upgrading.md` if needed * [ ] Update the version number in `SQLite.swift.podspec` * [ ] Run `pod lib lint` locally * [ ] Update the version numbers mentioned in `README.md`, `Documentation/Index.md` diff --git a/Documentation/Upgrading.md b/Documentation/Upgrading.md new file mode 100644 index 00000000..8c398467 --- /dev/null +++ b/Documentation/Upgrading.md @@ -0,0 +1,11 @@ +# Upgrading + +## 0.13 → 0.14 + +- `Expression.asSQL()` is no longer available. Expressions now implement `CustomStringConvertible`, + where `description` returns the SQL. +- `Statement.prepareRowIterator()` is now longer available. Instead, use the methods + of the same name on `Connection`. +- `Blob` no longer wraps byte arrays and now uses `NSData`, which enables memory and + performance improvements. +- `Connection.registerTokenizer` is no longer available to register custom FTS4 tokenizers. diff --git a/README.md b/README.md index 1b04c7c8..0d9396c6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ syntax _and_ intent. - [Well-documented][See Documentation] - Extensively tested - [SQLCipher][] support via CocoaPods + - [Schema query/migration][] - Works on [Linux](Documentation/Linux.md) (with some limitations) - Active support at [StackOverflow](https://stackoverflow.com/questions/tagged/sqlite.swift), @@ -27,6 +28,7 @@ syntax _and_ intent. [SQLCipher]: https://www.zetetic.net/sqlcipher/ [Full-text search]: Documentation/Index.md#full-text-search +[Schema query/migration]: Documentation/Index.md#querying-the-schema [See Documentation]: Documentation/Index.md#sqliteswift-documentation @@ -115,17 +117,10 @@ interactively, from the Xcode project’s playground. ![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) -For a more comprehensive example, see -[this article][Create a Data Access Layer with SQLite.swift and Swift 2] -and the [companion repository][SQLiteDataAccessLayer2]. - - -[Create a Data Access Layer with SQLite.swift and Swift 2]: https://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html -[SQLiteDataAccessLayer2]: https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master - ## Installation -> _Note:_ Version 0.11.6 and later requires Swift 5 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.2) or greater. Version 0.11.5 requires Swift 4.2 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.1) or greater. +> _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 @@ -136,7 +131,7 @@ Swift code. ```swift dependencies: [ - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.3") + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.0") ] ``` @@ -160,7 +155,7 @@ install SQLite.swift with Carthage: 2. Update your Cartfile to include the following: ```ruby - github "stephencelis/SQLite.swift" ~> 0.13.3 + github "stephencelis/SQLite.swift" ~> 0.14.0 ``` 3. Run `carthage update` and @@ -191,7 +186,7 @@ SQLite.swift with CocoaPods: use_frameworks! target 'YourAppTargetName' do - pod 'SQLite.swift', '~> 0.13.3' + pod 'SQLite.swift', '~> 0.14.0' end ``` @@ -250,7 +245,7 @@ device: [Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork -## Author +## Original author - [Stephen Celis](mailto:stephen@stephencelis.com) ([@stephencelis](https://twitter.com/stephencelis)) @@ -267,19 +262,14 @@ These projects enhance or use SQLite.swift: - [SQLiteMigrationManager.swift][] (inspired by [FMDBMigrationManager][]) - - [Delta: Math helper](https://apps.apple.com/app/delta-math-helper/id1436506800) - (see [Delta/Utils/Database.swift](https://github.com/GroupeMINASTE/Delta-iOS/blob/master/Delta/Utils/Database.swift) for production implementation example) - ## Alternatives Looking for something else? Try another Swift wrapper (or [FMDB][]): - - [Camembert](https://github.com/remirobert/Camembert) - [GRDB](https://github.com/groue/GRDB.swift) - [SQLiteDB](https://github.com/FahimF/SQLiteDB) - [Squeal](https://github.com/nerdyc/Squeal) - - [SwiftData](https://github.com/ryanfowler/SwiftData) [Swift]: https://swift.org/ [SQLite3]: https://www.sqlite.org diff --git a/SQLite.playground/Contents.swift b/SQLite.playground/Contents.swift index 5a7ea379..0eafdd0d 100644 --- a/SQLite.playground/Contents.swift +++ b/SQLite.playground/Contents.swift @@ -52,6 +52,7 @@ for user in try Array(rowIterator) { /// also with `map()` let mapRowIterator = try db.prepareRowIterator(users) + let userIds = try mapRowIterator.map { $0[id] } /// using `failableNext()` on `RowIterator` diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec index d36d0330..c30aa765 100644 --- a/SQLite.swift.podspec +++ b/SQLite.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SQLite.swift" - s.version = "0.13.3" + s.version = "0.14.0" s.summary = "A type-safe, Swift-language layer over SQLite3." s.description = <<-DESC diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index 6fe89283..3d62e980 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -1497,7 +1497,7 @@ INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 0.13.3; + MARKETING_VERSION = 0.14.0; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; @@ -1520,7 +1520,7 @@ INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 0.13.3; + MARKETING_VERSION = 0.14.0; PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; PRODUCT_NAME = SQLite; SKIP_INSTALL = YES; diff --git a/Sources/SQLite/Schema/SchemaChanger.swift b/Sources/SQLite/Schema/SchemaChanger.swift index 67f2b0d3..af710908 100644 --- a/Sources/SQLite/Schema/SchemaChanger.swift +++ b/Sources/SQLite/Schema/SchemaChanger.swift @@ -141,8 +141,8 @@ public class SchemaChanger: CustomStringConvertible { } } - public func drop(table: String) throws { - try dropTable(table) + public func drop(table: String, ifExists: Bool = true) throws { + try dropTable(table, ifExists: ifExists) } // Beginning with release 3.25.0 (2018-09-15), references to the table within trigger bodies and @@ -192,7 +192,7 @@ public class SchemaChanger: CustomStringConvertible { private func moveTable(from: String, to: String, options: Options = .default, operation: Operation? = nil) throws { try copyTable(from: from, to: to, options: options, operation: operation) - try dropTable(from) + try dropTable(from, ifExists: true) } private func copyTable(from: String, to: String, options: Options = .default, operation: Operation?) throws { @@ -225,8 +225,8 @@ public class SchemaChanger: CustomStringConvertible { } } - private func dropTable(_ table: String) throws { - try connection.run("DROP TABLE IF EXISTS \(table.quote())") + private func dropTable(_ table: String, ifExists: Bool) throws { + try connection.run("DROP TABLE \(ifExists ? "IF EXISTS" : "") \(table.quote())") } private func copyTableContents(from: TableDefinition, to: TableDefinition) throws { diff --git a/Sources/SQLite/Typed/Expression.swift b/Sources/SQLite/Typed/Expression.swift index 95cdd3b9..af125cc2 100644 --- a/Sources/SQLite/Typed/Expression.swift +++ b/Sources/SQLite/Typed/Expression.swift @@ -64,32 +64,31 @@ public struct Expression: ExpressionType { } -public protocol Expressible { +public protocol Expressible: CustomStringConvertible { var expression: Expression { get } } extension Expressible { + public var description: String { + asSQL() + } // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE - // FIXME: make internal (0.13.0) - public func asSQL() -> String { + func asSQL() -> String { let expressed = expression - var idx = 0 - return expressed.template.reduce("") { template, character in - let transcoded: String + return expressed.template.reduce(("", 0)) { memo, character in + let (template, index) = memo if character == "?" { - transcoded = transcode(expressed.bindings[idx]) - idx += 1 + precondition(index < expressed.bindings.count, "not enough bindings for expression") + return (template + transcode(expressed.bindings[index]), index + 1) } else { - transcoded = String(character) + return (template + String(character), index) } - return template + transcoded - } + }.0 } - } extension ExpressionType { diff --git a/Tests/SPM/Package.swift b/Tests/SPM/Package.swift index dd1cd45c..d5d2e9ff 100644 --- a/Tests/SPM/Package.swift +++ b/Tests/SPM/Package.swift @@ -15,7 +15,7 @@ let package = Package( // for testing from same repository .package(path: "../..") // normally this would be: - // .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0") + // .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.0") ], targets: [ .target( diff --git a/Tests/SQLiteTests/Schema/SchemaChangerTests.swift b/Tests/SQLiteTests/Schema/SchemaChangerTests.swift index 2894e5ce..40f65b62 100644 --- a/Tests/SQLiteTests/Schema/SchemaChangerTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaChangerTests.swift @@ -135,6 +135,20 @@ class SchemaChangerTests: SQLiteTestCase { } } + func test_drop_table_if_exists_true() throws { + try schemaChanger.drop(table: "xxx", ifExists: true) + } + + func test_drop_table_if_exists_false() throws { + XCTAssertThrowsError(try schemaChanger.drop(table: "xxx", ifExists: false)) { error in + if case Result.error(let message, _, _) = error { + XCTAssertEqual(message, "no such table: xxx") + } else { + XCTFail("unexpected error \(error)") + } + } + } + func test_rename_table() throws { try schemaChanger.rename(table: "users", to: "users_new") let users_new = Table("users_new") diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index 2a8c7e39..65cff8bb 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -12,7 +12,7 @@ class SQLiteTestCase: XCTestCase { trace = [String: Int]() db.trace { SQL in - print(SQL) + // print("SQL: \(SQL)") self.trace[SQL, default: 0] += 1 } } diff --git a/Tests/SQLiteTests/Typed/ExpressionTests.swift b/Tests/SQLiteTests/Typed/ExpressionTests.swift index 32100d4d..9155a45c 100644 --- a/Tests/SQLiteTests/Typed/ExpressionTests.swift +++ b/Tests/SQLiteTests/Typed/ExpressionTests.swift @@ -1,5 +1,30 @@ import XCTest -import SQLite +@testable import SQLite class ExpressionTests: XCTestCase { + + func test_asSQL_expression_bindings() { + let expression = Expression("foo ? bar", ["baz"]) + XCTAssertEqual(expression.asSQL(), "foo 'baz' bar") + } + + func test_asSQL_expression_bindings_quoting() { + let expression = Expression("foo ? bar", ["'baz'"]) + XCTAssertEqual(expression.asSQL(), "foo '''baz''' bar") + } + + func test_expression_custom_string_convertible() { + let expression = Expression("foo ? bar", ["baz"]) + XCTAssertEqual(expression.asSQL(), expression.description) + } + + func test_init_literal() { + let expression = Expression(literal: "literal") + XCTAssertEqual(expression.template, "literal") + } + + func test_init_identifier() { + let expression = Expression("identifier") + XCTAssertEqual(expression.template, "\"identifier\"") + } } diff --git a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift index 27b78c0a..3fd388e9 100644 --- a/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/Typed/QueryIntegrationTests.swift @@ -192,7 +192,12 @@ class QueryIntegrationTests: SQLiteTestCase { 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 sql = query3.union(query4).order(Expression(literal: "weight")).asSQL() + XCTAssertEqual(sql, + """ + SELECT "users".*, 1 AS weight FROM "users" WHERE ("email" = 'sally@example.com') UNION \ + SELECT "users".*, 2 AS weight FROM "users" WHERE ("email" = 'alice@example.com') ORDER BY weight + """) let orderedIDs = try db.prepare(query3.union(query4).order(Expression(literal: "weight"), email)).map { $0[id] } XCTAssertEqual(Array(expectedIDs.reversed()), orderedIDs)