Merge branch 'develop' into beta

This commit is contained in:
Simon Binder 2019-09-22 21:45:51 +02:00
commit 18639a364c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
269 changed files with 10677 additions and 2808 deletions

View File

@ -76,7 +76,8 @@ updates that span multiple versions, we should follow these steps
2. Make sure each package has the correct dependencies: `moor_flutter` version `1.x` should depend
on moor `1.x` as well to ensure users will always `pub get` moor packages that are compatible
with each other.
3. Comment out the `dependency_overrides` section
3. Comment out the `dependency_overrides` section in `moor`, `moor/tool/analyzer_plugin`, `moor_flutter`,
`moor_generator` and `sqlparser`.
4. Publish packages in this order to avoid scoring penalties caused by versions not existing:
1. `moor`
2. `moor_generator`

View File

@ -7,7 +7,6 @@ analyzer:
unused_local_variable: error
dead_code: error
override_on_non_overriding_method: error
deprecated_member_use_from_same_package: ignore
exclude:
- "**/*.g.dart"
# Will be analyzed anyway, nobody knows why ¯\_(ツ)_/¯. We're only analyzing lib/ and test/ as a workaround
@ -82,4 +81,4 @@ linter:
- unnecessary_this
- unrelated_type_equality_checks
- use_rethrow_when_possible
- valid_regexps
- valid_regexps

View File

@ -18,18 +18,14 @@ targets:
builders:
moor_generator:
options:
generate_private_watch_methods: true
write_from_json_string_constructor: true
```
At the moment, moor supports these options:
* `generate_private_watch_methods`: boolean. There was a bug in the generator where
[compiled queries]({{<relref "../Using SQL/custom_queries.md">}}) that start with
an underscore did generate a watch method that didn't start with an underscore
(see [#107](https://github.com/simolus3/moor/issues/107)). Fixing this would be
a breaking change, so the fix is opt-in by enabling this option. This flag is
available since 1.7 and will be removed in moor 2.0, where this flag will always
be enabled.
* `write_from_json_string_constructor`: boolean. Adds a `.fromJsonString` factory
constructor to generated data classes. By default, we only write a `.fromJson`
constructor that takes a `Map<String, dynamic>`.
constructor that takes a `Map<String, dynamic>`.
* `override_hash_and_equals_in_result_sets`: boolean. When moor generates another class
to hold the result of generated select queries, this flag controls whether moor should
override `operator ==` and `hashCode` in those classes.

View File

@ -7,6 +7,7 @@ description: Example apps using moor
We have an [example in the repo](https://github.com/simolus3/moor/tree/master/moor_flutter/example), it's a simple todo list app,
written with moor.
written with moor. [Rody Davis](https://github.com/AppleEducate) has built a cleaner version of the example that works on all
Flutter platforms - including Web and Desktop! You can check it out [here](https://github.com/AppleEducate/moor_shared).
The [HackerNews reader app](https://github.com/filiph/hn_app) from the [Boring Flutter Show](https://www.youtube.com/playlist?list=PLjxrf2q8roU3ahJVrSgAnPjzkpGmL9Czl) also uses moor to keep a list of favorite articles.

View File

@ -0,0 +1,78 @@
---
title: "Expressions"
linkTitle: "Expressions"
description: Deep-dive into what kind of SQL expressions can be written in Dart
weight: 200
---
Expressions are pieces of sql that return a value when the database interprets them.
The Dart API from moor allows you to write most expressions in Dart and then convert
them to sql. Expressions are used in all kinds of situations. For instance, `where`
expects an expression that returns a boolean.
In most cases, you're writing an expression that combines other expressions. Any
column name is a valid expression, so for most `where` clauses you'll be writing
a expression that wraps a column name in some kind of comparison.
## Comparisons
Every expression can be compared to a value by using `equals`. If you want to compare
an expression to another expression, you can use `equalsExpr`. For numeric and datetime
expressions, you can also use a variety of methods like `isSmallerThan`, `isSmallerOrEqual`
and so on to compare them:
```dart
// find all animals with less than 5 legs:
(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get();
// find all animals who's average livespan is shorter than their amount of legs (poor flies)
(select(animals)..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs)));
Future<List<Animal>> findAnimalsByLegs(int legCount) {
return (select(animals)..where((a) => a.legs.equals(legCount))).get();
}
```
## Boolean algebra
You can nest boolean expressions by using the top-level `and`, `or` and `not` functions
exposed by moor:
```dart
// find all animals that aren't mammals and have 4 legs
select(animals)..where((a) => and(not(a.isMammal), a.amountOfLegs.equals(4)))
```
## Nullability
To check whether an expression returns null, you can use the top-level `isNull` function,
which takes any expression and returns a boolean expression. The expression returned will
resolve to `true` if the inner expression resolves to null and `false` otherwise.
As you would expect, `isNotNull` works the other way around.
## Date and Time
For columns and expressions that return a `DateTime`, you can use the top-level
`year`, `month`, `day`, `hour`, `minute` and `second` functions to extract individual
fields from that date:
```dart
select(users)..where((u) => year(u.birthDate).isLessThan(1950))
```
To obtain the current date or the current time as an expression, use the `currentDate`
and `currentDateAndTime` constants provided by moor.
## `IN` and `NOT IN`
You can check whether an expression is in a list of values by using the `isIn` function:
```dart
select(animals)..where((a) => isIn(a.amountOfLegs, [3, 7, 4, 2]))
```
Again, the `isNotIn` function works the other way around.
## Custom expressions
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
It takes a `sql` parameter that let's you write custom expressions:
```dart
const inactive = CustomExpression<bool, BoolType>("julianday('now') - julianday(last_login) > 60");
select(users)..where((u) => inactive);
```
_Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like
you need to use them because a feature you use is not available in moor, consider creating an issue
to let us know. If you just prefer sql, you could also take a look at
[compiled sql]({{< ref "../Using SQL/custom_queries.md" >}}) which is typesafe to use.

View File

@ -4,6 +4,7 @@ linkTitle: "Writing queries"
description: Learn how to write database queries in pure Dart with moor
aliases:
- /queries/
weight: 100
---
{{% pageinfo %}}
@ -55,6 +56,20 @@ Future<List<TodoEntry>> sortEntriesAlphabetically() {
```
You can also reverse the order by setting the `mode` property of the `OrderingTerm` to
`OrderingMode.desc`.
### Single values
If you now 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) {
return (select(todos)..where((t) => t.id.equals(id))).watchSingle();
}
```
If an entry with the provided id exists, it will be sent to the stream. Otherwise,
`null` will be added to stream. If a query used with `watchSingle` ever returns
more than one entry (which is impossible in this case), an error will be added
instead.
## Updates and deletes
You can use the generated classes to update individual fields of any row:
```dart
@ -88,6 +103,21 @@ Future feelingLazy() {
__⚠ Caution:__ If you don't explicitly add a `where` clause on updates or deletes,
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
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.
Why is that necessary? If a field was set to `null`, we wouldn't know whether we need
to set that column back to null in the database or if we should just leave it unchanged.
Fields in the companions have a special `Value.absent()` state which makes this explicit.
Companions also have a special constructor for inserts - all columns which don't have
a default value and aren't nullable are marked `@required` on that constructor. This makes
companions easier to use for inserts because you know which fields to set.
{{% /alert %}}
## Inserts
You can very easily insert any valid object into tables. As some values can be absent
(like default values that we don't have to set explicitly), we again use the
@ -109,4 +139,7 @@ addTodoEntry(
```
If a column is nullable or has a default value (this includes auto-increments), the field
can be omitted. All other fields must be set and non-null. The `insert` method will throw
otherwise.
otherwise.
Multiple inserts can be batched by using `insertAll` - it takes a list of companions instead
of a single companion.

View File

@ -1,11 +1,18 @@
---
title: "Custom queries"
title: "(Legacy) Custom queries"
weight: 10
description: Let moor generate Dart from your SQL statements
aliases:
- /queries/custom
---
{{% alert title="Outdated feature" color="warning" %}}
With moor 2.0, we moved the new `.moor` files out of preview and added some powerful features to them.
They are easier to use than the approaches described here. While these features will continue to
be supported, moor files will get better tooling support in the future and we recommend to
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
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
@ -39,6 +46,15 @@ Queries can have parameters in them by using the `?` or `:name` syntax. When you
moor will figure out an appropriate type for them and include them in the generated methods. For instance,
`'categoryById': 'SELECT * FROM categories WHERE id = :id'` will generate the method `categoryById(int id)`.
{{% alert title="On table names" color="info" %}}
To use this feature, it's helpful to know how Dart tables are named in sql. For tables that don't
override `tableName`, the name in sql will be the `snake_case` of the class name. So a Dart table
called `Categories` will be named `categories`, a table called `UserAddressInformation` would be
called `user_address_information`. The same rule applies to column getters without an explicit name.
Tables and columns declared in [Moor files]({{< relref "moor_files.md" >}}) will always have the
name you specified.
{{% /alert %}}
You can also use `UPDATE` or `DELETE` statements here. Of course, this feature is also available for
[daos]({{< relref "../Advanced Features/daos.md" >}}),
and it perfectly integrates with auto-updating streams by analyzing what tables you're reading from or

View File

@ -1,42 +0,0 @@
---
title: "Tables from SQL"
weight: 20
description: Generate tables from `CREATE TABLE` statements.
---
{{% alert title="Experimental feature" %}}
At the moment, creating table classes from `CREATE TABLE` statements is an experimental feature.
If you run into any issues, please create an issue and let us know, thanks!
{{% /alert %}}
With moor, you can specify your table classes in Dart and it will generate matching
`CREATE TABLE` statements for you. But if you prefer to write `CREATE TABLE` statements and have
moor generating fitting Dart classes, that works too.
To use this feature, create a (or multiple) `.moor` file somewhere in your project. You can fill
them with create table statements:
```sql
CREATE TABLE states (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
CREATE TABLE experiments (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL,
state INT REFERENCES states(id) ON UPDATE CASCADE ON DELETE SET NULL
)
```
Then, import these tables to your database with:
```dart
@UseMoor(include: {'experiments.moor'})
class ExperimentsDb extends _$ExperimentsDb {
```
All the tables will then be available inside your database class, just like they
would be if you wrote them in Dart. If you want to use this feature on an DAO,
you'll also need to `include` the .moor file on that class. Moor supports both
relative imports (like above) and absolute imports (like `package:your_app/src/tables/experiments.moor`)
Of course, this feature works perfectly together with features like generated
custom queries and query-streams.

View File

@ -0,0 +1,148 @@
---
title: "Moor files"
weight: 1
description: Learn everything about the new `.moor` files which can contain tables and queries
aliases:
- /docs/using-sql/custom_tables/ # Redirect from outdated "custom tables" page which has been deleted
---
Moor files are a new feature that lets you write all your database code in SQL - moor will generate typesafe APIs for them.
## Getting started
To use this feature, lets create two files: `database.dart` and `tables.moor`. The Dart file is pretty straightforward:
```dart
import 'package:moor/moor.dart';
part 'database.g.dart';
@UseMoor(
include: {'tables.moor'},
)
class MoorDb extends _$MoorDb {
MoorDb() : super(FlutterQueryExecutor.inDatabaseFolder('app.db'));
@override
int get schemaVersion => 1;
}
```
We can now declare tables and queries in the moor file:
```sql
CREATE TABLE todos (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category INTEGER REFERENCES categories(id)
);
CREATE TABLE categories (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL
) AS Category; -- the AS xyz after the table defines the data class name
-- we can put named sql queries in here as well:
createEntry: INSERT INTO todos (title, content) VALUES (:title, :content);
deleteById: DELETE FROM todos WHERE id = :id;
watchAllTodos: SELECT * FROM todos;
```
After running the build runner, moor will write the `database.g.dart`
file which contains the `_$MoorDb` superclass. Let's take a look at
what we got:
- Generated data classes (`Todo` and `Category`), and companion versions
for inserts (see [Dart Interop](#dart-interop) for info). By default,
we strip a trailing "s" from the table name for the class. That's why
we used `AS Category` on the second table - it would have been called
`Categorie` otherwise.
- Methods to run the queries:
- a `Future<int> createEntry(String title, String content)` method. It
creates a new todo entry with the provided data and returns the id of
the entry created.
- `Future<int> deleteById(int id)`: Deletes a todo entry by its id, and
returns the amount of rows affected.
- `Selectable<AllTodosResult> allTodos()`. It can be used to get, or
watch, all todo entries. It can be used with `allTodos().get()` and
`allTodos().watch()`.
- Classes for select statements that don't match a table. In the example
above, thats the `AllTodosResult` class, which contains all fields from
`todos` and the description of the associated category.
## Variables
We support regular variables (`?`), explictly 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.
### Arrays
If you want to check whether a value is in an array of values, you can
use `IN ?`. That's not valid sql, but moor will desugar that at runtime. So, for this query:
```sql
entriesWithId: SELECT * FROM todos WHERE id IN ?;
```
Moor will generate a `Selectable<Todo> entriesWithId(List<int> ids)`
method (`entriesWithId([1,2])` would run `SELECT * ... id IN (?1, ?2)`
and bind the arguments accordingly). To support this, we only have two
restrictions:
1. __No explicit variables__: Running `WHERE id IN ?2` will be rejected
at build time. As the variable is expanded, giving it a single index is
invalid.
2. __No higher explicit index after a variable__: Running
`WHERE id IN ? OR title = ?2` will also be rejected. Expanding the
variable can clash with the explicit index, which is why moor forbids
it. Of course, `id IN ? OR title = ?` will work as expected.
## Imports
{{% alert title="Limited support" %}}
> Importing a moor file from another moor file will work as expected.
Unfortunately, importing a Dart file from moor does not work in all
scenarios. Please upvote [this issue](https://github.com/dart-lang/build/issues/493)
on the build package to help solve this.
{{% /alert %}}
You can put import statements at the top of a `moor` file:
```sql
import 'other.moor'; -- single quotes are required for imports
```
All tables reachable from the other file will then also be visible in
the current file and to the database that `includes` it. Importing
Dart files into a moor file will also work - then, all the tables
declared via Dart tables can be used inside queries.
## Dart interop
Moor files work perfectly together with moor's existing Dart API:
- you can write Dart queries for moor files:
```dart
Future<void> insert(TodosCompanion companion) async {
await into(todos).insert(companion);
}
```
- by importing Dart files into a moor file, you can write sql queries for
tables declared in Dart.
- generated methods for queries can be used in transactions, they work
together with auto-updating queries, etc.
You can make most of both SQL and Dart by "Dart Templates", which is a
Dart expression that gets inlined to a query. To use them, declare a
$-variable in a query:
```sql
_filterTodos: SELECT * FROM todos WHERE $predicate;
```
Moor will generate a `Selectable<Todo> _filterTodos(Expression<bool, BoolType> predicate)` method which can be used to construct dynamic
filters at runtime:
```dart
Stream<List<Todo>> watchInCategory(int category) {
return _filterTodos(todos.category.equals(category)).watch();
}
```
This feature works for
- expressions
- single ordering terms: `SELECT * FROM todos ORDER BY $term, id ASC`
will generate a method taking an `OrderingTerm`.
- whole order-by clauses: `SELECT * FROM todos ORDER BY $order`
- limit clauses: `SELECT * FROM todos LIMIT $limit`

View File

@ -16,7 +16,7 @@ following example, which deals with deleting a category, we move all todo entrie
in that category back to the default category:
```dart
Future deleteCategory(Category category) {
return transaction((_) async {
return transaction(() async {
// first, move the affected todo entries back to the default category
await customUpdate(
'UPDATE todos SET category = NULL WHERE category = ?',
@ -30,22 +30,9 @@ Future deleteCategory(Category category) {
}
```
{{% alert title="About that _" color="info" %}}
You might have noticed that `_` parameter on the `transaction()` callback. That parameter would
be a special version of the database that runs all the methods called on it inside the transaction.
In previous moor versions, it was important to call everything on that parameter, e.g.
```dart
transaction((db) async {
await db.delete(categories).delete(category);
});
```
Starting from moor 1.6, this is no longer neccessary, we can figure out that you meant to run that
in a transaction because it was called from inside a `transaction` callback. We're going to remove
that parameter entirely in moor 2.0.
{{% /alert %}}
## ⚠️ Gotchas
There are a couple of things that should be kept in mind when working with transactions:
1. __Await all calls__: All queries inside the transaction must be `await`-ed. The transaction
will complete when the inner method completes. Without `await`, some queries might be operating
on the transaction after it has been closed! This can cause data loss or runtime crashes.

View File

@ -0,0 +1,34 @@
import 'dart:io';
import 'package:flutter/widgets.dart' show WidgetsFlutterBinding;
import 'package:moor_ffi/moor_ffi.dart';
import 'package:tests/tests.dart';
import 'package:moor_flutter/moor_flutter.dart';
import 'package:sqflite/sqflite.dart' show getDatabasesPath;
import 'package:path/path.dart';
class FfiExecutor extends TestExecutor {
final String dbPath;
FfiExecutor(this.dbPath);
@override
QueryExecutor createExecutor() {
return VmDatabase(File(join(dbPath, 'app_ffi.db')));
}
@override
Future deleteData() async {
final file = File(join(dbPath, 'app_ffi.db'));
if (await file.exists()) {
await file.delete();
}
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final dbPath = await getDatabasesPath();
runAllTests(FfiExecutor(dbPath));
}

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart' show WidgetsFlutterBinding;
import 'package:tests/tests.dart';
import 'package:test/test.dart';
import 'package:moor_flutter/moor_flutter.dart';
@ -27,7 +28,9 @@ class SqfliteExecutor extends TestExecutor {
}
}
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runAllTests(SqfliteExecutor());
// Additional integration test for flutter: Test loading a database from asset

View File

@ -7,7 +7,7 @@ packages:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.36.4"
version: "0.38.3"
args:
dependency: transitive
description:
@ -21,14 +21,14 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "2.3.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "1.0.5"
charcode:
dependency: transitive
description:
@ -56,7 +56,7 @@ packages:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.1.3"
csslib:
dependency: transitive
description:
@ -75,7 +75,7 @@ packages:
name: front_end
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.19"
version: "0.1.25"
glob:
dependency: transitive
description:
@ -131,21 +131,14 @@ packages:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "3.0.0"
kernel:
dependency: transitive
description:
name: kernel
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.19"
version: "0.3.25"
matcher:
dependency: transitive
description:
@ -173,14 +166,21 @@ packages:
path: "../../../moor"
relative: true
source: path
version: "1.6.0"
version: "1.7.2"
moor_ffi:
dependency: "direct main"
description:
path: "../../../moor_ffi"
relative: true
source: path
version: "0.0.1"
moor_flutter:
dependency: "direct main"
description:
path: "../../../moor_flutter"
relative: true
source: path
version: "1.6.0"
version: "1.7.0"
multi_server_socket:
dependency: transitive
description:
@ -194,14 +194,14 @@ packages:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.5"
version: "1.4.8"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.1.0"
package_resolver:
dependency: transitive
description:
@ -215,14 +215,14 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
version: "1.6.4"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0+1"
pool:
dependency: transitive
description:
@ -297,7 +297,7 @@ packages:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6+2"
version: "1.1.6+5"
stack_trace:
dependency: transitive
description:
@ -318,7 +318,7 @@ packages:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "1.0.5"
synchronized:
dependency: transitive
description:
@ -339,21 +339,21 @@ packages:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.3"
version: "1.6.10"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
version: "0.2.7"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
version: "0.2.9+1"
tests:
dependency: "direct main"
description:
@ -375,13 +375,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
vm_service_client:
vm_service:
dependency: transitive
description:
name: vm_service_client
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.6+2"
version: "2.0.0"
watcher:
dependency: transitive
description:
@ -395,14 +395,14 @@ packages:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.13"
version: "1.0.15"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.16"
version: "2.2.0"
sdks:
dart: ">=2.4.0 <3.0.0"
dart: ">=2.5.0-dev <=2.6.0-dev.1.0.flutter-7c1821c4aa"
flutter: ">=1.2.1 <2.0.0"

View File

@ -11,6 +11,8 @@ dependencies:
moor_flutter:
tests:
path: ../tests
moor_ffi:
path: ../../../moor_ffi
dev_dependencies:
test:

View File

@ -75,6 +75,7 @@ class PreferenceConverter extends TypeConverter<Preferences, String> {
WHERE (f.first_user = :user OR f.second_user = :user)''',
'userCount': 'SELECT COUNT(id) FROM users',
'settingsFor': 'SELECT preferences FROM users WHERE id = :user',
'usersById': 'SELECT * FROM users WHERE id IN ?',
},
)
class Database extends _$Database {
@ -98,7 +99,7 @@ class Database extends _$Database {
await m.createTable(friendships);
}
},
beforeOpen: (_, details) async {
beforeOpen: (details) async {
if (details.wasCreated) {
await into(users)
.insertAll([People.dash, People.duke, People.gopher]);
@ -108,10 +109,11 @@ class Database extends _$Database {
}
Future<void> deleteUser(User user, {bool fail = false}) {
return transaction((_) async {
return transaction(() async {
final id = user.id;
delete(friendships)
.where((f) => or(f.firstUser.equals(id), f.secondUser.equals(id)));
await (delete(friendships)
..where((f) => or(f.firstUser.equals(id), f.secondUser.equals(id))))
.go();
if (fail) {
throw Exception('oh no, the query misteriously failed!');

View File

@ -21,7 +21,7 @@ Map<String, dynamic> _$PreferencesToJson(Preferences instance) =>
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class User extends DataClass implements Insertable<User> {
final int id;
final String name;
@ -127,11 +127,11 @@ class User extends DataClass implements Insertable<User> {
bool operator ==(other) =>
identical(this, other) ||
(other is User &&
other.id == id &&
other.name == name &&
other.birthDate == birthDate &&
other.profilePicture == profilePicture &&
other.preferences == preferences);
other.id == this.id &&
other.name == this.name &&
other.birthDate == this.birthDate &&
other.profilePicture == this.profilePicture &&
other.preferences == this.preferences);
}
class UsersCompanion extends UpdateCompanion<User> {
@ -147,6 +147,14 @@ class UsersCompanion extends UpdateCompanion<User> {
this.profilePicture = const Value.absent(),
this.preferences = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
@required String name,
@required DateTime birthDate,
this.profilePicture = const Value.absent(),
this.preferences = const Value.absent(),
}) : name = Value(name),
birthDate = Value(birthDate);
UsersCompanion copyWith(
{Value<int> id,
Value<String> name,
@ -172,7 +180,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _nameMeta = const VerificationMeta('name');
@ -388,9 +397,9 @@ class Friendship extends DataClass implements Insertable<Friendship> {
bool operator ==(other) =>
identical(this, other) ||
(other is Friendship &&
other.firstUser == firstUser &&
other.secondUser == secondUser &&
other.reallyGoodFriends == reallyGoodFriends);
other.firstUser == this.firstUser &&
other.secondUser == this.secondUser &&
other.reallyGoodFriends == this.reallyGoodFriends);
}
class FriendshipsCompanion extends UpdateCompanion<Friendship> {
@ -402,6 +411,12 @@ class FriendshipsCompanion extends UpdateCompanion<Friendship> {
this.secondUser = const Value.absent(),
this.reallyGoodFriends = const Value.absent(),
});
FriendshipsCompanion.insert({
@required int firstUser,
@required int secondUser,
this.reallyGoodFriends = const Value.absent(),
}) : firstUser = Value(firstUser),
secondUser = Value(secondUser);
FriendshipsCompanion copyWith(
{Value<int> firstUser,
Value<int> secondUser,
@ -520,27 +535,6 @@ class $FriendshipsTable extends Friendships
}
}
class AmountOfGoodFriendsResult {
final int count;
AmountOfGoodFriendsResult({
this.count,
});
}
class UserCountResult {
final int cOUNTid;
UserCountResult({
this.cOUNTid,
});
}
class SettingsForResult {
final Preferences preferences;
SettingsForResult({
this.preferences,
});
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$UsersTable _users;
@ -558,125 +552,99 @@ abstract class _$Database extends GeneratedDatabase {
);
}
Future<List<User>> mostPopularUsers(
int amount,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
Selectable<User> mostPopularUsersQuery(int amount) {
return customSelectQuery(
'SELECT * FROM users u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
variables: [
Variable.withInt(amount),
]).then((rows) => rows.map(_rowToUser).toList());
variables: [Variable.withInt(amount)],
readsFrom: {users, friendships}).map(_rowToUser);
}
Future<List<User>> mostPopularUsers(int amount) {
return mostPopularUsersQuery(amount).get();
}
Stream<List<User>> watchMostPopularUsers(int amount) {
return customSelectStream(
'SELECT * FROM users u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
variables: [
Variable.withInt(amount),
],
readsFrom: {
users,
friendships
}).map((rows) => rows.map(_rowToUser).toList());
return mostPopularUsersQuery(amount).watch();
}
AmountOfGoodFriendsResult _rowToAmountOfGoodFriendsResult(QueryRow row) {
return AmountOfGoodFriendsResult(
count: row.readInt('COUNT(*)'),
);
}
Future<List<AmountOfGoodFriendsResult>> amountOfGoodFriends(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
Selectable<int> amountOfGoodFriendsQuery(int user) {
return customSelectQuery(
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToAmountOfGoodFriendsResult).toList());
}
Stream<List<AmountOfGoodFriendsResult>> watchAmountOfGoodFriends(int user) {
return customSelectStream(
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
Variable.withInt(user)
],
readsFrom: {
friendships
}).map((rows) => rows.map(_rowToAmountOfGoodFriendsResult).toList());
}).map((QueryRow row) => row.readInt('COUNT(*)'));
}
Future<List<User>> friendsOf(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
Future<List<int>> amountOfGoodFriends(int user) {
return amountOfGoodFriendsQuery(user).get();
}
Stream<List<int>> watchAmountOfGoodFriends(int user) {
return amountOfGoodFriendsQuery(user).watch();
}
Selectable<User> friendsOfQuery(int user) {
return customSelectQuery(
'SELECT u.* FROM friendships f\n INNER JOIN users u ON u.id IN (f.first_user, f.second_user) AND\n u.id != :user\n WHERE (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToUser).toList());
variables: [Variable.withInt(user)],
readsFrom: {friendships, users}).map(_rowToUser);
}
Future<List<User>> friendsOf(int user) {
return friendsOfQuery(user).get();
}
Stream<List<User>> watchFriendsOf(int user) {
return customSelectStream(
'SELECT u.* FROM friendships f\n INNER JOIN users u ON u.id IN (f.first_user, f.second_user) AND\n u.id != :user\n WHERE (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
],
readsFrom: {
friendships,
users
}).map((rows) => rows.map(_rowToUser).toList());
return friendsOfQuery(user).watch();
}
UserCountResult _rowToUserCountResult(QueryRow row) {
return UserCountResult(
cOUNTid: row.readInt('COUNT(id)'),
);
Selectable<int> userCountQuery() {
return customSelectQuery('SELECT COUNT(id) FROM users',
variables: [],
readsFrom: {users}).map((QueryRow row) => row.readInt('COUNT(id)'));
}
Future<List<UserCountResult>> userCount(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect('SELECT COUNT(id) FROM users',
variables: []).then((rows) => rows.map(_rowToUserCountResult).toList());
Future<List<int>> userCount() {
return userCountQuery().get();
}
Stream<List<UserCountResult>> watchUserCount() {
return customSelectStream('SELECT COUNT(id) FROM users',
variables: [], readsFrom: {users})
.map((rows) => rows.map(_rowToUserCountResult).toList());
Stream<List<int>> watchUserCount() {
return userCountQuery().watch();
}
SettingsForResult _rowToSettingsForResult(QueryRow row) {
return SettingsForResult(
preferences:
$UsersTable.$converter0.mapToDart(row.readString('preferences')),
);
Selectable<Preferences> settingsForQuery(int user) {
return customSelectQuery('SELECT preferences FROM users WHERE id = :user',
variables: [Variable.withInt(user)], readsFrom: {users})
.map((QueryRow row) =>
$UsersTable.$converter0.mapToDart(row.readString('preferences')));
}
Future<List<SettingsForResult>> settingsFor(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT preferences FROM users WHERE id = :user',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToSettingsForResult).toList());
Future<List<Preferences>> settingsFor(int user) {
return settingsForQuery(user).get();
}
Stream<List<SettingsForResult>> watchSettingsFor(int user) {
return customSelectStream('SELECT preferences FROM users WHERE id = :user',
variables: [
Variable.withInt(user),
],
readsFrom: {
users
}).map((rows) => rows.map(_rowToSettingsForResult).toList());
Stream<List<Preferences>> watchSettingsFor(int user) {
return settingsForQuery(user).watch();
}
Selectable<User> usersByIdQuery(List<int> var1) {
var $arrayStartIndex = 1;
final expandedvar1 = $expandVar($arrayStartIndex, var1.length);
$arrayStartIndex += var1.length;
return customSelectQuery('SELECT * FROM users WHERE id IN ($expandedvar1)',
variables: [for (var $ in var1) Variable.withInt($)],
readsFrom: {users}).map(_rowToUser);
}
Future<List<User>> usersById(List<int> var1) {
return usersByIdQuery(var1).get();
}
Stream<List<User>> watchUsersById(List<int> var1) {
return usersByIdQuery(var1).watch();
}
@override

View File

@ -23,4 +23,13 @@ void crudTests(TestExecutor executor) {
await db.makeFriends(a, b);
await expectation;
});
test('IN ? expressions can be expanded', () async {
// regression test for https://github.com/simolus3/moor/issues/156
final db = Database(executor.createExecutor());
final result = await db.usersById([1, 2, 3]);
expect(result.map((u) => u.name), ['Dash', 'Duke', 'Go Gopher']);
});
}

View File

@ -7,13 +7,13 @@ void customObjectTests(TestExecutor executor) {
test('custom objects', () async {
final db = Database(executor.createExecutor());
var preferences = await db.settingsFor(1);
expect(preferences.single.preferences, isNull);
var preferences = await db.settingsForQuery(1).getSingle();
expect(preferences, isNull);
await db.updateSettings(1, Preferences(true));
preferences = await db.settingsFor(1);
preferences = await db.settingsForQuery(1).getSingle();
expect(preferences.single.preferences.receiveEmails, true);
expect(preferences.receiveEmails, true);
await db.close();
});

View File

@ -9,8 +9,8 @@ void migrationTests(TestExecutor executor) {
final database = Database(executor.createExecutor(), schemaVersion: 1);
// we write 3 users when the database is created
final count = await database.userCount();
expect(count.single.cOUNTid, 3);
final count = await database.userCountQuery().getSingle();
expect(count, 3);
await database.close();
});
@ -23,8 +23,8 @@ void migrationTests(TestExecutor executor) {
database = Database(executor.createExecutor(), schemaVersion: 2);
// the 3 initial users plus People.florian
final count = await database.userCount();
expect(count.single.cOUNTid, 4);
final count = await database.userCountQuery().getSingle();
expect(count, 4);
await database.close();
});

View File

@ -8,7 +8,8 @@ void transactionTests(TestExecutor executor) {
test('transactions write data', () async {
final db = Database(executor.createExecutor());
await db.transaction((_) async {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
await db.transaction(() async {
final florianId = await db.writeUser(People.florian);
final dash = await db.getUserById(People.dashId);
@ -18,10 +19,10 @@ void transactionTests(TestExecutor executor) {
});
final countResult = await db.userCount();
expect(countResult.single.cOUNTid, 4);
expect(countResult.single, 4);
final friendsResult = await db.amountOfGoodFriends(People.dashId);
expect(friendsResult.single.count, 1);
expect(friendsResult.single, 1);
await db.close();
});
@ -30,7 +31,8 @@ void transactionTests(TestExecutor executor) {
final db = Database(executor.createExecutor());
try {
await db.transaction((_) async {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
await db.transaction(() async {
final florianId = await db.writeUser(People.florian);
final dash = await db.getUserById(People.dashId);
@ -43,10 +45,10 @@ void transactionTests(TestExecutor executor) {
} on Exception catch (_) {}
final countResult = await db.userCount();
expect(countResult.single.cOUNTid, 3); // only the default folks
expect(countResult.single, 3); // only the default folks
final friendsResult = await db.amountOfGoodFriends(People.dashId);
expect(friendsResult.single.count, 0); // no friendship was inserted
expect(friendsResult.single, 0); // no friendship was inserted
await db.close();
});

11
extras/integration_tests/vm/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Files and directories created by pub
.dart_tool/
.packages
# Remove the following pattern if you wish to check in your lock file
pubspec.lock
# Conventional directory for build outputs
build/
# Directory created by dartdoc
doc/api/

View File

@ -0,0 +1,21 @@
name: vm
description: A sample command-line application.
# version: 1.0.0
# homepage: https://www.example.com
# author: Simon Binder <oss@simonbinder.eu>
environment:
sdk: '>=2.4.0 <3.0.0'
dependencies:
moor_ffi:
path: ../../../moor_ffi
tests:
path: ../tests
dev_dependencies:
test: ^1.5.0
dependency_overrides:
moor:
path: ../../../moor

View File

@ -0,0 +1,27 @@
import 'dart:io';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:tests/tests.dart';
import 'package:path/path.dart' show join;
class VmExecutor extends TestExecutor {
static String fileName = 'moor-vm-tests-${DateTime.now().toIso8601String()}';
final file = File(join(Directory.systemTemp.path, fileName));
@override
QueryExecutor createExecutor() {
return VmDatabase(file);
}
@override
Future deleteData() async {
if (await file.exists()) {
await file.delete();
}
}
}
void main() {
runAllTests(VmExecutor());
}

35
extras/plugin_example/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Created by https://www.gitignore.io/api/dart,intellij
# Edit at https://www.gitignore.io/?templates=dart,intellij
.vscode
### Dart ###
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
android/
ios/
### Intellij ###
.idea/**/*
# End of https://www.gitignore.io/api/dart,intellij

View File

@ -0,0 +1,41 @@
Playground to test the analyzer plugin for `.moor` files.
Currently, we support
- showing errors in moor files
- outline
- (kinda) folding
## Setup
To use this plugin, you'll need to perform these steps once. It is assumed that you
have already cloned the `moor` repository.
1. Clone https://github.com/simolus3/Dart-Code and checkout the
`use-custom-file-endings` branch.
2. Run `npm install` and `npm run build` to verify that everything is working.
3. Open the forked Dart-Code repo in your regular VS Code installation.
4. Press `F5` to run the plugin in another editor instance. All subsequent
steps need to be completed in that editor.
5. In the settings of that editor, change `dart.additionalAnalyzerFileExtensions`
to include `moor` files:
```json
{
"dart.additionalAnalyzerFileExtensions": ["moor"]
}
```
6. Close that editor.
7. Uncomment the plugin lines in `analysis_options.yaml`
## Running
After you completed the setup, these steps will open an editor instance that runs the plugin.
1. chdir into `moor_generator` and run `lib/plugin.dart`. You can run that file from an IDE if
you need debugging capabilities, but starting it from the command line is fine. Keep that
script running.
2. Re-open the "inner" editor with the custom Dart plugin
2. Open this folder in the editor that runs the custom Dart plugin. Wait ~15s, you should start
to see some log entries in the output of step 1. As soon as they appear, the plugin is ready
to go.
_Note_: `lib/plugin.dart` doesn't support multiple clients. Whenever you close or reload the
editor, that script needs to be restarted as well. That script should also be running before
starting the analysis server.

View File

@ -0,0 +1,5 @@
include: package:pedantic/analysis_options.yaml
#analyzer:
# plugins:
# - moor

View File

@ -0,0 +1 @@
class Test {}

View File

@ -0,0 +1,3 @@
CREATE TABLE playground (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT
)

View File

@ -0,0 +1,14 @@
import 'first.dart';
import 'second.dart';
import 'last.dart';
CREATE TABLE playground (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL
);
unknownColumn: SELECT * FROM playground WHERE bar = 'foo';
syntaxError: SELECT 3 + FROM playground;
lints: INSERT INTO playground DEFAULT VALUES;
variables: SELECT * FROM playground WHERE id BETWEEN :foo AND :bar;
dartTemplate: SELECT * FROM playground WHERE $predicate ORDER BY $ordering;

View File

@ -0,0 +1,13 @@
name: plugin_example
description: Playground to test the analyzer plugin
environment:
sdk: '>=2.4.0 <3.0.0'
dependencies:
moor:
path: ../../moor
dev_dependencies:
pedantic: ^1.7.0
test: ^1.5.0

View File

@ -1,4 +1,101 @@
## unreleased
## 2.0.0
This is the first major update after the initial release and moor and we have a lot to cover:
The `.moor` files can now have their own imports and queries, you can embed Dart in sql queries
using the new templates feature and we have a prototype of a pure-Dart SQL IDE ready.
Finally, we also removed a variety of deprecated features. See the breaking changes
section to learn what components are affected and what alternatives are available.
### New features
#### Updates to the sql parser
`.moor` files were introduced in moor 1.7 as an experimental way to declare tables by using
`CREATE TABLE` statements. In this version, they become stable and support their own import
and query system. This allows you to write queries in their own file:
```sql
CREATE TABLE users (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL
);
findByName: SELECT * FROM users WHERE name LIKE :query;
```
When this file is included from a `@UseMoor` annotation, moor will generate methods to run the
query. Of course, you can also write Dart queries for tables declared in sql:
```dart
Stream<User> loadUserById(int id) {
return (select(users)..where((u) => u.id.equals(2))).watchSingle();
}
```
Moor files can also import other moor files by using an `import 'other.moor';'` statement at the
top. Then, all tables defined in `other.moor` will also be available to the current file.
Moor takes Dart and SQL interop even further with the new "Dart in SQL templates". You can define
a query like this:
```sql
findDynamic: SELECT * FROM users WHERE $condition;
```
And moor will generate a method `findDynamic(Expression<bool, BoolType> condition)` for you. This
allows you to bind the template with a predicate as complex as you'd like. At the moment, Dart
templates are supported for expressions, `OrderBy`, `OrderingTerm` and `Limit`.
`INSERT` statements can now be used as a compiled statement - both in moor files and
in a `@UseMoor` or `@UseDao` annotation. A new builtin linter will even warn you when you forget
to provide a value for a non-nullable column - right at compile time!
And finally, we now generate better query code when queries only return a single column. Instead of
generating a whole new class for that, we simply return the value directly.
#### Experimental ffi support
TODO: Describe ffi port
### Minor changes
- a `Constant<String>` can now be written to SQL, it used to throw before. This is useful
if you need default values for strings columns.
- new `LazyDatabase` when you want to construct a database asynchronously (for instance, if
you first need to find a file before you can open a database).
### Breaking changes
- __THIS LIKELY AFFECTS YOUR APP:__ Removed the `transaction` parameter for callbacks
in transactions and `beforeOpen` callbacks. So, instead of writing
```dart
transaction((t) async {
await t.update(table)...;
});
```
simply write
```dart
transaction(() async {
await update(table)...;
});
```
Similarly, instead of using `onOpen: (db, details) async {...}`, use
`onOpen: (details) async {...}`. You don't have to worry about calling methods on
your database instead of a transaction objects. They will be delegated automatically.
On a similar note, we also removed the `operateOn` parameter from compiled queries.
- Compiled queries that return only a single column (e.g. `SELECT COUNT(*) FROM users`)
will just return their value (in this case, an `int`) now. Moor no longer generates a
new class in that case.
- Removed `MigrationStrategy.onFinished`. Use `beforeOpen` instead.
- Compiled sql queries starting with an underscore will now generate private match queries.
Previously, the query `_allUsers` would generate a `watchAllUsers` method, that has been
adopted to `_watchAllUsers`. The `generate_private_watch_methods` builder option, which
backported this fix to older versions, has thus been removed.
- Removed `InsertStatement.insertOrReplace`. Use `insert(data, orReplace: true)` instead.
## 1.7.2
- Fixed a race condition that caused the database to be opened multiple times on slower devices.
This problem was introduced in `1.7.0` and was causing problems during migrations.
## 1.7.1
- Better documentation on `getSingle` and `watchSingle` for queries.
- Fix `INTEGER NOT NULL PRIMARY KEY` wrongly requiring a value during insert (this never affected
`AUTOINCREMENT` columns, and only affects columns declared in a `.moor` file)
## 1.7.0
- Support custom columns via type converters. See the [docs](https://moor.simonbinder.eu/type_converters)
for details on how to use this feature.
- Transactions now roll back when not completed successfully, they also rethrow the exception

View File

@ -3,4 +3,4 @@ targets:
builders:
moor_generator:
options:
generate_private_watch_methods: true
override_hash_and_equals_in_result_sets: true

View File

@ -59,10 +59,9 @@ class Database extends _$Database {
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (engine, details) async {
beforeOpen: (details) async {
// populate data
await engine
.into(categories)
await into(categories)
.insert(const CategoriesCompanion(description: Value('Sweets')));
},
);

View File

@ -6,7 +6,7 @@ part of 'example.dart';
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class Category extends DataClass implements Insertable<Category> {
final int id;
final String description;
@ -66,7 +66,9 @@ class Category extends DataClass implements Insertable<Category> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is Category && other.id == id && other.description == description);
(other is Category &&
other.id == this.id &&
other.description == this.description);
}
class CategoriesCompanion extends UpdateCompanion<Category> {
@ -76,6 +78,10 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
this.id = const Value.absent(),
this.description = const Value.absent(),
});
CategoriesCompanion.insert({
this.id = const Value.absent(),
this.description = const Value.absent(),
});
CategoriesCompanion copyWith({Value<int> id, Value<String> description}) {
return CategoriesCompanion(
id: id ?? this.id,
@ -94,7 +100,8 @@ class $CategoriesTable extends Categories
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _descriptionMeta =
@ -248,10 +255,10 @@ class Recipe extends DataClass implements Insertable<Recipe> {
bool operator ==(other) =>
identical(this, other) ||
(other is Recipe &&
other.id == id &&
other.title == title &&
other.instructions == instructions &&
other.category == category);
other.id == this.id &&
other.title == this.title &&
other.instructions == this.instructions &&
other.category == this.category);
}
class RecipesCompanion extends UpdateCompanion<Recipe> {
@ -265,6 +272,13 @@ class RecipesCompanion extends UpdateCompanion<Recipe> {
this.instructions = const Value.absent(),
this.category = const Value.absent(),
});
RecipesCompanion.insert({
this.id = const Value.absent(),
@required String title,
@required String instructions,
this.category = const Value.absent(),
}) : title = Value(title),
instructions = Value(instructions);
RecipesCompanion copyWith(
{Value<int> id,
Value<String> title,
@ -288,7 +302,8 @@ class $RecipesTable extends Recipes with TableInfo<$RecipesTable, Recipe> {
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _titleMeta = const VerificationMeta('title');
@ -466,9 +481,9 @@ class Ingredient extends DataClass implements Insertable<Ingredient> {
bool operator ==(other) =>
identical(this, other) ||
(other is Ingredient &&
other.id == id &&
other.name == name &&
other.caloriesPer100g == caloriesPer100g);
other.id == this.id &&
other.name == this.name &&
other.caloriesPer100g == this.caloriesPer100g);
}
class IngredientsCompanion extends UpdateCompanion<Ingredient> {
@ -480,6 +495,12 @@ class IngredientsCompanion extends UpdateCompanion<Ingredient> {
this.name = const Value.absent(),
this.caloriesPer100g = const Value.absent(),
});
IngredientsCompanion.insert({
this.id = const Value.absent(),
@required String name,
@required int caloriesPer100g,
}) : name = Value(name),
caloriesPer100g = Value(caloriesPer100g);
IngredientsCompanion copyWith(
{Value<int> id, Value<String> name, Value<int> caloriesPer100g}) {
return IngredientsCompanion(
@ -500,7 +521,8 @@ class $IngredientsTable extends Ingredients
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _nameMeta = const VerificationMeta('name');
@ -671,9 +693,9 @@ class IngredientInRecipe extends DataClass
bool operator ==(other) =>
identical(this, other) ||
(other is IngredientInRecipe &&
other.recipe == recipe &&
other.ingredient == ingredient &&
other.amountInGrams == amountInGrams);
other.recipe == this.recipe &&
other.ingredient == this.ingredient &&
other.amountInGrams == this.amountInGrams);
}
class IngredientInRecipesCompanion extends UpdateCompanion<IngredientInRecipe> {
@ -685,6 +707,13 @@ class IngredientInRecipesCompanion extends UpdateCompanion<IngredientInRecipe> {
this.ingredient = const Value.absent(),
this.amountInGrams = const Value.absent(),
});
IngredientInRecipesCompanion.insert({
@required int recipe,
@required int ingredient,
@required int amountInGrams,
}) : recipe = Value(recipe),
ingredient = Value(ingredient),
amountInGrams = Value(amountInGrams);
IngredientInRecipesCompanion copyWith(
{Value<int> recipe, Value<int> ingredient, Value<int> amountInGrams}) {
return IngredientInRecipesCompanion(
@ -802,15 +831,6 @@ class $IngredientInRecipesTable extends IngredientInRecipes
}
}
class TotalWeightResult {
final String title;
final int totalWeight;
TotalWeightResult({
this.title,
this.totalWeight,
});
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$CategoriesTable _categories;
@ -829,25 +849,39 @@ abstract class _$Database extends GeneratedDatabase {
);
}
Future<List<TotalWeightResult>> _totalWeight(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
variables: []).then((rows) => rows.map(_rowToTotalWeightResult).toList());
Selectable<TotalWeightResult> _totalWeightQuery() {
return customSelectQuery(
'SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id',
variables: [],
readsFrom: {recipes, ingredientInRecipes}).map(_rowToTotalWeightResult);
}
Future<List<TotalWeightResult>> _totalWeight() {
return _totalWeightQuery().get();
}
Stream<List<TotalWeightResult>> _watchTotalWeight() {
return customSelectStream(
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
variables: [],
readsFrom: {
recipes,
ingredientInRecipes
}).map((rows) => rows.map(_rowToTotalWeightResult).toList());
return _totalWeightQuery().watch();
}
@override
List<TableInfo> get allTables =>
[categories, recipes, ingredients, ingredientInRecipes];
}
class TotalWeightResult {
final String title;
final int totalWeight;
TotalWeightResult({
this.title,
this.totalWeight,
});
@override
int get hashCode => $mrjf($mrjc(title.hashCode, totalWeight.hashCode));
@override
bool operator ==(other) =>
identical(this, other) ||
(other is TotalWeightResult &&
other.title == this.title &&
other.totalWeight == this.totalWeight);
}

View File

@ -28,7 +28,7 @@ class Database extends _$Database {
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async => await m.createAllTables(),
beforeOpen: (engine, details) async {
beforeOpen: (details) async {
if (details.wasCreated) {
// populate default data
await createTodoEntry(

View File

@ -1,81 +0,0 @@
/// A utility library to find an edit script that turns a list into another.
/// This is useful when displaying a updating stream of immutable lists in a
/// list that can be updated.
@Deprecated('Will be removed in moor 2.0')
library diff_util;
import 'package:moor/src/utils/android_diffutils_port.dart' as impl;
class EditAction {
/// The index of the first list on which this action should be applied. If
/// this action [isDelete], that index and the next [amount] indices should be
/// deleted. Otherwise, this index should be moved back by [amount] and
/// entries from the second list (starting at [indexFromOther]) should be
/// inserted into the gap.
final int index;
/// The amount of entries affected by this action
final int amount;
/// If this action [isInsert], this is the first index from the second list
/// from where the items should be taken from.
final int indexFromOther;
/// Whether this action should delete entries from the first list
bool get isDelete => indexFromOther == null;
/// Whether this action should insert entries into the first list
bool get isInsert => indexFromOther != null;
EditAction(this.index, this.amount, this.indexFromOther);
@override
String toString() {
if (isDelete) {
return 'EditAction: Delete $amount entries from the first list, starting '
'at index $index';
} else {
return 'EditAction: Insert $amount entries into the first list, taking '
'them from the second list starting at $indexFromOther. The entries '
'should be written starting at index $index';
}
}
}
/// Finds the shortest edit script that turns list [a] into list [b].
/// The implementation is ported from androids DiffUtil, which in turn
/// implements a variation of Eugene W. Myer's difference algorithm. The
/// algorithm is optimized for space and uses O(n) space to find the minimal
/// number of addition and removal operations between the two lists. It has
/// O(N + D^2) time performance, where D is the minimum amount of edits needed
/// to turn a into b.
List<EditAction> diff<T>(List<T> a, List<T> b,
{bool Function(T a, T b) equals}) {
final defaultEquals = equals ?? (T a, T b) => a == b;
final snakes = impl.calculateDiff(impl.DiffInput<T>(a, b, defaultEquals));
final actions = <EditAction>[];
var posOld = a.length;
var posNew = b.length;
for (var snake in snakes.reversed) {
final snakeSize = snake.size;
final endX = snake.x + snakeSize;
final endY = snake.y + snakeSize;
if (endX < posOld) {
// starting (including) with index endX, delete posOld - endX chars from a
actions.add(EditAction(endX, posOld - endX, null));
}
if (endY < posNew) {
// starting with index endX, insert posNex - endY characters into a. The
// characters should be taken from b, starting (including) at the index
// endY. The char that was at index endX should be pushed back.
actions.add(EditAction(endX, posNew - endY, endY));
}
posOld = snake.x;
posNew = snake.y;
}
return actions;
}

View File

@ -13,6 +13,7 @@ export 'package:moor/src/dsl/database.dart';
export 'package:moor/src/runtime/components/join.dart'
show innerJoin, leftOuterJoin, crossJoin;
export 'package:moor/src/runtime/components/limit.dart';
export 'package:moor/src/runtime/components/order_by.dart';
export 'package:moor/src/runtime/executor/executor.dart';
export 'package:moor/src/types/type_system.dart';
@ -34,3 +35,4 @@ export 'package:moor/src/runtime/migration.dart';
export 'package:moor/src/runtime/exceptions.dart';
export 'package:moor/src/utils/expand_variables.dart';
export 'package:moor/src/utils/hash.dart';
export 'package:moor/src/utils/lazy_database.dart';

View File

@ -28,8 +28,16 @@ class GenerationContext {
final QueryExecutor executor;
final List<dynamic> _boundVariables = [];
/// The values of [introducedVariables] that will be sent to the underlying
/// engine.
List<dynamic> get boundVariables => _boundVariables;
/// All variables ("?" in sql) that were added to this context.
final List<Variable> introducedVariables = [];
int get amountOfVariables => boundVariables.length;
/// The string buffer contains the sql query as it's being constructed.
final StringBuffer buffer = StringBuffer();
@ -49,7 +57,8 @@ class GenerationContext {
/// that the prepared statement can be executed with the variable. The value
/// must be a type that is supported by the sqflite library. A list of
/// supported types can be found [here](https://github.com/tekartik/sqflite#supported-sqlite-types).
void introduceVariable(dynamic value) {
void introduceVariable(Variable v, dynamic value) {
introducedVariables.add(v);
_boundVariables.add(value);
}

View File

@ -44,10 +44,12 @@ class OrderBy extends Component {
OrderBy(this.terms);
@override
void writeInto(GenerationContext context) {
void writeInto(GenerationContext context, {bool writeOrderBy = true}) {
var first = true;
context.buffer.write('ORDER BY ');
if (writeOrderBy) {
context.buffer.write('ORDER BY ');
}
for (var term in terms) {
if (first) {

View File

@ -12,6 +12,9 @@ import 'package:moor/src/runtime/statements/update.dart';
const _zoneRootUserKey = #DatabaseConnectionUser;
typedef _CustomWriter<T> = Future<T> Function(
QueryExecutor e, String sql, List<dynamic> vars);
/// Class that runs queries to a subset of all available queries in a database.
///
/// This comes in handy to structure large amounts of database code better: The
@ -111,6 +114,7 @@ mixin QueryEngine on DatabaseConnectionUser {
/// although it is very likely that the user meant to call it on the
/// [Transaction] t. We can detect this by calling the function passed to
/// `transaction` in a forked [Zone] storing the transaction in
@protected
bool get topLevel => false;
/// We can detect when a user called methods on the wrong [QueryEngine]
@ -169,32 +173,59 @@ mixin QueryEngine on DatabaseConnectionUser {
/// You can use the [updates] parameter so that moor knows which tables are
/// affected by your query. All select streams that depend on a table
/// specified there will then issue another query.
@protected
@visibleForTesting
Future<int> customUpdate(String query,
{List<Variable> variables = const [], Set<TableInfo> updates}) async {
return _customWrite(query, variables, updates, (executor, sql, vars) {
return executor.runUpdate(sql, vars);
});
}
/// Executes a custom insert statement and returns the last inserted rowid.
///
/// You can tell moor which tables your query is going to affect by using the
/// [updates] parameter. Query-streams running on any of these tables will
/// then be re-run.
@protected
@visibleForTesting
Future<int> customInsert(String query,
{List<Variable> variables = const [], Set<TableInfo> updates}) {
return _customWrite(query, variables, updates, (executor, sql, vars) {
return executor.runInsert(sql, vars);
});
}
/// Common logic for [customUpdate] and [customInsert] which takes care of
/// mapping the variables, running the query and optionally informing the
/// stream-queries.
Future<T> _customWrite<T>(String query, List<Variable> variables,
Set<TableInfo> updates, _CustomWriter<T> writer) async {
final engine = _resolvedEngine;
final executor = engine.executor;
final ctx = GenerationContext.fromDb(engine);
final mappedArgs = variables.map((v) => v.mapToSimpleValue(ctx)).toList();
final affectedRows = await executor
.doWhenOpened((_) => executor.runUpdate(query, mappedArgs));
final result =
await executor.doWhenOpened((e) => writer(e, query, mappedArgs));
if (updates != null) {
await engine.streamQueries.handleTableUpdates(updates);
}
return affectedRows;
return result;
}
/// Executes a custom select statement once. To use the variables, mark them
/// with a "?" in your [query]. They will then be changed to the appropriate
/// value.
@protected
@visibleForTesting
@Deprecated('use customSelectQuery(...).get() instead')
Future<List<QueryRow>> customSelect(String query,
{List<Variable> variables = const []}) async {
return CustomSelectStatement(
query, variables, <TableInfo>{}, _resolvedEngine)
.get();
return customSelectQuery(query, variables: variables).get();
}
/// Creates a stream from a custom select statement.To use the variables, mark
@ -202,15 +233,36 @@ mixin QueryEngine on DatabaseConnectionUser {
/// appropriate value. The stream will re-emit items when any table in
/// [readsFrom] changes, so be sure to set it to the set of tables your query
/// reads data from.
@protected
@visibleForTesting
@Deprecated('use customSelectQuery(...).watch() instead')
Stream<List<QueryRow>> customSelectStream(String query,
{List<Variable> variables = const [], Set<TableInfo> readsFrom}) {
final tables = readsFrom ?? <TableInfo>{};
final statement =
CustomSelectStatement(query, variables, tables, _resolvedEngine);
return statement.watch();
return customSelectQuery(query, variables: variables, readsFrom: readsFrom)
.watch();
}
/// Creates a custom select statement from the given sql [query]. To run the
/// query once, use [Selectable.get]. For an auto-updating streams, set the
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
/// know the query will never emit more than one row, you can also use
/// [Selectable.getSingle] and [Selectable.watchSingle] which return the item
/// directly or wrapping it into a list.
///
/// If you use variables in your query (for instance with "?"), they will be
/// bound to the [variables] you specify on this query.
@protected
@visibleForTesting
Selectable<QueryRow> customSelectQuery(String query,
{List<Variable> variables = const [],
Set<TableInfo> readsFrom = const {}}) {
readsFrom ??= {};
return CustomSelectStatement(query, variables, readsFrom, _resolvedEngine);
}
/// Executes the custom sql [statement] on the database.
@protected
@visibleForTesting
Future<void> customStatement(String statement) {
return _resolvedEngine.executor.runCustom(statement);
}
@ -226,10 +278,12 @@ mixin QueryEngine on DatabaseConnectionUser {
/// might be different than that of the "global" database instance.
/// 2. Nested transactions are not supported. Creating another transaction
/// inside a transaction returns the parent transaction.
Future transaction(Future Function(QueryEngine transaction) action) async {
@protected
@visibleForTesting
Future transaction(Future Function() action) async {
final resolved = _resolvedEngine;
if (resolved is Transaction) {
return action(resolved);
return action();
}
final executor = resolved.executor;
@ -240,7 +294,7 @@ mixin QueryEngine on DatabaseConnectionUser {
return _runEngineZoned(transaction, () async {
var success = false;
try {
await action(transaction);
await action();
success = true;
} catch (e) {
await transactionExecutor.rollback();
@ -266,6 +320,22 @@ mixin QueryEngine on DatabaseConnectionUser {
QueryEngine engine, Future<T> Function() calculation) {
return runZoned(calculation, zoneValues: {_zoneRootUserKey: engine});
}
/// Will be used by generated code to resolve inline Dart expressions in sql.
@protected
GenerationContext $write(Component component) {
final context = GenerationContext.fromDb(this);
// we don't want ORDER BY clauses to write the ORDER BY tokens because those
// are already declared in sql
if (component is OrderBy) {
component.writeInto(context, writeOrderBy: false);
} else {
component.writeInto(context);
}
return context;
}
}
/// A base class for all generated databases.
@ -322,13 +392,11 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
Future<void> beforeOpenCallback(
QueryExecutor executor, OpeningDetails details) async {
final migration = _resolvedMigration;
if (migration.onFinished != null) {
await migration.onFinished();
}
if (migration.beforeOpen != null) {
final engine = BeforeOpenEngine(this, executor);
await _runEngineZoned(engine, () {
return migration.beforeOpen(engine, details);
return migration.beforeOpen(details);
});
}
}

View File

@ -220,7 +220,6 @@ class _BeforeOpeningExecutor extends QueryExecutor
/// work to a [DatabaseDelegate].
class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
final DatabaseDelegate delegate;
Completer<bool> _openingCompleter;
@override
bool logStatements;
@ -233,6 +232,8 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
@override
SqlDialect get dialect => delegate.dialect;
final Lock _openingLock = Lock();
DelegatedDatabase(this.delegate,
{this.logStatements, this.isSequential = false}) {
// not using default value because it's commonly set to null
@ -240,29 +241,17 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
}
@override
Future<bool> ensureOpen() async {
// if we're already opening the database or if its already open, return that
// status
if (_openingCompleter != null) {
return _openingCompleter.future;
}
Future<bool> ensureOpen() {
return _openingLock.synchronized(() async {
final alreadyOpen = await delegate.isOpen;
if (alreadyOpen) {
return true;
}
final alreadyOpen = await delegate.isOpen;
if (alreadyOpen) return true;
// ignore: invariant_booleans
if (_openingCompleter != null) {
return _openingCompleter.future;
}
// not already open or opening. Open the database now!
_openingCompleter = Completer();
await delegate.open(databaseInfo);
await _runMigrations();
_openingCompleter.complete(true);
_openingCompleter = null;
return true;
await delegate.open(databaseInfo);
await _runMigrations();
return true;
});
}
Future<void> _runMigrations() async {

View File

@ -1,6 +1,7 @@
export 'bools.dart' show and, or, not;
export 'custom.dart';
export 'datetimes.dart';
export 'expression.dart' show Expression;
export 'in.dart';
export 'null_check.dart';
export 'text.dart' show Collate;

View File

@ -45,12 +45,18 @@ class Variable<T, S extends SqlType<T>> extends Expression<T, S> {
/// database engine. For instance, a [DateTime] will me mapped to its unix
/// timestamp.
dynamic mapToSimpleValue(GenerationContext context) {
return _mapToSimpleValue(context, value);
final type = context.typeSystem.forDartType<T>();
return type.mapToSqlVariable(value);
}
@override
void writeInto(GenerationContext context) {
_writeVariableIntoContext(context, value);
if (value != null) {
context.buffer.write('?');
context.introduceVariable(this, mapToSimpleValue(context));
} else {
context.buffer.write('NULL');
}
}
}
@ -67,27 +73,7 @@ class Constant<T, S extends SqlType<T>> extends Expression<T, S> {
@override
void writeInto(GenerationContext context) {
// Instead of writing string literals (which we don't support because of
// possible sql injections), just write the variable.
if (value is String) {
_writeVariableIntoContext(context, value);
} else {
final type = context.typeSystem.forDartType<T>();
context.buffer.write(type.mapToSqlConstant(value));
}
final type = context.typeSystem.forDartType<T>();
context.buffer.write(type.mapToSqlConstant(value));
}
}
void _writeVariableIntoContext<T>(GenerationContext context, T value) {
if (value != null) {
context.buffer.write('?');
context.introduceVariable(_mapToSimpleValue<T>(context, value));
} else {
context.buffer.write('NULL');
}
}
dynamic _mapToSimpleValue<T>(GenerationContext context, T value) {
final type = context.typeSystem.forDartType<T>();
return type.mapToSqlVariable(value);
}

View File

@ -16,8 +16,7 @@ typedef Future<void> OnMigrationFinished();
/// Signature of a function that's called before a database is marked opened by
/// moor, but after migrations took place. This is a suitable callback to to
/// populate initial data or issue `PRAGMA` statements that you want to use.
typedef OnBeforeOpen = Future<void> Function(
QueryEngine db, OpeningDetails details);
typedef OnBeforeOpen = Future<void> Function(OpeningDetails details);
Future<void> _defaultOnCreate(Migrator m) => m.createAllTables();
Future<void> _defaultOnUpdate(Migrator m, int from, int to) async =>
@ -33,14 +32,6 @@ class MigrationStrategy {
/// happened at a lower [GeneratedDatabase.schemaVersion].
final OnUpgrade onUpgrade;
/// Executes after the database is ready and all migrations ran, but before
/// any other queries will be executed, making this method suitable to
/// populate data.
@Deprecated(
'This callback is broken and only exists for backwards compatibility. '
'Use beforeOpen instead')
final OnMigrationFinished onFinished;
/// Executes after the database is ready to be used (ie. it has been opened
/// and all migrations ran), but before any other queries will be sent. This
/// makes it a suitable place to populate data after the database has been
@ -51,8 +42,6 @@ class MigrationStrategy {
this.onCreate = _defaultOnCreate,
this.onUpgrade = _defaultOnUpdate,
this.beforeOpen,
@Deprecated('This callback is broken. Use beforeOpen instead')
this.onFinished,
});
}

View File

@ -4,8 +4,6 @@ import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'update.dart';
class InsertStatement<D extends DataClass> {
@protected
final QueryEngine database;
@ -72,19 +70,6 @@ class InsertStatement<D extends DataClass> {
database.markTablesUpdated({table});
}
/// Updates the row with the same primary key in the database or creates one
/// if it doesn't exist.
///
/// Behaves similar to [UpdateStatement.replace], meaning that all fields from
/// [entity] will be written to override rows with the same primary key, which
/// includes setting columns with null values back to null.
///
/// However, if no such row exists, a new row will be written instead.
@Deprecated('Use insert with orReplace: true instead')
Future<void> insertOrReplace(Insertable<D> entity) async {
return await insert(entity, orReplace: true);
}
GenerationContext _createContext(Insertable<D> entry, bool replace) {
final map = table.entityToSql(entry.createCompanion(true))
..removeWhere((_, value) => value == null);

View File

@ -79,8 +79,21 @@ abstract class Selectable<T> {
/// result too many values, this method will throw. If no row is returned,
/// `null` will be returned instead.
///
/// {@template moor_single_query_expl}
/// Be aware that this operation won't put a limit clause on this statement,
/// if that's needed you would have to do that yourself.
/// if that's needed you would have to do use [SimpleSelectStatement.limit]:
/// ```dart
/// Future<TodoEntry> loadMostImportant() {
/// return (select(todos)
/// ..orderBy([(t) => OrderingTerm(expression: t.priority, mode: OrderingMode.desc)])
/// ..limit(1)
/// ).getSingle();
/// }
/// ```
/// You should only use this method if you know the query won't have more than
/// one row, for instance because you used `limit(1)` or you know the `where`
/// clause will only allow one row.
/// {@endtemplate}
Future<T> getSingle() async {
final list = await get();
final iterator = list.iterator;
@ -100,10 +113,40 @@ abstract class Selectable<T> {
/// However, it is assumed that the query will only emit one result, so
/// instead of returning a [Stream<List<T>>], this returns a [Stream<T>]. If
/// the query emits more than one row at some point, an error will be emitted
/// to the stream instead.
/// to the stream instead. If the query emits zero rows at some point, `null`
/// will be added to the stream instead.
///
/// {@macro moor_single_query_expl}
Stream<T> watchSingle() {
return watch().transform(singleElements());
}
/// Maps this selectable by using [mapper].
///
/// Each entry emitted by this [Selectable] will be transformed by the
/// [mapper] and then emitted to the selectable returned.
Selectable<N> map<N>(N Function(T) mapper) {
return _MappedSelectable<T, N>(this, mapper);
}
}
class _MappedSelectable<S, T> extends Selectable<T> {
final Selectable<S> _source;
final T Function(S) _mapper;
_MappedSelectable(this._source, this._mapper);
@override
Future<List<T>> get() {
return _source.get().then(_mapResults);
}
@override
Stream<List<T>> watch() {
return _source.watch().map(_mapResults);
}
List<T> _mapResults(List<S> results) => results.map(_mapper).toList();
}
mixin SingleTableQueryMixin<T extends Table, D extends DataClass>

View File

@ -174,6 +174,17 @@ class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
class GeneratedIntColumn extends GeneratedColumn<int, IntType>
with ComparableExpr
implements IntColumn {
/// Whether this column was declared to be a primary key via a column
/// constraint. The only way to do this in Dart is with
/// [IntColumnBuilder.autoIncrement]. In `.moor` files, declaring a column
/// to be `INTEGER NOT NULL PRIMARY KEY` will set this flag but not
/// [hasAutoIncrement]. If either field is enabled, this column will be an
/// alias for the rowid.
final bool declaredAsPrimaryKey;
/// Whether this column was declared to be an `AUTOINCREMENT` column, either
/// with [IntColumnBuilder.autoIncrement] or with an `AUTOINCREMENT` clause
/// in a `.moor` file.
final bool hasAutoIncrement;
@override
@ -183,6 +194,7 @@ class GeneratedIntColumn extends GeneratedColumn<int, IntType>
String name,
String tableName,
bool nullable, {
this.declaredAsPrimaryKey = false,
this.hasAutoIncrement = false,
String $customConstraints,
Expression<int, IntType> defaultValue,
@ -190,18 +202,18 @@ class GeneratedIntColumn extends GeneratedColumn<int, IntType>
$customConstraints: $customConstraints, defaultValue: defaultValue);
@override
void writeColumnDefinition(GenerationContext into) {
// todo make this work with custom constraints, default values, etc.
void writeCustomConstraints(StringBuffer into) {
if (hasAutoIncrement) {
into.buffer.write('${$name} $typeName PRIMARY KEY AUTOINCREMENT');
} else {
super.writeColumnDefinition(into);
into.write(' PRIMARY KEY AUTOINCREMENT');
} else if (declaredAsPrimaryKey) {
into.write(' PRIMARY KEY');
}
}
@override
bool get isRequired {
return !hasAutoIncrement && super.isRequired;
final aliasForRowId = declaredAsPrimaryKey || hasAutoIncrement;
return !aliasForRowId && super.isRequired;
}
}

View File

@ -11,14 +11,13 @@ mixin TableInfo<TableDsl extends Table, D extends DataClass> on Table {
/// The primary key of this table. Can be null or empty if no custom primary
/// key has been specified.
///
/// Additional to the [Table.primaryKey] columns declared by an user, this
/// also contains auto-increment integers, which are primary key by default.
Set<GeneratedColumn> get $primaryKey => null;
// The "primaryKey" is what users define on their table classes, the
// "$primaryKey" is what moor generates in the implementation table info
// classes. Having two of them is pretty pointless, we're going to remove
// the "$primaryKey$ getter in Moor 2.0. Until then, let's make sure they're
// consistent for classes from CREATE TABLE statements, where the info class
// and the table class is the same thing but primaryKey isn't overriden.
// ensure the primaryKey getter is consistent with $primarKey, which can
// contain additional columns.
@override
Set<Column> get primaryKey => $primaryKey;

View File

@ -56,9 +56,13 @@ class StringType extends SqlType<String> {
@override
String mapToSqlConstant(String content) {
// TODO: implement mapToSqlConstant, we would probably have to take care
// of sql injection vulnerabilities here
throw UnimplementedError("Strings can't be mapped to sql literals yet");
// From the sqlite docs: (https://www.sqlite.org/lang_expr.html)
// A string constant is formed by enclosing the string in single quotes (').
// A single quote within the string can be encoded by putting two single
// quotes in a row - as in Pascal. C-style escapes using the backslash
// character are not supported because they are not standard SQL.
final escapedChars = content.replaceAll('\'', '\'\'');
return "'$escapedChars'";
}
@override

View File

@ -1,249 +0,0 @@
// ignore_for_file: cascade_invocations
/*
This implementation is copied from the DiffUtil class of the android support
library, available at https://chromium.googlesource.com/android_tools/+/refs/heads/master/sdk/sources/android-25/android/support/v7/util/DiffUtil.java
It has the following license:
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class Snake {
int x;
int y;
int size;
bool removal;
bool reverse;
}
class Range {
int oldListStart, oldListEnd;
int newListStart, newListEnd;
Range.nullFields();
Range(this.oldListStart, this.oldListEnd, this.newListStart, this.newListEnd);
}
class DiffInput<T> {
final List<T> from;
final List<T> to;
final bool Function(T a, T b) equals;
DiffInput(this.from, this.to, this.equals);
bool areItemsTheSame(int fromPos, int toPos) {
return equals(from[fromPos], to[toPos]);
}
}
@Deprecated('Will be removed in moor 2.0')
List<Snake> calculateDiff(DiffInput input) {
final oldSize = input.from.length;
final newSize = input.to.length;
final snakes = <Snake>[];
final stack = <Range>[];
stack.add(Range(0, oldSize, 0, newSize));
final max = oldSize + newSize + (oldSize - newSize).abs();
final forward = List<int>(max * 2);
final backward = List<int>(max * 2);
final rangePool = <Range>[];
while (stack.isNotEmpty) {
final range = stack.removeLast();
final snake = _diffPartial(input, range.oldListStart, range.oldListEnd,
range.newListStart, range.newListEnd, forward, backward, max);
if (snake != null) {
if (snake.size > 0) {
snakes.add(snake);
}
// offset the snake to convert its coordinates from the Range's are to
// global
snake.x += range.oldListStart;
snake.y += range.newListStart;
// add new ranges for left and right
final left =
rangePool.isEmpty ? Range.nullFields() : rangePool.removeLast();
left.oldListStart = range.oldListStart;
left.newListStart = range.newListStart;
if (snake.reverse) {
left.oldListEnd = snake.x;
left.newListEnd = snake.y;
} else {
if (snake.removal) {
left.oldListEnd = snake.x - 1;
left.newListEnd = snake.y;
} else {
left.oldListEnd = snake.x;
left.newListEnd = snake.y - 1;
}
}
stack.add(left);
final right = range;
if (snake.reverse) {
if (snake.removal) {
right.oldListStart = snake.x + snake.size + 1;
right.newListStart = snake.y + snake.size;
} else {
right.oldListStart = snake.x + snake.size;
right.newListStart = snake.y + snake.size + 1;
}
} else {
right.oldListStart = snake.x + snake.size;
right.newListStart = snake.y + snake.size;
}
stack.add(right);
} else {
rangePool.add(range);
}
}
snakes.sort((a, b) {
final cmpX = a.x - b.x;
return cmpX == 0 ? a.y - b.y : cmpX;
});
// add root snake
final first = snakes.isEmpty ? null : snakes.first;
if (first == null || first.x != 0 || first.y != 0) {
snakes.insert(
0,
Snake()
..x = 0
..y = 0
..removal = false
..size = 0
..reverse = false);
}
return snakes;
}
Snake _diffPartial(DiffInput input, int startOld, int endOld, int startNew,
int endNew, List<int> forward, List<int> backward, int kOffset) {
final oldSize = endOld - startOld;
final newSize = endNew - startNew;
if (endOld - startOld < 1 || endNew - startNew < 1) return null;
final delta = oldSize - newSize;
final dLimit = (oldSize + newSize + 1) ~/ 2;
forward.fillRange(kOffset - dLimit - 1, kOffset + dLimit + 1, 0);
backward.fillRange(
kOffset - dLimit - 1 + delta, kOffset + dLimit + 1 + delta, oldSize);
final checkInFwd = delta.isOdd;
for (var d = 0; d <= dLimit; d++) {
for (var k = -d; k <= d; k += 2) {
// find forward path
// we can reach k from k - 1 or k + 1. Check which one is further in the
// graph.
int x;
bool removal;
if (k == -d ||
k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1]) {
x = forward[kOffset + k + 1];
removal = false;
} else {
x = forward[kOffset + k - 1] + 1;
removal = true;
}
// set y based on x
var y = x - k;
// move diagonal as long as items match
while (x < oldSize &&
y < newSize &&
input.areItemsTheSame(startOld + x, startNew + y)) {
x++;
y++;
}
forward[kOffset + k] = x;
if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
if (forward[kOffset + k] >= backward[kOffset + k]) {
final outSnake = Snake()..x = backward[kOffset + k];
outSnake
..y = outSnake.x - k
..size = forward[kOffset + k] - backward[kOffset + k]
..removal = removal
..reverse = false;
return outSnake;
}
}
}
for (var k = -d; k <= d; k += 2) {
// find reverse path at k + delta, in reverse
final backwardK = k + delta;
int x;
bool removal;
if (backwardK == d + delta ||
backwardK != -d + delta &&
backward[kOffset + backwardK - 1] <
backward[kOffset + backwardK + 1]) {
x = backward[kOffset + backwardK - 1];
removal = false;
} else {
x = backward[kOffset + backwardK + 1] - 1;
removal = true;
}
// set y based on x
var y = x - backwardK;
// move diagonal as long as items match
while (x > 0 &&
y > 0 &&
input.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
x--;
y--;
}
backward[kOffset + backwardK] = x;
if (!checkInFwd && k + delta >= -d && k + delta <= d) {
if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
final outSnake = Snake()..x = backward[kOffset + backwardK];
outSnake
..y = outSnake.x - backwardK
..size =
forward[kOffset + backwardK] - backward[kOffset + backwardK]
..removal = removal
..reverse = true;
return outSnake;
}
}
}
}
throw StateError("Unexpected case: Please make sure the lists don't change "
'during a diff');
}

View File

@ -0,0 +1,74 @@
import 'dart:async';
import 'package:moor/backends.dart';
import 'package:moor/moor.dart';
/// Signature of a function that opens a database connection when instructed to.
typedef DatabaseOpener = FutureOr<QueryExecutor> Function();
/// A special database executor that delegates work to another [QueryExecutor].
/// The other executor is lazily opened by a [DatabaseOpener].
class LazyDatabase extends QueryExecutor {
QueryExecutor _delegate;
Completer<void> _openDelegate;
/// The function that will open the database when this [LazyDatabase] gets
/// opened for the first time.
final DatabaseOpener opener;
LazyDatabase(this.opener);
@override
set databaseInfo(GeneratedDatabase db) {
super.databaseInfo = db;
_delegate?.databaseInfo = db;
}
Future<void> _awaitOpened() {
if (_delegate != null) {
return Future.value();
} else if (_openDelegate != null) {
return _openDelegate.future;
} else {
_openDelegate = Completer();
Future.value(opener()).then((database) {
_delegate = database;
_delegate.databaseInfo = databaseInfo;
_openDelegate.complete();
});
return _openDelegate.future;
}
}
@override
TransactionExecutor beginTransaction() => _delegate.beginTransaction();
@override
Future<bool> ensureOpen() {
return _awaitOpened().then((_) => _delegate.ensureOpen());
}
@override
Future<void> runBatched(List<BatchedStatement> statements) =>
_delegate.runBatched(statements);
@override
Future<void> runCustom(String statement, [List args]) =>
_delegate.runCustom(statement, args);
@override
Future<int> runDelete(String statement, List args) =>
_delegate.runDelete(statement, args);
@override
Future<int> runInsert(String statement, List args) =>
_delegate.runInsert(statement, args);
@override
Future<List<Map<String, dynamic>>> runSelect(String statement, List args) =>
_delegate.runSelect(statement, args);
@override
Future<int> runUpdate(String statement, List args) =>
_delegate.runUpdate(statement, args);
}

View File

@ -1,6 +1,6 @@
name: moor
description: Moor is a safe and reactive persistence library for Dart applications
version: 1.6.0
version: 1.7.2
repository: https://github.com/simolus3/moor
homepage: https://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues
@ -9,16 +9,16 @@ authors:
maintainer: Simon Binder (@simolus3)
environment:
sdk: '>=2.2.2 <3.0.0'
sdk: '>=2.3.0 <3.0.0'
dependencies:
meta: '>= 1.0.0 <2.0.0'
collection: '>= 1.0.0 <2.0.0'
meta: ^1.0.0
collection: ^1.0.0
synchronized: ^2.1.0
pedantic: any
pedantic: ^1.0.0
dev_dependencies:
moor_generator: ^1.6.0
moor_generator: ^1.7.0
build_runner: '>=1.3.0 <2.0.0'
build_test: ^0.10.8
test: ^1.6.4

View File

@ -0,0 +1,38 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:test_api/test_api.dart';
import 'package:moor/moor.dart';
import '../data/tables/todos.dart';
void main() {
test('int column writes AUTOINCREMENT constraint', () {
final column = GeneratedIntColumn(
'foo',
'tbl',
false,
declaredAsPrimaryKey: true,
hasAutoIncrement: true,
);
final context = GenerationContext.fromDb(TodoDb(null));
column.writeColumnDefinition(context);
expect(
context.sql, equals('foo INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT'));
});
test('int column writes PRIMARY KEY constraint', () {
final column = GeneratedIntColumn(
'foo',
'tbl',
false,
declaredAsPrimaryKey: true,
hasAutoIncrement: false,
);
final context = GenerationContext.fromDb(TodoDb(null));
column.writeColumnDefinition(context);
expect(context.sql, equals('foo INTEGER NOT NULL PRIMARY KEY'));
});
}

View File

@ -1,3 +1,4 @@
import 'package:moor/moor.dart';
import 'package:test_api/test_api.dart';
import 'data/tables/todos.dart';
@ -6,10 +7,12 @@ import 'data/utils/mocks.dart';
void main() {
TodoDb db;
MockExecutor executor;
MockStreamQueries streamQueries;
setUp(() {
executor = MockExecutor();
db = TodoDb(executor);
streamQueries = MockStreamQueries();
db = TodoDb(executor)..streamQueries = streamQueries;
});
group('compiled custom queries', () {
@ -25,4 +28,23 @@ void main() {
);
});
});
test('custom update informs stream queries', () async {
await db.customUpdate('UPDATE tbl SET a = ?',
variables: [Variable.withString('hi')], updates: {db.users});
verify(executor.runUpdate('UPDATE tbl SET a = ?', ['hi']));
verify(streamQueries.handleTableUpdates({db.users}));
});
test('custom insert', () async {
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(32));
final id =
await db.customInsert('fake insert', variables: [Variable.withInt(3)]);
expect(id, 32);
// shouldn't call stream queries - we didn't set the updates parameter
verifyNever(streamQueries.handleTableUpdates(any));
});
}

View File

@ -2,7 +2,10 @@ import 'package:moor/moor.dart';
part 'custom_tables.g.dart';
@UseMoor(include: {'tables.moor'})
@UseMoor(
include: {'tables.moor'},
queries: {'writeConfig': 'REPLACE INTO config VALUES (:key, :value)'},
)
class CustomTablesDb extends _$CustomTablesDb {
CustomTablesDb(QueryExecutor e) : super(e);

View File

@ -6,7 +6,7 @@ part of 'custom_tables.dart';
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class NoId extends DataClass implements Insertable<NoId> {
final Uint8List payload;
NoId({@required this.payload});
@ -55,7 +55,8 @@ class NoId extends DataClass implements Insertable<NoId> {
int get hashCode => $mrjf(payload.hashCode);
@override
bool operator ==(other) =>
identical(this, other) || (other is NoId && other.payload == payload);
identical(this, other) ||
(other is NoId && other.payload == this.payload);
}
class NoIdsCompanion extends UpdateCompanion<NoId> {
@ -63,6 +64,9 @@ class NoIdsCompanion extends UpdateCompanion<NoId> {
const NoIdsCompanion({
this.payload = const Value.absent(),
});
NoIdsCompanion.insert({
@required Uint8List payload,
}) : payload = Value(payload);
NoIdsCompanion copyWith({Value<Uint8List> payload}) {
return NoIdsCompanion(
payload: payload ?? this.payload,
@ -187,7 +191,7 @@ class WithDefault extends DataClass implements Insertable<WithDefault> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is WithDefault && other.a == a && other.b == b);
(other is WithDefault && other.a == this.a && other.b == this.b);
}
class WithDefaultsCompanion extends UpdateCompanion<WithDefault> {
@ -197,6 +201,10 @@ class WithDefaultsCompanion extends UpdateCompanion<WithDefault> {
this.a = const Value.absent(),
this.b = const Value.absent(),
});
WithDefaultsCompanion.insert({
this.a = const Value.absent(),
this.b = const Value.absent(),
});
WithDefaultsCompanion copyWith({Value<String> a, Value<int> b}) {
return WithDefaultsCompanion(
a: a ?? this.a,
@ -347,7 +355,10 @@ class WithConstraint extends DataClass implements Insertable<WithConstraint> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is WithConstraint && other.a == a && other.b == b && other.c == c);
(other is WithConstraint &&
other.a == this.a &&
other.b == this.b &&
other.c == this.c);
}
class WithConstraintsCompanion extends UpdateCompanion<WithConstraint> {
@ -359,6 +370,11 @@ class WithConstraintsCompanion extends UpdateCompanion<WithConstraint> {
this.b = const Value.absent(),
this.c = const Value.absent(),
});
WithConstraintsCompanion.insert({
this.a = const Value.absent(),
@required int b,
this.c = const Value.absent(),
}) : b = Value(b);
WithConstraintsCompanion copyWith(
{Value<String> a, Value<int> b, Value<double> c}) {
return WithConstraintsCompanion(
@ -462,24 +478,24 @@ class WithConstraints extends Table
final bool dontWriteConstraints = true;
}
class ConfigData extends DataClass implements Insertable<ConfigData> {
class Config extends DataClass implements Insertable<Config> {
final String configKey;
final String configValue;
ConfigData({@required this.configKey, this.configValue});
factory ConfigData.fromData(Map<String, dynamic> data, GeneratedDatabase db,
Config({@required this.configKey, this.configValue});
factory Config.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
final effectivePrefix = prefix ?? '';
final stringType = db.typeSystem.forDartType<String>();
return ConfigData(
return Config(
configKey: stringType
.mapFromDatabaseResponse(data['${effectivePrefix}config_key']),
configValue: stringType
.mapFromDatabaseResponse(data['${effectivePrefix}config_value']),
);
}
factory ConfigData.fromJson(Map<String, dynamic> json,
factory Config.fromJson(Map<String, dynamic> json,
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
return ConfigData(
return Config(
configKey: serializer.fromJson<String>(json['configKey']),
configValue: serializer.fromJson<String>(json['configValue']),
);
@ -494,7 +510,7 @@ class ConfigData extends DataClass implements Insertable<ConfigData> {
}
@override
T createCompanion<T extends UpdateCompanion<ConfigData>>(bool nullToAbsent) {
T createCompanion<T extends UpdateCompanion<Config>>(bool nullToAbsent) {
return ConfigCompanion(
configKey: configKey == null && nullToAbsent
? const Value.absent()
@ -505,13 +521,13 @@ class ConfigData extends DataClass implements Insertable<ConfigData> {
) as T;
}
ConfigData copyWith({String configKey, String configValue}) => ConfigData(
Config copyWith({String configKey, String configValue}) => Config(
configKey: configKey ?? this.configKey,
configValue: configValue ?? this.configValue,
);
@override
String toString() {
return (StringBuffer('ConfigData(')
return (StringBuffer('Config(')
..write('configKey: $configKey, ')
..write('configValue: $configValue')
..write(')'))
@ -523,18 +539,22 @@ class ConfigData extends DataClass implements Insertable<ConfigData> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is ConfigData &&
other.configKey == configKey &&
other.configValue == configValue);
(other is Config &&
other.configKey == this.configKey &&
other.configValue == this.configValue);
}
class ConfigCompanion extends UpdateCompanion<ConfigData> {
class ConfigCompanion extends UpdateCompanion<Config> {
final Value<String> configKey;
final Value<String> configValue;
const ConfigCompanion({
this.configKey = const Value.absent(),
this.configValue = const Value.absent(),
});
ConfigCompanion.insert({
@required String configKey,
this.configValue = const Value.absent(),
}) : configKey = Value(configKey);
ConfigCompanion copyWith(
{Value<String> configKey, Value<String> configValue}) {
return ConfigCompanion(
@ -544,10 +564,10 @@ class ConfigCompanion extends UpdateCompanion<ConfigData> {
}
}
class Config extends Table with TableInfo<Config, ConfigData> {
class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
final GeneratedDatabase _db;
final String _alias;
Config(this._db, [this._alias]);
ConfigTable(this._db, [this._alias]);
final VerificationMeta _configKeyMeta = const VerificationMeta('configKey');
GeneratedTextColumn _configKey;
GeneratedTextColumn get configKey => _configKey ??= _constructConfigKey();
@ -569,7 +589,7 @@ class Config extends Table with TableInfo<Config, ConfigData> {
@override
List<GeneratedColumn> get $columns => [configKey, configValue];
@override
Config get asDslTable => this;
ConfigTable get asDslTable => this;
@override
String get $tableName => _alias ?? 'config';
@override
@ -596,9 +616,9 @@ class Config extends Table with TableInfo<Config, ConfigData> {
@override
Set<GeneratedColumn> get $primaryKey => {configKey};
@override
ConfigData map(Map<String, dynamic> data, {String tablePrefix}) {
Config map(Map<String, dynamic> data, {String tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return ConfigData.fromData(data, _db, prefix: effectivePrefix);
return Config.fromData(data, _db, prefix: effectivePrefix);
}
@override
@ -614,8 +634,168 @@ class Config extends Table with TableInfo<Config, ConfigData> {
}
@override
Config createAlias(String alias) {
return Config(_db, alias);
ConfigTable createAlias(String alias) {
return ConfigTable(_db, alias);
}
@override
final bool dontWriteConstraints = true;
}
class MytableData extends DataClass implements Insertable<MytableData> {
final int someid;
final String sometext;
MytableData({@required this.someid, this.sometext});
factory MytableData.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
return MytableData(
someid: intType.mapFromDatabaseResponse(data['${effectivePrefix}someid']),
sometext: stringType
.mapFromDatabaseResponse(data['${effectivePrefix}sometext']),
);
}
factory MytableData.fromJson(Map<String, dynamic> json,
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
return MytableData(
someid: serializer.fromJson<int>(json['someid']),
sometext: serializer.fromJson<String>(json['sometext']),
);
}
@override
Map<String, dynamic> toJson(
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
return {
'someid': serializer.toJson<int>(someid),
'sometext': serializer.toJson<String>(sometext),
};
}
@override
T createCompanion<T extends UpdateCompanion<MytableData>>(bool nullToAbsent) {
return MytableCompanion(
someid:
someid == null && nullToAbsent ? const Value.absent() : Value(someid),
sometext: sometext == null && nullToAbsent
? const Value.absent()
: Value(sometext),
) as T;
}
MytableData copyWith({int someid, String sometext}) => MytableData(
someid: someid ?? this.someid,
sometext: sometext ?? this.sometext,
);
@override
String toString() {
return (StringBuffer('MytableData(')
..write('someid: $someid, ')
..write('sometext: $sometext')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(someid.hashCode, sometext.hashCode));
@override
bool operator ==(other) =>
identical(this, other) ||
(other is MytableData &&
other.someid == this.someid &&
other.sometext == this.sometext);
}
class MytableCompanion extends UpdateCompanion<MytableData> {
final Value<int> someid;
final Value<String> sometext;
const MytableCompanion({
this.someid = const Value.absent(),
this.sometext = const Value.absent(),
});
MytableCompanion.insert({
this.someid = const Value.absent(),
this.sometext = const Value.absent(),
});
MytableCompanion copyWith({Value<int> someid, Value<String> sometext}) {
return MytableCompanion(
someid: someid ?? this.someid,
sometext: sometext ?? this.sometext,
);
}
}
class Mytable extends Table with TableInfo<Mytable, MytableData> {
final GeneratedDatabase _db;
final String _alias;
Mytable(this._db, [this._alias]);
final VerificationMeta _someidMeta = const VerificationMeta('someid');
GeneratedIntColumn _someid;
GeneratedIntColumn get someid => _someid ??= _constructSomeid();
GeneratedIntColumn _constructSomeid() {
return GeneratedIntColumn('someid', $tableName, false,
declaredAsPrimaryKey: true, $customConstraints: 'NOT NULL PRIMARY KEY');
}
final VerificationMeta _sometextMeta = const VerificationMeta('sometext');
GeneratedTextColumn _sometext;
GeneratedTextColumn get sometext => _sometext ??= _constructSometext();
GeneratedTextColumn _constructSometext() {
return GeneratedTextColumn('sometext', $tableName, true,
$customConstraints: '');
}
@override
List<GeneratedColumn> get $columns => [someid, sometext];
@override
Mytable get asDslTable => this;
@override
String get $tableName => _alias ?? 'mytable';
@override
final String actualTableName = 'mytable';
@override
VerificationContext validateIntegrity(MytableCompanion d,
{bool isInserting = false}) {
final context = VerificationContext();
if (d.someid.present) {
context.handle(
_someidMeta, someid.isAcceptableValue(d.someid.value, _someidMeta));
} else if (someid.isRequired && isInserting) {
context.missing(_someidMeta);
}
if (d.sometext.present) {
context.handle(_sometextMeta,
sometext.isAcceptableValue(d.sometext.value, _sometextMeta));
} else if (sometext.isRequired && isInserting) {
context.missing(_sometextMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {someid};
@override
MytableData map(Map<String, dynamic> data, {String tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return MytableData.fromData(data, _db, prefix: effectivePrefix);
}
@override
Map<String, Variable> entityToSql(MytableCompanion d) {
final map = <String, Variable>{};
if (d.someid.present) {
map['someid'] = Variable<int, IntType>(d.someid.value);
}
if (d.sometext.present) {
map['sometext'] = Variable<String, StringType>(d.sometext.value);
}
return map;
}
@override
Mytable createAlias(String alias) {
return Mytable(_db, alias);
}
@override
@ -632,9 +812,57 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
WithConstraints _withConstraints;
WithConstraints get withConstraints =>
_withConstraints ??= WithConstraints(this);
Config _config;
Config get config => _config ??= Config(this);
ConfigTable _config;
ConfigTable get config => _config ??= ConfigTable(this);
Mytable _mytable;
Mytable get mytable => _mytable ??= Mytable(this);
Config _rowToConfig(QueryRow row) {
return Config(
configKey: row.readString('config_key'),
configValue: row.readString('config_value'),
);
}
Selectable<Config> readConfig(String var1) {
return customSelectQuery('SELECT * FROM config WHERE config_key = ?',
variables: [Variable.withString(var1)],
readsFrom: {config}).map(_rowToConfig);
}
Selectable<Config> readMultiple(List<String> var1, OrderBy clause) {
var $arrayStartIndex = 1;
final expandedvar1 = $expandVar($arrayStartIndex, var1.length);
$arrayStartIndex += var1.length;
final generatedclause = $write(clause);
$arrayStartIndex += generatedclause.amountOfVariables;
return customSelectQuery(
'SELECT * FROM config WHERE config_key IN ($expandedvar1) ORDER BY ${generatedclause.sql}',
variables: [
for (var $ in var1) Variable.withString($),
...generatedclause.introducedVariables
],
readsFrom: {
config
}).map(_rowToConfig);
}
Selectable<Config> readDynamic(Expression<bool, BoolType> predicate) {
final generatedpredicate = $write(predicate);
return customSelectQuery(
'SELECT * FROM config WHERE ${generatedpredicate.sql}',
variables: [...generatedpredicate.introducedVariables],
readsFrom: {config}).map(_rowToConfig);
}
Future<int> writeConfig(String key, String value) {
return customInsert(
'REPLACE INTO config VALUES (:key, :value)',
variables: [Variable.withString(key), Variable.withString(value)],
updates: {config},
);
}
@override
List<TableInfo> get allTables =>
[noIds, withDefaults, withConstraints, config];
[noIds, withDefaults, withConstraints, config, mytable];
}

View File

@ -5,7 +5,7 @@ CREATE TABLE no_ids (
CREATE TABLE with_defaults (
a TEXT DEFAULT 'something',
b INT UNIQUE
)
);
CREATE TABLE with_constraints (
a TEXT,
@ -13,9 +13,18 @@ CREATE TABLE with_constraints (
c FLOAT(10, 2),
FOREIGN KEY (a, b) REFERENCES with_defaults (a, b)
)
);
create table config (
config_key TEXT not null primary key,
config_value TEXT
) AS "Config";
CREATE TABLE mytable (
someid INTEGER NOT NULL PRIMARY KEY,
sometext TEXT
);
readConfig: SELECT * FROM config WHERE config_key = ?;
readMultiple: SELECT * FROM config WHERE config_key IN ? ORDER BY $clause;
readDynamic: SELECT * FROM config WHERE $predicate;

View File

@ -6,7 +6,7 @@ part of 'todos.dart';
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class TodoEntry extends DataClass implements Insertable<TodoEntry> {
final int id;
final String title;
@ -113,11 +113,11 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
bool operator ==(other) =>
identical(this, other) ||
(other is TodoEntry &&
other.id == id &&
other.title == title &&
other.content == content &&
other.targetDate == targetDate &&
other.category == category);
other.id == this.id &&
other.title == this.title &&
other.content == this.content &&
other.targetDate == this.targetDate &&
other.category == this.category);
}
class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
@ -133,6 +133,13 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
this.targetDate = const Value.absent(),
this.category = const Value.absent(),
});
TodosTableCompanion.insert({
this.id = const Value.absent(),
this.title = const Value.absent(),
@required String content,
this.targetDate = const Value.absent(),
this.category = const Value.absent(),
}) : content = Value(content);
TodosTableCompanion copyWith(
{Value<int> id,
Value<String> title,
@ -159,7 +166,8 @@ class $TodosTableTable extends TodosTable
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _titleMeta = const VerificationMeta('title');
@ -347,7 +355,9 @@ class Category extends DataClass implements Insertable<Category> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is Category && other.id == id && other.description == description);
(other is Category &&
other.id == this.id &&
other.description == this.description);
}
class CategoriesCompanion extends UpdateCompanion<Category> {
@ -357,6 +367,10 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
this.id = const Value.absent(),
this.description = const Value.absent(),
});
CategoriesCompanion.insert({
this.id = const Value.absent(),
@required String description,
}) : description = Value(description);
CategoriesCompanion copyWith({Value<int> id, Value<String> description}) {
return CategoriesCompanion(
id: id ?? this.id,
@ -375,7 +389,8 @@ class $CategoriesTable extends Categories
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _descriptionMeta =
@ -547,11 +562,11 @@ class User extends DataClass implements Insertable<User> {
bool operator ==(other) =>
identical(this, other) ||
(other is User &&
other.id == id &&
other.name == name &&
other.isAwesome == isAwesome &&
other.profilePicture == profilePicture &&
other.creationTime == creationTime);
other.id == this.id &&
other.name == this.name &&
other.isAwesome == this.isAwesome &&
other.profilePicture == this.profilePicture &&
other.creationTime == this.creationTime);
}
class UsersCompanion extends UpdateCompanion<User> {
@ -567,6 +582,14 @@ class UsersCompanion extends UpdateCompanion<User> {
this.profilePicture = const Value.absent(),
this.creationTime = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
@required String name,
this.isAwesome = const Value.absent(),
@required Uint8List profilePicture,
this.creationTime = const Value.absent(),
}) : name = Value(name),
profilePicture = Value(profilePicture);
UsersCompanion copyWith(
{Value<int> id,
Value<String> name,
@ -592,7 +615,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _nameMeta = const VerificationMeta('name');
@ -779,7 +803,9 @@ class SharedTodo extends DataClass implements Insertable<SharedTodo> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is SharedTodo && other.todo == todo && other.user == user);
(other is SharedTodo &&
other.todo == this.todo &&
other.user == this.user);
}
class SharedTodosCompanion extends UpdateCompanion<SharedTodo> {
@ -789,6 +815,11 @@ class SharedTodosCompanion extends UpdateCompanion<SharedTodo> {
this.todo = const Value.absent(),
this.user = const Value.absent(),
});
SharedTodosCompanion.insert({
@required int todo,
@required int user,
}) : todo = Value(todo),
user = Value(user);
SharedTodosCompanion copyWith({Value<int> todo, Value<int> user}) {
return SharedTodosCompanion(
todo: todo ?? this.todo,
@ -961,9 +992,9 @@ class TableWithoutPKData extends DataClass
bool operator ==(other) =>
identical(this, other) ||
(other is TableWithoutPKData &&
other.notReallyAnId == notReallyAnId &&
other.someFloat == someFloat &&
other.custom == custom);
other.notReallyAnId == this.notReallyAnId &&
other.someFloat == this.someFloat &&
other.custom == this.custom);
}
class TableWithoutPKCompanion extends UpdateCompanion<TableWithoutPKData> {
@ -975,6 +1006,13 @@ class TableWithoutPKCompanion extends UpdateCompanion<TableWithoutPKData> {
this.someFloat = const Value.absent(),
this.custom = const Value.absent(),
});
TableWithoutPKCompanion.insert({
@required int notReallyAnId,
@required double someFloat,
@required MyCustomObject custom,
}) : notReallyAnId = Value(notReallyAnId),
someFloat = Value(someFloat),
custom = Value(custom);
TableWithoutPKCompanion copyWith(
{Value<int> notReallyAnId,
Value<double> someFloat,
@ -1149,7 +1187,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
@override
bool operator ==(other) =>
identical(this, other) ||
(other is PureDefault && other.id == id && other.txt == txt);
(other is PureDefault && other.id == this.id && other.txt == this.txt);
}
class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
@ -1159,6 +1197,10 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
this.id = const Value.absent(),
this.txt = const Value.absent(),
});
PureDefaultsCompanion.insert({
this.id = const Value.absent(),
this.txt = const Value.absent(),
});
PureDefaultsCompanion copyWith({Value<int> id, Value<String> txt}) {
return PureDefaultsCompanion(
id: id ?? this.id,
@ -1177,7 +1219,8 @@ class $PureDefaultsTable extends PureDefaults
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _txtMeta = const VerificationMeta('txt');
@ -1243,32 +1286,6 @@ class $PureDefaultsTable extends PureDefaults
}
}
class AllTodosWithCategoryResult {
final int id;
final String title;
final String content;
final DateTime targetDate;
final int category;
final int catId;
final String catDesc;
AllTodosWithCategoryResult({
this.id,
this.title,
this.content,
this.targetDate,
this.category,
this.catId,
this.catDesc,
});
}
class FindCustomResult {
final MyCustomObject custom;
FindCustomResult({
this.custom,
});
}
abstract class _$TodoDb extends GeneratedDatabase {
_$TodoDb(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$TodosTableTable _todosTable;
@ -1299,33 +1316,28 @@ abstract class _$TodoDb extends GeneratedDatabase {
);
}
Future<List<AllTodosWithCategoryResult>> allTodosWithCategory(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT t.*, c.id as catId, c."desc" as catDesc FROM todos t INNER JOIN categories c ON c.id = t.category',
variables: []).then((rows) => rows.map(_rowToAllTodosWithCategoryResult).toList());
}
Stream<List<AllTodosWithCategoryResult>> watchAllTodosWithCategory() {
return customSelectStream(
Selectable<AllTodosWithCategoryResult> allTodosWithCategoryQuery() {
return customSelectQuery(
'SELECT t.*, c.id as catId, c."desc" as catDesc FROM todos t INNER JOIN categories c ON c.id = t.category',
variables: [],
readsFrom: {
categories,
todosTable
}).map((rows) => rows.map(_rowToAllTodosWithCategoryResult).toList());
}).map(_rowToAllTodosWithCategoryResult);
}
Future<int> deleteTodoById(
int var1,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customUpdate(
Future<List<AllTodosWithCategoryResult>> allTodosWithCategory() {
return allTodosWithCategoryQuery().get();
}
Stream<List<AllTodosWithCategoryResult>> watchAllTodosWithCategory() {
return allTodosWithCategoryQuery().watch();
}
Future<int> deleteTodoById(int var1) {
return customUpdate(
'DELETE FROM todos WHERE id = ?',
variables: [
Variable.withInt(var1),
],
variables: [Variable.withInt(var1)],
updates: {todosTable},
);
}
@ -1340,84 +1352,61 @@ abstract class _$TodoDb extends GeneratedDatabase {
);
}
Future<List<TodoEntry>> withIn(
String var1,
String var2,
List<int> var3,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
var $highestIndex = 3;
final expandedvar3 = $expandVar($highestIndex, var3.length);
$highestIndex += var3.length;
return (operateOn ?? this).customSelect(
Selectable<TodoEntry> withInQuery(String var1, String var2, List<int> var3) {
var $arrayStartIndex = 3;
final expandedvar3 = $expandVar($arrayStartIndex, var3.length);
$arrayStartIndex += var3.length;
return customSelectQuery(
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
variables: [
Variable.withString(var1),
Variable.withString(var2),
for (var $ in var3) Variable.withInt($),
]).then((rows) => rows.map(_rowToTodoEntry).toList());
for (var $ in var3) Variable.withInt($)
],
readsFrom: {
todosTable
}).map(_rowToTodoEntry);
}
Future<List<TodoEntry>> withIn(String var1, String var2, List<int> var3) {
return withInQuery(var1, var2, var3).get();
}
Stream<List<TodoEntry>> watchWithIn(
String var1, String var2, List<int> var3) {
var $highestIndex = 3;
final expandedvar3 = $expandVar($highestIndex, var3.length);
$highestIndex += var3.length;
return customSelectStream(
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
variables: [
Variable.withString(var1),
Variable.withString(var2),
for (var $ in var3) Variable.withInt($),
],
readsFrom: {
todosTable
}).map((rows) => rows.map(_rowToTodoEntry).toList());
return withInQuery(var1, var2, var3).watch();
}
Future<List<TodoEntry>> search(
int id,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
Selectable<TodoEntry> searchQuery(int id) {
return customSelectQuery(
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
variables: [
Variable.withInt(id),
]).then((rows) => rows.map(_rowToTodoEntry).toList());
variables: [Variable.withInt(id)],
readsFrom: {todosTable}).map(_rowToTodoEntry);
}
Future<List<TodoEntry>> search(int id) {
return searchQuery(id).get();
}
Stream<List<TodoEntry>> watchSearch(int id) {
return customSelectStream(
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
variables: [
Variable.withInt(id),
],
readsFrom: {
todosTable
}).map((rows) => rows.map(_rowToTodoEntry).toList());
return searchQuery(id).watch();
}
FindCustomResult _rowToFindCustomResult(QueryRow row) {
return FindCustomResult(
custom:
$TableWithoutPKTable.$converter0.mapToDart(row.readString('custom')),
);
}
Future<List<FindCustomResult>> findCustom(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
variables: []).then((rows) => rows.map(_rowToFindCustomResult).toList());
}
Stream<List<FindCustomResult>> watchFindCustom() {
return customSelectStream(
Selectable<MyCustomObject> findCustomQuery() {
return customSelectQuery(
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
variables: [],
readsFrom: {tableWithoutPK})
.map((rows) => rows.map(_rowToFindCustomResult).toList());
.map((QueryRow row) => $TableWithoutPKTable.$converter0
.mapToDart(row.readString('custom')));
}
Future<List<MyCustomObject>> findCustom() {
return findCustomQuery().get();
}
Stream<List<MyCustomObject>> watchFindCustom() {
return findCustomQuery().watch();
}
@override
@ -1431,6 +1420,47 @@ abstract class _$TodoDb extends GeneratedDatabase {
];
}
class AllTodosWithCategoryResult {
final int id;
final String title;
final String content;
final DateTime targetDate;
final int category;
final int catId;
final String catDesc;
AllTodosWithCategoryResult({
this.id,
this.title,
this.content,
this.targetDate,
this.category,
this.catId,
this.catDesc,
});
@override
int get hashCode => $mrjf($mrjc(
id.hashCode,
$mrjc(
title.hashCode,
$mrjc(
content.hashCode,
$mrjc(
targetDate.hashCode,
$mrjc(category.hashCode,
$mrjc(catId.hashCode, catDesc.hashCode)))))));
@override
bool operator ==(other) =>
identical(this, other) ||
(other is AllTodosWithCategoryResult &&
other.id == this.id &&
other.title == this.title &&
other.content == this.content &&
other.targetDate == this.targetDate &&
other.category == this.category &&
other.catId == this.catId &&
other.catDesc == this.catDesc);
}
// **************************************************************************
// DaoGenerator
// **************************************************************************
@ -1449,27 +1479,18 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
);
}
Future<List<TodoEntry>> todosForUser(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
Selectable<TodoEntry> todosForUserQuery(int user) {
return customSelectQuery(
'SELECT t.* FROM todos t INNER JOIN shared_todos st ON st.todo = t.id INNER JOIN users u ON u.id = st.user WHERE u.id = :user',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToTodoEntry).toList());
variables: [Variable.withInt(user)],
readsFrom: {todosTable, sharedTodos, users}).map(_rowToTodoEntry);
}
Future<List<TodoEntry>> todosForUser(int user) {
return todosForUserQuery(user).get();
}
Stream<List<TodoEntry>> watchTodosForUser(int user) {
return customSelectStream(
'SELECT t.* FROM todos t INNER JOIN shared_todos st ON st.todo = t.id INNER JOIN users u ON u.id = st.user WHERE u.id = :user',
variables: [
Variable.withInt(user),
],
readsFrom: {
todosTable,
sharedTodos,
users
}).map((rows) => rows.map(_rowToTodoEntry).toList());
return todosForUserQuery(user).watch();
}
}

View File

@ -10,19 +10,45 @@ typedef Future<T> _EnsureOpenAction<T>(QueryExecutor e);
class MockExecutor extends Mock implements QueryExecutor {
final MockTransactionExecutor transactions = MockTransactionExecutor();
var _opened = false;
MockExecutor() {
when(runSelect(any, any)).thenAnswer((_) => Future.value([]));
when(runUpdate(any, any)).thenAnswer((_) => Future.value(0));
when(runDelete(any, any)).thenAnswer((_) => Future.value(0));
when(runInsert(any, any)).thenAnswer((_) => Future.value(0));
when(runSelect(any, any)).thenAnswer((_) {
assert(_opened);
return Future.value([]);
});
when(runUpdate(any, any)).thenAnswer((_) {
assert(_opened);
return Future.value(0);
});
when(runDelete(any, any)).thenAnswer((_) {
assert(_opened);
return Future.value(0);
});
when(runInsert(any, any)).thenAnswer((_) {
assert(_opened);
return Future.value(0);
});
when(beginTransaction()).thenAnswer((_) {
assert(_opened);
return transactions;
});
when(ensureOpen()).thenAnswer((i) {
_opened = true;
return Future.value(true);
});
when(doWhenOpened(any)).thenAnswer((i) {
_opened = true;
final action = i.positionalArguments.single as _EnsureOpenAction;
return action(this);
});
when(beginTransaction()).thenAnswer((_) => transactions);
when(close()).thenAnswer((_) async {
_opened = false;
});
}
}

View File

@ -15,9 +15,11 @@ class _FakeDb extends GeneratedDatabase {
onUpgrade: (m, from, to) async {
await m.issueCustomQuery('updated from $from to $to');
},
beforeOpen: (db, details) async {
await db.customSelect(
'opened: ${details.versionBefore} to ${details.versionNow}');
beforeOpen: (details) async {
// this fake select query is verified via mocks
await customSelectQuery(
'opened: ${details.versionBefore} to ${details.versionNow}')
.get();
},
);
}

View File

@ -1,28 +0,0 @@
import 'package:test_api/test_api.dart';
import 'package:moor/diff_util.dart';
List<T> applyEditScript<T>(List<T> a, List<T> b, List<EditAction> actions) {
final copy = List.of(a);
for (var action in actions) {
if (action.isDelete) {
final deleteStartIndex = action.index;
copy.removeRange(deleteStartIndex, deleteStartIndex + action.amount);
} else if (action.isInsert) {
final toAdd = b.getRange(
action.indexFromOther, action.indexFromOther + action.amount);
copy.insertAll(action.index, toAdd);
}
}
return copy;
}
void main() {
final a = ['a', 'b', 'c', 'a', 'b', 'b', 'a'];
final b = ['c', 'b', 'a', 'b', 'a', 'c'];
test('diff matcher should produce a correct edit script', () {
expect(applyEditScript(a, b, diff(a, b)), b);
});
}

View File

@ -0,0 +1,30 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:test_api/test_api.dart';
import '../data/tables/todos.dart';
void main() {
group('string literals', () {
test('can be written as constants', () {
testStringMapping('hello world', "'hello world'");
});
test('supports escaping snigle quotes', () {
testStringMapping('what\'s that?', "'what\'\'s that?'");
});
test('other chars are not escaped', () {
testStringMapping('\\\$"', "'\\\$\"'");
});
});
}
void testStringMapping(String dart, String expectedLiteral) {
final ctx = GenerationContext.fromDb(TodoDb(null));
final constant = Constant(dart);
constant.writeInto(ctx);
expect(ctx.sql, expectedLiteral);
}

View File

@ -19,6 +19,10 @@ const _createConfig = 'CREATE TABLE IF NOT EXISTS config ('
'config_key VARCHAR not null primary key, '
'config_value VARCHAR);';
const _createMyTable = 'CREATE TABLE IF NOT EXISTS mytable ('
'someid INTEGER NOT NULL PRIMARY KEY, '
'sometext VARCHAR);';
void main() {
// see ../data/tables/tables.moor
test('creates tables as specified in .moor files', () async {
@ -31,6 +35,7 @@ void main() {
verify(mockQueryExecutor.call(_createWithDefaults, []));
verify(mockQueryExecutor.call(_createWithConstraints, []));
verify(mockQueryExecutor.call(_createConfig, []));
verify(mockQueryExecutor.call(_createMyTable, []));
});
test('infers primary keys correctly', () async {
@ -40,4 +45,42 @@ void main() {
expect(db.withDefaults.primaryKey, isEmpty);
expect(db.config.primaryKey, [db.config.configKey]);
});
test('supports absent values for primary key integers', () async {
// regression test for #112: https://github.com/simolus3/moor/issues/112
final mock = MockExecutor();
final db = CustomTablesDb(mock);
await db.into(db.mytable).insert(const MytableCompanion());
verify(mock.runInsert('INSERT INTO mytable DEFAULT VALUES', []));
});
test('runs queries with arrays and Dart templates', () async {
final mock = MockExecutor();
final db = CustomTablesDb(mock);
await db.readMultiple(['a', 'b'],
OrderBy([OrderingTerm(expression: db.config.configKey)])).get();
verify(mock.runSelect(
'SELECT * FROM config WHERE config_key IN (?1, ?2) ORDER BY config_key ASC',
['a', 'b'],
));
});
test('runs query with variables from template', () async {
final mock = MockExecutor();
final db = CustomTablesDb(mock);
final mockResponse = {'config_key': 'key', 'config_value': 'value'};
when(mock.runSelect(any, any))
.thenAnswer((_) => Future.value([mockResponse]));
final parsed =
await db.readDynamic(db.config.configKey.equals('key')).getSingle();
verify(
mock.runSelect('SELECT * FROM config WHERE config_key = ?', ['key']));
expect(parsed, Config(configKey: 'key', configValue: 'value'));
});
}

View File

@ -22,19 +22,21 @@ void main() {
// should create todos, categories, users and shared_todos table
verify(mockQueryExecutor.call(
'CREATE TABLE IF NOT EXISTS todos '
'(id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR NULL, '
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title VARCHAR NULL, '
'content VARCHAR NOT NULL, target_date INTEGER NULL, '
'category INTEGER NULL);',
[]));
verify(mockQueryExecutor.call(
'CREATE TABLE IF NOT EXISTS categories '
'(id INTEGER PRIMARY KEY AUTOINCREMENT, `desc` VARCHAR NOT NULL UNIQUE);',
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
'`desc` VARCHAR NOT NULL UNIQUE);',
[]));
verify(mockQueryExecutor.call(
'CREATE TABLE IF NOT EXISTS users '
'(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, '
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
'name VARCHAR NOT NULL, '
'is_awesome BOOLEAN NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1)), '
'profile_picture BLOB NOT NULL, '
'creation_time INTEGER NOT NULL '
@ -66,7 +68,8 @@ void main() {
verify(mockQueryExecutor.call(
'CREATE TABLE IF NOT EXISTS users '
'(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, '
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
'name VARCHAR NOT NULL, '
'is_awesome BOOLEAN NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1)), '
'profile_picture BLOB NOT NULL, '
'creation_time INTEGER NOT NULL '

View File

@ -33,16 +33,16 @@ void main() {
test("transactions don't allow creating streams", () {
expect(() async {
await db.transaction((t) async {
t.select(db.users).watch();
await db.transaction(() async {
db.select(db.users).watch();
});
}, throwsStateError);
});
test('nested transactions use the outer transaction', () async {
await db.transaction((t) async {
await t.transaction((t2) async {
expect(t2, equals(t));
await db.transaction(() async {
await db.transaction(() async {
// todo how can we test that these are really equal?
});
// the outer callback has not completed yet, so shouldn't send
@ -55,7 +55,7 @@ void main() {
test('code in callback uses transaction', () async {
// notice how we call .select on the database, but it should be called on
// transaction executor.
await db.transaction((_) async {
await db.transaction(() async {
await db.select(db.users).get();
});
@ -65,7 +65,7 @@ void main() {
test('transactions rollback after errors', () async {
final exception = Exception('oh no');
final future = db.transaction((_) async {
final future = db.transaction(() async {
throw exception;
});
@ -79,8 +79,8 @@ void main() {
when(executor.transactions.runUpdate(any, any))
.thenAnswer((_) => Future.value(2));
await db.transaction((t) async {
await t
await db.transaction(() async {
await db
.update(db.users)
.write(const UsersCompanion(name: Value('Updated name')));
@ -95,7 +95,7 @@ void main() {
});
test('the database is opened before starting a transaction', () async {
await db.transaction((t) async {
await db.transaction(() async {
verify(executor.doWhenOpened(any));
});
});

View File

@ -0,0 +1,59 @@
import 'package:moor/moor.dart';
import 'package:pedantic/pedantic.dart';
import 'package:test_api/test_api.dart';
import '../data/tables/todos.dart';
import '../data/utils/mocks.dart';
void main() {
test('lazy database delegates work', () async {
final inner = MockExecutor();
final lazy = LazyDatabase(() => inner);
await lazy.ensureOpen();
clearInteractions(inner);
lazy.beginTransaction();
await lazy.runBatched(null);
await lazy.runCustom('custom_stmt');
await lazy.runDelete('delete_stmt', [1]);
await lazy.runInsert('insert_stmt', [2]);
await lazy.runSelect('select_stmt', [3]);
await lazy.runUpdate('update_stmt', [4]);
verifyInOrder([
inner.runBatched(null),
inner.runCustom('custom_stmt'),
inner.runDelete('delete_stmt', [1]),
inner.runInsert('insert_stmt', [2]),
inner.runSelect('select_stmt', [3]),
inner.runUpdate('update_stmt', [4]),
]);
});
test('database is only opened once', () async {
final inner = MockExecutor();
var openCount = 0;
final lazy = LazyDatabase(() {
openCount++;
return inner;
});
for (var i = 0; i < 10; i++) {
unawaited(lazy.ensureOpen());
}
await pumpEventQueue();
expect(openCount, 1);
});
test('sets generated database property', () async {
final inner = MockExecutor();
final db = TodoDb(LazyDatabase(() => inner));
// run a statement to make sure the database has been opened
await db.customSelectQuery('custom_select').get();
verify(inner.databaseInfo = db);
});
}

View File

@ -0,0 +1,36 @@
import 'dart:convert';
import 'dart:isolate';
//import 'package:moor_generator/plugin.dart';
import 'package:web_socket_channel/io.dart';
const useProxyPlugin = true;
void main(List<String> args, SendPort sendPort) {
PluginProxy(sendPort).start();
// start(args, sendPort);
}
class PluginProxy {
final SendPort sendToAnalysisServer;
ReceivePort _receive;
IOWebSocketChannel _channel;
PluginProxy(this.sendToAnalysisServer);
void start() async {
_channel = IOWebSocketChannel.connect('ws://localhost:9999');
_receive = ReceivePort();
sendToAnalysisServer.send(_receive.sendPort);
_receive.listen((data) {
// the server will send messages as maps, convert to json
_channel.sink.add(json.encode(data));
});
_channel.stream.listen((data) {
sendToAnalysisServer.send(json.decode(data as String));
});
}
}

View File

@ -0,0 +1,16 @@
name: analyzer_load_moor_plugin
version: 1.0.0
description: This pubspec is a part of moor and determines the version of the moor analyzer to load
dependencies:
# moor_generator:
web_socket_channel: ^1.0.15
# To work on this plugin, you need to add the absolute paths here. Relative paths aren't supported yet
# https://github.com/dart-lang/sdk/issues/35281
#dependency_overrides:
# moor_generator:
# path: /home/simon/IdeaProjects/moor/moor_generator
# sqlparser:
# path: /home/simon/IdeaProjects/moor/sqlparser

12
moor_ffi/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/
pubspec.lock
# todo determine whether metadata should be added to gitignore (the file says it shouldn't, but does this break)?
.metadata

3
moor_ffi/CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

24
moor_ffi/LICENSE Normal file
View File

@ -0,0 +1,24 @@
MIT License
Copyright (c) 2019 Simon Binder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This project also bundles sqlite, which is in the Public Domain.
See https://www.sqlite.org/copyright.html

104
moor_ffi/README.md Normal file
View File

@ -0,0 +1,104 @@
# moor_ffi
Experimental bindings to sqlite by using `dart:ffi`. This library contains utils to make
integration with [moor](https://pub.dev/packages/moor) easier, but it can also be used
as a standalone package.
## Warnings
At the moment, `dart:ffi` is in preview and there will be breaking changes that this
library has to adapt to. This library has been tested on Dart `2.5.0`.
If you're using a development Dart version (this includes Flutter channels that are not
`stable`), this library might not work.
If you just want to use moor, using the [moor_flutter](https://pub.dev/packages/moor_flutter)
package is the better option at the moment.
## Supported platforms
You can make this library work on any platform that let's you obtain a `DynamicLibrary`
from which moor_ffi loads the functions (see below).
Out of the box, this libraries supports all platforms where `sqlite3` is installed:
- iOS: Yes
- macOS: Yes
- Linux: Available on most distros
- Windows: When the user has installed sqlite (they probably have)
- Android: Yes when used with Flutter
This library works with and without Flutter.
If you're using Flutter, this library will bundle `sqlite3` in your Android app. This
requires the Android NDK to be installed (You can get the NDK in the [SDK Manager](https://developer.android.com/studio/intro/update.html#sdk-manager)
of Android Studio). Note that the first `flutter run` is going to take a very long time as
we need to compile sqlite.
### On other platforms
Using this library on platforms that are not supported out of the box is fairly
straightforward. For instance, if you release your own `sqlite3.so` with your application,
you could use
```dart
import 'dart:ffi';
import 'dart:io';
import 'package:moor_ffi/database.dart';
import 'package:moor_ffi/open_helper.dart';
void main() {
open.overrideFor(OperatingSystem.linux, _openOnLinux);
final db = Database.memory();
db.close();
}
DynamicLibrary _openOnLinux() {
final script = File(Platform.script.toFilePath());
final libraryNextToScript = File('${script.path}/sqlite3.so');
return DynamicLibrary.open(libraryNextToScript.path);
}
```
Just be sure to first override the behavior and then opening the database. Further,
if you want to use the isolate api, you can only use a static method or top-level
function to open the library.
### Supported datatypes
This library supports `null`, `int`, other `num`s (converted to double),
`String` and `Uint8List` to bind args. Returned columns from select statements
will have the same types.
## Using without moor
```dart
import 'package:moor_ffi/database.dart';
void main() {
final database = Database.memory();
// run some database operations. See the example for details
database.close();
}
```
You can also use an asynchronous API on a background isolate by using `IsolateDb.openFile`
or `IsolateDb.openMemory`, respectively. be aware that the asynchronous API is much slower,
but it moves work out of the UI isolate.
Be sure to __always__ call `Database.close` to avoid memory leaks!
## Migrating from moor_flutter
__Note__: For production apps, please use `moor_flutter` until this package
reaches a stable version.
Add both `moor` and `moor_ffi` to your pubspec, the `moor_flutter` dependency can be dropped.
```yaml
dependencies:
moor: ^1.7.0
moor_ffi: ^0.0.1
dev_dependencies:
moor_generator: ^1.7.0
```
In the file where you created a `FlutterQueryExecutor`, replace the `moor_flutter` import
with both `package:moor/moor.dart` and `package:moor_ffi/moor_ffi.dart`.
In all other project files that use moor apis (e.g. a `Value` class for companions), just import `package:moor/moor.dart`.
Finally, replace usages of `FlutterQueryExecutor` with `VmDatabase`.
Note that, at the moment, there is no counterpart for `FlutterQueryExecutor.inDatabasePath` and that the async API using
a background isolate is not available yet. Both shortcomings with be fixed by the upcoming moor 2.0 release.

12
moor_ffi/android/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild/
cpp/sqlite*
sqlite_*.zip

View File

@ -0,0 +1,66 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
plugins {
id "de.undercouch.download" version "4.0.0"
}
group 'eu.simonbinder.moor_ffi'
version '1.0'
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path "cpp/CMakeLists.txt"
}
}
lintOptions {
disable 'InvalidPackage'
}
}
task downloadSqlite(type: Download) {
src 'https://sqlite.org/2019/sqlite-amalgamation-3290000.zip'
dest 'sqlite_3290000.zip'
overwrite false
}
task extractSqlite(dependsOn: downloadSqlite, type: Copy) {
from zipTree(downloadSqlite.dest).matching {
include '*/sqlite3.c'
eachFile { it.setPath(it.getName()) } // Don't use top-level folder in zip
}
into 'cpp'
}
preBuild.dependsOn extractSqlite

View File

@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.4.1)
project(sqlite3)
add_library(sqlite3 SHARED sqlite3.c)

View File

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx1536M

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip

View File

@ -0,0 +1 @@
rootProject.name = 'moor_ffi'

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.simonbinder.moor_ffi"
android:versionCode="1"
android:versionName="1.0" >
</manifest>

View File

@ -0,0 +1,27 @@
import 'package:moor_ffi/database.dart';
const _createTable = r'''
CREATE TABLE frameworks (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL
);
''';
void main() {
final db = Database.memory();
db.execute(_createTable);
final insertStmt = db.prepare('INSERT INTO frameworks(name) VALUES (?)');
insertStmt.execute(['Flutter']);
insertStmt.execute(['AngularDart']);
insertStmt.close();
final selectStmt = db.prepare('SELECT * FROM frameworks ORDER BY name');
final result = selectStmt.select();
for (var row in result) {
print('${row['id']}: ${row['name']}');
}
selectStmt.close();
db.close();
}

View File

@ -0,0 +1,28 @@
import 'package:moor_ffi/database.dart';
const _createTable = r'''
CREATE TABLE frameworks (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL
);
''';
void main() async {
final db = await IsolateDb.openMemory();
await db.execute(_createTable);
final insertStmt =
await db.prepare('INSERT INTO frameworks(name) VALUES (?)');
await insertStmt.execute(['Flutter']);
await insertStmt.execute(['AngularDart']);
await insertStmt.close();
final selectStmt = await db.prepare('SELECT * FROM frameworks ORDER BY name');
final result = await selectStmt.select();
for (var row in result) {
print('${row['id']}: ${row['name']}');
}
await selectStmt.close();
await db.close();
}

37
moor_ffi/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/flutter_export_environment.sh

View File

View File

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface MoorFfiPlugin : NSObject<FlutterPlugin>
@end

View File

@ -0,0 +1,8 @@
#import "MoorFfiPlugin.h"
#import <moor_ffi/moor_ffi-Swift.h>
@implementation MoorFfiPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftMoorFfiPlugin registerWithRegistrar:registrar];
}
@end

View File

@ -0,0 +1,14 @@
import Flutter
import UIKit
public class SwiftMoorFfiPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "moor_ffi", binaryMessenger: registrar.messenger())
let instance = SwiftMoorFfiPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
result("iOS " + UIDevice.current.systemVersion)
}
}

View File

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'moor_ffi'
s.version = '0.0.1'
s.summary = 'A new flutter plugin project.'
s.description = <<-DESC
A new flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
#s.source_files = 'Classes/**/*'
#s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
# when we run on Dart 2.6, we should use this library and also use DynamicLibrary.executable()
# s.dependency 'sqlite3'
s.ios.deployment_target = '8.0'
end

View File

@ -0,0 +1,11 @@
/// Exports the low-level [Database] and [IsolateDb] classes to run operations
/// on a sqflite database.
library database;
import 'package:moor_ffi/src/bindings/types.dart';
import 'src/impl/isolate/isolate_db.dart';
export 'src/api/database.dart';
export 'src/api/result.dart';
export 'src/impl/database.dart' show SqliteException, Database;
export 'src/impl/isolate/isolate_db.dart';

View File

@ -0,0 +1,7 @@
import 'dart:io';
import 'package:moor/backends.dart';
import 'package:moor/moor.dart';
import 'package:moor_ffi/database.dart';
part 'src/vm_database.dart';

View File

@ -0,0 +1,7 @@
/// Utils to open a [DynamicLibrary] on platforms that aren't supported by
/// `moor_ffi` by default.
library open_helper;
import 'dart:ffi';
export 'src/load_library.dart';

View File

@ -0,0 +1,44 @@
import 'dart:async';
import 'package:moor_ffi/database.dart';
/// A opened sqlite database.
abstract class BaseDatabase {
/// Closes this database connection and releases the resources it uses. If
/// an error occurs while closing the database, an exception will be thrown.
/// The allocated memory will be freed either way.
FutureOr<void> close();
/// Executes the [sql] statement and ignores the result. Will throw if an
/// error occurs while executing.
FutureOr<void> execute(String sql);
/// Prepares the [sql] statement.
FutureOr<BasePreparedStatement> prepare(String sql);
/// Get the application defined version of this database.
FutureOr<int> userVersion();
/// Update the application defined version of this database.
FutureOr<void> setUserVersion(int version);
/// Returns the amount of rows affected by the last INSERT, UPDATE or DELETE
/// statement.
FutureOr<int> getUpdatedRows();
/// Returns the row-id of the last inserted row.
FutureOr<int> getLastInsertId();
}
/// A prepared statement that can be executed multiple times.
abstract class BasePreparedStatement {
/// Executes this prepared statement as a select statement. The returned rows
/// will be returned.
FutureOr<Result> select([List<dynamic> args]);
/// Executes this prepared statement.
FutureOr<void> execute([List<dynamic> params]);
/// Closes this prepared statement and releases its resources.
FutureOr<void> close();
}

View File

@ -0,0 +1,64 @@
import 'dart:collection';
import 'package:collection/collection.dart';
/// Stores the result of a select statement.
class Result extends Iterable<Row> {
final List<String> columnNames;
// a result set can have multiple columns with the same name, but that's rare
// and users usually use a name as index. So we cache that for O(1) lookups
Map<String, int> _calculatedIndexes;
final List<List<dynamic>> rows;
Result(this.columnNames, this.rows) {
_calculatedIndexes = {
for (var column in columnNames) column: columnNames.lastIndexOf(column),
};
}
@override
Iterator<Row> get iterator => _ResultIterator(this);
}
/// Stores a single row in the result of a select statement.
class Row extends MapMixin<String, dynamic>
with UnmodifiableMapMixin<String, dynamic> {
final Result _result;
final int _rowIndex;
Row._(this._result, this._rowIndex);
/// Returns the value stored in the [i]-th column in this row (zero-indexed).
dynamic columnAt(int i) {
return _result.rows[_rowIndex][i];
}
@override
operator [](Object key) {
if (key is! String) return null;
final index = _result._calculatedIndexes[key];
if (index == null) return null;
return columnAt(index);
}
@override
Iterable<String> get keys => _result.columnNames;
}
class _ResultIterator extends Iterator<Row> {
final Result result;
int index = -1;
_ResultIterator(this.result);
@override
Row get current => Row._(result, index);
@override
bool moveNext() {
index++;
return index < result.rows.length;
}
}

View File

@ -0,0 +1,197 @@
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ffi';
import 'package:moor_ffi/open_helper.dart';
import '../ffi/blob.dart';
import 'signatures.dart';
import 'types.dart';
// ignore_for_file: comment_references, non_constant_identifier_names
class _SQLiteBindings {
DynamicLibrary sqlite;
int Function(Pointer<CBlob> filename, Pointer<Pointer<Database>> databaseOut,
int flags, Pointer<CBlob> vfs) sqlite3_open_v2;
int Function(Pointer<Database> database) sqlite3_close_v2;
void Function(Pointer<Void> ptr) sqlite3_free;
int Function(
Pointer<Database> database,
Pointer<CBlob> query,
int nbytes,
Pointer<Pointer<Statement>> statementOut,
Pointer<Pointer<CBlob>> tail) sqlite3_prepare_v2;
int Function(
Pointer<Database> database,
Pointer<CBlob> query,
Pointer<Void> callback,
Pointer<Void> cbFirstArg,
Pointer<Pointer<CBlob>> errorMsgOut,
) sqlite3_exec;
int Function(Pointer<Statement> statement) sqlite3_step;
int Function(Pointer<Statement> statement) sqlite3_reset;
int Function(Pointer<Statement> statement) sqlite3_finalize;
int Function(Pointer<Statement> statement) sqlite3_column_count;
Pointer<CBlob> Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_name;
Pointer<CBlob> Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_decltype;
int Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_type;
Pointer<Value> Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_value;
double Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_double;
int Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_int;
Pointer<CBlob> Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_text;
Pointer<CBlob> Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_blob;
/// Returns the amount of bytes to read when using [sqlite3_column_blob].
int Function(Pointer<Statement> statement, int columnIndex)
sqlite3_column_bytes;
int Function(Pointer<Database> db) sqlite3_changes;
int Function(Pointer<Database> db) sqlite3_last_insert_rowid;
Pointer<CBlob> Function(int code) sqlite3_errstr;
Pointer<CBlob> Function(Pointer<Database> database) sqlite3_errmsg;
int Function(Pointer<Statement> statement, int columnIndex, double value)
sqlite3_bind_double;
int Function(Pointer<Statement> statement, int columnIndex, int value)
sqlite3_bind_int;
int Function(
Pointer<Statement> statement,
int columnIndex,
Pointer<CBlob> value,
int minusOne,
Pointer<Void> disposeCb) sqlite3_bind_text;
int Function(
Pointer<Statement> statement,
int columnIndex,
Pointer<CBlob> value,
int length,
Pointer<Void> disposeCb) sqlite3_bind_blob;
int Function(Pointer<Statement> statement, int columnIndex) sqlite3_bind_null;
_SQLiteBindings() {
sqlite = open.openSqlite();
sqlite3_bind_double = sqlite
.lookup<NativeFunction<sqlite3_bind_double_native>>(
'sqlite3_bind_double')
.asFunction();
sqlite3_bind_int = sqlite
.lookup<NativeFunction<sqlite3_bind_int_native>>('sqlite3_bind_int')
.asFunction();
sqlite3_bind_text = sqlite
.lookup<NativeFunction<sqlite3_bind_text_native>>('sqlite3_bind_text')
.asFunction();
sqlite3_bind_blob = sqlite
.lookup<NativeFunction<sqlite3_bind_blob_native>>('sqlite3_bind_blob')
.asFunction();
sqlite3_bind_null = sqlite
.lookup<NativeFunction<sqlite3_bind_null_native>>('sqlite3_bind_null')
.asFunction();
sqlite3_open_v2 = sqlite
.lookup<NativeFunction<sqlite3_open_v2_native_t>>('sqlite3_open_v2')
.asFunction();
sqlite3_close_v2 = sqlite
.lookup<NativeFunction<sqlite3_close_v2_native_t>>('sqlite3_close_v2')
.asFunction();
sqlite3_free = sqlite
.lookup<NativeFunction<sqlite3_free_native>>('sqlite3_free')
.asFunction();
sqlite3_prepare_v2 = sqlite
.lookup<NativeFunction<sqlite3_prepare_v2_native_t>>(
'sqlite3_prepare_v2')
.asFunction();
sqlite3_exec = sqlite
.lookup<NativeFunction<sqlite3_exec_native>>('sqlite3_exec')
.asFunction();
sqlite3_step = sqlite
.lookup<NativeFunction<sqlite3_step_native_t>>('sqlite3_step')
.asFunction();
sqlite3_reset = sqlite
.lookup<NativeFunction<sqlite3_reset_native_t>>('sqlite3_reset')
.asFunction();
sqlite3_changes = sqlite
.lookup<NativeFunction<sqlite3_changes_native>>('sqlite3_changes')
.asFunction();
sqlite3_last_insert_rowid = sqlite
.lookup<NativeFunction<sqlite3_last_insert_rowid_native>>(
'sqlite3_last_insert_rowid')
.asFunction();
sqlite3_finalize = sqlite
.lookup<NativeFunction<sqlite3_finalize_native_t>>('sqlite3_finalize')
.asFunction();
sqlite3_errstr = sqlite
.lookup<NativeFunction<sqlite3_errstr_native_t>>('sqlite3_errstr')
.asFunction();
sqlite3_errmsg = sqlite
.lookup<NativeFunction<sqlite3_errmsg_native_t>>('sqlite3_errmsg')
.asFunction();
sqlite3_column_count = sqlite
.lookup<NativeFunction<sqlite3_column_count_native_t>>(
'sqlite3_column_count')
.asFunction();
sqlite3_column_name = sqlite
.lookup<NativeFunction<sqlite3_column_name_native_t>>(
'sqlite3_column_name')
.asFunction();
sqlite3_column_decltype = sqlite
.lookup<NativeFunction<sqlite3_column_decltype_native_t>>(
'sqlite3_column_decltype')
.asFunction();
sqlite3_column_type = sqlite
.lookup<NativeFunction<sqlite3_column_type_native_t>>(
'sqlite3_column_type')
.asFunction();
sqlite3_column_value = sqlite
.lookup<NativeFunction<sqlite3_column_value_native_t>>(
'sqlite3_column_value')
.asFunction();
sqlite3_column_double = sqlite
.lookup<NativeFunction<sqlite3_column_double_native_t>>(
'sqlite3_column_double')
.asFunction();
sqlite3_column_int = sqlite
.lookup<NativeFunction<sqlite3_column_int_native_t>>(
'sqlite3_column_int')
.asFunction();
sqlite3_column_text = sqlite
.lookup<NativeFunction<sqlite3_column_text_native_t>>(
'sqlite3_column_text')
.asFunction();
sqlite3_column_blob = sqlite
.lookup<NativeFunction<sqlite3_column_blob_native_t>>(
'sqlite3_column_blob')
.asFunction();
sqlite3_column_bytes = sqlite
.lookup<NativeFunction<sqlite3_column_bytes_native_t>>(
'sqlite3_column_bytes')
.asFunction();
}
}
_SQLiteBindings _cachedBindings;
_SQLiteBindings get bindings => _cachedBindings ??= _SQLiteBindings();

View File

@ -0,0 +1,184 @@
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: constant_identifier_names
/// Result Codes
///
/// Many SQLite functions return an integer result code from the set shown
/// here in order to indicates success or failure.
///
/// New error codes may be added in future versions of SQLite.
///
/// See also: SQLITE_IOERR_READ | extended result codes,
/// sqlite3_vtab_on_conflict() SQLITE_ROLLBACK | result codes.
class Errors {
/// Successful result
static const int SQLITE_OK = 0;
/// Generic error
static const int SQLITE_ERROR = 1;
/// Internal logic error in SQLite
static const int SQLITE_INTERNAL = 2;
/// Access permission denied
static const int SQLITE_PERM = 3;
/// Callback routine requested an abort
static const int SQLITE_ABORT = 4;
/// The database file is locked
static const int SQLITE_BUSY = 5;
/// A table in the database is locked
static const int SQLITE_LOCKED = 6;
/// A malloc() failed
static const int SQLITE_NOMEM = 7;
/// Attempt to write a readonly database
static const int SQLITE_READONLY = 8;
/// Operation terminated by sqlite3_interrupt()
static const int SQLITE_INTERRUPT = 9;
/// Some kind of disk I/O error occurred
static const int SQLITE_IOERR = 10;
/// The database disk image is malformed
static const int SQLITE_CORRUPT = 11;
/// Unknown opcode in sqlite3_file_control()
static const int SQLITE_NOTFOUND = 12;
/// Insertion failed because database is full
static const int SQLITE_FULL = 13;
/// Unable to open the database file
static const int SQLITE_CANTOPEN = 14;
/// Database lock protocol error
static const int SQLITE_PROTOCOL = 15;
/// Internal use only
static const int SQLITE_EMPTY = 16;
/// The database schema changed
static const int SQLITE_SCHEMA = 17;
/// String or BLOB exceeds size limit
static const int SQLITE_TOOBIG = 18;
/// Abort due to constraint violation
static const int SQLITE_CONSTRAINT = 19;
/// Data type mismatch
static const int SQLITE_MISMATCH = 20;
/// Library used incorrectly
static const int SQLITE_MISUSE = 21;
/// Uses OS features not supported on host
static const int SQLITE_NOLFS = 22;
/// Authorization denied
static const int SQLITE_AUTH = 23;
/// Not used
static const int SQLITE_FORMAT = 24;
/// 2nd parameter to sqlite3_bind out of range
static const int SQLITE_RANGE = 25;
/// File opened that is not a database file
static const int SQLITE_NOTADB = 26;
/// Notifications from sqlite3_log()
static const int SQLITE_NOTICE = 27;
/// Warnings from sqlite3_log()
static const int SQLITE_WARNING = 28;
/// sqlite3_step() has another row ready
static const int SQLITE_ROW = 100;
/// sqlite3_step() has finished executing
static const int SQLITE_DONE = 101;
}
/// Flags For File Open Operations
///
/// These bit values are intended for use in the
/// 3rd parameter to the [sqlite3_open_v2()] interface and
/// in the 4th parameter to the `sqlite3_vfs.xOpen` method.
class Flags {
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_READONLY = 0x00000001;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_READWRITE = 0x00000002;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_CREATE = 0x00000004;
/// VFS only
static const int SQLITE_OPEN_DELETEONCLOSE = 0x00000008;
/// VFS only
static const int SQLITE_OPEN_EXCLUSIVE = 0x00000010;
/// VFS only
static const int SQLITE_OPEN_AUTOPROXY = 0x00000020;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_URI = 0x00000040;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_MEMORY = 0x00000080;
/// VFS only
static const int SQLITE_OPEN_MAIN_DB = 0x00000100;
/// VFS only
static const int SQLITE_OPEN_TEMP_DB = 0x00000200;
/// VFS only
static const int SQLITE_OPEN_TRANSIENT_DB = 0x00000400;
/// VFS only
static const int SQLITE_OPEN_MAIN_JOURNAL = 0x00000800;
/// VFS only
static const int SQLITE_OPEN_TEMP_JOURNAL = 0x00001000;
/// VFS only
static const int SQLITE_OPEN_SUBJOURNAL = 0x00002000;
/// VFS only
static const int SQLITE_OPEN_MASTER_JOURNAL = 0x00004000;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_NOMUTEX = 0x00008000;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_FULLMUTEX = 0x00010000;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_SHAREDCACHE = 0x00020000;
/// Ok for sqlite3_open_v2()
static const int SQLITE_OPEN_PRIVATECACHE = 0x00040000;
/// VFS only
static const int SQLITE_OPEN_WAL = 0x00080000;
}
class Types {
static const int SQLITE_INTEGER = 1;
static const int SQLITE_FLOAT = 2;
static const int SQLITE_TEXT = 3;
static const int SQLITE_BLOB = 4;
static const int SQLITE_NULL = 5;
}

View File

@ -0,0 +1,95 @@
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ffi';
import '../ffi/blob.dart';
import 'types.dart';
typedef sqlite3_open_v2_native_t = Int32 Function(Pointer<CBlob> filename,
Pointer<Pointer<Database>> ppDb, Int32 flags, Pointer<CBlob> vfs);
typedef sqlite3_close_v2_native_t = Int32 Function(Pointer<Database> database);
typedef sqlite3_free_native = Void Function(Pointer<Void> pointer);
typedef sqlite3_prepare_v2_native_t = Int32 Function(
Pointer<Database> database,
Pointer<CBlob> query,
Int32 nbytes,
Pointer<Pointer<Statement>> statementOut,
Pointer<Pointer<CBlob>> tail);
typedef sqlite3_exec_native = Int32 Function(
Pointer<Database> database,
Pointer<CBlob> query,
Pointer<Void> callback,
Pointer<Void> firstCbArg,
Pointer<Pointer<CBlob>> errorOut);
typedef sqlite3_step_native_t = Int32 Function(Pointer<Statement> statement);
typedef sqlite3_reset_native_t = Int32 Function(Pointer<Statement> statement);
typedef sqlite3_finalize_native_t = Int32 Function(
Pointer<Statement> statement);
typedef sqlite3_errstr_native_t = Pointer<CBlob> Function(Int32 error);
typedef sqlite3_errmsg_native_t = Pointer<CBlob> Function(
Pointer<Database> database);
typedef sqlite3_column_count_native_t = Int32 Function(
Pointer<Statement> statement);
typedef sqlite3_column_name_native_t = Pointer<CBlob> Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_decltype_native_t = Pointer<CBlob> Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_type_native_t = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_value_native_t = Pointer<Value> Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_double_native_t = Double Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_int_native_t = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_text_native_t = Pointer<CBlob> Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_blob_native_t = Pointer<CBlob> Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_column_bytes_native_t = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex);
typedef sqlite3_changes_native = Int32 Function(Pointer<Database> database);
typedef sqlite3_last_insert_rowid_native = Int64 Function(
Pointer<Database> database);
typedef sqlite3_bind_double_native = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex, Double value);
typedef sqlite3_bind_int_native = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex, Int32 value);
typedef sqlite3_bind_text_native = Int32 Function(
Pointer<Statement> statement,
Int32 columnIndex,
Pointer<CBlob> value,
Int32 length,
Pointer<Void> callback);
typedef sqlite3_bind_blob_native = Int32 Function(
Pointer<Statement> statement,
Int32 columnIndex,
Pointer<CBlob> value,
Int32 length,
Pointer<Void> callback);
typedef sqlite3_bind_null_native = Int32 Function(
Pointer<Statement> statement, Int32 columnIndex);

View File

@ -0,0 +1,77 @@
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ffi';
// ignore_for_file: comment_references
/// Database Connection Handle
///
/// Each open SQLite database is represented by a pointer to an instance of
/// the opaque structure named "sqlite3". It is useful to think of an sqlite3
/// pointer as an object. The [sqlite3_open()], [sqlite3_open16()], and
/// [sqlite3_open_v2()] interfaces are its constructors, and [sqlite3_close()]
/// is its destructor. There are many other interfaces (such as
/// [sqlite3_prepare_v2()], [sqlite3_create_function()], and
/// [sqlite3_busy_timeout()] to name but three) that are methods on an
class Database extends Struct<Database> {}
/// SQL Statement Object
///
/// An instance of this object represents a single SQL statement.
/// This object is variously known as a "prepared statement" or a
/// "compiled SQL statement" or simply as a "statement".
///
/// The life of a statement object goes something like this:
///
/// <ol>
/// <li> Create the object using [sqlite3_prepare_v2()] or a related
/// function.
/// <li> Bind values to [host parameters] using the sqlite3_bind_*()
/// interfaces.
/// <li> Run the SQL by calling [sqlite3_step()] one or more times.
/// <li> Reset the statement using [sqlite3_reset()] then go back
/// to step 2. Do this zero or more times.
/// <li> Destroy the object using [sqlite3_finalize()].
/// </ol>
///
/// Refer to documentation on individual methods above for additional
/// information.
class Statement extends Struct<Statement> {}
/// Dynamically Typed Value Object
///
/// SQLite uses the sqlite3_value object to represent all values
/// that can be stored in a database table. SQLite uses dynamic typing
/// for the values it stores. ^Values stored in sqlite3_value objects
/// can be integers, floating point values, strings, BLOBs, or NULL.
///
/// An sqlite3_value object may be either "protected" or "unprotected".
/// Some interfaces require a protected sqlite3_value. Other interfaces
/// will accept either a protected or an unprotected sqlite3_value.
/// Every interface that accepts sqlite3_value arguments specifies
/// whether or not it requires a protected sqlite3_value.
///
/// The terms "protected" and "unprotected" refer to whether or not
/// a mutex is held. An internal mutex is held for a protected
/// sqlite3_value object but no mutex is held for an unprotected
/// sqlite3_value object. If SQLite is compiled to be single-threaded
/// (with [SQLITE_THREADSAFE=0] and with [sqlite3_threadsafe()] returning 0)
/// or if SQLite is run in one of reduced mutex modes
/// [SQLITE_CONFIG_SINGLETHREAD] or [SQLITE_CONFIG_MULTITHREAD]
/// then there is no distinction between protected and unprotected
/// sqlite3_value objects and they can be used interchangeably. However,
/// for maximum code portability it is recommended that applications
/// still make the distinction between protected and unprotected
/// sqlite3_value objects even when not strictly required.
///
/// ^The sqlite3_value objects that are passed as parameters into the
/// implementation of [application-defined SQL functions] are protected.
/// ^The sqlite3_value object returned by
/// [sqlite3_column_value()] is unprotected.
/// Unprotected sqlite3_value objects may only be used with
/// [sqlite3_result_value()] and [sqlite3_bind_value()].
/// The [sqlite3_value_blob | sqlite3_value_type()] family of
/// interfaces require protected sqlite3_value objects.
class Value extends Struct<Value> {}

View File

@ -0,0 +1,55 @@
import 'dart:convert';
import 'dart:ffi';
import 'dart:typed_data';
import 'package:moor_ffi/src/ffi/utils.dart';
/// Pointer to arbitrary blobs in C.
class CBlob extends Struct<CBlob> {
@Uint8()
int data;
static Pointer<CBlob> allocate(Uint8List blob) {
final str = Pointer<CBlob>.allocate(count: blob.length);
for (var i = 0; i < blob.length; i++) {
str.elementAt(i).load<CBlob>().data = blob[i];
}
return str;
}
/// Allocates a 0-terminated string, encoded as utf8 and read from the
/// [string].
static Pointer<CBlob> allocateString(String string) {
final encoded = utf8.encode(string);
final data = Uint8List(encoded.length + 1) // already filled with zeroes
..setAll(0, encoded);
return CBlob.allocate(data);
}
/// Reads [bytesToRead] bytes from the current position.
Uint8List read(int bytesToRead) {
assert(bytesToRead >= 0);
final str = addressOf;
if (isNullPointer(str)) return null;
// todo can we user Pointer.asExternalTypedData here?
final blob = Uint8List(bytesToRead);
for (var i = 0; i < bytesToRead; ++i) {
blob[i] = str.elementAt(i).load<CBlob>().data;
}
return blob;
}
/// Reads a 0-terminated string, decoded with utf8.
String readString() {
final str = addressOf;
if (isNullPointer(str)) return null;
var len = 0;
while (str.elementAt(++len).load<CBlob>().data != 0) {}
final units = read(len);
return utf8.decode(units);
}
}

View File

@ -0,0 +1,3 @@
import 'dart:ffi';
bool isNullPointer<T extends NativeType>(Pointer<T> ptr) => ptr == nullptr;

View File

@ -0,0 +1,164 @@
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
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/types.dart' as types;
import 'package:moor_ffi/src/bindings/bindings.dart';
import 'package:moor_ffi/src/ffi/blob.dart';
import 'package:moor_ffi/src/ffi/utils.dart';
part 'errors.dart';
part 'prepared_statement.dart';
const _openingFlags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE;
class Database implements BaseDatabase {
final Pointer<types.Database> _db;
final List<PreparedStatement> _preparedStmt = [];
bool _isClosed = false;
Database._(this._db);
/// Opens the [file] as a sqlite3 database. The file will be created if it
/// doesn't exist.
factory Database.openFile(File file) => Database.open(file.absolute.path);
/// Opens an in-memory sqlite3 database.
factory Database.memory() => Database.open(':memory:');
/// Opens an sqlite3 database from a filename.
factory Database.open(String fileName) {
final dbOut = Pointer<Pointer<types.Database>>.allocate();
final pathC = CBlob.allocateString(fileName);
final resultCode =
bindings.sqlite3_open_v2(pathC, dbOut, _openingFlags, nullptr.cast());
final dbPointer = dbOut.load<Pointer<types.Database>>();
dbOut.free();
pathC.free();
if (resultCode == Errors.SQLITE_OK) {
return Database._(dbPointer);
} else {
bindings.sqlite3_close_v2(dbPointer);
throw SqliteException._fromErrorCode(dbPointer, resultCode);
}
}
void _ensureOpen() {
if (_isClosed) {
throw Exception('This database has already been closed');
}
}
@override
void close() {
// close all prepared statements first
_isClosed = true;
for (var stmt in _preparedStmt) {
stmt.close();
}
final code = bindings.sqlite3_close_v2(_db);
SqliteException exception;
if (code != Errors.SQLITE_OK) {
exception = SqliteException._fromErrorCode(_db, code);
}
_isClosed = true;
// we don't need to deallocate the _db pointer, sqlite takes care of that
if (exception != null) {
throw exception;
}
}
void _handleStmtFinalized(PreparedStatement stmt) {
if (!_isClosed) {
_preparedStmt.remove(stmt);
}
}
@override
void execute(String sql) {
_ensureOpen();
final sqlPtr = CBlob.allocateString(sql);
final errorOut = Pointer<Pointer<CBlob>>.allocate();
final result =
bindings.sqlite3_exec(_db, sqlPtr, nullptr, nullptr, errorOut);
sqlPtr.free();
final errorPtr = errorOut.load<Pointer<CBlob>>();
errorOut.free();
String errorMsg;
if (!isNullPointer(errorPtr)) {
errorMsg = errorPtr.load<CBlob>().readString();
// the message was allocated from sqlite, we need to free it
bindings.sqlite3_free(errorPtr.cast());
}
if (result != Errors.SQLITE_OK) {
throw SqliteException(errorMsg);
}
}
@override
PreparedStatement prepare(String sql) {
_ensureOpen();
final stmtOut = Pointer<Pointer<types.Statement>>.allocate();
final sqlPtr = CBlob.allocateString(sql);
final resultCode =
bindings.sqlite3_prepare_v2(_db, sqlPtr, -1, stmtOut, nullptr.cast());
sqlPtr.free();
final stmt = stmtOut.load<Pointer<types.Statement>>();
stmtOut.free();
if (resultCode != Errors.SQLITE_OK) {
// we don't need to worry about freeing the statement. If preparing the
// statement was unsuccessful, stmtOut.load() will be null
throw SqliteException._fromErrorCode(_db, resultCode);
}
final prepared = PreparedStatement._(stmt, this);
_preparedStmt.add(prepared);
return prepared;
}
@override
int userVersion() {
final stmt = prepare('PRAGMA user_version');
final result = stmt.select();
stmt.close();
return result.first.columnAt(0) as int;
}
@override
void setUserVersion(int version) {
execute('PRAGMA user_version = $version');
}
@override
int getUpdatedRows() {
_ensureOpen();
return bindings.sqlite3_changes(_db);
}
@override
int getLastInsertId() {
_ensureOpen();
return bindings.sqlite3_last_insert_rowid(_db);
}
}

View File

@ -0,0 +1,33 @@
part of 'database.dart';
class SqliteException implements Exception {
final String message;
final String explanation;
SqliteException(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."
// https://www.sqlite.org/c3ref/errcode.html
final dbMessage = bindings.sqlite3_errmsg(db).load<CBlob>().readString();
String explanation;
if (code != null) {
explanation = bindings.sqlite3_errstr(code).load<CBlob>().readString();
}
return SqliteException(dbMessage, explanation);
}
@override
String toString() {
if (explanation == null) {
return 'SqliteException: $message';
} else {
return 'SqliteException: $message, $explanation';
}
}
}

Some files were not shown because too many files have changed in this diff Show More