mirror of https://github.com/AMT-Cheif/drift.git
moor_ffi: Support user-defined functions
This commit is contained in:
parent
10b12a5976
commit
2d2d102654
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue