mirror of https://github.com/AMT-Cheif/drift.git
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:
commit
1546f323b2
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/).
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -30,5 +30,6 @@ Future<void> main() async {
|
|||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dbPath = await getDatabasesPath();
|
||||
Directory(dbPath).createSync(recursive: true);
|
||||
runAllTests(FfiExecutor(dbPath));
|
||||
}
|
|
@ -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');
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue