From 2a2990e3625b3fbbbd1c843c7b358e975a401fdd Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 17 Jan 2024 18:17:31 +0100 Subject: [PATCH] Support jsonb functions in query builder --- drift/CHANGELOG.md | 1 + drift/lib/extensions/json1.dart | 66 +++++++++++++++++ .../extensions/json1_integration_test.dart | 35 ++++++++- drift/test/extensions/json1_test.dart | 72 ++++++++++++++++++- drift/tool/download_sqlite3.dart | 4 +- 5 files changed, 171 insertions(+), 7 deletions(-) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index b67d826c..70375de5 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -8,6 +8,7 @@ - Close wasm databases hosted in workers after the last client disconnects. - Add `enableMigrations` parameter to `NativeDatabase` which can be used to optionally disable database migrations when opening databases. +- Support `jsonb` functions in the Dart query builder. ## 2.14.1 diff --git a/drift/lib/extensions/json1.dart b/drift/lib/extensions/json1.dart index ba8ebc69..9031491a 100644 --- a/drift/lib/extensions/json1.dart +++ b/drift/lib/extensions/json1.dart @@ -12,6 +12,22 @@ import '../drift.dart'; /// Defines extensions on string expressions to support the json1 api from Dart. extension JsonExtensions on Expression { + /// Reads `this` expression as a JSON structure and outputs the JSON in a + /// minified format. + /// + /// For details, see https://www.sqlite.org/json1.html#jmini. + Expression json() { + return FunctionCallExpression('json', [this]); + } + + /// Reads `this` expression as a JSON structure and outputs the JSON in a + /// binary format internal to sqlite3. + /// + /// For details, see https://www.sqlite.org/json1.html#jminib. + Expression jsonb() { + return FunctionCallExpression('jsonb', [this]); + } + /// Assuming that this string is a json array, returns the length of this json /// array. /// @@ -85,6 +101,56 @@ extension JsonExtensions on Expression { } } +/// Defines extensions for the binary `JSONB` format introduced in sqlite3 +/// version 3.45. +/// +/// For details, see https://www.sqlite.org/json1.html#jsonb +extension JsonbExtensions on Expression { + /// Reads this binary JSONB structure and emits its textual representation as + /// minified JSON. + /// + /// For details, see https://www.sqlite.org/json1.html#jmini. + Expression json() { + return dartCast().json(); + } + + /// Assuming that `this` is an expression evaluating to a binary JSONB array, + /// returns the length of the array. + /// + /// See [JsonExtensions.jsonArrayLength] for more details and + /// https://www.sqlite.org/json1.html#jsonb for details on jsonb. + Expression jsonArrayLength([String? path]) { + // the function accepts both formats, and this way we avoid some duplicate + // code. + return dartCast().jsonArrayLength(path); + } + + /// Assuming that `this` is an expression evaluating to a binary JSONB object + /// or array, extracts the part of the structure identified by [path]. + /// + /// For more details, see [JsonExtensions.jsonExtract] or + /// https://www.sqlite.org/json1.html#jex. + Expression jsonExtract(String path) { + return dartCast().jsonExtract(path); + } + + /// Calls the `json_each` table-valued function on `this` binary JSON buffer, + /// optionally using [path] as the root path. + /// + /// See [JsonTableFunction] and [JsonExtensions.jsonEach] for more details. + JsonTableFunction jsonEach(DatabaseConnectionUser database, [String? path]) { + return dartCast().jsonEach(database, path); + } + + /// Calls the `json_tree` table-valued function on `this` binary JSON buffer, + /// optionally using [path] as the root path. + /// + /// See [JsonTableFunction] and [JsonExtensions.jsonTree] for more details. + JsonTableFunction jsonTree(DatabaseConnectionUser database, [String? path]) { + return dartCast().jsonTree(database, path); + } +} + /// Calls [json table-valued functions](https://sqlite.org/json1.html#jeach) in /// drift. /// diff --git a/drift/test/extensions/json1_integration_test.dart b/drift/test/extensions/json1_integration_test.dart index fcb232ad..dbb0dbe5 100644 --- a/drift/test/extensions/json1_integration_test.dart +++ b/drift/test/extensions/json1_integration_test.dart @@ -70,9 +70,38 @@ void main() { expect(rows, [ (DriftAny('bar'), 0), - (DriftAny('one'), 4), - (DriftAny('two'), 4), - (DriftAny('three'), 4), + (DriftAny('one'), 10), + (DriftAny('two'), 10), + (DriftAny('three'), 10), ]); }); + + group('jsonb', () { + setUp(() async { + await db.categories + .insertOne(CategoriesCompanion.insert(description: '_')); + }); + + Expression jsonb(Object? dart) { + return Variable(json.encode(dart)).jsonb(); + } + + Future eval(Expression expr) { + final query = db.selectOnly(db.categories)..addColumns([expr]); + return query.getSingle().then((row) => row.read(expr)); + } + + test('json', () async { + expect(await eval(jsonb([1, 2, 3]).json()), '[1,2,3]'); + }); + + test('jsonArrayLength', () async { + expect(await eval(jsonb([1, 2, 3]).jsonArrayLength()), 3); + }); + + test('jsonExtract', () async { + expect( + await eval(jsonb(jsonObject).jsonExtract(r'$.foo')), 'bar'); + }); + }); } diff --git a/drift/test/extensions/json1_test.dart b/drift/test/extensions/json1_test.dart index 0ac69230..a581cd27 100644 --- a/drift/test/extensions/json1_test.dart +++ b/drift/test/extensions/json1_test.dart @@ -2,12 +2,14 @@ import 'package:drift/drift.dart'; import 'package:drift/extensions/json1.dart'; import 'package:test/test.dart'; +import '../generated/todos.dart'; import '../test_utils/test_utils.dart'; void main() { - test('json1 functions generate valid sql', () { - const column = CustomExpression('col'); + const column = CustomExpression('col'); + const binary = CustomExpression('bin'); + test('json1 functions generate valid sql', () { expect(column.jsonArrayLength(), generates('json_array_length(col)')); expect( column.jsonArrayLength(r'$.c'), @@ -19,4 +21,70 @@ void main() { generates('json_extract(col, ?)', [r'$.c']), ); }); + + group('textual', () { + test('json', () { + expect(column.json(), generates('json(col)')); + }); + + test('jsonb', () { + expect(column.jsonb(), generates('jsonb(col)')); + }); + + test('jsonArrayLength', () { + expect(column.jsonArrayLength(), generates('json_array_length(col)')); + }); + + test('jsonExtract', () { + expect(column.jsonExtract(r'$.c'), + generates(r'json_extract(col, ?)', [r'$.c'])); + }); + + test('jsonEach', () async { + final db = TodoDb(); + addTearDown(db.close); + + final query = db.select(Variable.withString('{}').jsonEach(db)); + expect(query, generates('SELECT * FROM json_each(?)', [anything])); + }); + + test('jsonTree', () async { + final db = TodoDb(); + addTearDown(db.close); + + final query = db.select(Variable.withString('{}').jsonTree(db)); + expect(query, generates('SELECT * FROM json_tree(?)', [anything])); + }); + }); + + group('binary', () { + test('json', () { + expect(column.jsonb().json(), generates('json(jsonb(col))')); + }); + + test('jsonArrayLength', () { + expect(binary.jsonArrayLength(), generates('json_array_length(bin)')); + }); + + test('jsonExtract', () { + expect(binary.jsonExtract(r'$.c'), + generates(r'json_extract(bin, ?)', [r'$.c'])); + }); + + test('jsonEach', () async { + final db = TodoDb(); + addTearDown(db.close); + + final query = db.select(Variable.withBlob(Uint8List(0)).jsonEach(db)); + expect(query, generates('SELECT * FROM json_each(?)', [anything])); + }); + + test('jsonTree', () async { + final db = TodoDb(); + addTearDown(db.close); + + final query = db.select(Variable.withBlob(Uint8List(0)).jsonTree(db)); + expect(query, generates('SELECT * FROM json_tree(?)', [anything])); + }); + }); } diff --git a/drift/tool/download_sqlite3.dart b/drift/tool/download_sqlite3.dart index 80cb354e..823b1a19 100644 --- a/drift/tool/download_sqlite3.dart +++ b/drift/tool/download_sqlite3.dart @@ -4,8 +4,8 @@ import 'package:archive/archive_io.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as p; -const _version = '3410000'; -const _year = '2023'; +const _version = '3450000'; +const _year = '2024'; const _url = 'https://www.sqlite.org/$_year/sqlite-autoconf-$_version.tar.gz'; Future main(List args) async {