Skip to content

Commit

Permalink
Merge pull request #1699 from groue/dev/jsonb
Browse files Browse the repository at this point in the history
Support for JSONB expressions
  • Loading branch information
groue authored Jan 26, 2025
2 parents 0e664cf + ed17710 commit 2e2c0ea
Show file tree
Hide file tree
Showing 4 changed files with 1,716 additions and 108 deletions.
3 changes: 3 additions & 0 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
339 changes: 337 additions & 2 deletions GRDB/JSON/SQLJSONFunctions.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#if GRDBCUSTOMSQLITE || GRDBCIPHER
// MARK: - JSON

extension Database {
/// Validates and minifies a JSON string, with the `JSON` SQL function.
///
Expand Down Expand Up @@ -339,8 +341,19 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jvalid>
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 <https://www.sqlite.org/json1.html#the_json_valid_function>.
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.
Expand Down Expand Up @@ -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: <https://www.sqlite.org/json1.html#jmini>
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: <https://www.sqlite.org/json1.html#jarray>
public static func jsonbArray(
_ values: some Collection<some SQLExpressible>
) -> 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: <https://www.sqlite.org/json1.html#jarray>
public static func jsonbArray(
_ values: some Collection<any SQLExpressible>
) -> 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: <https://www.sqlite.org/json1.html#jex>
///
/// - 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: <https://www.sqlite.org/json1.html#jex>
///
/// - 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<some SQLExpressible>
) -> 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: <https://www.sqlite.org/json1.html#jinsb>
///
/// - 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: <https://www.sqlite.org/json1.html#jinsb>
///
/// - 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: <https://www.sqlite.org/json1.html#jinsb>
///
/// - 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: <https://www.sqlite.org/json1.html#jobj>
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: <https://www.sqlite.org/json1.html#jpatch>
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: <https://www.sqlite.org/json1.html#jrm>
///
/// - 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: <https://www.sqlite.org/json1.html#jrm>
///
/// - 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<some SQLExpressible>
) -> 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: <https://www.sqlite.org/json1.html#jgrouparray>
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: <https://www.sqlite.org/json1.html#jgrouparray>
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.
///
Expand Down
Loading

0 comments on commit 2e2c0ea

Please # to comment.