Merge branch 'master' into develop

# Conflicts:
#	docs/content/en/docs/Getting started/advanced_dart_tables.md
#	docs/content/en/docs/Using SQL/moor_files.md
#	extras/integration_tests/flutter_db/lib/moor_flutter.dart
#	moor/CHANGELOG.md
#	moor/lib/src/runtime/isolate/client.dart
#	moor/lib/src/runtime/isolate/server.dart
#	moor/pubspec.yaml
#	moor_ffi/test/database/database_test.dart
This commit is contained in:
Simon Binder 2020-04-25 12:39:35 +02:00
commit 1546f323b2
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
33 changed files with 297 additions and 68 deletions

View File

@ -71,7 +71,7 @@ fields from that date:
select(users)..where((u) => u.birthDate.year.isLessThan(1950))
```
The individual fileds like `year`, `month` and so on are expressions themselves. This means
The individual fields like `year`, `month` and so on are expressions themselves. This means
that you can use operators and comparisons on them.
To obtain the current date or the current time as an expression, use the `currentDate`
and `currentDateAndTime` constants provided by moor.

View File

@ -92,7 +92,7 @@ Future<MoorIsolate> _createMoorIsolate() async {
}
void _startBackground(_IsolateStartRequest request) {
// this is the entrypoint from the background isolate! Let's create
// this is the entry point from the background isolate! Let's create
// the database from the path we received
final executor = VmDatabase(File(request.targetPath));
// we're using MoorIsolate.inCurrent here as this method already runs on a
@ -105,7 +105,7 @@ void _startBackground(_IsolateStartRequest request) {
request.sendMoorIsolate.send(moorIsolate);
}
// used to bundle the SendPort and the target path, since isolate entrypoint
// used to bundle the SendPort and the target path, since isolate entry point
// functions can only take one parameter.
class _IsolateStartRequest {
final SendPort sendMoorIsolate;
@ -139,7 +139,7 @@ a setup where you have three or more threads:
- A foreground isolate, probably for Flutter
- Another background isolate, which could be used for networking.
You can the read data from the foreground isolate or start query streams, similar to the example
You can then read data from the foreground isolate or start query streams, similar to the example
above. The background isolate would _also_ call `MoorIsolate.connect` and create its own instance
of the generated database class. Writes to one database will be visible to the other isolate and
also update query streams.
@ -153,7 +153,7 @@ All moor features are supported on background isolates and work out of the box.
- Batched updates and inserts
- Custom statements or those generated from an sql api
Please note that, will using a background isolate can reduce lag on the UI thread, the overall
Please note that, while using a background isolate can reduce lag on the UI thread, the overall
database is going to be slower! There's a overhead involved in sending data between
isolates, and that's exactly what moor has to do internally. If you're not running into dropped
frames because of moor, using a background isolate is probably not necessary for your app.

View File

@ -158,7 +158,7 @@ comes from multiple rows. Common questions include
- what's the average length of a todo entry?
What these queries have in common is that data from multiple rows needs to be combined into a single
row. In sql, this can be achieved with "aggregate functins", for which moor has
row. In sql, this can be achieved with "aggregate functions", for which moor has
[builtin support]({{< relref "expressions.md#aggregate" >}}).
_Additional info_: A good tutorial for group by in sql is available [here](https://www.sqlitetutorial.net/sqlite-group-by/).

View File

@ -91,5 +91,5 @@ yet. You can just delete your apps' data and reinstall the app - the database wi
will be created again. Please note that uninstalling is not enough sometimes - Android might have backed up
the database file and will re-create it when installing the app again.
You can also delete and re-create all tables everytime your app is opened, see [this comment](https://github.com/simolus3/moor/issues/188#issuecomment-542682912)
You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/moor/issues/188#issuecomment-542682912)
on how that can be achieved.

View File

@ -53,7 +53,7 @@ INFO: test/fake_db.dart has moor databases or daos: TodoDb, SomeDao
### Export
This subcommand expects two paths, a Dart file and a target. The Dart file should contain
excactly one class annotated with `@UseMoor`. Running the following command will export
exactly one class annotated with `@UseMoor`. Running the following command will export
the database schema to json.
```
@ -68,4 +68,4 @@ The generated file (`schema.json` in this case) contains information about all
- `@create`-queries from included moor files
- dependecies thereof
The schema format is still work-in-progress and might change in the future.
The schema format is still a work-in-progress and might change in the future.

View File

@ -87,7 +87,7 @@ Future<CartWithItems> createEmptyCart() async {
```
## Selecting a cart
As our `CartWithItems` class consists of multiple compontents that are separated in the
As our `CartWithItems` class consists of multiple components that are separated in the
database (information about the cart, and information about the added items), we'll have
to merge two streams together. The `rxdart` library helps here by providing the
`combineLatest2` method, allowing us to write

View File

@ -83,7 +83,7 @@ examples. Otherwise, the generator won't be able to know what's going on.
## Generating the code
Moor integrates with Dart's `build` system, so you can generate all the code needed with
`flutter packages pub run build_runner build`. If you want to continuously rebuild the generated code
whever you change your code, run `flutter packages pub run build_runner watch` instead.
where you change your code, run `flutter packages pub run build_runner watch` instead.
After running either command once, the moor generator will have created a class for your
database and data classes for your entities. To use it, change the `MyDatabase` class as
follows:
@ -92,6 +92,8 @@ follows:
import 'package:moor_ffi/moor_ffi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:moor/moor.dart';
import 'dart:io';
LazyDatabase _openConnection() {
// the LazyDatabase util lets us find the right location for the file async.

View File

@ -93,7 +93,7 @@ class Users extends Table {
Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something
simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use
`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This
can be more efficient, but doesn't suppport dynamic values.
can be more efficient, but doesn't support dynamic values.
## Primary keys

View File

@ -27,7 +27,7 @@ class MyDatabase extends _$MyDatabase {
// watches all todo entries in a given category. The stream will automatically
// emit new items whenever the underlying data changes.
Stream<List<TodoEntry>> watchEntriesInCategory(Category c) {
Stream<List<Todo>> watchEntriesInCategory(Category c) {
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
}
}
@ -47,11 +47,17 @@ details on expressions, see [this guide]({{< relref "expressions.md" >}}).
### Limit
You can limit the amount of results returned by calling `limit` on queries. The method accepts
the amount of rows to return and an optional offset.
```dart
Future<List<Todo>> limitTodos(int limit, {int offset}) {
return (select(todos)..limit(limit, offset: offset)).get();
}
```
### Ordering
You can use the `orderBy` method on the select statement. It expects a list of functions that extract the individual
ordering terms from the table.
```dart
Future<List<TodoEntry>> sortEntriesAlphabetically() {
Future<List<Todo>> sortEntriesAlphabetically() {
return (select(todos)..orderBy([(t) => OrderingTerm(expression: t.title)])).get();
}
```
@ -62,7 +68,7 @@ You can also reverse the order by setting the `mode` property of the `OrderingTe
If you know a query is never going to return more than one row, wrapping the result in a `List`
can be tedious. Moor lets you work around that with `getSingle` and `watchSingle`:
```dart
Stream<TodoEntry> entryById(int id) {
Stream<Todo> entryById(int id) {
return (select(todos)..where((t) => t.id.equals(id))).watchSingle();
}
```
@ -105,7 +111,7 @@ Future moveImportantTasksIntoCategory(Category target) {
);
}
Future update(TodoEntry entry) {
Future update(Todo entry) {
// using replace will update all fields from the entry that are not marked as a primary key.
// it will also make sure that only the entry with the same primary key will be updated.
// Here, this means that the row that has the same id as entry will be updated to reflect
@ -124,7 +130,7 @@ the statement will affect all rows in the table!
{{% alert title="Entries, companions - why do we need all of this?" %}}
You might have noticed that we used a `TodosCompanion` for the first update instead of
just passing a `TodoEntry`. Moor generates the `TodoEntry` class (also called _data
just passing a `Todo`. Moor generates the `Todo` class (also called _data
class_ for the table) to hold a __full__ row with all its data. For _partial_ data,
prefer to use companions. In the example above, we only set the the `category` column,
so we used a companion.
@ -143,13 +149,13 @@ You can very easily insert any valid object into tables. As some values can be a
companion version.
```dart
// returns the generated id
Future<int> addTodoEntry(TodosCompanion entry) {
Future<int> addTodo(TodosCompanion entry) {
return into(todos).insert(entry);
}
```
All row classes generated will have a constructor that can be used to create objects:
```dart
addTodoEntry(
addTodo(
TodosCompanion(
title: Value('Important task'),
content: Value('Refactor persistence code'),

View File

@ -3,18 +3,10 @@ title: Encryption
description: Use moor on encrypted databases
---
{{% alert title="Security notice" color="warning" %}}
> This feature uses an external library for all the encryption work. Importing
that library as described here would always pull the latest version from git
when running `pub upgrade`. If you want to be sure that you're using a safe version
that you can trust, consider pulling `sqflite_sqlcipher` and `encrypted_moor` once
and then include your local version via a path in the pubspec.
{{% /alert %}}
Starting from 1.7, we have a version of moor that can work with encrypted databases by using the
[sqflite_sqlcipher](https://github.com/davidmartos96/sqflite_sqlcipher) library
[sqflite_sqlcipher](https://pub.dev/packages/sqflite_sqlcipher) library
by [@davidmartos96](https://github.com/davidmartos96). To use it, you need to
remove the dependency on `moor_flutter` from your `pubspec.yaml` and replace it
remove the dependency on `moor_flutter` and `moor_ffi` from your `pubspec.yaml` and replace it
with this:
```yaml
dependencies:
@ -28,4 +20,10 @@ dependencies:
Instead of importing `package:moor_flutter/moor_flutter` in your apps, you would then import
both `package:moor/moor.dart` and `package:encrypted_moor/encrypted_moor.dart`.
Finally, you can replace `FlutterQueryExecutor` with an `EncryptedExecutor`.
Finally, you can replace `FlutterQueryExecutor` with an `EncryptedExecutor`.
## Extra setup on Android and iOS
Some extra steps may have to be taken in your project so that SQLCipher works correctly. For example, the ProGuard configuration on Android for apps built for release.
[Read instructions](https://pub.dev/packages/sqflite_sqlcipher) (Usage and installation instructions of the package can be ignored, as that is handled internally by `moor`)

View File

@ -100,7 +100,7 @@ LazyDatabase(() async {
## Used compile options on Android
Note: Android is the only platform where moor_ffi will compile sqlite. The sqlite3 library from the system
is used on all other platforms. The choosen options help reduce binary size by removing features not used by
is used on all other platforms. The chosen options help reduce binary size by removing features not used by
moor. Important options are marked in bold.
- We use the `-O3` performance option

View File

@ -13,10 +13,10 @@ be supported, moor files will get better tooling support in the future and we re
migrate. See [their api]({{%relref "moor_files.md"%}}) for details.
{{% /alert %}}
Altough moor includes a fluent api that can be used to model most statements, advanced
Although moor includes a fluent api that can be used to model most statements, advanced
features like `GROUP BY` statements or window functions are not yet supported. You can
use these features with custom statements. You don't have to miss out on other benefits
moor brings, though: Moor helps you parse the result rows and qustom queries also
moor brings, though: Moor helps you parse the result rows and custom queries also
support auto-updating streams.
## Statements with a generated api

View File

@ -76,13 +76,13 @@ what we got:
## Variables
Inside of named queries, you can use variables just like you would expect with
sql. We support regular variables (`?`), explictly indexed variables (`?123`)
sql. We support regular variables (`?`), explicitly indexed variables (`?123`)
and colon-named variables (`:id`). We don't support variables declared
with @ or $. The compiler will attempt to infer the variable's type by
looking at its context. This lets moor generate typesafe apis for your
queries, the variables will be written as parameters to your method.
When it's ambigous, the analyzer might be unable to resolve the type of
When it's ambiguous, the analyzer might be unable to resolve the type of
a variable. For those scenarios, you can also denote the explicit type
of a variable:
```sql

View File

@ -9,7 +9,7 @@ description: Welcome to the moor documentation. This site shows you what moor ca
---
## So what's moor?
Moor is a reactive persistence library for Dart and Flutter applications. It's built ontop
Moor is a reactive persistence library for Dart and Flutter applications. It's built on top
of database libraries like [sqflite](https://pub.dev/packages/sqflite) or [sql.js](https://github.com/kripken/sql.js/)
and provides additional features, like:

View File

@ -18,7 +18,7 @@ dev_dependencies:
For this guide, we're going to test a very simple database that stores user names. The only important change from a regular moor
database is the constructor: We make the `QueryExecutor` argument explicit instead of having a no-args constructor that passes
a `FlutterQueryExector` to the superclass.
a `FlutterQueryExecutor` to the superclass.
```dart
import 'package:moor/moor.dart';

View File

@ -44,12 +44,12 @@ they behave.
Query streams that have been created outside a transaction work nicely together with
updates made in a transaction: All changes to tables will only be reported after the
transaction completes. Updates inside a transaction don't have an immediate effect on
streams, so your data will always be consistent and there aren't any uneccessary updates.
streams, so your data will always be consistent and there aren't any unnecessary updates.
With streams created _inside_ a `transaction` block (or a nested call in there), it's
a different story. Notably, they
- reflect on changes made in the transaction immediatly
- reflect on changes made in the transaction immediately
- complete when the transaction completes
This behavior is useful if you're collapsing streams inside a transaction, for instance by

View File

@ -10,7 +10,7 @@ import 'package:meta/meta.dart';
import 'package:path/path.dart';
import 'package:moor/moor.dart';
import 'package:moor/backends.dart';
import 'package:sqflite/sqflite.dart' as s;
import 'package:sqflite_sqlcipher/sqflite.dart' as s;
/// Signature of a function that runs when a database doesn't exist on file.
/// This can be useful to, for instance, load the database from an asset if it
@ -128,7 +128,7 @@ mixin _SqfliteExecutor on QueryDelegate {
@override
Future<void> runCustom(String statement, List args) {
return db.execute(statement);
return db.execute(statement, args);
}
@override

View File

@ -8,10 +8,7 @@ environment:
dependencies:
moor: ^2.0.0
sqflite:
git:
url: https://www.github.com/davidmartos96/sqflite_sqlcipher.git
path: sqflite
sqflite_sqlcipher: ^1.0.0+3
dependency_overrides:
moor:

View File

@ -30,5 +30,6 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final dbPath = await getDatabasesPath();
Directory(dbPath).createSync(recursive: true);
runAllTests(FfiExecutor(dbPath));
}

View File

@ -35,18 +35,15 @@ Future<void> main() async {
// Additional integration test for flutter: Test loading a database from asset
test('can load a database from asset', () async {
const dbNameInDevice = 'app_from_asset.db';
final folder = await getDatabasesPath();
final file = File(join(folder, dbNameInDevice));
if (await file.exists()) {
await file.delete();
final databasesPath = await getDatabasesPath();
final dbFile = File(join(databasesPath, 'app_from_asset.db'));
if (await dbFile.exists()) {
await dbFile.delete();
}
var didCallCreator = false;
final executor = FlutterQueryExecutor.inDatabaseFolder(
path: dbNameInDevice,
final executor = FlutterQueryExecutor(
path: dbFile.path,
singleInstance: true,
creator: (file) async {
final content = await rootBundle.load('test_asset.db');

View File

@ -38,6 +38,13 @@
[here](https://moor.simonbinder.eu/docs/getting-started/writing_queries/#upserts).
- Support using `MoorIsolates` in scenarios where only primitive messages can be passed between isolates.
## 2.4.2
- Fix `beforeOpen` callback deadlocking when using the isolate executor
([#431](https://github.com/simolus3/moor/issues/431))
- Fix limit clause not being applied when using `.join` afterwards
([#433](https://github.com/simolus3/moor/issues/433))
## 2.4.1
- Don't generate double quoted string literals in date time functions

View File

@ -139,7 +139,7 @@ class _TransactionIsolateExecutor extends _BaseExecutor
Future<bool> _openAtServer() async {
_executorId =
await client._channel.request(_NoArgsRequest.startTransaction) as int;
await client._channel.request<int>(_NoArgsRequest.startTransaction);
return true;
}

View File

@ -37,8 +37,8 @@ class _MoorServer {
return channels.isEmpty ? null : channels.first;
}
dynamic _handleRequest(Request r) {
final payload = r.payload;
dynamic _handleRequest(Request request) {
final payload = request.payload;
if (payload is _NoArgsRequest) {
switch (payload) {

View File

@ -74,6 +74,9 @@ class SimpleSelectStatement<T extends Table, D extends DataClass>
if (orderByExpr != null) {
statement.orderBy(orderByExpr.terms);
}
if (limitExpr != null) {
statement.limitExpr = limitExpr;
}
return statement;
}

View File

@ -20,7 +20,7 @@ void main() {
tearDown(() => db.close());
test('plus and minus on DateTimes', () async {
final nowExpr = currentDateAndTime;
const nowExpr = currentDateAndTime;
final tomorrow = nowExpr + const Duration(days: 1);
final nowStamp = nowExpr.secondsSinceEpoch;
final tomorrowStamp = tomorrow.secondsSinceEpoch;

View File

@ -88,6 +88,21 @@ void _runTests(
expect(result, isEmpty);
});
test('can run beforeOpen', () async {
var beforeOpenCalled = false;
final database = TodoDb.connect(isolateConnection);
database.migration = MigrationStrategy(beforeOpen: (details) async {
await database.customStatement('PRAGMA foreign_keys = ON');
beforeOpenCalled = true;
});
// run a select statement to verify that the database is open
await database.customSelectQuery('SELECT 1').get();
await database.close();
expect(beforeOpenCalled, isTrue);
});
test('stream queries work as expected', () async {
final database = TodoDb.connect(isolateConnection);
final initialCompanion = TodosTableCompanion.insert(content: 'my content');

View File

@ -116,6 +116,18 @@ void main() {
argThat(contains('WHERE t.id < ? ORDER BY t.title ASC')), [3]));
});
test('limit clause is kept', () async {
final todos = db.alias(db.todosTable, 't');
final categories = db.alias(db.categories, 'c');
final normalQuery = db.select(todos)..limit(10, offset: 5);
await normalQuery.join(
[innerJoin(categories, categories.id.equalsExp(todos.category))]).get();
verify(executor.runSelect(argThat(contains('LIMIT 10 OFFSET 5')), []));
});
test('can be watched', () {
final todos = db.alias(db.todosTable, 't');
final categories = db.alias(db.categories, 'c');

View File

@ -70,6 +70,8 @@ class _SQLiteBindings {
int Function(Pointer<Database> db) sqlite3_extended_errcode;
Pointer<CBlob> Function(int code) sqlite3_errstr;
Pointer<CBlob> Function(Pointer<Database> database) sqlite3_errmsg;
int Function(Pointer<Database> database, int onOff)
sqlite3_extended_result_codes;
int Function(Pointer<Statement> statement, int columnIndex, double value)
sqlite3_bind_double;
@ -176,6 +178,10 @@ class _SQLiteBindings {
sqlite3_errmsg = sqlite
.lookup<NativeFunction<sqlite3_errmsg_native_t>>('sqlite3_errmsg')
.asFunction();
sqlite3_extended_result_codes = sqlite
.lookup<NativeFunction<sqlite3_extended_result_codes_t>>(
'sqlite3_extended_result_codes')
.asFunction();
sqlite3_column_count = sqlite
.lookup<NativeFunction<sqlite3_column_count_native_t>>(
'sqlite3_column_count')

View File

@ -44,6 +44,9 @@ typedef sqlite3_errstr_native_t = Pointer<CBlob> Function(Int32 error);
typedef sqlite3_errmsg_native_t = Pointer<CBlob> Function(
Pointer<Database> database);
typedef sqlite3_extended_result_codes_t = Int32 Function(
Pointer<Database> database, Int32 onOff);
typedef sqlite3_column_count_native_t = Int32 Function(
Pointer<Statement> statement);

View File

@ -20,6 +20,7 @@ part 'moor_functions.dart';
part 'prepared_statement.dart';
const _openingFlags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE;
const _readOnlyOpeningFlags = Flags.SQLITE_OPEN_READONLY;
/// A opened sqlite database.
class Database {
@ -39,18 +40,25 @@ class Database {
factory Database.memory() => Database.open(':memory:');
/// Opens an sqlite3 database from a filename.
factory Database.open(String fileName) {
///
/// Unless [readOnly] is set to true, database is opened in read/write mode.
factory Database.open(String fileName, {bool readOnly = false}) {
final dbOut = allocate<Pointer<types.Database>>();
final pathC = CBlob.allocateString(fileName);
final openingFlags =
(readOnly ?? false) ? _readOnlyOpeningFlags : _openingFlags;
final resultCode =
bindings.sqlite3_open_v2(pathC, dbOut, _openingFlags, nullPtr());
bindings.sqlite3_open_v2(pathC, dbOut, openingFlags, nullPtr());
final dbPointer = dbOut.value;
dbOut.free();
pathC.free();
if (resultCode == Errors.SQLITE_OK) {
// Turn extended result code to on.
bindings.sqlite3_extended_result_codes(dbPointer, 1);
return Database._(dbPointer);
} else {
bindings.sqlite3_close_v2(dbPointer);
@ -110,7 +118,6 @@ class Database {
final result =
bindings.sqlite3_exec(_db, sqlPtr, nullPtr(), nullPtr(), errorOut);
sqlPtr.free();
final errorPtr = errorOut.value;
@ -124,7 +131,7 @@ class Database {
}
if (result != Errors.SQLITE_OK) {
throw SqliteException(errorMsg);
throw SqliteException(result, errorMsg);
}
}

View File

@ -4,10 +4,21 @@ class SqliteException implements Exception {
final String message;
final String explanation;
SqliteException(this.message, [this.explanation]);
/// SQLite extended result code.
///
/// As defined in https://sqlite.org/rescode.html, it represent an error code,
/// providing some idea of the cause of the failure.
final int extendedResultCode;
factory SqliteException._fromErrorCode(Pointer<types.Database> db,
[int code]) {
/// SQLite primary result code.
///
/// As defined in https://sqlite.org/rescode.html, it represent an error code,
/// providing some idea of the cause of the failure.
int get resultCode => extendedResultCode & 0xFF;
SqliteException(this.extendedResultCode, this.message, [this.explanation]);
factory SqliteException._fromErrorCode(Pointer<types.Database> db, int code) {
// We don't need to free the pointer returned by sqlite3_errmsg: "Memory to
// hold the error message string is managed internally. The application does
// not need to worry about freeing the result."
@ -24,15 +35,15 @@ class SqliteException implements Exception {
explanation = '$errStr (code $extendedCode)';
}
return SqliteException(dbMessage, explanation);
return SqliteException(code, dbMessage, explanation);
}
@override
String toString() {
if (explanation == null) {
return 'SqliteException: $message';
return 'SqliteException($extendedResultCode): $message';
} else {
return 'SqliteException: $message, $explanation';
return 'SqliteException($extendedResultCode): $message, $explanation';
}
}
}

View File

@ -1,4 +1,7 @@
import 'dart:io';
import 'package:moor_ffi/database.dart';
import 'package:path/path.dart';
import 'package:test/test.dart';
void main() {
@ -53,4 +56,40 @@ void main() {
db.close();
});
test('open read-only', () async {
final path = join('.dart_tool', 'moor_ffi', 'test', 'read_only.db');
// Make sure the path exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
// but not the db
try {
await File(path).delete();
} catch (_) {}
// Opening a non-existent database should fail
try {
Database.open(path, readOnly: true);
fail('should fail');
} on SqliteException catch (_) {}
// Open in read-write mode to create the database
var db = Database.open(path);
// Change the user version to test read-write access
db.setUserVersion(1);
db.close();
// Open in read-only
db = Database.open(path, readOnly: true);
// Change the user version to test read-only mode
try {
db.setUserVersion(2);
fail('should fail');
} on SqliteException catch (_) {}
// Check that it has not changed
expect(db.userVersion(), 1);
db.close();
});
}

View File

@ -0,0 +1,125 @@
import 'dart:io';
import 'package:moor_ffi/database.dart';
import 'package:moor_ffi/src/bindings/constants.dart';
import 'package:path/path.dart';
import 'package:test/test.dart';
void main() {
test('open read-only exception', () async {
final path =
join('.dart_tool', 'moor_ffi', 'test', 'read_only_exception.db');
// Make sure the path exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
// but not the db
try {
await File(path).delete();
} catch (_) {}
// Opening a non-existent database should fail
try {
Database.open(path, readOnly: true);
fail('should fail');
} on SqliteException catch (e) {
expect(e.extendedResultCode, Errors.SQLITE_CANTOPEN);
expect(e.toString(), startsWith('SqliteException(14): '));
}
});
test('statement exception', () async {
// Only testing some common errors...
final db = Database.memory();
// Basic syntax error
try {
db.execute('DUMMY');
fail('should fail');
} on SqliteException catch (e) {
expect(e.extendedResultCode, Errors.SQLITE_ERROR);
expect(e.resultCode, Errors.SQLITE_ERROR);
expect(e.toString(), startsWith('SqliteException(1): '));
}
// No table
try {
db.execute('SELECT * FROM missing_table');
fail('should fail');
} on SqliteException catch (e) {
expect(e.extendedResultCode, Errors.SQLITE_ERROR);
expect(e.resultCode, Errors.SQLITE_ERROR);
}
// Constraint primary key
db.execute('CREATE TABLE Test (name TEXT PRIMARY KEY)');
db.execute("INSERT INTO Test(name) VALUES('test1')");
try {
db.execute("INSERT INTO Test(name) VALUES('test1')");
fail('should fail');
} on SqliteException catch (e) {
// SQLITE_CONSTRAINT_PRIMARYKEY (1555)
expect(e.extendedResultCode, 1555);
expect(e.resultCode, Errors.SQLITE_CONSTRAINT);
expect(e.toString(), startsWith('SqliteException(1555): '));
}
// Constraint using prepared statement
db.execute('CREATE TABLE Test2 (id PRIMARY KEY, name TEXT UNIQUE)');
final prepared = db.prepare('INSERT INTO Test2(name) VALUES(?)');
prepared.execute(['test2']);
try {
prepared.execute(['test2']);
fail('should fail');
} on SqliteException catch (e) {
// SQLITE_CONSTRAINT_UNIQUE (2067)
expect(e.extendedResultCode, 2067);
expect(e.resultCode, Errors.SQLITE_CONSTRAINT);
}
db.close();
});
test('busy exception', () async {
final path = join('.dart_tool', 'moor_ffi', 'test', 'busy.db');
// Make sure the path exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
// but not the db
try {
await File(path).delete();
} catch (_) {}
final db1 = Database.open(path);
final db2 = Database.open(path);
db1.execute('BEGIN EXCLUSIVE TRANSACTION');
try {
db2.execute('BEGIN EXCLUSIVE TRANSACTION');
fail('should fail');
} on SqliteException catch (e) {
expect(e.extendedResultCode, Errors.SQLITE_BUSY);
expect(e.resultCode, Errors.SQLITE_BUSY);
}
db1.close();
db2.close();
});
test('invalid format', () async {
final path = join('.dart_tool', 'moor_ffi', 'test', 'invalid_format.db');
// Make sure the path exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
await File(path).writeAsString('not a database file');
final db = Database.open(path);
try {
db.setUserVersion(1);
fail('should fail');
} on SqliteException catch (e) {
expect(e.extendedResultCode, Errors.SQLITE_NOTADB);
expect(e.resultCode, Errors.SQLITE_NOTADB);
}
db.close();
});
}