Begin migration to sqlite3 package

This commit is contained in:
Simon Binder 2020-07-08 19:04:57 +02:00
parent 634d1318e0
commit e4fa5fb936
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
17 changed files with 524 additions and 12 deletions

View File

@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:moor_ffi/moor_ffi.dart'; import 'package:moor/ffi.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:tests/database/database.dart'; import 'package:tests/database/database.dart';

View File

@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:moor_ffi/moor_ffi.dart'; import 'package:moor/ffi.dart';
import 'package:tests/tests.dart'; import 'package:tests/tests.dart';
import 'package:path/path.dart' show join; import 'package:path/path.dart' show join;

4
moor/lib/ffi.dart Normal file
View File

@ -0,0 +1,4 @@
/// Moor implementation using `package:sqlite3/`.
library moor.ffi;
export 'src/ffi/vm_database.dart';

View File

@ -0,0 +1,197 @@
import 'dart:math';
import 'package:sqlite3/sqlite3.dart';
// ignore_for_file: avoid_returning_null, only_throw_errors
/// Extension to register moor-specific sql functions.
extension EnableMoorFunctions on Database {
/// Enables moor-specific sql functions on this database.
void useMoorVersions() {
createFunction(
functionName: 'power',
deterministic: true,
argumentCount: const AllowedArgumentCount(2),
function: _pow,
);
createFunction(
functionName: 'pow',
deterministic: true,
argumentCount: const AllowedArgumentCount(2),
function: _pow,
);
createFunction(
functionName: 'sqrt',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(sqrt),
);
createFunction(
functionName: 'sin',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(sin),
);
createFunction(
functionName: 'cos',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(cos),
);
createFunction(
functionName: 'tan',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(tan),
);
createFunction(
functionName: 'asin',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(asin),
);
createFunction(
functionName: 'acos',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(acos),
);
createFunction(
functionName: 'atan',
deterministic: true,
argumentCount: const AllowedArgumentCount(1),
function: _unaryNumFunction(atan),
);
createFunction(
functionName: 'regexp',
deterministic: true,
argumentCount: const AllowedArgumentCount(2),
function: _regexpImpl,
);
// Third argument can be used to set flags (like multiline, case
// sensitivity, etc.)
createFunction(
functionName: 'regexp_moor_ffi',
deterministic: true,
argumentCount: const AllowedArgumentCount(3),
function: _regexpImpl,
);
createFunction(
functionName: 'moor_contains',
deterministic: true,
argumentCount: const AllowedArgumentCount(2),
function: _containsImpl,
);
createFunction(
functionName: 'moor_contains',
deterministic: true,
argumentCount: const AllowedArgumentCount(3),
function: _containsImpl,
);
}
}
num _pow(List<dynamic> args) {
final first = args[0];
final second = args[1];
if (first == null || second == null || first is! num || second is! num) {
return null;
}
return pow(first as num, second as num);
}
/// Base implementation for a sqlite function that takes one numerical argument
/// and returns one numerical argument.
///
/// When not called with a number, returns will null. Otherwise, returns with
/// [calculation].
num Function(List<dynamic>) _unaryNumFunction(num Function(num) calculation) {
return (List<dynamic> args) {
// sqlite will ensure that this is only called with one argument
final value = args[0];
if (value is num) {
return calculation(value);
} else {
return null;
}
};
}
bool _regexpImpl(List<dynamic> args) {
var multiLine = false;
var caseSensitive = true;
var unicode = false;
var dotAll = false;
final argCount = args.length;
if (argCount < 2 || argCount > 3) {
throw 'Expected two or three arguments to regexp';
}
final firstParam = args[0];
final secondParam = args[1];
if (firstParam == null || secondParam == null) {
return null;
}
if (firstParam is! String || secondParam is! String) {
throw 'Expected two strings as parameters to regexp';
}
if (argCount == 3) {
// In the variant with three arguments, the last (int) arg can be used to
// enable regex flags. See the regexp() extension in moor for details.
final value = args[2];
if (value is int) {
multiLine = (value & 1) == 1;
caseSensitive = (value & 2) != 2;
unicode = (value & 4) == 4;
dotAll = (value & 8) == 8;
}
}
RegExp regex;
try {
regex = RegExp(
firstParam as String,
multiLine: multiLine,
caseSensitive: caseSensitive,
unicode: unicode,
dotAll: dotAll,
);
} on FormatException {
throw 'Invalid regex';
}
return regex.hasMatch(secondParam as String);
}
bool _containsImpl(List<dynamic> args) {
final argCount = args.length;
if (argCount < 2 || argCount > 3) {
throw 'Expected 2 or 3 arguments to moor_contains';
}
final first = args[0];
final second = args[1];
if (first is! String || second is! String) {
throw 'First two args to contains must be strings';
}
final caseSensitive = argCount == 3 && args[2] == 1;
final firstAsString = first as String;
final secondAsString = second as String;
final result = caseSensitive
? firstAsString.contains(secondAsString)
: firstAsString.toLowerCase().contains(secondAsString.toLowerCase());
return result;
}

View File

@ -0,0 +1,127 @@
import 'dart:io';
import 'package:moor/backends.dart';
import 'package:sqlite3/sqlite3.dart';
import 'moor_ffi_functions.dart';
/// A moor database that runs on the Dart VM.
class VmDatabase extends DelegatedDatabase {
VmDatabase._(DatabaseDelegate delegate, bool logStatements)
: super(delegate, isSequential: true, logStatements: logStatements);
/// Creates a database that will store its result in the [file], creating it
/// if it doesn't exist.
factory VmDatabase(File file, {bool logStatements = false}) {
return VmDatabase._(_VmDelegate(file), logStatements);
}
/// Creates an in-memory database won't persist its changes on disk.
factory VmDatabase.memory({bool logStatements = false}) {
return VmDatabase._(_VmDelegate(null), logStatements);
}
}
class _VmDelegate extends DatabaseDelegate {
Database _db;
final File file;
_VmDelegate(this.file);
@override
final TransactionDelegate transactionDelegate = const NoTransactionDelegate();
@override
DbVersionDelegate versionDelegate;
@override
Future<bool> get isOpen => Future.value(_db != null);
@override
Future<void> open(QueryExecutorUser user) async {
if (file != null) {
_db = sqlite3.open(file.path);
} else {
_db = sqlite3.openInMemory();
}
_db.useMoorVersions();
versionDelegate = _VmVersionDelegate(_db);
return Future.value();
}
@override
Future<void> runBatched(BatchedStatements statements) async {
final prepared = [
for (final stmt in statements.statements) _db.prepare(stmt),
];
for (final application in statements.arguments) {
final stmt = prepared[application.statementIndex];
stmt.execute(application.arguments);
}
for (final stmt in prepared) {
stmt.dispose();
}
return Future.value();
}
Future _runWithArgs(String statement, List<dynamic> args) async {
if (args.isEmpty) {
_db.execute(statement);
} else {
final stmt = _db.prepare(statement);
stmt.execute(args);
stmt.dispose();
}
}
@override
Future<void> runCustom(String statement, List args) async {
await _runWithArgs(statement, args);
}
@override
Future<int> runInsert(String statement, List args) async {
await _runWithArgs(statement, args);
return _db.lastInsertRowId;
}
@override
Future<int> runUpdate(String statement, List args) async {
await _runWithArgs(statement, args);
return _db.getUpdatedRows();
}
@override
Future<QueryResult> runSelect(String statement, List args) async {
final stmt = _db.prepare(statement);
final result = stmt.select(args);
stmt.dispose();
return Future.value(QueryResult.fromRows(result.toList()));
}
@override
Future<void> close() async {
_db.dispose();
}
}
class _VmVersionDelegate extends DynamicVersionDelegate {
final Database database;
_VmVersionDelegate(this.database);
@override
Future<int> get schemaVersion => Future.value(database.userVersion);
@override
Future<void> setSchemaVersion(int version) {
database.userVersion = version;
return Future.value();
}
}

View File

@ -14,12 +14,14 @@ dependencies:
collection: ^1.0.0 collection: ^1.0.0
synchronized: ^2.1.0 synchronized: ^2.1.0
pedantic: ^1.0.0 pedantic: ^1.0.0
sqlite3:
git:
url: https://github.com/simolus3/sqlite3.dart.git
path: sqlite3
dev_dependencies: dev_dependencies:
moor_generator: ^3.2.0 moor_generator: ^3.2.0
uuid: ^2.0.0 uuid: ^2.0.0
moor_ffi: # Used to run integration tests
path: ../moor_ffi
path: ^1.6.4 path: ^1.6.4
build_runner: '>=1.3.0 <2.0.0' build_runner: '>=1.3.0 <2.0.0'
test: ^1.9.0 test: ^1.9.0

View File

@ -1,9 +1,10 @@
@TestOn('vm') @TestOn('vm')
import 'dart:io'; import 'dart:io';
import 'package:moor/ffi.dart';
import 'package:moor/isolate.dart'; import 'package:moor/isolate.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:path/path.dart' show join; import 'package:path/path.dart' show join;

View File

@ -1,7 +1,7 @@
import 'package:moor/moor.dart';
@TestOn('vm') @TestOn('vm')
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:moor_ffi/moor_ffi.dart';
import '../data/tables/todos.dart'; import '../data/tables/todos.dart';

View File

@ -1,6 +1,6 @@
@Tags(['integration']) @Tags(['integration'])
@TestOn('vm') @TestOn('vm')
import 'package:moor_ffi/moor_ffi.dart'; import 'package:moor/ffi.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../data/tables/custom_tables.dart'; import '../data/tables/custom_tables.dart';

View File

@ -2,9 +2,9 @@
@TestOn('vm') @TestOn('vm')
import 'dart:convert'; import 'dart:convert';
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor/extensions/json1.dart'; import 'package:moor/extensions/json1.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../data/tables/todos.dart'; import '../data/tables/todos.dart';

View File

@ -1,7 +1,7 @@
@TestOn('vm') @TestOn('vm')
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor/extensions/moor_ffi.dart'; import 'package:moor/extensions/moor_ffi.dart';
import 'package:moor_ffi/moor_ffi.dart'; import 'package:moor/ffi.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../data/tables/todos.dart'; import '../data/tables/todos.dart';

View File

@ -0,0 +1,178 @@
import 'dart:math';
import 'package:moor/src/ffi/moor_ffi_functions.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.dart';
void main() {
Database db;
setUp(() => db = sqlite3.openInMemory()..useMoorVersions());
tearDown(() => db.dispose());
dynamic selectSingle(String expression) {
final stmt = db.prepare('SELECT $expression AS r;');
final rows = stmt.select();
stmt.dispose();
return rows.single['r'];
}
group('pow', () {
dynamic _resultOfPow(String a, String b) {
return selectSingle('pow($a, $b)');
}
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);
});
}
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);
});
test('supports flags', () {
final stmt =
db.prepare(r"SELECT regexp_moor_ffi('^bar', 'foo\nbar', 8) AS r;");
final result = stmt.select();
expect(result.single['r'], 0);
});
test('returns null when either argument is null', () {
final stmt = db.prepare('SELECT ? REGEXP ?');
expect(stmt.select(['foo', null]).single.columnAt(0), isNull);
expect(stmt.select([null, 'foo']).single.columnAt(0), isNull);
stmt.dispose();
});
});
group('moor_contains', () {
test('checks for type errors', () {
expect(() => db.execute('SELECT moor_contains(12, 1);'),
throwsA(isA<SqliteException>()));
});
test('case insensitive without parameter', () {
expect(selectSingle("moor_contains('foo', 'O')"), 1);
});
test('case insensitive with parameter', () {
expect(selectSingle("moor_contains('foo', 'O', 0)"), 1);
});
test('case sensitive', () {
expect(selectSingle("moor_contains('Hello', 'hell', 1)"), 0);
expect(selectSingle("moor_contains('hi', 'i', 1)"), 1);
});
});
}
// 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),
];

View File

@ -1,5 +1,5 @@
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart'; import 'package:moor/ffi.dart';
@TestOn('vm') @TestOn('vm')
import 'package:test/test.dart'; import 'package:test/test.dart';

View File

@ -4,7 +4,7 @@ import 'dart:isolate';
import 'package:moor/isolate.dart'; import 'package:moor/isolate.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart'; import 'package:moor/ffi.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'data/tables/todos.dart'; import 'data/tables/todos.dart';

View File

@ -1,5 +1,6 @@
/// Exports the low-level [Database] class to run operations on a sqlite /// Exports the low-level [Database] class to run operations on a sqlite
/// database via `dart:ffi`. /// database via `dart:ffi`.
@Deprecated('Consider migrating to package:sqlite3/sqlite3.dart')
library database; library database;
import 'package:moor_ffi/src/bindings/types.dart'; import 'package:moor_ffi/src/bindings/types.dart';

View File

@ -1,3 +1,4 @@
@Deprecated('Use package:moor/ffi.dart instead')
import 'dart:io'; import 'dart:io';
import 'package:moor/backends.dart'; import 'package:moor/backends.dart';

View File

@ -1,5 +1,6 @@
/// Utils to open a [DynamicLibrary] on platforms that aren't supported by /// Utils to open a [DynamicLibrary] on platforms that aren't supported by
/// `moor_ffi` by default. /// `moor_ffi` by default.
@Deprecated('Consider migrating to package:sqlite3/open.dart')
library open_helper; library open_helper;
import 'dart:ffi'; import 'dart:ffi';