diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index f3e8a8e3..73b7abbd 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -9,7 +9,8 @@ return ... }).get(); ``` - +- Provide Dart apis for the json1 extension in the `package:moor/extensions/json1.dart` library. Note that + json1 is not supported on most platforms. - Batches are now always sent in a transaction, this used to be implementation specific before ## 2.1.0 diff --git a/moor/lib/extensions/json1.dart b/moor/lib/extensions/json1.dart new file mode 100644 index 00000000..62363916 --- /dev/null +++ b/moor/lib/extensions/json1.dart @@ -0,0 +1,57 @@ +/// Experimental bindings to the [json1](https://www.sqlite.org/json1.html) +/// sqlite extension. +/// +/// Note that the json1 extension might not be available on all runtimes. In +/// particular, it can only work reliably on Android when using the +/// [moor_ffi](https://moor.simonbinder.eu/docs/other-engines/vm/) and it might +/// not work on older iOS versions. +@experimental +library json1; + +import 'package:meta/meta.dart'; +import '../moor.dart'; + +/// Defines extensions on string expressions to support the json1 api from Dart. +extension JsonExtensions on Expression { + /// Assuming that this string is a json array, returns the length of this json + /// array. + /// + /// The [path] parameter is optional. If it's set, it must refer to a valid + /// path in this json that will be used instead of `this`. See the + /// [sqlite documentation](https://www.sqlite.org/json1.html#path_arguments) + /// for details. If [path] is an invalid path, this expression can cause an + /// error when run by sqlite. + /// + /// For this method to be valid, `this` must be a string representing a valid + /// json array. Otherwise, sqlite will report an error when attempting to + /// evaluate this expression. + /// + /// See also: + /// - the [sqlite documentation for this function](https://www.sqlite.org/json1.html#the_json_array_length_function) + Expression jsonArrayLength([String path]) { + return FunctionCallExpression('json_array_length', [ + this, + if (path != null) Variable.withString(path), + ]); + } + + /// Assuming that this string is a json object or array, extracts a part of + /// this structure identified by [path]. + /// + /// For more details on how to format the [path] argument, see the + /// [sqlite documentation](https://www.sqlite.org/json1.html#path_arguments). + /// + /// Evaluating this expression will cause an error if [path] has an invalid + /// format or `this` isn't well-formatted json. + /// + /// Note that, since the return type of this function is dynamic, it can't be + /// used in [JoinedSelectStatement.addColumns]. It's also recommended to use + /// [Expression.equalsExp] with an explicit [Variable] instead of + /// [Expression.equals]. + Expression jsonExtract(String path) { + return FunctionCallExpression('json_extract', [ + this, + Variable.withString(path), + ]); + } +} diff --git a/moor/lib/src/dsl/database.dart b/moor/lib/src/dsl/database.dart index c8d6713d..60714d51 100644 --- a/moor/lib/src/dsl/database.dart +++ b/moor/lib/src/dsl/database.dart @@ -47,10 +47,7 @@ class UseMoor { /// Defines the `.moor` files to include when building the table structure for /// this database. For details on how to integrate `.moor` files into your /// Dart code, see [the documentation](https://moor.simonbinder.eu/docs/using-sql/custom_tables/). - /// - /// Please note that this feature is experimental at the moment. /// {@endtemplate} - @experimental final Set include; /// Use this class as an annotation to inform moor_generator that a database @@ -59,7 +56,7 @@ class UseMoor { @required this.tables, this.daos = const [], this.queries = const {}, - @experimental this.include = const {}, + this.include = const {}, }); } @@ -92,7 +89,6 @@ class UseDao { final Map queries; /// {@macro moor_include_param} - @experimental final Set include; /// Annotation for a class to declare it as an dao. See [UseDao] and the @@ -100,5 +96,5 @@ class UseDao { const UseDao( {@required this.tables, this.queries = const {}, - @experimental this.include = const {}}); + this.include = const {}}); } diff --git a/moor/lib/src/runtime/query_builder/expressions/expression.dart b/moor/lib/src/runtime/query_builder/expressions/expression.dart index 0817b1a7..7795c776 100644 --- a/moor/lib/src/runtime/query_builder/expressions/expression.dart +++ b/moor/lib/src/runtime/query_builder/expressions/expression.dart @@ -313,15 +313,24 @@ class _CastExpression, S2 extends SqlType> } } -class _FunctionCallExpression> - extends Expression { +/// A sql expression that calls a function. +/// +/// This class is mainly used by moor internally. If you find yourself using +/// this class, consider [creating an issue](https://github.com/simolus3/moor/issues/new) +/// to request native support in moor. +class FunctionCallExpression> extends Expression { + /// The name of the function to call final String functionName; + + /// The arguments passed to the function, as expressions. final List arguments; @override final Precedence precedence = Precedence.primary; - _FunctionCallExpression(this.functionName, this.arguments); + /// Constructs a function call expression in sql from the [functionName] and + /// the target [arguments]. + FunctionCallExpression(this.functionName, this.arguments); @override void writeInto(GenerationContext context) { @@ -345,7 +354,7 @@ class _FunctionCallExpression> @override bool operator ==(other) { - return other is _FunctionCallExpression && + return other is FunctionCallExpression && other.functionName == functionName && _equality.equals(other.arguments, arguments); } diff --git a/moor/lib/src/runtime/query_builder/expressions/text.dart b/moor/lib/src/runtime/query_builder/expressions/text.dart index 8533a5ac..a4e65ae7 100644 --- a/moor/lib/src/runtime/query_builder/expressions/text.dart +++ b/moor/lib/src/runtime/query_builder/expressions/text.dart @@ -28,7 +28,7 @@ extension StringExpressionOperators on Expression { /// See also: /// - https://www.w3resource.com/sqlite/core-functions-upper.php Expression upper() { - return _FunctionCallExpression('UPPER', [this]); + return FunctionCallExpression('UPPER', [this]); } /// Calls the sqlite function `LOWER` on `this` string. Please note that, in @@ -37,7 +37,7 @@ extension StringExpressionOperators on Expression { /// See also: /// - https://www.w3resource.com/sqlite/core-functions-lower.php Expression lower() { - return _FunctionCallExpression('LOWER', [this]); + return FunctionCallExpression('LOWER', [this]); } /// Calls the sqlite function `LENGTH` on `this` string, which counts the @@ -47,7 +47,7 @@ extension StringExpressionOperators on Expression { /// See also: /// - https://www.w3resource.com/sqlite/core-functions-length.php Expression get length { - return _FunctionCallExpression('LENGTH', [this]); + return FunctionCallExpression('LENGTH', [this]); } } diff --git a/moor/test/data/utils/expect_generated.dart b/moor/test/data/utils/expect_generated.dart index d59ed692..1fc4b7e0 100644 --- a/moor/test/data/utils/expect_generated.dart +++ b/moor/test/data/utils/expect_generated.dart @@ -2,10 +2,14 @@ import 'package:moor/moor.dart'; import 'package:test/test.dart'; extension ComponentExpectations on Component { - void expectGenerates(String sql) { + void expectGenerates(String sql, [List variables]) { final ctx = GenerationContext(SqlTypeSystem.defaultInstance, null); writeInto(ctx); expect(ctx.sql, sql); + + if (variables != null) { + expect(ctx.boundVariables, variables); + } } } diff --git a/moor/test/extensions/json1_test.dart b/moor/test/extensions/json1_test.dart new file mode 100644 index 00000000..afda7d5d --- /dev/null +++ b/moor/test/extensions/json1_test.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:moor/moor.dart'; +import 'package:moor/extensions/json1.dart'; +import 'package:moor_ffi/moor_ffi.dart'; +import 'package:test/test.dart'; + +import '../data/tables/todos.dart'; +import '../data/utils/expect_generated.dart'; + +void main() { + test('json1 functions generate valid sql', () { + final column = GeneratedTextColumn('col', 'tbl', false); + + column.jsonArrayLength().expectGenerates('json_array_length(col)'); + column + .jsonArrayLength(r'$.c') + .expectGenerates('json_array_length(col, ?)', [r'$.c']); + + column + .jsonExtract(r'$.c') + .expectGenerates('json_extract(col, ?)', [r'$.c']); + }); + + test('json1 integration test', () async { + final db = TodoDb(VmDatabase.memory()); + const jsonObject = { + 'foo': 'bar', + 'array': [ + 'one', + 'two', + 'three', + ], + }; + await db + .into(db.pureDefaults) + .insert(PureDefaultsCompanion(txt: Value(json.encode(jsonObject)))); + + final arrayLengthExpr = db.pureDefaults.txt.jsonArrayLength(r'$.array'); + final query = db.select(db.pureDefaults).addColumns([arrayLengthExpr]); + query.where(db.pureDefaults.txt + .jsonExtract(r'$.foo') + .equalsExp(Variable.withString('bar'))); + + final resultRow = await query.getSingle(); + expect(resultRow.read(arrayLengthExpr), 3); + }); +}