diff --git a/CHANGELOG.md b/CHANGELOG.md index 474cae0042..b85e72fead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,9 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - **New**: [#1429](https://github.com/groue/GRDB.swift/pull/1429) by [@JhonnyBillM](https://github.com/JhonnyBillM): Allow `DatabaseValueConvertible` types to customize their database JSON format - **New**: `Database` has learned to create indexes on expressions, and specify specific collations on indexed columns, with the `create(index:on:expressions:options:condition:)` method. +- **New**: [#1436](https://github.com/groue/GRDB.swift/pull/1436) by [@myyra](https://github.com/myyra) and [@groue](https://github.com/groue): JSON functions +- **New**: Codable records can specify coding strategies for their `Data` properties. See [#1436](https://github.com/groue/GRDB.swift/pull/1436) for more information. +- **Documentation Update**: A new [JSON Support](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/json) article provides an overview of JSON handling. ## 6.18.0 diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 78de977bb8..5f6ae7ecda 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -13,6 +13,10 @@ 56012B9F257404DF00B4925B /* CommonTableExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56012B742574048B00B4925B /* CommonTableExpression.swift */; }; 560233C42724234F00529DF3 /* SharedValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560233C32724234F00529DF3 /* SharedValueObservation.swift */; }; 560233C92724338800529DF3 /* SharedValueObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560233C82724338800529DF3 /* SharedValueObservationTests.swift */; }; + 5603CEBA2AC862EC00CF097D /* SQLJSONFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEB62AC862EC00CF097D /* SQLJSONFunctions.swift */; }; + 5603CEBB2AC862EC00CF097D /* SQLJSONExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEB72AC862EC00CF097D /* SQLJSONExpressible.swift */; }; + 5603CEBC2AC862EC00CF097D /* JSONColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEB82AC862EC00CF097D /* JSONColumn.swift */; }; + 5603CED42AC8642F00CF097D /* JSONExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEC92AC8631600CF097D /* JSONExpressionsTests.swift */; }; 560432A0228F00C2009D3FE2 /* OrderedDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56043299228F00C2009D3FE2 /* OrderedDictionaryTests.swift */; }; 560432A3228F1668009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560432A2228F1667009D3FE2 /* AssociationPrefetchingObservationTests.swift */; }; 5604484925DEEEF7002BAA79 /* AssociationPrefetchingRelationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5604484825DEEEF7002BAA79 /* AssociationPrefetchingRelationTests.swift */; }; @@ -54,6 +58,9 @@ 561CFA7823735016000C8BAA /* TableRecordUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA7123735015000C8BAA /* TableRecordUpdateTests.swift */; }; 561CFA982376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA912376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift */; }; 561CFA9C2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA9B2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift */; }; + 561F38D82AC88A550051EEE9 /* JSONColumnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38D72AC88A550051EEE9 /* JSONColumnTests.swift */; }; + 561F38EF2AC9CE130051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38EE2AC9CE130051EEE9 /* DatabaseDataEncodingStrategyTests.swift */; }; + 561F38F42AC9CE510051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F32AC9CE510051EEE9 /* DatabaseDataDecodingStrategyTests.swift */; }; 562205F11E420E47005860AC /* DatabasePoolReleaseMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363CF1C943D13000BE133 /* DatabasePoolReleaseMemoryTests.swift */; }; 562205F21E420E47005860AC /* DatabasePoolSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */; }; 562205F31E420E47005860AC /* DatabaseQueueReleaseMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */; }; @@ -418,6 +425,10 @@ 56012B742574048B00B4925B /* CommonTableExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonTableExpression.swift; sourceTree = ""; }; 560233C32724234F00529DF3 /* SharedValueObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedValueObservation.swift; sourceTree = ""; }; 560233C82724338800529DF3 /* SharedValueObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedValueObservationTests.swift; sourceTree = ""; }; + 5603CEB62AC862EC00CF097D /* SQLJSONFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLJSONFunctions.swift; sourceTree = ""; }; + 5603CEB72AC862EC00CF097D /* SQLJSONExpressible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLJSONExpressible.swift; sourceTree = ""; }; + 5603CEB82AC862EC00CF097D /* JSONColumn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONColumn.swift; sourceTree = ""; }; + 5603CEC92AC8631600CF097D /* JSONExpressionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONExpressionsTests.swift; sourceTree = ""; }; 56043299228F00C2009D3FE2 /* OrderedDictionaryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionaryTests.swift; sourceTree = ""; }; 560432A2228F1667009D3FE2 /* AssociationPrefetchingObservationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingObservationTests.swift; sourceTree = ""; }; 5604484825DEEEF7002BAA79 /* AssociationPrefetchingRelationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRelationTests.swift; sourceTree = ""; }; @@ -458,6 +469,9 @@ 561CFA7123735015000C8BAA /* TableRecordUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecordUpdateTests.swift; sourceTree = ""; }; 561CFA912376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyThroughOrderingTests.swift; sourceTree = ""; }; 561CFA9B2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyOrderingTests.swift; sourceTree = ""; }; + 561F38D72AC88A550051EEE9 /* JSONColumnTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONColumnTests.swift; sourceTree = ""; }; + 561F38EE2AC9CE130051EEE9 /* DatabaseDataEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataEncodingStrategyTests.swift; sourceTree = ""; }; + 561F38F32AC9CE510051EEE9 /* DatabaseDataDecodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataDecodingStrategyTests.swift; sourceTree = ""; }; 562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = ""; }; 5623932F1DEDFC5700A6B01F /* AnyCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCursorTests.swift; sourceTree = ""; }; 5623934D1DEDFEFB00A6B01F /* EnumeratedCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedCursorTests.swift; sourceTree = ""; }; @@ -842,6 +856,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5603CEB52AC862EC00CF097D /* JSON */ = { + isa = PBXGroup; + children = ( + 5603CEB82AC862EC00CF097D /* JSONColumn.swift */, + 5603CEB72AC862EC00CF097D /* SQLJSONExpressible.swift */, + 5603CEB62AC862EC00CF097D /* SQLJSONFunctions.swift */, + ); + path = JSON; + sourceTree = ""; + }; + 5603CEC82AC8630300CF097D /* JSON */ = { + isa = PBXGroup; + children = ( + 561F38D72AC88A550051EEE9 /* JSONColumnTests.swift */, + 5603CEC92AC8631600CF097D /* JSONExpressionsTests.swift */, + ); + name = JSON; + sourceTree = ""; + }; 5605F1471C672E4000235C62 /* Support */ = { isa = PBXGroup; children = ( @@ -921,6 +954,7 @@ isa = PBXGroup; children = ( D263F40926C613090038B07F /* DatabaseColumnEncodingStrategyTests.swift */, + 561F38EE2AC9CE130051EEE9 /* DatabaseDataEncodingStrategyTests.swift */, 5665FA322129EEA0004D8612 /* DatabaseDateEncodingStrategyTests.swift */, 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */, 566A843F2041914000E50BFD /* MutablePersistableRecordChangesTests.swift */, @@ -948,12 +982,13 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( + 56677C14241D14450050755D /* FailureTestCase.swift */, + 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, - 56677C14241D14450050755D /* FailureTestCase.swift */, 5698AC3E1DA2BEBB0056AF8C /* FTS */, 56176CA01EACEE2A000F3F2B /* GRDBCipher */, - 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, + 5603CEC82AC8630300CF097D /* JSON */, 56A238231B9C74A90082EB20 /* Migrations */, 569978D31B539038005EBEED /* Private */, 56300B5C1C53C38F005A543B /* QueryInterface */, @@ -1278,6 +1313,7 @@ 5674A7251F30A8EF0095F066 /* FetchableRecord */ = { isa = PBXGroup; children = ( + 561F38F32AC9CE510051EEE9 /* DatabaseDataDecodingStrategyTests.swift */, 5665FA132129C9D6004D8612 /* DatabaseDateDecodingStrategyTests.swift */, 5674A7261F30A9090095F066 /* FetchableRecordDecodableTests.swift */, 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */, @@ -1644,6 +1680,7 @@ 566DDE0C288D763C0000DCFB /* Fixits.swift */, 56A2386F1B9C75030082EB20 /* Core */, 5698AC291D9E5A480056AF8C /* FTS */, + 5603CEB52AC862EC00CF097D /* JSON */, 56A238911B9C750B0082EB20 /* Migration */, 56300B6D1C53F592005A543B /* QueryInterface */, 56A2389F1B9C753B0082EB20 /* Record */, @@ -1839,6 +1876,7 @@ 56CC9243201E034D00CB597E /* PrefixWhileCursorTests.swift in Sources */, 560714E3227DD0810091BB10 /* AssociationPrefetchingSQLTests.swift in Sources */, 56D496841D813147008276D7 /* SelectStatementTests.swift in Sources */, + 561F38D82AC88A550051EEE9 /* JSONColumnTests.swift in Sources */, 56D496B11D8133BC008276D7 /* DatabaseQueueReadOnlyTests.swift in Sources */, 56D4968C1D81316E008276D7 /* RawRepresentable+DatabaseValueConvertibleTests.swift in Sources */, 56419C6D24A519A2004967E1 /* ValueObservationPublisherTests.swift in Sources */, @@ -1853,6 +1891,7 @@ 56D496BF1D8135D4008276D7 /* TableDefinitionTests.swift in Sources */, 5674A7171F3087710095F066 /* DatabaseValueConvertibleDecodableTests.swift in Sources */, 56D496801D813131008276D7 /* StatementColumnConvertibleFetchTests.swift in Sources */, + 5603CED42AC8642F00CF097D /* JSONExpressionsTests.swift in Sources */, 563B0705218627F800B38F35 /* ValueObservationRowTests.swift in Sources */, 56D4966E1D81309E008276D7 /* RecordPrimaryKeyMultipleTests.swift in Sources */, 56D496891D81316E008276D7 /* DatabaseValueConvertibleFetchTests.swift in Sources */, @@ -1930,6 +1969,7 @@ 56D496B81D813465008276D7 /* DataMemoryTests.swift in Sources */, 563B06CA2185D2E500B38F35 /* ValueObservationFetchTests.swift in Sources */, 56D496541D812F5B008276D7 /* SQLExpressionLiteralTests.swift in Sources */, + 561F38F42AC9CE510051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */, 56D496961D81317B008276D7 /* PersistableRecordTests.swift in Sources */, 5616B4FB28B5F5220052017E /* SingletonRecordTest.swift in Sources */, 56419C5724A51998004967E1 /* Inverted.swift in Sources */, @@ -1940,6 +1980,7 @@ 56057C552291B16A00A7CB10 /* AssociationHasManyRowScopeTests.swift in Sources */, 56FEB8F8248403000081AF83 /* DatabaseTraceTests.swift in Sources */, 56419C5124A51998004967E1 /* Finished.swift in Sources */, + 561F38EF2AC9CE130051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */, 56176C5E1EACCCC7000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */, 564D4F7E261C6DC200F55856 /* CaseInsensitiveIdentifierTests.swift in Sources */, 56FEE7FB1F47253700D930EA /* TableRecordTests.swift in Sources */, @@ -2153,6 +2194,7 @@ 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */, 566BE71E2342542F00A8254B /* LockedBox.swift in Sources */, 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */, + 5603CEBB2AC862EC00CF097D /* SQLJSONExpressible.swift in Sources */, 56F89DF72A57EAA9002FE2AA /* ColumnDefinition.swift in Sources */, 5611620825757583007AAF99 /* JoinAssociation.swift in Sources */, 5695311F1C907A8C00CF1A2B /* DatabaseSchemaCache.swift in Sources */, @@ -2181,9 +2223,11 @@ 56D110BF28AFC51000E64463 /* MutablePersistableRecord+Insert.swift in Sources */, 566B9C2025C6CC24004542CF /* RowDecodingError.swift in Sources */, 5698AD211DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, + 5603CEBC2AC862EC00CF097D /* JSONColumn.swift in Sources */, 56A238831B9C75030082EB20 /* DatabaseQueue.swift in Sources */, 5605F1671C672E4000235C62 /* NSNumber.swift in Sources */, 56E9FADA221053DD00C703A8 /* SQL.swift in Sources */, + 5603CEBA2AC862EC00CF097D /* SQLJSONFunctions.swift in Sources */, 56717271261C68E900423B6F /* CaseInsensitiveIdentifier.swift in Sources */, 563CBBE12A595131008905CE /* SQLIndexGenerator.swift in Sources */, C96C0F2B2084A442006B2981 /* SQLiteDateParser.swift in Sources */, diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 86ef1b8fa6..532054b08a 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1826,6 +1826,14 @@ extension Database { /// The `TEXT` column type. public static let text = ColumnType(rawValue: "TEXT") + /// The `TEXT` column type, suitable for JSON columns. + /// + /// SQLite JSON functions and operators are + /// [documented](https://www.sqlite.org/json1.html#interface_overview) + /// to throw errors if any of their arguments are binary blobs. + /// That's the reason why it is recommended to store JSON as text. + public static let jsonText = ColumnType(rawValue: "TEXT") + /// The `INTEGER` column type. public static let integer = ColumnType(rawValue: "INTEGER") diff --git a/GRDB/Documentation.docc/DatabaseSchema.md b/GRDB/Documentation.docc/DatabaseSchema.md index 5e466a204c..10cfc3c9f3 100644 --- a/GRDB/Documentation.docc/DatabaseSchema.md +++ b/GRDB/Documentation.docc/DatabaseSchema.md @@ -24,7 +24,9 @@ When a schema change is not directly supported by SQLite, or not available on th Even though all schema are supported, some features of the library and of the Swift language are easier to use when the schema follows a few conventions described below. -When those conventions are not applied, or not applicable, you will have to perform extra configurations. +When those conventions are not applied, or not applicable, you will have to perform extra configurations. + +For recommendations specific to JSON columns, see . ### Table names should be English, singular, and camelCased diff --git a/GRDB/Documentation.docc/GRDB.md b/GRDB/Documentation.docc/GRDB.md index cf3bb29a01..f8e875c70a 100644 --- a/GRDB/Documentation.docc/GRDB.md +++ b/GRDB/Documentation.docc/GRDB.md @@ -93,14 +93,9 @@ let players: [Player] = try dbQueue.read { db in - - -### Responding to Database Changes +### Database Tools - - -### Full-Text Search - - - -### Combine Publishers - +- - ``DatabasePublishers`` diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md new file mode 100644 index 0000000000..cbef771379 --- /dev/null +++ b/GRDB/Documentation.docc/JSON.md @@ -0,0 +1,151 @@ +# JSON Support + +Store and use JSON values in SQLite databases. + +## Overview + +SQLite and GRDB can store and fetch JSON values in database columns. Starting SQLite 3.38.0 (iOS 16+, macOS 13.2+, tvOS 17+, and watchOS 9+), JSON values can be manipulated at the database level. + +## Store and fetch JSON values + +### JSON columns in the database schema + +It is recommended to store JSON values in text columns. In the example below, we create a ``Database/ColumnType/jsonText`` column with ``Database/create(table:options:body:)``: + +```swift +try db.create(table: "player") { t in + t.primaryKey("id", .text) + t.column("name", .text).notNull() + t.column("address", .jsonText).notNull() // A JSON column +} +``` + +> Note: `.jsonText` and `.text` are equivalent, because both build a TEXT column in SQL. Yet the former better describes the intent of the column. +> +> Note: SQLite JSON functions and operators are [documented](https://www.sqlite.org/json1.html#interface_overview) to throw errors if any of their arguments are binary blobs. That's the reason why it is recommended to store JSON as text. + +> Tip: When an application performs queries on values embedded inside JSON columns, indexes can help performance: +> +> ```swift +> // CREATE INDEX "player_on_country" +> // ON "player"("address" ->> 'country') +> try db.create( +> index: "player_on_country", +> on: "player", +> expressions: [ +> JSONColumn("address")["country"], +> ]) +> +> // SELECT * FROM player +> // WHERE "address" ->> 'country' = 'FR' +> let germanPlayers = try Player +> .filter(JSONColumn("address")["country"] == "DE") +> .fetchAll(db) +> ``` + +### Strict and flexible JSON schemas + +[Codable Records](https://github.com/groue/GRDB.swift/blob/master/README.md#codable-records) handle both strict and flexible JSON schemas. + +**For strict schemas**, use `Codable` properties. They will be stored as JSON strings in the database: + +```swift +struct Address: Codable { + var street: String + var city: String + var country: String +} + +struct Player: Codable { + var id: String + var name: String + + // Stored as a JSON string + // {"street": "...", "city": "...", "country": "..."} + var address: Address +} + +extension Player: FetchableRecord, PersistableRecord { } +``` + +**For flexible schemas**, use `String` or `Data` properties. + +In the specific case of `Data` properties, it is recommended to store them as text in the database, because SQLite JSON functions and operators are [documented](https://www.sqlite.org/json1.html#interface_overview) to throw errors if any of their arguments are binary blobs. This encoding is automatic with ``DatabaseDataEncodingStrategy/text``: + +```swift +// JSON String property +struct Player: Codable { + var id: String + var name: String + var address: String // JSON string +} + +extension Player: FetchableRecord, PersistableRecord { } + +// JSON Data property, saved as text in the database +struct Team: Codable { + var id: String + var color: String + var info: Data // JSON UTF8 data +} + +extension Team: FetchableRecord, PersistableRecord { + // Support SQLite JSON functions and operators + // by storing JSON data as database text: + static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text +} +``` + +## Manipulate JSON values at the database level + +[SQLite JSON functions and operators](https://www.sqlite.org/json1.html) are available starting SQLite 3.38.0 (iOS 16+, macOS 13.2+, tvOS 17+, and watchOS 9+). + +Functions such as `JSON`, `JSON_EXTRACT`, `JSON_PATCH` and others are available as static methods on `Database`: ``Database/json(_:)``, ``Database/jsonExtract(_:atPath:)``, ``Database/jsonPatch(_:with:)``, etc. + +See the full list below. + +## JSON table-valued functions + +The JSON table-valued functions `json_each` and `json_tree` are not supported. + +## Topics + +### JSON Values + +- ``SQLJSONExpressible`` +- ``JSONColumn`` + +### Access JSON subcomponents, and query JSON values, at the SQL level + +The `->` and `->>` SQL operators are available on the ``SQLJSONExpressible`` protocol. + +- ``Database/jsonArrayLength(_:)`` +- ``Database/jsonArrayLength(_:atPath:)`` +- ``Database/jsonExtract(_:atPath:)`` +- ``Database/jsonExtract(_:atPaths:)`` +- ``Database/jsonType(_:)`` +- ``Database/jsonType(_:atPath:)`` + +### Build new JSON values at the SQL level + +- ``Database/json(_:)`` +- ``Database/jsonArray(_:)-8xxe3`` +- ``Database/jsonArray(_:)-469db`` +- ``Database/jsonObject(_:)`` +- ``Database/jsonQuote(_:)`` +- ``Database/jsonGroupArray(_:)`` +- ``Database/jsonGroupObject(key:value:)`` + +### Modify JSON values at the SQL level + +- ``Database/jsonInsert(_:_:)`` +- ``Database/jsonPatch(_:with:)`` +- ``Database/jsonReplace(_:_:)`` +- ``Database/jsonRemove(_:atPath:)`` +- ``Database/jsonRemove(_:atPaths:)`` +- ``Database/jsonSet(_:_:)`` + +### Validate JSON values at the SQL level + +- ``Database/jsonErrorPosition(_:)`` +- ``Database/jsonIsValid(_:)`` diff --git a/GRDB/Documentation.docc/QueryInterface.md b/GRDB/Documentation.docc/QueryInterface.md index 39357bd47e..f09b20f473 100644 --- a/GRDB/Documentation.docc/QueryInterface.md +++ b/GRDB/Documentation.docc/QueryInterface.md @@ -17,16 +17,22 @@ For an overview, see [Records](https://github.com/groue/GRDB.swift/blob/master/R - ``PersistableRecord`` - ``TableRecord`` -### Associations +### Expressions -- ``Association`` +- ``Column`` +- ``JSONColumn`` +- ``SQLExpression`` -### Query Interface Requests +### Requests - ``CommonTableExpression`` - ``QueryInterfaceRequest`` - ``Table`` +### Associations + +- ``Association`` + ### Errors - ``RecordError`` @@ -34,9 +40,11 @@ For an overview, see [Records](https://github.com/groue/GRDB.swift/blob/master/R ### Supporting Types +- ``ColumnExpression`` - ``DerivableRequest`` - ``SQLExpressible`` +- ``SQLJSONExpressible`` - ``SQLSpecificExpressible`` +- ``SQLSubqueryable`` - ``SQLOrderingTerm`` - ``SQLSelectable`` -- ``SQLSubqueryable`` diff --git a/GRDB/JSON/JSONColumn.swift b/GRDB/JSON/JSONColumn.swift new file mode 100644 index 0000000000..6ca345dd74 --- /dev/null +++ b/GRDB/JSON/JSONColumn.swift @@ -0,0 +1,91 @@ +/// A JSON column in a database table. +/// +/// ## Overview +/// +/// `JSONColumn` has benefits over ``Column`` for database columns that +/// contain JSON strings. +/// +/// It behaves like a regular `Column`, with all extra conveniences and +/// behaviors of ``SQLJSONExpressible``. +/// +/// For example, the sample code below directly accesses the "countryCode" +/// key of the "address" JSON column: +/// +/// ```swift +/// struct Player: Codable { +/// var id: Int64 +/// var name: String +/// var address: Address +/// } +/// +/// struct Address: Codable { +/// var street: String +/// var city: String +/// var countryCode: String +/// } +/// +/// extension Player: FetchableRecord, PersistableRecord { +/// enum Columns { +/// static let id = Column(CodingKeys.id) +/// static let name = Column(CodingKeys.name) +/// static let address = JSONColumn(CodingKeys.address) // JSONColumn! +/// } +/// } +/// +/// try dbQueue.write { db in +/// // In a real app, table creation should happen in a migration. +/// try db.create(table: "player") { t in +/// t.autoIncrementedPrimaryKey("id") +/// t.column("name", .text).notNull() +/// t.column("address", .jsonText).notNull() +/// } +/// +/// // Fetch all country codes +/// // SELECT DISTINCT address ->> 'countryCode' FROM player +/// let countryCodes: [String] = try Player +/// .select(Player.Columns.address["countryCode"], as: String.self) +/// .distinct() +/// .fetchAll(db) +/// } +/// ``` +/// +/// > Tip: When you can not create a `JSONColumn`, you'll get the same +/// > convenient access to JSON subcomponents +/// > with ``SQLSpecificExpressible/asJSON``. +/// > +/// > For example, the above sample can be adapted as below: +/// > +/// > ```swift +/// > extension Player: FetchableRecord, PersistableRecord { +/// > // That's another valid way to define columns. +/// > // But we don't have any JSONColumn this time. +/// > enum Columns: String, ColumnExpression { +/// > case id, name, address +/// > } +/// > } +/// > +/// > try dbQueue.write { db in +/// > // Fetch all country codes +/// > // SELECT DISTINCT address ->> 'countryCode' FROM player +/// > let countryCodes: [String] = try Player +/// > .select(Player.Columns.address.asJSON["countryCode"], as: String.self) +/// > .distinct() +/// > .fetchAll(db) +/// > } +/// > ``` +public struct JSONColumn: ColumnExpression, SQLJSONExpressible { + public var name: String + + /// Creates a `JSONColumn` given its name. + /// + /// The name should be unqualified, such as `"score"`. Qualified name such + /// as `"player.score"` are unsupported. + public init(_ name: String) { + self.name = name + } + + /// Creates a `JSONColumn` given a `CodingKey`. + public init(_ codingKey: some CodingKey) { + self.name = codingKey.stringValue + } +} diff --git a/GRDB/JSON/SQLJSONExpressible.swift b/GRDB/JSON/SQLJSONExpressible.swift new file mode 100644 index 0000000000..fed63174cd --- /dev/null +++ b/GRDB/JSON/SQLJSONExpressible.swift @@ -0,0 +1,435 @@ +/// A type of SQL expression that is interpreted as a JSON value. +/// +/// ## Overview +/// +/// JSON values that conform to `SQLJSONExpressible` have two purposes: +/// +/// - They provide Swift APIs for accessing their JSON subcomponents at +/// the SQL level. +/// +/// - When used in a JSON-building function such as +/// ``Database/jsonArray(_:)-8xxe3`` or ``Database/jsonObject(_:)``, +/// they are parsed and interpreted as JSON, not as plain strings. +/// +/// To build a JSON value, create a ``JSONColumn``, or call the +/// ``SQLSpecificExpressible/asJSON`` property of any +/// other expression. +/// +/// For example, here are some JSON values: +/// +/// ```swift +/// // JSON columns: +/// JSONColumn("info") +/// Column("info").asJSON +/// +/// // The JSON array [1, 2, 3]: +/// "[1, 2, 3]".databaseValue.asJSON +/// +/// // A JSON value that will trigger a +/// // "malformed JSON" SQLite error when +/// // parsed by SQLite: +/// "{foo".databaseValue.asJSON +/// ``` +/// +/// The expressions below are not JSON values: +/// +/// ```swift +/// // A plain column: +/// Column("info") +/// +/// // Plain strings: +/// "[1, 2, 3]" +/// "{foo" +/// ``` +/// +/// ## Access JSON subcomponents +/// +/// JSON values provide access to the [`->` and `->>` SQL operators](https://www.sqlite.org/json1.html) +/// and other SQLite JSON functions: +/// +/// ```swift +/// let info = JSONColumn("info") +/// +/// // SELECT info ->> 'firstName' FROM player +/// // → 'Arthur' +/// let firstName = try Player +/// .select(info["firstName"], as: String.self) +/// .fetchOne(db) +/// +/// // SELECT info ->> 'address' FROM player +/// // → '{"street":"Rue de Belleville","city":"Paris"}' +/// let address = try Player +/// .select(info["address"], as: String.self) +/// .fetchOne(db) +/// ``` +/// +/// ## Build JSON objects and arrays from JSON values +/// +/// When used in a JSON-building function such as +/// ``Database/jsonArray(_:)-8xxe3`` or ``Database/jsonObject(_:)-5iswr``, +/// JSON values are parsed and interpreted as JSON, not as plain strings. +/// +/// In the example below, we can see how the `JSONColumn` is interpreted as +/// JSON, while the `Column` with the same name is interpreted as a +/// plain string: +/// +/// ```swift +/// let elements: [any SQLExpressible] = [ +/// JSONColumn("address"), +/// Column("address"), +/// ] +/// +/// let array = Database.jsonArray(elements) +/// +/// // SELECT JSON_ARRAY(JSON(address), address) FROM player +/// // → '[{"country":"FR"},"{\"country\":\"FR\"}"]' +/// // <--- object ---> <------ string ------> +/// let json = try Player +/// .select(array, as: String.self) +/// .fetchOne(db) +/// ``` +/// +/// ## Topics +/// +/// ### Accessing JSON subcomponents +/// +/// - ``subscript(_:)`` +/// - ``jsonExtract(atPath:)`` +/// - ``jsonExtract(atPaths:)`` +/// - ``jsonRepresentation(atPath:)`` +/// +/// ### Supporting Types +/// +/// - ``AnySQLJSONExpressible`` +public protocol SQLJSONExpressible: SQLSpecificExpressible { } + +extension ColumnExpression where Self: SQLJSONExpressible { + /// Returns an SQL column that is interpreted as a JSON value. + public var sqlExpression: SQLExpression { + .column(name).withPreferredJSONInterpretation(.jsonValue) + } +} + +// This type only grants access to `SQLJSONExpressible` apis. The fact that +// it is a JSON value is embedded in its +// `sqlExpression.preferredJSONInterpretation`. +/// A type-erased ``SQLJSONExpressible``. +public struct AnySQLJSONExpressible: SQLJSONExpressible { + /// An SQL expression that is interpreted as a JSON value. + public let sqlExpression: SQLExpression + + public init(_ base: some SQLJSONExpressible) { + self.init(sqlExpression: base.sqlExpression) + } + + /// - Precondition: `sqlExpression` is a JSON value + init(sqlExpression: SQLExpression) { + assert(sqlExpression.preferredJSONInterpretation == .jsonValue) + self.sqlExpression = sqlExpression + } +} + +extension SQLSpecificExpressible { + /// Returns an expression that is interpreted as a JSON value. + /// + /// For example: + /// + /// ```swift + /// let info = Column("info").asJSON + /// + /// // SELECT info ->> 'firstName' FROM player + /// // → 'Arthur' + /// let firstName = try Player + /// .select(info["firstName"], as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// For more information, see ``SQLJSONExpressible``. + public var asJSON: AnySQLJSONExpressible { + AnySQLJSONExpressible(sqlExpression: sqlExpression.withPreferredJSONInterpretation(.jsonValue)) + } +} + +#if GRDBCUSTOMSQLITE || GRDBCIPHER +extension SQLJSONExpressible { + /// The `->>` SQL operator. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT info ->> 'firstName' FROM player + /// // → 'Arthur' + /// let firstName = try Player + /// .select(info["firstName"], as: String.self) + /// .fetchOne(db) + /// + /// // SELECT info ->> 'address' FROM player + /// // → '{"street":"Rue de Belleville","city":"Paris"}' + /// let address = try Player + /// .select(info["address"], as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments), + /// or an JSON object field label, or an array index. + public subscript(_ path: some SQLExpressible) -> SQLExpression { + .binary(.jsonExtractSQL, sqlExpression, path.sqlExpression) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT JSON_EXTRACT(info, '$.firstName') FROM player + /// // → 'Arthur' + /// let firstName = try Player + /// .select(info.jsonExtract(atPath: "$.firstName"), as: String.self) + /// .fetchOne(db) + /// + /// // SELECT JSON_EXTRACT(info, '$.address') FROM player + /// // → '{"street":"Rue de Belleville","city":"Paris"}' + /// let address = try Player + /// .select(info.jsonExtract(atPath: "$.address"), as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public func jsonExtract(atPath path: some SQLExpressible) -> SQLExpression { + Database.jsonExtract(self, atPath: path) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT JSON_EXTRACT(info, '$.firstName', '$.lastName') FROM player + /// // → '["Arthur","Miller"]' + /// let nameComponents = try Player + /// .select(info.jsonExtract(atPaths: ["$.firstName", "$.lastName"]), as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public func jsonExtract(atPaths paths: C) -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + Database.jsonExtract(self, atPaths: paths) + } + + /// Returns a valid JSON string with the `->` SQL operator. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT info -> 'firstName' FROM player + /// // → '"Arthur"' + /// let name = try Player + /// .select(info.jsonRepresentation(atPath: "firstName"), as: String.self) + /// .fetchOne(db) + /// + /// // SELECT info -> 'address' FROM player + /// // → '{"street":"Rue de Belleville","city":"Paris"}' + /// let name = try Player + /// .select(info.jsonRepresentation(atPath: "address"), as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments), + /// or an JSON object field label, or an array index. + public func jsonRepresentation(atPath path: some SQLExpressible) -> SQLExpression { + .binary(.jsonExtractJSON, sqlExpression, path.sqlExpression) + } +} +#else +@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+ +extension SQLJSONExpressible { + /// The `->>` SQL operator. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT info ->> 'firstName' FROM player + /// // → 'Arthur' + /// let firstName = try Player + /// .select(info["firstName"], as: String.self) + /// .fetchOne(db) + /// + /// // SELECT info ->> 'address' FROM player + /// // → '{"street":"Rue de Belleville","city":"Paris"}' + /// let address = try Player + /// .select(info["address"], as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments), + /// or an JSON object field label, or an array index. + public subscript(_ path: some SQLExpressible) -> SQLExpression { + .binary(.jsonExtractSQL, sqlExpression, path.sqlExpression) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT JSON_EXTRACT(info, '$.firstName') FROM player + /// // → 'Arthur' + /// let firstName = try Player + /// .select(info.jsonExtract(atPath: "$.firstName"), as: String.self) + /// .fetchOne(db) + /// + /// // SELECT JSON_EXTRACT(info, '$.address') FROM player + /// // → '{"street":"Rue de Belleville","city":"Paris"}' + /// let address = try Player + /// .select(info.jsonExtract(atPath: "$.address"), as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public func jsonExtract(atPath path: some SQLExpressible) -> SQLExpression { + Database.jsonExtract(self, atPath: path) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT JSON_EXTRACT(info, '$.firstName', '$.lastName') FROM player + /// // → '["Arthur","Miller"]' + /// let nameComponents = try Player + /// .select(info.jsonExtract(atPaths: ["$.firstName", "$.lastName"]), as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public func jsonExtract(atPaths paths: C) -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + Database.jsonExtract(self, atPaths: paths) + } + + /// Returns a valid JSON string with the `->` SQL operator. + /// + /// For example: + /// + /// ```swift + /// let info = JSONColumn("info") + /// + /// // SELECT info -> 'firstName' FROM player + /// // → '"Arthur"' + /// let name = try Player + /// .select(info.jsonRepresentation(atPath: "firstName"), as: String.self) + /// .fetchOne(db) + /// + /// // SELECT info -> 'address' FROM player + /// // → '{"street":"Rue de Belleville","city":"Paris"}' + /// let name = try Player + /// .select(info.jsonRepresentation(atPath: "address"), as: String.self) + /// .fetchOne(db) + /// ``` + /// + /// Related SQL documentation: + /// + /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments), + /// or an JSON object field label, or an array index. + public func jsonRepresentation(atPath path: some SQLExpressible) -> SQLExpression { + .binary(.jsonExtractJSON, sqlExpression, path.sqlExpression) + } +} + +// TODO: Enable when those apis are ready. +// @available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+ +// extension ColumnExpression where Self: SQLJSONExpressible { +// /// Updates a columns with the `JSON_PATCH` SQL function. +// /// +// /// For example: +// /// +// /// ```swift +// /// // UPDATE player SET address = JSON_PATCH(address, '{"country": "FR"}') +// /// try Player.updateAll(db, [ +// /// JSONColumn("address").jsonPatch(#"{"country": "FR"}"#) +// /// ]) +// /// ``` +// /// +// /// Related SQLite documentation: +// public func jsonPatch( +// with patch: some SQLExpressible) +// -> ColumnAssignment +// { +// .init(columnName: name, value: Database.jsonPatch(self, with: patch)) +// } +// +// /// Updates a columns with the `JSON_REMOVE` SQL function. +// /// +// /// For example: +// /// +// /// ```swift +// /// // UPDATE player SET address = JSON_REMOVE(address, '$.country') +// /// try Player.updateAll(db, [ +// /// JSONColumn("address").jsonRemove(atPath: "$.country") +// /// ]) +// /// ``` +// /// +// /// Related SQLite documentation: +// /// +// /// - Parameters: +// /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). +// public func jsonRemove(atPath path: some SQLExpressible) -> ColumnAssignment { +// .init(columnName: name, value: Database.jsonRemove(self, atPath: path)) +// } +// +// /// Updates a columns with the `JSON_REMOVE` SQL function. +// /// +// /// For example: +// /// +// /// ```swift +// /// // UPDATE player SET address = JSON_REMOVE(address, '$.country', '$.city') +// /// try Player.updateAll(db, [ +// /// JSONColumn("address").jsonRemove(atPatsh: ["$.country", "$.city"]) +// /// ]) +// /// ``` +// /// +// /// Related SQLite documentation: +// /// +// /// - Parameters: +// /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). +// public func jsonRemove(atPaths paths: C) +// -> ColumnAssignment +// where C: Collection, C.Element: SQLExpressible +// { +// .init(columnName: name, value: Database.jsonRemove(self, atPaths: paths)) +// } +// +// } +#endif diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift new file mode 100644 index 0000000000..0f172876f2 --- /dev/null +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -0,0 +1,777 @@ +#if GRDBCUSTOMSQLITE || GRDBCIPHER +extension Database { + /// Validates and minifies a JSON string, with the `JSON` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON(' { "a": [ "test" ] } ') → '{"a":["test"]}' + /// Database.json(#" { "a": [ "test" ] } "#) + /// ``` + /// + /// Related SQLite documentation: + public static func json(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON", [value.sqlExpression]) + } + + /// Creates a JSON array with the `JSON_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY(1, 2, 3, 4) → '[1,2,3,4]' + /// Database.jsonArray(1...4) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonArray(_ values: C) -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) + } + + /// Creates a JSON array with the `JSON_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY(1, 2, '3', 4) → '[1,2,"3",4]' + /// Database.jsonArray([1, 2, "3", 4]) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonArray(_ values: C) -> SQLExpression + where C: Collection, C.Element == any SQLExpressible + { + .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) + } + + /// The number of elements in a JSON array, as returned by the + /// `JSON_ARRAY_LENGTH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY_LENGTH('[1,2,3,4]') → 4 + /// Database.jsonArrayLength("[1,2,3,4]") + /// ``` + /// + /// Related SQLite documentation: + public static func jsonArrayLength(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_ARRAY_LENGTH", [value.sqlExpression]) + } + + /// The number of elements in a JSON array, as returned by the + /// `JSON_ARRAY_LENGTH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY_LENGTH('{"one":[1,2,3]}', '$.one') → 3 + /// Database.jsonArrayLength(#"{"one":[1,2,3]}"#, atPath: "$.one") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON array. + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonArrayLength( + _ value: some SQLExpressible, + atPath path: some SQLExpressible) + -> SQLExpression + { + .function("JSON_ARRAY_LENGTH", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_ERROR_POSITION` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ERROR_POSITION(info) + /// Database.jsonErrorPosition(Column("info")) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonErrorPosition(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_ERROR_POSITION", [value.sqlExpression]) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_EXTRACT('{"a":123}', '$.a') → 123 + /// Database.jsonExtract(#"{"a":123}"#, atPath: "$.a") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonExtract(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSON_EXTRACT", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_EXTRACT('{"a":2,"c":[4,5]}','$.c','$.a') → '[[4,5],2]' + /// Database.jsonExtract(#"{"a":2,"c":[4,5]}"#, atPaths: ["$.c", "$.a"]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonExtract(_ value: some SQLExpressible, atPaths paths: C) + -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + .function("JSON_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) + } + + /// The `JSON_INSERT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_INSERT('[1,2,3,4]','$[#]',99) → '[1,2,3,4,99]' + /// Database.jsonInsert("[1,2,3,4]", ["$[#]": value: 99]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - assignments: A collection of key/value pairs, where keys are + /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonInsert( + _ value: some SQLExpressible, + _ assignments: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_INSERT", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSON_REPLACE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_REPLACE('{"a":2,"c":4}', '$.a', 99) → '{"a":99,"c":4}' + /// Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": 99]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - assignments: A collection of key/value pairs, where keys are + /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonReplace( + _ value: some SQLExpressible, + _ assignments: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_REPLACE", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSON_SET` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_SET('{"a":2,"c":4}', '$.a', 99) → '{"a":99,"c":4}' + /// Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": 99]]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - assignments: A collection of key/value pairs, where keys are + /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonSet( + _ value: some SQLExpressible, + _ assignments: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_SET", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// Creates a JSON object with the `JSON_OBJECT` SQL function. Pass + /// key/value pairs with a Swift collection such as a `Dictionary`. + /// + /// For example: + /// + /// ```swift + /// // JSON_OBJECT('c', '{"e":5}') → '{"c":"{\"e\":5}"}' + /// Database.jsonObject([ + /// "c": #"{"e":5}"#, + /// ]) + /// + /// // JSON_OBJECT('c', JSON_OBJECT('e', 5)) → '{"c":{"e":5}}' + /// Database.jsonObject([ + /// "c": Database.jsonObject(["e": 5])), + /// ]) + /// + /// // JSON_OBJECT('c', JSON('{"e":5}')) → '{"c":{"e":5}}' + /// Database.jsonObject([ + /// "c": Database.json(#"{"e":5}"#), + /// ]) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonObject(_ elements: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_OBJECT", elements.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSON_PATCH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_PATCH('{"a":1,"b":2}','{"c":3,"d":4}') → '{"a":1,"b":2,"c":3,"d":4}' + /// Database.jsonPatch(#"{"a":1,"b":2}"#, #"{"c":3,"d":4}"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonPatch( + _ value: some SQLExpressible, + with patch: some SQLExpressible) + -> SQLExpression + { + .function("JSON_PATCH", [value.sqlExpression, patch.sqlExpression]) + } + + /// The `JSON_REMOVE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_REMOVE('[0,1,2,3,4]', '$[2]') → '[0,1,3,4]' + /// Database.jsonRemove("[0,1,2,3,4]", atPath: "$[2]") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSON_REMOVE", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_REMOVE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_REMOVE('[0,1,2,3,4]', '$[2]','$[0]') → '[1,3,4]' + /// Database.jsonRemove("[0,1,2,3,4]", atPaths: ["$[2]", "$[0]"]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonRemove(_ value: some SQLExpressible, atPaths paths: C) + -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + .function("JSON_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) + } + + /// The `JSON_TYPE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}') → 'object' + /// Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonType(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_TYPE", [value.sqlExpression]) + } + + /// The `JSON_TYPE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}', '$.a') → 'object' + /// Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#, atPath: "$.a") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonType(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSON_TYPE", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_VALID` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_VALID('{"x":35') → 0 + /// Database.jsonIsValid(#"{"x":35"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonIsValid(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_VALID", [value.sqlExpression]) + } + + /// The `JSON_QUOTE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_QUOTE('[1]') → '"[1]"' + /// Database.jsonQuote("[1]") + /// + /// // JSON_QUOTE(JSON('[1]')) → '[1]' + /// Database.jsonQuote(Database.json("[1]")) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonQuote(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_QUOTE", [value.sqlExpression.jsonBuilderExpression]) + } + + /// The `JSON_GROUP_ARRAY` SQL function. + /// + /// Related SQLite documentation: + public static func jsonGroupArray(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_GROUP_ARRAY", [value.sqlExpression.jsonBuilderExpression]) + } + + /// The `JSON_GROUP_OBJECT` SQL function. + /// + /// Related SQLite documentation: + public static func jsonGroupObject(key: some SQLExpressible, value: some SQLExpressible) -> SQLExpression { + .function("JSON_GROUP_OBJECT", [key.sqlExpression, value.sqlExpression.jsonBuilderExpression]) + } +} +#else +@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+ +extension Database { + /// Validates and minifies a JSON string, with the `JSON` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON(' { "a": [ "test" ] } ') → '{"a":["test"]}' + /// Database.json(#" { "a": [ "test" ] } "#) + /// ``` + /// + /// Related SQLite documentation: + public static func json(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON", [value.sqlExpression]) + } + + /// Creates a JSON array with the `JSON_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY(1, 2, 3, 4) → '[1,2,3,4]' + /// Database.jsonArray(1...4) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonArray(_ values: C) -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) + } + + /// Creates a JSON array with the `JSON_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY(1, 2, '3', 4) → '[1,2,"3",4]' + /// Database.jsonArray([1, 2, "3", 4]) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonArray(_ values: C) -> SQLExpression + where C: Collection, C.Element == any SQLExpressible + { + .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) + } + + /// The number of elements in a JSON array, as returned by the + /// `JSON_ARRAY_LENGTH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY_LENGTH('[1,2,3,4]') → 4 + /// Database.jsonArrayLength("[1,2,3,4]") + /// ``` + /// + /// Related SQLite documentation: + public static func jsonArrayLength(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_ARRAY_LENGTH", [value.sqlExpression]) + } + + /// The number of elements in a JSON array, as returned by the + /// `JSON_ARRAY_LENGTH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ARRAY_LENGTH('{"one":[1,2,3]}', '$.one') → 3 + /// Database.jsonArrayLength(#"{"one":[1,2,3]}"#, atPath: "$.one") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON array. + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonArrayLength( + _ value: some SQLExpressible, + atPath path: some SQLExpressible) + -> SQLExpression + { + .function("JSON_ARRAY_LENGTH", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_ERROR_POSITION` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_ERROR_POSITION(info) + /// Database.jsonErrorPosition(Column("info")) + /// ``` + /// + /// Related SQLite documentation: + @available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) + public static func jsonErrorPosition(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_ERROR_POSITION", [value.sqlExpression]) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_EXTRACT('{"a":123}', '$.a') → 123 + /// Database.jsonExtract(#"{"a":123}"#, atPath: "$.a") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonExtract(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSON_EXTRACT", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_EXTRACT('{"a":2,"c":[4,5]}','$.c','$.a') → '[[4,5],2]' + /// Database.jsonExtract(#"{"a":2,"c":[4,5]}"#, atPaths: ["$.c", "$.a"]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonExtract(_ value: some SQLExpressible, atPaths paths: C) + -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + .function("JSON_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) + } + + /// The `JSON_INSERT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_INSERT('[1,2,3,4]','$[#]',99) → '[1,2,3,4,99]' + /// Database.jsonInsert("[1,2,3,4]", ["$[#]": value: 99]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - assignments: A collection of key/value pairs, where keys are + /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonInsert( + _ value: some SQLExpressible, + _ assignments: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_INSERT", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSON_REPLACE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_REPLACE('{"a":2,"c":4}', '$.a', 99) → '{"a":99,"c":4}' + /// Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": 99]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - assignments: A collection of key/value pairs, where keys are + /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonReplace( + _ value: some SQLExpressible, + _ assignments: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_REPLACE", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSON_SET` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_SET('{"a":2,"c":4}', '$.a', 99) → '{"a":99,"c":4}' + /// Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": 99]]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - assignments: A collection of key/value pairs, where keys are + /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonSet( + _ value: some SQLExpressible, + _ assignments: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_SET", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// Creates a JSON object with the `JSON_OBJECT` SQL function. Pass + /// key/value pairs with a Swift collection such as a `Dictionary`. + /// + /// For example: + /// + /// ```swift + /// // JSON_OBJECT('c', '{"e":5}') → '{"c":"{\"e\":5}"}' + /// Database.jsonObject([ + /// "c": #"{"e":5}"#, + /// ]) + /// + /// // JSON_OBJECT('c', JSON_OBJECT('e', 5)) → '{"c":{"e":5}}' + /// Database.jsonObject([ + /// "c": Database.jsonObject(["e": 5])), + /// ]) + /// + /// // JSON_OBJECT('c', JSON('{"e":5}')) → '{"c":{"e":5}}' + /// Database.jsonObject([ + /// "c": Database.json(#"{"e":5}"#), + /// ]) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonObject(_ elements: C) + -> SQLExpression + where C: Collection, + C.Element == (key: String, value: any SQLExpressible) + { + .function("JSON_OBJECT", elements.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSON_PATCH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_PATCH('{"a":1,"b":2}','{"c":3,"d":4}') → '{"a":1,"b":2,"c":3,"d":4}' + /// Database.jsonPatch(#"{"a":1,"b":2}"#, #"{"c":3,"d":4}"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonPatch( + _ value: some SQLExpressible, + with patch: some SQLExpressible) + -> SQLExpression + { + .function("JSON_PATCH", [value.sqlExpression, patch.sqlExpression]) + } + + /// The `JSON_REMOVE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_REMOVE('[0,1,2,3,4]', '$[2]') → '[0,1,3,4]' + /// Database.jsonRemove("[0,1,2,3,4]", atPath: "$[2]") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSON_REMOVE", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_REMOVE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_REMOVE('[0,1,2,3,4]', '$[2]','$[0]') → '[1,3,4]' + /// Database.jsonRemove("[0,1,2,3,4]", atPaths: ["$[2]", "$[0]"]) + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonRemove(_ value: some SQLExpressible, atPaths paths: C) + -> SQLExpression + where C: Collection, C.Element: SQLExpressible + { + .function("JSON_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) + } + + /// The `JSON_TYPE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}') → 'object' + /// Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonType(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_TYPE", [value.sqlExpression]) + } + + /// The `JSON_TYPE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}', '$.a') → 'object' + /// Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#, atPath: "$.a") + /// ``` + /// + /// Related SQLite documentation: + /// + /// - Parameters: + /// - value: A JSON value. + /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + public static func jsonType(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSON_TYPE", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSON_VALID` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_VALID('{"x":35') → 0 + /// Database.jsonIsValid(#"{"x":35"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonIsValid(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_VALID", [value.sqlExpression]) + } + + /// Returns a valid JSON string with the `JSON_QUOTE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSON_QUOTE('[1]') → '"[1]"' + /// Database.jsonQuote("[1]") + /// + /// // JSON_QUOTE(JSON('[1]')) → '[1]' + /// Database.jsonQuote(Database.json("[1]")) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonQuote(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_QUOTE", [value.sqlExpression.jsonBuilderExpression]) + } + + /// The `JSON_GROUP_ARRAY` SQL function. + /// + /// Related SQLite documentation: + public static func jsonGroupArray(_ value: some SQLExpressible) -> SQLExpression { + .function("JSON_GROUP_ARRAY", [value.sqlExpression.jsonBuilderExpression]) + } + + /// The `JSON_GROUP_OBJECT` SQL function. + /// + /// Related SQLite documentation: + public static func jsonGroupObject(key: some SQLExpressible, value: some SQLExpressible) -> SQLExpression { + .function("JSON_GROUP_OBJECT", [key.sqlExpression, value.sqlExpression.jsonBuilderExpression]) + } +} +#endif diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index e253baec8b..9e64e7d259 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -442,7 +442,11 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { // make it impractical to define `filter(id:)`, `fetchOne(_:key:)`, // `deleteAll(_:ids:)` etc. if let recordType = RowDecoder.self as? any EncodableRecord.Type { - if Sequence.Element.self == Date.self || Sequence.Element.self == Optional.self { + if Sequence.Element.self == Data.self || Sequence.Element.self == Optional.self { + let strategy = recordType.databaseDataEncodingStrategy + let keys = keys.compactMap { ($0 as! Data?).flatMap(strategy.encode)?.databaseValue } + return filter(rawKeys: keys) + } else if Sequence.Element.self == Date.self || Sequence.Element.self == Optional.self { let strategy = recordType.databaseDateEncodingStrategy let keys = keys.compactMap { ($0 as! Date?).flatMap(strategy.encode)?.databaseValue } return filter(rawKeys: keys) diff --git a/GRDB/QueryInterface/SQL/Column.swift b/GRDB/QueryInterface/SQL/Column.swift index c540eca4de..a81c07d0a1 100644 --- a/GRDB/QueryInterface/SQL/Column.swift +++ b/GRDB/QueryInterface/SQL/Column.swift @@ -27,6 +27,7 @@ public protocol ColumnExpression: SQLSpecificExpressible { } extension ColumnExpression { + /// Returns an SQL column. public var sqlExpression: SQLExpression { .column(name) } @@ -65,11 +66,27 @@ extension ColumnExpression where Self == Column { /// A column in a database table. /// +/// For example: +/// +/// ```swift +/// struct Player: TableRecord { +/// var score: Int +/// } +/// +/// let maximumScore = try dbQueue.read { db in +/// // SELECT MAX(score) FROM player +/// try Player +/// .select(max(Column("score")), as: Int.self) +/// .fetchOne(db) +/// } +/// ``` +/// /// ## Topics /// /// ### Standard Columns /// /// - ``rowID-3bn70`` +/// - ``rank`` /// /// ### Creating A Column /// diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index 46660ef721..89b809cbb2 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -37,6 +37,25 @@ public struct SQLExpression { private var impl: Impl + /// The preferred interpretation of the expression in JSON + /// building contexts (see `jsonBuilderExpression`). + /// + /// ```swift + /// // Considering: + /// // JSON_ARRAY('[1, 2, 3]') → '["[1, 2, 3]"]' + /// // JSON_ARRAY(JSON('[1, 2, 3]')) → [[1,2,3]] + /// + /// // Compare an expression with preferredJSONInterpretation = .unspecified: + /// // JSON_ARRAY("info") + /// Database.jsonArray([Column("info")]) + /// + /// // ...with an expression with preferredJSONInterpretation = .jsonValue: + /// // JSON_ARRAY(JSON("info")) + /// Database.jsonArray([Column("info").jsonValue]) + /// Database.jsonArray([JSONColumn("info")]) + /// ``` + var preferredJSONInterpretation = JSONInterpretation.deferredToSQLite + /// The private implementation of the public `SQLExpression`. private enum Impl { /// A column. @@ -428,11 +447,22 @@ public struct SQLExpression { /// The SQL operator let sql: String - /// Creates a binary operator + /// A boolean value indicating if the operator is known to return a + /// JSON value. /// - /// BinaryOperator("-") - init(_ sql: String) { + /// A false value does not provide any information. + let isJSONValue: Bool + + /// Creates a binary operator. + /// + /// For example: + /// + /// ``` + /// BinaryOperator("-") + /// ``` + init(_ sql: String, isJSONValue: Bool = false) { self.sql = sql + self.isJSONValue = isJSONValue } /// The `<` binary operator @@ -461,6 +491,14 @@ public struct SQLExpression { /// The `>>` bitwise right shift operator static let rightShift = BinaryOperator(">>") + + // Not guarded by availability checks, but only available for SQLite 3.38+ + /// The `->` SQL operator + static let jsonExtractJSON = BinaryOperator("->", isJSONValue: true) + + // Not guarded by availability checks, but only available for SQLite 3.38+ + /// The `->>` SQL operator + static let jsonExtractSQL = BinaryOperator("->>") } /// `EscapableBinaryOperator` is an SQLite binary operator that accepts an @@ -528,6 +566,36 @@ public struct SQLExpression { /// The `~` unary operator static let bitwiseNot = UnaryOperator("~") } + + /// Describes the interpretation of an expression in a JSON + /// building context. + enum JSONInterpretation { + /// JSON interpretation is deferred to SQLite: + /// + /// ```swift + /// // JSON_ARRAY('[1, 2, 3]') → '["[1, 2, 3]"]' + /// Database.jsonArray(["[1, 2, 3]"]) + /// + /// // JSON_ARRAY(JSON('[1, 2, 3]')) → '[[1, 2, 3]]' + /// Database.jsonArray([Database.json("[1, 2, 3]")]) + /// + /// // JSON_ARRAY("info") + /// Database.jsonArray([Column("info")]) + /// ``` + case deferredToSQLite + + /// Expression is interpreted as a JSON value: + /// + /// ```swift + /// // JSON_ARRAY(JSON('[1, 2, 3]')) → '[[1, 2, 3]]' + /// Database.jsonArray(["[1, 2, 3]"].jsonValue) + /// + /// // JSON_ARRAY(JSON("info")) + /// Database.jsonArray([Column("info").jsonValue]) + /// Database.jsonArray([JSONColumn("info")]) + /// ``` + case jsonValue + } } // MARK: - Creating Expressions @@ -922,7 +990,11 @@ extension SQLExpression { extension SQLExpression { /// Returns a qualified expression func qualified(with alias: TableAlias) -> Self { - .init(impl: impl.qualified(with: alias)) + .init(impl: impl.qualified(with: alias), preferredJSONInterpretation: preferredJSONInterpretation) + } + + func withPreferredJSONInterpretation(_ interpretation: JSONInterpretation) -> Self { + .init(impl: impl, preferredJSONInterpretation: interpretation) } } @@ -1730,6 +1802,12 @@ struct SQLFunctionFlags { /// A boolean value indicating if the function should have `DISTINCT` /// in its SQL generation (as in `COUNT(DISTINCT ...)`). var isDistinct = false + + /// A boolean value indicating if a function is known to return a + /// JSON value. + /// + /// A false value does not provide any information. + var isJSONValue = false } extension SQLFunctionFlags { @@ -1796,6 +1874,20 @@ extension SQLFunctionFlags { "TOTAL", ] + private static let knownFunctionsReturningJSONValue: Set = [ + "JSON", + "JSON_ARRAY", + "JSON_GROUP_ARRAY", + "JSON_GROUP_OBJECT", + "JSON_INSERT", + "JSON_OBJECT", + "JSON_PATCH", + "JSON_REMOVE", + "JSON_REPLACE", + "JSON_SET", + "JSON_QUOTE", + ] + /// Infers flags from the function name and number of arguments. static func defaultFlags(for functionName: String, argumentCount: Int) -> Self { var flags = SQLFunctionFlags() @@ -1810,22 +1902,105 @@ extension SQLFunctionFlags { flags.isAggregate = Self.knownAggregateFunctions.contains(name.uppercased()) } + if name == "JSON_EXTRACT" && argumentCount > 2 { + flags.isJSONValue = true + } else { + flags.isJSONValue = Self.knownFunctionsReturningJSONValue.contains(name) + } + return flags } } +// MARK: - JSON + +extension SQLExpression { + /// A boolean value indicating if the expression is known to be a + /// JSON value. + /// + /// A false value does not provide any information. + /// + /// For examples: + /// + /// ```swift + /// // isJSONValue is true: + /// // + /// // NULL + /// // JSON('[1, 2, 3]') + /// // info -> 'address' + /// DatabaseValue.null + /// Database.json("[1, 2, 3]") + /// JSONColumn("info").jsonRepresentation(forKey: "address") + /// + /// // isJSONValue is false + /// // + /// // '[1, 2, 3]' + /// // info + /// // info ->> 'address' + /// [1, 2, 3].databaseValue + /// JSONColumn("info") + /// JSONColumn("info")["address"] + /// ``` + var isJSONValue: Bool { + switch impl { + case .databaseValue(.null): + return true + + case let .binary(op, _, _): + return op.isJSONValue + + case let .collated(expression, _): + return expression.isJSONValue + + case let .function(_, flags: flags, arguments: _): + return flags.isJSONValue + + default: + return false + } + } + +#if GRDBCUSTOMSQLITE || GRDBCIPHER + /// Returns an expression suitable in JSON building contexts. + var jsonBuilderExpression: SQLExpression { + switch preferredJSONInterpretation { + case .deferredToSQLite: + return self + + case .jsonValue: + if isJSONValue { + return self + } else { + // Needs explicit call to JSON() + return .function("JSON", [self]) + } + } + } +#else + @available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+ + /// Returns an expression suitable in JSON building contexts. + var jsonBuilderExpression: SQLExpression { + switch preferredJSONInterpretation { + case .deferredToSQLite: + return self + + case .jsonValue: + if isJSONValue { + return self + } else { + // Needs explicit call to JSON() + return .function("JSON", [self]) + } + } + } +#endif +} // MARK: - SQLExpressible /// A type that can be used as an SQL expression. /// /// Related SQLite documentation -/// -/// ## Topics -/// -/// ### Supporting Type -/// -/// - ``SQLExpression`` public protocol SQLExpressible { /// Returns an SQL expression. var sqlExpression: SQLExpression { get } @@ -1850,11 +2025,6 @@ extension SQLExpressible where Self == Column { /// /// ## Topics /// -/// ### Column Expressions -/// -/// - ``Column`` -/// - ``ColumnExpression`` -/// /// ### Applying a Collation /// /// - ``collating(_:)-2mr78`` @@ -1862,6 +2032,8 @@ extension SQLExpressible where Self == Column { /// /// ### SQL Functions & Operators /// +/// See also JSON functions in . +/// /// - ``abs(_:)-5l6xp`` /// - ``average(_:)`` /// - ``capitalized`` @@ -1882,6 +2054,10 @@ extension SQLExpressible where Self == Column { /// - ``uppercased`` /// - ``SQLDateModifier`` /// +/// ### Interpreting an expression as JSON +/// +/// - ``asJSON`` +/// /// ### Creating Ordering Terms /// /// - ``asc`` diff --git a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift index cc865de97f..d364a6bbba 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift @@ -431,6 +431,15 @@ public class TableAlias { expression.sqlExpression.qualified(with: self) } + public subscript(_ expression: some SQLJSONExpressible & + SQLSpecificExpressible & + SQLSelectable & + SQLOrderingTerm) + -> AnySQLJSONExpressible + { + AnySQLJSONExpressible(sqlExpression: expression.sqlExpression.qualified(with: self)) + } + /// Returns an SQL ordering term that refers to the aliased table. /// /// For example, let's sort books by author name first, and then by title: diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index 0782dc6b30..c7816c6723 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -128,7 +128,9 @@ private class RecordEncoder: Encoder { } fileprivate func encode(_ value: T, forKey key: any CodingKey) throws where T: Encodable { - if let date = value as? Date { + if let data = value as? Data { + persist(Record.databaseDataEncodingStrategy.encode(data), forKey: key) + } else if let date = value as? Date { persist(Record.databaseDateEncodingStrategy.encode(date), forKey: key) } else if let uuid = value as? UUID { persist(Record.databaseUUIDEncodingStrategy.encode(uuid), forKey: key) diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index 6818fbd28b..30bf4bb483 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -20,11 +20,13 @@ import Foundation // For JSONEncoder /// ### Configuring Persistence for the Standard Encodable Protocol /// /// - ``databaseColumnEncodingStrategy-5sx4v`` +/// - ``databaseDataEncodingStrategy-9y0c7`` /// - ``databaseDateEncodingStrategy-2gtc1`` /// - ``databaseEncodingUserInfo-8upii`` /// - ``databaseJSONEncoder(for:)-6x62c`` /// - ``databaseUUIDEncodingStrategy-2t96q`` /// - ``DatabaseColumnEncodingStrategy`` +/// - ``DatabaseDataEncodingStrategy`` /// - ``DatabaseDateEncodingStrategy`` /// - ``DatabaseUUIDEncodingStrategy`` /// @@ -115,6 +117,24 @@ public protocol EncodableRecord { /// ``encode(to:)-1mrt`` implementation. static func databaseJSONEncoder(for column: String) -> JSONEncoder + /// The strategy for encoding `Data` columns. + /// + /// This property is dedicated to ``EncodableRecord`` types that also + /// conform to the standard `Encodable` protocol and use the default + /// ``encode(to:)-1mrt`` implementation. + /// + /// For example: + /// + /// ```swift + /// struct Player: EncodableRecord, Encodable { + /// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text + /// + /// // Encoded as SQL text. Data must contain valid UTF8 bytes. + /// var jsonData: Data + /// } + /// ``` + static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get } + /// The strategy for encoding `Date` columns. /// /// This property is dedicated to ``EncodableRecord`` types that also @@ -199,6 +219,12 @@ extension EncodableRecord { return encoder } + /// Returns the default strategy for encoding `Data` columns: + /// ``DatabaseDataEncodingStrategy/deferredToData``. + public static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { + .deferredToData + } + /// Returns the default strategy for encoding `Date` columns: /// ``DatabaseDateEncodingStrategy/deferredToDate``. public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { @@ -423,6 +449,48 @@ extension Row { } } +// MARK: - DatabaseDataEncodingStrategy + +/// `DatabaseDataEncodingStrategy` specifies how `EncodableRecord` types that +/// also adopt the standard `Encodable` protocol encode their `Data` properties +/// in the default +/// implementation. +/// +/// For example: +/// +/// ```swift +/// struct Player: EncodableRecord, Encodable { +/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text +/// +/// // Encoded as SQL text. Data must contain valid UTF8 bytes. +/// var jsonData: Data +/// } +/// ``` +public enum DatabaseDataEncodingStrategy { + /// Encodes `Data` columns as SQL blob. + case deferredToData + + /// Encodes `Data` columns as SQL text. Data must contain valid UTF8 bytes. + case text + + /// Encodes `Data` column as the result of the user-provided function. + case custom((Data) -> (any DatabaseValueConvertible)?) + + func encode(_ data: Data) -> DatabaseValue { + switch self { + case .deferredToData: + return data.databaseValue + case .text: + guard let string = String(data: data, encoding: .utf8) else { + fatalError("Invalid UTF8 data") + } + return string.databaseValue + case .custom(let format): + return format(data)?.databaseValue ?? .null + } + } +} + // MARK: - DatabaseDateEncodingStrategy /// `DatabaseDateEncodingStrategy` specifies how `EncodableRecord` types that diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 1d42f5c026..e1d7db7156 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -51,6 +51,7 @@ extension FetchableRecord where Self: Decodable { /// The behavior of the decoder depends on the decoded type. See: /// /// - ``FetchableRecord/databaseColumnDecodingStrategy-6uefz`` +/// - ``FetchableRecord/databaseDataDecodingStrategy-71bh1`` /// - ``FetchableRecord/databaseDateDecodingStrategy-78y03`` /// - ``FetchableRecord/databaseDecodingUserInfo-77jim`` /// - ``FetchableRecord/databaseJSONDecoder(for:)-7lmxd`` @@ -258,7 +259,11 @@ private struct _RowDecoder: Decoder { { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - if type == Date.self { + if type == Data.self { + return try R.databaseDataDecodingStrategy.decodeIfPresent( + fromRow: row, + atUncheckedIndex: index) as! T? + } else if type == Date.self { return try R.databaseDateDecodingStrategy.decodeIfPresent( fromRow: row, atUncheckedIndex: index) as! T? @@ -303,7 +308,9 @@ private struct _RowDecoder: Decoder { { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - if type == Date.self { + if type == Data.self { + return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T + } else if type == Date.self { return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecode(fromRow: row, atUncheckedIndex: index) as! T @@ -546,9 +553,10 @@ extension ColumnDecoder: SingleValueDecodingContainer { func decode(_ type: String.Type) throws -> String { try row.decode(atIndex: columnIndex) } func decode(_ type: T.Type) throws -> T where T: Decodable { - // Prefer DatabaseValueConvertible decoding over Decodable. - // This allows decoding Date from String, or DatabaseValue from NULL. - if type == Date.self { + // TODO: not tested + if type == Data.self { + return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T + } else if type == Date.self { return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecode(fromRow: row, atUncheckedIndex: columnIndex) as! T @@ -566,6 +574,105 @@ private let iso8601Formatter: ISO8601DateFormatter = { return formatter }() +extension DatabaseDataDecodingStrategy { + fileprivate func decodeIfPresent(fromRow row: Row, atUncheckedIndex index: Int) throws -> Data? { + if let sqliteStatement = row.sqliteStatement { + return try decodeIfPresent( + fromStatement: sqliteStatement, + atUncheckedIndex: CInt(index), + context: RowDecodingContext(row: row, key: .columnIndex(index))) + } else { + return try decodeIfPresent( + fromDatabaseValue: row[index], + context: RowDecodingContext(row: row, key: .columnIndex(index))) + } + } + + fileprivate func decode(fromRow row: Row, atUncheckedIndex index: Int) throws -> Data { + if let sqliteStatement = row.sqliteStatement { + return try decode( + fromStatement: sqliteStatement, + atUncheckedIndex: CInt(index), + context: RowDecodingContext(row: row, key: .columnIndex(index))) + } else { + return try decode( + fromDatabaseValue: row[index], + context: RowDecodingContext(row: row, key: .columnIndex(index))) + } + } + + /// - precondition: value is not NULL + fileprivate func decode( + fromStatement sqliteStatement: SQLiteStatement, + atUncheckedIndex index: CInt, + context: @autoclosure () -> RowDecodingContext) + throws -> Data + { + assert(sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL, "unexpected NULL value") + switch self { + case .deferredToData: + return Data(sqliteStatement: sqliteStatement, index: index) + case .custom(let format): + let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) + guard let data = format(dbValue) else { + throw RowDecodingError.valueMismatch( + Data.self, + context: context(), + databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index)) + } + return data + } + } + + fileprivate func decodeIfPresent( + fromStatement sqliteStatement: SQLiteStatement, + atUncheckedIndex index: CInt, + context: @autoclosure () -> RowDecodingContext) + throws -> Data? + { + if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { + return nil + } + return try decode(fromStatement: sqliteStatement, atUncheckedIndex: index, context: context()) + } + + fileprivate func decode( + fromDatabaseValue dbValue: DatabaseValue, + context: @autoclosure () -> RowDecodingContext) + throws -> Data + { + if let data = dataFromDatabaseValue(dbValue) { + return data + } else { + throw RowDecodingError.valueMismatch(Data.self, context: context(), databaseValue: dbValue) + } + } + + fileprivate func decodeIfPresent( + fromDatabaseValue dbValue: DatabaseValue, + context: @autoclosure () -> RowDecodingContext) + throws -> Data? + { + if dbValue.isNull { + return nil + } else if let data = dataFromDatabaseValue(dbValue) { + return data + } else { + throw RowDecodingError.valueMismatch(Data.self, context: context(), databaseValue: dbValue) + } + } + + // Returns nil if decoding fails + private func dataFromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { + switch self { + case .deferredToData: + return Data.fromDatabaseValue(dbValue) + case .custom(let format): + return format(dbValue) + } + } +} + extension DatabaseDateDecodingStrategy { fileprivate func decodeIfPresent(fromRow row: Row, atUncheckedIndex index: Int) throws -> Date? { if let sqliteStatement = row.sqliteStatement { diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 31616d6724..b91cef5177 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -89,10 +89,12 @@ import Foundation /// ### Configuring Row Decoding for the Standard Decodable Protocol /// /// - ``databaseColumnDecodingStrategy-6uefz`` +/// - ``databaseDataDecodingStrategy-71bh1`` /// - ``databaseDateDecodingStrategy-78y03`` /// - ``databaseDecodingUserInfo-77jim`` /// - ``databaseJSONDecoder(for:)-7lmxd`` /// - ``DatabaseColumnDecodingStrategy`` +/// - ``DatabaseDataDecodingStrategy`` /// - ``DatabaseDateDecodingStrategy`` /// /// ### Supporting Types @@ -153,6 +155,29 @@ public protocol FetchableRecord { /// ``init(row:)-4ptlh`` implementation. static func databaseJSONDecoder(for column: String) -> JSONDecoder + /// The strategy for decoding `Data` columns. + /// + /// This property is dedicated to ``FetchableRecord`` types that also + /// conform to the standard `Decodable` protocol and use the default + /// ``init(row:)-4ptlh`` implementation. + /// + /// For example: + /// + /// ```swift + /// struct Player: FetchableRecord, Decodable { + /// static let databaseDataDecodingStrategy = DatabaseDataDecodingStrategy.custom { dbValue + /// guard let base64Data = Data.fromDatabaseValue(dbValue) else { + /// return nil + /// } + /// return Data(base64Encoded: base64Data) + /// } + /// + /// // Decoded from both database base64 strings and blobs + /// var myData: Data + /// } + /// ``` + static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { get } + /// The strategy for decoding `Date` columns. /// /// This property is dedicated to ``FetchableRecord`` types that also @@ -216,6 +241,12 @@ extension FetchableRecord { return decoder } + /// The default strategy for decoding `Data` columns is + /// ``DatabaseDataDecodingStrategy/deferredToData``. + public static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { + .deferredToData + } + /// The default strategy for decoding `Date` columns is /// ``DatabaseDateDecodingStrategy/deferredToDate``. public static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { @@ -823,6 +854,39 @@ public final class RecordCursor: DatabaseCursor { } } +// MARK: - DatabaseDataDecodingStrategy + +/// `DatabaseDataDecodingStrategy` specifies how `FetchableRecord` types that +/// also adopt the standard `Decodable` protocol decode their +/// `Data` properties. +/// +/// For example: +/// +/// ```swift +/// struct Player: FetchableRecord, Decodable { +/// static let databaseDataDecodingStrategy = DatabaseDataDecodingStrategy.custom { dbValue +/// guard let base64Data = Data.fromDatabaseValue(dbValue) else { +/// return nil +/// } +/// return Data(base64Encoded: base64Data) +/// } +/// +/// // Decoded from both database base64 strings and blobs +/// var myData: Data +/// } +/// ``` +public enum DatabaseDataDecodingStrategy { + /// Decodes `Data` columns from SQL blobs and UTF8 text. + case deferredToData + + /// Decodes `Data` columns according to the user-provided function. + /// + /// If the database value does not contain a suitable value, the function + /// must return nil (GRDB will interpret this nil result as a conversion + /// error, and react accordingly). + case custom((DatabaseValue) -> Data?) +} + // MARK: - DatabaseDateDecodingStrategy /// `DatabaseDateDecodingStrategy` specifies how `FetchableRecord` types that diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index b7a5602da9..2b2be73134 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -13,6 +13,10 @@ 56012B82257404A400B4925B /* CommonTableExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56012B80257404A300B4925B /* CommonTableExpression.swift */; }; 560233CF2724339A00529DF3 /* SharedValueObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560233CC2724339A00529DF3 /* SharedValueObservationTests.swift */; }; 560233D127243A9200529DF3 /* SharedValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560233D027243A9100529DF3 /* SharedValueObservation.swift */; }; + 5603CEC42AC862F800CF097D /* SQLJSONFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEBF2AC862F800CF097D /* SQLJSONFunctions.swift */; }; + 5603CEC52AC862F800CF097D /* SQLJSONExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEC02AC862F800CF097D /* SQLJSONExpressible.swift */; }; + 5603CEC62AC862F800CF097D /* JSONColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CEC12AC862F800CF097D /* JSONColumn.swift */; }; + 5603CED52AC8643800CF097D /* JSONExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CECC2AC8633B00CF097D /* JSONExpressionsTests.swift */; }; 56043296228F00A9009D3FE2 /* OrderedDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56043295228F00A9009D3FE2 /* OrderedDictionaryTests.swift */; }; 560432A6228F167A009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560432A5228F167A009D3FE2 /* AssociationPrefetchingObservationTests.swift */; }; 5604484E25DEEF7C002BAA79 /* AssociationPrefetchingRelationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5604484C25DEEF7C002BAA79 /* AssociationPrefetchingRelationTests.swift */; }; @@ -33,6 +37,9 @@ 561CFA7D2373503D000C8BAA /* TableRecordUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA7B2373503D000C8BAA /* TableRecordUpdateTests.swift */; }; 561CFAA12376EF4F000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA9F2376EF4F000C8BAA /* AssociationHasManyOrderingTests.swift */; }; 561CFAA42376EF59000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFAA32376EF59000C8BAA /* AssociationHasManyThroughOrderingTests.swift */; }; + 561F38DB2AC8914D0051EEE9 /* JSONColumnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38DA2AC8914D0051EEE9 /* JSONColumnTests.swift */; }; + 561F38F22AC9CE220051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F02AC9CE220051EEE9 /* DatabaseDataEncodingStrategyTests.swift */; }; + 561F38F62AC9CE5A0051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F52AC9CE5A0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */; }; 562205FA1E420E49005860AC /* DatabasePoolReleaseMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363CF1C943D13000BE133 /* DatabasePoolReleaseMemoryTests.swift */; }; 562205FB1E420E49005860AC /* DatabasePoolSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */; }; 562205FC1E420E49005860AC /* DatabaseQueueReleaseMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */; }; @@ -434,6 +441,10 @@ 56012B80257404A300B4925B /* CommonTableExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonTableExpression.swift; sourceTree = ""; }; 560233CC2724339A00529DF3 /* SharedValueObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedValueObservationTests.swift; sourceTree = ""; }; 560233D027243A9100529DF3 /* SharedValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedValueObservation.swift; sourceTree = ""; }; + 5603CEBF2AC862F800CF097D /* SQLJSONFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLJSONFunctions.swift; sourceTree = ""; }; + 5603CEC02AC862F800CF097D /* SQLJSONExpressible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLJSONExpressible.swift; sourceTree = ""; }; + 5603CEC12AC862F800CF097D /* JSONColumn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONColumn.swift; sourceTree = ""; }; + 5603CECC2AC8633B00CF097D /* JSONExpressionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONExpressionsTests.swift; sourceTree = ""; }; 56043295228F00A9009D3FE2 /* OrderedDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionaryTests.swift; sourceTree = ""; }; 560432A5228F167A009D3FE2 /* AssociationPrefetchingObservationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingObservationTests.swift; sourceTree = ""; }; 5604484C25DEEF7C002BAA79 /* AssociationPrefetchingRelationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRelationTests.swift; sourceTree = ""; }; @@ -470,6 +481,9 @@ 561CFA7B2373503D000C8BAA /* TableRecordUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecordUpdateTests.swift; sourceTree = ""; }; 561CFA9F2376EF4F000C8BAA /* AssociationHasManyOrderingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyOrderingTests.swift; sourceTree = ""; }; 561CFAA32376EF59000C8BAA /* AssociationHasManyThroughOrderingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyThroughOrderingTests.swift; sourceTree = ""; }; + 561F38DA2AC8914D0051EEE9 /* JSONColumnTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONColumnTests.swift; sourceTree = ""; }; + 561F38F02AC9CE220051EEE9 /* DatabaseDataEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataEncodingStrategyTests.swift; sourceTree = ""; }; + 561F38F52AC9CE5A0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataDecodingStrategyTests.swift; sourceTree = ""; }; 56231E6025CEBF06001DFD2F /* RowDecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowDecodingError.swift; sourceTree = ""; }; 562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = ""; }; 5623932F1DEDFC5700A6B01F /* AnyCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCursorTests.swift; sourceTree = ""; }; @@ -855,6 +869,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5603CEBE2AC862F800CF097D /* JSON */ = { + isa = PBXGroup; + children = ( + 5603CEC12AC862F800CF097D /* JSONColumn.swift */, + 5603CEC02AC862F800CF097D /* SQLJSONExpressible.swift */, + 5603CEBF2AC862F800CF097D /* SQLJSONFunctions.swift */, + ); + path = JSON; + sourceTree = ""; + }; + 5603CECB2AC8632A00CF097D /* JSON */ = { + isa = PBXGroup; + children = ( + 561F38DA2AC8914D0051EEE9 /* JSONColumnTests.swift */, + 5603CECC2AC8633B00CF097D /* JSONExpressionsTests.swift */, + ); + name = JSON; + sourceTree = ""; + }; 5605F1471C672E4000235C62 /* Support */ = { isa = PBXGroup; children = ( @@ -933,6 +966,7 @@ 560B3FA41C19DFF800C58EC7 /* PersistableRecord */ = { isa = PBXGroup; children = ( + 561F38F02AC9CE220051EEE9 /* DatabaseDataEncodingStrategyTests.swift */, 5665FA3B2129EED8004D8612 /* DatabaseDateEncodingStrategyTests.swift */, 56703299212B5461007D270F /* DatabaseUUIDEncodingStrategyTests.swift */, 566A84422041AB2D00E50BFD /* MutablePersistableRecordChangesTests.swift */, @@ -967,12 +1001,13 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( + 567E4207242AB3CB00CAAD2C /* FailureTestCase.swift */, + 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, - 567E4207242AB3CB00CAAD2C /* FailureTestCase.swift */, 5698AC3E1DA2BEBB0056AF8C /* FTS */, 56176CA01EACEE2A000F3F2B /* GRDBCipher */, - 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, + 5603CECB2AC8632A00CF097D /* JSON */, 56A238231B9C74A90082EB20 /* Migrations */, 569978D31B539038005EBEED /* Private */, 56300B5C1C53C38F005A543B /* QueryInterface */, @@ -1296,6 +1331,7 @@ 5674A7251F30A8EF0095F066 /* FetchableRecord */ = { isa = PBXGroup; children = ( + 561F38F52AC9CE5A0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */, 5665FA1C2129D807004D8612 /* DatabaseDateDecodingStrategyTests.swift */, 5674A7261F30A9090095F066 /* FetchableRecordDecodableTests.swift */, 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */, @@ -1647,6 +1683,7 @@ 566DDE11288D76400000DCFB /* Fixits.swift */, 56A2386F1B9C75030082EB20 /* Core */, 5698AC291D9E5A480056AF8C /* FTS */, + 5603CEBE2AC862F800CF097D /* JSON */, 56A238911B9C750B0082EB20 /* Migration */, 5656A8252295BD56001FF3FF /* QueryInterface */, 56A2389F1B9C753B0082EB20 /* Record */, @@ -1984,6 +2021,7 @@ F3BA807F1CFB2E61003DC1BA /* NSString.swift in Sources */, 56F89DFA2A57EAB9002FE2AA /* ColumnDefinition.swift in Sources */, 56F89E1C2A585E0D002FE2AA /* SQLTableGenerator.swift in Sources */, + 5603CEC52AC862F800CF097D /* SQLJSONExpressible.swift in Sources */, 560233D127243A9200529DF3 /* SharedValueObservation.swift in Sources */, 5656A8592295BD56001FF3FF /* SQLGenerationContext.swift in Sources */, 5690C3421D23E82A00E59934 /* Data.swift in Sources */, @@ -2012,9 +2050,11 @@ 5698AD231DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 5656A85B2295BD56001FF3FF /* TableDefinition.swift in Sources */, 5656A8552295BD56001FF3FF /* FTS5+QueryInterface.swift in Sources */, + 5603CEC62AC862F800CF097D /* JSONColumn.swift in Sources */, 5656A88F2295BD56001FF3FF /* Column.swift in Sources */, 564CE5B721B8FBEB00652B19 /* DatabaseRegionObservation.swift in Sources */, 5656A8612295BD56001FF3FF /* TableRecord+Association.swift in Sources */, + 5603CEC42AC862F800CF097D /* SQLJSONFunctions.swift in Sources */, F3BA808C1CFB2E75003DC1BA /* DatabaseMigrator.swift in Sources */, 563CBBE42A595141008905CE /* SQLIndexGenerator.swift in Sources */, 5613ED6121A95E6100DC7A68 /* ValueObservation.swift in Sources */, @@ -2042,6 +2082,7 @@ F3BA80CC1CFB2FD8003DC1BA /* DatabaseQueueTests.swift in Sources */, F3BA81101CFB3057003DC1BA /* Row+FoundationTests.swift in Sources */, F3BA812C1CFB3064003DC1BA /* RecordMinimalPrimaryKeyRowIDTests.swift in Sources */, + 561F38DB2AC8914D0051EEE9 /* JSONColumnTests.swift in Sources */, 5698AC991DA4B0430056AF8C /* FTS4RecordTests.swift in Sources */, 562EA8321F17B9EB00FA528C /* CompilationSubClassTests.swift in Sources */, 5653EB6C20961FB200F46237 /* AssociationParallelDecodableRecordTests.swift in Sources */, @@ -2083,6 +2124,7 @@ F3BA81301CFB3064003DC1BA /* RecordPrimaryKeyRowIDTests.swift in Sources */, 56DAA2D51DE99DAB006E10C8 /* DatabaseCursorTests.swift in Sources */, 5653EB7A20961FB200F46237 /* AssociationParallelRowScopesTests.swift in Sources */, + 561F38F22AC9CE220051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */, 563C67B824628C0C00E94EDC /* DatabasePoolTests.swift in Sources */, F3BA80FB1CFB3021003DC1BA /* StatementArgumentsTests.swift in Sources */, F3BA80EE1CFB3017003DC1BA /* RowAdapterTests.swift in Sources */, @@ -2210,6 +2252,7 @@ 5698AC431DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */, 563B533B267E2FA4009549B5 /* TableTests.swift in Sources */, 5653EB6E20961FB200F46237 /* AssociationBelongsToSQLDerivationTests.swift in Sources */, + 561F38F62AC9CE5A0051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */, 564CE5C621B8FFE600652B19 /* DatabaseRegionObservationTests.swift in Sources */, F3BA80E11CFB300F003DC1BA /* DatabaseValueConversionTests.swift in Sources */, 5623931B1DECC02000A6B01F /* RowFetchTests.swift in Sources */, @@ -2253,6 +2296,7 @@ F3BA812D1CFB3064003DC1BA /* RecordMinimalPrimaryKeySingleTests.swift in Sources */, 5698AC831DA380A20056AF8C /* VirtualTableModuleTests.swift in Sources */, F3BA811B1CFB305F003DC1BA /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, + 5603CED52AC8643800CF097D /* JSONExpressionsTests.swift in Sources */, F3BA811A1CFB305F003DC1BA /* Record+QueryInterfaceRequestTests.swift in Sources */, F3BA81111CFB3057003DC1BA /* StatementArguments+FoundationTests.swift in Sources */, F3BA81371CFB3064003DC1BA /* RecordInitializersTests.swift in Sources */, diff --git a/README.md b/README.md index c31d947411..53447ca803 100644 --- a/README.md +++ b/README.md @@ -2571,7 +2571,7 @@ For more information about Codable records, see: - [JSON Columns] - [Column Names Coding Strategies] -- [Date and UUID Coding Strategies] +- [Data, Date, and UUID Coding Strategies] - [The userInfo Dictionary] - [Tip: Derive Columns from Coding Keys](#tip-derive-columns-from-coding-keys) @@ -2646,9 +2646,9 @@ protocol EncodableRecord { See [DatabaseColumnDecodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasecolumndecodingstrategy) and [DatabaseColumnEncodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasecolumnencodingstrategy/) to learn about all available strategies. -### Date and UUID Coding Strategies +### Data, Date, and UUID Coding Strategies -By default, [Codable Records] encode and decode their Date and UUID properties as described in the general [Date and DateComponents](#date-and-datecomponents) and [UUID](#uuid) chapters. +By default, [Codable Records] encode and decode their Data properties as blobs, and Date and UUID properties as described in the general [Date and DateComponents](#date-and-datecomponents) and [UUID](#uuid) chapters. To sum up: dates encode themselves in the "YYYY-MM-DD HH:MM:SS.SSS" format, in the UTC time zone, and decode a variety of date formats and timestamps. UUIDs encode themselves as 16-bytes data blobs, and decode both 16-bytes data blobs and strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F". @@ -2656,27 +2656,29 @@ Those behaviors can be overridden: ```swift protocol FetchableRecord { + static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { get } static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get } } protocol EncodableRecord { + static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get } static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get } static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } } ``` -See [DatabaseDateDecodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasedatedecodingstrategy/), [DatabaseDateEncodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasedateencodingstrategy/), and [DatabaseUUIDEncodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseuuidencodingstrategy/) to learn about all available strategies. +See [DatabaseDataDecodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasedatadecodingstrategy/), [DatabaseDateDecodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasedatedecodingstrategy/), [DatabaseDataEncodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasedataencodingstrategy/), [DatabaseDateEncodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasedateencodingstrategy/), and [DatabaseUUIDEncodingStrategy](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseuuidencodingstrategy/) to learn about all available strategies. There is no customization of uuid decoding, because UUID can already decode all its encoded variants (16-bytes blobs and uuid strings, both uppercase and lowercase). -Customized date and uuid handling apply: +Customized coding strategies apply: - When encoding and decoding database rows to and from records (fetching and persistence methods). - In requests by single-column primary key: `fetchOne(_:id:)`, `filter(id:)`, `deleteAll(_:keys:)`, etc. -*They do not apply* in other requests based on date or uuid values. +*They do not apply* in other requests based on data, date, or uuid values. -So make sure that dates and uuids are properly encoded in your requests. For example: +So make sure that those are properly encoded in your requests. For example: ```swift struct Player: Codable, FetchableRecord, PersistableRecord, Identifiable { @@ -3449,11 +3451,11 @@ This is the list of record methods, along with their required protocols. The [Re | **[Codable Records]** | | | | `Type.databaseDecodingUserInfo` | [FetchableRecord] | [*](#the-userinfo-dictionary) | | `Type.databaseJSONDecoder(for:)` | [FetchableRecord] | [*](#json-columns) | -| `Type.databaseDateDecodingStrategy` | [FetchableRecord] | [*](#date-and-uuid-coding-strategies) | +| `Type.databaseDateDecodingStrategy` | [FetchableRecord] | [*](#data-date-and-uuid-coding-strategies) | | `Type.databaseEncodingUserInfo` | [EncodableRecord] | [*](#the-userinfo-dictionary) | | `Type.databaseJSONEncoder(for:)` | [EncodableRecord] | [*](#json-columns) | -| `Type.databaseDateEncodingStrategy` | [EncodableRecord] | [*](#date-and-uuid-coding-strategies) | -| `Type.databaseUUIDEncodingStrategy` | [EncodableRecord] | [*](#date-and-uuid-coding-strategies) | +| `Type.databaseDateEncodingStrategy` | [EncodableRecord] | [*](#data-date-and-uuid-coding-strategies) | +| `Type.databaseUUIDEncodingStrategy` | [EncodableRecord] | [*](#data-date-and-uuid-coding-strategies) | | **Define [Associations]** | | | | `Type.belongsTo(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | | `Type.hasMany(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | @@ -6213,6 +6215,10 @@ This chapter has [moved](https://swiftpackageindex.com/groue/grdb.swift/document This chapter was removed. See the references of [DatabaseReader](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasereader) and [DatabaseWriter](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasewriter). +#### Date and UUID Coding Strategies + +This chapter has been renamed [Data, Date, and UUID Coding Strategies]. + #### Dealing with External Connections This chapter has been superseded by the [Sharing a Database] guide. @@ -6308,7 +6314,7 @@ This chapter has been superseded by [ValueObservation] and [DatabaseRegionObserv [Common Table Expressions]: Documentation/CommonTableExpressions.md [Conflict Resolution]: #conflict-resolution [Column Names Coding Strategies]: #column-names-coding-strategies -[Date and UUID Coding Strategies]: #date-and-uuid-coding-strategies +[Data, Date, and UUID Coding Strategies]: #data-date-and-uuid-coding-strategies [Fetching from Requests]: #fetching-from-requests [Embedding SQL in Query Interface Requests]: #embedding-sql-in-query-interface-requests [Full-Text Search]: Documentation/FullTextSearch.md diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index bc0906d61a..25b79c854c 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -3,10 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 5603CECF2AC8636E00CF097D /* JSONExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CECE2AC8636E00CF097D /* JSONExpressionsTests.swift */; }; + 5603CED02AC8636E00CF097D /* JSONExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603CECE2AC8636E00CF097D /* JSONExpressionsTests.swift */; }; + 561F38DD2AC891710051EEE9 /* JSONColumnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38DC2AC891710051EEE9 /* JSONColumnTests.swift */; }; + 561F38DE2AC891710051EEE9 /* JSONColumnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38DC2AC891710051EEE9 /* JSONColumnTests.swift */; }; + 561F38F92AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F72AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */; }; + 561F38FA2AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F72AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */; }; + 561F38FB2AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F82AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift */; }; + 561F38FC2AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38F82AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift */; }; 56419D6724A54062004967E1 /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */; }; 56419D6824A54062004967E1 /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */; }; 56419D6924A54062004967E1 /* ResultCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9D24A54053004967E1 /* ResultCodeTests.swift */; }; @@ -459,6 +467,10 @@ /* Begin PBXFileReference section */ 04298D834C818285823558AB /* Pods-GRDBTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTests.release.xcconfig"; path = "Target Support Files/Pods-GRDBTests/Pods-GRDBTests.release.xcconfig"; sourceTree = ""; }; 47C5D1B9AFFE795AA1D6EA5D /* Pods-GRDBTestsEncrypted.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTestsEncrypted.release.xcconfig"; path = "Target Support Files/Pods-GRDBTestsEncrypted/Pods-GRDBTestsEncrypted.release.xcconfig"; sourceTree = ""; }; + 5603CECE2AC8636E00CF097D /* JSONExpressionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONExpressionsTests.swift; sourceTree = ""; }; + 561F38DC2AC891710051EEE9 /* JSONColumnTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONColumnTests.swift; sourceTree = ""; }; + 561F38F72AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataDecodingStrategyTests.swift; sourceTree = ""; }; + 561F38F82AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataEncodingStrategyTests.swift; sourceTree = ""; }; 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 56419C9D24A54053004967E1 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = ""; }; 56419C9E24A54053004967E1 /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; @@ -827,6 +839,8 @@ 56419CE824A54059004967E1 /* DatabaseCollationTests.swift */, 56419D1124A5405C004967E1 /* DatabaseConfigurationTests.swift */, 56419CA424A54054004967E1 /* DatabaseCursorTests.swift */, + 561F38F72AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift */, + 561F38F82AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift */, 56419CCE24A54057004967E1 /* DatabaseDateDecodingStrategyTests.swift */, 56419CE424A54058004967E1 /* DatabaseDateEncodingStrategyTests.swift */, 56419CAF24A54054004967E1 /* DatabaseErrorTests.swift */, @@ -911,6 +925,8 @@ 56419CDE24A54058004967E1 /* IndexInfoTests.swift */, 56419CF024A54059004967E1 /* InflectionsTests.swift */, 56419D0024A5405A004967E1 /* JoinSupportTests.swift */, + 561F38DC2AC891710051EEE9 /* JSONColumnTests.swift */, + 5603CECE2AC8636E00CF097D /* JSONExpressionsTests.swift */, 56419CDC24A54058004967E1 /* MapCursorTests.swift */, 56419D0224A5405A004967E1 /* MutablePersistableRecordChangesTests.swift */, 56419D2F24A5405E004967E1 /* MutablePersistableRecordEncodableTests.swift */, @@ -1226,6 +1242,7 @@ 56419DF324A54062004967E1 /* AssociationBelongsToSQLDerivationTests.swift in Sources */, 56419E6724A54062004967E1 /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */, 56419E4D24A54062004967E1 /* FoundationNSDecimalNumberTests.swift in Sources */, + 561F38F92AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */, 56419E6B24A54062004967E1 /* ValueObservationFetchTests.swift in Sources */, 56419D8524A54062004967E1 /* FTS4RecordTests.swift in Sources */, 5641A1B224A540D6004967E1 /* Next.swift in Sources */, @@ -1257,6 +1274,7 @@ 56419ED924A54063004967E1 /* AssociationHasOneSQLTests.swift in Sources */, 56419EAB24A54063004967E1 /* DatabaseValueTests.swift in Sources */, 56419D7724A54062004967E1 /* DatabaseCursorTests.swift in Sources */, + 5603CECF2AC8636E00CF097D /* JSONExpressionsTests.swift in Sources */, 56419E1524A54062004967E1 /* AssociationChainRowScopesTests.swift in Sources */, 56F61DF0283D484700AF9884 /* getThreadsCount.c in Sources */, 56419EC724A54063004967E1 /* RecordPrimaryKeyMultipleTests.swift in Sources */, @@ -1267,6 +1285,7 @@ 56419DA524A54062004967E1 /* QueryInterfacePromiseTests.swift in Sources */, 56419EE324A54063004967E1 /* Row+FoundationTests.swift in Sources */, 56419E7124A54062004967E1 /* DatabaseTests.swift in Sources */, + 561F38DD2AC891710051EEE9 /* JSONColumnTests.swift in Sources */, 56419E9524A54063004967E1 /* PrefixWhileCursorTests.swift in Sources */, 56419E2F24A54062004967E1 /* JoinSupportTests.swift in Sources */, 56419E0524A54062004967E1 /* AssociationParallelSQLTests.swift in Sources */, @@ -1383,6 +1402,7 @@ 56419EA924A54063004967E1 /* DatabasePoolReleaseMemoryTests.swift in Sources */, 56419E7F24A54063004967E1 /* DatabaseQueueConcurrencyTests.swift in Sources */, 56419EF124A54063004967E1 /* StatementColumnConvertibleFetchTests.swift in Sources */, + 561F38FB2AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */, 56419EA324A54063004967E1 /* TableRecordDeleteTests.swift in Sources */, 56419EFB24A54063004967E1 /* FoundationDateComponentsTests.swift in Sources */, 56419E3D24A54062004967E1 /* FTS3TableBuilderTests.swift in Sources */, @@ -1452,6 +1472,7 @@ 56419DF424A54062004967E1 /* AssociationBelongsToSQLDerivationTests.swift in Sources */, 56419E6824A54062004967E1 /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */, 56419E4E24A54062004967E1 /* FoundationNSDecimalNumberTests.swift in Sources */, + 561F38FA2AC9CE6D0051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */, 56419E6C24A54062004967E1 /* ValueObservationFetchTests.swift in Sources */, 56419D8624A54062004967E1 /* FTS4RecordTests.swift in Sources */, 5641A1B324A540D6004967E1 /* Next.swift in Sources */, @@ -1483,6 +1504,7 @@ 56419EDA24A54063004967E1 /* AssociationHasOneSQLTests.swift in Sources */, 56419EAC24A54063004967E1 /* DatabaseValueTests.swift in Sources */, 56419D7824A54062004967E1 /* DatabaseCursorTests.swift in Sources */, + 5603CED02AC8636E00CF097D /* JSONExpressionsTests.swift in Sources */, 56419E1624A54062004967E1 /* AssociationChainRowScopesTests.swift in Sources */, 56F61DF1283D484700AF9884 /* getThreadsCount.c in Sources */, 56419EC824A54063004967E1 /* RecordPrimaryKeyMultipleTests.swift in Sources */, @@ -1493,6 +1515,7 @@ 56419DA624A54062004967E1 /* QueryInterfacePromiseTests.swift in Sources */, 56419EE424A54063004967E1 /* Row+FoundationTests.swift in Sources */, 56419E7224A54062004967E1 /* DatabaseTests.swift in Sources */, + 561F38DE2AC891710051EEE9 /* JSONColumnTests.swift in Sources */, 56419E9624A54063004967E1 /* PrefixWhileCursorTests.swift in Sources */, 56419E3024A54062004967E1 /* JoinSupportTests.swift in Sources */, 56419E0624A54062004967E1 /* AssociationParallelSQLTests.swift in Sources */, @@ -1609,6 +1632,7 @@ 56419EAA24A54063004967E1 /* DatabasePoolReleaseMemoryTests.swift in Sources */, 56419E8024A54063004967E1 /* DatabaseQueueConcurrencyTests.swift in Sources */, 56419EF224A54063004967E1 /* StatementColumnConvertibleFetchTests.swift in Sources */, + 561F38FC2AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */, 56419EA424A54063004967E1 /* TableRecordDeleteTests.swift in Sources */, 56419EFC24A54063004967E1 /* FoundationDateComponentsTests.swift in Sources */, 56419E3E24A54062004967E1 /* FTS3TableBuilderTests.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index 46926ec6dc..1a667d385c 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -3,10 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 561F38E12AC891890051EEE9 /* JSONExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38DF2AC891890051EEE9 /* JSONExpressionsTests.swift */; }; + 561F38E22AC891890051EEE9 /* JSONExpressionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38DF2AC891890051EEE9 /* JSONExpressionsTests.swift */; }; + 561F38E32AC891890051EEE9 /* JSONColumnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38E02AC891890051EEE9 /* JSONColumnTests.swift */; }; + 561F38E42AC891890051EEE9 /* JSONColumnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38E02AC891890051EEE9 /* JSONColumnTests.swift */; }; + 561F38FF2AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38FD2AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift */; }; + 561F39002AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38FD2AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift */; }; + 561F39012AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38FE2AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift */; }; + 561F39022AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561F38FE2AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift */; }; 56419FC824A540A1004967E1 /* FetchRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFD24A54093004967E1 /* FetchRequestTests.swift */; }; 56419FC924A540A1004967E1 /* FetchRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFD24A54093004967E1 /* FetchRequestTests.swift */; }; 56419FCA24A540A1004967E1 /* DatabasePoolBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFE24A54093004967E1 /* DatabasePoolBackupTests.swift */; }; @@ -475,6 +483,10 @@ /* Begin PBXFileReference section */ 04298D834C818285823558AB /* Pods-GRDBTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTests.release.xcconfig"; path = "Target Support Files/Pods-GRDBTests/Pods-GRDBTests.release.xcconfig"; sourceTree = ""; }; 47C5D1B9AFFE795AA1D6EA5D /* Pods-GRDBTestsEncrypted.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTestsEncrypted.release.xcconfig"; path = "Target Support Files/Pods-GRDBTestsEncrypted/Pods-GRDBTestsEncrypted.release.xcconfig"; sourceTree = ""; }; + 561F38DF2AC891890051EEE9 /* JSONExpressionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONExpressionsTests.swift; sourceTree = ""; }; + 561F38E02AC891890051EEE9 /* JSONColumnTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONColumnTests.swift; sourceTree = ""; }; + 561F38FD2AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataEncodingStrategyTests.swift; sourceTree = ""; }; + 561F38FE2AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataDecodingStrategyTests.swift; sourceTree = ""; }; 56419EFD24A54093004967E1 /* FetchRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequestTests.swift; sourceTree = ""; }; 56419EFE24A54093004967E1 /* DatabasePoolBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolBackupTests.swift; sourceTree = ""; }; 56419EFF24A54093004967E1 /* TableRecordDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecordDeleteTests.swift; sourceTree = ""; }; @@ -856,6 +868,8 @@ 568C3F852A5AB3A800A2309D /* DatabaseColumnEncodingStrategyTests.swift */, 56419F7024A5409A004967E1 /* DatabaseConfigurationTests.swift */, 56419F2324A54095004967E1 /* DatabaseCursorTests.swift */, + 561F38FE2AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift */, + 561F38FD2AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift */, 56419FBE24A540A0004967E1 /* DatabaseDateDecodingStrategyTests.swift */, 56419F2824A54095004967E1 /* DatabaseDateEncodingStrategyTests.swift */, 56419F5824A54098004967E1 /* DatabaseErrorTests.swift */, @@ -942,6 +956,8 @@ 56419FA824A5409E004967E1 /* IndexInfoTests.swift */, 56419F3824A54096004967E1 /* InflectionsTests.swift */, 56419F6D24A5409A004967E1 /* JoinSupportTests.swift */, + 561F38E02AC891890051EEE9 /* JSONColumnTests.swift */, + 561F38DF2AC891890051EEE9 /* JSONExpressionsTests.swift */, 56419F7B24A5409B004967E1 /* MapCursorTests.swift */, 56419F4524A54097004967E1 /* MutablePersistableRecordChangesTests.swift */, 56419F0224A54093004967E1 /* MutablePersistableRecordEncodableTests.swift */, @@ -1345,6 +1361,7 @@ 5641A08224A540A1004967E1 /* AssociationAggregateTests.swift in Sources */, 5641A12424A540A1004967E1 /* DatabaseTraceTests.swift in Sources */, 56419FE224A540A1004967E1 /* AssociationBelongsToRowScopeTests.swift in Sources */, + 561F38E12AC891890051EEE9 /* JSONExpressionsTests.swift in Sources */, 5641A07624A540A1004967E1 /* CompilationProtocolTests.swift in Sources */, 5641A14624A540A2004967E1 /* DatabaseValueConvertibleFetchTests.swift in Sources */, 5641A03C24A540A1004967E1 /* AssociationBelongsToSQLTests.swift in Sources */, @@ -1355,6 +1372,7 @@ 5641A0F424A540A1004967E1 /* ColumnInfoTests.swift in Sources */, 5641A07A24A540A1004967E1 /* QueryInterfacePromiseTests.swift in Sources */, 5641A0E824A540A1004967E1 /* AssociationParallelSQLTests.swift in Sources */, + 561F39012AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */, 56419FE024A540A1004967E1 /* AssociationChainSQLTests.swift in Sources */, 5641A03E24A540A1004967E1 /* InflectionsTests.swift in Sources */, 5641A06224A540A1004967E1 /* DropWhileCursorTests.swift in Sources */, @@ -1398,6 +1416,7 @@ 56419FE624A540A1004967E1 /* RecordMinimalPrimaryKeyRowIDTests.swift in Sources */, 5641A04424A540A1004967E1 /* FTS3TokenizerTests.swift in Sources */, 5641A14024A540A2004967E1 /* RecordPrimaryKeyRowIDTests.swift in Sources */, + 561F38FF2AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */, 5641A08A24A540A1004967E1 /* EncryptionTests.swift in Sources */, 5641A11E24A540A1004967E1 /* IndexInfoTests.swift in Sources */, 56419FEC24A540A1004967E1 /* FTS4TableBuilderTests.swift in Sources */, @@ -1432,6 +1451,7 @@ 5641A05624A540A1004967E1 /* AssociationPrefetchingSQLTests.swift in Sources */, 5641A12224A540A1004967E1 /* FTS5PatternTests.swift in Sources */, 5641A0F624A540A1004967E1 /* FTS5WrapperTokenizerTests.swift in Sources */, + 561F38E32AC891890051EEE9 /* JSONColumnTests.swift in Sources */, 5641A15C24A540A2004967E1 /* AssociationHasOneSQLDerivationTests.swift in Sources */, 5641A03024A540A1004967E1 /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */, 5641A00224A540A1004967E1 /* PersistableRecordTests.swift in Sources */, @@ -1578,6 +1598,7 @@ 5641A08324A540A1004967E1 /* AssociationAggregateTests.swift in Sources */, 5641A12524A540A1004967E1 /* DatabaseTraceTests.swift in Sources */, 56419FE324A540A1004967E1 /* AssociationBelongsToRowScopeTests.swift in Sources */, + 561F38E22AC891890051EEE9 /* JSONExpressionsTests.swift in Sources */, 5641A07724A540A1004967E1 /* CompilationProtocolTests.swift in Sources */, 5641A14724A540A2004967E1 /* DatabaseValueConvertibleFetchTests.swift in Sources */, 5641A03D24A540A1004967E1 /* AssociationBelongsToSQLTests.swift in Sources */, @@ -1588,6 +1609,7 @@ 5641A0F524A540A1004967E1 /* ColumnInfoTests.swift in Sources */, 5641A07B24A540A1004967E1 /* QueryInterfacePromiseTests.swift in Sources */, 5641A0E924A540A1004967E1 /* AssociationParallelSQLTests.swift in Sources */, + 561F39022AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift in Sources */, 56419FE124A540A1004967E1 /* AssociationChainSQLTests.swift in Sources */, 5641A03F24A540A1004967E1 /* InflectionsTests.swift in Sources */, 5641A06324A540A1004967E1 /* DropWhileCursorTests.swift in Sources */, @@ -1631,6 +1653,7 @@ 56419FE724A540A1004967E1 /* RecordMinimalPrimaryKeyRowIDTests.swift in Sources */, 5641A04524A540A1004967E1 /* FTS3TokenizerTests.swift in Sources */, 5641A14124A540A2004967E1 /* RecordPrimaryKeyRowIDTests.swift in Sources */, + 561F39002AC9CE870051EEE9 /* DatabaseDataEncodingStrategyTests.swift in Sources */, 5641A08B24A540A1004967E1 /* EncryptionTests.swift in Sources */, 5641A11F24A540A1004967E1 /* IndexInfoTests.swift in Sources */, 56419FED24A540A1004967E1 /* FTS4TableBuilderTests.swift in Sources */, @@ -1665,6 +1688,7 @@ 5641A05724A540A1004967E1 /* AssociationPrefetchingSQLTests.swift in Sources */, 5641A12324A540A1004967E1 /* FTS5PatternTests.swift in Sources */, 5641A0F724A540A1004967E1 /* FTS5WrapperTokenizerTests.swift in Sources */, + 561F38E42AC891890051EEE9 /* JSONColumnTests.swift in Sources */, 5641A15D24A540A2004967E1 /* AssociationHasOneSQLDerivationTests.swift in Sources */, 5641A03124A540A1004967E1 /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */, 5641A00324A540A1004967E1 /* PersistableRecordTests.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift new file mode 100644 index 0000000000..8ff1da13b2 --- /dev/null +++ b/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift @@ -0,0 +1,139 @@ +import Foundation +import XCTest +@testable import GRDB // TODO: remove @testable when RowDecodingError is public + +private protocol StrategyProvider { + static var strategy: DatabaseDataDecodingStrategy { get } +} + +private enum StrategyDeferredToData: StrategyProvider { + static let strategy: DatabaseDataDecodingStrategy = .deferredToData +} + +private enum StrategyCustom: StrategyProvider { + static let strategy: DatabaseDataDecodingStrategy = .custom { dbValue in + if dbValue == "invalid".databaseValue { + return nil + } + return "foo".data(using: .utf8)! + } +} + +private struct RecordWithData: FetchableRecord, Decodable { + static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { Strategy.strategy } + var data: Data +} + +private struct RecordWithOptionalData: FetchableRecord, Decodable { + static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { Strategy.strategy } + var data: Data? +} + +class DatabaseDataDecodingStrategyTests: GRDBTestCase { + /// test the conversion from a database value to a data extracted from a record + private func test( + _ db: Database, + record: T.Type, + data: (T) -> Data?, + databaseValue: (any DatabaseValueConvertible)?, + with test: (Data?) -> Void) throws + { + let request = SQLRequest(sql: "SELECT ? AS data", arguments: [databaseValue]) + do { + // test decoding straight from SQLite + let record = try T.fetchOne(db, request)! + test(data(record)) + } + do { + // test decoding from copied row + let record = try T(row: Row.fetchOne(db, request)!) + test(data(record)) + } + } + + /// test the conversion from a database value to a data with a given strategy + private func test( + _ db: Database, + strategy: Strategy.Type, + databaseValue: some DatabaseValueConvertible, + _ test: (Data) -> Void) + throws + { + try self.test(db, record: RecordWithData.self, data: { $0.data }, databaseValue: databaseValue, with: { test($0!) }) + try self.test(db, record: RecordWithOptionalData.self, data: { $0.data }, databaseValue: databaseValue, with: { test($0!) }) + } + + private func testNullDecoding(_ db: Database, strategy: Strategy.Type) throws { + try self.test(db, record: RecordWithOptionalData.self, data: { $0.data }, databaseValue: nil) { data in + XCTAssertNil(data) + } + } +} + +// MARK: - deferredToData + +extension DatabaseDataDecodingStrategyTests { + func testDeferredToData() throws { + try makeDatabaseQueue().read { db in + // Null + try testNullDecoding(db, strategy: StrategyDeferredToData.self) + + // Empty string + try test(db, strategy: StrategyDeferredToData.self, databaseValue: "") { data in + XCTAssertEqual(data, Data()) + } + + // String + try test(db, strategy: StrategyDeferredToData.self, databaseValue: "foo") { data in + XCTAssertEqual(data, "foo".data(using: .utf8)) + } + + // Empty blob + try test(db, strategy: StrategyDeferredToData.self, databaseValue: Data()) { data in + XCTAssertEqual(data, Data()) + } + + // Blob + try test(db, strategy: StrategyDeferredToData.self, databaseValue: "foo".data(using: .utf8)) { data in + XCTAssertEqual(data, "foo".data(using: .utf8)) + } + } + } +} + +// MARK: - custom((DatabaseValue) -> Data? + +extension DatabaseDataDecodingStrategyTests { + func testCustom() throws { + try makeDatabaseQueue().read { db in + // Null + try testNullDecoding(db, strategy: StrategyCustom.self) + + // Data + try test(db, strategy: StrategyCustom.self, databaseValue: "valid") { data in + XCTAssertEqual(data, "foo".data(using: .utf8)!) + } + + // error + do { + try test(db, strategy: StrategyCustom.self, databaseValue: "invalid") { data in + XCTFail("Unexpected Data") + } + } catch let error as RowDecodingError { + switch error { + case .valueMismatch: + XCTAssertEqual(error.description, """ + could not decode Data from database value "invalid" - \ + column: "data", \ + column index: 0, \ + row: [data:"invalid"], \ + sql: `SELECT ? AS data`, \ + arguments: ["invalid"] + """) + default: + XCTFail("Unexpected Error") + } + } + } + } +} diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift new file mode 100644 index 0000000000..6fdf7d8759 --- /dev/null +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -0,0 +1,309 @@ +import XCTest +import Foundation +@testable import GRDB + +private protocol StrategyProvider { + static var strategy: DatabaseDataEncodingStrategy { get } +} + +private enum StrategyDeferredToData: StrategyProvider { + static let strategy: DatabaseDataEncodingStrategy = .deferredToData +} + +private enum StrategyTextUTF8: StrategyProvider { + static let strategy: DatabaseDataEncodingStrategy = .text +} + +private enum StrategyCustom: StrategyProvider { + static let strategy: DatabaseDataEncodingStrategy = .custom { _ in "custom" } +} + +private struct RecordWithData: EncodableRecord, Encodable { + static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { Strategy.strategy } + var data: Data +} + +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +extension RecordWithData: Identifiable { + var id: Data { data } +} + +private struct RecordWithOptionalData: EncodableRecord, Encodable { + static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { Strategy.strategy } + var data: Data? +} + +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +extension RecordWithOptionalData: Identifiable { + var id: Data? { data } +} + +class DatabaseDataEncodingStrategyTests: GRDBTestCase { + let testedDatas = [ + "foo".data(using: .utf8)!, + Data(), + ] + + private func test( + record: T, + expectedStorage: DatabaseValue.Storage) + throws + { + var container = PersistenceContainer() + try record.encode(to: &container) + if let dbValue = container["data"]?.databaseValue { + XCTAssertEqual(dbValue.storage, expectedStorage) + } else { + XCTAssertEqual(.null, expectedStorage) + } + } + + private func test( + strategy: Strategy.Type, + encodesData data: Data, + as value: some DatabaseValueConvertible) + throws + { + try test(record: RecordWithData(data: data), expectedStorage: value.databaseValue.storage) + try test(record: RecordWithOptionalData(data: data), expectedStorage: value.databaseValue.storage) + } + + private func testNullEncoding(strategy: Strategy.Type) throws { + try test(record: RecordWithOptionalData(data: nil), expectedStorage: .null) + } +} + +// MARK: - deferredToData + +extension DatabaseDataEncodingStrategyTests { + func testDeferredToData() throws { + try testNullEncoding(strategy: StrategyDeferredToData.self) + + for (data, value) in zip(testedDatas, [ + "foo".data(using: .utf8)!, + Data(), + ]) { try test(strategy: StrategyDeferredToData.self, encodesData: data, as: value) } + } +} + +// MARK: - text(UTF8) + +extension DatabaseDataEncodingStrategyTests { + func testTextUTF8() throws { + try testNullEncoding(strategy: StrategyTextUTF8.self) + + for (data, value) in zip(testedDatas, [ + "foo", + "", + ]) { try test(strategy: StrategyTextUTF8.self, encodesData: data, as: value) } + } +} + +// MARK: - custom((Data) -> DatabaseValueConvertible?) + +extension DatabaseDataEncodingStrategyTests { + func testCustom() throws { + try testNullEncoding(strategy: StrategyCustom.self) + + for (data, value) in zip(testedDatas, [ + "custom", + "custom", + ]) { try test(strategy: StrategyCustom.self, encodesData: data, as: value) } + } +} + +// MARK: - Filter + +extension DatabaseDataEncodingStrategyTests { + func testFilterKey() throws { + try makeDatabaseQueue().write { db in + try db.create(table: "t") { $0.primaryKey("id", .blob) } + + do { + let request = Table>("t").filter(key: testedDatas[0]) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" = x'666f6f' + """) + } + + do { + let request = Table>("t").filter(keys: testedDatas) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" IN (x'666f6f', x'') + """) + } + + do { + let request = Table>("t").filter(key: testedDatas[0]) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" = 'foo' + """) + } + + do { + let request = Table>("t").filter(keys: testedDatas) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" IN ('foo', '') + """) + } + } + } + + func testFilterID() throws { + guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + throw XCTSkip("Identifiable not available") + } + + try makeDatabaseQueue().write { db in + try db.create(table: "t") { $0.primaryKey("id", .blob) } + + do { + let request = Table>("t").filter(id: testedDatas[0]) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" = x'666f6f' + """) + } + + do { + let request = Table>("t").filter(ids: testedDatas) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" IN (x'666f6f', x'') + """) + } + + do { + let request = Table>("t").filter(id: testedDatas[0]) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" = 'foo' + """) + } + + do { + let request = Table>("t").filter(ids: testedDatas) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" IN ('foo', '') + """) + } + + do { + let request = Table>("t").filter(id: nil) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE 0 + """) + } + + do { + let request = Table>("t").filter(id: testedDatas[0]) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" = x'666f6f' + """) + } + + do { + let request = Table>("t").filter(ids: testedDatas) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" IN (x'666f6f', x'') + """) + } + + do { + let request = Table>("t").filter(id: nil) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE 0 + """) + } + + do { + let request = Table>("t").filter(id: testedDatas[0]) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" = 'foo' + """) + } + + do { + let request = Table>("t").filter(ids: testedDatas) + try assertEqualSQL(db, request, """ + SELECT * FROM "t" WHERE "id" IN ('foo', '') + """) + } + } + } + + func testDeleteID() throws { + guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + throw XCTSkip("Identifiable not available") + } + + try makeDatabaseQueue().write { db in + try db.create(table: "t") { $0.primaryKey("id", .blob) } + + do { + try Table>("t").deleteOne(db, id: testedDatas[0]) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" = x'666f6f' + """) + } + + do { + try Table>("t").deleteAll(db, ids: testedDatas) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" IN (x'666f6f', x'') + """) + } + + do { + try Table>("t").deleteOne(db, id: testedDatas[0]) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" = 'foo' + """) + } + + do { + try Table>("t").deleteAll(db, ids: testedDatas) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" IN ('foo', '') + """) + } + + do { + sqlQueries.removeAll() + try Table>("t").deleteOne(db, id: nil) + XCTAssertNil(lastSQLQuery) // Database not hit + } + + do { + try Table>("t").deleteOne(db, id: testedDatas[0]) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" = x'666f6f' + """) + } + + do { + try Table>("t").deleteAll(db, ids: testedDatas) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" IN (x'666f6f', x'') + """) + } + + do { + sqlQueries.removeAll() + try Table>("t").deleteOne(db, id: nil) + XCTAssertNil(lastSQLQuery) // Database not hit + } + + do { + try Table>("t").deleteOne(db, id: testedDatas[0]) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" = 'foo' + """) + } + + do { + try Table>("t").deleteAll(db, ids: testedDatas) + XCTAssertEqual(lastSQLQuery, """ + DELETE FROM "t" WHERE "id" IN ('foo', '') + """) + } + } + } +} diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 7192b93204..9028d491f4 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -159,6 +159,19 @@ class GRDBTestCase: XCTestCase { assertEqualSQL(lastSQLQuery!, sql, file: file, line: line) } + // Compare SQL strings. + func assertEqualSQL( + _ db: Database, + _ expression: some SQLExpressible, + _ sql: String, + file: StaticString = #file, + line: UInt = #line) + throws + { + let request: SQLRequest = "SELECT \(expression)" + try assertEqualSQL(db, request, "SELECT \(sql)", file: file, line: line) + } + // Compare SQL strings (ignoring leading and trailing white space and semicolons. func assertEqualSQL( _ databaseReader: some DatabaseReader, @@ -172,7 +185,7 @@ class GRDBTestCase: XCTestCase { try assertEqualSQL(db, request, sql, file: file, line: line) } } - + func sql( _ databaseReader: some DatabaseReader, _ request: some FetchRequest) diff --git a/Tests/GRDBTests/JSONColumnTests.swift b/Tests/GRDBTests/JSONColumnTests.swift new file mode 100644 index 0000000000..1a78c9c96b --- /dev/null +++ b/Tests/GRDBTests/JSONColumnTests.swift @@ -0,0 +1,94 @@ +import XCTest +import GRDB + +final class JSONColumnTests: GRDBTestCase { + func test_JSONColumn_derived_from_CodingKey() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + struct Player: Codable, TableRecord, FetchableRecord, PersistableRecord { + var id: Int64 + var info: Data + + enum CodingKeys: String, CodingKey { + case id + case info = "info_json" + } + + enum Columns { + static let id = Column(CodingKeys.id) + static let info = JSONColumn(CodingKeys.info) + } + + static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.info] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + t.column("info_json", .jsonText) + } + + try assertEqualSQL(db, Player.all(), """ + SELECT "id", "info_json" FROM "player" + """) + } + } + + func test_extraction() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + t.column("info", .jsonText) + } + + let player = Table("player") + let info = JSONColumn("info") + + try assertEqualSQL(db, player.select(info["score"]), """ + SELECT "info" ->> 'score' FROM "player" + """) + + try assertEqualSQL(db, player.select(info["$.score"]), """ + SELECT "info" ->> '$.score' FROM "player" + """) + + try assertEqualSQL(db, player.select(info.jsonExtract(atPath: "$.score")), """ + SELECT JSON_EXTRACT("info", '$.score') FROM "player" + """) + + try assertEqualSQL(db, player.select(info.jsonExtract(atPaths: ["$.score", "$.bonus"])), """ + SELECT JSON_EXTRACT("info", '$.score', '$.bonus') FROM "player" + """) + + try assertEqualSQL(db, player.select(info.jsonRepresentation(atPath: "score")), """ + SELECT "info" -> 'score' FROM "player" + """) + + try assertEqualSQL(db, player.select(info.jsonRepresentation(atPath: "$.score")), """ + SELECT "info" -> '$.score' FROM "player" + """) + } + } +} diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift new file mode 100644 index 0000000000..46daefbc65 --- /dev/null +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -0,0 +1,1294 @@ +import XCTest +import GRDB + +final class JSONExpressionsTests: GRDBTestCase { + func test_Database_json() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.json(#" { "a": [ "test" ] } "#), """ + JSON(' { "a": [ "test" ] } ') + """) + + try assertEqualSQL(db, player.select(Database.json(nameColumn)), """ + SELECT JSON("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.json(infoColumn)), """ + SELECT JSON("info") FROM "player" + """) + } + } + + func test_asJSON() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, player.select([ + #"[1, 2, 3]"#.databaseValue.asJSON, + DatabaseValue.null.asJSON, + nameColumn.asJSON, + infoColumn.asJSON, + abs(nameColumn).asJSON, + abs(infoColumn).asJSON, + ]), """ + SELECT \ + '[1, 2, 3]', \ + NULL, \ + "name", \ + "info", \ + ABS("name"), \ + ABS("info") \ + FROM "player" + """) + + try assertEqualSQL(db, player.select([ + Database.jsonArray([ + #"[1, 2, 3]"#.databaseValue.asJSON, + DatabaseValue.null.asJSON, + nameColumn.asJSON, + infoColumn.asJSON, + abs(nameColumn).asJSON, + abs(infoColumn).asJSON, + ]) + ]), """ + SELECT JSON_ARRAY(\ + JSON('[1, 2, 3]'), \ + NULL, \ + JSON("name"), \ + JSON("info"), \ + JSON(ABS("name")), \ + JSON(ABS("info"))\ + ) FROM "player" + """) + } + } + + func test_Database_jsonArray() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonArray(1...4), """ + JSON_ARRAY(1, 2, 3, 4) + """) + + try assertEqualSQL(db, Database.jsonArray([1, 2, 3, 4]), """ + JSON_ARRAY(1, 2, 3, 4) + """) + + try assertEqualSQL(db, Database.jsonArray([1, 2, "3", 4]), """ + JSON_ARRAY(1, 2, '3', 4) + """) + + // Note: this JSON(JSON_EXTRACT(...)) is useful, when the extracted value is a string that contains JSON + try assertEqualSQL(db, player + .select( + Database.jsonArray([ + nameColumn, + nameColumn.asJSON, + infoColumn, + infoColumn["score"], + infoColumn["score"].asJSON, + infoColumn.jsonExtract(atPath: "address"), + infoColumn.jsonExtract(atPath: "address").asJSON, + infoColumn.jsonRepresentation(atPath: "address"), + infoColumn.jsonRepresentation(atPath: "address").asJSON, + ] as [any SQLExpressible]) + ), """ + SELECT JSON_ARRAY(\ + "name", \ + JSON("name"), \ + JSON("info"), \ + "info" ->> 'score', \ + JSON("info" ->> 'score'), \ + JSON_EXTRACT("info", 'address'), \ + JSON(JSON_EXTRACT("info", 'address')), \ + "info" -> 'address', \ + "info" -> 'address'\ + ) FROM "player" + """) + + let alias = TableAlias(name: "p") + + try assertEqualSQL(db, player + .aliased(alias) + .select( + alias[ + Database.jsonArray([ + nameColumn, + nameColumn.asJSON, + infoColumn, + infoColumn["score"], + infoColumn.jsonExtract(atPath: "address"), + infoColumn.jsonRepresentation(atPath: "address"), + ] as [any SQLExpressible]) + ] + ), """ + SELECT JSON_ARRAY(\ + "p"."name", \ + JSON("p"."name"), \ + JSON("p"."info"), \ + "p"."info" ->> 'score', \ + JSON_EXTRACT("p"."info", 'address'), \ + "p"."info" -> 'address'\ + ) FROM "player" "p" + """) + + try assertEqualSQL(db, player + .aliased(alias) + .select( + Database.jsonArray([ + alias[nameColumn], + alias[nameColumn.asJSON], + alias[infoColumn], + alias[infoColumn["score"]], + alias[infoColumn.jsonExtract(atPath: "address")], + alias[infoColumn.jsonRepresentation(atPath: "address")], + ] as [any SQLExpressible]) + ), """ + SELECT JSON_ARRAY(\ + "p"."name", \ + JSON("p"."name"), \ + JSON("p"."info"), \ + "p"."info" ->> 'score', \ + JSON_EXTRACT("p"."info", 'address'), \ + "p"."info" -> 'address'\ + ) FROM "player" "p" + """) + + try assertEqualSQL(db, player + .aliased(alias) + .select( + Database.jsonArray([ + alias[nameColumn], + alias[nameColumn].asJSON, + alias[infoColumn], + alias[infoColumn]["score"], + alias[infoColumn].jsonExtract(atPath: "address"), + alias[infoColumn].jsonRepresentation(atPath: "address"), + ] as [any SQLExpressible]) + ), """ + SELECT JSON_ARRAY(\ + "p"."name", \ + JSON("p"."name"), \ + JSON("p"."info"), \ + "p"."info" ->> 'score', \ + JSON_EXTRACT("p"."info", 'address'), \ + "p"."info" -> 'address'\ + ) FROM "player" "p" + """) + } + } + + func test_Database_jsonArrayLength() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonArrayLength("[1,2,3,4]"), """ + JSON_ARRAY_LENGTH('[1,2,3,4]') + """) + + try assertEqualSQL(db, player.select(Database.jsonArrayLength(nameColumn)), """ + SELECT JSON_ARRAY_LENGTH("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonArrayLength(infoColumn)), """ + SELECT JSON_ARRAY_LENGTH("info") FROM "player" + """) + } + } + + func test_Database_jsonArrayLength_atPath() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonArrayLength(#"{"one":[1,2,3]}"#, atPath: "$.one"), """ + JSON_ARRAY_LENGTH('{"one":[1,2,3]}', '$.one') + """) + + try assertEqualSQL(db, player.select(Database.jsonArrayLength(nameColumn, atPath: "$.a")), """ + SELECT JSON_ARRAY_LENGTH("name", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonArrayLength(#"{"one":[1,2,3]}"#, atPath: nameColumn)), """ + SELECT JSON_ARRAY_LENGTH('{"one":[1,2,3]}', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonArrayLength(infoColumn, atPath: "$.a")), """ + SELECT JSON_ARRAY_LENGTH("info", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonArrayLength(#"{"one":[1,2,3]}"#, atPath: infoColumn)), """ + SELECT JSON_ARRAY_LENGTH('{"one":[1,2,3]}', "info") FROM "player" + """) + } + } + + func test_Database_jsonErrorPosition() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3042000 else { + throw XCTSkip("JSON_ERROR_JSON is not available") + } +#else + guard #available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) else { + throw XCTSkip("JSON_ERROR_JSON is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonErrorPosition(#" { "a": [ "test" ] } "#), """ + JSON_ERROR_POSITION(' { "a": [ "test" ] } ') + """) + + try assertEqualSQL(db, player.select(Database.jsonErrorPosition(nameColumn)), """ + SELECT JSON_ERROR_POSITION("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonErrorPosition(infoColumn)), """ + SELECT JSON_ERROR_POSITION("info") FROM "player" + """) + } + } + + func test_Database_jsonExtract_atPath() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonExtract(#"{"a":123}"#, atPath: "$.a"), """ + JSON_EXTRACT('{"a":123}', '$.a') + """) + + try assertEqualSQL(db, player.select(Database.jsonExtract(nameColumn, atPath: "$.a")), """ + SELECT JSON_EXTRACT("name", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonExtract(infoColumn, atPath: "$.a")), """ + SELECT JSON_EXTRACT("info", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonExtract(#"{"a":123}"#, atPath: nameColumn)), """ + SELECT JSON_EXTRACT('{"a":123}', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonExtract(#"{"a":123}"#, atPath: infoColumn)), """ + SELECT JSON_EXTRACT('{"a":123}', "info") FROM "player" + """) + } + } + + func test_Database_jsonExtract_atPaths() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonExtract(#"{"a":2,"c":[4,5]}"#, atPaths: ["$.c", "$.a"]), """ + JSON_EXTRACT('{"a":2,"c":[4,5]}', '$.c', '$.a') + """) + + try assertEqualSQL(db, player.select(Database.jsonExtract(nameColumn, atPaths: ["$.c", "$.a"])), """ + SELECT JSON_EXTRACT("name", '$.c', '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonExtract(infoColumn, atPaths: ["$.c", "$.a"])), """ + SELECT JSON_EXTRACT("info", '$.c', '$.a') FROM "player" + """) + } + } + + func test_Database_jsonInsert() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonInsert("[1,2,3,4]", ["$[#]": #"{"e":5}"#]), """ + JSON_INSERT('[1,2,3,4]', '$[#]', '{"e":5}') + """) + + try assertEqualSQL(db, Database.jsonInsert("[1,2,3,4]", ["$[#]": #"{"e":5}"#.databaseValue.asJSON]), """ + JSON_INSERT('[1,2,3,4]', '$[#]', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonInsert("[1,2,3,4]", ["$[#]": Database.json(#"{"e":5}"#)]), """ + JSON_INSERT('[1,2,3,4]', '$[#]', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonInsert("[1,2,3,4]", ["$[#]": Database.jsonObject(["e": 5])]), """ + JSON_INSERT('[1,2,3,4]', '$[#]', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, player.select(Database.jsonInsert(nameColumn, ["$[#]": 99])), """ + SELECT JSON_INSERT("name", '$[#]', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonInsert(infoColumn, ["$[#]": 99])), """ + SELECT JSON_INSERT("info", '$[#]', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonInsert("[1,2,3,4]", ["$[#]": nameColumn])), """ + SELECT JSON_INSERT('[1,2,3,4]', '$[#]', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonInsert("[1,2,3,4]", ["$[#]": infoColumn])), """ + SELECT JSON_INSERT('[1,2,3,4]', '$[#]', JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonReplace() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#]), """ + JSON_REPLACE('{"a":2,"c":4}', '$.a', '{"e":5}') + """) + + try assertEqualSQL(db, Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#.databaseValue.asJSON]), """ + JSON_REPLACE('{"a":2,"c":4}', '$.a', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": Database.json(#"{"e":5}"#)]), """ + JSON_REPLACE('{"a":2,"c":4}', '$.a', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": Database.jsonObject(["e": 5])]), """ + JSON_REPLACE('{"a":2,"c":4}', '$.a', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, player.select(Database.jsonReplace(nameColumn, ["$.a": 99])), """ + SELECT JSON_REPLACE("name", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonReplace(infoColumn, ["$.a": 99])), """ + SELECT JSON_REPLACE("info", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": nameColumn])), """ + SELECT JSON_REPLACE('{"a":2,"c":4}', '$.a', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": infoColumn])), """ + SELECT JSON_REPLACE('{"a":2,"c":4}', '$.a', JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonSet() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#]), """ + JSON_SET('{"a":2,"c":4}', '$.a', '{"e":5}') + """) + + try assertEqualSQL(db, Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#.databaseValue.asJSON]), """ + JSON_SET('{"a":2,"c":4}', '$.a', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": Database.json(#"{"e":5}"#)]), """ + JSON_SET('{"a":2,"c":4}', '$.a', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": Database.jsonObject(["e": 5])]), """ + JSON_SET('{"a":2,"c":4}', '$.a', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, player.select(Database.jsonSet(nameColumn, ["$.a": 99])), """ + SELECT JSON_SET("name", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonSet(infoColumn, ["$.a": 99])), """ + SELECT JSON_SET("info", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": nameColumn])), """ + SELECT JSON_SET('{"a":2,"c":4}', '$.a', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonSet(#"{"a":2,"c":4}"#, ["$.a": infoColumn])), """ + SELECT JSON_SET('{"a":2,"c":4}', '$.a', JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonObject_from_Dictionary() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL( + db, + Database.jsonObject([ + "a": 2, + ] as [String: Int]), """ + JSON_OBJECT('a', 2) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "c": #"{"e":5}"#, + ] as [String: any SQLExpressible]), """ + JSON_OBJECT('c', '{"e":5}') + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "c": #"{"e":5}"#.databaseValue.asJSON, + ] as [String: any SQLExpressible]), """ + JSON_OBJECT('c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "c": Database.jsonObject(["e": 5]), + ]), """ + JSON_OBJECT('c', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "c": Database.json(#"{"e":5}"#), + ]), """ + JSON_OBJECT('c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + "a": nameColumn, + ]) + ), """ + SELECT JSON_OBJECT('a', "name") FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + "c": infoColumn, + ]) + ), """ + SELECT JSON_OBJECT('c', JSON("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + "a": Database.json(nameColumn), + ]) + ), """ + SELECT JSON_OBJECT('a', JSON("name")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + "c": Database.json(infoColumn), + ]) + ), """ + SELECT JSON_OBJECT('c', JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonObject_from_Array() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + // Ordered Array + + try assertEqualSQL( + db, + Database.jsonObject([ + (key: "a", value: 2), + (key: "c", value: #"{"e":5}"#), + ] as [(key: String, value: any SQLExpressible)]), """ + JSON_OBJECT('a', 2, 'c', '{"e":5}') + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + (key: "a", value: 2), + (key: "c", value: #"{"e":5}"#.databaseValue.asJSON), + ] as [(key: String, value: any SQLExpressible)]), """ + JSON_OBJECT('a', 2, 'c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + (key: "a", value: 2), + (key: "c", value: Database.jsonObject(["e": 5])), + ] as [(key: String, value: any SQLExpressible)]), """ + JSON_OBJECT('a', 2, 'c', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + (key: "a", value: 2), + (key: "c", value: Database.json(#"{"e":5}"#)), + ] as [(key: String, value: any SQLExpressible)]), """ + JSON_OBJECT('a', 2, 'c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + (key: "a", value: nameColumn), + (key: "c", value: infoColumn), + ] as [(key: String, value: any SQLExpressible)]) + ), """ + SELECT JSON_OBJECT('a', "name", 'c', JSON("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + (key: "a", value: Database.json(nameColumn)), + (key: "c", value: Database.json(infoColumn)), + ] as [(key: String, value: SQLExpression)]) + ), """ + SELECT JSON_OBJECT('a', JSON("name"), 'c', JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonObject_from_KeyValuePairs() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + // Ordered Array + + try assertEqualSQL( + db, + Database.jsonObject([ + "a": 2, + "c": #"{"e":5}"#, + ] as KeyValuePairs), """ + JSON_OBJECT('a', 2, 'c', '{"e":5}') + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "a": 2, + "c": #"{"e":5}"#.databaseValue.asJSON, + ] as KeyValuePairs), """ + JSON_OBJECT('a', 2, 'c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "a": 2, + "c": Database.jsonObject(["e": 5]), + ] as KeyValuePairs), """ + JSON_OBJECT('a', 2, 'c', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonObject([ + "a": 2, + "c": Database.json(#"{"e":5}"#), + ] as KeyValuePairs), """ + JSON_OBJECT('a', 2, 'c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + "a": nameColumn, + "c": infoColumn, + ] as KeyValuePairs) + ), """ + SELECT JSON_OBJECT('a', "name", 'c', JSON("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonObject([ + "a": Database.json(nameColumn), + "c": Database.json(infoColumn), + ] as KeyValuePairs) + ), """ + SELECT JSON_OBJECT('a', JSON("name"), 'c', JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonPatch() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonPatch(#"{"a":1,"b":2}"#, with: #"{"c":3,"d":4}"#), """ + JSON_PATCH('{"a":1,"b":2}', '{"c":3,"d":4}') + """) + + try assertEqualSQL(db, player.select(Database.jsonPatch(#"{"a":1,"b":2}"#, with: nameColumn)), """ + SELECT JSON_PATCH('{"a":1,"b":2}', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonPatch(#"{"a":1,"b":2}"#, with: infoColumn)), """ + SELECT JSON_PATCH('{"a":1,"b":2}', "info") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonPatch(nameColumn, with: #"{"c":3,"d":4}"#)), """ + SELECT JSON_PATCH("name", '{"c":3,"d":4}') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonPatch(infoColumn, with: #"{"c":3,"d":4}"#)), """ + SELECT JSON_PATCH("info", '{"c":3,"d":4}') FROM "player" + """) + } + } + + func test_Database_jsonRemove_atPath() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonRemove("[0,1,2,3,4]", atPath: "$[2]"), """ + JSON_REMOVE('[0,1,2,3,4]', '$[2]') + """) + + try assertEqualSQL(db, player.select(Database.jsonRemove(nameColumn, atPath: "$[2]")), """ + SELECT JSON_REMOVE("name", '$[2]') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonRemove("[0,1,2,3,4]", atPath: nameColumn)), """ + SELECT JSON_REMOVE('[0,1,2,3,4]', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonRemove(infoColumn, atPath: "$[2]")), """ + SELECT JSON_REMOVE("info", '$[2]') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonRemove("[0,1,2,3,4]", atPath: infoColumn)), """ + SELECT JSON_REMOVE('[0,1,2,3,4]', "info") FROM "player" + """) + } + } + + func test_Database_jsonRemove_atPaths() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonRemove("[0,1,2,3,4]", atPaths: ["$[2]", "$[0]"]), """ + JSON_REMOVE('[0,1,2,3,4]', '$[2]', '$[0]') + """) + + try assertEqualSQL(db, player.select(Database.jsonRemove(nameColumn, atPaths: ["$[2]", "$[0]"])), """ + SELECT JSON_REMOVE("name", '$[2]', '$[0]') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonRemove(infoColumn, atPaths: ["$[2]", "$[0]"])), """ + SELECT JSON_REMOVE("info", '$[2]', '$[0]') FROM "player" + """) + } + } + + func test_Database_jsonType() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#), """ + JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}') + """) + + try assertEqualSQL(db, player.select(Database.jsonType(nameColumn)), """ + SELECT JSON_TYPE("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonType(infoColumn)), """ + SELECT JSON_TYPE("info") FROM "player" + """) + } + } + + func test_Database_jsonType_atPath() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#, atPath: "$.a"), """ + JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}', '$.a') + """) + + try assertEqualSQL(db, player.select(Database.jsonType(nameColumn, atPath: "$.a")), """ + SELECT JSON_TYPE("name", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonType(infoColumn, atPath: "$.a")), """ + SELECT JSON_TYPE("info", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#, atPath: nameColumn)), """ + SELECT JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonType(#"{"a":[2,3.5,true,false,null,"x"]}"#, atPath: infoColumn)), """ + SELECT JSON_TYPE('{"a":[2,3.5,true,false,null,"x"]}', "info") FROM "player" + """) + } + } + + func test_Database_jsonIsValid() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonIsValid(#"{"x":35""#), """ + JSON_VALID('{"x":35"') + """) + + try assertEqualSQL(db, player.select(Database.jsonIsValid(nameColumn)), """ + SELECT JSON_VALID("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonIsValid(infoColumn)), """ + SELECT JSON_VALID("info") FROM "player" + """) + } + } + + func test_Database_jsonQuote() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonQuote(#"{"e":5}"#), """ + JSON_QUOTE('{"e":5}') + """) + + try assertEqualSQL(db, Database.jsonQuote(#"{"e":5}"#.databaseValue.asJSON), """ + JSON_QUOTE(JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonQuote(Database.json(#"{"e":5}"#)), """ + JSON_QUOTE(JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonQuote(Database.jsonObject(["e": 5])), """ + JSON_QUOTE(JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, player.select(Database.jsonQuote(nameColumn)), """ + SELECT JSON_QUOTE("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonQuote(infoColumn)), """ + SELECT JSON_QUOTE(JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonGroupArray() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(nameColumn)), """ + SELECT JSON_GROUP_ARRAY("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn)), """ + SELECT JSON_GROUP_ARRAY(JSON("info")) FROM "player" + """) + } + } + + func test_Database_jsonGroupObject() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("key", .text) + t.column("value", .jsonText) + } + let player = Table("player") + let keyColumn = Column("key") + let valueColumn = JSONColumn("value") + + try assertEqualSQL(db, player.select(Database.jsonGroupObject(key: keyColumn, value: valueColumn)), """ + SELECT JSON_GROUP_OBJECT("key", JSON("value")) FROM "player" + """) + } + } + + func test_index_and_generated_columns() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3038000 else { + throw XCTSkip("JSON support is not available") + } +#else + guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { + throw XCTSkip("JSON support is not available") + } +#endif + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.primaryKey("id", .integer) + t.column("address", .jsonText) + t.column("country", .text) + .generatedAs(JSONColumn("address")["country"]) + .indexed() + } + + XCTAssertEqual(Array(sqlQueries.suffix(2)), [ + """ + CREATE TABLE "player" ("id" INTEGER PRIMARY KEY, "address" TEXT, "country" TEXT GENERATED ALWAYS AS ("address" ->> 'country') VIRTUAL) + """, + """ + CREATE INDEX "player_on_country" ON "player"("country") + """, + ]) + + try db.create(index: "player_on_address", on: "player", expressions: [ + JSONColumn("address")["country"], + JSONColumn("address")["city"], + JSONColumn("address")["street"], + ]) + + XCTAssertEqual(lastSQLQuery, """ + CREATE INDEX "player_on_address" ON "player"("address" ->> 'country', "address" ->> 'city', "address" ->> 'street') + """) + + try db.execute(literal: """ + INSERT INTO player VALUES ( + NULL, + '{"street": "Rue de Belleville", "city": "Paris", "country": "France"}' + ) + """) + + try XCTAssertEqual(String.fetchOne(db, sql: "SELECT country FROM player"), "France") + } + } + +// TODO: Enable when those apis are ready. +// func test_ColumnAssignment() throws { +// #if GRDBCUSTOMSQLITE || GRDBCIPHER +// // Prevent SQLCipher failures +// guard sqlite3_libversion_number() >= 3038000 else { +// throw XCTSkip("JSON support is not available") +// } +// #else +// guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else { +// throw XCTSkip("JSON support is not available") +// } +// #endif +// +// try makeDatabaseQueue().inDatabase { db in +// try db.create(table: "player") { t in +// t.column("name", .text) +// t.column("info", .jsonText) +// } +// +// struct Player: TableRecord { } +// +// try Player.updateAll(db, [ +// JSONColumn("info").jsonPatch(with: Database.jsonObject(["city": "Paris"])) +// ]) +// XCTAssertEqual(lastSQLQuery, """ +// UPDATE "player" SET "info" = JSON_PATCH("info", JSON_OBJECT('city', 'Paris')) +// """) +// +// try Player.updateAll(db, [ +// JSONColumn("info").jsonRemove(atPath: "$.country") +// ]) +// print(lastSQLQuery!) +// XCTAssertEqual(lastSQLQuery, """ +// UPDATE "player" SET "info" = JSON_REMOVE("info", '$.country') +// """) +// +// try Player.updateAll(db, [ +// JSONColumn("info").jsonRemove(atPaths: ["$.country", "$.city"]) +// ]) +// print(lastSQLQuery!) +// XCTAssertEqual(lastSQLQuery, """ +// UPDATE "player" SET "info" = JSON_REMOVE("info", '$.country', '$.city') +// """) +// } +// } +}