mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into beta
This commit is contained in:
commit
18639a364c
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>`.
|
||||
* `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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
|
@ -110,3 +140,6 @@ 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.
|
||||
|
||||
Multiple inserts can be batched by using `insertAll` - it takes a list of companions instead
|
||||
of a single companion.
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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`
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -11,6 +11,8 @@ dependencies:
|
|||
moor_flutter:
|
||||
tests:
|
||||
path: ../tests
|
||||
moor_ffi:
|
||||
path: ../../../moor_ffi
|
||||
|
||||
dev_dependencies:
|
||||
test:
|
||||
|
|
|
@ -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!');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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/
|
|
@ -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
|
|
@ -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());
|
||||
}
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
|
||||
#analyzer:
|
||||
# plugins:
|
||||
# - moor
|
|
@ -0,0 +1 @@
|
|||
class Test {}
|
|
@ -0,0 +1,3 @@
|
|||
CREATE TABLE playground (
|
||||
id INT NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
)
|
|
@ -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;
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -3,4 +3,4 @@ targets:
|
|||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
generate_private_watch_methods: true
|
||||
override_hash_and_equals_in_result_sets: true
|
||||
|
|
|
@ -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')));
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
if (writeOrderBy) {
|
||||
context.buffer.write('ORDER BY ');
|
||||
}
|
||||
|
||||
for (var term in terms) {
|
||||
if (first) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
// ignore: invariant_booleans
|
||||
if (_openingCompleter != null) {
|
||||
return _openingCompleter.future;
|
||||
if (alreadyOpen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// not already open or opening. Open the database now!
|
||||
_openingCompleter = Completer();
|
||||
await delegate.open(databaseInfo);
|
||||
await _runMigrations();
|
||||
|
||||
_openingCompleter.complete(true);
|
||||
_openingCompleter = null;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runMigrations() async {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 '
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1,12 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
.externalNativeBuild/
|
||||
cpp/sqlite*
|
||||
sqlite_*.zip
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.4.1)
|
||||
project(sqlite3)
|
||||
|
||||
add_library(sqlite3 SHARED sqlite3.c)
|
|
@ -0,0 +1,2 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = 'moor_ffi'
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface MoorFfiPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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> {}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import 'dart:ffi';
|
||||
|
||||
bool isNullPointer<T extends NativeType>(Pointer<T> ptr) => ptr == nullptr;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue