mirror of https://github.com/AMT-Cheif/drift.git
Support table-valued functions
This commit is contained in:
parent
4c2841d1df
commit
84659e06fd
|
@ -0,0 +1,80 @@
|
||||||
|
// #docregion existing
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/extensions/json1.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
// #enddocregion existing
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
|
||||||
|
part 'json.g.dart';
|
||||||
|
|
||||||
|
// #docregion existing
|
||||||
|
@JsonSerializable()
|
||||||
|
class ContactData {
|
||||||
|
final String name;
|
||||||
|
final List<String> phoneNumbers;
|
||||||
|
|
||||||
|
ContactData(this.name, this.phoneNumbers);
|
||||||
|
|
||||||
|
factory ContactData.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ContactDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, Object?> toJson() => _$ContactDataToJson(this);
|
||||||
|
}
|
||||||
|
// #enddocregion existing
|
||||||
|
|
||||||
|
// #docregion contacts
|
||||||
|
class _ContactsConverter extends TypeConverter<ContactData, String> {
|
||||||
|
@override
|
||||||
|
ContactData fromSql(String fromDb) {
|
||||||
|
return ContactData.fromJson(json.decode(fromDb) as Map<String, Object?>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(ContactData value) {
|
||||||
|
return json.encode(value.toJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Contacts extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get data => text().map(_ContactsConverter())();
|
||||||
|
|
||||||
|
TextColumn get name => text().generatedAs(data.jsonExtract(r'$.name'))();
|
||||||
|
}
|
||||||
|
// #enddocregion contacts
|
||||||
|
|
||||||
|
// #docregion calls
|
||||||
|
class Calls extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
BoolColumn get incoming => boolean()();
|
||||||
|
TextColumn get phoneNumber => text()();
|
||||||
|
DateTimeColumn get callTime => dateTime()();
|
||||||
|
}
|
||||||
|
// #enddocregion calls
|
||||||
|
|
||||||
|
@DriftDatabase(tables: [Contacts, Calls])
|
||||||
|
class MyDatabase extends _$MyDatabase {
|
||||||
|
MyDatabase() : super(NativeDatabase.memory());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
// #docregion calls-with-contacts
|
||||||
|
Future<List<(Call, Contact)>> callsWithContact() async {
|
||||||
|
final phoneNumbersForContact =
|
||||||
|
contacts.data.jsonEach(this, r'$.phoneNumbers');
|
||||||
|
final phoneNumberQuery = selectOnly(phoneNumbersForContact)
|
||||||
|
..addColumns([phoneNumbersForContact.value]);
|
||||||
|
|
||||||
|
final query = select(calls).join(
|
||||||
|
[innerJoin(contacts, calls.phoneNumber.isInQuery(phoneNumberQuery))]);
|
||||||
|
|
||||||
|
return query
|
||||||
|
.map((row) => (row.readTable(calls), row.readTable(contacts)))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
// #enddocregion calls-with-contacts
|
||||||
|
}
|
|
@ -217,3 +217,37 @@ joining this select statement onto a larger one grouping by category:
|
||||||
{% include "blocks/snippet" snippets = snippets name = 'subquery' %}
|
{% include "blocks/snippet" snippets = snippets name = 'subquery' %}
|
||||||
|
|
||||||
Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement.
|
Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement.
|
||||||
|
|
||||||
|
## JSON support
|
||||||
|
|
||||||
|
{% assign json_snippet = 'package:drift_docs/snippets/queries/json.dart.excerpt.json' | readString | json_decode %}
|
||||||
|
|
||||||
|
sqlite3 has great support for [JSON operators](https://sqlite.org/json1.html) that are also available
|
||||||
|
in drift (under the additional `'package:drift/extensions/json1.dart'` import).
|
||||||
|
JSON support is helpful when storing a dynamic structure that is best represented with JSON, or when
|
||||||
|
you have an existing structure (perhaps because you're migrating from a document-based storage)
|
||||||
|
that you need to support.
|
||||||
|
|
||||||
|
As an example, consider a contact book application that started with a JSON structure to store
|
||||||
|
contacts:
|
||||||
|
|
||||||
|
{% include "blocks/snippet" snippets = json_snippet name = 'existing' %}
|
||||||
|
|
||||||
|
To easily store this contact representation in a drift database, one could use a JSON column:
|
||||||
|
|
||||||
|
{% include "blocks/snippet" snippets = json_snippet name = 'contacts' %}
|
||||||
|
|
||||||
|
Note the `name` column as well: It uses `generatedAs` with the `jsonExtract` function to
|
||||||
|
extract the `name` field from the JSON value on the fly.
|
||||||
|
The full syntax for JSON path arguments is explained on the [sqlite3 website](https://sqlite.org/json1.html#path_arguments).
|
||||||
|
|
||||||
|
To make the example more complex, let's look at another table storing a log of phone calls:
|
||||||
|
|
||||||
|
{% include "blocks/snippet" snippets = json_snippet name = 'calls' %}
|
||||||
|
|
||||||
|
Let's say we wanted to find the contact for each call, if there is any with a matching phone number.
|
||||||
|
For this to be expressible in SQL, each `contacts` row would somehow have to be expanded into a row
|
||||||
|
for each stored phone number.
|
||||||
|
Luckily, the `json_each` function in sqlite3 can do exactly that, and drift exposes it:
|
||||||
|
|
||||||
|
{% include "blocks/snippet" snippets = json_snippet name = 'calls-with-contacts' %}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
## 2.12.0-dev
|
||||||
|
|
||||||
|
- Add support for table-valued functions in the Dart query builder.
|
||||||
|
- Support `json_each` and `json_tree`.
|
||||||
|
|
||||||
## 2.11.1
|
## 2.11.1
|
||||||
|
|
||||||
- Allow using `.read()` for a column added to a join from the table, fixing a
|
- Allow using `.read()` for a column added to a join from the table, fixing a
|
||||||
|
|
|
@ -53,4 +53,119 @@ extension JsonExtensions on Expression<String> {
|
||||||
Variable.withString(path),
|
Variable.withString(path),
|
||||||
]).dartCast<T>();
|
]).dartCast<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls the `json_each` table-valued function on `this` string, optionally
|
||||||
|
/// using [path] as the root path.
|
||||||
|
///
|
||||||
|
/// This can be used to join every element in a JSON structure to a drift
|
||||||
|
/// query.
|
||||||
|
///
|
||||||
|
/// See also: The [sqlite3 documentation](https://sqlite.org/json1.html#jeach)
|
||||||
|
/// and [JsonTableFunction].
|
||||||
|
JsonTableFunction jsonEach(DatabaseConnectionUser database, [String? path]) {
|
||||||
|
return JsonTableFunction._(database, functionName: 'json_each', arguments: [
|
||||||
|
this,
|
||||||
|
if (path != null) Variable(path),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls the `json_tree` table-valued function on `this` string, optionally
|
||||||
|
/// using [path] as the root path.
|
||||||
|
///
|
||||||
|
/// This can be used to join every element in a JSON structure to a drift
|
||||||
|
/// query.
|
||||||
|
///
|
||||||
|
/// See also: The [sqlite3 documentation](https://sqlite.org/json1.html#jeach)
|
||||||
|
/// and [JsonTableFunction].
|
||||||
|
JsonTableFunction jsonTree(DatabaseConnectionUser database, [String? path]) {
|
||||||
|
return JsonTableFunction._(database, functionName: 'json_tree', arguments: [
|
||||||
|
this,
|
||||||
|
if (path != null) Variable(path),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls [json table-valued functions](https://sqlite.org/json1.html#jeach) in
|
||||||
|
/// drift.
|
||||||
|
///
|
||||||
|
/// With [JsonExtensions.jsonEach] and [JsonExtensions.jsonTree], a JSON value
|
||||||
|
/// can be used a table-like structure available in queries and joins.
|
||||||
|
///
|
||||||
|
/// For an example and more details, see the [drift documentation](https://drift.simonbinder.eu/docs/advanced-features/joins/#json-support)
|
||||||
|
final class JsonTableFunction extends TableValuedFunction<JsonTableFunction> {
|
||||||
|
JsonTableFunction._(
|
||||||
|
super.attachedDatabase, {
|
||||||
|
required super.functionName,
|
||||||
|
required super.arguments,
|
||||||
|
super.alias,
|
||||||
|
}) : super(
|
||||||
|
columns: [
|
||||||
|
GeneratedColumn<DriftAny>('key', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.any),
|
||||||
|
GeneratedColumn<DriftAny>('value', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.any),
|
||||||
|
GeneratedColumn<String>('type', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.string),
|
||||||
|
GeneratedColumn<DriftAny>('atom', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.any),
|
||||||
|
GeneratedColumn<int>('id', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.int),
|
||||||
|
GeneratedColumn<int>('parent', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.int),
|
||||||
|
GeneratedColumn<String>('fullkey', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.string),
|
||||||
|
GeneratedColumn<String>('path', alias ?? functionName, true,
|
||||||
|
type: DriftSqlType.string),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Expression<T> _col<T extends Object>(String name) {
|
||||||
|
return columnsByName[name]! as Expression<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JSON key under which this element can be found in its parent, or
|
||||||
|
/// `null` if this is the root element.
|
||||||
|
///
|
||||||
|
/// Child elements of objects have a string key, elements in arrays are
|
||||||
|
/// represented by their index.
|
||||||
|
Expression<DriftAny> get key => _col('key');
|
||||||
|
|
||||||
|
/// The value for the current value.
|
||||||
|
///
|
||||||
|
/// Scalar values are returned directly, objects and arrays are returned as
|
||||||
|
/// JSON strings.
|
||||||
|
Expression<DriftAny> get value => _col('value');
|
||||||
|
|
||||||
|
/// The result of calling [`sqlite3_type`](https://sqlite.org/json1.html#the_json_type_function)
|
||||||
|
/// on this JSON element.
|
||||||
|
Expression<String> get type => _col('type');
|
||||||
|
|
||||||
|
/// The [value], or `null` if this is not a scalar value (so either an object
|
||||||
|
/// or an array).
|
||||||
|
Expression<DriftAny> get atom => _col('atom');
|
||||||
|
|
||||||
|
/// An id uniquely identifying this element in the original JSON tree.
|
||||||
|
Expression<int> get id => _col('id');
|
||||||
|
|
||||||
|
/// The [id] of the parent of this element.
|
||||||
|
Expression<int> get parent => _col('parent');
|
||||||
|
|
||||||
|
/// The JSON key that can be passed to functions like
|
||||||
|
/// [JsonExtensions.jsonExtract] to find this value.
|
||||||
|
Expression<String> get fullKey => _col('fullkey');
|
||||||
|
|
||||||
|
/// Similar to [fullKey], but relative to the `root` argument passed to
|
||||||
|
/// [JsonExtensions.jsonEach] or [JsonExtensions.jsonTree].
|
||||||
|
Expression<String> get path => _col('path');
|
||||||
|
|
||||||
|
@override
|
||||||
|
ResultSetImplementation<JsonTableFunction, TypedResult> createAlias(
|
||||||
|
String alias) {
|
||||||
|
return JsonTableFunction._(
|
||||||
|
attachedDatabase,
|
||||||
|
functionName: entityName,
|
||||||
|
arguments: arguments,
|
||||||
|
alias: alias,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,15 +121,6 @@ class Subquery<Row> extends ResultSetImplementation<Subquery, Row>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix}) {
|
FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
if (tablePrefix == null) {
|
return select._mapRow(data.withoutPrefix(tablePrefix));
|
||||||
return select._mapRow(data);
|
|
||||||
} else {
|
|
||||||
final withoutPrefix = {
|
|
||||||
for (final MapEntry(:key, :value) in columnsByName.entries)
|
|
||||||
key: data['$tablePrefix.$value']
|
|
||||||
};
|
|
||||||
|
|
||||||
return select._mapRow(withoutPrefix);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../../../dsl/dsl.dart';
|
||||||
|
import '../../api/runtime_api.dart';
|
||||||
|
import '../../utils.dart';
|
||||||
|
import '../query_builder.dart';
|
||||||
|
|
||||||
|
/// In sqlite3, a table-valued function is a function that resolves to a result
|
||||||
|
/// set, meaning that it can be selected from.
|
||||||
|
///
|
||||||
|
/// For more information on table-valued functions in general, visit their
|
||||||
|
/// [documentation](https://sqlite.org/vtab.html#tabfunc2) on the sqlite website.
|
||||||
|
///
|
||||||
|
/// This class is meant to be extended for each table-valued function, so that
|
||||||
|
/// the [Self] type parameter points to the actual implementation class. The
|
||||||
|
/// class must also implement [createAlias] correctly (ensuring that every
|
||||||
|
/// column has its [GeneratedColumn.tableName] set to the [aliasedName]).
|
||||||
|
///
|
||||||
|
/// For an example of a table-valued function in drift, see the
|
||||||
|
/// `JsonTableFunction` in `package:drift/json1.dart`. It makes the `json_each`
|
||||||
|
/// and `json_tree` table-valued functions available to drift.
|
||||||
|
@experimental
|
||||||
|
abstract base class TableValuedFunction<Self extends ResultSetImplementation>
|
||||||
|
extends ResultSetImplementation<Self, TypedResult>
|
||||||
|
implements HasResultSet, Component {
|
||||||
|
final String _functionName;
|
||||||
|
|
||||||
|
/// The arguments passed to the table-valued function.
|
||||||
|
final List<Expression> arguments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DatabaseConnectionUser attachedDatabase;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final List<GeneratedColumn<Object>> $columns;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String aliasedName;
|
||||||
|
|
||||||
|
/// Constructor for table-valued functions.
|
||||||
|
///
|
||||||
|
/// This takes the [attachedDatabase] (used to interpret results), the name
|
||||||
|
/// of the function as well as arguments passed to it and finally the schema
|
||||||
|
/// of the table (in the form of [columns]).
|
||||||
|
TableValuedFunction(
|
||||||
|
this.attachedDatabase, {
|
||||||
|
required String functionName,
|
||||||
|
required this.arguments,
|
||||||
|
required List<GeneratedColumn> columns,
|
||||||
|
String? alias,
|
||||||
|
}) : _functionName = functionName,
|
||||||
|
$columns = columns,
|
||||||
|
aliasedName = alias ?? functionName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Self get asDslTable => this as Self;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final Map<String, GeneratedColumn<Object>> columnsByName = {
|
||||||
|
for (final column in $columns) column.name: column,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get entityName => _functionName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<TypedResult> map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final row = QueryRow(data.withoutPrefix(tablePrefix), attachedDatabase);
|
||||||
|
return TypedResult(
|
||||||
|
const {},
|
||||||
|
row,
|
||||||
|
{
|
||||||
|
for (final column in $columns)
|
||||||
|
column: attachedDatabase.typeMapping
|
||||||
|
.read(column.type, row.data[column.name]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void writeInto(GenerationContext context) {
|
||||||
|
context.buffer
|
||||||
|
..write(_functionName)
|
||||||
|
..write('(');
|
||||||
|
|
||||||
|
var first = true;
|
||||||
|
for (final argument in arguments) {
|
||||||
|
if (!first) {
|
||||||
|
context.buffer.write(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
argument.writeInto(context);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.buffer.write(')');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,31 @@
|
||||||
@internal
|
@internal
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'query_builder.dart';
|
||||||
|
|
||||||
/// Internal utilities for building queries that aren't exported.
|
/// Internal utilities for building queries that aren't exported.
|
||||||
extension WriteDefinition on GenerationContext {
|
extension WriteDefinition on GenerationContext {
|
||||||
/// Writes the result set to this context, suitable to implement `FROM`
|
/// Writes the result set to this context, suitable to implement `FROM`
|
||||||
/// clauses and joins.
|
/// clauses and joins.
|
||||||
void writeResultSet(ResultSetImplementation resultSet) {
|
void writeResultSet(ResultSetImplementation resultSet) {
|
||||||
if (resultSet is Subquery) {
|
switch (resultSet) {
|
||||||
buffer.write('(');
|
case Subquery(:final select):
|
||||||
resultSet.select.writeInto(this);
|
buffer.write('(');
|
||||||
buffer
|
select.writeInto(this);
|
||||||
..write(') ')
|
buffer
|
||||||
..write(resultSet.aliasedName);
|
..write(') ')
|
||||||
} else {
|
..write(resultSet.aliasedName);
|
||||||
buffer.write(resultSet.tableWithAlias);
|
case TableValuedFunction():
|
||||||
watchedTables.add(resultSet);
|
resultSet.writeInto(this);
|
||||||
|
|
||||||
|
if (resultSet.aliasedName != resultSet.entityName) {
|
||||||
|
buffer.write(' ${resultSet.aliasedName}');
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
buffer.write(resultSet.tableWithAlias);
|
||||||
|
watchedTables.add(resultSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import 'package:drift/src/utils/single_transformer.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../../utils/async.dart';
|
import '../../utils/async.dart';
|
||||||
|
import '../utils.dart';
|
||||||
// New files should not be part of this mega library, which we're trying to
|
// New files should not be part of this mega library, which we're trying to
|
||||||
// split up.
|
// split up.
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ import 'expressions/case_when.dart';
|
||||||
import 'expressions/internal.dart';
|
import 'expressions/internal.dart';
|
||||||
import 'helpers.dart';
|
import 'helpers.dart';
|
||||||
|
|
||||||
|
export 'components/table_valued_function.dart';
|
||||||
export 'expressions/bitwise.dart';
|
export 'expressions/bitwise.dart';
|
||||||
export 'expressions/case_when.dart';
|
export 'expressions/case_when.dart';
|
||||||
export 'on_table.dart';
|
export 'on_table.dart';
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/// Provides [withoutPrefix], a method useful to interpret SQL results.
|
||||||
|
extension RemovePrefix on Map<String, Object?> {
|
||||||
|
/// Returns a map of all keys starting with the table prefix, but with the
|
||||||
|
/// prefix removed.
|
||||||
|
Map<String, Object?> withoutPrefix(String? tablePrefix) {
|
||||||
|
if (tablePrefix != null) {
|
||||||
|
final actualPrefix = '$tablePrefix.';
|
||||||
|
final prefixLength = actualPrefix.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
for (final MapEntry(:key, :value) in entries)
|
||||||
|
if (key.startsWith(actualPrefix)) key.substring(prefixLength): value,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart' hide isNull;
|
import 'package:drift/drift.dart' hide isNull;
|
||||||
|
import 'package:drift/extensions/json1.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
@ -240,4 +241,19 @@ void main() {
|
||||||
|
|
||||||
verify(executor.runSelect('SELECT * FROM (SELECT * FROM "todos") s;', []));
|
verify(executor.runSelect('SELECT * FROM (SELECT * FROM "todos") s;', []));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('select from table-valued function', () async {
|
||||||
|
final each = db.todosTable.content.jsonEach(db, r'$.foo');
|
||||||
|
|
||||||
|
final query = db
|
||||||
|
.select(db.todosTable)
|
||||||
|
.join([innerJoin(each, each.atom.isNotNull(), useColumns: false)]);
|
||||||
|
|
||||||
|
await query.get();
|
||||||
|
|
||||||
|
verify(executor.runSelect(
|
||||||
|
'SELECT "todos"."id" AS "todos.id", "todos"."title" AS "todos.title", "todos"."content" AS "todos.content", "todos"."target_date" AS "todos.target_date", "todos"."category" AS "todos.category", "todos"."status" AS "todos.status" FROM "todos" INNER JOIN json_each("todos"."content", ?) ON "json_each"."atom" IS NOT NULL;',
|
||||||
|
[r'$.foo'],
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@Tags(['integration'])
|
@Tags(['integration'])
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart' hide isNull;
|
||||||
import 'package:drift/extensions/json1.dart';
|
import 'package:drift/extensions/json1.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
@ -9,16 +9,21 @@ import '../generated/todos.dart';
|
||||||
import '../test_utils/test_utils.dart';
|
import '../test_utils/test_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
const jsonObject = {
|
||||||
|
'foo': 'bar',
|
||||||
|
'array': [
|
||||||
|
'one',
|
||||||
|
'two',
|
||||||
|
'three',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
late TodoDb db;
|
||||||
|
|
||||||
|
setUp(() => db = TodoDb(testInMemoryDatabase()));
|
||||||
|
tearDown(() => db.close());
|
||||||
|
|
||||||
test('json1 integration test', () async {
|
test('json1 integration test', () async {
|
||||||
final db = TodoDb(testInMemoryDatabase());
|
|
||||||
const jsonObject = {
|
|
||||||
'foo': 'bar',
|
|
||||||
'array': [
|
|
||||||
'one',
|
|
||||||
'two',
|
|
||||||
'three',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await db.into(db.pureDefaults).insert(PureDefaultsCompanion(
|
await db.into(db.pureDefaults).insert(PureDefaultsCompanion(
|
||||||
txt: Value(MyCustomObject(json.encode(jsonObject)))));
|
txt: Value(MyCustomObject(json.encode(jsonObject)))));
|
||||||
|
|
||||||
|
@ -30,5 +35,44 @@ void main() {
|
||||||
|
|
||||||
final resultRow = await query.getSingle();
|
final resultRow = await query.getSingle();
|
||||||
expect(resultRow.read(arrayLengthExpr), 3);
|
expect(resultRow.read(arrayLengthExpr), 3);
|
||||||
}, tags: const ['integration']);
|
});
|
||||||
|
|
||||||
|
test('json_each', () async {
|
||||||
|
final function = Variable<String>(json.encode(jsonObject)).jsonEach(db);
|
||||||
|
final rows = await db.select(function).get();
|
||||||
|
|
||||||
|
expect(rows, hasLength(2));
|
||||||
|
|
||||||
|
expect(rows[0].read(function.key), DriftAny('foo'));
|
||||||
|
expect(rows[0].read(function.value), DriftAny('bar'));
|
||||||
|
expect(rows[0].read(function.type), 'text');
|
||||||
|
expect(rows[0].read(function.atom), DriftAny('bar'));
|
||||||
|
expect(rows[0].read(function.id), 2);
|
||||||
|
expect(rows[0].read(function.parent), isNull);
|
||||||
|
expect(rows[0].read(function.fullKey), r'$.foo');
|
||||||
|
expect(rows[0].read(function.path), r'$');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('json_tree', () async {
|
||||||
|
// Make sure we can use aliases as well
|
||||||
|
final function = Variable<String>(json.encode(jsonObject)).jsonTree(db);
|
||||||
|
final parent = db.alias(function, 'parent');
|
||||||
|
|
||||||
|
final query = db
|
||||||
|
.selectOnly(function)
|
||||||
|
.join([leftOuterJoin(parent, parent.id.equalsExp(function.parent))])
|
||||||
|
..addColumns([function.atom, parent.id])
|
||||||
|
..where(function.atom.isNotNull());
|
||||||
|
|
||||||
|
final rows = await query
|
||||||
|
.map((row) => (row.read(function.atom), row.read(parent.id)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
expect(rows, [
|
||||||
|
(DriftAny('bar'), 0),
|
||||||
|
(DriftAny('one'), 4),
|
||||||
|
(DriftAny('two'), 4),
|
||||||
|
(DriftAny('three'), 4),
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue