mirror of https://github.com/AMT-Cheif/drift.git
Enable mathematical functions in moor_ffi (#397)
This commit is contained in:
parent
1832b59848
commit
fa5411fb5d
|
@ -123,4 +123,15 @@ moor. Important options are marked in bold.
|
|||
- `SQLITE_ENABLE_FTS5`: Enable the [fts5](https://www.sqlite.org/fts5.html) engine for full-text search.
|
||||
- `SQLITE_ENABLE_JSON1`: Enable the [json1](https://www.sqlite.org/json1.html) extension for json support in sql query.
|
||||
|
||||
For more details on sqlite compile options, see [their documentation](https://www.sqlite.org/compile.html).
|
||||
For more details on sqlite compile options, see [their documentation](https://www.sqlite.org/compile.html).
|
||||
|
||||
## Moor-only functions
|
||||
|
||||
`moor_ffi` includes additional sql functions not available in standard sqlite:
|
||||
|
||||
- `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`:
|
||||
|
||||
Note that `NaN`, `-infinity` or `+infinity` are represented as `NULL` in sql.
|
|
@ -1,3 +1,7 @@
|
|||
## unreleased
|
||||
|
||||
-Enable mathematical functions in sql (`pow`, `power`, `sin`, `cos`, `tan`, `asin`, `atan`, `acos`, `sqrt`)
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Use precompiled libraries for faster build times
|
||||
|
|
|
@ -110,4 +110,14 @@ extension SqliteFunctionContextPointer on Pointer<FunctionContext> {
|
|||
void resultDouble(double value) {
|
||||
bindings.sqlite3_result_double(this, value);
|
||||
}
|
||||
|
||||
void resultNum(num value) {
|
||||
if (value is int) {
|
||||
resultInt(value);
|
||||
} else if (value is double) {
|
||||
resultDouble(value);
|
||||
}
|
||||
|
||||
throw AssertionError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
@ -15,6 +16,7 @@ import 'package:moor_ffi/src/ffi/blob.dart';
|
|||
import 'package:moor_ffi/src/ffi/utils.dart';
|
||||
|
||||
part 'errors.dart';
|
||||
part 'moor_functions.dart';
|
||||
part 'prepared_statement.dart';
|
||||
|
||||
const _openingFlags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE;
|
||||
|
@ -169,7 +171,6 @@ class Database {
|
|||
///
|
||||
/// See also:
|
||||
/// - https://sqlite.org/c3ref/create_function.html
|
||||
/// - [SqliteFunctionHandler]
|
||||
@visibleForTesting
|
||||
void createFunction(
|
||||
String name,
|
||||
|
@ -218,6 +219,21 @@ class Database {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enables non-standard mathematical functions that ship with `moor_ffi`.
|
||||
///
|
||||
/// After calling [enableMathematicalFunctions], the following functions can
|
||||
/// be used in sql: `power`, `pow`, `sqrt`, `sin`, `cos`, `tan`, `asin`,
|
||||
/// `acos`, `atan`.
|
||||
///
|
||||
/// 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() {
|
||||
_registerOn(this);
|
||||
}
|
||||
|
||||
/// Get the application defined version of this database.
|
||||
int userVersion() {
|
||||
final stmt = prepare('PRAGMA user_version');
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
part of 'database.dart';
|
||||
|
||||
void _powImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
// sqlite will ensure that this is only called with 2 arguments
|
||||
final first = args[0].value;
|
||||
final second = args[1].value;
|
||||
|
||||
if (first == null || second == null || first is! num || second is! num) {
|
||||
ctx.resultNull();
|
||||
return;
|
||||
}
|
||||
|
||||
final result = pow(first as num, second as num);
|
||||
ctx.resultNum(result);
|
||||
}
|
||||
|
||||
/// Base implementation for a sqlite function that takes one numerical argument
|
||||
/// and returns one numerical argument.
|
||||
///
|
||||
/// If [argCount] is not `1` or the single argument is not of a numerical type,
|
||||
/// [ctx] will complete to null. Otherwise, it will complete to the result of
|
||||
/// [calculation] with the casted argument.
|
||||
void _unaryNumFunction(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args, num Function(num) calculation) {
|
||||
// sqlite will ensure that this is only called with one argument
|
||||
final value = args[0].value;
|
||||
if (value is num) {
|
||||
ctx.resultNum(calculation(value));
|
||||
} else {
|
||||
ctx.resultNull();
|
||||
}
|
||||
}
|
||||
|
||||
void _sinImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, sin);
|
||||
}
|
||||
|
||||
void _cosImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, cos);
|
||||
}
|
||||
|
||||
void _tanImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, tan);
|
||||
}
|
||||
|
||||
void _sqrtImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, sqrt);
|
||||
}
|
||||
|
||||
void _asinImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, asin);
|
||||
}
|
||||
|
||||
void _acosImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, acos);
|
||||
}
|
||||
|
||||
void _atanImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||
Pointer<Pointer<SqliteValue>> args) {
|
||||
_unaryNumFunction(ctx, argCount, args, atan);
|
||||
}
|
||||
|
||||
void _registerOn(Database db) {
|
||||
final powImplPointer =
|
||||
Pointer.fromFunction<sqlite3_function_handler>(_powImpl);
|
||||
db.createFunction('power', 2, powImplPointer);
|
||||
db.createFunction('pow', 2, powImplPointer);
|
||||
|
||||
db.createFunction('sqrt', 1, Pointer.fromFunction(_sqrtImpl));
|
||||
|
||||
db.createFunction('sin', 1, Pointer.fromFunction(_sinImpl));
|
||||
db.createFunction('cos', 1, Pointer.fromFunction(_cosImpl));
|
||||
db.createFunction('tan', 1, Pointer.fromFunction(_tanImpl));
|
||||
db.createFunction('asin', 1, Pointer.fromFunction(_asinImpl));
|
||||
db.createFunction('acos', 1, Pointer.fromFunction(_acosImpl));
|
||||
db.createFunction('atan', 1, Pointer.fromFunction(_atanImpl));
|
||||
}
|
|
@ -40,6 +40,7 @@ class _VmDelegate extends DatabaseDelegate {
|
|||
} else {
|
||||
_db = Database.memory();
|
||||
}
|
||||
_db.enableMathematicalFunctions();
|
||||
versionDelegate = _VmVersionDelegate(_db);
|
||||
return Future.value();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:moor_ffi/database.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
Database db;
|
||||
|
||||
setUp(() => db = Database.memory()..enableMathematicalFunctions());
|
||||
tearDown(() => db.close());
|
||||
|
||||
group('pow', () {
|
||||
dynamic _resultOfPow(String a, String b) {
|
||||
final stmt = db.prepare('SELECT pow($a, $b) AS r;');
|
||||
final rows = stmt.select();
|
||||
stmt.close();
|
||||
|
||||
return rows.single['r'];
|
||||
}
|
||||
|
||||
test('returns null when any argument is null', () {
|
||||
expect(_resultOfPow('null', 'null'), isNull);
|
||||
expect(_resultOfPow('3', 'null'), isNull);
|
||||
expect(_resultOfPow('null', '3'), isNull);
|
||||
});
|
||||
|
||||
test('returns correct results', () {
|
||||
expect(_resultOfPow('10', '0'), 1);
|
||||
expect(_resultOfPow('0', '10'), 0);
|
||||
expect(_resultOfPow('0', '0'), 1);
|
||||
expect(_resultOfPow('2', '5'), 32);
|
||||
expect(_resultOfPow('3.5', '2'), 12.25);
|
||||
expect(_resultOfPow('10', '-1'), 0.1);
|
||||
});
|
||||
});
|
||||
|
||||
for (final scenario in _testCases) {
|
||||
final function = scenario.sqlFunction;
|
||||
|
||||
test(function, () {
|
||||
final stmt = db.prepare('SELECT $function(?) AS r');
|
||||
|
||||
for (final input in scenario.inputs) {
|
||||
final sqlResult = stmt.select([input]).single['r'];
|
||||
final dartResult = scenario.dartEquivalent(input);
|
||||
|
||||
// NaN in sqlite is null, account for that
|
||||
if (dartResult.isNaN) {
|
||||
expect(
|
||||
sqlResult,
|
||||
null,
|
||||
reason: '$function($input) = $dartResult',
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
sqlResult,
|
||||
equals(dartResult),
|
||||
reason: '$function($input) = $dartResult',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final resultWithNull = stmt.select([null]);
|
||||
expect(resultWithNull.single['r'], isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// utils to verify the sql functions behave exactly like the ones from the VM
|
||||
|
||||
class _UnaryFunctionTestCase {
|
||||
final String sqlFunction;
|
||||
final num Function(num) dartEquivalent;
|
||||
final List<num> inputs;
|
||||
|
||||
const _UnaryFunctionTestCase(
|
||||
this.sqlFunction, this.dartEquivalent, this.inputs);
|
||||
}
|
||||
|
||||
const _unaryInputs = [
|
||||
pi,
|
||||
0,
|
||||
pi / 2,
|
||||
e,
|
||||
123,
|
||||
];
|
||||
|
||||
const _testCases = <_UnaryFunctionTestCase>[
|
||||
_UnaryFunctionTestCase('sin', sin, _unaryInputs),
|
||||
_UnaryFunctionTestCase('cos', cos, _unaryInputs),
|
||||
_UnaryFunctionTestCase('tan', tan, _unaryInputs),
|
||||
_UnaryFunctionTestCase('sqrt', sqrt, _unaryInputs),
|
||||
_UnaryFunctionTestCase('asin', asin, _unaryInputs),
|
||||
_UnaryFunctionTestCase('acos', acos, _unaryInputs),
|
||||
_UnaryFunctionTestCase('atan', atan, _unaryInputs),
|
||||
];
|
Loading…
Reference in New Issue