diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 61354847b1..859c7f42ad 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -2058,6 +2058,9 @@ extension Database { /// That's the reason why it is recommended to store JSON as text. public static let jsonText = ColumnType(rawValue: "TEXT") + /// The `BLOB` column type, suitable for JSONB columns. + public static let jsonb = ColumnType(rawValue: "BLOB") + /// The `INTEGER` column type. public static let integer = ColumnType(rawValue: "INTEGER") diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index 7b3d3d16ef..50fc7b29df 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -1,4 +1,6 @@ #if GRDBCUSTOMSQLITE || GRDBCIPHER +// MARK: - JSON + extension Database { /// Validates and minifies a JSON string, with the `JSON` SQL function. /// @@ -339,8 +341,19 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonIsValid(_ value: some SQLExpressible) -> SQLExpression { - .function("JSON_VALID", [value.sqlExpression]) + /// + /// - parameter value: The tested value. + /// - parameter options: See eventual second argument of the + /// `JSON_VALID` function. See . + public static func jsonIsValid( + _ value: some SQLExpressible, + options: JSONValidationOptions? = nil + ) -> SQLExpression { + if let options { + .function("JSON_VALID", [value.sqlExpression, options.rawValue.sqlExpression]) + } else { + .function("JSON_VALID", [value.sqlExpression]) + } } /// The `JSON_QUOTE` SQL function. @@ -419,7 +432,329 @@ extension Database { isJSONValue: true) } } + +// MARK: - JSONB + +extension Database { + public struct JSONValidationOptions: OptionSet, Sendable { + public let rawValue: Int + + public init(rawValue: Int) { self.rawValue = rawValue } + + public static let json = JSONValidationOptions(rawValue: 1) + public static let json5 = JSONValidationOptions(rawValue: 2) + public static let probablyJSONB = JSONValidationOptions(rawValue: 4) + public static let jsonb = JSONValidationOptions(rawValue: 8) + } + + /// Validates and returns a binary JSONB representation of the provided + /// JSON, with the `JSONB` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB(' { "a": [ "test" ] } ') → '{"a":["test"]}' + /// Database.jsonb(#" { "a": [ "test" ] } "#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonb(_ value: some SQLExpressible) -> SQLExpression { + .function("JSONB", [value.sqlExpression]) + } + + /// Creates a binary JSONB array with the `JSONB_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_ARRAY(1, 2, 3, 4) → '[1,2,3,4]' + /// Database.jsonbArray(1...4) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonbArray( + _ values: some Collection + ) -> SQLExpression { + .function("JSONB_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) + } + + /// Creates a binary JSONB array with the `JSONB_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_ARRAY(1, 2, '3', 4) → '[1,2,"3",4]' + /// Database.jsonbArray([1, 2, "3", 4]) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonbArray( + _ values: some Collection + ) -> SQLExpression { + .function("JSONB_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) + } + + /// The `JSONB_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_EXTRACT('{"a":123}', '$.a') → 123 + /// Database.jsonbExtract(#"{"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 jsonbExtract(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSONB_EXTRACT", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSONB_EXTRACT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_EXTRACT('{"a":2,"c":[4,5]}','$.c','$.a') → '[[4,5],2]' + /// Database.jsonbExtract(#"{"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 jsonbExtract( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { + .function("JSONB_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) + } + + /// The `JSONB_INSERT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_INSERT('[1,2,3,4]','$[#]',99) → '[1,2,3,4,99]' + /// Database.jsonbInsert("[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 jsonbInsert( + _ value: some SQLExpressible, + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { + .function("JSONB_INSERT", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSONB_REPLACE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_REPLACE('{"a":2,"c":4}', '$.a', 99) → '{"a":99,"c":4}' + /// Database.jsonbReplace(#"{"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 jsonbReplace( + _ value: some SQLExpressible, + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { + .function("JSONB_REPLACE", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSONB_SET` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_SET('{"a":2,"c":4}', '$.a', 99) → '{"a":99,"c":4}' + /// Database.jsonbSet(#"{"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 jsonbSet( + _ value: some SQLExpressible, + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { + .function("JSONB_SET", [value.sqlExpression] + assignments.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// Creates a binary JSONB object with the `JSONB_OBJECT` SQL function. + /// Pass key/value pairs with a Swift collection such as a `Dictionary`. + /// + /// For example: + /// + /// ```swift + /// // JSONB_OBJECT('c', '{"e":5}') → '{"c":"{\"e\":5}"}' + /// Database.jsonbObject([ + /// "c": #"{"e":5}"#, + /// ]) + /// + /// // JSONB_OBJECT('c', JSONB_OBJECT('e', 5)) → '{"c":{"e":5}}' + /// Database.jsonbObject([ + /// "c": Database.jsonbObject(["e": 5])), + /// ]) + /// + /// // JSONB_OBJECT('c', JSONB('{"e":5}')) → '{"c":{"e":5}}' + /// Database.jsonbObject([ + /// "c": Database.jsonb(#"{"e":5}"#), + /// ]) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonbObject( + _ elements: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { + .function("JSONB_OBJECT", elements.flatMap { + [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] + }) + } + + /// The `JSONB_PATCH` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_PATCH('{"a":1,"b":2}','{"c":3,"d":4}') → '{"a":1,"b":2,"c":3,"d":4}' + /// Database.jsonbPatch(#"{"a":1,"b":2}"#, #"{"c":3,"d":4}"#) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonbPatch( + _ value: some SQLExpressible, + with patch: some SQLExpressible) + -> SQLExpression + { + .function("JSONB_PATCH", [value.sqlExpression, patch.sqlExpression]) + } + + /// The `JSONB_REMOVE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_REMOVE('[0,1,2,3,4]', '$[2]') → '[0,1,3,4]' + /// Database.jsonbRemove("[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 jsonbRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { + .function("JSONB_REMOVE", [value.sqlExpression, path.sqlExpression]) + } + + /// The `JSONB_REMOVE` SQL function. + /// + /// For example: + /// + /// ```swift + /// // JSONB_REMOVE('[0,1,2,3,4]', '$[2]','$[0]') → '[1,3,4]' + /// Database.jsonbRemove("[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 jsonbRemove( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { + .function("JSONB_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) + } + + /// The `JSONB_GROUP_ARRAY` SQL function. + /// + /// For example: + /// + /// ```swift + /// // SELECT JSONB_GROUP_ARRAY(name) FROM player + /// Player.select(Database.jsonbGroupArray(Column("name"))) + /// + /// // SELECT JSONB_GROUP_ARRAY(name) FILTER (WHERE score > 0) FROM player + /// Player.select(Database.jsonbGroupArray(Column("name"), filter: Column("score") > 0)) + /// + /// // SELECT JSONB_GROUP_ARRAY(name ORDER BY name) FROM player + /// Player.select(Database.jsonbGroupArray(Column("name"), orderBy: Column("name"))) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonbGroupArray( + _ value: some SQLExpressible, + orderBy ordering: (any SQLOrderingTerm)? = nil, + filter: (any SQLSpecificExpressible)? = nil) + -> SQLExpression { + .aggregateFunction( + "JSONB_GROUP_ARRAY", + [value.sqlExpression.jsonBuilderExpression], + ordering: ordering?.sqlOrdering, + filter: filter?.sqlExpression, + isJSONValue: true) + } + + /// The `JSONB_GROUP_OBJECT` SQL function. + /// + /// For example: + /// + /// ```swift + /// // SELECT JSONB_GROUP_OBJECT(name, score) FROM player + /// Player.select(Database.jsonbGroupObject( + /// key: Column("name"), + /// value: Column("score"))) + /// + /// // SELECT JSONB_GROUP_OBJECT(name, score) FILTER (WHERE score > 0) FROM player + /// Player.select(Database.jsonbGroupObject( + /// key: Column("name"), + /// value: Column("score"), + /// filter: Column("score") > 0)) + /// ``` + /// + /// Related SQLite documentation: + public static func jsonbGroupObject( + key: some SQLExpressible, + value: some SQLExpressible, + filter: (any SQLSpecificExpressible)? = nil + ) -> SQLExpression { + .aggregateFunction( + "JSONB_GROUP_OBJECT", + [key.sqlExpression, value.sqlExpression.jsonBuilderExpression], + filter: filter?.sqlExpression, + isJSONValue: true) + } +} #else +// MARK: - JSON + extension Database { /// Validates and minifies a JSON string, with the `JSON` SQL function. /// diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index a4955fcadc..2790262875 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// An SQL expression. /// /// `SQLExpression` is an opaque representation of an SQL expression. @@ -954,6 +963,17 @@ extension SQLExpression { "JSON_REPLACE", "JSON_SET", "JSON_QUOTE", + "JSONB", + "JSONB_ARRAY", + "JSONB_EXTRACT", + "JSONB_GROUP_ARRAY", + "JSONB_GROUP_OBJECT", + "JSONB_INSERT", + "JSONB_OBJECT", + "JSONB_PATCH", + "JSONB_REMOVE", + "JSONB_REPLACE", + "JSONB_SET", "LENGTH", "LIKE", "LIKELIHOOD", @@ -1005,6 +1025,16 @@ extension SQLExpression { "JSON_REPLACE", "JSON_SET", "JSON_QUOTE", + "JSONB", + "JSONB_ARRAY", + "JSONB_GROUP_ARRAY", + "JSONB_GROUP_OBJECT", + "JSONB_INSERT", + "JSONB_OBJECT", + "JSONB_PATCH", + "JSONB_REMOVE", + "JSONB_REPLACE", + "JSONB_SET", ] /// The `COUNT(*)` expression. @@ -2099,8 +2129,9 @@ extension SQLExpression { case .jsonValue: if isJSONValue { return self + } else if sqlite3_libversion_number() >= 3045000 { + return .function("JSONB", [self]) } else { - // Needs explicit call to JSON() return .function("JSON", [self]) } } @@ -2116,8 +2147,9 @@ extension SQLExpression { case .jsonValue: if isJSONValue { return self + } else if sqlite3_libversion_number() >= 3045000 { + return .function("JSONB", [self]) } else { - // Needs explicit call to JSON() return .function("JSON", [self]) } } diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift index 465fc06923..05b34e310e 100644 --- a/Tests/GRDBTests/JSONExpressionsTests.swift +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -1,7 +1,19 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB final class JSONExpressionsTests: GRDBTestCase { + /// The SQL function used to build JSON expressions + private let jsonFunction = (sqlite3_libversion_number() >= 3045000) ? "JSONB" : "JSON" + func test_Database_json() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -37,6 +49,39 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_Database_jsonb() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonb) + } + let player = Table("player") + let nameColumn = Column("name") + let infoColumn = JSONColumn("info") + + try assertEqualSQL(db, Database.jsonb(#" { "a": [ "test" ] } "#), """ + JSONB(' { "a": [ "test" ] } ') + """) + + try assertEqualSQL(db, player.select(Database.jsonb(nameColumn)), """ + SELECT JSONB("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonb(infoColumn)), """ + SELECT JSONB("info") FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + func test_asJSON() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -87,12 +132,12 @@ final class JSONExpressionsTests: GRDBTestCase { ]) ]), """ SELECT JSON_ARRAY(\ - JSON('[1, 2, 3]'), \ + \(jsonFunction)('[1, 2, 3]'), \ NULL, \ - JSON("name"), \ - JSON("info"), \ - JSON(ABS("name")), \ - JSON(ABS("info"))\ + \(jsonFunction)("name"), \ + \(jsonFunction)("info"), \ + \(jsonFunction)(ABS("name")), \ + \(jsonFunction)(ABS("info"))\ ) FROM "player" """) } @@ -144,10 +189,10 @@ final class JSONExpressionsTests: GRDBTestCase { ), """ SELECT JSON_ARRAY(\ "name", \ - JSON("name"), \ - JSON("info"), \ + \(jsonFunction)("name"), \ + \(jsonFunction)("info"), \ JSON_EXTRACT("info", 'address'), \ - JSON(JSON_EXTRACT("info", 'address'))\ + \(jsonFunction)(JSON_EXTRACT("info", 'address'))\ ) FROM "player" """) } @@ -191,12 +236,12 @@ final class JSONExpressionsTests: GRDBTestCase { ), """ SELECT JSON_ARRAY(\ "name", \ - JSON("name"), \ - JSON("info"), \ + \(jsonFunction)("name"), \ + \(jsonFunction)("info"), \ "info" ->> 'score', \ - JSON("info" ->> 'score'), \ + \(jsonFunction)("info" ->> 'score'), \ JSON_EXTRACT("info", 'address'), \ - JSON(JSON_EXTRACT("info", 'address')), \ + \(jsonFunction)(JSON_EXTRACT("info", 'address')), \ "info" -> 'address', \ "info" -> 'address'\ ) FROM "player" @@ -220,8 +265,8 @@ final class JSONExpressionsTests: GRDBTestCase { ), """ SELECT JSON_ARRAY(\ "p"."name", \ - JSON("p"."name"), \ - JSON("p"."info"), \ + \(jsonFunction)("p"."name"), \ + \(jsonFunction)("p"."info"), \ "p"."info" ->> 'score', \ JSON_EXTRACT("p"."info", 'address'), \ "p"."info" -> 'address'\ @@ -242,8 +287,8 @@ final class JSONExpressionsTests: GRDBTestCase { ), """ SELECT JSON_ARRAY(\ "p"."name", \ - JSON("p"."name"), \ - JSON("p"."info"), \ + \(jsonFunction)("p"."name"), \ + \(jsonFunction)("p"."info"), \ "p"."info" ->> 'score', \ JSON_EXTRACT("p"."info", 'address'), \ "p"."info" -> 'address'\ @@ -264,14 +309,186 @@ final class JSONExpressionsTests: GRDBTestCase { ), """ SELECT JSON_ARRAY(\ "p"."name", \ - JSON("p"."name"), \ - JSON("p"."info"), \ + \(jsonFunction)("p"."name"), \ + \(jsonFunction)("p"."info"), \ + "p"."info" ->> 'score', \ + JSON_EXTRACT("p"."info", 'address'), \ + "p"."info" -> 'address'\ + ) FROM "player" "p" + """) + } + } + + func test_Database_jsonbArray() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbArray(1...4), """ + JSONB_ARRAY(1, 2, 3, 4) + """) + + try assertEqualSQL(db, Database.jsonbArray([1, 2, 3, 4]), """ + JSONB_ARRAY(1, 2, 3, 4) + """) + + try assertEqualSQL(db, Database.jsonbArray([1, 2, "3", 4]), """ + JSONB_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.jsonbArray([ + nameColumn, + nameColumn.asJSON, + infoColumn, + infoColumn.jsonExtract(atPath: "address"), + infoColumn.jsonExtract(atPath: "address").asJSON, + ] as [any SQLExpressible]) + ), """ + SELECT JSONB_ARRAY(\ + "name", \ + JSONB("name"), \ + JSONB("info"), \ + JSON_EXTRACT("info", 'address'), \ + JSONB(JSON_EXTRACT("info", 'address'))\ + ) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + func test_Database_jsonbArray_from_SQLJSONExpressible() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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") + + // Note: this JSON(JSON_EXTRACT(...)) is useful, when the extracted value is a string that contains JSON + try assertEqualSQL(db, player + .select( + Database.jsonbArray([ + 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 JSONB_ARRAY(\ + "name", \ + JSONB("name"), \ + JSONB("info"), \ + "info" ->> 'score', \ + JSONB("info" ->> 'score'), \ + JSON_EXTRACT("info", 'address'), \ + JSONB(JSON_EXTRACT("info", 'address')), \ + "info" -> 'address', \ + "info" -> 'address'\ + ) FROM "player" + """) + + let alias = TableAlias(name: "p") + + try assertEqualSQL(db, player + .aliased(alias) + .select( + alias[ + Database.jsonbArray([ + nameColumn, + nameColumn.asJSON, + infoColumn, + infoColumn["score"], + infoColumn.jsonExtract(atPath: "address"), + infoColumn.jsonRepresentation(atPath: "address"), + ] as [any SQLExpressible]) + ] + ), """ + SELECT JSONB_ARRAY(\ + "p"."name", \ + JSONB("p"."name"), \ + JSONB("p"."info"), \ + "p"."info" ->> 'score', \ + JSON_EXTRACT("p"."info", 'address'), \ + "p"."info" -> 'address'\ + ) FROM "player" "p" + """) + + try assertEqualSQL(db, player + .aliased(alias) + .select( + Database.jsonbArray([ + alias[nameColumn], + alias[nameColumn.asJSON], + alias[infoColumn], + alias[infoColumn["score"]], + alias[infoColumn.jsonExtract(atPath: "address")], + alias[infoColumn.jsonRepresentation(atPath: "address")], + ] as [any SQLExpressible]) + ), """ + SELECT JSONB_ARRAY(\ + "p"."name", \ + JSONB("p"."name"), \ + JSONB("p"."info"), \ + "p"."info" ->> 'score', \ + JSON_EXTRACT("p"."info", 'address'), \ + "p"."info" -> 'address'\ + ) FROM "player" "p" + """) + + try assertEqualSQL(db, player + .aliased(alias) + .select( + Database.jsonbArray([ + alias[nameColumn], + alias[nameColumn].asJSON, + alias[infoColumn], + alias[infoColumn]["score"], + alias[infoColumn].jsonExtract(atPath: "address"), + alias[infoColumn].jsonRepresentation(atPath: "address"), + ] as [any SQLExpressible]) + ), """ + SELECT JSONB_ARRAY(\ + "p"."name", \ + JSONB("p"."name"), \ + JSONB("p"."info"), \ "p"."info" ->> 'score', \ JSON_EXTRACT("p"."info", 'address'), \ "p"."info" -> 'address'\ ) FROM "player" "p" """) } +#else + throw XCTSkip("JSONB is not available") +#endif } func test_Database_jsonArrayLength() throws { @@ -463,6 +680,80 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_Database_jsonbExtract_atPath() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbExtract(#"{"a":123}"#, atPath: "$.a"), """ + JSONB_EXTRACT('{"a":123}', '$.a') + """) + + try assertEqualSQL(db, player.select(Database.jsonbExtract(nameColumn, atPath: "$.a")), """ + SELECT JSONB_EXTRACT("name", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbExtract(infoColumn, atPath: "$.a")), """ + SELECT JSONB_EXTRACT("info", '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbExtract(#"{"a":123}"#, atPath: nameColumn)), """ + SELECT JSONB_EXTRACT('{"a":123}', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbExtract(#"{"a":123}"#, atPath: infoColumn)), """ + SELECT JSONB_EXTRACT('{"a":123}', "info") FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + func test_Database_jsonbExtract_atPaths() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbExtract(#"{"a":2,"c":[4,5]}"#, atPaths: ["$.c", "$.a"]), """ + JSONB_EXTRACT('{"a":2,"c":[4,5]}', '$.c', '$.a') + """) + + try assertEqualSQL(db, player.select(Database.jsonbExtract(nameColumn, atPaths: ["$.c", "$.a"])), """ + SELECT JSONB_EXTRACT("name", '$.c', '$.a') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbExtract(infoColumn, atPaths: ["$.c", "$.a"])), """ + SELECT JSONB_EXTRACT("info", '$.c', '$.a') FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + func test_Database_jsonInsert() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -489,7 +780,7 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, Database.jsonInsert("[1,2,3,4]", ["$[#]": #"{"e":5}"#.databaseValue.asJSON]), """ - JSON_INSERT('[1,2,3,4]', '$[#]', JSON('{"e":5}')) + JSON_INSERT('[1,2,3,4]', '$[#]', \(jsonFunction)('{"e":5}')) """) try assertEqualSQL(db, Database.jsonInsert("[1,2,3,4]", ["$[#]": Database.json(#"{"e":5}"#)]), """ @@ -513,22 +804,17 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, player.select(Database.jsonInsert("[1,2,3,4]", ["$[#]": infoColumn])), """ - SELECT JSON_INSERT('[1,2,3,4]', '$[#]', JSON("info")) FROM "player" + SELECT JSON_INSERT('[1,2,3,4]', '$[#]', \(jsonFunction)("info")) FROM "player" """) } } - func test_Database_jsonReplace() throws { + func test_Database_jsonbInsert() 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, tvOS 17, watchOS 9, *) else { - throw XCTSkip("JSON support is not available") + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") } -#endif try makeDatabaseQueue().inDatabase { db in try db.create(table: "player") { t in @@ -539,41 +825,52 @@ final class JSONExpressionsTests: GRDBTestCase { 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.jsonbInsert("[1,2,3,4]", ["$[#]": #"{"e":5}"#]), """ + JSONB_INSERT('[1,2,3,4]', '$[#]', '{"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.jsonbInsert("[1,2,3,4]", ["$[#]": #"{"e":5}"#.databaseValue.asJSON]), """ + JSONB_INSERT('[1,2,3,4]', '$[#]', JSONB('{"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.jsonbInsert("[1,2,3,4]", ["$[#]": Database.json(#"{"e":5}"#)]), """ + JSONB_INSERT('[1,2,3,4]', '$[#]', 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, Database.jsonbInsert("[1,2,3,4]", ["$[#]": Database.jsonb(#"{"e":5}"#)]), """ + JSONB_INSERT('[1,2,3,4]', '$[#]', JSONB('{"e":5}')) """) - try assertEqualSQL(db, player.select(Database.jsonReplace(nameColumn, ["$.a": 99])), """ - SELECT JSON_REPLACE("name", '$.a', 99) FROM "player" + try assertEqualSQL(db, Database.jsonbInsert("[1,2,3,4]", ["$[#]": Database.jsonObject(["e": 5])]), """ + JSONB_INSERT('[1,2,3,4]', '$[#]', JSON_OBJECT('e', 5)) """) - try assertEqualSQL(db, player.select(Database.jsonReplace(infoColumn, ["$.a": 99])), """ - SELECT JSON_REPLACE("info", '$.a', 99) FROM "player" + try assertEqualSQL(db, Database.jsonbInsert("[1,2,3,4]", ["$[#]": Database.jsonbObject(["e": 5])]), """ + JSONB_INSERT('[1,2,3,4]', '$[#]', JSONB_OBJECT('e', 5)) """) - 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.jsonbInsert(nameColumn, ["$[#]": 99])), """ + SELECT JSONB_INSERT("name", '$[#]', 99) 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" + try assertEqualSQL(db, player.select(Database.jsonbInsert(infoColumn, ["$[#]": 99])), """ + SELECT JSONB_INSERT("info", '$[#]', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbInsert("[1,2,3,4]", ["$[#]": nameColumn])), """ + SELECT JSONB_INSERT('[1,2,3,4]', '$[#]', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbInsert("[1,2,3,4]", ["$[#]": infoColumn])), """ + SELECT JSONB_INSERT('[1,2,3,4]', '$[#]', JSONB("info")) FROM "player" """) } +#else + throw XCTSkip("JSONB is not available") +#endif } - func test_Database_jsonSet() throws { + func test_Database_jsonReplace() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures guard sqlite3_libversion_number() >= 3038000 else { @@ -594,21 +891,137 @@ final class JSONExpressionsTests: GRDBTestCase { 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.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#]), """ + JSON_REPLACE('{"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.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#.databaseValue.asJSON]), """ + JSON_REPLACE('{"a":2,"c":4}', '$.a', \(jsonFunction)('{"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.jsonReplace(#"{"a":2,"c":4}"#, ["$.a": Database.json(#"{"e":5}"#)]), """ + JSON_REPLACE('{"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, 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', \(jsonFunction)("info")) FROM "player" + """) + } + } + + func test_Database_jsonbReplace() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#]), """ + JSONB_REPLACE('{"a":2,"c":4}', '$.a', '{"e":5}') + """) + + try assertEqualSQL(db, Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#.databaseValue.asJSON]), """ + JSONB_REPLACE('{"a":2,"c":4}', '$.a', JSONB('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": Database.json(#"{"e":5}"#)]), """ + JSONB_REPLACE('{"a":2,"c":4}', '$.a', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": Database.jsonb(#"{"e":5}"#)]), """ + JSONB_REPLACE('{"a":2,"c":4}', '$.a', JSONB('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": Database.jsonObject(["e": 5])]), """ + JSONB_REPLACE('{"a":2,"c":4}', '$.a', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": Database.jsonbObject(["e": 5])]), """ + JSONB_REPLACE('{"a":2,"c":4}', '$.a', JSONB_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, player.select(Database.jsonbReplace(nameColumn, ["$.a": 99])), """ + SELECT JSONB_REPLACE("name", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbReplace(infoColumn, ["$.a": 99])), """ + SELECT JSONB_REPLACE("info", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": nameColumn])), """ + SELECT JSONB_REPLACE('{"a":2,"c":4}', '$.a', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbReplace(#"{"a":2,"c":4}"#, ["$.a": infoColumn])), """ + SELECT JSONB_REPLACE('{"a":2,"c":4}', '$.a', JSONB("info")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + 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, 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', \(jsonFunction)('{"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" @@ -623,11 +1036,72 @@ final class JSONExpressionsTests: GRDBTestCase { """) 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" + SELECT JSON_SET('{"a":2,"c":4}', '$.a', \(jsonFunction)("info")) FROM "player" """) } } + func test_Database_jsonbSet() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#]), """ + JSONB_SET('{"a":2,"c":4}', '$.a', '{"e":5}') + """) + + try assertEqualSQL(db, Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": #"{"e":5}"#.databaseValue.asJSON]), """ + JSONB_SET('{"a":2,"c":4}', '$.a', JSONB('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": Database.json(#"{"e":5}"#)]), """ + JSONB_SET('{"a":2,"c":4}', '$.a', JSON('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": Database.jsonb(#"{"e":5}"#)]), """ + JSONB_SET('{"a":2,"c":4}', '$.a', JSONB('{"e":5}')) + """) + + try assertEqualSQL(db, Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": Database.jsonObject(["e": 5])]), """ + JSONB_SET('{"a":2,"c":4}', '$.a', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": Database.jsonbObject(["e": 5])]), """ + JSONB_SET('{"a":2,"c":4}', '$.a', JSONB_OBJECT('e', 5)) + """) + + try assertEqualSQL(db, player.select(Database.jsonbSet(nameColumn, ["$.a": 99])), """ + SELECT JSONB_SET("name", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbSet(infoColumn, ["$.a": 99])), """ + SELECT JSONB_SET("info", '$.a', 99) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": nameColumn])), """ + SELECT JSONB_SET('{"a":2,"c":4}', '$.a', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbSet(#"{"a":2,"c":4}"#, ["$.a": infoColumn])), """ + SELECT JSONB_SET('{"a":2,"c":4}', '$.a', JSONB("info")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + func test_Database_jsonObject_from_Dictionary() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -670,7 +1144,7 @@ final class JSONExpressionsTests: GRDBTestCase { Database.jsonObject([ "c": #"{"e":5}"#.databaseValue.asJSON, ] as [String: any SQLExpressible]), """ - JSON_OBJECT('c', JSON('{"e":5}')) + JSON_OBJECT('c', \(jsonFunction)('{"e":5}')) """) try assertEqualSQL( @@ -706,7 +1180,7 @@ final class JSONExpressionsTests: GRDBTestCase { "c": infoColumn, ]) ), """ - SELECT JSON_OBJECT('c', JSON("info")) FROM "player" + SELECT JSON_OBJECT('c', \(jsonFunction)("info")) FROM "player" """) try assertEqualSQL( @@ -769,7 +1243,7 @@ final class JSONExpressionsTests: GRDBTestCase { (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}')) + JSON_OBJECT('a', 2, 'c', \(jsonFunction)('{"e":5}')) """) try assertEqualSQL( @@ -798,7 +1272,7 @@ final class JSONExpressionsTests: GRDBTestCase { (key: "c", value: infoColumn), ] as [(key: String, value: any SQLExpressible)]) ), """ - SELECT JSON_OBJECT('a', "name", 'c', JSON("info")) FROM "player" + SELECT JSON_OBJECT('a', "name", 'c', \(jsonFunction)("info")) FROM "player" """) try assertEqualSQL( @@ -852,7 +1326,7 @@ final class JSONExpressionsTests: GRDBTestCase { "a": 2, "c": #"{"e":5}"#.databaseValue.asJSON, ] as KeyValuePairs), """ - JSON_OBJECT('a', 2, 'c', JSON('{"e":5}')) + JSON_OBJECT('a', 2, 'c', \(jsonFunction)('{"e":5}')) """) try assertEqualSQL( @@ -881,7 +1355,7 @@ final class JSONExpressionsTests: GRDBTestCase { "c": infoColumn, ] as KeyValuePairs) ), """ - SELECT JSON_OBJECT('a', "name", 'c', JSON("info")) FROM "player" + SELECT JSON_OBJECT('a', "name", 'c', \(jsonFunction)("info")) FROM "player" """) try assertEqualSQL( @@ -897,6 +1371,363 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_Database_jsonbObject_from_Dictionary() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbObject([ + "a": 2, + ] as [String: Int]), """ + JSONB_OBJECT('a', 2) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "c": #"{"e":5}"#, + ] as [String: any SQLExpressible]), """ + JSONB_OBJECT('c', '{"e":5}') + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "c": #"{"e":5}"#.databaseValue.asJSON, + ] as [String: any SQLExpressible]), """ + JSONB_OBJECT('c', JSONB('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "c": Database.jsonObject(["e": 5]), + ]), """ + JSONB_OBJECT('c', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "c": Database.jsonbObject(["e": 5]), + ]), """ + JSONB_OBJECT('c', JSONB_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "c": Database.json(#"{"e":5}"#), + ]), """ + JSONB_OBJECT('c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "c": Database.jsonb(#"{"e":5}"#), + ]), """ + JSONB_OBJECT('c', JSONB('{"e":5}')) + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "a": nameColumn, + ]) + ), """ + SELECT JSONB_OBJECT('a', "name") FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "c": infoColumn, + ]) + ), """ + SELECT JSONB_OBJECT('c', JSONB("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "a": Database.json(nameColumn), + ]) + ), """ + SELECT JSONB_OBJECT('a', JSON("name")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "a": Database.jsonb(nameColumn), + ]) + ), """ + SELECT JSONB_OBJECT('a', JSONB("name")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "c": Database.json(infoColumn), + ]) + ), """ + SELECT JSONB_OBJECT('c', JSON("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "c": Database.jsonb(infoColumn), + ]) + ), """ + SELECT JSONB_OBJECT('c', JSONB("info")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + func test_Database_jsonbObject_from_Array() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbObject([ + (key: "a", value: 2), + (key: "c", value: #"{"e":5}"#), + ] as [(key: String, value: any SQLExpressible)]), """ + JSONB_OBJECT('a', 2, 'c', '{"e":5}') + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + (key: "a", value: 2), + (key: "c", value: #"{"e":5}"#.databaseValue.asJSON), + ] as [(key: String, value: any SQLExpressible)]), """ + JSONB_OBJECT('a', 2, 'c', JSONB('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + (key: "a", value: 2), + (key: "c", value: Database.jsonObject(["e": 5])), + ] as [(key: String, value: any SQLExpressible)]), """ + JSONB_OBJECT('a', 2, 'c', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + (key: "a", value: 2), + (key: "c", value: Database.jsonbObject(["e": 5])), + ] as [(key: String, value: any SQLExpressible)]), """ + JSONB_OBJECT('a', 2, 'c', JSONB_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + (key: "a", value: 2), + (key: "c", value: Database.json(#"{"e":5}"#)), + ] as [(key: String, value: any SQLExpressible)]), """ + JSONB_OBJECT('a', 2, 'c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + (key: "a", value: 2), + (key: "c", value: Database.jsonb(#"{"e":5}"#)), + ] as [(key: String, value: any SQLExpressible)]), """ + JSONB_OBJECT('a', 2, 'c', JSONB('{"e":5}')) + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + (key: "a", value: nameColumn), + (key: "c", value: infoColumn), + ] as [(key: String, value: any SQLExpressible)]) + ), """ + SELECT JSONB_OBJECT('a', "name", 'c', JSONB("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + (key: "a", value: Database.json(nameColumn)), + (key: "c", value: Database.json(infoColumn)), + ] as [(key: String, value: SQLExpression)]) + ), """ + SELECT JSONB_OBJECT('a', JSON("name"), 'c', JSON("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + (key: "a", value: Database.jsonb(nameColumn)), + (key: "c", value: Database.jsonb(infoColumn)), + ] as [(key: String, value: SQLExpression)]) + ), """ + SELECT JSONB_OBJECT('a', JSONB("name"), 'c', JSONB("info")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + func test_Database_jsonbObject_from_KeyValuePairs() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbObject([ + "a": 2, + "c": #"{"e":5}"#, + ] as KeyValuePairs), """ + JSONB_OBJECT('a', 2, 'c', '{"e":5}') + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "a": 2, + "c": #"{"e":5}"#.databaseValue.asJSON, + ] as KeyValuePairs), """ + JSONB_OBJECT('a', 2, 'c', JSONB('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "a": 2, + "c": Database.jsonObject(["e": 5]), + ] as KeyValuePairs), """ + JSONB_OBJECT('a', 2, 'c', JSON_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "a": 2, + "c": Database.jsonbObject(["e": 5]), + ] as KeyValuePairs), """ + JSONB_OBJECT('a', 2, 'c', JSONB_OBJECT('e', 5)) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "a": 2, + "c": Database.json(#"{"e":5}"#), + ] as KeyValuePairs), """ + JSONB_OBJECT('a', 2, 'c', JSON('{"e":5}')) + """) + + try assertEqualSQL( + db, + Database.jsonbObject([ + "a": 2, + "c": Database.jsonb(#"{"e":5}"#), + ] as KeyValuePairs), """ + JSONB_OBJECT('a', 2, 'c', JSONB('{"e":5}')) + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "a": nameColumn, + "c": infoColumn, + ] as KeyValuePairs) + ), """ + SELECT JSONB_OBJECT('a', "name", 'c', JSONB("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "a": Database.json(nameColumn), + "c": Database.json(infoColumn), + ] as KeyValuePairs) + ), """ + SELECT JSONB_OBJECT('a', JSON("name"), 'c', JSON("info")) FROM "player" + """) + + try assertEqualSQL( + db, + player.select( + Database.jsonbObject([ + "a": Database.jsonb(nameColumn), + "c": Database.jsonb(infoColumn), + ] as KeyValuePairs) + ), """ + SELECT JSONB_OBJECT('a', JSONB("name"), 'c', JSONB("info")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + func test_Database_jsonPatch() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -940,17 +1771,131 @@ final class JSONExpressionsTests: GRDBTestCase { } } - func test_Database_jsonRemove_atPath() throws { + func test_Database_jsonbPatch() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbPatch(#"{"a":1,"b":2}"#, with: #"{"c":3,"d":4}"#), """ + JSONB_PATCH('{"a":1,"b":2}', '{"c":3,"d":4}') + """) + + try assertEqualSQL(db, player.select(Database.jsonbPatch(#"{"a":1,"b":2}"#, with: nameColumn)), """ + SELECT JSONB_PATCH('{"a":1,"b":2}', "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbPatch(#"{"a":1,"b":2}"#, with: infoColumn)), """ + SELECT JSONB_PATCH('{"a":1,"b":2}', "info") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbPatch(nameColumn, with: #"{"c":3,"d":4}"#)), """ + SELECT JSONB_PATCH("name", '{"c":3,"d":4}') FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbPatch(infoColumn, with: #"{"c":3,"d":4}"#)), """ + SELECT JSONB_PATCH("info", '{"c":3,"d":4}') FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + 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, 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, 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_jsonbRemove_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, tvOS 17, watchOS 9, *) else { - throw XCTSkip("JSON support is not available") + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") } -#endif try makeDatabaseQueue().inDatabase { db in try db.create(table: "player") { t in @@ -961,39 +1906,37 @@ final class JSONExpressionsTests: GRDBTestCase { 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, Database.jsonbRemove("[0,1,2,3,4]", atPath: "$[2]"), """ + JSONB_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.jsonbRemove(nameColumn, atPath: "$[2]")), """ + SELECT JSONB_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.jsonbRemove("[0,1,2,3,4]", atPath: nameColumn)), """ + SELECT JSONB_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.jsonbRemove(infoColumn, atPath: "$[2]")), """ + SELECT JSONB_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" + try assertEqualSQL(db, player.select(Database.jsonbRemove("[0,1,2,3,4]", atPath: infoColumn)), """ + SELECT JSONB_REMOVE('[0,1,2,3,4]', "info") FROM "player" """) } +#else + throw XCTSkip("JSONB is not available") +#endif } - func test_Database_jsonRemove_atPaths() throws { + func test_Database_jsonbRemove_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, tvOS 17, watchOS 9, *) else { - throw XCTSkip("JSON support is not available") + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") } -#endif try makeDatabaseQueue().inDatabase { db in try db.create(table: "player") { t in @@ -1004,18 +1947,21 @@ final class JSONExpressionsTests: GRDBTestCase { 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, Database.jsonbRemove("[0,1,2,3,4]", atPaths: ["$[2]", "$[0]"]), """ + JSONB_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.jsonbRemove(nameColumn, atPaths: ["$[2]", "$[0]"])), """ + SELECT JSONB_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" + try assertEqualSQL(db, player.select(Database.jsonbRemove(infoColumn, atPaths: ["$[2]", "$[0]"])), """ + SELECT JSONB_REMOVE("info", '$[2]', '$[0]') FROM "player" """) } +#else + throw XCTSkip("JSONB is not available") +#endif } func test_Database_jsonType() throws { @@ -1131,6 +2077,40 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_Database_jsonIsValid_options() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSON_VALID options are not available") + } + + try makeDatabaseQueue().inDatabase { db in + try assertEqualSQL(db, Database.jsonIsValid(#"{"x":35""#, options: .json), """ + JSON_VALID('{"x":35"', 1) + """) + + try assertEqualSQL(db, Database.jsonIsValid(#"{"x":35""#, options: .json5), """ + JSON_VALID('{"x":35"', 2) + """) + + try assertEqualSQL(db, Database.jsonIsValid(#"{"x":35""#, options: .probablyJSONB), """ + JSON_VALID('{"x":35"', 4) + """) + + try assertEqualSQL(db, Database.jsonIsValid(#"{"x":35""#, options: .jsonb), """ + JSON_VALID('{"x":35"', 8) + """) + + try assertEqualSQL(db, Database.jsonIsValid(#"{"x":35""#, options: [.json, .jsonb]), """ + JSON_VALID('{"x":35"', 9) + """) + + } +#else + throw XCTSkip("JSON_VALID options are not available") +#endif + } + func test_Database_jsonQuote() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures @@ -1157,7 +2137,7 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, Database.jsonQuote(#"{"e":5}"#.databaseValue.asJSON), """ - JSON_QUOTE(JSON('{"e":5}')) + JSON_QUOTE(\(jsonFunction)('{"e":5}')) """) try assertEqualSQL(db, Database.jsonQuote(Database.json(#"{"e":5}"#)), """ @@ -1173,7 +2153,7 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, player.select(Database.jsonQuote(infoColumn)), """ - SELECT JSON_QUOTE(JSON("info")) FROM "player" + SELECT JSON_QUOTE(\(jsonFunction)("info")) FROM "player" """) } } @@ -1204,9 +2184,63 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn)), """ - SELECT JSON_GROUP_ARRAY(JSON("info")) FROM "player" + SELECT JSON_GROUP_ARRAY(\(jsonFunction)("info")) FROM "player" + """) + } + } + + func test_Database_jsonGroupArray_from_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, 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) + } + try db.execute(sql: """ + INSERT INTO player (info) VALUES ('{"foo": "bar"}') + """) + let player = Table("player") + let infoColumn = JSONColumn("info") + let request = player.select(Database.jsonGroupArray(infoColumn)) + let json = try String.fetchOne(db, request) + XCTAssertEqual(json, #"[{"foo":"bar"}]"#) + } + } + + func test_Database_jsonGroupArray_from_JSONB() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.column("name", .text) + t.column("info", .jsonText) + } + try db.execute(sql: """ + INSERT INTO player (info) VALUES (JSONB('{"foo": "bar"}')) """) + let player = Table("player") + let infoColumn = JSONColumn("info") + let request = player.select(Database.jsonGroupArray(infoColumn)) + let json = try String.fetchOne(db, request) + XCTAssertEqual(json, #"[{"foo":"bar"}]"#) } +#else + throw XCTSkip("JSONB is not available") +#endif } func test_Database_jsonGroupArray_filter() throws { @@ -1235,13 +2269,13 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn, filter: length(nameColumn) > 0)), """ - SELECT JSON_GROUP_ARRAY(JSON("info")) FILTER (WHERE LENGTH("name") > 0) FROM "player" + SELECT JSON_GROUP_ARRAY(\(jsonFunction)("info")) FILTER (WHERE LENGTH("name") > 0) FROM "player" """) } } -#if GRDBCUSTOMSQLITE || GRDBCIPHER func test_Database_jsonGroupArray_order() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures guard sqlite3_libversion_number() >= 3044000 else { throw XCTSkip("JSON support is not available") @@ -1261,19 +2295,116 @@ final class JSONExpressionsTests: GRDBTestCase { """) try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn, orderBy: nameColumn.desc)), """ - SELECT JSON_GROUP_ARRAY(JSON("info") ORDER BY "name" DESC) FROM "player" + SELECT JSON_GROUP_ARRAY(\(jsonFunction)("info") ORDER BY "name" DESC) FROM "player" """) - + try assertEqualSQL(db, player.select(Database.jsonGroupArray(nameColumn, orderBy: nameColumn, filter: length(nameColumn) > 0)), """ SELECT JSON_GROUP_ARRAY("name" ORDER BY "name") FILTER (WHERE LENGTH("name") > 0) FROM "player" """) try assertEqualSQL(db, player.select(Database.jsonGroupArray(infoColumn, orderBy: nameColumn.desc, filter: length(nameColumn) > 0)), """ - SELECT JSON_GROUP_ARRAY(JSON("info") ORDER BY "name" DESC) FILTER (WHERE LENGTH("name") > 0) FROM "player" + SELECT JSON_GROUP_ARRAY(\(jsonFunction)("info") ORDER BY "name" DESC) FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + } +#else + throw XCTSkip("JSON_GROUP_ARRAY(ORDER) is not available") +#endif + } + + func test_Database_jsonbGroupArray() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbGroupArray(nameColumn)), """ + SELECT JSONB_GROUP_ARRAY("name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbGroupArray(infoColumn)), """ + SELECT JSONB_GROUP_ARRAY(JSONB("info")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + func test_Database_jsonbGroupArray_filter() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbGroupArray(nameColumn, filter: length(nameColumn) > 0)), """ + SELECT JSONB_GROUP_ARRAY("name") FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbGroupArray(infoColumn, filter: length(nameColumn) > 0)), """ + SELECT JSONB_GROUP_ARRAY(JSONB("info")) FILTER (WHERE LENGTH("name") > 0) FROM "player" """) } +#else + throw XCTSkip("JSONB is not available") +#endif } + + func test_Database_jsonbGroupArray_order() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbGroupArray(nameColumn, orderBy: nameColumn)), """ + SELECT JSONB_GROUP_ARRAY("name" ORDER BY "name") FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbGroupArray(infoColumn, orderBy: nameColumn.desc)), """ + SELECT JSONB_GROUP_ARRAY(JSONB("info") ORDER BY "name" DESC) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbGroupArray(nameColumn, orderBy: nameColumn, filter: length(nameColumn) > 0)), """ + SELECT JSONB_GROUP_ARRAY("name" ORDER BY "name") FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + + try assertEqualSQL(db, player.select(Database.jsonbGroupArray(infoColumn, orderBy: nameColumn.desc, filter: length(nameColumn) > 0)), """ + SELECT JSONB_GROUP_ARRAY(JSONB("info") ORDER BY "name" DESC) FILTER (WHERE LENGTH("name") > 0) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") #endif + } func test_Database_jsonGroupObject() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER @@ -1297,7 +2428,7 @@ final class JSONExpressionsTests: GRDBTestCase { let valueColumn = JSONColumn("value") try assertEqualSQL(db, player.select(Database.jsonGroupObject(key: keyColumn, value: valueColumn)), """ - SELECT JSON_GROUP_OBJECT("key", JSON("value")) FROM "player" + SELECT JSON_GROUP_OBJECT("key", \(jsonFunction)("value")) FROM "player" """) } } @@ -1324,12 +2455,62 @@ final class JSONExpressionsTests: GRDBTestCase { let valueColumn = JSONColumn("value") try assertEqualSQL(db, player.select(Database.jsonGroupObject(key: keyColumn, value: valueColumn, filter: length(valueColumn) > 0)), """ - SELECT JSON_GROUP_OBJECT("key", JSON("value")) FILTER (WHERE LENGTH("value") > 0) FROM "player" + SELECT JSON_GROUP_OBJECT("key", \(jsonFunction)("value")) FILTER (WHERE LENGTH("value") > 0) FROM "player" + """) + } + } + + func test_Database_jsonbGroupObject() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbGroupObject(key: keyColumn, value: valueColumn)), """ + SELECT JSONB_GROUP_OBJECT("key", \(jsonFunction)("value")) FROM "player" + """) + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + + func test_Database_jsonbGroupObject_filter() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + 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.jsonbGroupObject(key: keyColumn, value: valueColumn, filter: length(valueColumn) > 0)), """ + SELECT JSONB_GROUP_OBJECT("key", \(jsonFunction)("value")) FILTER (WHERE LENGTH("value") > 0) FROM "player" """) } +#else + throw XCTSkip("JSONB is not available") +#endif } - func test_index_and_generated_columns() throws { + func test_JSON_index_and_generated_columns() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER // Prevent SQLCipher failures guard sqlite3_libversion_number() >= 3038000 else { @@ -1389,6 +2570,63 @@ final class JSONExpressionsTests: GRDBTestCase { } } + func test_JSONB_index_and_generated_columns() throws { +#if GRDBCUSTOMSQLITE || GRDBCIPHER + // Prevent SQLCipher failures + guard sqlite3_libversion_number() >= 3045000 else { + throw XCTSkip("JSONB is not available") + } + + try makeDatabaseQueue().inDatabase { db in + try db.create(table: "player") { t in + t.primaryKey("id", .integer) + t.column("address", .jsonb) + t.column("country", .text) + .generatedAs(JSONColumn("address").jsonExtract(atPath: "$.country")) + .indexed() + } + + XCTAssertEqual(Array(sqlQueries.suffix(2)), [ + """ + CREATE TABLE "player" (\ + "id" INTEGER PRIMARY KEY, \ + "address" BLOB, \ + "country" TEXT GENERATED ALWAYS AS (JSON_EXTRACT("address", '$.country')) VIRTUAL\ + ) + """, + """ + CREATE INDEX "player_on_country" ON "player"("country") + """, + ]) + + try db.create(index: "player_on_address", on: "player", expressions: [ + JSONColumn("address").jsonExtract(atPath: "$.country"), + JSONColumn("address").jsonExtract(atPath: "$.city"), + JSONColumn("address").jsonExtract(atPath: "$.street"), + ]) + + XCTAssertEqual(lastSQLQuery, """ + CREATE INDEX "player_on_address" ON "player"(\ + JSON_EXTRACT("address", '$.country'), \ + JSON_EXTRACT("address", '$.city'), \ + JSON_EXTRACT("address", '$.street')\ + ) + """) + + try db.execute(literal: """ + INSERT INTO player VALUES ( + NULL, + JSONB('{"street": "Rue de Belleville", "city": "Paris", "country": "France"}') + ) + """) + + try XCTAssertEqual(String.fetchOne(db, sql: "SELECT country FROM player"), "France") + } +#else + throw XCTSkip("JSONB is not available") +#endif + } + // TODO: Enable when those apis are ready. // func test_ColumnAssignment() throws { // #if GRDBCUSTOMSQLITE || GRDBCIPHER