diff --git a/CHANGELOG.md b/CHANGELOG.md index 786084f8..37e96955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ -0.13.3 (25-01-2022), [diff][diff-0.13.3] +0.14.0 (tbd), [diff][diff-0.14.0] +======================================== + +* Support `ATTACH`/`DETACH` ([#30][], [#1142][]) +* Support `WITH` clause ([#1139][]) +* Add `Value` conformance for `NSURL` ([#1110][], [#1141][]) +* Add decoding for `UUID` ([#1137][]) +* Fix `insertMany([Encodable])` ([#1130][], [#1138][]) +* Fix incorrect spelling of `remove_diacritics` ([#1128][]) +* Fix project build order ([#1131][]) +* Performance improvements ([#1109][], [#1115][], [#1132][]) + +0.13.3 (27-03-2022), [diff][diff-0.13.3] ======================================== * UUID Fix ([#1112][]) @@ -110,7 +122,9 @@ [diff-0.13.1]: https://github.com/stephencelis/SQLite.swift/compare/0.13.0...0.13.1 [diff-0.13.2]: https://github.com/stephencelis/SQLite.swift/compare/0.13.1...0.13.2 [diff-0.13.3]: https://github.com/stephencelis/SQLite.swift/compare/0.13.2...0.13.3 +[diff-0.14.0]: https://github.com/stephencelis/SQLite.swift/compare/0.13.3...0.14.0 +[#30]: https://github.com/stephencelis/SQLite.swift/issues/30 [#142]: https://github.com/stephencelis/SQLite.swift/issues/142 [#315]: https://github.com/stephencelis/SQLite.swift/issues/315 [#426]: https://github.com/stephencelis/SQLite.swift/pull/426 @@ -150,6 +164,18 @@ [#1095]: https://github.com/stephencelis/SQLite.swift/pull/1095 [#1100]: https://github.com/stephencelis/SQLite.swift/pull/1100 [#1105]: https://github.com/stephencelis/SQLite.swift/pull/1105 +[#1109]: https://github.com/stephencelis/SQLite.swift/issues/1109 +[#1110]: https://github.com/stephencelis/SQLite.swift/pull/1110 [#1112]: https://github.com/stephencelis/SQLite.swift/pull/1112 +[#1115]: https://github.com/stephencelis/SQLite.swift/pull/1115 [#1119]: https://github.com/stephencelis/SQLite.swift/pull/1119 [#1121]: https://github.com/stephencelis/SQLite.swift/pull/1121 +[#1128]: https://github.com/stephencelis/SQLite.swift/issues/1128 +[#1130]: https://github.com/stephencelis/SQLite.swift/issues/1130 +[#1131]: https://github.com/stephencelis/SQLite.swift/pull/1131 +[#1132]: https://github.com/stephencelis/SQLite.swift/pull/1132 +[#1137]: https://github.com/stephencelis/SQLite.swift/pull/1137 +[#1138]: https://github.com/stephencelis/SQLite.swift/pull/1138 +[#1139]: https://github.com/stephencelis/SQLite.swift/pull/1139 +[#1141]: https://github.com/stephencelis/SQLite.swift/pull/1141 +[#1142]: https://github.com/stephencelis/SQLite.swift/pull/1142 diff --git a/Documentation/Index.md b/Documentation/Index.md index 7a9a390f..75c1d2cc 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -10,6 +10,7 @@ - [Read-Write Databases](#read-write-databases) - [Read-Only Databases](#read-only-databases) - [In-Memory Databases](#in-memory-databases) + - [URI parameters](#uri-parameters) - [Thread-Safety](#thread-safety) - [Building Type-Safe SQL](#building-type-safe-sql) - [Expressions](#expressions) @@ -61,9 +62,9 @@ - [Custom Collations](#custom-collations) - [Full-text Search](#full-text-search) - [Executing Arbitrary SQL](#executing-arbitrary-sql) + - [Attaching and detaching databases](#attaching-and-detaching-databases) - [Logging](#logging) - [↩]: #sqliteswift-documentation @@ -334,6 +335,16 @@ let db = try Connection(.temporary) In-memory databases are automatically deleted when the database connection is closed. +#### URI parameters + +We can pass `.uri` to the `Connection` initializer to control more aspects of +the database connection with the help of `URIQueryParameter`s: + +```swift +let db = try Connection(.uri("file.sqlite", parameters: [.cache(.private), .noLock(true)])) +``` + +See [Uniform Resource Identifiers](https://www.sqlite.org/uri.html#recognized_query_parameters) for more details. #### Thread-Safety @@ -372,6 +383,9 @@ to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). | `String` | `TEXT` | | `nil` | `NULL` | | `SQLite.Blob`† | `BLOB` | +| `URL` | `TEXT` | +| `UUID` | `TEXT` | +| `Date` | `TEXT` | > *While `Int64` is the basic, raw type (to preserve 64-bit integers on > 32-bit platforms), `Int` and `Bool` work transparently. @@ -1089,8 +1103,8 @@ users.limit(5, offset: 5) #### Recursive and Hierarchical Queries -We can perform a recursive or hierarchical query using a [query's](#queries) `with` -function. +We can perform a recursive or hierarchical query using a [query's](#queries) +[`WITH`](https://sqlite.org/lang_with.html) function. ```swift // Get the management chain for the manager with id == 8 @@ -2070,6 +2084,25 @@ let backup = try db.backup(usingConnection: target) try backup.step() ``` +## Attaching and detaching databases + +We can [ATTACH](https://www3.sqlite.org/lang_attach.html) and [DETACH](https://www3.sqlite.org/lang_detach.html) +databases to an existing connection: + +```swift +let db = try Connection("db.sqlite") + +try db.attach(.uri("external.sqlite", parameters: [.mode(.readOnly)]), as: "external") +// ATTACH DATABASE 'file:external.sqlite?mode=ro' AS 'external' + +let table = Table("table", database: "external") +let count = try db.scalar(table.count) +// SELECT count(*) FROM 'external.table' + +try db.detach("external") +// DETACH DATABASE 'external' +``` + ## Logging We can log SQL using the database’s `trace` function. diff --git a/Documentation/Planning.md b/Documentation/Planning.md index 0dea6d71..6067b81e 100644 --- a/Documentation/Planning.md +++ b/Documentation/Planning.md @@ -21,8 +21,6 @@ be referred to when it comes time to add the corresponding feature._ ### Features - * encapsulate ATTACH DATABASE / DETACH DATABASE as methods, per - [#30](https://github.com/stephencelis/SQLite.swift/issues/30) * provide separate threads for update vs read, so updates don't block reads, per [#236](https://github.com/stephencelis/SQLite.swift/issues/236) * expose triggers, per diff --git a/Package.swift b/Package.swift index 24898bca..b32fd881 100644 --- a/Package.swift +++ b/Package.swift @@ -40,8 +40,7 @@ let package = Package( "Info.plist" ], resources: [ - .copy("fixtures/encrypted-3.x.sqlite"), - .copy("fixtures/encrypted-4.x.sqlite") + .copy("Resources") ] ) ] @@ -55,10 +54,15 @@ package.targets = [ dependencies: [.product(name: "CSQLite", package: "CSQLite")], exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"] ), - .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests", exclude: [ - "FTSIntegrationTests.swift", - "FTS4Tests.swift", - "FTS5Tests.swift" - ]) + .testTarget( + name: "SQLiteTests", + dependencies: ["SQLite"], + path: "Tests/SQLiteTests", exclude: [ + "FTSIntegrationTests.swift", + "FTS4Tests.swift", + "FTS5Tests.swift" + ], + resources: [ .copy("Resources") ] + ) ] #endif diff --git a/SQLite.swift.podspec b/SQLite.swift.podspec index 4a1787bb..451b4886 100644 --- a/SQLite.swift.podspec +++ b/SQLite.swift.podspec @@ -35,7 +35,7 @@ Pod::Spec.new do |s| ss.library = 'sqlite3' ss.test_spec 'tests' do |test_spec| - test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.resources = 'Tests/SQLiteTests/Resources/*' test_spec.source_files = 'Tests/SQLiteTests/*.swift' test_spec.ios.deployment_target = ios_deployment_target test_spec.tvos.deployment_target = tvos_deployment_target @@ -55,7 +55,7 @@ Pod::Spec.new do |s| ss.dependency 'sqlite3' ss.test_spec 'tests' do |test_spec| - test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.resources = 'Tests/SQLiteTests/Resources/*' test_spec.source_files = 'Tests/SQLiteTests/*.swift' test_spec.ios.deployment_target = ios_deployment_target test_spec.tvos.deployment_target = tvos_deployment_target @@ -73,7 +73,7 @@ Pod::Spec.new do |s| ss.dependency 'SQLCipher', '>= 4.0.0' ss.test_spec 'tests' do |test_spec| - test_spec.resources = 'Tests/SQLiteTests/fixtures/*' + test_spec.resources = 'Tests/SQLiteTests/Resources/*' test_spec.source_files = 'Tests/SQLiteTests/*.swift' test_spec.ios.deployment_target = ios_deployment_target test_spec.tvos.deployment_target = tvos_deployment_target diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index fc38e5e9..45ff075f 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -68,7 +68,6 @@ 19A17490543609FCED53CACC /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; - 19A175DFF47B84757E547C62 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; 19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; 19A176406BDE9D9C80CC9FA3 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */; }; 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; @@ -92,10 +91,8 @@ 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; 19A17F1B3F0A3C96B5ED6D64 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; - 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; - 19A17FDA323BAFDEC627E76F /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; 19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 3717F908221F5D8800B9BD3D /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */; }; 3717F909221F5D8900B9BD3D /* CustomAggregationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */; }; @@ -126,6 +123,20 @@ 3DDC113626CDBE1900CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3DDC113726CDBE1900CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3DDC113826CDBE1C00CE369F /* SQLiteObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78B28842972005DD8CA /* Connection+Attach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78728842972005DD8CA /* Connection+Attach.swift */; }; + 3DF7B78D28842C23005DD8CA /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78C28842C23005DD8CA /* ResultTests.swift */; }; + 3DF7B78E28842C23005DD8CA /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78C28842C23005DD8CA /* ResultTests.swift */; }; + 3DF7B78F28842C23005DD8CA /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B78C28842C23005DD8CA /* ResultTests.swift */; }; + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */; }; + 3DF7B79628846FCC005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79828846FED005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; + 3DF7B79928847055005DD8CA /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 3DF7B79528846FCC005DD8CA /* Resources */; }; 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; @@ -256,13 +267,18 @@ 19A17B93B48B5560E6E51791 /* Fixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctions.swift; sourceTree = ""; }; 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryIntegrationTests.swift; sourceTree = ""; }; - 19A17E2695737FAB5D6086E3 /* fixtures */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = folder; path = fixtures; sourceTree = ""; }; 19A17E723300E5ED3771DCB5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 19A17EA3A313F129011B3FA0 /* Release.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Release.md; sourceTree = ""; }; 3717F907221F5D7C00B9BD3D /* CustomAggregationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAggregationTests.swift; sourceTree = ""; }; 3D3C3CCB26E5568800759140 /* SQLite.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SQLite.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS3.0.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; 3DDC112E26CDBA0200CE369F /* SQLiteObjc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SQLiteObjc.h; path = ../SQLiteObjc/include/SQLiteObjc.h; sourceTree = ""; }; + 3DF7B78728842972005DD8CA /* Connection+Attach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Connection+Attach.swift"; sourceTree = ""; }; + 3DF7B78C28842C23005DD8CA /* ResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; + 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIQueryParameter.swift; sourceTree = ""; }; + 3DF7B79528846FCC005DD8CA /* Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Resources; sourceTree = ""; }; + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + 3DF7B79B2884C901005DD8CA /* Planning.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Planning.md; sourceTree = ""; }; 49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; 997DF2AD287FC06D00F8DF95 /* Query+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+with.swift"; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -434,7 +450,7 @@ EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { isa = PBXGroup; children = ( - 19A17E2695737FAB5D6086E3 /* fixtures */, + 3DF7B79528846FCC005DD8CA /* Resources */, EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */, EE247B1B1C3F137700AE3E12 /* BlobTests.swift */, EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */, @@ -461,6 +477,7 @@ D4DB368A20C09C9B00D5A58E /* SelectTests.swift */, 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */, 19A171FDE4D67879B14ACBDF /* FTSIntegrationTests.swift */, + 3DF7B78C28842C23005DD8CA /* ResultTests.swift */, ); name = SQLiteTests; path = Tests/SQLiteTests; @@ -479,6 +496,8 @@ 02A43A9722738CF100FEC494 /* Backup.swift */, 19A17E723300E5ED3771DCB5 /* Result.swift */, 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */, + 3DF7B78728842972005DD8CA /* Connection+Attach.swift */, + 3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */, ); path = Core; sourceTree = ""; @@ -517,6 +536,7 @@ isa = PBXGroup; children = ( EE247B771C3F40D700AE3E12 /* README.md */, + 3DF7B79A2884C353005DD8CA /* CHANGELOG.md */, EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, EE247B8D1C3F821200AE3E12 /* Makefile */, @@ -532,6 +552,7 @@ isa = PBXGroup; children = ( EE247B8F1C3F822500AE3E12 /* Index.md */, + 3DF7B79B2884C901005DD8CA /* Planning.md */, EE247B901C3F822500AE3E12 /* Resources */, 19A17EA3A313F129011B3FA0 /* Release.md */, 19A1794B7972D14330A65BBD /* Linux.md */, @@ -795,7 +816,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */, + 3DF7B79828846FED005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -817,7 +838,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A17FDA323BAFDEC627E76F /* fixtures in Resources */, + 3DF7B79628846FCC005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -832,7 +853,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 19A175DFF47B84757E547C62 /* fixtures in Resources */, + 3DF7B79928847055005DD8CA /* Resources in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -847,12 +868,14 @@ 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, + 3DF7B793288449BA005DD8CA /* URIQueryParameter.swift in Sources */, 03A65E791C6BB2EF0062603F /* SQLiteObjc.m in Sources */, 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */, 03A65E821C6BB2FB0062603F /* Expression.swift in Sources */, 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */, 03A65E7F1C6BB2FB0062603F /* Collation.swift in Sources */, 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */, + 3DF7B78A28842972005DD8CA /* Connection+Attach.swift in Sources */, 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */, 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */, 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */, @@ -877,6 +900,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B78F28842C23005DD8CA /* ResultTests.swift in Sources */, 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */, 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */, 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */, @@ -910,6 +934,7 @@ buildActionMask = 2147483647; files = ( 3D67B3F91DB246E700A4F4C6 /* SQLiteObjc.m in Sources */, + 3DF7B78B28842972005DD8CA /* Connection+Attach.swift in Sources */, 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */, 997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */, 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */, @@ -925,6 +950,7 @@ 3D67B3F11DB246D100A4F4C6 /* CustomFunctions.swift in Sources */, 3D67B3F21DB246D100A4F4C6 /* Expression.swift in Sources */, 3D67B3F31DB246D100A4F4C6 /* Operators.swift in Sources */, + 3DF7B794288449BA005DD8CA /* URIQueryParameter.swift in Sources */, 3D67B3F41DB246D100A4F4C6 /* Query.swift in Sources */, 3D67B3F51DB246D100A4F4C6 /* Schema.swift in Sources */, 3D67B3F61DB246D100A4F4C6 /* Setter.swift in Sources */, @@ -946,12 +972,14 @@ 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */, + 3DF7B791288449BA005DD8CA /* URIQueryParameter.swift in Sources */, EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */, EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */, EE247B111C3F06E900AE3E12 /* Expression.swift in Sources */, EE247B0C1C3F06E900AE3E12 /* Helpers.swift in Sources */, EE247B0E1C3F06E900AE3E12 /* Collation.swift in Sources */, EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */, + 3DF7B78828842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */, EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */, EE247B081C3F06E900AE3E12 /* Value.swift in Sources */, @@ -976,6 +1004,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B78D28842C23005DD8CA /* ResultTests.swift in Sources */, EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */, EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */, EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */, @@ -1012,12 +1041,14 @@ 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, + 3DF7B792288449BA005DD8CA /* URIQueryParameter.swift in Sources */, EE247B681C3F3FEC00AE3E12 /* SQLiteObjc.m in Sources */, EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */, EE247B711C3F3FEC00AE3E12 /* Expression.swift in Sources */, EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */, EE247B6E1C3F3FEC00AE3E12 /* Collation.swift in Sources */, EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */, + 3DF7B78928842972005DD8CA /* Connection+Attach.swift in Sources */, EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */, EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */, EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */, @@ -1042,6 +1073,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3DF7B78E28842C23005DD8CA /* ResultTests.swift in Sources */, EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */, EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */, EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */, diff --git a/Sources/SQLite/Core/Connection+Attach.swift b/Sources/SQLite/Core/Connection+Attach.swift new file mode 100644 index 00000000..b9c08117 --- /dev/null +++ b/Sources/SQLite/Core/Connection+Attach.swift @@ -0,0 +1,23 @@ +import Foundation +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif os(Linux) +import CSQLite +#else +import SQLite3 +#endif + +extension Connection { + + /// See https://www3.sqlite.org/lang_attach.html + public func attach(_ location: Location, as schemaName: String) throws { + try run("ATTACH DATABASE ? AS ?", location.description, schemaName) + } + + /// See https://www3.sqlite.org/lang_detach.html + public func detach(_ schemaName: String) throws { + try run("DETACH DATABASE ?", schemaName) + } +} diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift index 72e8d857..68b83821 100644 --- a/Sources/SQLite/Core/Connection.swift +++ b/Sources/SQLite/Core/Connection.swift @@ -55,7 +55,8 @@ public final class Connection { /// See: /// /// - Parameter filename: A URI filename - case uri(String) + /// - Parameter parameters: optional query parameters + case uri(String, parameters: [URIQueryParameter] = []) } /// An SQL operation passed to update callbacks. @@ -103,8 +104,11 @@ public final class Connection { /// /// - Returns: A new database connection. public init(_ location: Location = .inMemory, readonly: Bool = false) throws { - let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE - try check(sqlite3_open_v2(location.description, &_handle, flags | SQLITE_OPEN_FULLMUTEX, nil)) + let flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE) + try check(sqlite3_open_v2(location.description, + &_handle, + flags | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI, + nil)) queue.setSpecific(key: Connection.queueKey, value: queueContext) } @@ -727,8 +731,17 @@ extension Connection.Location: CustomStringConvertible { return ":memory:" case .temporary: return "" - case .uri(let URI): - return URI + case let .uri(URI, parameters): + guard parameters.count > 0, + var components = URLComponents(string: URI) else { + return URI + } + components.queryItems = + (components.queryItems ?? []) + parameters.map(\.queryItem) + if components.scheme == nil { + components.scheme = "file" + } + return components.description } } diff --git a/Sources/SQLite/Core/URIQueryParameter.swift b/Sources/SQLite/Core/URIQueryParameter.swift new file mode 100644 index 00000000..abbab2e7 --- /dev/null +++ b/Sources/SQLite/Core/URIQueryParameter.swift @@ -0,0 +1,53 @@ +import Foundation + +/// See https://www.sqlite.org/uri.html +public enum URIQueryParameter: CustomStringConvertible { + public enum FileMode: String { + case readOnly = "ro", readWrite = "rw", readWriteCreate = "rwc", memory + } + + public enum CacheMode: String { + case shared, `private` + } + + /// The cache query parameter determines if the new database is opened using shared cache mode or with a private cache. + case cache(CacheMode) + + /// The immutable query parameter is a boolean that signals to SQLite that the underlying database file is held on read-only media + /// and cannot be modified, even by another process with elevated privileges. + case immutable(Bool) + + /// When creating a new database file during `sqlite3_open_v2()` on unix systems, SQLite will try to set the permissions of the new database + /// file to match the existing file "filename". + case modeOf(String) + + /// The mode query parameter determines if the new database is opened read-only, read-write, read-write and created if it does not exist, + /// or that the database is a pure in-memory database that never interacts with disk, respectively. + case mode(FileMode) + + /// The nolock query parameter is a boolean that disables all calls to the `xLock`, ` xUnlock`, and `xCheckReservedLock` methods + /// of the VFS when true. + case nolock(Bool) + + /// The psow query parameter overrides the `powersafe_overwrite` property of the database file being opened. + case powersafeOverwrite(Bool) + + /// The vfs query parameter causes the database connection to be opened using the VFS called NAME. + case vfs(String) + + public var description: String { + queryItem.description + } + + var queryItem: URLQueryItem { + switch self { + case .cache(let mode): return .init(name: "cache", value: mode.rawValue) + case .immutable(let bool): return .init(name: "immutable", value: NSNumber(value: bool).description) + case .modeOf(let filename): return .init(name: "modeOf", value: filename) + case .mode(let fileMode): return .init(name: "mode", value: fileMode.rawValue) + case .nolock(let bool): return .init(name: "nolock", value: NSNumber(value: bool).description) + case .powersafeOverwrite(let bool): return .init(name: "psow", value: NSNumber(value: bool).description) + case .vfs(let name): return .init(name: "vfs", value: name) + } + } +} diff --git a/Tests/SQLiteTests/CipherTests.swift b/Tests/SQLiteTests/CipherTests.swift index 0995fca0..e2d09901 100644 --- a/Tests/SQLiteTests/CipherTests.swift +++ b/Tests/SQLiteTests/CipherTests.swift @@ -4,12 +4,13 @@ import SQLite import SQLCipher class CipherTests: XCTestCase { - - let db1 = try! Connection() - let db2 = try! Connection() + var db1: Connection! + var db2: Connection! override func setUpWithError() throws { - // db + db1 = try Connection() + db2 = try Connection() + // db1 try db1.key("hello") @@ -26,41 +27,41 @@ class CipherTests: XCTestCase { try super.setUpWithError() } - func test_key() { - XCTAssertEqual(1, try! db1.scalar("SELECT count(*) FROM foo") as? Int64) + func test_key() throws { + XCTAssertEqual(1, try db1.scalar("SELECT count(*) FROM foo") as? Int64) } - func test_key_blob_literal() { - let db = try! Connection() - try! db.key("x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'") + func test_key_blob_literal() throws { + let db = try Connection() + try db.key("x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'") } - func test_rekey() { - try! db1.rekey("goodbye") - XCTAssertEqual(1, try! db1.scalar("SELECT count(*) FROM foo") as? Int64) + func test_rekey() throws { + try db1.rekey("goodbye") + XCTAssertEqual(1, try db1.scalar("SELECT count(*) FROM foo") as? Int64) } - func test_data_key() { - XCTAssertEqual(1, try! db2.scalar("SELECT count(*) FROM foo") as? Int64) + func test_data_key() throws { + XCTAssertEqual(1, try db2.scalar("SELECT count(*) FROM foo") as? Int64) } - func test_data_rekey() { + func test_data_rekey() throws { let newKey = keyData() - try! db2.rekey(Blob(bytes: newKey.bytes, length: newKey.length)) - XCTAssertEqual(1, try! db2.scalar("SELECT count(*) FROM foo") as? Int64) + try db2.rekey(Blob(bytes: newKey.bytes, length: newKey.length)) + XCTAssertEqual(1, try db2.scalar("SELECT count(*) FROM foo") as? Int64) } - func test_keyFailure() { + func test_keyFailure() throws { let path = "\(NSTemporaryDirectory())/db.sqlite3" _ = try? FileManager.default.removeItem(atPath: path) - let connA = try! Connection(path) - defer { try! FileManager.default.removeItem(atPath: path) } + let connA = try Connection(path) + defer { try? FileManager.default.removeItem(atPath: path) } - try! connA.key("hello") - try! connA.run("CREATE TABLE foo (bar TEXT)") + try connA.key("hello") + try connA.run("CREATE TABLE foo (bar TEXT)") - let connB = try! Connection(path, readonly: true) + let connB = try Connection(path, readonly: true) do { try connB.key("world") @@ -72,7 +73,7 @@ class CipherTests: XCTestCase { } } - func test_open_db_encrypted_with_sqlcipher() { + func test_open_db_encrypted_with_sqlcipher() throws { // $ sqlcipher Tests/SQLiteTests/fixtures/encrypted-[version].x.sqlite // sqlite> pragma key = 'sqlcipher-test'; // sqlite> CREATE TABLE foo (bar TEXT); @@ -85,17 +86,17 @@ class CipherTests: XCTestCase { fixture("encrypted-3.x", withExtension: "sqlite") : fixture("encrypted-4.x", withExtension: "sqlite") - try! FileManager.default.setAttributes([FileAttributeKey.immutable: 1], ofItemAtPath: encryptedFile) + 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) + 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) + let conn = try Connection(encryptedFile) + try conn.key("sqlcipher-test") + XCTAssertEqual(1, try conn.scalar("SELECT count(*) FROM foo") as? Int64) } private func keyData(length: Int = 64) -> NSMutableData { diff --git a/Tests/SQLiteTests/ConnectionTests.swift b/Tests/SQLiteTests/ConnectionTests.swift index 136271a4..347516f5 100644 --- a/Tests/SQLiteTests/ConnectionTests.swift +++ b/Tests/SQLiteTests/ConnectionTests.swift @@ -20,39 +20,54 @@ class ConnectionTests: SQLiteTestCase { try createUsersTable() } - func test_init_withInMemory_returnsInMemoryConnection() { - let db = try! Connection(.inMemory) + func test_init_withInMemory_returnsInMemoryConnection() throws { + let db = try Connection(.inMemory) XCTAssertEqual("", db.description) } - func test_init_returnsInMemoryByDefault() { - let db = try! Connection() + func test_init_returnsInMemoryByDefault() throws { + let db = try Connection() XCTAssertEqual("", db.description) } - func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! Connection(.temporary) + func test_init_withTemporary_returnsTemporaryConnection() throws { + let db = try Connection(.temporary) XCTAssertEqual("", db.description) } - func test_init_withURI_returnsURIConnection() { - let db = try! Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + func test_init_withURI_returnsURIConnection() throws { + let db = try Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) let url = URL(fileURLWithPath: db.description) XCTAssertEqual(url.lastPathComponent, "SQLite.swift Tests.sqlite3") } - func test_init_withString_returnsURIConnection() { - let db = try! Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") + func test_init_withString_returnsURIConnection() throws { + let db = try Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") let url = URL(fileURLWithPath: db.description) XCTAssertEqual(url.lastPathComponent, "SQLite.swift Tests.sqlite3") } + func test_init_with_Uri_and_Parameters() throws { + let testDb = fixture("test", withExtension: "sqlite") + _ = try Connection(.uri(testDb, parameters: [.cache(.shared)])) + } + + func test_location_without_Uri_parameters() { + let location: Connection.Location = .uri("foo") + XCTAssertEqual(location.description, "foo") + } + + func test_location_with_Uri_parameters() { + let location: Connection.Location = .uri("foo", parameters: [.mode(.readOnly), .cache(.private)]) + XCTAssertEqual(location.description, "file:foo?mode=ro&cache=private") + } + func test_readonly_returnsFalseOnReadWriteConnections() { XCTAssertFalse(db.readonly) } - func test_readonly_returnsTrueOnReadOnlyConnections() { - let db = try! Connection(readonly: true) + func test_readonly_returnsTrueOnReadOnlyConnections() throws { + let db = try Connection(readonly: true) XCTAssertTrue(db.readonly) } @@ -60,14 +75,14 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(0, db.changes) } - func test_lastInsertRowid_returnsLastIdAfterInserts() { - try! insertUser("alice") + func test_lastInsertRowid_returnsLastIdAfterInserts() throws { + try insertUser("alice") XCTAssertEqual(1, db.lastInsertRowid) } - func test_lastInsertRowid_doesNotResetAfterError() { + func test_lastInsertRowid_doesNotResetAfterError() throws { XCTAssert(db.lastInsertRowid == 0) - try! insertUser("alice") + try insertUser("alice") XCTAssertEqual(1, db.lastInsertRowid) XCTAssertThrowsError( try db.run("INSERT INTO \"users\" (email, age, admin) values ('invalid@example.com', 12, 'invalid')") @@ -81,18 +96,18 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, db.lastInsertRowid) } - func test_changes_returnsNumberOfChanges() { - try! insertUser("alice") + func test_changes_returnsNumberOfChanges() throws { + try insertUser("alice") XCTAssertEqual(1, db.changes) - try! insertUser("betsy") + try insertUser("betsy") XCTAssertEqual(1, db.changes) } - func test_totalChanges_returnsTotalNumberOfChanges() { + func test_totalChanges_returnsTotalNumberOfChanges() throws { XCTAssertEqual(0, db.totalChanges) - try! insertUser("alice") + try insertUser("alice") XCTAssertEqual(1, db.totalChanges) - try! insertUser("betsy") + try insertUser("betsy") XCTAssertEqual(2, db.totalChanges) } @@ -101,53 +116,53 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(2, db.userVersion!) } - func test_prepare_preparesAndReturnsStatements() { - _ = try! db.prepare("SELECT * FROM users WHERE admin = 0") - _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", 0) - _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", [0]) - _ = try! db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) + func test_prepare_preparesAndReturnsStatements() throws { + _ = try db.prepare("SELECT * FROM users WHERE admin = 0") + _ = try db.prepare("SELECT * FROM users WHERE admin = ?", 0) + _ = try db.prepare("SELECT * FROM users WHERE admin = ?", [0]) + _ = try db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) } - func test_run_preparesRunsAndReturnsStatements() { - try! db.run("SELECT * FROM users WHERE admin = 0") - try! db.run("SELECT * FROM users WHERE admin = ?", 0) - try! db.run("SELECT * FROM users WHERE admin = ?", [0]) - try! db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) + func test_run_preparesRunsAndReturnsStatements() throws { + try db.run("SELECT * FROM users WHERE admin = 0") + try db.run("SELECT * FROM users WHERE admin = ?", 0) + try db.run("SELECT * FROM users WHERE admin = ?", [0]) + try db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) assertSQL("SELECT * FROM users WHERE admin = 0", 4) } - func test_vacuum() { - try! db.vacuum() + func test_vacuum() throws { + try db.vacuum() } - func test_scalar_preparesRunsAndReturnsScalarValues() { - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as? Int64) - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = $admin", ["$admin": 0]) as? Int64) + func test_scalar_preparesRunsAndReturnsScalarValues() throws { + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users WHERE admin = $admin", ["$admin": 0]) as? Int64) assertSQL("SELECT count(*) FROM users WHERE admin = 0", 4) } - func test_execute_comment() { - try! db.run("-- this is a comment\nSELECT 1") + func test_execute_comment() throws { + try db.run("-- this is a comment\nSELECT 1") assertSQL("-- this is a comment", 0) assertSQL("SELECT 1", 0) } - func test_transaction_executesBeginDeferred() { - try! db.transaction(.deferred) {} + func test_transaction_executesBeginDeferred() throws { + try db.transaction(.deferred) {} assertSQL("BEGIN DEFERRED TRANSACTION") } - func test_transaction_executesBeginImmediate() { - try! db.transaction(.immediate) {} + func test_transaction_executesBeginImmediate() throws { + try db.transaction(.immediate) {} assertSQL("BEGIN IMMEDIATE TRANSACTION") } - func test_transaction_executesBeginExclusive() { - try! db.transaction(.exclusive) {} + func test_transaction_executesBeginExclusive() throws { + try db.transaction(.exclusive) {} assertSQL("BEGIN EXCLUSIVE TRANSACTION") } @@ -164,10 +179,10 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(users.map { $0[0] as? String }, ["alice@example.com", "betsy@example.com"]) } - func test_transaction_beginsAndCommitsTransactions() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + func test_transaction_beginsAndCommitsTransactions() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - try! db.transaction { + try db.transaction { try stmt.run() } @@ -177,8 +192,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("ROLLBACK TRANSACTION", 0) } - func test_transaction_rollsBackTransactionsIfCommitsFail() { - let sqliteVersion = String(describing: try! db.scalar("SELECT sqlite_version()")!) + func test_transaction_rollsBackTransactionsIfCommitsFail() throws { + let sqliteVersion = String(describing: try db.scalar("SELECT sqlite_version()")!) .split(separator: ".").compactMap { Int($0) } // PRAGMA defer_foreign_keys only supported in SQLite >= 3.8.0 guard sqliteVersion[0] == 3 && sqliteVersion[1] >= 8 else { @@ -187,9 +202,9 @@ class ConnectionTests: SQLiteTestCase { } // This test case needs to emulate an environment where the individual statements succeed, but committing the // transaction fails. Using deferred foreign keys is one option to achieve this. - try! db.execute("PRAGMA foreign_keys = ON;") - try! db.execute("PRAGMA defer_foreign_keys = ON;") - let stmt = try! db.prepare("INSERT INTO users (email, manager_id) VALUES (?, ?)", "alice@example.com", 100) + try db.execute("PRAGMA foreign_keys = ON;") + try db.execute("PRAGMA defer_foreign_keys = ON;") + let stmt = try db.prepare("INSERT INTO users (email, manager_id) VALUES (?, ?)", "alice@example.com", 100) do { try db.transaction { @@ -209,14 +224,14 @@ class ConnectionTests: SQLiteTestCase { // Run another transaction to ensure that a subsequent transaction does not fail with an "cannot start a // transaction within a transaction" error. - let stmt2 = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - try! db.transaction { + let stmt2 = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + try db.transaction { try stmt2.run() } } - func test_transaction_beginsAndRollsTransactionsBack() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + func test_transaction_beginsAndRollsTransactionsBack() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { try db.transaction { @@ -232,8 +247,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("COMMIT TRANSACTION", 0) } - func test_savepoint_beginsAndCommitsSavepoints() { - try! db.savepoint("1") { + func test_savepoint_beginsAndCommitsSavepoints() throws { + try db.savepoint("1") { try db.savepoint("2") { try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") } @@ -248,8 +263,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("ROLLBACK TO SAVEPOINT '1'", 0) } - func test_savepoint_beginsAndRollsSavepointsBack() { - let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + func test_savepoint_beginsAndRollsSavepointsBack() throws { + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { try db.savepoint("1") { @@ -276,8 +291,8 @@ class ConnectionTests: SQLiteTestCase { assertSQL("RELEASE SAVEPOINT '1'", 0) } - func test_updateHook_setsUpdateHook_withInsert() { - async { done in + func test_updateHook_setsUpdateHook_withInsert() throws { + try async { done in db.updateHook { operation, db, table, rowid in XCTAssertEqual(Connection.Operation.insert, operation) XCTAssertEqual("main", db) @@ -285,13 +300,13 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, rowid) done() } - try! insertUser("alice") + try insertUser("alice") } } - func test_updateHook_setsUpdateHook_withUpdate() { - try! insertUser("alice") - async { done in + func test_updateHook_setsUpdateHook_withUpdate() throws { + try insertUser("alice") + try async { done in db.updateHook { operation, db, table, rowid in XCTAssertEqual(Connection.Operation.update, operation) XCTAssertEqual("main", db) @@ -299,13 +314,13 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, rowid) done() } - try! db.run("UPDATE users SET email = 'alice@example.com'") + try db.run("UPDATE users SET email = 'alice@example.com'") } } - func test_updateHook_setsUpdateHook_withDelete() { - try! insertUser("alice") - async { done in + func test_updateHook_setsUpdateHook_withDelete() throws { + try insertUser("alice") + try async { done in db.updateHook { operation, db, table, rowid in XCTAssertEqual(Connection.Operation.delete, operation) XCTAssertEqual("main", db) @@ -313,24 +328,24 @@ class ConnectionTests: SQLiteTestCase { XCTAssertEqual(1, rowid) done() } - try! db.run("DELETE FROM users WHERE id = 1") + try db.run("DELETE FROM users WHERE id = 1") } } - func test_commitHook_setsCommitHook() { - async { done in + func test_commitHook_setsCommitHook() throws { + try async { done in db.commitHook { done() } - try! db.transaction { + try db.transaction { try insertUser("alice") } - XCTAssertEqual(1, try! db.scalar("SELECT count(*) FROM users") as? Int64) + XCTAssertEqual(1, try db.scalar("SELECT count(*) FROM users") as? Int64) } } - func test_rollbackHook_setsRollbackHook() { - async { done in + func test_rollbackHook_setsRollbackHook() throws { + try async { done in db.rollbackHook(done) do { try db.transaction { @@ -339,12 +354,12 @@ class ConnectionTests: SQLiteTestCase { } } catch { } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users") as? Int64) } } - func test_commitHook_withRollback_rollsBack() { - async { done in + func test_commitHook_withRollback_rollsBack() throws { + try async { done in db.commitHook { throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) } @@ -355,41 +370,41 @@ class ConnectionTests: SQLiteTestCase { } } catch { } - XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) + XCTAssertEqual(0, try db.scalar("SELECT count(*) FROM users") as? Int64) } } // https://github.com/stephencelis/SQLite.swift/issues/1071 #if !os(Linux) - func test_createFunction_withArrayArguments() { + func test_createFunction_withArrayArguments() throws { db.createFunction("hello") { $0[0].map { "Hello, \($0)!" } } - XCTAssertEqual("Hello, world!", try! db.scalar("SELECT hello('world')") as? String) - XCTAssert(try! db.scalar("SELECT hello(NULL)") == nil) + XCTAssertEqual("Hello, world!", try db.scalar("SELECT hello('world')") as? String) + XCTAssert(try db.scalar("SELECT hello(NULL)") == nil) } - func test_createFunction_createsQuotableFunction() { + func test_createFunction_createsQuotableFunction() throws { db.createFunction("hello world") { $0[0].map { "Hello, \($0)!" } } - XCTAssertEqual("Hello, world!", try! db.scalar("SELECT \"hello world\"('world')") as? String) - XCTAssert(try! db.scalar("SELECT \"hello world\"(NULL)") == nil) + XCTAssertEqual("Hello, world!", try db.scalar("SELECT \"hello world\"('world')") as? String) + XCTAssert(try db.scalar("SELECT \"hello world\"(NULL)") == nil) } - func test_createCollation_createsCollation() { - try! db.createCollation("NODIACRITIC") { lhs, rhs in + func test_createCollation_createsCollation() throws { + try db.createCollation("NODIACRITIC") { lhs, rhs in lhs.compare(rhs, options: .diacriticInsensitive) } - XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) + XCTAssertEqual(1, try db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) } - func test_createCollation_createsQuotableCollation() { - try! db.createCollation("NO DIACRITIC") { lhs, rhs in + func test_createCollation_createsQuotableCollation() throws { + try db.createCollation("NO DIACRITIC") { lhs, rhs in lhs.compare(rhs, options: .diacriticInsensitive) } - XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) + XCTAssertEqual(1, try db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) } - func test_interrupt_interruptsLongRunningQuery() { + func test_interrupt_interruptsLongRunningQuery() throws { let semaphore = DispatchSemaphore(value: 0) db.createFunction("sleep") { _ in DispatchQueue.global(qos: .background).async { @@ -399,7 +414,7 @@ class ConnectionTests: SQLiteTestCase { semaphore.wait() return nil } - let stmt = try! db.prepare("SELECT sleep()") + let stmt = try db.prepare("SELECT sleep()") XCTAssertThrowsError(try stmt.run()) { error in if case Result.error(_, let code, _) = error { XCTAssertEqual(code, SQLITE_INTERRUPT) @@ -410,13 +425,13 @@ class ConnectionTests: SQLiteTestCase { } #endif - func test_concurrent_access_single_connection() { + func test_concurrent_access_single_connection() throws { // test can fail on iOS/tvOS 9.x: SQLite compile-time differences? guard #available(iOS 10.0, OSX 10.10, tvOS 10.0, watchOS 2.2, *) else { return } - let conn = try! Connection("\(NSTemporaryDirectory())/\(UUID().uuidString)") - try! conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") - try! conn.run("INSERT INTO test(value) VALUES(?)", 0) + let conn = try Connection("\(NSTemporaryDirectory())/\(UUID().uuidString)") + try conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") + try conn.run("INSERT INTO test(value) VALUES(?)", 0) let queue = DispatchQueue(label: "Readers", attributes: [.concurrent]) let nReaders = 5 @@ -430,43 +445,51 @@ class ConnectionTests: SQLiteTestCase { } semaphores.forEach { $0.wait() } } -} -class ResultTests: XCTestCase { - let connection = try! Connection(.inMemory) + func test_attach_detach_memory_database() throws { + let schemaName = "test" - func test_init_with_ok_code_returns_nil() { - XCTAssertNil(Result(errorCode: SQLITE_OK, connection: connection, statement: nil) as Result?) - } + try db.attach(.inMemory, as: schemaName) - func test_init_with_row_code_returns_nil() { - XCTAssertNil(Result(errorCode: SQLITE_ROW, connection: connection, statement: nil) as Result?) - } + let table = Table("attached_users", database: schemaName) + let name = Expression("string") - func test_init_with_done_code_returns_nil() { - XCTAssertNil(Result(errorCode: SQLITE_DONE, connection: connection, statement: nil) as Result?) - } + // create a table, insert some data + try db.run(table.create { builder in + builder.column(name) + }) + _ = try db.run(table.insert(name <- "test")) - func test_init_with_other_code_returns_error() { - if case .some(.error(let message, let code, let statement)) = - Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil) { - XCTAssertEqual("not an error", message) - XCTAssertEqual(SQLITE_MISUSE, code) - XCTAssertNil(statement) - XCTAssert(connection === connection) - } else { - XCTFail("no error") - } + // query data + let rows = try db.prepare(table.select(name)).map { $0[name] } + XCTAssertEqual(["test"], rows) + + try db.detach(schemaName) } - func test_description_contains_error_code() { - XCTAssertEqual("not an error (code: 21)", - Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil)?.description) + func test_attach_detach_file_database() throws { + let schemaName = "test" + let testDb = fixture("test", withExtension: "sqlite") + + try db.attach(.uri(testDb, parameters: [.mode(.readOnly)]), as: schemaName) + + let table = Table("tests", database: schemaName) + let email = Expression("email") + + let rows = try db.prepare(table.select(email)).map { $0[email] } + XCTAssertEqual(["foo@bar.com"], rows) + + try db.detach(schemaName) } - func test_description_contains_statement_and_error_code() { - let statement = try! Statement(connection, "SELECT 1") - XCTAssertEqual("not an error (SELECT 1) (code: 21)", - Result(errorCode: SQLITE_MISUSE, connection: connection, statement: statement)?.description) + func test_detach_invalid_schema_name_errors_with_no_such_database() throws { + XCTAssertThrowsError(try db.detach("no-exist")) { error in + if case let Result.error(message, code, _) = error { + XCTAssertEqual(code, SQLITE_ERROR) + XCTAssertEqual("no such database: no-exist", message) + } else { + XCTFail("unexpected error: \(error)") + } + } } } diff --git a/Tests/SQLiteTests/CustomAggregationTests.swift b/Tests/SQLiteTests/CustomAggregationTests.swift index 5cbfbbef..71cbba9c 100644 --- a/Tests/SQLiteTests/CustomAggregationTests.swift +++ b/Tests/SQLiteTests/CustomAggregationTests.swift @@ -25,7 +25,7 @@ class CustomAggregationTests: SQLiteTestCase { try insertUser("Eve", age: 28, admin: false) } - func testUnsafeCustomSum() { + func testUnsafeCustomSum() throws { let step = { (bindings: [Binding?], state: UnsafeMutablePointer) in if let v = bindings[0] as? Int64 { state.pointee += v @@ -43,7 +43,7 @@ class CustomAggregationTests: SQLiteTestCase { v[0] = 0 return v.baseAddress! } - let result = try! db.prepare("SELECT mySUM1(age) AS s FROM users") + let result = try db.prepare("SELECT mySUM1(age) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { let value = row[i] as? Int64 @@ -51,7 +51,7 @@ class CustomAggregationTests: SQLiteTestCase { } } - func testUnsafeCustomSumGrouping() { + func testUnsafeCustomSumGrouping() throws { let step = { (bindings: [Binding?], state: UnsafeMutablePointer) in if let v = bindings[0] as? Int64 { state.pointee += v @@ -68,19 +68,19 @@ class CustomAggregationTests: SQLiteTestCase { v[0] = 0 return v.baseAddress! } - let result = try! db.prepare("SELECT mySUM2(age) AS s FROM users GROUP BY admin ORDER BY s") + let result = try db.prepare("SELECT mySUM2(age) AS s FROM users GROUP BY admin ORDER BY s") let i = result.columnNames.firstIndex(of: "s")! let values = result.compactMap { $0[i] as? Int64 } XCTAssertTrue(values.elementsEqual([28, 55])) } - func testCustomSum() { + func testCustomSum() throws { let reduce: (Int64, [Binding?]) -> Int64 = { (last, bindings) in let v = (bindings[0] as? Int64) ?? 0 return last + v } db.createAggregation("myReduceSUM1", initialValue: Int64(2000), reduce: reduce, result: { $0 }) - let result = try! db.prepare("SELECT myReduceSUM1(age) AS s FROM users") + let result = try db.prepare("SELECT myReduceSUM1(age) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { let value = row[i] as? Int64 @@ -88,26 +88,26 @@ class CustomAggregationTests: SQLiteTestCase { } } - func testCustomSumGrouping() { + func testCustomSumGrouping() throws { let reduce: (Int64, [Binding?]) -> Int64 = { (last, bindings) in let v = (bindings[0] as? Int64) ?? 0 return last + v } db.createAggregation("myReduceSUM2", initialValue: Int64(3000), reduce: reduce, result: { $0 }) - let result = try! db.prepare("SELECT myReduceSUM2(age) AS s FROM users GROUP BY admin ORDER BY s") + let result = try db.prepare("SELECT myReduceSUM2(age) AS s FROM users GROUP BY admin ORDER BY s") let i = result.columnNames.firstIndex(of: "s")! let values = result.compactMap { $0[i] as? Int64 } XCTAssertTrue(values.elementsEqual([3028, 3055])) } - func testCustomStringAgg() { + func testCustomStringAgg() throws { let initial = String(repeating: " ", count: 64) let reduce: (String, [Binding?]) -> String = { (last, bindings) in let v = (bindings[0] as? String) ?? "" return last + v } db.createAggregation("myReduceSUM3", initialValue: initial, reduce: reduce, result: { $0 }) - let result = try! db.prepare("SELECT myReduceSUM3(email) AS s FROM users") + let result = try db.prepare("SELECT myReduceSUM3(email) AS s FROM users") let i = result.columnNames.firstIndex(of: "s")! for row in result { @@ -116,7 +116,7 @@ class CustomAggregationTests: SQLiteTestCase { } } - func testCustomObjectSum() { + func testCustomObjectSum() throws { { let initial = TestObject(value: 1000) let reduce: (TestObject, [Binding?]) -> TestObject = { (last, bindings) in diff --git a/Tests/SQLiteTests/CustomFunctionsTests.swift b/Tests/SQLiteTests/CustomFunctionsTests.swift index 3d897f8e..c4eb51e6 100644 --- a/Tests/SQLiteTests/CustomFunctionsTests.swift +++ b/Tests/SQLiteTests/CustomFunctionsTests.swift @@ -8,19 +8,19 @@ class CustomFunctionNoArgsTests: SQLiteTestCase { typealias FunctionNoOptional = () -> Expression typealias FunctionResultOptional = () -> Expression - func testFunctionNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { + func testFunctionNoOptional() throws { + let _: FunctionNoOptional = try db.createFunction("test", deterministic: true) { "a" } - let result = try! db.prepare("SELECT test()").scalar() as! String + let result = try db.prepare("SELECT test()").scalar() as! String XCTAssertEqual("a", result) } - func testFunctionResultOptional() { - let _: FunctionResultOptional = try! db.createFunction("test", deterministic: true) { + func testFunctionResultOptional() throws { + let _: FunctionResultOptional = try db.createFunction("test", deterministic: true) { "a" } - let result = try! db.prepare("SELECT test()").scalar() as! String? + let result = try db.prepare("SELECT test()").scalar() as! String? XCTAssertEqual("a", result) } } @@ -31,35 +31,35 @@ class CustomFunctionWithOneArgTests: SQLiteTestCase { typealias FunctionResultOptional = (Expression) -> Expression typealias FunctionLeftResultOptional = (Expression) -> Expression - func testFunctionNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { a in + func testFunctionNoOptional() throws { + let _: FunctionNoOptional = try db.createFunction("test", deterministic: true) { a in "b" + a } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String + let result = try db.prepare("SELECT test(?)").scalar("a") as! String XCTAssertEqual("ba", result) } - func testFunctionLeftOptional() { - let _: FunctionLeftOptional = try! db.createFunction("test", deterministic: true) { a in + func testFunctionLeftOptional() throws { + let _: FunctionLeftOptional = try db.createFunction("test", deterministic: true) { a in "b" + a! } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String + let result = try db.prepare("SELECT test(?)").scalar("a") as! String XCTAssertEqual("ba", result) } - func testFunctionResultOptional() { - let _: FunctionResultOptional = try! db.createFunction("test", deterministic: true) { a in + func testFunctionResultOptional() throws { + let _: FunctionResultOptional = try db.createFunction("test", deterministic: true) { a in "b" + a } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String + let result = try db.prepare("SELECT test(?)").scalar("a") as! String XCTAssertEqual("ba", result) } - func testFunctionLeftResultOptional() { - let _: FunctionLeftResultOptional = try! db.createFunction("test", deterministic: true) { (a: String?) -> String? in + func testFunctionLeftResultOptional() throws { + let _: FunctionLeftResultOptional = try db.createFunction("test", deterministic: true) { (a: String?) -> String? in "b" + a! } - let result = try! db.prepare("SELECT test(?)").scalar("a") as! String + let result = try db.prepare("SELECT test(?)").scalar("a") as! String XCTAssertEqual("ba", result) } } @@ -74,76 +74,76 @@ class CustomFunctionWithTwoArgsTests: SQLiteTestCase { typealias FunctionRightResultOptional = (Expression, Expression) -> Expression typealias FunctionLeftRightResultOptional = (Expression, Expression) -> Expression - func testNoOptional() { - let _: FunctionNoOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testNoOptional() throws { + let _: FunctionNoOptional = try db.createFunction("test", deterministic: true) { a, b in a + b } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String XCTAssertEqual("ab", result) } - func testLeftOptional() { - let _: FunctionLeftOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testLeftOptional() throws { + let _: FunctionLeftOptional = try db.createFunction("test", deterministic: true) { a, b in a! + b } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String XCTAssertEqual("ab", result) } - func testRightOptional() { - let _: FunctionRightOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testRightOptional() throws { + let _: FunctionRightOptional = try db.createFunction("test", deterministic: true) { a, b in a + b! } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String XCTAssertEqual("ab", result) } - func testResultOptional() { - let _: FunctionResultOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testResultOptional() throws { + let _: FunctionResultOptional = try db.createFunction("test", deterministic: true) { a, b in a + b } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? XCTAssertEqual("ab", result) } - func testFunctionLeftRightOptional() { - let _: FunctionLeftRightOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testFunctionLeftRightOptional() throws { + let _: FunctionLeftRightOptional = try db.createFunction("test", deterministic: true) { a, b in a! + b! } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String XCTAssertEqual("ab", result) } - func testFunctionLeftResultOptional() { - let _: FunctionLeftResultOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testFunctionLeftResultOptional() throws { + let _: FunctionLeftResultOptional = try db.createFunction("test", deterministic: true) { a, b in a! + b } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? XCTAssertEqual("ab", result) } - func testFunctionRightResultOptional() { - let _: FunctionRightResultOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testFunctionRightResultOptional() throws { + let _: FunctionRightResultOptional = try db.createFunction("test", deterministic: true) { a, b in a + b! } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? XCTAssertEqual("ab", result) } - func testFunctionLeftRightResultOptional() { - let _: FunctionLeftRightResultOptional = try! db.createFunction("test", deterministic: true) { a, b in + func testFunctionLeftRightResultOptional() throws { + let _: FunctionLeftRightResultOptional = try db.createFunction("test", deterministic: true) { a, b in a! + b! } - let result = try! db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? + let result = try db.prepare("SELECT test(?, ?)").scalar("a", "b") as! String? XCTAssertEqual("ab", result) } } class CustomFunctionTruncation: SQLiteTestCase { // https://github.com/stephencelis/SQLite.swift/issues/468 - func testStringTruncation() { - _ = try! db.createFunction("customLower") { (value: String) in value.lowercased() } - let result = try! db.prepare("SELECT customLower(?)").scalar("TÖL-AA 12") as? String + func testStringTruncation() throws { + _ = try db.createFunction("customLower") { (value: String) in value.lowercased() } + let result = try db.prepare("SELECT customLower(?)").scalar("TÖL-AA 12") as? String XCTAssertEqual("töl-aa 12", result) } } diff --git a/Tests/SQLiteTests/FTS4Tests.swift b/Tests/SQLiteTests/FTS4Tests.swift index d6385b0c..aa6ac41f 100644 --- a/Tests/SQLiteTests/FTS4Tests.swift +++ b/Tests/SQLiteTests/FTS4Tests.swift @@ -191,7 +191,7 @@ class FTS4ConfigTests: XCTestCase { class FTS4IntegrationTests: SQLiteTestCase { #if !SQLITE_SWIFT_STANDALONE && !SQLITE_SWIFT_SQLCIPHER - func test_registerTokenizer_registersTokenizer() { + func test_registerTokenizer_registersTokenizer() throws { let emails = VirtualTable("emails") let subject = Expression("subject") let body = Expression("body") @@ -200,7 +200,7 @@ class FTS4IntegrationTests: SQLiteTestCase { let tokenizerName = "tokenizer" let tokenizer = CFStringTokenizerCreate(nil, "" as CFString, CFRangeMake(0, 0), UInt(kCFStringTokenizerUnitWord), locale) - try! db.registerTokenizer(tokenizerName) { string in + try db.registerTokenizer(tokenizerName) { string in CFStringTokenizerSetString(tokenizer, string as CFString, CFRangeMake(0, CFStringGetLength(string as CFString))) if CFStringTokenizerAdvanceToNextToken(tokenizer).isEmpty { @@ -214,14 +214,14 @@ class FTS4IntegrationTests: SQLiteTestCase { return (token as String, string.range(of: input as String)!) } - try! db.run(emails.create(.FTS4([subject, body], tokenize: .Custom(tokenizerName)))) + try db.run(emails.create(.FTS4([subject, body], tokenize: .Custom(tokenizerName)))) assertSQL(""" CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=\"SQLite.swift\" \"tokenizer\") """.replacingOccurrences(of: "\n", with: "")) - try! _ = db.run(emails.insert(subject <- "Aún más cáfe!")) - XCTAssertEqual(1, try! db.scalar(emails.filter(emails.match("aun")).count)) + try _ = db.run(emails.insert(subject <- "Aún más cáfe!")) + XCTAssertEqual(1, try db.scalar(emails.filter(emails.match("aun")).count)) } #endif } diff --git a/Tests/SQLiteTests/Fixtures.swift b/Tests/SQLiteTests/Fixtures.swift index d0683130..95b2b6dc 100644 --- a/Tests/SQLiteTests/Fixtures.swift +++ b/Tests/SQLiteTests/Fixtures.swift @@ -1,8 +1,18 @@ import Foundation func fixture(_ name: String, withExtension: String?) -> String { + #if SWIFT_PACKAGE + let testBundle = Bundle.module + #else let testBundle = Bundle(for: SQLiteTestCase.self) - return testBundle.url( - forResource: name, - withExtension: withExtension)!.path + #endif + + for resource in [name, "Resources/\(name)"] { + if let url = testBundle.url( + forResource: resource, + withExtension: withExtension) { + return url.path + } + } + fatalError("Cannot find \(name).\(withExtension ?? "")") } diff --git a/Tests/SQLiteTests/QueryIntegrationTests.swift b/Tests/SQLiteTests/QueryIntegrationTests.swift index db4e2e4e..27b78c0a 100644 --- a/Tests/SQLiteTests/QueryIntegrationTests.swift +++ b/Tests/SQLiteTests/QueryIntegrationTests.swift @@ -23,45 +23,45 @@ class QueryIntegrationTests: SQLiteTestCase { // MARK: - - func test_select() { + func test_select() throws { let managerId = Expression("manager_id") let managers = users.alias("managers") - let alice = try! db.run(users.insert(email <- "alice@example.com")) - _ = try! db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) + let alice = try db.run(users.insert(email <- "alice@example.com")) + _ = try db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) - for user in try! db.prepare(users.join(managers, on: managers[id] == users[managerId])) { + for user in try db.prepare(users.join(managers, on: managers[id] == users[managerId])) { _ = user[users[managerId]] } } - func test_prepareRowIterator() { + func test_prepareRowIterator() throws { let names = ["a", "b", "c"] - try! insertUsers(names) + try insertUsers(names) let emailColumn = Expression("email") - let emails = try! db.prepareRowIterator(users).map { $0[emailColumn] } + let emails = try db.prepareRowIterator(users).map { $0[emailColumn] } XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) } - func test_ambiguousMap() { + func test_ambiguousMap() throws { let names = ["a", "b", "c"] - try! insertUsers(names) + try insertUsers(names) - let emails = try! db.prepare("select email from users", []).map { $0[0] as! String } + let emails = try db.prepare("select email from users", []).map { $0[0] as! String } XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) } - func test_select_optional() { + func test_select_optional() throws { let managerId = Expression("manager_id") let managers = users.alias("managers") - let alice = try! db.run(users.insert(email <- "alice@example.com")) - _ = try! db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) + let alice = try db.run(users.insert(email <- "alice@example.com")) + _ = try db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) - for user in try! db.prepare(users.join(managers, on: managers[id] == users[managerId])) { + for user in try db.prepare(users.join(managers, on: managers[id] == users[managerId])) { _ = user[users[managerId]] } } @@ -107,26 +107,26 @@ class QueryIntegrationTests: SQLiteTestCase { XCTAssertNil(values[0].sub?.sub) } - func test_scalar() { - XCTAssertEqual(0, try! db.scalar(users.count)) - XCTAssertEqual(false, try! db.scalar(users.exists)) + func test_scalar() throws { + XCTAssertEqual(0, try db.scalar(users.count)) + XCTAssertEqual(false, try db.scalar(users.exists)) - try! insertUsers("alice") - XCTAssertEqual(1, try! db.scalar(users.select(id.average))) + try insertUsers("alice") + XCTAssertEqual(1, try db.scalar(users.select(id.average))) } - func test_pluck() { - let rowid = try! db.run(users.insert(email <- "alice@example.com")) - XCTAssertEqual(rowid, try! db.pluck(users)![id]) + func test_pluck() throws { + let rowid = try db.run(users.insert(email <- "alice@example.com")) + XCTAssertEqual(rowid, try db.pluck(users)![id]) } - func test_insert() { - let id = try! db.run(users.insert(email <- "alice@example.com")) + func test_insert() throws { + let id = try db.run(users.insert(email <- "alice@example.com")) XCTAssertEqual(1, id) } - func test_insert_many() { - let id = try! db.run(users.insertMany([[email <- "alice@example.com"], [email <- "geoff@example.com"]])) + func test_insert_many() throws { + let id = try db.run(users.insertMany([[email <- "alice@example.com"], [email <- "geoff@example.com"]])) XCTAssertEqual(2, id) } @@ -167,13 +167,13 @@ class QueryIntegrationTests: SQLiteTestCase { XCTAssertEqual(42, try fetchAge()) } - func test_update() { - let changes = try! db.run(users.update(email <- "alice@example.com")) + func test_update() throws { + let changes = try db.run(users.update(email <- "alice@example.com")) XCTAssertEqual(0, changes) } - func test_delete() { - let changes = try! db.run(users.delete()) + func test_delete() throws { + let changes = try db.run(users.delete()) XCTAssertEqual(0, changes) } @@ -200,8 +200,8 @@ class QueryIntegrationTests: SQLiteTestCase { func test_no_such_column() throws { let doesNotExist = Expression("doesNotExist") - try! insertUser("alice") - let row = try! db.pluck(users.filter(email == "alice@example.com"))! + try insertUser("alice") + let row = try db.pluck(users.filter(email == "alice@example.com"))! XCTAssertThrowsError(try row.get(doesNotExist)) { error in if case QueryError.noSuchColumn(let name, _) = error { @@ -212,8 +212,8 @@ class QueryIntegrationTests: SQLiteTestCase { } } - func test_catchConstraintError() { - try! db.run(users.insert(email <- "alice@example.com")) + func test_catchConstraintError() throws { + try db.run(users.insert(email <- "alice@example.com")) do { try db.run(users.insert(email <- "alice@example.com")) XCTFail("expected error") @@ -231,19 +231,19 @@ class QueryIntegrationTests: SQLiteTestCase { XCTAssertEqual(1, result.count) } - func test_with_recursive() { + func test_with_recursive() throws { let nodes = Table("nodes") let id = Expression("id") let parent = Expression("parent") let value = Expression("value") - try! db.run(nodes.create { builder in + try db.run(nodes.create { builder in builder.column(id) builder.column(parent) builder.column(value) }) - try! db.run(nodes.insertMany([ + try db.run(nodes.insertMany([ [id <- 0, parent <- nil, value <- 2], [id <- 1, parent <- 0, value <- 4], [id <- 2, parent <- 0, value <- 9], @@ -254,7 +254,7 @@ class QueryIntegrationTests: SQLiteTestCase { // Compute the sum of the values of node 5 and its ancestors let ancestors = Table("ancestors") - let sum = try! db.scalar( + let sum = try db.scalar( ancestors .select(value.sum) .with(ancestors, diff --git a/Tests/SQLiteTests/QueryTests.swift b/Tests/SQLiteTests/QueryTests.swift index eae1d923..82a33808 100644 --- a/Tests/SQLiteTests/QueryTests.swift +++ b/Tests/SQLiteTests/QueryTests.swift @@ -375,25 +375,25 @@ class QueryTests: XCTestCase { } #endif - func test_insert_and_search_for_UUID() { + func test_insert_and_search_for_UUID() throws { struct Test: Codable { var uuid: UUID var string: String } let testUUID = UUID() let testValue = Test(uuid: testUUID, string: "value") - let db = try! Connection(.temporary) - try! db.run(table.create { t in + let db = try Connection(.temporary) + try db.run(table.create { t in t.column(uuid) t.column(string) } ) - let iQuery = try! table.insert(testValue) - try! db.run(iQuery) + let iQuery = try table.insert(testValue) + try db.run(iQuery) let fQuery = table.filter(uuid == testUUID) - if let result = try! db.pluck(fQuery) { + if let result = try db.pluck(fQuery) { let testValueReturned = Test(uuid: result[uuid], string: result[string]) XCTAssertEqual(testUUID, testValueReturned.uuid) } else { diff --git a/Tests/SQLiteTests/fixtures/encrypted-3.x.sqlite b/Tests/SQLiteTests/Resources/encrypted-3.x.sqlite similarity index 100% rename from Tests/SQLiteTests/fixtures/encrypted-3.x.sqlite rename to Tests/SQLiteTests/Resources/encrypted-3.x.sqlite diff --git a/Tests/SQLiteTests/fixtures/encrypted-4.x.sqlite b/Tests/SQLiteTests/Resources/encrypted-4.x.sqlite similarity index 100% rename from Tests/SQLiteTests/fixtures/encrypted-4.x.sqlite rename to Tests/SQLiteTests/Resources/encrypted-4.x.sqlite diff --git a/Tests/SQLiteTests/Resources/test.sqlite b/Tests/SQLiteTests/Resources/test.sqlite new file mode 100644 index 00000000..7b39b423 Binary files /dev/null and b/Tests/SQLiteTests/Resources/test.sqlite differ diff --git a/Tests/SQLiteTests/ResultTests.swift b/Tests/SQLiteTests/ResultTests.swift new file mode 100644 index 00000000..fab4a0bb --- /dev/null +++ b/Tests/SQLiteTests/ResultTests.swift @@ -0,0 +1,56 @@ +import XCTest +import Foundation +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif os(Linux) +import CSQLite +#else +import SQLite3 +#endif + +class ResultTests: XCTestCase { + var connection: Connection! + + override func setUpWithError() throws { + connection = try Connection(.inMemory) + } + + func test_init_with_ok_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_OK, connection: connection, statement: nil) as Result?) + } + + func test_init_with_row_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_ROW, connection: connection, statement: nil) as Result?) + } + + func test_init_with_done_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_DONE, connection: connection, statement: nil) as Result?) + } + + func test_init_with_other_code_returns_error() { + if case .some(.error(let message, let code, let statement)) = + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil) { + XCTAssertEqual("not an error", message) + XCTAssertEqual(SQLITE_MISUSE, code) + XCTAssertNil(statement) + XCTAssert(connection === connection) + } else { + XCTFail("no error") + } + } + + func test_description_contains_error_code() { + XCTAssertEqual("not an error (code: 21)", + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil)?.description) + } + + func test_description_contains_statement_and_error_code() throws { + let statement = try Statement(connection, "SELECT 1") + XCTAssertEqual("not an error (SELECT 1) (code: 21)", + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: statement)?.description) + } +} diff --git a/Tests/SQLiteTests/RowTests.swift b/Tests/SQLiteTests/RowTests.swift index 36721f80..506f6b10 100644 --- a/Tests/SQLiteTests/RowTests.swift +++ b/Tests/SQLiteTests/RowTests.swift @@ -3,9 +3,9 @@ import XCTest class RowTests: XCTestCase { - public func test_get_value() { + public func test_get_value() throws { let row = Row(["\"foo\"": 0], ["value"]) - let result = try! row.get(Expression("foo")) + let result = try row.get(Expression("foo")) XCTAssertEqual("value", result) } @@ -17,9 +17,9 @@ class RowTests: XCTestCase { XCTAssertEqual("value", result) } - public func test_get_value_optional() { + public func test_get_value_optional() throws { let row = Row(["\"foo\"": 0], ["value"]) - let result = try! row.get(Expression("foo")) + let result = try row.get(Expression("foo")) XCTAssertEqual("value", result) } @@ -31,9 +31,9 @@ class RowTests: XCTestCase { XCTAssertEqual("value", result) } - public func test_get_value_optional_nil() { + public func test_get_value_optional_nil() throws { let row = Row(["\"foo\"": 0], [nil]) - let result = try! row.get(Expression("foo")) + let result = try row.get(Expression("foo")) XCTAssertNil(result) } @@ -56,9 +56,9 @@ class RowTests: XCTestCase { } } - public func test_get_type_mismatch_optional_returns_nil() { + public func test_get_type_mismatch_optional_returns_nil() throws { let row = Row(["\"foo\"": 0], ["value"]) - let result = try! row.get(Expression("foo")) + let result = try row.get(Expression("foo")) XCTAssertNil(result) } diff --git a/Tests/SQLiteTests/SelectTests.swift b/Tests/SQLiteTests/SelectTests.swift index d1126b66..52d5bb6b 100644 --- a/Tests/SQLiteTests/SelectTests.swift +++ b/Tests/SQLiteTests/SelectTests.swift @@ -20,7 +20,7 @@ class SelectTests: SQLiteTestCase { ) } - func test_select_columns_from_multiple_tables() { + func test_select_columns_from_multiple_tables() throws { let usersData = Table("users_name") let users = Table("users") @@ -29,14 +29,14 @@ class SelectTests: SQLiteTestCase { let userID = Expression("user_id") let email = Expression("email") - try! insertUser("Joey") - try! db.run(usersData.insert( + try insertUser("Joey") + try db.run(usersData.insert( id <- 1, userID <- 1, name <- "Joey" )) - try! db.prepare(users.select(name, email).join(usersData, on: userID == users[id])).forEach { + try db.prepare(users.select(name, email).join(usersData, on: userID == users[id])).forEach { XCTAssertEqual($0[name], "Joey") XCTAssertEqual($0[email], "Joey@example.com") } diff --git a/Tests/SQLiteTests/StatementTests.swift b/Tests/SQLiteTests/StatementTests.swift index aa05de34..3286b792 100644 --- a/Tests/SQLiteTests/StatementTests.swift +++ b/Tests/SQLiteTests/StatementTests.swift @@ -7,30 +7,30 @@ class StatementTests: SQLiteTestCase { try createUsersTable() } - func test_cursor_to_blob() { - try! insertUsers("alice") - let statement = try! db.prepare("SELECT email FROM users") - XCTAssert(try! statement.step()) + func test_cursor_to_blob() throws { + try insertUsers("alice") + let statement = try db.prepare("SELECT email FROM users") + XCTAssert(try statement.step()) let blob = statement.row[0] as Blob XCTAssertEqual("alice@example.com", String(bytes: blob.bytes, encoding: .utf8)!) } - func test_zero_sized_blob_returns_null() { + func test_zero_sized_blob_returns_null() throws { let blobs = Table("blobs") let blobColumn = Expression("blob_column") - try! db.run(blobs.create { $0.column(blobColumn) }) - try! db.run(blobs.insert(blobColumn <- Blob(bytes: []))) - let blobValue = try! db.scalar(blobs.select(blobColumn).limit(1, offset: 0)) + try db.run(blobs.create { $0.column(blobColumn) }) + try db.run(blobs.insert(blobColumn <- Blob(bytes: []))) + let blobValue = try db.scalar(blobs.select(blobColumn).limit(1, offset: 0)) XCTAssertEqual([], blobValue.bytes) } - func test_prepareRowIterator() { + func test_prepareRowIterator() throws { let names = ["a", "b", "c"] - try! insertUsers(names) + try insertUsers(names) let emailColumn = Expression("email") - let statement = try! db.prepare("SELECT email FROM users") - let emails = try! statement.prepareRowIterator().map { $0[emailColumn] } + let statement = try db.prepare("SELECT email FROM users") + let emails = try statement.prepareRowIterator().map { $0[emailColumn] } XCTAssertEqual(names.map({ "\($0)@example.com" }), emails.sorted()) } diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index f43310c9..2a8c7e39 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -54,8 +54,8 @@ class SQLiteTestCase: XCTestCase { ) } - func assertSQL(_ SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { - try! statement.run() + func assertSQL(_ SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) throws { + try statement.run() assertSQL(SQL, 1, message, file: file, line: line) if let count = trace[SQL] { trace[SQL] = count - 1 } } @@ -66,9 +66,9 @@ class SQLiteTestCase: XCTestCase { // if let count = trace[SQL] { trace[SQL] = count - 1 } // } - func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) -> Void) { + func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) throws -> Void) throws { let expectation = self.expectation(description: description) - block({ expectation.fulfill() }) + try block({ expectation.fulfill() }) waitForExpectations(timeout: timeout, handler: nil) }