Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the macOS availability of JSON functions #1442

Merged
merged 3 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions GRDB/Documentation.docc/JSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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.
SQLite and GRDB can store and fetch JSON values in database columns. Starting iOS 16+, macOS 10.15+, tvOS 17+, and watchOS 9+, JSON values can be manipulated at the database level.

## Store and fetch JSON values

Expand Down Expand Up @@ -98,7 +98,7 @@ extension Team: FetchableRecord, PersistableRecord {

## 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+).
[SQLite JSON functions and operators](https://www.sqlite.org/json1.html) are available starting iOS 16+, macOS 10.15+, 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.

Expand Down
9 changes: 7 additions & 2 deletions GRDB/JSON/SQLJSONExpressible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,6 @@ extension SQLJSONExpressible {
}
}
#else
@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+
extension SQLJSONExpressible {
/// The `->>` SQL operator.
///
Expand All @@ -285,6 +284,7 @@ extension SQLJSONExpressible {
///
/// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments),
/// or an JSON object field label, or an array index.
@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+
public subscript(_ path: some SQLExpressible) -> SQLExpression {
.binary(.jsonExtractSQL, sqlExpression, path.sqlExpression)
}
Expand Down Expand Up @@ -312,6 +312,7 @@ extension SQLJSONExpressible {
/// Related SQL documentation: <https://www.sqlite.org/json1.html#jex>
///
/// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public func jsonExtract(atPath path: some SQLExpressible) -> SQLExpression {
Database.jsonExtract(self, atPath: path)
}
Expand All @@ -333,6 +334,7 @@ extension SQLJSONExpressible {
/// Related SQL documentation: <https://www.sqlite.org/json1.html#jex>
///
/// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public func jsonExtract<C>(atPaths paths: C) -> SQLExpression
where C: Collection, C.Element: SQLExpressible
{
Expand Down Expand Up @@ -363,13 +365,13 @@ extension SQLJSONExpressible {
///
/// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments),
/// or an JSON object field label, or an array index.
@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+
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.
// ///
Expand All @@ -383,6 +385,7 @@ extension SQLJSONExpressible {
// /// ```
// ///
// /// Related SQLite documentation: <https://www.sqlite.org/json1.html#jpatch>
// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
// public func jsonPatch(
// with patch: some SQLExpressible)
// -> ColumnAssignment
Expand All @@ -405,6 +408,7 @@ extension SQLJSONExpressible {
// ///
// /// - Parameters:
// /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments).
// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
// public func jsonRemove(atPath path: some SQLExpressible) -> ColumnAssignment {
// .init(columnName: name, value: Database.jsonRemove(self, atPath: path))
// }
Expand All @@ -424,6 +428,7 @@ extension SQLJSONExpressible {
// ///
// /// - Parameters:
// /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
// public func jsonRemove<C>(atPaths paths: C)
// -> ColumnAssignment
// where C: Collection, C.Element: SQLExpressible
Expand Down
21 changes: 20 additions & 1 deletion GRDB/JSON/SQLJSONFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ extension Database {
}
}
#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.
///
Expand All @@ -398,6 +397,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jmini>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func json(_ value: some SQLExpressible) -> SQLExpression {
.function("JSON", [value.sqlExpression])
}
Expand All @@ -412,6 +412,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jarray>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonArray<C>(_ values: C) -> SQLExpression
where C: Collection, C.Element: SQLExpressible
{
Expand All @@ -428,6 +429,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jarray>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonArray<C>(_ values: C) -> SQLExpression
where C: Collection, C.Element == any SQLExpressible
{
Expand All @@ -445,6 +447,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jarraylen>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonArrayLength(_ value: some SQLExpressible) -> SQLExpression {
.function("JSON_ARRAY_LENGTH", [value.sqlExpression])
}
Expand All @@ -464,6 +467,7 @@ extension Database {
/// - Parameters:
/// - value: A JSON array.
/// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonArrayLength(
_ value: some SQLExpressible,
atPath path: some SQLExpressible)
Expand Down Expand Up @@ -501,6 +505,7 @@ extension Database {
/// - Parameters:
/// - value: A JSON value.
/// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonExtract(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression {
.function("JSON_EXTRACT", [value.sqlExpression, path.sqlExpression])
}
Expand All @@ -519,6 +524,7 @@ extension Database {
/// - Parameters:
/// - value: A JSON value.
/// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonExtract<C>(_ value: some SQLExpressible, atPaths paths: C)
-> SQLExpression
where C: Collection, C.Element: SQLExpressible
Expand All @@ -541,6 +547,7 @@ extension Database {
/// - value: A JSON value.
/// - assignments: A collection of key/value pairs, where keys are
/// [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonInsert<C>(
_ value: some SQLExpressible,
_ assignments: C)
Expand Down Expand Up @@ -568,6 +575,7 @@ extension Database {
/// - value: A JSON value.
/// - assignments: A collection of key/value pairs, where keys are
/// [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonReplace<C>(
_ value: some SQLExpressible,
_ assignments: C)
Expand Down Expand Up @@ -595,6 +603,7 @@ extension Database {
/// - value: A JSON value.
/// - assignments: A collection of key/value pairs, where keys are
/// [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonSet<C>(
_ value: some SQLExpressible,
_ assignments: C)
Expand Down Expand Up @@ -630,6 +639,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jobj>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonObject<C>(_ elements: C)
-> SQLExpression
where C: Collection,
Expand All @@ -650,6 +660,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jpatch>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonPatch(
_ value: some SQLExpressible,
with patch: some SQLExpressible)
Expand All @@ -672,6 +683,7 @@ extension Database {
/// - Parameters:
/// - value: A JSON value.
/// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression {
.function("JSON_REMOVE", [value.sqlExpression, path.sqlExpression])
}
Expand All @@ -690,6 +702,7 @@ extension Database {
/// - Parameters:
/// - value: A JSON value.
/// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonRemove<C>(_ value: some SQLExpressible, atPaths paths: C)
-> SQLExpression
where C: Collection, C.Element: SQLExpressible
Expand All @@ -707,6 +720,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jtype>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonType(_ value: some SQLExpressible) -> SQLExpression {
.function("JSON_TYPE", [value.sqlExpression])
}
Expand All @@ -725,6 +739,7 @@ extension Database {
/// - Parameters:
/// - value: A JSON value.
/// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments).
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonType(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression {
.function("JSON_TYPE", [value.sqlExpression, path.sqlExpression])
}
Expand All @@ -739,6 +754,7 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jvalid>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonIsValid(_ value: some SQLExpressible) -> SQLExpression {
.function("JSON_VALID", [value.sqlExpression])
}
Expand All @@ -756,20 +772,23 @@ extension Database {
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jquote>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonQuote(_ value: some SQLExpressible) -> SQLExpression {
.function("JSON_QUOTE", [value.sqlExpression.jsonBuilderExpression])
}

/// The `JSON_GROUP_ARRAY` SQL function.
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jgrouparray>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonGroupArray(_ value: some SQLExpressible) -> SQLExpression {
.function("JSON_GROUP_ARRAY", [value.sqlExpression.jsonBuilderExpression])
}

/// The `JSON_GROUP_OBJECT` SQL function.
///
/// Related SQLite documentation: <https://www.sqlite.org/json1.html#jgrouparray>
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
public static func jsonGroupObject(key: some SQLExpressible, value: some SQLExpressible) -> SQLExpression {
.function("JSON_GROUP_OBJECT", [key.sqlExpression, value.sqlExpression.jsonBuilderExpression])
}
Expand Down
14 changes: 11 additions & 3 deletions GRDB/QueryInterface/SQL/SQLExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,21 @@ public struct SQLExpression {
/// The `>>` bitwise right shift operator
static let rightShift = BinaryOperator(">>")

// Not guarded by availability checks, but only available for SQLite 3.38+
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// The `->` SQL operator
static let jsonExtractJSON = BinaryOperator("->", isJSONValue: true)

/// The `->>` SQL operator
static let jsonExtractSQL = BinaryOperator("->>")
#else
/// The `->` SQL operator
@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+
static let jsonExtractJSON = BinaryOperator("->", isJSONValue: true)

// Not guarded by availability checks, but only available for SQLite 3.38+
/// The `->>` SQL operator
@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+
static let jsonExtractSQL = BinaryOperator("->>")
#endif
}

/// `EscapableBinaryOperator` is an SQLite binary operator that accepts an
Expand Down Expand Up @@ -1977,7 +1985,7 @@ extension SQLExpression {
}
}
#else
@available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) // SQLite 3.38+
@available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS
/// Returns an expression suitable in JSON building contexts.
var jsonBuilderExpression: SQLExpression {
switch preferredJSONInterpretation {
Expand Down
48 changes: 36 additions & 12 deletions Tests/GRDBTests/JSONColumnTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class JSONColumnTests: GRDBTestCase {
throw XCTSkip("JSON support is not available")
}
#else
guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else {
guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else {
throw XCTSkip("JSON support is not available")
}
#endif
Expand Down Expand Up @@ -44,15 +44,47 @@ final class JSONColumnTests: GRDBTestCase {
}
}

func test_extraction() throws {
func test_JSON_EXTRACT() throws {
#if GRDBCUSTOMSQLITE || GRDBCIPHER
// Prevent SQLCipher failures
guard sqlite3_libversion_number() >= 3038000 else {
throw XCTSkip("JSON support is not available")
throw XCTSkip("JSON_EXTRACT is not available")
}
#else
guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else {
throw XCTSkip("JSON_EXTRACT 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.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"
""")
}
}

func test_extraction_operators() throws {
#if GRDBCUSTOMSQLITE || GRDBCIPHER
// Prevent SQLCipher failures
guard sqlite3_libversion_number() >= 3038000 else {
throw XCTSkip("JSON operators are not available")
}
#else
guard #available(iOS 16, macOS 13.2, tvOS 17, watchOS 9, *) else {
throw XCTSkip("JSON support is not available")
throw XCTSkip("JSON operators are not available")
}
#endif

Expand All @@ -74,14 +106,6 @@ final class JSONColumnTests: GRDBTestCase {
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"
""")
Expand Down
Loading
Loading