Support for REGEXP (#410)

This commit is contained in:
Simon Binder 2020-03-04 13:48:25 +01:00
parent 84149d7e15
commit c8f4f739e9
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
12 changed files with 138 additions and 12 deletions

View File

@ -130,6 +130,12 @@ use the `filter` parameter.
To count all rows (instead of a single value), you can use the top-level `countAll()`.
## Mathematical functions and regexp
When using `moor_ffi`, a basic set of trigonometric functions will be available.
It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries.
For more information, see the [list of functions]({{< relref "../Other engines/vm.md#moor-only-functions" >}}) here.
## Custom expressions
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
It takes a `sql` parameter that let's you write custom expressions:

View File

@ -132,7 +132,9 @@ For more details on sqlite compile options, see [their documentation](https://ww
- `pow(base, exponent)` and `power(base, exponent)`: This function takes two numerical arguments and returns `base` raised to the power of `exponent`.
If `base` or `exponent` aren't numerical values or null, this function will return `null`. This function behaves exactly like `pow` in `dart:math`.
- `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`: These functions take a single argument. If that argument is null or not a numerical value,
returns null. Otherwise, returns the result of applying the matching function in `dart:math`:
returns null. Otherwise, returns the result of applying the matching function in `dart:math`.
- `regexp`: Wraps the Dart `RegExp` apis, so that `foo REGEXP bar` is equivalent to `RegExp(bar).hasMatch(foo)`. Note that we have to create a new
`RegExp` instance for each `regexp` sql call, which can impact performance on large queries.
Note that `NaN`, `-infinity` or `+infinity` are represented as `NULL` in sql.

View File

@ -9,6 +9,14 @@ extension StringExpressionOperators on Expression<String> {
return _LikeOperator(this, Variable.withString(regex));
}
/// Matches this string against the regular expression in [regex].
///
/// Note that this function is only available when using `moor_ffi`. If
/// possible, consider using [like] instead.
Expression<bool> regexp(String regex) {
return _LikeOperator(this, Variable.withString(regex), operator: 'REGEXP');
}
/// Uses the given [collate] sequence when comparing this column to other
/// values.
Expression<String> collate(Collate collate) {
@ -59,27 +67,34 @@ class _LikeOperator extends Expression<bool> {
/// The regex-like expression to test the [target] against.
final Expression<String> regex;
/// The operator to use when matching. Defaults to `LIKE`.
final String operator;
@override
final Precedence precedence = Precedence.comparisonEq;
/// Perform a like operator with the target and the regex.
_LikeOperator(this.target, this.regex);
_LikeOperator(this.target, this.regex, {this.operator = 'LIKE'});
@override
void writeInto(GenerationContext context) {
writeInner(context, target);
context.buffer.write(' LIKE ');
context.writeWhitespace();
context.buffer.write(operator);
context.writeWhitespace();
writeInner(context, regex);
}
@override
int get hashCode => $mrjf($mrjc(target.hashCode, regex.hashCode));
int get hashCode =>
$mrjf($mrjc(target.hashCode, $mrjc(regex.hashCode, operator.hashCode)));
@override
bool operator ==(dynamic other) {
return other is _LikeOperator &&
other.target == target &&
other.regex == regex;
other.regex == regex &&
other.operator == operator;
}
}

View File

@ -41,7 +41,8 @@ class _GeneratesSqlMatcher extends Matcher {
if (matchState.containsKey('vars')) {
final vars = matchState['vars'] as List;
mismatchDescription = mismatchDescription.add('generated $vars, which ');
mismatchDescription =
mismatchDescription.add('used variables $vars, which ');
mismatchDescription = _matchVariables.describeMismatch(
vars, mismatchDescription, matchState, verbose);
}

View File

@ -16,6 +16,13 @@ void main() {
expect(ctx.boundVariables, ['pattern']);
});
test('generates regexp expressions', () {
expect(
expression.regexp('fo+'),
generates('col REGEXP ?', ['fo+']),
);
});
test('generates collate expressions', () {
final ctx = GenerationContext.fromDb(db);
expression.collate(Collate.noCase).writeInto(ctx);

View File

@ -111,6 +111,8 @@ class _SQLiteBindings {
void Function(Pointer<FunctionContext> ctx, int value) sqlite3_result_int64;
void Function(Pointer<FunctionContext> ctx, double value)
sqlite3_result_double;
void Function(Pointer<FunctionContext> ctx, Pointer<CBlob> msg, int length)
sqlite3_result_error;
_SQLiteBindings() {
sqlite = open.openSqlite();
@ -238,6 +240,10 @@ class _SQLiteBindings {
.lookup<NativeFunction<sqlite3_result_double_native>>(
'sqlite3_result_double')
.asFunction();
sqlite3_result_error = sqlite
.lookup<NativeFunction<sqlite3_result_error_native>>(
'sqlite3_result_error')
.asFunction();
}
}

View File

@ -126,3 +126,5 @@ typedef sqlite3_result_double_native = Void Function(
Pointer<FunctionContext> context, Double value);
typedef sqlite3_result_int64_native = Void Function(
Pointer<FunctionContext> context, Int64 value);
typedef sqlite3_result_error_native = Void Function(
Pointer<FunctionContext> context, Pointer<CBlob> char, Int32 len);

View File

@ -2,11 +2,13 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:ffi';
import 'package:moor/moor.dart';
import '../ffi/blob.dart';
import '../ffi/utils.dart';
import 'bindings.dart';
import 'constants.dart';
@ -120,4 +122,19 @@ extension SqliteFunctionContextPointer on Pointer<FunctionContext> {
throw AssertionError();
}
void resultBool(bool value) {
resultInt(value ? 1 : 0);
}
void resultError(String message) {
final encoded = Uint8List.fromList(utf8.encode(message));
final ptr = CBlob.allocate(encoded);
bindings.sqlite3_result_error(this, ptr, encoded.length);
// Note that sqlite3_result_error makes a private copy of error message
// before returning. Hence, we can deallocate the message here.
ptr.free();
}
}

View File

@ -219,18 +219,18 @@ class Database {
}
}
/// Enables non-standard mathematical functions that ship with `moor_ffi`.
/// Enables non-standard functions that ship with `moor_ffi`.
///
/// After calling [enableMathematicalFunctions], the following functions can
/// After calling [enableMoorFfiFunctions], the following functions can
/// be used in sql: `power`, `pow`, `sqrt`, `sin`, `cos`, `tan`, `asin`,
/// `acos`, `atan`.
/// `acos`, `atan` and `regexp`.
///
/// At the moment, these functions are only available in statements. In
/// particular, they're not available in triggers, check constraints, index
/// expressions.
///
/// This should only be called once per database.
void enableMathematicalFunctions() {
void enableMoorFfiFunctions() {
_registerOn(this);
}

View File

@ -67,6 +67,32 @@ void _atanImpl(Pointer<FunctionContext> ctx, int argCount,
_unaryNumFunction(ctx, argCount, args, atan);
}
void _regexpImpl(Pointer<FunctionContext> ctx, int argCount,
Pointer<Pointer<SqliteValue>> args) {
if (argCount != 2) {
ctx.resultError('Expected two arguments to regexp');
return;
}
final firstParam = args[0].value;
final secondParam = args[1].value;
if (firstParam is! String || secondParam is! String) {
ctx.resultError('Expected two strings as parameters to regexp');
return;
}
RegExp regex;
try {
regex = RegExp(firstParam as String);
} on FormatException catch (e) {
ctx.resultError('Invalid regex: $e');
return;
}
ctx.resultBool(regex.hasMatch(secondParam as String));
}
void _registerOn(Database db) {
final powImplPointer =
Pointer.fromFunction<sqlite3_function_handler>(_powImpl);
@ -88,4 +114,7 @@ void _registerOn(Database db) {
isDeterministic: true);
db.createFunction('atan', 1, Pointer.fromFunction(_atanImpl),
isDeterministic: true);
db.createFunction('regexp', 2, Pointer.fromFunction(_regexpImpl),
isDeterministic: true);
}

View File

@ -40,7 +40,7 @@ class _VmDelegate extends DatabaseDelegate {
} else {
_db = Database.memory();
}
_db.enableMathematicalFunctions();
_db.enableMoorFfiFunctions();
versionDelegate = _VmVersionDelegate(_db);
return Future.value();
}

View File

@ -6,7 +6,7 @@ import 'package:test/test.dart';
void main() {
Database db;
setUp(() => db = Database.memory()..enableMathematicalFunctions());
setUp(() => db = Database.memory()..enableMoorFfiFunctions());
tearDown(() => db.close());
group('pow', () {
@ -64,6 +64,47 @@ void main() {
expect(resultWithNull.single['r'], isNull);
});
}
group('regexp', () {
test('cannot be called with more or fewer than 2 parameters', () {
expect(() => db.execute("SELECT regexp('foo')"),
throwsA(isA<SqliteException>()));
expect(() => db.execute("SELECT regexp('foo', 'bar', 'baz')"),
throwsA(isA<SqliteException>()));
});
test('results in error when not passing a string', () {
final complainsAboutTypes = throwsA(isA<SqliteException>().having(
(e) => e.message,
'message',
contains('Expected two strings as parameters to regexp'),
));
expect(() => db.execute("SELECT 'foo' REGEXP 3"), complainsAboutTypes);
expect(() => db.execute("SELECT 3 REGEXP 'foo'"), complainsAboutTypes);
});
test('fails on invalid regex', () {
expect(
() => db.execute("SELECT 'foo' REGEXP '('"),
throwsA(isA<SqliteException>()
.having((e) => e.message, 'message', contains('Invalid regex'))),
);
});
test('returns true on a match', () {
final stmt = db.prepare("SELECT 'foo' REGEXP 'fo+' AS r");
final result = stmt.select();
expect(result.single['r'], 1);
});
test("returns false when the regex doesn't match", () {
final stmt = db.prepare("SELECT 'bar' REGEXP 'fo+' AS r");
final result = stmt.select();
expect(result.single['r'], 0);
});
});
}
// utils to verify the sql functions behave exactly like the ones from the VM