Support jsonb functions in query builder

This commit is contained in:
Simon Binder 2024-01-17 18:17:31 +01:00
parent c4169f6f91
commit 2a2990e362
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
5 changed files with 171 additions and 7 deletions

View File

@ -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

View File

@ -12,6 +12,22 @@ import '../drift.dart';
/// Defines extensions on string expressions to support the json1 api from Dart.
extension JsonExtensions on Expression<String> {
/// 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<String> 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<Uint8List> 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<String> {
}
}
/// 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<Uint8List> {
/// Reads this binary JSONB structure and emits its textual representation as
/// minified JSON.
///
/// For details, see https://www.sqlite.org/json1.html#jmini.
Expression<String> json() {
return dartCast<String>().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<int> jsonArrayLength([String? path]) {
// the function accepts both formats, and this way we avoid some duplicate
// code.
return dartCast<String>().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<T> jsonExtract<T extends Object>(String path) {
return dartCast<String>().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<String>().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<String>().jsonTree(database, path);
}
}
/// Calls [json table-valued functions](https://sqlite.org/json1.html#jeach) in
/// drift.
///

View File

@ -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<Uint8List> jsonb(Object? dart) {
return Variable(json.encode(dart)).jsonb();
}
Future<T?> eval<T extends Object>(Expression<T> 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<String>(r'$.foo')), 'bar');
});
});
}

View File

@ -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<String>('col');
const column = CustomExpression<String>('col');
const binary = CustomExpression<Uint8List>('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]));
});
});
}

View File

@ -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<void> main(List<String> args) async {