Enable mathematical functions in moor_ffi (#397)

This commit is contained in:
Simon Binder 2020-02-18 22:05:18 +01:00
parent 1832b59848
commit fa5411fb5d
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
7 changed files with 224 additions and 2 deletions

View File

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

View File

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

View File

@ -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();
}
}

View File

@ -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');

View File

@ -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));
}

View File

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

View File

@ -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),
];