moor_ffi: Support user-defined functions

This commit is contained in:
Simon Binder 2020-02-18 21:13:06 +01:00
parent 10b12a5976
commit 2d2d102654
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 305 additions and 3 deletions

View File

@ -5,5 +5,6 @@ library database;
import 'package:moor_ffi/src/bindings/types.dart';
export 'src/api/result.dart';
export 'src/bindings/types.dart' hide Database, Statement;
export 'src/impl/database.dart'
show SqliteException, Database, PreparedStatement;

View File

@ -88,6 +88,30 @@ class _SQLiteBindings {
Pointer<Void> disposeCb) sqlite3_bind_blob;
int Function(Pointer<Statement> statement, int columnIndex) sqlite3_bind_null;
int Function(
Pointer<Database> db,
Pointer<Uint8> zFunctionName,
int argCount,
int eTextRep,
Pointer<Void> arg,
Pointer<NativeFunction<sqlite3_function_handler>> handler,
Pointer<NativeFunction<sqlite3_function_handler>> step,
Pointer<NativeFunction<sqlite3_function_finalizer>> finalizer,
Pointer<NativeFunction<sqlite3_finalizer>> destroyArg,
) sqlite3_create_function_v2;
Pointer<CBlob> Function(Pointer<SqliteValue> value) sqlite3_value_blob;
Pointer<CBlob> Function(Pointer<SqliteValue> value) sqlite3_value_text;
double Function(Pointer<SqliteValue> value) sqlite3_value_double;
int Function(Pointer<SqliteValue> value) sqlite3_value_int64;
int Function(Pointer<SqliteValue> value) sqlite3_value_bytes;
int Function(Pointer<SqliteValue> value) sqlite3_value_type;
void Function(Pointer<FunctionContext> ctx) sqlite3_result_null;
void Function(Pointer<FunctionContext> ctx, int value) sqlite3_result_int64;
void Function(Pointer<FunctionContext> ctx, double value)
sqlite3_result_double;
_SQLiteBindings() {
sqlite = open.openSqlite();
@ -177,6 +201,43 @@ class _SQLiteBindings {
.lookup<NativeFunction<sqlite3_column_bytes_native_t>>(
'sqlite3_column_bytes')
.asFunction();
sqlite3_create_function_v2 = sqlite
.lookup<NativeFunction<sqlite3_create_function_v2_native>>(
'sqlite3_create_function_v2')
.asFunction();
sqlite3_value_blob = sqlite
.lookup<NativeFunction<sqlite3_value_blob_native>>('sqlite3_value_blob')
.asFunction();
sqlite3_value_text = sqlite
.lookup<NativeFunction<sqlite3_value_text_native>>('sqlite3_value_text')
.asFunction();
sqlite3_value_double = sqlite
.lookup<NativeFunction<sqlite3_value_double_native>>(
'sqlite3_value_double')
.asFunction();
sqlite3_value_int64 = sqlite
.lookup<NativeFunction<sqlite3_value_int64_native>>(
'sqlite3_value_int64')
.asFunction();
sqlite3_value_bytes = sqlite
.lookup<NativeFunction<sqlite3_value_bytes_native>>(
'sqlite3_value_bytes')
.asFunction();
sqlite3_value_type = sqlite
.lookup<NativeFunction<sqlite3_value_type_native>>('sqlite3_value_type')
.asFunction();
sqlite3_result_null = sqlite
.lookup<NativeFunction<sqlite3_result_null_native>>(
'sqlite3_result_null')
.asFunction();
sqlite3_result_int64 = sqlite
.lookup<NativeFunction<sqlite3_result_int64_native>>(
'sqlite3_result_int64')
.asFunction();
sqlite3_result_double = sqlite
.lookup<NativeFunction<sqlite3_result_double_native>>(
'sqlite3_result_double')
.asFunction();
}
}

View File

@ -175,6 +175,15 @@ class Flags {
static const int SQLITE_OPEN_WAL = 0x00080000;
}
class TextEncodings {
static const int SQLITE_UTF8 = 1;
}
class FunctionFlags {
static const int SQLITE_DETERMINISTIC = 0x000000800;
static const int SQLITE_DIRECTONLY = 0x000080000;
}
class Types {
static const int SQLITE_INTEGER = 1;
static const int SQLITE_FLOAT = 2;

View File

@ -87,3 +87,42 @@ typedef sqlite3_bind_blob_native = Int32 Function(
Pointer<Void> callback);
typedef sqlite3_bind_null_native = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_function_handler = Void Function(
Pointer<FunctionContext> context,
Int32 argCount,
Pointer<Pointer<SqliteValue>> args);
typedef sqlite3_function_finalizer = Void Function(
Pointer<FunctionContext> context);
typedef sqlite3_finalizer = Void Function(Pointer<Void> ptr);
typedef sqlite3_create_function_v2_native = Int32 Function(
Pointer<Database> db,
Pointer<Uint8> zFunctionName,
Int32 nArg,
Int32 eTextRep,
Pointer<Void> pApp,
Pointer<NativeFunction<sqlite3_function_handler>> xFunc,
Pointer<NativeFunction<sqlite3_function_handler>> xStep,
Pointer<NativeFunction<sqlite3_function_finalizer>> xDestroy,
Pointer<NativeFunction<sqlite3_finalizer>> finalizePApp,
);
typedef sqlite3_value_blob_native = Pointer<CBlob> Function(
Pointer<SqliteValue> value);
typedef sqlite3_value_double_native = Double Function(
Pointer<SqliteValue> value);
typedef sqlite3_value_int64_native = Int64 Function(Pointer<SqliteValue> value);
typedef sqlite3_value_text_native = Pointer<CBlob> Function(
Pointer<SqliteValue> value);
typedef sqlite3_value_bytes_native = Int32 Function(Pointer<SqliteValue> value);
typedef sqlite3_value_type_native = Int32 Function(Pointer<SqliteValue> value);
typedef sqlite3_result_null_native = Void Function(
Pointer<FunctionContext> context);
typedef sqlite3_result_double_native = Void Function(
Pointer<FunctionContext> context, Double value);
typedef sqlite3_result_int64_native = Void Function(
Pointer<FunctionContext> context, Int64 value);

View File

@ -4,6 +4,12 @@
import 'dart:ffi';
import 'package:moor/moor.dart';
import '../ffi/blob.dart';
import 'bindings.dart';
import 'constants.dart';
// ignore_for_file: comment_references
/// Database Connection Handle
@ -39,3 +45,61 @@ class Database extends Struct {}
/// Refer to documentation on individual methods above for additional
/// information.
class Statement extends Struct {}
/// The context in which an SQL function executes is stored in this object.
/// A pointer to this object is always the first paramater to
/// application-defined SQL functions.
///
/// See also:
/// - https://www.sqlite.org/c3ref/context.html
class FunctionContext extends Struct {}
/// A value object in sqlite, which can represent all values that can be stored
/// in a database table.
class SqliteValue extends Struct {}
/// Extension to extract value from a [SqliteValue].
extension SqliteValuePointer on Pointer<SqliteValue> {
/// Extracts the raw value from the object.
///
/// Depending on the type of this value as set in sqlite, [value] returns
/// - a [String]
/// - a [Uint8List]
/// - a [int]
/// - a [double]
/// - `null`
///
/// For texts and bytes, the value be copied.
dynamic get value {
final api = bindings;
final type = api.sqlite3_value_type(this);
switch (type) {
case Types.SQLITE_INTEGER:
return api.sqlite3_value_int64(this);
case Types.SQLITE_FLOAT:
return api.sqlite3_value_double(this);
case Types.SQLITE_TEXT:
final length = api.sqlite3_value_bytes(this);
return api.sqlite3_value_text(this).readAsStringWithLength(length);
case Types.SQLITE_BLOB:
final length = api.sqlite3_value_bytes(this);
if (length == 0) {
// sqlite3_value_bytes returns a null pointer for non-null blobs with
// a length of 0. Note that we can distinguish this from a proper null
// by checking the type (which isn't SQLITE_NULL)
return Uint8List(0);
}
return api.sqlite3_value_blob(this).readBytes(length);
case Types.SQLITE_NULL:
default:
return null;
}
}
}
extension SqliteFunctonContextPointer on Pointer<FunctionContext> {
void resultNull() {
bindings.sqlite3_result_null(this);
}
}

View File

@ -8,12 +8,16 @@ import 'package:ffi/ffi.dart' as ffi;
/// Pointer to arbitrary blobs in C.
class CBlob extends Struct {
static Pointer<CBlob> allocate(Uint8List blob) {
final str = ffi.allocate<Uint8>(count: blob.length);
static Pointer<CBlob> allocate(Uint8List blob, {int paddingAtEnd = 0}) {
final str = ffi.allocate<Uint8>(count: blob.length + paddingAtEnd);
final asList = str.asTypedList(blob.length);
final asList = str.asTypedList(blob.length + paddingAtEnd);
asList.setAll(0, blob);
if (paddingAtEnd != 0) {
asList.fillRange(blob.length, blob.length + paddingAtEnd, 0);
}
return str.cast();
}

View File

@ -1,11 +1,14 @@
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:meta/meta.dart';
import 'package:moor_ffi/database.dart';
import 'package:moor_ffi/src/api/result.dart';
import 'package:moor_ffi/src/bindings/constants.dart';
import 'package:moor_ffi/src/bindings/signatures.dart';
import 'package:moor_ffi/src/bindings/types.dart' as types;
import 'package:moor_ffi/src/bindings/bindings.dart';
import 'package:moor_ffi/src/ffi/blob.dart';
@ -16,10 +19,23 @@ part 'prepared_statement.dart';
const _openingFlags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE;
/// Signature of a Dart function that can be called from sql statements.
///
/// It receives a pointer to a [types.FunctionContext], which can be used to
/// set the return value (or indicate execution failure). Under no circumstances
/// should this function throw a Dart exception.
typedef SqliteFunctionHandler = void Function(
Pointer<types.FunctionContext> context,
int argumentCount,
Pointer<Pointer<types.SqliteValue>> arguments,
);
/// A opened sqlite database.
class Database {
final Pointer<types.Database> _db;
final List<PreparedStatement> _preparedStmt = [];
final List<Pointer<Void>> _furtherAllocations = [];
bool _isClosed = false;
Database._(this._db);
@ -76,6 +92,10 @@ class Database {
}
_isClosed = true;
for (final additional in _furtherAllocations) {
additional.free();
}
// we don't need to deallocate the _db pointer, sqlite takes care of that
if (exception != null) {
@ -143,6 +163,72 @@ class Database {
return prepared;
}
/// Registers a custom sqlite function by its [name].
///
/// The function must take [argumentCount] arguments, and it may not take more
/// than 127 arguments. If it can take a variable amount of arguments,
/// [argumentCount] should be set to `-1`.
///
/// When the output of the function depends solely on its input,
/// [isDeterministic] should be set. This allows sqlite's query planer to make
/// further optimizations.
/// When [directOnly] is set (defaults to true), the function can't be used
/// outside a query (e.g. in triggers, views, check constraints, index
/// expressions, etc.). Unless necessary, this should be enabled for security
/// purposes. See the discussion at the link for more details
/// The length of the utf8 encoding of [name] must not exceed 255 bytes.
///
/// See also:
/// - https://sqlite.org/c3ref/create_function.html
/// - [SqliteFunctionHandler]
@visibleForTesting
void createFunction(
String name,
int argumentCount,
Pointer<NativeFunction<sqlite3_function_handler>> implementation, {
bool isDeterministic = false,
bool directOnly = true,
}) {
_ensureOpen();
final encodedName = Uint8List.fromList(utf8.encode(name));
// length of encoded name is limited to 255 bytes in utf8, excluding the 0
// terminator
if (encodedName.length > 255) {
throw ArgumentError.value(
name, 'name', 'Must be at most 255 bytes when encoded as utf8');
}
// argument length should be between -1 and 127
if (argumentCount < -1 || argumentCount > 127) {
throw ArgumentError.value(
argumentCount, 'argumentCount', 'Should be between -1 and 127');
}
final namePtr = CBlob.allocate(encodedName, paddingAtEnd: 1);
_furtherAllocations.add(namePtr.cast());
var textFlag = TextEncodings.SQLITE_UTF8;
if (isDeterministic) textFlag |= FunctionFlags.SQLITE_DETERMINISTIC;
if (directOnly) textFlag |= FunctionFlags.SQLITE_DIRECTONLY;
final result = bindings.sqlite3_create_function_v2(
_db,
namePtr.cast(),
argumentCount,
textFlag,
nullPtr(), // *pApp, we don't use that
implementation,
nullPtr(), // *xStep, null for regular functions
nullPtr(), // *xFinal, null for regular functions
nullPtr(), // finalizer for *pApp,
);
if (result != Errors.SQLITE_OK) {
throw SqliteException._fromErrorCode(_db, result);
}
}
/// Get the application defined version of this database.
int userVersion() {
final stmt = prepare('PRAGMA user_version');

View File

@ -0,0 +1,38 @@
import 'dart:ffi';
import 'package:moor/moor.dart';
import 'package:moor_ffi/database.dart';
import 'package:test/test.dart';
final _params = <dynamic>[];
void _testFunImpl(Pointer<FunctionContext> ctx, int argCount,
Pointer<Pointer<SqliteValue>> args) {
_params.clear();
for (var i = 0; i < argCount; i++) {
_params.add(args[i].value);
}
ctx.resultNull();
}
void main() {
test('can read arguments of user defined functions', () {
final db = Database.memory();
db.createFunction('test_fun', 6, Pointer.fromFunction(_testFunImpl));
db.execute(
r'''SELECT test_fun(1, 2.5, 'hello world', X'ff00ff', X'', NULL)''');
db.close();
expect(_params, [
1,
2.5,
'hello world',
Uint8List.fromList([255, 0, 255]),
Uint8List(0),
null,
]);
});
}