mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop'
# Conflicts: # moor/CHANGELOG.md # moor/pubspec.yaml # moor/test/batch_test.dart
This commit is contained in:
commit
ec1072dce8
|
@ -82,6 +82,10 @@ weight = 1
|
||||||
name = "GitHub"
|
name = "GitHub"
|
||||||
weight = 110
|
weight = 110
|
||||||
url = "https://github.com/simolus3/moor/"
|
url = "https://github.com/simolus3/moor/"
|
||||||
|
[[menu.main]]
|
||||||
|
name = "API docs"
|
||||||
|
weight = 120
|
||||||
|
url = "https://pub.dev/documentation/moor/latest/"
|
||||||
|
|
||||||
# Everything below this are Site Params
|
# Everything below this are Site Params
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,8 @@ At the moment, moor supports these options:
|
||||||
* `legacy_type_inference`: Use the old type inference from moor 1 and 2. Note that `use_experimental_inference`
|
* `legacy_type_inference`: Use the old type inference from moor 1 and 2. Note that `use_experimental_inference`
|
||||||
is now the default and no longer exists.
|
is now the default and no longer exists.
|
||||||
If you're using this flag, please open an issue and explain how the new inference isn't working for you, thanks!
|
If you're using this flag, please open an issue and explain how the new inference isn't working for you, thanks!
|
||||||
|
* `data_class_to_companions` (defaults to `true`): Controls whether moor will write the `toCompanion` method in generated
|
||||||
|
data classes.
|
||||||
|
|
||||||
## Available extensions
|
## Available extensions
|
||||||
|
|
||||||
|
@ -90,12 +92,14 @@ We currently support the following extensions:
|
||||||
|
|
||||||
## Recommended options
|
## Recommended options
|
||||||
|
|
||||||
In general, we recommend not enabling these options unless you need to. There are some exceptions though:
|
In general, we recommend using the default options.
|
||||||
|
|
||||||
- `compact_query_methods` and `use_column_name_as_json_key_when_defined_in_moor_file`: We recommend enabling
|
You can disable some default moor features and reduce the amount of generated code with the following options:
|
||||||
both flags for new projects because they'll be the only option in the next breaking release.
|
|
||||||
- `skip_verification_code`: You can remove a significant portion of generated code with this option. The
|
- `skip_verification_code: true`: You can remove a significant portion of generated code with this option. The
|
||||||
downside is that error messages when inserting invalid data will be less specific.
|
downside is that error messages when inserting invalid data will be less specific.
|
||||||
|
- `data_class_to_companions: false`: Don't generate the `toCompanion` method on data classes. If you don't need that
|
||||||
|
method, you can disable this option.
|
||||||
|
|
||||||
## Using moor classes in other builders
|
## Using moor classes in other builders
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,48 @@ The generated `User` class will then have a `preferences` column of type
|
||||||
the object in `select`, `update` and `insert` statements. This feature
|
the object in `select`, `update` and `insert` statements. This feature
|
||||||
also works with [compiled custom queries]({{ "/queries/custom" | absolute_url }}).
|
also works with [compiled custom queries]({{ "/queries/custom" | absolute_url }}).
|
||||||
|
|
||||||
|
### Implicit enum converters
|
||||||
|
|
||||||
|
A common scenario for type converters is to map between enums and integers by representing enums
|
||||||
|
as their index. Since this is so common, moor has the integrated `intEnum` column type to make this
|
||||||
|
easier.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
enum Status {
|
||||||
|
none,
|
||||||
|
running,
|
||||||
|
stopped,
|
||||||
|
paused
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tasks extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get status => intEnum<Status>()();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{% alert title="Caution with enums" color="warning" %}}
|
||||||
|
> It can be easy to accidentally invalidate your database by introducing another enum value.
|
||||||
|
For instance, let's say we inserted a `Task` into the database in the above example and set its
|
||||||
|
`Status` to `running` (index = 1).
|
||||||
|
Now we `Status` enum to include another entry:
|
||||||
|
```dart
|
||||||
|
enum Status {
|
||||||
|
none,
|
||||||
|
starting, // new!
|
||||||
|
running,
|
||||||
|
stopped,
|
||||||
|
paused
|
||||||
|
}
|
||||||
|
```
|
||||||
|
When selecting the task, it will now report as `starting`, as that's the new value at index 1.
|
||||||
|
For this reason, it's best to add new values at the end of the enumeration, where they can't conflict
|
||||||
|
with existing values. Otherwise you'd need to bump your schema version and run a custom update statement
|
||||||
|
to fix this.
|
||||||
|
{{% /alert %}}
|
||||||
|
|
||||||
|
Also note that you can't apply another type converter on a column declared with an enum converter.
|
||||||
|
|
||||||
## Using converters in moor
|
## Using converters in moor
|
||||||
|
|
||||||
Since moor 2.4, type converters can also be used inside moor files.
|
Since moor 2.4, type converters can also be used inside moor files.
|
||||||
|
@ -91,4 +133,17 @@ CREATE TABLE users (
|
||||||
name TEXT,
|
name TEXT,
|
||||||
preferences TEXT MAPPED BY `const PreferenceConverter()`
|
preferences TEXT MAPPED BY `const PreferenceConverter()`
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Moor files also have special support for implicit enum converters:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
import 'status.dart';
|
||||||
|
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
status ENUM(Status)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course, the warning about automatic enum converters also applies to moor files.
|
|
@ -69,9 +69,9 @@ Future<void> writeShoppingCart(CartWithItems entry) {
|
||||||
.go();
|
.go();
|
||||||
|
|
||||||
// And write the new ones
|
// And write the new ones
|
||||||
await into(shoppingCartEntries).insertAll([
|
for (final item in entry.items) {
|
||||||
for (var item in entry.items) ShoppingCartEntry(shoppingCart: cart.id, item: item.id)
|
await into(shoppingCartEntries).insert(ShoppingCartEntry(shoppingCart: cart.id, item: item.id));
|
||||||
]);
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -181,6 +181,8 @@ Future<void> insertMultipleEntries() async{
|
||||||
TodosCompanion.insert(
|
TodosCompanion.insert(
|
||||||
title: 'Another entry',
|
title: 'Another entry',
|
||||||
content: 'More content',
|
content: 'More content',
|
||||||
|
// columns that aren't required for inserts are still wrapped in a Value:
|
||||||
|
category: Value(3),
|
||||||
),
|
),
|
||||||
// ...
|
// ...
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Welcome to Moor"
|
title: "Welcome to Moor"
|
||||||
linkTitle: "Documentation"
|
linkTitle: "Documentation & Guides"
|
||||||
weight: 20
|
weight: 20
|
||||||
menu:
|
menu:
|
||||||
main:
|
main:
|
||||||
|
|
|
@ -104,3 +104,10 @@ Firebase is a very good option when
|
||||||
|
|
||||||
- your data model can be expressed as documents instead of relations
|
- your data model can be expressed as documents instead of relations
|
||||||
- you don't have your own backend, but still need to synchronize data
|
- you don't have your own backend, but still need to synchronize data
|
||||||
|
|
||||||
|
## Can I view a moor database?
|
||||||
|
|
||||||
|
Yes! Moor stores its data in a sqlite3 database file that can be extracted from the device and inspected locally.
|
||||||
|
|
||||||
|
To inspect a database on a device directly, you can use the [`moor_db_viewer`](https://pub.dev/packages/moor_db_viewer)
|
||||||
|
package by Koen Van Looveren.
|
|
@ -98,37 +98,41 @@ class Database extends _$Database {
|
||||||
/// It will be set in the onUpgrade callback. Null if no migration occurred
|
/// It will be set in the onUpgrade callback. Null if no migration occurred
|
||||||
int schemaVersionChangedTo;
|
int schemaVersionChangedTo;
|
||||||
|
|
||||||
|
MigrationStrategy overrideMigration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
return MigrationStrategy(
|
return overrideMigration ??
|
||||||
onCreate: (m) async {
|
MigrationStrategy(
|
||||||
await m.createTable(users);
|
onCreate: (m) async {
|
||||||
if (schemaVersion >= 2) {
|
await m.createTable(users);
|
||||||
// ensure that transactions can be used in a migration callback.
|
if (schemaVersion >= 2) {
|
||||||
await transaction(() async {
|
// ensure that transactions can be used in a migration callback.
|
||||||
await m.createTable(friendships);
|
await transaction(() async {
|
||||||
});
|
await m.createTable(friendships);
|
||||||
}
|
});
|
||||||
},
|
}
|
||||||
onUpgrade: (m, from, to) async {
|
},
|
||||||
schemaVersionChangedFrom = from;
|
onUpgrade: (m, from, to) async {
|
||||||
schemaVersionChangedTo = to;
|
schemaVersionChangedFrom = from;
|
||||||
|
schemaVersionChangedTo = to;
|
||||||
|
|
||||||
if (from == 1) {
|
if (from == 1) {
|
||||||
await m.createTable(friendships);
|
await m.createTable(friendships);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
if (details.wasCreated) {
|
if (details.wasCreated) {
|
||||||
// make sure that transactions can be used in the beforeOpen callback.
|
// make sure that transactions can be used in the beforeOpen callback.
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
await batch((batch) {
|
await batch((batch) {
|
||||||
batch.insertAll(users, [people.dash, people.duke, people.gopher]);
|
batch.insertAll(
|
||||||
});
|
users, [people.dash, people.duke, people.gopher]);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
},
|
}
|
||||||
);
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteUser(User user, {bool fail = false}) {
|
Future<void> deleteUser(User user, {bool fail = false}) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:tests/data/sample_data.dart' as people;
|
import 'package:tests/data/sample_data.dart' as people;
|
||||||
import 'package:tests/database/database.dart';
|
import 'package:tests/database/database.dart';
|
||||||
|
import 'package:tests/tests.dart';
|
||||||
|
|
||||||
import 'suite.dart';
|
import 'suite.dart';
|
||||||
|
|
||||||
|
@ -31,6 +32,19 @@ void migrationTests(TestExecutor executor) {
|
||||||
await database.close();
|
await database.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can use destructive migration', () async {
|
||||||
|
final old = Database(executor.createConnection(), schemaVersion: 1);
|
||||||
|
await old.executor.ensureOpen(old);
|
||||||
|
await old.close();
|
||||||
|
|
||||||
|
final database = Database(executor.createConnection(), schemaVersion: 2);
|
||||||
|
database.overrideMigration = database.destructiveFallback;
|
||||||
|
|
||||||
|
// No users now, we deleted everything
|
||||||
|
final count = await database.userCount().getSingle();
|
||||||
|
expect(count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('runs the migrator when downgrading', () async {
|
test('runs the migrator when downgrading', () async {
|
||||||
var database = Database(executor.createConnection(), schemaVersion: 2);
|
var database = Database(executor.createConnection(), schemaVersion: 2);
|
||||||
await database.executor.ensureOpen(database); // Create the database
|
await database.executor.ensureOpen(database); // Create the database
|
||||||
|
|
|
@ -24,4 +24,10 @@ void runAllTests(TestExecutor executor) {
|
||||||
migrationTests(executor);
|
migrationTests(executor);
|
||||||
customObjectTests(executor);
|
customObjectTests(executor);
|
||||||
transactionTests(executor);
|
transactionTests(executor);
|
||||||
|
|
||||||
|
test('can close database without interacting with it', () async {
|
||||||
|
final connection = executor.createConnection();
|
||||||
|
|
||||||
|
await connection.executor.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
## 3.1.0
|
||||||
|
|
||||||
|
- Update companions now implement `==` and `hashCode`
|
||||||
|
- New `containsCase` method for text in `package:moor/extensions/moor_ffi.dart`
|
||||||
|
- The `toCompanion` method is back for data classes, but its generation can be disabled with a build option
|
||||||
|
- New feature: [Implicit enum converters](https://moor.simonbinder.eu/docs/advanced-features/type_converters/#implicit-enum-converters)
|
||||||
|
- Added the `destructiveFallback` extension to databases. It can be used in `migrationStrategy` to delete
|
||||||
|
and then re-create all tables, indices and triggers.
|
||||||
|
|
||||||
## 3.0.2
|
## 3.0.2
|
||||||
|
|
||||||
- Fix update statements not escaping column names ([#539](https://github.com/simolus3/moor/issues/539))
|
- Fix update statements not escaping column names ([#539](https://github.com/simolus3/moor/issues/539))
|
||||||
|
|
|
@ -34,6 +34,15 @@ class Category extends DataClass implements Insertable<Category> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CategoriesCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return CategoriesCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
description: description == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(description),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory Category.fromJson(Map<String, dynamic> json,
|
factory Category.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -230,6 +239,20 @@ class Recipe extends DataClass implements Insertable<Recipe> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecipesCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return RecipesCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
title:
|
||||||
|
title == null && nullToAbsent ? const Value.absent() : Value(title),
|
||||||
|
instructions: instructions == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(instructions),
|
||||||
|
category: category == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(category),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory Recipe.fromJson(Map<String, dynamic> json,
|
factory Recipe.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -481,6 +504,16 @@ class Ingredient extends DataClass implements Insertable<Ingredient> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IngredientsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return IngredientsCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
name: name == null && nullToAbsent ? const Value.absent() : Value(name),
|
||||||
|
caloriesPer100g: caloriesPer100g == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(caloriesPer100g),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory Ingredient.fromJson(Map<String, dynamic> json,
|
factory Ingredient.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -708,6 +741,19 @@ class IngredientInRecipe extends DataClass
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IngredientInRecipesCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return IngredientInRecipesCompanion(
|
||||||
|
recipe:
|
||||||
|
recipe == null && nullToAbsent ? const Value.absent() : Value(recipe),
|
||||||
|
ingredient: ingredient == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(ingredient),
|
||||||
|
amountInGrams: amountInGrams == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(amountInGrams),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory IngredientInRecipe.fromJson(Map<String, dynamic> json,
|
factory IngredientInRecipe.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
|
|
@ -81,3 +81,33 @@ Expression<num> sqlAcos(Expression<num> value) {
|
||||||
Expression<num> sqlAtan(Expression<num> value) {
|
Expression<num> sqlAtan(Expression<num> value) {
|
||||||
return FunctionCallExpression('atan', [value]);
|
return FunctionCallExpression('atan', [value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds functionality to string expressions that only work when using
|
||||||
|
/// `moor_ffi`.
|
||||||
|
extension MoorFfiSpecificStringExtensions on Expression<String> {
|
||||||
|
/// Version of `contains` that allows controlling case sensitivity better.
|
||||||
|
///
|
||||||
|
/// The default `contains` method uses sqlite's `LIKE`, which is case-
|
||||||
|
/// insensitive for the English alphabet only. [containsCase] is implemented
|
||||||
|
/// in Dart with better support for casing.
|
||||||
|
/// When [caseSensitive] is false (the default), this is equivalent to the
|
||||||
|
/// Dart expression `this.contains(substring)`, where `this` is the string
|
||||||
|
/// value this expression evaluates to.
|
||||||
|
/// When [caseSensitive] is true, the equivalent Dart expression would be
|
||||||
|
/// `this.toLowerCase().contains(substring.toLowerCase())`.
|
||||||
|
///
|
||||||
|
/// Note that, while Dart has better support for an international alphabet,
|
||||||
|
/// it can still yield unexpected results like the
|
||||||
|
/// [Turkish İ Problem](https://haacked.com/archive/2012/07/05/turkish-i-problem-and-why-you-should-care.aspx/)
|
||||||
|
///
|
||||||
|
/// Note that this is only available when using `moor_ffi` version 0.6.0 or
|
||||||
|
/// greater.
|
||||||
|
Expression<bool> containsCase(String substring,
|
||||||
|
{bool caseSensitive = false}) {
|
||||||
|
return FunctionCallExpression('moor_contains', [
|
||||||
|
this,
|
||||||
|
Variable<String>(substring),
|
||||||
|
if (caseSensitive) const Constant<int>(1) else const Constant<int>(0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -62,6 +62,14 @@ abstract class Table {
|
||||||
@protected
|
@protected
|
||||||
IntColumnBuilder integer() => _isGenerated();
|
IntColumnBuilder integer() => _isGenerated();
|
||||||
|
|
||||||
|
/// Creates a column to store an `enum` class [T].
|
||||||
|
///
|
||||||
|
/// In the database, the column will be represented as an integer
|
||||||
|
/// corresponding to the enums index. Note that this can invalidate your data
|
||||||
|
/// if you add another value to the enum class.
|
||||||
|
@protected
|
||||||
|
IntColumnBuilder intEnum<T>() => _isGenerated();
|
||||||
|
|
||||||
/// Use this as the body of a getter to declare a column that holds strings.
|
/// Use this as the body of a getter to declare a column that holds strings.
|
||||||
/// Example (inside the body of a table class):
|
/// Example (inside the body of a table class):
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moor/moor.dart';
|
import 'package:moor/moor.dart';
|
||||||
|
|
||||||
|
@ -59,6 +60,25 @@ abstract class DataClass {
|
||||||
abstract class UpdateCompanion<D extends DataClass> implements Insertable<D> {
|
abstract class UpdateCompanion<D extends DataClass> implements Insertable<D> {
|
||||||
/// Constant constructor so that generated companion classes can be constant.
|
/// Constant constructor so that generated companion classes can be constant.
|
||||||
const UpdateCompanion();
|
const UpdateCompanion();
|
||||||
|
|
||||||
|
static const _mapEquality = MapEquality<dynamic, dynamic>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return _mapEquality.hash(toColumns(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is! UpdateCompanion<D>) return false;
|
||||||
|
|
||||||
|
return _mapEquality.equals(
|
||||||
|
// ignore: test_types_in_equals
|
||||||
|
(other as UpdateCompanion<D>).toColumns(false),
|
||||||
|
toColumns(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [Insertable] implementation based on raw column expressions.
|
/// An [Insertable] implementation based on raw column expressions.
|
||||||
|
|
|
@ -291,7 +291,12 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
return delegate.close();
|
if (_ensureOpenCalled) {
|
||||||
|
return delegate.close();
|
||||||
|
} else {
|
||||||
|
// User never attempted to open the database, so this is a no-op.
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,4 +322,7 @@ class _BeforeOpeningExecutor extends QueryExecutor
|
||||||
|
|
||||||
@override
|
@override
|
||||||
QueryDelegate get impl => _base.impl;
|
QueryDelegate get impl => _base.impl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get logStatements => _base.logStatements;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ class Variable<T> extends Expression<T> {
|
||||||
@override
|
@override
|
||||||
Precedence get precedence => Precedence.primary;
|
Precedence get precedence => Precedence.primary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => value.hashCode;
|
||||||
|
|
||||||
/// Constructs a new variable from the [value].
|
/// Constructs a new variable from the [value].
|
||||||
const Variable(this.value);
|
const Variable(this.value);
|
||||||
|
|
||||||
|
@ -68,6 +71,11 @@ class Variable<T> extends Expression<T> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Variable($value)';
|
String toString() => 'Variable($value)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return other is Variable && other.value == value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An expression that represents the value of a dart object encoded to sql
|
/// An expression that represents the value of a dart object encoded to sql
|
||||||
|
|
|
@ -176,6 +176,26 @@ class Migrator {
|
||||||
return issueCustomQuery(index.createIndexStmt, const []);
|
return issueCustomQuery(index.createIndexStmt, const []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drops a table, trigger or index.
|
||||||
|
Future<void> drop(DatabaseSchemaEntity entity) async {
|
||||||
|
final escapedName = escapeIfNeeded(entity.entityName);
|
||||||
|
|
||||||
|
String kind;
|
||||||
|
|
||||||
|
if (entity is TableInfo) {
|
||||||
|
kind = 'TABLE';
|
||||||
|
} else if (entity is Trigger) {
|
||||||
|
kind = 'TRIGGER';
|
||||||
|
} else if (entity is Index) {
|
||||||
|
kind = 'INDEX';
|
||||||
|
} else {
|
||||||
|
// Entity that can't be dropped.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await issueCustomQuery('DROP $kind IF EXISTS $escapedName;');
|
||||||
|
}
|
||||||
|
|
||||||
/// Deletes the table with the given name. Note that this function does not
|
/// Deletes the table with the given name. Note that this function does not
|
||||||
/// escape the [name] parameter.
|
/// escape the [name] parameter.
|
||||||
Future<void> deleteTable(String name) async {
|
Future<void> deleteTable(String name) async {
|
||||||
|
@ -220,3 +240,36 @@ class OpeningDetails {
|
||||||
/// Used internally by moor when opening a database.
|
/// Used internally by moor when opening a database.
|
||||||
const OpeningDetails(this.versionBefore, this.versionNow);
|
const OpeningDetails(this.versionBefore, this.versionNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extension providing the [destructiveFallback] strategy.
|
||||||
|
extension DestructiveMigrationExtension on GeneratedDatabase {
|
||||||
|
/// Provides a destructive [MigrationStrategy] that will delete and then
|
||||||
|
/// re-create all tables, triggers and indices.
|
||||||
|
///
|
||||||
|
/// To use this behavior, override the `migration` getter in your database:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// @UseMoor(...)
|
||||||
|
/// class MyDatabase extends _$MyDatabase {
|
||||||
|
/// @override
|
||||||
|
/// MigrationStrategy get migration => destructiveFallback;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
MigrationStrategy get destructiveFallback {
|
||||||
|
return MigrationStrategy(
|
||||||
|
onCreate: _defaultOnCreate,
|
||||||
|
onUpgrade: (m, from, to) async {
|
||||||
|
// allSchemaEntities are sorted topologically references between them.
|
||||||
|
// Reverse order for deletion in order to not break anything.
|
||||||
|
final reversedEntities = m._db.allSchemaEntities.toList().reversed;
|
||||||
|
|
||||||
|
for (final entity in reversedEntities) {
|
||||||
|
await m.drop(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-create them now
|
||||||
|
await m.createAll();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -68,9 +68,10 @@ mixin TableInfo<TableDsl extends Table, D extends DataClass> on Table
|
||||||
'evaluated by a database engine.');
|
'evaluated by a database engine.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final context = GenerationContext(SqlTypeSystem.defaultInstance, null);
|
||||||
final rawValues = asColumnMap
|
final rawValues = asColumnMap
|
||||||
.cast<String, Variable>()
|
.cast<String, Variable>()
|
||||||
.map((key, value) => MapEntry(key, value.value));
|
.map((key, value) => MapEntry(key, value.mapToSimpleValue(context)));
|
||||||
|
|
||||||
return map(rawValues);
|
return map(rawValues);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,10 @@ class DeleteStatement<T extends Table, D extends DataClass> extends Query<T, D>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes just this entity. May not be used together with [where].
|
/// Deletes just this entity. May not be used together with [where].
|
||||||
|
///
|
||||||
|
/// Returns the amount of rows that were deleted by this statement directly
|
||||||
|
/// (not including additional rows that might be affected through triggers or
|
||||||
|
/// foreign key constraints).
|
||||||
Future<int> delete(Insertable<D> entity) {
|
Future<int> delete(Insertable<D> entity) {
|
||||||
assert(
|
assert(
|
||||||
whereExpr == null,
|
whereExpr == null,
|
||||||
|
@ -25,6 +29,10 @@ class DeleteStatement<T extends Table, D extends DataClass> extends Query<T, D>
|
||||||
|
|
||||||
/// Deletes all rows matched by the set [where] clause and the optional
|
/// Deletes all rows matched by the set [where] clause and the optional
|
||||||
/// limit.
|
/// limit.
|
||||||
|
///
|
||||||
|
/// Returns the amount of rows that were deleted by this statement directly
|
||||||
|
/// (not including additional rows that might be affected through triggers or
|
||||||
|
/// foreign key constraints).
|
||||||
Future<int> go() async {
|
Future<int> go() async {
|
||||||
final ctx = constructQuery();
|
final ctx = constructQuery();
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,23 @@ abstract class TypeConverter<D, S> {
|
||||||
/// nullable.
|
/// nullable.
|
||||||
D mapToDart(S fromDb);
|
D mapToDart(S fromDb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implementation for an enum to int converter that uses the index of the enum
|
||||||
|
/// as the value stored in the database.
|
||||||
|
class EnumIndexConverter<T> extends TypeConverter<T, int> {
|
||||||
|
/// All values of the enum.
|
||||||
|
final List<T> values;
|
||||||
|
|
||||||
|
/// Constant default constructor.
|
||||||
|
const EnumIndexConverter(this.values);
|
||||||
|
|
||||||
|
@override
|
||||||
|
T mapToDart(int fromDb) {
|
||||||
|
return fromDb == null ? null : values[fromDb];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int mapToSql(T value) {
|
||||||
|
return (value as dynamic)?.index as int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: moor
|
name: moor
|
||||||
description: Moor is a safe and reactive persistence library for Dart applications
|
description: Moor is a safe and reactive persistence library for Dart applications
|
||||||
version: 3.0.2
|
version: 3.1.0
|
||||||
repository: https://github.com/simolus3/moor
|
repository: https://github.com/simolus3/moor
|
||||||
homepage: https://moor.simonbinder.eu/
|
homepage: https://moor.simonbinder.eu/
|
||||||
issue_tracker: https://github.com/simolus3/moor/issues
|
issue_tracker: https://github.com/simolus3/moor/issues
|
||||||
|
|
|
@ -52,7 +52,7 @@ void main() {
|
||||||
'INSERT INTO todos (content) VALUES (?)',
|
'INSERT INTO todos (content) VALUES (?)',
|
||||||
'UPDATE users SET name = ?;',
|
'UPDATE users SET name = ?;',
|
||||||
'UPDATE users SET name = ? WHERE name = ?;',
|
'UPDATE users SET name = ? WHERE name = ?;',
|
||||||
'UPDATE categories SET `desc` = ? WHERE id = ?;',
|
'UPDATE categories SET `desc` = ?, priority = 0 WHERE id = ?;',
|
||||||
'DELETE FROM categories WHERE 1;',
|
'DELETE FROM categories WHERE 1;',
|
||||||
'DELETE FROM todos WHERE id = ?;',
|
'DELETE FROM todos WHERE id = ?;',
|
||||||
],
|
],
|
||||||
|
|
|
@ -11,7 +11,12 @@ class Config extends DataClass implements Insertable<Config> {
|
||||||
final String configKey;
|
final String configKey;
|
||||||
final String configValue;
|
final String configValue;
|
||||||
final SyncType syncState;
|
final SyncType syncState;
|
||||||
Config({@required this.configKey, this.configValue, this.syncState});
|
final SyncType syncStateImplicit;
|
||||||
|
Config(
|
||||||
|
{@required this.configKey,
|
||||||
|
this.configValue,
|
||||||
|
this.syncState,
|
||||||
|
this.syncStateImplicit});
|
||||||
factory Config.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
factory Config.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||||
{String prefix}) {
|
{String prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
|
@ -24,6 +29,9 @@ class Config extends DataClass implements Insertable<Config> {
|
||||||
.mapFromDatabaseResponse(data['${effectivePrefix}config_value']),
|
.mapFromDatabaseResponse(data['${effectivePrefix}config_value']),
|
||||||
syncState: ConfigTable.$converter0.mapToDart(intType
|
syncState: ConfigTable.$converter0.mapToDart(intType
|
||||||
.mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])),
|
.mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])),
|
||||||
|
syncStateImplicit: ConfigTable.$converter1.mapToDart(
|
||||||
|
intType.mapFromDatabaseResponse(
|
||||||
|
data['${effectivePrefix}sync_state_implicit'])),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
@ -39,9 +47,31 @@ class Config extends DataClass implements Insertable<Config> {
|
||||||
final converter = ConfigTable.$converter0;
|
final converter = ConfigTable.$converter0;
|
||||||
map['sync_state'] = Variable<int>(converter.mapToSql(syncState));
|
map['sync_state'] = Variable<int>(converter.mapToSql(syncState));
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || syncStateImplicit != null) {
|
||||||
|
final converter = ConfigTable.$converter1;
|
||||||
|
map['sync_state_implicit'] =
|
||||||
|
Variable<int>(converter.mapToSql(syncStateImplicit));
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return ConfigCompanion(
|
||||||
|
configKey: configKey == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(configKey),
|
||||||
|
configValue: configValue == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(configValue),
|
||||||
|
syncState: syncState == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(syncState),
|
||||||
|
syncStateImplicit: syncStateImplicit == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(syncStateImplicit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory Config.fromJson(Map<String, dynamic> json,
|
factory Config.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -49,6 +79,8 @@ class Config extends DataClass implements Insertable<Config> {
|
||||||
configKey: serializer.fromJson<String>(json['config_key']),
|
configKey: serializer.fromJson<String>(json['config_key']),
|
||||||
configValue: serializer.fromJson<String>(json['config_value']),
|
configValue: serializer.fromJson<String>(json['config_value']),
|
||||||
syncState: serializer.fromJson<SyncType>(json['sync_state']),
|
syncState: serializer.fromJson<SyncType>(json['sync_state']),
|
||||||
|
syncStateImplicit:
|
||||||
|
serializer.fromJson<SyncType>(json['sync_state_implicit']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
factory Config.fromJsonString(String encodedJson,
|
factory Config.fromJsonString(String encodedJson,
|
||||||
|
@ -62,71 +94,88 @@ class Config extends DataClass implements Insertable<Config> {
|
||||||
'config_key': serializer.toJson<String>(configKey),
|
'config_key': serializer.toJson<String>(configKey),
|
||||||
'config_value': serializer.toJson<String>(configValue),
|
'config_value': serializer.toJson<String>(configValue),
|
||||||
'sync_state': serializer.toJson<SyncType>(syncState),
|
'sync_state': serializer.toJson<SyncType>(syncState),
|
||||||
|
'sync_state_implicit': serializer.toJson<SyncType>(syncStateImplicit),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Config copyWith({String configKey, String configValue, SyncType syncState}) =>
|
Config copyWith(
|
||||||
|
{String configKey,
|
||||||
|
String configValue,
|
||||||
|
SyncType syncState,
|
||||||
|
SyncType syncStateImplicit}) =>
|
||||||
Config(
|
Config(
|
||||||
configKey: configKey ?? this.configKey,
|
configKey: configKey ?? this.configKey,
|
||||||
configValue: configValue ?? this.configValue,
|
configValue: configValue ?? this.configValue,
|
||||||
syncState: syncState ?? this.syncState,
|
syncState: syncState ?? this.syncState,
|
||||||
|
syncStateImplicit: syncStateImplicit ?? this.syncStateImplicit,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('Config(')
|
return (StringBuffer('Config(')
|
||||||
..write('configKey: $configKey, ')
|
..write('configKey: $configKey, ')
|
||||||
..write('configValue: $configValue, ')
|
..write('configValue: $configValue, ')
|
||||||
..write('syncState: $syncState')
|
..write('syncState: $syncState, ')
|
||||||
|
..write('syncStateImplicit: $syncStateImplicit')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => $mrjf($mrjc(
|
||||||
configKey.hashCode, $mrjc(configValue.hashCode, syncState.hashCode)));
|
configKey.hashCode,
|
||||||
|
$mrjc(configValue.hashCode,
|
||||||
|
$mrjc(syncState.hashCode, syncStateImplicit.hashCode))));
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) =>
|
bool operator ==(dynamic other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is Config &&
|
(other is Config &&
|
||||||
other.configKey == this.configKey &&
|
other.configKey == this.configKey &&
|
||||||
other.configValue == this.configValue &&
|
other.configValue == this.configValue &&
|
||||||
other.syncState == this.syncState);
|
other.syncState == this.syncState &&
|
||||||
|
other.syncStateImplicit == this.syncStateImplicit);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfigCompanion extends UpdateCompanion<Config> {
|
class ConfigCompanion extends UpdateCompanion<Config> {
|
||||||
final Value<String> configKey;
|
final Value<String> configKey;
|
||||||
final Value<String> configValue;
|
final Value<String> configValue;
|
||||||
final Value<SyncType> syncState;
|
final Value<SyncType> syncState;
|
||||||
|
final Value<SyncType> syncStateImplicit;
|
||||||
const ConfigCompanion({
|
const ConfigCompanion({
|
||||||
this.configKey = const Value.absent(),
|
this.configKey = const Value.absent(),
|
||||||
this.configValue = const Value.absent(),
|
this.configValue = const Value.absent(),
|
||||||
this.syncState = const Value.absent(),
|
this.syncState = const Value.absent(),
|
||||||
|
this.syncStateImplicit = const Value.absent(),
|
||||||
});
|
});
|
||||||
ConfigCompanion.insert({
|
ConfigCompanion.insert({
|
||||||
@required String configKey,
|
@required String configKey,
|
||||||
this.configValue = const Value.absent(),
|
this.configValue = const Value.absent(),
|
||||||
this.syncState = const Value.absent(),
|
this.syncState = const Value.absent(),
|
||||||
|
this.syncStateImplicit = const Value.absent(),
|
||||||
}) : configKey = Value(configKey);
|
}) : configKey = Value(configKey);
|
||||||
static Insertable<Config> custom({
|
static Insertable<Config> custom({
|
||||||
Expression<String> configKey,
|
Expression<String> configKey,
|
||||||
Expression<String> configValue,
|
Expression<String> configValue,
|
||||||
Expression<int> syncState,
|
Expression<int> syncState,
|
||||||
|
Expression<int> syncStateImplicit,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (configKey != null) 'config_key': configKey,
|
if (configKey != null) 'config_key': configKey,
|
||||||
if (configValue != null) 'config_value': configValue,
|
if (configValue != null) 'config_value': configValue,
|
||||||
if (syncState != null) 'sync_state': syncState,
|
if (syncState != null) 'sync_state': syncState,
|
||||||
|
if (syncStateImplicit != null) 'sync_state_implicit': syncStateImplicit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigCompanion copyWith(
|
ConfigCompanion copyWith(
|
||||||
{Value<String> configKey,
|
{Value<String> configKey,
|
||||||
Value<String> configValue,
|
Value<String> configValue,
|
||||||
Value<SyncType> syncState}) {
|
Value<SyncType> syncState,
|
||||||
|
Value<SyncType> syncStateImplicit}) {
|
||||||
return ConfigCompanion(
|
return ConfigCompanion(
|
||||||
configKey: configKey ?? this.configKey,
|
configKey: configKey ?? this.configKey,
|
||||||
configValue: configValue ?? this.configValue,
|
configValue: configValue ?? this.configValue,
|
||||||
syncState: syncState ?? this.syncState,
|
syncState: syncState ?? this.syncState,
|
||||||
|
syncStateImplicit: syncStateImplicit ?? this.syncStateImplicit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +192,11 @@ class ConfigCompanion extends UpdateCompanion<Config> {
|
||||||
final converter = ConfigTable.$converter0;
|
final converter = ConfigTable.$converter0;
|
||||||
map['sync_state'] = Variable<int>(converter.mapToSql(syncState.value));
|
map['sync_state'] = Variable<int>(converter.mapToSql(syncState.value));
|
||||||
}
|
}
|
||||||
|
if (syncStateImplicit.present) {
|
||||||
|
final converter = ConfigTable.$converter1;
|
||||||
|
map['sync_state_implicit'] =
|
||||||
|
Variable<int>(converter.mapToSql(syncStateImplicit.value));
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,8 +231,19 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
|
||||||
$customConstraints: '');
|
$customConstraints: '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _syncStateImplicitMeta =
|
||||||
|
const VerificationMeta('syncStateImplicit');
|
||||||
|
GeneratedIntColumn _syncStateImplicit;
|
||||||
|
GeneratedIntColumn get syncStateImplicit =>
|
||||||
|
_syncStateImplicit ??= _constructSyncStateImplicit();
|
||||||
|
GeneratedIntColumn _constructSyncStateImplicit() {
|
||||||
|
return GeneratedIntColumn('sync_state_implicit', $tableName, true,
|
||||||
|
$customConstraints: '');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [configKey, configValue, syncState];
|
List<GeneratedColumn> get $columns =>
|
||||||
|
[configKey, configValue, syncState, syncStateImplicit];
|
||||||
@override
|
@override
|
||||||
ConfigTable get asDslTable => this;
|
ConfigTable get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -203,6 +268,7 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
|
||||||
data['config_value'], _configValueMeta));
|
data['config_value'], _configValueMeta));
|
||||||
}
|
}
|
||||||
context.handle(_syncStateMeta, const VerificationResult.success());
|
context.handle(_syncStateMeta, const VerificationResult.success());
|
||||||
|
context.handle(_syncStateImplicitMeta, const VerificationResult.success());
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,6 +286,8 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static TypeConverter<SyncType, int> $converter0 = const SyncTypeConverter();
|
static TypeConverter<SyncType, int> $converter0 = const SyncTypeConverter();
|
||||||
|
static TypeConverter<SyncType, int> $converter1 =
|
||||||
|
const EnumIndexConverter<SyncType>(SyncType.values);
|
||||||
@override
|
@override
|
||||||
bool get dontWriteConstraints => true;
|
bool get dontWriteConstraints => true;
|
||||||
}
|
}
|
||||||
|
@ -250,6 +318,13 @@ class WithDefault extends DataClass implements Insertable<WithDefault> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WithDefaultsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return WithDefaultsCompanion(
|
||||||
|
a: a == null && nullToAbsent ? const Value.absent() : Value(a),
|
||||||
|
b: b == null && nullToAbsent ? const Value.absent() : Value(b),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory WithDefault.fromJson(Map<String, dynamic> json,
|
factory WithDefault.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -415,6 +490,14 @@ class NoId extends DataClass implements Insertable<NoId> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NoIdsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return NoIdsCompanion(
|
||||||
|
payload: payload == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory NoId.fromJson(Map<String, dynamic> json,
|
factory NoId.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -569,6 +652,14 @@ class WithConstraint extends DataClass implements Insertable<WithConstraint> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WithConstraintsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return WithConstraintsCompanion(
|
||||||
|
a: a == null && nullToAbsent ? const Value.absent() : Value(a),
|
||||||
|
b: b == null && nullToAbsent ? const Value.absent() : Value(b),
|
||||||
|
c: c == null && nullToAbsent ? const Value.absent() : Value(c),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory WithConstraint.fromJson(Map<String, dynamic> json,
|
factory WithConstraint.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -786,6 +877,22 @@ class MytableData extends DataClass implements Insertable<MytableData> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MytableCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return MytableCompanion(
|
||||||
|
someid:
|
||||||
|
someid == null && nullToAbsent ? const Value.absent() : Value(someid),
|
||||||
|
sometext: sometext == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(sometext),
|
||||||
|
somebool: somebool == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(somebool),
|
||||||
|
somedate: somedate == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(somedate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory MytableData.fromJson(Map<String, dynamic> json,
|
factory MytableData.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -1024,6 +1131,16 @@ class EMail extends DataClass implements Insertable<EMail> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmailCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return EmailCompanion(
|
||||||
|
sender:
|
||||||
|
sender == null && nullToAbsent ? const Value.absent() : Value(sender),
|
||||||
|
title:
|
||||||
|
title == null && nullToAbsent ? const Value.absent() : Value(title),
|
||||||
|
body: body == null && nullToAbsent ? const Value.absent() : Value(body),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory EMail.fromJson(Map<String, dynamic> json,
|
factory EMail.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -1237,6 +1354,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
configKey: row.readString('config_key'),
|
configKey: row.readString('config_key'),
|
||||||
configValue: row.readString('config_value'),
|
configValue: row.readString('config_value'),
|
||||||
syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')),
|
syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')),
|
||||||
|
syncStateImplicit:
|
||||||
|
ConfigTable.$converter1.mapToDart(row.readInt('sync_state_implicit')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1321,6 +1440,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
configKey: row.readString('config_key'),
|
configKey: row.readString('config_key'),
|
||||||
configValue: row.readString('config_value'),
|
configValue: row.readString('config_value'),
|
||||||
syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')),
|
syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')),
|
||||||
|
syncStateImplicit:
|
||||||
|
ConfigTable.$converter1.mapToDart(row.readInt('sync_state_implicit')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1418,17 +1539,21 @@ class ReadRowIdResult {
|
||||||
final String configKey;
|
final String configKey;
|
||||||
final String configValue;
|
final String configValue;
|
||||||
final SyncType syncState;
|
final SyncType syncState;
|
||||||
|
final SyncType syncStateImplicit;
|
||||||
ReadRowIdResult({
|
ReadRowIdResult({
|
||||||
this.rowid,
|
this.rowid,
|
||||||
this.configKey,
|
this.configKey,
|
||||||
this.configValue,
|
this.configValue,
|
||||||
this.syncState,
|
this.syncState,
|
||||||
|
this.syncStateImplicit,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => $mrjf($mrjc(
|
||||||
rowid.hashCode,
|
rowid.hashCode,
|
||||||
$mrjc(configKey.hashCode,
|
$mrjc(
|
||||||
$mrjc(configValue.hashCode, syncState.hashCode))));
|
configKey.hashCode,
|
||||||
|
$mrjc(configValue.hashCode,
|
||||||
|
$mrjc(syncState.hashCode, syncStateImplicit.hashCode)))));
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) =>
|
bool operator ==(dynamic other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
@ -1436,5 +1561,6 @@ class ReadRowIdResult {
|
||||||
other.rowid == this.rowid &&
|
other.rowid == this.rowid &&
|
||||||
other.configKey == this.configKey &&
|
other.configKey == this.configKey &&
|
||||||
other.configValue == this.configValue &&
|
other.configValue == this.configValue &&
|
||||||
other.syncState == this.syncState);
|
other.syncState == this.syncState &&
|
||||||
|
other.syncStateImplicit == this.syncStateImplicit);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,8 @@ CREATE TABLE with_constraints (
|
||||||
create table config (
|
create table config (
|
||||||
config_key TEXT not null primary key,
|
config_key TEXT not null primary key,
|
||||||
config_value TEXT,
|
config_value TEXT,
|
||||||
sync_state INTEGER MAPPED BY `const SyncTypeConverter()`
|
sync_state INTEGER MAPPED BY `const SyncTypeConverter()`,
|
||||||
|
sync_state_implicit ENUM(SyncType)
|
||||||
) AS "Config";
|
) AS "Config";
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS value_idx ON config (config_value);
|
CREATE INDEX IF NOT EXISTS value_idx ON config (config_value);
|
||||||
|
|
|
@ -33,8 +33,12 @@ class Users extends Table with AutoIncrement {
|
||||||
class Categories extends Table with AutoIncrement {
|
class Categories extends Table with AutoIncrement {
|
||||||
TextColumn get description =>
|
TextColumn get description =>
|
||||||
text().named('desc').customConstraint('NOT NULL UNIQUE')();
|
text().named('desc').customConstraint('NOT NULL UNIQUE')();
|
||||||
|
IntColumn get priority =>
|
||||||
|
intEnum<CategoryPriority>().withDefault(const Constant(0))();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CategoryPriority { low, medium, high }
|
||||||
|
|
||||||
class SharedTodos extends Table {
|
class SharedTodos extends Table {
|
||||||
IntColumn get todo => integer()();
|
IntColumn get todo => integer()();
|
||||||
IntColumn get user => integer()();
|
IntColumn get user => integer()();
|
||||||
|
|
|
@ -58,6 +58,23 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TodosTableCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return TodosTableCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
title:
|
||||||
|
title == null && nullToAbsent ? const Value.absent() : Value(title),
|
||||||
|
content: content == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(content),
|
||||||
|
targetDate: targetDate == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(targetDate),
|
||||||
|
category: category == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(category),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory TodoEntry.fromJson(Map<String, dynamic> json,
|
factory TodoEntry.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -319,7 +336,9 @@ class $TodosTableTable extends TodosTable
|
||||||
class Category extends DataClass implements Insertable<Category> {
|
class Category extends DataClass implements Insertable<Category> {
|
||||||
final int id;
|
final int id;
|
||||||
final String description;
|
final String description;
|
||||||
Category({@required this.id, @required this.description});
|
final CategoryPriority priority;
|
||||||
|
Category(
|
||||||
|
{@required this.id, @required this.description, @required this.priority});
|
||||||
factory Category.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
factory Category.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||||
{String prefix}) {
|
{String prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
|
@ -329,6 +348,8 @@ class Category extends DataClass implements Insertable<Category> {
|
||||||
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
||||||
description:
|
description:
|
||||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}desc']),
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}desc']),
|
||||||
|
priority: $CategoriesTable.$converter0.mapToDart(
|
||||||
|
intType.mapFromDatabaseResponse(data['${effectivePrefix}priority'])),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
@ -340,15 +361,32 @@ class Category extends DataClass implements Insertable<Category> {
|
||||||
if (!nullToAbsent || description != null) {
|
if (!nullToAbsent || description != null) {
|
||||||
map['desc'] = Variable<String>(description);
|
map['desc'] = Variable<String>(description);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || priority != null) {
|
||||||
|
final converter = $CategoriesTable.$converter0;
|
||||||
|
map['priority'] = Variable<int>(converter.mapToSql(priority));
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CategoriesCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return CategoriesCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
description: description == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(description),
|
||||||
|
priority: priority == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(priority),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory Category.fromJson(Map<String, dynamic> json,
|
factory Category.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
return Category(
|
return Category(
|
||||||
id: serializer.fromJson<int>(json['id']),
|
id: serializer.fromJson<int>(json['id']),
|
||||||
description: serializer.fromJson<String>(json['description']),
|
description: serializer.fromJson<String>(json['description']),
|
||||||
|
priority: serializer.fromJson<CategoryPriority>(json['priority']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
factory Category.fromJsonString(String encodedJson,
|
factory Category.fromJsonString(String encodedJson,
|
||||||
|
@ -362,57 +400,72 @@ class Category extends DataClass implements Insertable<Category> {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<int>(id),
|
'id': serializer.toJson<int>(id),
|
||||||
'description': serializer.toJson<String>(description),
|
'description': serializer.toJson<String>(description),
|
||||||
|
'priority': serializer.toJson<CategoryPriority>(priority),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Category copyWith({int id, String description}) => Category(
|
Category copyWith({int id, String description, CategoryPriority priority}) =>
|
||||||
|
Category(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
|
priority: priority ?? this.priority,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('Category(')
|
return (StringBuffer('Category(')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('description: $description')
|
..write('description: $description, ')
|
||||||
|
..write('priority: $priority')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(id.hashCode, description.hashCode));
|
int get hashCode =>
|
||||||
|
$mrjf($mrjc(id.hashCode, $mrjc(description.hashCode, priority.hashCode)));
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) =>
|
bool operator ==(dynamic other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is Category &&
|
(other is Category &&
|
||||||
other.id == this.id &&
|
other.id == this.id &&
|
||||||
other.description == this.description);
|
other.description == this.description &&
|
||||||
|
other.priority == this.priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoriesCompanion extends UpdateCompanion<Category> {
|
class CategoriesCompanion extends UpdateCompanion<Category> {
|
||||||
final Value<int> id;
|
final Value<int> id;
|
||||||
final Value<String> description;
|
final Value<String> description;
|
||||||
|
final Value<CategoryPriority> priority;
|
||||||
const CategoriesCompanion({
|
const CategoriesCompanion({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
this.description = const Value.absent(),
|
this.description = const Value.absent(),
|
||||||
|
this.priority = const Value.absent(),
|
||||||
});
|
});
|
||||||
CategoriesCompanion.insert({
|
CategoriesCompanion.insert({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
@required String description,
|
@required String description,
|
||||||
|
this.priority = const Value.absent(),
|
||||||
}) : description = Value(description);
|
}) : description = Value(description);
|
||||||
static Insertable<Category> custom({
|
static Insertable<Category> custom({
|
||||||
Expression<int> id,
|
Expression<int> id,
|
||||||
Expression<String> description,
|
Expression<String> description,
|
||||||
|
Expression<int> priority,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
if (description != null) 'desc': description,
|
if (description != null) 'desc': description,
|
||||||
|
if (priority != null) 'priority': priority,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoriesCompanion copyWith({Value<int> id, Value<String> description}) {
|
CategoriesCompanion copyWith(
|
||||||
|
{Value<int> id,
|
||||||
|
Value<String> description,
|
||||||
|
Value<CategoryPriority> priority}) {
|
||||||
return CategoriesCompanion(
|
return CategoriesCompanion(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
|
priority: priority ?? this.priority,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +478,10 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
|
||||||
if (description.present) {
|
if (description.present) {
|
||||||
map['desc'] = Variable<String>(description.value);
|
map['desc'] = Variable<String>(description.value);
|
||||||
}
|
}
|
||||||
|
if (priority.present) {
|
||||||
|
final converter = $CategoriesTable.$converter0;
|
||||||
|
map['priority'] = Variable<int>(converter.mapToSql(priority.value));
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -454,8 +511,17 @@ class $CategoriesTable extends Categories
|
||||||
$customConstraints: 'NOT NULL UNIQUE');
|
$customConstraints: 'NOT NULL UNIQUE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _priorityMeta = const VerificationMeta('priority');
|
||||||
|
GeneratedIntColumn _priority;
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [id, description];
|
GeneratedIntColumn get priority => _priority ??= _constructPriority();
|
||||||
|
GeneratedIntColumn _constructPriority() {
|
||||||
|
return GeneratedIntColumn('priority', $tableName, false,
|
||||||
|
defaultValue: const Constant(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [id, description, priority];
|
||||||
@override
|
@override
|
||||||
$CategoriesTable get asDslTable => this;
|
$CategoriesTable get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -476,6 +542,7 @@ class $CategoriesTable extends Categories
|
||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_descriptionMeta);
|
context.missing(_descriptionMeta);
|
||||||
}
|
}
|
||||||
|
context.handle(_priorityMeta, const VerificationResult.success());
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,6 +558,9 @@ class $CategoriesTable extends Categories
|
||||||
$CategoriesTable createAlias(String alias) {
|
$CategoriesTable createAlias(String alias) {
|
||||||
return $CategoriesTable(_db, alias);
|
return $CategoriesTable(_db, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static TypeConverter<CategoryPriority, int> $converter0 =
|
||||||
|
const EnumIndexConverter<CategoryPriority>(CategoryPriority.values);
|
||||||
}
|
}
|
||||||
|
|
||||||
class User extends DataClass implements Insertable<User> {
|
class User extends DataClass implements Insertable<User> {
|
||||||
|
@ -545,6 +615,22 @@ class User extends DataClass implements Insertable<User> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UsersCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return UsersCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
name: name == null && nullToAbsent ? const Value.absent() : Value(name),
|
||||||
|
isAwesome: isAwesome == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(isAwesome),
|
||||||
|
profilePicture: profilePicture == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(profilePicture),
|
||||||
|
creationTime: creationTime == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(creationTime),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory User.fromJson(Map<String, dynamic> json,
|
factory User.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -828,6 +914,13 @@ class SharedTodo extends DataClass implements Insertable<SharedTodo> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SharedTodosCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return SharedTodosCompanion(
|
||||||
|
todo: todo == null && nullToAbsent ? const Value.absent() : Value(todo),
|
||||||
|
user: user == null && nullToAbsent ? const Value.absent() : Value(user),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory SharedTodo.fromJson(Map<String, dynamic> json,
|
factory SharedTodo.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -1027,6 +1120,19 @@ class TableWithoutPKData extends DataClass
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TableWithoutPKCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return TableWithoutPKCompanion(
|
||||||
|
notReallyAnId: notReallyAnId == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(notReallyAnId),
|
||||||
|
someFloat: someFloat == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(someFloat),
|
||||||
|
custom:
|
||||||
|
custom == null && nullToAbsent ? const Value.absent() : Value(custom),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory TableWithoutPKData.fromJson(Map<String, dynamic> json,
|
factory TableWithoutPKData.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
@ -1252,6 +1358,13 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PureDefaultsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return PureDefaultsCompanion(
|
||||||
|
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||||
|
txt: txt == null && nullToAbsent ? const Value.absent() : Value(txt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory PureDefault.fromJson(Map<String, dynamic> json,
|
factory PureDefault.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer serializer}) {
|
{ValueSerializer serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
|
|
@ -71,6 +71,45 @@ void main() {
|
||||||
expect(recovered.isAwesome, user.isAwesome);
|
expect(recovered.isAwesome, user.isAwesome);
|
||||||
expect(recovered.profilePicture, user.profilePicture);
|
expect(recovered.profilePicture, user.profilePicture);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generated data classes can be converted to companions', () {
|
||||||
|
final entry = Category(
|
||||||
|
id: 3,
|
||||||
|
description: 'description',
|
||||||
|
priority: CategoryPriority.low,
|
||||||
|
);
|
||||||
|
final companion = entry.toCompanion(false);
|
||||||
|
|
||||||
|
expect(companion.runtimeType, CategoriesCompanion);
|
||||||
|
expect(
|
||||||
|
companion,
|
||||||
|
equals(CategoriesCompanion.insert(
|
||||||
|
description: 'description',
|
||||||
|
id: const Value(3),
|
||||||
|
priority: const Value(CategoryPriority.low),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('data classes can be converted to companions with null to absent', () {
|
||||||
|
final entry = PureDefault(id: null, txt: null);
|
||||||
|
|
||||||
|
expect(entry.toCompanion(false),
|
||||||
|
const PureDefaultsCompanion(id: Value(null), txt: Value(null)));
|
||||||
|
expect(entry.toCompanion(true), const PureDefaultsCompanion());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('companions support hash and equals', () {
|
||||||
|
const first = CategoriesCompanion(description: Value('foo'));
|
||||||
|
final equalToFirst = CategoriesCompanion.insert(description: 'foo');
|
||||||
|
const different = CategoriesCompanion(description: Value('bar'));
|
||||||
|
|
||||||
|
expect(first.hashCode, equalToFirst.hashCode);
|
||||||
|
expect(first, equals(equalToFirst));
|
||||||
|
|
||||||
|
expect(first, isNot(equals(different)));
|
||||||
|
expect(first, equals(first));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MySerializer extends ValueSerializer {
|
class _MySerializer extends ValueSerializer {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
@TestOn('vm')
|
||||||
import 'package:moor/extensions/moor_ffi.dart';
|
import 'package:moor/extensions/moor_ffi.dart';
|
||||||
import 'package:moor/src/runtime/query_builder/query_builder.dart';
|
import 'package:moor/src/runtime/query_builder/query_builder.dart';
|
||||||
|
import 'package:moor_ffi/moor_ffi.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../data/tables/todos.dart';
|
||||||
import '../data/utils/expect_generated.dart';
|
import '../data/utils/expect_generated.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
@ -19,4 +22,38 @@ void main() {
|
||||||
test('asin', () => expect(sqlAsin(a), generates('asin(a)')));
|
test('asin', () => expect(sqlAsin(a), generates('asin(a)')));
|
||||||
test('acos', () => expect(sqlAcos(a), generates('acos(a)')));
|
test('acos', () => expect(sqlAcos(a), generates('acos(a)')));
|
||||||
test('atan', () => expect(sqlAtan(a), generates('atan(a)')));
|
test('atan', () => expect(sqlAtan(a), generates('atan(a)')));
|
||||||
|
|
||||||
|
test('containsCase', () {
|
||||||
|
final c = GeneratedTextColumn('a', null, false);
|
||||||
|
|
||||||
|
expect(c.containsCase('foo'), generates('moor_contains(a, ?, 0)', ['foo']));
|
||||||
|
expect(
|
||||||
|
c.containsCase('foo', caseSensitive: true),
|
||||||
|
generates('moor_contains(a, ?, 1)', ['foo']),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('containsCase integration test', () async {
|
||||||
|
final db = TodoDb(VmDatabase.memory());
|
||||||
|
// insert exactly one row so that we can evaluate expressions from Dart
|
||||||
|
await db.into(db.pureDefaults).insert(PureDefaultsCompanion.insert());
|
||||||
|
|
||||||
|
Future<bool> evaluate(Expression<bool> expr) async {
|
||||||
|
final result = await (db.selectOnly(db.pureDefaults)..addColumns([expr]))
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
|
return result.read(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluate(const Variable('Häuser').containsCase('Ä')),
|
||||||
|
completion(isTrue),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluate(const Variable('Dart is cool')
|
||||||
|
.containsCase('dart', caseSensitive: false)),
|
||||||
|
completion(isTrue),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,4 +218,16 @@ void main() {
|
||||||
));
|
));
|
||||||
expect(id, 3);
|
expect(id, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('applies implicit type converter', () async {
|
||||||
|
await db.into(db.categories).insert(CategoriesCompanion.insert(
|
||||||
|
description: 'description',
|
||||||
|
priority: const Value(CategoryPriority.medium),
|
||||||
|
));
|
||||||
|
|
||||||
|
verify(executor.runInsert(
|
||||||
|
'INSERT INTO categories (`desc`, priority) VALUES (?, ?)',
|
||||||
|
['description', 1],
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,8 @@ void main() {
|
||||||
verify(executor.runSelect(
|
verify(executor.runSelect(
|
||||||
'SELECT t.id AS "t.id", t.title AS "t.title", '
|
'SELECT t.id AS "t.id", t.title AS "t.title", '
|
||||||
't.content AS "t.content", t.target_date AS "t.target_date", '
|
't.content AS "t.content", t.target_date AS "t.target_date", '
|
||||||
't.category AS "t.category", c.id AS "c.id", c.`desc` AS "c.desc" '
|
't.category AS "t.category", c.id AS "c.id", c.`desc` AS "c.desc", '
|
||||||
|
'c.priority AS "c.priority" '
|
||||||
'FROM todos t LEFT OUTER JOIN categories c ON c.id = t.category;',
|
'FROM todos t LEFT OUTER JOIN categories c ON c.id = t.category;',
|
||||||
argThat(isEmpty)));
|
argThat(isEmpty)));
|
||||||
});
|
});
|
||||||
|
@ -43,6 +44,7 @@ void main() {
|
||||||
't.category': 3,
|
't.category': 3,
|
||||||
'c.id': 3,
|
'c.id': 3,
|
||||||
'c.desc': 'description',
|
'c.desc': 'description',
|
||||||
|
'c.priority': 2,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -65,7 +67,13 @@ void main() {
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
row.readTable(categories), Category(id: 3, description: 'description'));
|
row.readTable(categories),
|
||||||
|
Category(
|
||||||
|
id: 3,
|
||||||
|
description: 'description',
|
||||||
|
priority: CategoryPriority.high,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
verify(executor.runSelect(argThat(contains('DISTINCT')), any));
|
verify(executor.runSelect(argThat(contains('DISTINCT')), any));
|
||||||
});
|
});
|
||||||
|
@ -167,20 +175,29 @@ void main() {
|
||||||
|
|
||||||
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
||||||
return [
|
return [
|
||||||
{'c.id': 3, 'c.desc': 'Description', 'c2': 11}
|
{'c.id': 3, 'c.desc': 'Description', 'c.priority': 1, 'c3': 11}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
|
|
||||||
verify(executor.runSelect(
|
verify(executor.runSelect(
|
||||||
'SELECT c.id AS "c.id", c.`desc` AS "c.desc", LENGTH(c.`desc`) AS "c2" '
|
'SELECT c.id AS "c.id", c.`desc` AS "c.desc", c.priority AS "c.priority"'
|
||||||
|
', LENGTH(c.`desc`) AS "c3" '
|
||||||
'FROM categories c;',
|
'FROM categories c;',
|
||||||
[],
|
[],
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(result.readTable(categories),
|
expect(
|
||||||
equals(Category(id: 3, description: 'Description')));
|
result.readTable(categories),
|
||||||
|
equals(
|
||||||
|
Category(
|
||||||
|
id: 3,
|
||||||
|
description: 'Description',
|
||||||
|
priority: CategoryPriority.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(result.read(descriptionLength), 11);
|
expect(result.read(descriptionLength), 11);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -205,20 +222,28 @@ void main() {
|
||||||
|
|
||||||
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
||||||
return [
|
return [
|
||||||
{'c.id': 3, 'c.desc': 'desc', 'c2': 10}
|
{'c.id': 3, 'c.desc': 'desc', 'c.priority': 0, 'c3': 10}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
|
|
||||||
verify(executor.runSelect(
|
verify(executor.runSelect(
|
||||||
'SELECT c.id AS "c.id", c.`desc` AS "c.desc", COUNT(t.id) AS "c2" '
|
'SELECT c.id AS "c.id", c.`desc` AS "c.desc", '
|
||||||
|
'c.priority AS "c.priority", COUNT(t.id) AS "c3" '
|
||||||
'FROM categories c INNER JOIN todos t ON t.category = c.id '
|
'FROM categories c INNER JOIN todos t ON t.category = c.id '
|
||||||
'GROUP BY c.id HAVING COUNT(t.id) >= ?;',
|
'GROUP BY c.id HAVING COUNT(t.id) >= ?;',
|
||||||
[10]));
|
[10]));
|
||||||
|
|
||||||
expect(result.readTable(todos), isNull);
|
expect(result.readTable(todos), isNull);
|
||||||
expect(result.readTable(categories), Category(id: 3, description: 'desc'));
|
expect(
|
||||||
|
result.readTable(categories),
|
||||||
|
Category(
|
||||||
|
id: 3,
|
||||||
|
description: 'desc',
|
||||||
|
priority: CategoryPriority.low,
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(result.read(amountOfTodos), 10);
|
expect(result.read(amountOfTodos), 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@ const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS with_constraints ('
|
||||||
const _createConfig = 'CREATE TABLE IF NOT EXISTS config ('
|
const _createConfig = 'CREATE TABLE IF NOT EXISTS config ('
|
||||||
'config_key VARCHAR not null primary key, '
|
'config_key VARCHAR not null primary key, '
|
||||||
'config_value VARCHAR, '
|
'config_value VARCHAR, '
|
||||||
'sync_state INTEGER);';
|
'sync_state INTEGER, '
|
||||||
|
'sync_state_implicit INTEGER);';
|
||||||
|
|
||||||
const _createMyTable = 'CREATE TABLE IF NOT EXISTS mytable ('
|
const _createMyTable = 'CREATE TABLE IF NOT EXISTS mytable ('
|
||||||
'someid INTEGER NOT NULL PRIMARY KEY, '
|
'someid INTEGER NOT NULL PRIMARY KEY, '
|
||||||
|
|
|
@ -28,7 +28,8 @@ void main() {
|
||||||
verify(mockExecutor.runCustom(
|
verify(mockExecutor.runCustom(
|
||||||
'CREATE TABLE IF NOT EXISTS categories '
|
'CREATE TABLE IF NOT EXISTS categories '
|
||||||
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
|
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
|
||||||
'`desc` VARCHAR NOT NULL UNIQUE);',
|
'`desc` VARCHAR NOT NULL UNIQUE, '
|
||||||
|
'priority INTEGER NOT NULL DEFAULT 0);',
|
||||||
[]));
|
[]));
|
||||||
|
|
||||||
verify(mockExecutor.runCustom(
|
verify(mockExecutor.runCustom(
|
||||||
|
@ -81,6 +82,18 @@ void main() {
|
||||||
verify(mockExecutor.runCustom('DROP TABLE IF EXISTS users;'));
|
verify(mockExecutor.runCustom('DROP TABLE IF EXISTS users;'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('drops indices', () async {
|
||||||
|
await db.createMigrator().drop(Index('desc', 'foo'));
|
||||||
|
|
||||||
|
verify(mockExecutor.runCustom('DROP INDEX IF EXISTS `desc`;'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drops triggers', () async {
|
||||||
|
await db.createMigrator().drop(Trigger('foo', 'my_trigger'));
|
||||||
|
|
||||||
|
verify(mockExecutor.runCustom('DROP TRIGGER IF EXISTS my_trigger;'));
|
||||||
|
});
|
||||||
|
|
||||||
test('adds columns', () async {
|
test('adds columns', () async {
|
||||||
await db.createMigrator().addColumn(db.users, db.users.isAwesome);
|
await db.createMigrator().addColumn(db.users, db.users.isAwesome);
|
||||||
|
|
||||||
|
|
|
@ -156,4 +156,27 @@ void main() {
|
||||||
..markTablesUpdated({db.todosTable});
|
..markTablesUpdated({db.todosTable});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('applies implicit type converter', () async {
|
||||||
|
when(executor.runSelect(any, any)).thenAnswer((_) {
|
||||||
|
return Future.value([
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'desc': 'description',
|
||||||
|
'priority': 2,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
final category = await db.select(db.categories).getSingle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
category,
|
||||||
|
Category(
|
||||||
|
id: 1,
|
||||||
|
description: 'description',
|
||||||
|
priority: CategoryPriority.high,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,8 @@ void main() {
|
||||||
id: Value(3),
|
id: Value(3),
|
||||||
name: Value('hi'),
|
name: Value('hi'),
|
||||||
profilePicture: Value.absent(),
|
profilePicture: Value.absent(),
|
||||||
isAwesome: Value(true),
|
// false for https://github.com/simolus3/moor/issues/559
|
||||||
|
isAwesome: Value(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
final user = db.users.mapFromCompanion(companion);
|
final user = db.users.mapFromCompanion(companion);
|
||||||
|
@ -49,7 +50,7 @@ void main() {
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'hi',
|
name: 'hi',
|
||||||
profilePicture: null,
|
profilePicture: null,
|
||||||
isAwesome: true,
|
isAwesome: false,
|
||||||
creationTime: null,
|
creationTime: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
- Added `moor_contains` sql function to support case-sensitive contains
|
||||||
|
- Workaround for `dlopen` issues on some Android devices.
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
- Provide mathematical functions in sql (`pow`, `power`, `sin`, `cos`, `tan`, `asin`, `atan`, `acos`, `sqrt`)
|
- Provide mathematical functions in sql (`pow`, `power`, `sin`, `cos`, `tan`, `asin`, `atan`, `acos`, `sqrt`)
|
||||||
|
|
|
@ -93,6 +93,33 @@ void _regexpImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||||
ctx.resultBool(regex.hasMatch(secondParam as String));
|
ctx.resultBool(regex.hasMatch(secondParam as String));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _containsImpl(Pointer<FunctionContext> ctx, int argCount,
|
||||||
|
Pointer<Pointer<SqliteValue>> args) {
|
||||||
|
if (argCount < 2 || argCount > 3) {
|
||||||
|
ctx.resultError('Expected 2 or 3 arguments to moor_contains');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final first = args[0].value;
|
||||||
|
final second = args[1].value;
|
||||||
|
|
||||||
|
if (first is! String || second is! String) {
|
||||||
|
ctx.resultError('First two args must be strings');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final caseSensitive = argCount == 3 && args[2].value == 1;
|
||||||
|
|
||||||
|
final firstAsString = first as String;
|
||||||
|
final secondAsString = second as String;
|
||||||
|
|
||||||
|
final result = caseSensitive
|
||||||
|
? firstAsString.contains(secondAsString)
|
||||||
|
: firstAsString.toLowerCase().contains(secondAsString.toLowerCase());
|
||||||
|
|
||||||
|
ctx.resultInt(result ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
void _registerOn(Database db) {
|
void _registerOn(Database db) {
|
||||||
final powImplPointer =
|
final powImplPointer =
|
||||||
Pointer.fromFunction<sqlite3_function_handler>(_powImpl);
|
Pointer.fromFunction<sqlite3_function_handler>(_powImpl);
|
||||||
|
@ -117,4 +144,11 @@ void _registerOn(Database db) {
|
||||||
|
|
||||||
db.createFunction('regexp', 2, Pointer.fromFunction(_regexpImpl),
|
db.createFunction('regexp', 2, Pointer.fromFunction(_regexpImpl),
|
||||||
isDeterministic: true);
|
isDeterministic: true);
|
||||||
|
|
||||||
|
final containsImplPointer =
|
||||||
|
Pointer.fromFunction<sqlite3_function_handler>(_containsImpl);
|
||||||
|
db.createFunction('moor_contains', 2, containsImplPointer,
|
||||||
|
isDeterministic: true);
|
||||||
|
db.createFunction('moor_contains', 3, containsImplPointer,
|
||||||
|
isDeterministic: true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
@ -23,7 +24,25 @@ final OpenDynamicLibrary open = OpenDynamicLibrary._();
|
||||||
|
|
||||||
DynamicLibrary _defaultOpen() {
|
DynamicLibrary _defaultOpen() {
|
||||||
if (Platform.isLinux || Platform.isAndroid) {
|
if (Platform.isLinux || Platform.isAndroid) {
|
||||||
return DynamicLibrary.open('libsqlite3.so');
|
try {
|
||||||
|
return DynamicLibrary.open('libsqlite3.so');
|
||||||
|
} catch (_) {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
// On some (especially old) Android devices, we somehow can't dlopen
|
||||||
|
// libraries shipped with the apk. We need to find the full path of the
|
||||||
|
// library (/data/data/<id>/lib/libsqlite3.so) and open that one.
|
||||||
|
// For details, see https://github.com/simolus3/moor/issues/420
|
||||||
|
final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync();
|
||||||
|
|
||||||
|
// app id ends with the first \0 character in here.
|
||||||
|
final endOfAppId = max(appIdAsBytes.indexOf(0), 0);
|
||||||
|
final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId));
|
||||||
|
|
||||||
|
return DynamicLibrary.open('/data/data/$appId/lib/libsqlite3.so');
|
||||||
|
}
|
||||||
|
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Platform.isMacOS || Platform.isIOS) {
|
if (Platform.isMacOS || Platform.isIOS) {
|
||||||
// todo: Consider including sqlite3 in the build and use DynamicLibrary.
|
// todo: Consider including sqlite3 in the build and use DynamicLibrary.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: moor_ffi
|
name: moor_ffi
|
||||||
description: "Provides sqlite bindings using dart:ffi, including a moor executor"
|
description: "Provides sqlite bindings using dart:ffi, including a moor executor"
|
||||||
version: 0.5.0
|
version: 0.6.0
|
||||||
homepage: https://github.com/simolus3/moor/tree/develop/moor_ffi
|
homepage: https://github.com/simolus3/moor/tree/develop/moor_ffi
|
||||||
issue_tracker: https://github.com/simolus3/moor/issues
|
issue_tracker: https://github.com/simolus3/moor/issues
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,17 @@ void main() {
|
||||||
setUp(() => db = Database.memory()..enableMoorFfiFunctions());
|
setUp(() => db = Database.memory()..enableMoorFfiFunctions());
|
||||||
tearDown(() => db.close());
|
tearDown(() => db.close());
|
||||||
|
|
||||||
|
dynamic selectSingle(String expression) {
|
||||||
|
final stmt = db.prepare('SELECT $expression AS r;');
|
||||||
|
final rows = stmt.select();
|
||||||
|
stmt.close();
|
||||||
|
|
||||||
|
return rows.single['r'];
|
||||||
|
}
|
||||||
|
|
||||||
group('pow', () {
|
group('pow', () {
|
||||||
dynamic _resultOfPow(String a, String b) {
|
dynamic _resultOfPow(String a, String b) {
|
||||||
final stmt = db.prepare('SELECT pow($a, $b) AS r;');
|
return selectSingle('pow($a, $b)');
|
||||||
final rows = stmt.select();
|
|
||||||
stmt.close();
|
|
||||||
|
|
||||||
return rows.single['r'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test('returns null when any argument is null', () {
|
test('returns null when any argument is null', () {
|
||||||
|
@ -105,6 +109,26 @@ void main() {
|
||||||
expect(result.single['r'], 0);
|
expect(result.single['r'], 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('moor_contains', () {
|
||||||
|
test('checks for type errors', () {
|
||||||
|
expect(() => db.execute('SELECT moor_contains(12, 1);'),
|
||||||
|
throwsA(isA<SqliteException>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('case insensitive without parameter', () {
|
||||||
|
expect(selectSingle("moor_contains('foo', 'O')"), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('case insensitive with parameter', () {
|
||||||
|
expect(selectSingle("moor_contains('foo', 'O', 0)"), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('case sensitive', () {
|
||||||
|
expect(selectSingle("moor_contains('Hello', 'hell', 1)"), 0);
|
||||||
|
expect(selectSingle("moor_contains('hi', 'i', 1)"), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils to verify the sql functions behave exactly like the ones from the VM
|
// utils to verify the sql functions behave exactly like the ones from the VM
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
## 3.1.0
|
||||||
|
|
||||||
|
- Respect foreign key constraints when calculating the stream update graph
|
||||||
|
|
||||||
## 3.0.0
|
## 3.0.0
|
||||||
|
|
||||||
Generate code for moor 3.0. This most notably includes custom companions and nested result sets.
|
Generate code for moor 3.0. This most notably includes custom companions and nested result sets.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
part of 'parser.dart';
|
part of 'parser.dart';
|
||||||
|
|
||||||
const String startInt = 'integer';
|
const String startInt = 'integer';
|
||||||
|
const String startEnum = 'intEnum';
|
||||||
const String startString = 'text';
|
const String startString = 'text';
|
||||||
const String startBool = 'boolean';
|
const String startBool = 'boolean';
|
||||||
const String startDateTime = 'dateTime';
|
const String startDateTime = 'dateTime';
|
||||||
|
@ -9,6 +10,7 @@ const String startReal = 'real';
|
||||||
|
|
||||||
const Set<String> starters = {
|
const Set<String> starters = {
|
||||||
startInt,
|
startInt,
|
||||||
|
startEnum,
|
||||||
startString,
|
startString,
|
||||||
startBool,
|
startBool,
|
||||||
startDateTime,
|
startDateTime,
|
||||||
|
@ -185,6 +187,28 @@ class ColumnParser {
|
||||||
sqlType: columnType);
|
sqlType: columnType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (foundStartMethod == startEnum) {
|
||||||
|
if (converter != null) {
|
||||||
|
base.step.reportError(ErrorInDartCode(
|
||||||
|
message: 'Using $startEnum will apply a custom converter by default, '
|
||||||
|
"so you can't add an additional converter",
|
||||||
|
affectedElement: getter.declaredElement,
|
||||||
|
severity: Severity.warning,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
final enumType = remainingExpr.typeArgumentTypes[0];
|
||||||
|
try {
|
||||||
|
converter = UsedTypeConverter.forEnumColumn(enumType);
|
||||||
|
} on InvalidTypeForEnumConverterException catch (e) {
|
||||||
|
base.step.errors.report(ErrorInDartCode(
|
||||||
|
message: e.errorDescription,
|
||||||
|
affectedElement: getter.declaredElement,
|
||||||
|
severity: Severity.error,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (foundDefaultExpression != null && clientDefaultExpression != null) {
|
if (foundDefaultExpression != null && clientDefaultExpression != null) {
|
||||||
base.step.reportError(
|
base.step.reportError(
|
||||||
ErrorInDartCode(
|
ErrorInDartCode(
|
||||||
|
@ -217,6 +241,7 @@ class ColumnParser {
|
||||||
startBool: ColumnType.boolean,
|
startBool: ColumnType.boolean,
|
||||||
startString: ColumnType.text,
|
startString: ColumnType.text,
|
||||||
startInt: ColumnType.integer,
|
startInt: ColumnType.integer,
|
||||||
|
startEnum: ColumnType.integer,
|
||||||
startDateTime: ColumnType.datetime,
|
startDateTime: ColumnType.datetime,
|
||||||
startBlob: ColumnType.blob,
|
startBlob: ColumnType.blob,
|
||||||
startReal: ColumnType.real,
|
startReal: ColumnType.real,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||||
import 'package:moor_generator/moor_generator.dart';
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||||
|
import 'package:moor_generator/src/backends/backend.dart';
|
||||||
import 'package:moor_generator/src/model/declarations/declaration.dart';
|
import 'package:moor_generator/src/model/declarations/declaration.dart';
|
||||||
import 'package:moor_generator/src/model/used_type_converter.dart';
|
import 'package:moor_generator/src/model/used_type_converter.dart';
|
||||||
import 'package:moor_generator/src/utils/names.dart';
|
import 'package:moor_generator/src/utils/names.dart';
|
||||||
|
@ -15,18 +18,25 @@ class CreateTableReader {
|
||||||
/// The AST of this `CREATE TABLE` statement.
|
/// The AST of this `CREATE TABLE` statement.
|
||||||
final TableInducingStatement stmt;
|
final TableInducingStatement stmt;
|
||||||
final Step step;
|
final Step step;
|
||||||
|
final List<ImportStatement> imports;
|
||||||
|
|
||||||
CreateTableReader(this.stmt, this.step);
|
static const _schemaReader = SchemaFromCreateTable(moorExtensions: true);
|
||||||
|
static final RegExp _enumRegex =
|
||||||
|
RegExp(r'^enum\((\w+)\)$', caseSensitive: false);
|
||||||
|
|
||||||
|
CreateTableReader(this.stmt, this.step, [this.imports = const []]);
|
||||||
|
|
||||||
Future<MoorTable> extractTable(TypeMapper mapper) async {
|
Future<MoorTable> extractTable(TypeMapper mapper) async {
|
||||||
Table table;
|
Table table;
|
||||||
try {
|
try {
|
||||||
table = SchemaFromCreateTable(moorExtensions: true).read(stmt);
|
table = _schemaReader.read(stmt);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
step.reportError(ErrorInMoorFile(
|
step.reportError(ErrorInMoorFile(
|
||||||
span: stmt.tableNameToken.span,
|
span: stmt.tableNameToken.span,
|
||||||
message: 'Could not extract schema information for this table: $e',
|
message: 'Could not extract schema information for this table: $e',
|
||||||
));
|
));
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final foundColumns = <String, MoorColumn>{};
|
final foundColumns = <String, MoorColumn>{};
|
||||||
|
@ -43,6 +53,33 @@ class CreateTableReader {
|
||||||
String defaultValue;
|
String defaultValue;
|
||||||
String overriddenJsonKey;
|
String overriddenJsonKey;
|
||||||
|
|
||||||
|
final enumMatch = column.definition != null
|
||||||
|
? _enumRegex.firstMatch(column.definition.typeName)
|
||||||
|
: null;
|
||||||
|
if (enumMatch != null) {
|
||||||
|
final dartTypeName = enumMatch.group(1);
|
||||||
|
final dartType = await _readDartType(dartTypeName);
|
||||||
|
|
||||||
|
if (dartType == null) {
|
||||||
|
step.reportError(ErrorInMoorFile(
|
||||||
|
message: 'Type $dartTypeName could not be found. Are you missing '
|
||||||
|
'an import?',
|
||||||
|
severity: Severity.error,
|
||||||
|
span: column.definition.typeNames.span,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
converter = UsedTypeConverter.forEnumColumn(dartType);
|
||||||
|
} on InvalidTypeForEnumConverterException catch (e) {
|
||||||
|
step.reportError(ErrorInMoorFile(
|
||||||
|
message: e.errorDescription,
|
||||||
|
severity: Severity.error,
|
||||||
|
span: column.definition.typeNames.span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// columns from virtual tables don't necessarily have a definition, so we
|
// columns from virtual tables don't necessarily have a definition, so we
|
||||||
// can't read the constraints.
|
// can't read the constraints.
|
||||||
final constraints = column.hasDefinition
|
final constraints = column.hasDefinition
|
||||||
|
@ -64,6 +101,18 @@ class CreateTableReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (constraint is MappedBy) {
|
if (constraint is MappedBy) {
|
||||||
|
if (converter != null) {
|
||||||
|
// Already has a converter from an ENUM type
|
||||||
|
step.reportError(ErrorInMoorFile(
|
||||||
|
message: 'This column has an ENUM type, which implicitly creates '
|
||||||
|
"a type converter. You can't apply another converter to such "
|
||||||
|
'column. ',
|
||||||
|
span: constraint.span,
|
||||||
|
severity: Severity.warning,
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
converter = await _readTypeConverter(moorType, constraint);
|
converter = await _readTypeConverter(moorType, constraint);
|
||||||
// don't write MAPPED BY constraints when creating the table, they're
|
// don't write MAPPED BY constraints when creating the table, they're
|
||||||
// a convenience feature by the compiler
|
// a convenience feature by the compiler
|
||||||
|
@ -156,4 +205,30 @@ class CreateTableReader {
|
||||||
return UsedTypeConverter(
|
return UsedTypeConverter(
|
||||||
expression: code, mappedType: typeInDart, sqlType: sqlType);
|
expression: code, mappedType: typeInDart, sqlType: sqlType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<DartType> _readDartType(String typeIdentifier) async {
|
||||||
|
final dartImports = imports
|
||||||
|
.map((import) => import.importedFile)
|
||||||
|
.where((importUri) => importUri.endsWith('.dart'));
|
||||||
|
|
||||||
|
for (final import in dartImports) {
|
||||||
|
final resolved = step.task.session.resolve(step.file, import);
|
||||||
|
LibraryElement library;
|
||||||
|
try {
|
||||||
|
library = await step.task.backend.resolveDart(resolved.uri);
|
||||||
|
} on NotALibraryException {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final foundElement = library.exportNamespace.get(typeIdentifier);
|
||||||
|
if (foundElement is ClassElement) {
|
||||||
|
return foundElement.instantiate(
|
||||||
|
typeArguments: const [],
|
||||||
|
nullabilitySuffix: NullabilitySuffix.none,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ import 'package:moor_generator/moor_generator.dart';
|
||||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/affected_tables_visitor.dart';
|
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
import 'package:sqlparser/utils/find_referenced_tables.dart';
|
||||||
|
|
||||||
/// Handles `REFERENCES` clauses in tables by resolving their columns and
|
/// Handles `REFERENCES` clauses in tables by resolving their columns and
|
||||||
/// reporting errors if they don't exist. Further, sets the
|
/// reporting errors if they don't exist. Further, sets the
|
||||||
|
|
|
@ -27,7 +27,8 @@ class MoorParser {
|
||||||
final importStmt = parsedStmt;
|
final importStmt = parsedStmt;
|
||||||
importStatements.add(importStmt);
|
importStatements.add(importStmt);
|
||||||
} else if (parsedStmt is TableInducingStatement) {
|
} else if (parsedStmt is TableInducingStatement) {
|
||||||
createdReaders.add(CreateTableReader(parsedStmt, step));
|
createdReaders
|
||||||
|
.add(CreateTableReader(parsedStmt, step, importStatements));
|
||||||
} else if (parsedStmt is CreateTriggerStatement) {
|
} else if (parsedStmt is CreateTriggerStatement) {
|
||||||
// the table will be resolved in the analysis step
|
// the table will be resolved in the analysis step
|
||||||
createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file));
|
createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file));
|
||||||
|
@ -62,7 +63,10 @@ class MoorParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final reader in createdReaders) {
|
for (final reader in createdReaders) {
|
||||||
createdEntities.add(await reader.extractTable(step.mapper));
|
final moorTable = await reader.extractTable(step.mapper);
|
||||||
|
if (moorTable != null) {
|
||||||
|
createdEntities.add(moorTable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final analyzedFile = ParsedMoorFile(
|
final analyzedFile = ParsedMoorFile(
|
||||||
|
|
|
@ -65,20 +65,25 @@ class MoorOptions {
|
||||||
@JsonKey(name: 'eagerly_load_dart_ast', defaultValue: false)
|
@JsonKey(name: 'eagerly_load_dart_ast', defaultValue: false)
|
||||||
final bool eagerlyLoadDartAst;
|
final bool eagerlyLoadDartAst;
|
||||||
|
|
||||||
|
@JsonKey(name: 'data_class_to_companions', defaultValue: true)
|
||||||
|
final bool dataClassToCompanions;
|
||||||
|
|
||||||
/// Whether the [module] has been enabled in this configuration.
|
/// Whether the [module] has been enabled in this configuration.
|
||||||
bool hasModule(SqlModule module) => modules.contains(module);
|
bool hasModule(SqlModule module) => modules.contains(module);
|
||||||
|
|
||||||
const MoorOptions(
|
const MoorOptions({
|
||||||
{this.generateFromJsonStringConstructor = false,
|
this.generateFromJsonStringConstructor = false,
|
||||||
this.overrideHashAndEqualsInResultSets = false,
|
this.overrideHashAndEqualsInResultSets = false,
|
||||||
this.compactQueryMethods = false,
|
this.compactQueryMethods = false,
|
||||||
this.skipVerificationCode = false,
|
this.skipVerificationCode = false,
|
||||||
this.useDataClassNameForCompanions = false,
|
this.useDataClassNameForCompanions = false,
|
||||||
this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = false,
|
this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = false,
|
||||||
this.generateConnectConstructor = false,
|
this.generateConnectConstructor = false,
|
||||||
this.legacyTypeInference = false,
|
this.legacyTypeInference = false,
|
||||||
this.eagerlyLoadDartAst = false,
|
this.eagerlyLoadDartAst = false,
|
||||||
this.modules = const []});
|
this.dataClassToCompanions = true,
|
||||||
|
this.modules = const [],
|
||||||
|
});
|
||||||
|
|
||||||
factory MoorOptions.fromJson(Map<String, dynamic> json) =>
|
factory MoorOptions.fromJson(Map<String, dynamic> json) =>
|
||||||
_$MoorOptionsFromJson(json);
|
_$MoorOptionsFromJson(json);
|
||||||
|
|
|
@ -18,7 +18,8 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
|
||||||
'generate_connect_constructor',
|
'generate_connect_constructor',
|
||||||
'legacy_type_inference',
|
'legacy_type_inference',
|
||||||
'sqlite_modules',
|
'sqlite_modules',
|
||||||
'eagerly_load_dart_ast'
|
'eagerly_load_dart_ast',
|
||||||
|
'data_class_to_companions'
|
||||||
]);
|
]);
|
||||||
final val = MoorOptions(
|
final val = MoorOptions(
|
||||||
generateFromJsonStringConstructor: $checkedConvert(
|
generateFromJsonStringConstructor: $checkedConvert(
|
||||||
|
@ -50,6 +51,9 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
|
||||||
eagerlyLoadDartAst:
|
eagerlyLoadDartAst:
|
||||||
$checkedConvert(json, 'eagerly_load_dart_ast', (v) => v as bool) ??
|
$checkedConvert(json, 'eagerly_load_dart_ast', (v) => v as bool) ??
|
||||||
false,
|
false,
|
||||||
|
dataClassToCompanions:
|
||||||
|
$checkedConvert(json, 'data_class_to_companions', (v) => v as bool) ??
|
||||||
|
true,
|
||||||
modules: $checkedConvert(
|
modules: $checkedConvert(
|
||||||
json,
|
json,
|
||||||
'sqlite_modules',
|
'sqlite_modules',
|
||||||
|
@ -69,8 +73,9 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
|
||||||
'useColumnNameAsJsonKeyWhenDefinedInMoorFile':
|
'useColumnNameAsJsonKeyWhenDefinedInMoorFile':
|
||||||
'use_column_name_as_json_key_when_defined_in_moor_file',
|
'use_column_name_as_json_key_when_defined_in_moor_file',
|
||||||
'generateConnectConstructor': 'generate_connect_constructor',
|
'generateConnectConstructor': 'generate_connect_constructor',
|
||||||
'useExperimentalInference': 'use_experimental_inference',
|
'legacyTypeInference': 'legacy_type_inference',
|
||||||
'eagerlyLoadDartAst': 'eagerly_load_dart_ast',
|
'eagerlyLoadDartAst': 'eagerly_load_dart_ast',
|
||||||
|
'dataClassToCompanions': 'data_class_to_companions',
|
||||||
'modules': 'sqlite_modules'
|
'modules': 'sqlite_modules'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ class FoundFile {
|
||||||
FileState state = FileState.dirty;
|
FileState state = FileState.dirty;
|
||||||
final ErrorSink errors = ErrorSink();
|
final ErrorSink errors = ErrorSink();
|
||||||
|
|
||||||
FoundFile(this.uri, this.type);
|
FoundFile(this.uri, this.type) : assert(uri.isAbsolute);
|
||||||
|
|
||||||
String get shortName => uri.pathSegments.last;
|
String get shortName => uri.pathSegments.last;
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import 'package:moor_generator/src/model/used_type_converter.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||||
import 'package:moor_generator/src/utils/type_converter_hint.dart';
|
import 'package:moor_generator/src/utils/type_converter_hint.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||||
|
import 'package:sqlparser/utils/find_referenced_tables.dart';
|
||||||
|
|
||||||
import 'affected_tables_visitor.dart';
|
|
||||||
import 'lints/linter.dart';
|
import 'lints/linter.dart';
|
||||||
|
|
||||||
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
|
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
import 'package:moor/moor.dart' as m;
|
||||||
import 'package:moor_generator/moor_generator.dart';
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/affected_tables_visitor.dart';
|
|
||||||
import 'package:moor_generator/src/model/sql_query.dart';
|
import 'package:moor_generator/src/model/sql_query.dart';
|
||||||
import 'package:moor_generator/src/utils/type_converter_hint.dart';
|
import 'package:moor_generator/src/utils/type_converter_hint.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
import 'package:sqlparser/utils/find_referenced_tables.dart' as s;
|
||||||
|
|
||||||
/// Converts tables and types between the moor_generator and the sqlparser
|
/// Converts tables and types between the moor_generator and the sqlparser
|
||||||
/// library.
|
/// library.
|
||||||
|
@ -224,7 +225,13 @@ class TypeMapper {
|
||||||
return _engineTablesToSpecified[table];
|
return _engineTablesToSpecified[table];
|
||||||
}
|
}
|
||||||
|
|
||||||
WrittenMoorTable writtenToMoor(WrittenTable table) {
|
WrittenMoorTable writtenToMoor(s.TableWrite table) {
|
||||||
return WrittenMoorTable(tableToMoor(table.table), table.kind);
|
final moorKind = const {
|
||||||
|
s.UpdateKind.insert: m.UpdateKind.insert,
|
||||||
|
s.UpdateKind.update: m.UpdateKind.update,
|
||||||
|
s.UpdateKind.delete: m.UpdateKind.delete,
|
||||||
|
}[table.kind];
|
||||||
|
|
||||||
|
return WrittenMoorTable(tableToMoor(table.table), moorKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ const _ignoredLints = [
|
||||||
'lines_longer_than_80_chars',*/
|
'lines_longer_than_80_chars',*/
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const _targetMajorVersion = 2;
|
||||||
|
const _targetMinorVersion = 6;
|
||||||
|
|
||||||
class MoorGenerator extends Generator implements BaseGenerator {
|
class MoorGenerator extends Generator implements BaseGenerator {
|
||||||
@override
|
@override
|
||||||
MoorBuilder builder;
|
MoorBuilder builder;
|
||||||
|
@ -33,6 +36,23 @@ class MoorGenerator extends Generator implements BaseGenerator {
|
||||||
DatabaseWriter(db, writer.child()).write();
|
DatabaseWriter(db, writer.child()).write();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsed.declaredDatabases.isNotEmpty) {
|
||||||
|
// Warn if the project uses an SDK version that is incompatible with what
|
||||||
|
// moor generates.
|
||||||
|
final major = library.element.languageVersionMajor;
|
||||||
|
final minor = library.element.languageVersionMinor;
|
||||||
|
|
||||||
|
const expected = '$_targetMajorVersion.$_targetMinorVersion';
|
||||||
|
|
||||||
|
if (major < _targetMajorVersion ||
|
||||||
|
(major == _targetMajorVersion && minor < _targetMinorVersion)) {
|
||||||
|
log.warning('The language version of this file is Dart $major.$minor. '
|
||||||
|
'Moor generates code for Dart $expected or later. Please consider '
|
||||||
|
'raising the minimum SDK version in your pubspec.yaml to at least '
|
||||||
|
'$expected.0.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return writer.writeGenerated();
|
return writer.writeGenerated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:analyzer/src/dart/element/type.dart' show DynamicTypeImpl;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moor_generator/src/backends/backend.dart';
|
import 'package:moor_generator/src/backends/backend.dart';
|
||||||
|
|
||||||
|
@ -14,7 +17,7 @@ class CommonBackend extends Backend {
|
||||||
final absolute = driver.absolutePath(Uri.parse(import), base: base);
|
final absolute = driver.absolutePath(Uri.parse(import), base: base);
|
||||||
if (absolute == null) return null;
|
if (absolute == null) return null;
|
||||||
|
|
||||||
return Uri.parse(absolute);
|
return Uri.file(absolute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +47,12 @@ class CommonTask extends BackendTask {
|
||||||
return await driver.resolveDart(path);
|
return await driver.resolveDart(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DartType> resolveTypeOf(Uri context, String dartExpression) async {
|
||||||
|
// todo: Override so that we don't throw. We should support this properly.
|
||||||
|
return DynamicTypeImpl.instance;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> exists(Uri uri) {
|
Future<bool> exists(Uri uri) {
|
||||||
return Future.value(driver.doesFileExist(uri.path));
|
return Future.value(driver.doesFileExist(uri.path));
|
||||||
|
|
|
@ -108,7 +108,7 @@ class MoorDriver implements AnalysisDriverGeneric {
|
||||||
session.notifyTaskFinished(task);
|
session.notifyTaskFinished(task);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logger.root.warning(
|
Logger.root.warning(
|
||||||
'Error while working on ${mostImportantFile.file.uri}', e, s);
|
'Error while working on ${mostImportantFile.file.uri}: ', e, s);
|
||||||
_tracker.removePending(mostImportantFile);
|
_tracker.removePending(mostImportantFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,11 @@ class MoorCompletingContributor implements CompletionContributor {
|
||||||
final autoComplete = request.parsedMoor.parseResult.autoCompleteEngine;
|
final autoComplete = request.parsedMoor.parseResult.autoCompleteEngine;
|
||||||
final results = autoComplete.suggestCompletions(request.offset);
|
final results = autoComplete.suggestCompletions(request.offset);
|
||||||
|
|
||||||
|
// todo: Fix calculation in sqlparser. Then, set offset to results.anchor
|
||||||
|
// and length to results.lengthBefore
|
||||||
collector
|
collector
|
||||||
..offset = results.anchor
|
..offset = request.offset // should be results.anchor
|
||||||
..length = results.lengthBefore;
|
..length = 0; // should be results.lengthBefore
|
||||||
|
|
||||||
for (final suggestion in results.suggestions) {
|
for (final suggestion in results.suggestions) {
|
||||||
collector.addSuggestion(CompletionSuggestion(
|
collector.addSuggestion(CompletionSuggestion(
|
||||||
|
|
|
@ -102,8 +102,10 @@ class _NavigationVisitor extends RecursiveVisitor<void, void> {
|
||||||
|
|
||||||
if (resolved is Table && resolved != null) {
|
if (resolved is Table && resolved != null) {
|
||||||
final declaration = resolved.meta<MoorTable>()?.declaration;
|
final declaration = resolved.meta<MoorTable>()?.declaration;
|
||||||
_reportForSpan(
|
if (declaration != null) {
|
||||||
e.span, ElementKind.CLASS, locationOfDeclaration(declaration));
|
_reportForSpan(
|
||||||
|
e.span, ElementKind.CLASS, locationOfDeclaration(declaration));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moor_generator/src/model/table.dart';
|
import 'package:moor_generator/src/model/table.dart';
|
||||||
|
@ -32,8 +33,46 @@ class UsedTypeConverter {
|
||||||
/// them. This will be the field name for this converter.
|
/// them. This will be the field name for this converter.
|
||||||
String get fieldName => '\$converter$index';
|
String get fieldName => '\$converter$index';
|
||||||
|
|
||||||
UsedTypeConverter(
|
UsedTypeConverter({
|
||||||
{@required this.expression,
|
@required this.expression,
|
||||||
@required this.mappedType,
|
@required this.mappedType,
|
||||||
@required this.sqlType});
|
@required this.sqlType,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UsedTypeConverter.forEnumColumn(DartType enumType) {
|
||||||
|
if (enumType.element is! ClassElement) {
|
||||||
|
throw InvalidTypeForEnumConverterException('Not a class', enumType);
|
||||||
|
}
|
||||||
|
|
||||||
|
final creatingClass = enumType.element as ClassElement;
|
||||||
|
if (!creatingClass.isEnum) {
|
||||||
|
throw InvalidTypeForEnumConverterException('Not an enum', enumType);
|
||||||
|
}
|
||||||
|
|
||||||
|
final className = creatingClass.name;
|
||||||
|
|
||||||
|
return UsedTypeConverter(
|
||||||
|
expression: 'const EnumIndexConverter<$className>($className.values)',
|
||||||
|
mappedType: enumType,
|
||||||
|
sqlType: ColumnType.integer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidTypeForEnumConverterException implements Exception {
|
||||||
|
final String reason;
|
||||||
|
final DartType invalidType;
|
||||||
|
|
||||||
|
InvalidTypeForEnumConverterException(this.reason, this.invalidType);
|
||||||
|
|
||||||
|
String get errorDescription {
|
||||||
|
return "Can't use the type ${invalidType.getDisplayString()} as an enum "
|
||||||
|
'type: $reason';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Invalid type for enum converter: '
|
||||||
|
'${invalidType.getDisplayString()}. Reason: $reason';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,31 +10,90 @@ class FindStreamUpdateRules {
|
||||||
StreamQueryUpdateRules identifyRules() {
|
StreamQueryUpdateRules identifyRules() {
|
||||||
final rules = <UpdateRule>[];
|
final rules = <UpdateRule>[];
|
||||||
|
|
||||||
for (final trigger in db.entities.whereType<MoorTrigger>()) {
|
for (final entity in db.entities) {
|
||||||
final target = trigger.declaration.node.target;
|
if (entity is MoorTrigger) {
|
||||||
UpdateKind targetKind;
|
_writeRulesForTrigger(entity, rules);
|
||||||
if (target is DeleteTarget) {
|
} else if (entity is MoorTable) {
|
||||||
targetKind = UpdateKind.delete;
|
_writeRulesForTable(entity, rules);
|
||||||
} else if (target is InsertTarget) {
|
|
||||||
targetKind = UpdateKind.insert;
|
|
||||||
} else {
|
|
||||||
targetKind = UpdateKind.update;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rules.add(
|
|
||||||
WritePropagation(
|
|
||||||
on: TableUpdateQuery.onTableName(
|
|
||||||
trigger.on.sqlName,
|
|
||||||
limitUpdateKind: targetKind,
|
|
||||||
),
|
|
||||||
result: [
|
|
||||||
for (final update in trigger.bodyUpdates)
|
|
||||||
TableUpdate(update.table.sqlName, kind: update.kind)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return StreamQueryUpdateRules(rules);
|
return StreamQueryUpdateRules(rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _writeRulesForTable(MoorTable table, List<UpdateRule> rules) {
|
||||||
|
final declaration = table.declaration;
|
||||||
|
|
||||||
|
// We only know about foreign key clauses from tables in moor files
|
||||||
|
if (declaration is! MoorTableDeclaration) return;
|
||||||
|
|
||||||
|
final moorDeclaration = declaration as MoorTableDeclaration;
|
||||||
|
if (moorDeclaration.node is! CreateTableStatement) return;
|
||||||
|
|
||||||
|
final stmt = moorDeclaration.node as CreateTableStatement;
|
||||||
|
final tableName = table.sqlName;
|
||||||
|
|
||||||
|
for (final fkClause in stmt.allDescendants.whereType<ForeignKeyClause>()) {
|
||||||
|
final referencedMoorTable = table.references.firstWhere(
|
||||||
|
(tbl) => tbl.sqlName == fkClause.foreignTable.tableName,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
void writeRule(UpdateKind listen, ReferenceAction action) {
|
||||||
|
TableUpdate effect;
|
||||||
|
switch (action) {
|
||||||
|
case ReferenceAction.setNull:
|
||||||
|
case ReferenceAction.setDefault:
|
||||||
|
effect = TableUpdate(tableName, kind: UpdateKind.update);
|
||||||
|
break;
|
||||||
|
case ReferenceAction.cascade:
|
||||||
|
effect = TableUpdate(tableName, kind: listen);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect != null) {
|
||||||
|
rules.add(
|
||||||
|
WritePropagation(
|
||||||
|
on: TableUpdateQuery.onTableName(
|
||||||
|
referencedMoorTable.sqlName,
|
||||||
|
limitUpdateKind: listen,
|
||||||
|
),
|
||||||
|
result: [effect],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referencedMoorTable == null) continue;
|
||||||
|
writeRule(UpdateKind.delete, fkClause.onDelete);
|
||||||
|
writeRule(UpdateKind.update, fkClause.onUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeRulesForTrigger(MoorTrigger trigger, List<UpdateRule> rules) {
|
||||||
|
final target = trigger.declaration.node.target;
|
||||||
|
UpdateKind targetKind;
|
||||||
|
if (target is DeleteTarget) {
|
||||||
|
targetKind = UpdateKind.delete;
|
||||||
|
} else if (target is InsertTarget) {
|
||||||
|
targetKind = UpdateKind.insert;
|
||||||
|
} else {
|
||||||
|
targetKind = UpdateKind.update;
|
||||||
|
}
|
||||||
|
|
||||||
|
rules.add(
|
||||||
|
WritePropagation(
|
||||||
|
on: TableUpdateQuery.onTableName(
|
||||||
|
trigger.on.sqlName,
|
||||||
|
limitUpdateKind: targetKind,
|
||||||
|
),
|
||||||
|
result: [
|
||||||
|
for (final update in trigger.bodyUpdates)
|
||||||
|
TableUpdate(update.table.sqlName, kind: update.kind)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,9 @@ class DataClassWriter {
|
||||||
_writeMappingConstructor();
|
_writeMappingConstructor();
|
||||||
|
|
||||||
_writeToColumnsOverride();
|
_writeToColumnsOverride();
|
||||||
|
if (scope.options.dataClassToCompanions) {
|
||||||
|
_writeToCompanion();
|
||||||
|
}
|
||||||
|
|
||||||
// And a serializer and deserializer method
|
// And a serializer and deserializer method
|
||||||
_writeFromJson();
|
_writeFromJson();
|
||||||
|
@ -214,6 +217,30 @@ class DataClassWriter {
|
||||||
_buffer.write('return map; \n}\n');
|
_buffer.write('return map; \n}\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _writeToCompanion() {
|
||||||
|
_buffer
|
||||||
|
..write(table.getNameForCompanionClass(scope.options))
|
||||||
|
..write(' toCompanion(bool nullToAbsent) {\n');
|
||||||
|
|
||||||
|
_buffer
|
||||||
|
..write('return ')
|
||||||
|
..write(table.getNameForCompanionClass(scope.options))
|
||||||
|
..write('(');
|
||||||
|
|
||||||
|
for (final column in table.columns) {
|
||||||
|
final dartName = column.dartGetterName;
|
||||||
|
_buffer
|
||||||
|
..write(dartName)
|
||||||
|
..write(': ')
|
||||||
|
..write(dartName)
|
||||||
|
..write(' == null && nullToAbsent ? const Value.absent() : Value (')
|
||||||
|
..write(dartName)
|
||||||
|
..write('),');
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffer.write(');\n}');
|
||||||
|
}
|
||||||
|
|
||||||
void _writeToString() {
|
void _writeToString() {
|
||||||
/*
|
/*
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: moor_generator
|
name: moor_generator
|
||||||
description: Dev-dependency to generate table and dataclasses together with the moor package.
|
description: Dev-dependency to generate table and dataclasses together with the moor package.
|
||||||
version: 3.0.0
|
version: 3.1.0-dev
|
||||||
repository: https://github.com/simolus3/moor
|
repository: https://github.com/simolus3/moor
|
||||||
homepage: https://moor.simonbinder.eu/
|
homepage: https://moor.simonbinder.eu/
|
||||||
issue_tracker: https://github.com/simolus3/moor/issues
|
issue_tracker: https://github.com/simolus3/moor/issues
|
||||||
|
@ -23,7 +23,7 @@ dependencies:
|
||||||
|
|
||||||
# Moor-specific analysis
|
# Moor-specific analysis
|
||||||
moor: ^3.0.0
|
moor: ^3.0.0
|
||||||
sqlparser: ^0.8.0
|
sqlparser: ^0.9.0
|
||||||
|
|
||||||
# Dart analysis
|
# Dart analysis
|
||||||
analyzer: '^0.39.0'
|
analyzer: '^0.39.0'
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestState state;
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
state = TestState.withContent({
|
||||||
|
'foo|lib/main.dart': '''
|
||||||
|
import 'package:moor/moor.dart';
|
||||||
|
|
||||||
|
enum Fruits {
|
||||||
|
apple, orange, banana
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotAnEnum {}
|
||||||
|
|
||||||
|
class ValidUsage extends Table {
|
||||||
|
IntColumn get fruit => intEnum<Fruits>()();
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidNoEnum extends Table {
|
||||||
|
IntColumn get fruit => intEnum<NotAnEnum>()();
|
||||||
|
}
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
await state.analyze('package:foo/main.dart');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses enum columns', () {
|
||||||
|
final file =
|
||||||
|
state.file('package:foo/main.dart').currentResult as ParsedDartFile;
|
||||||
|
final table =
|
||||||
|
file.declaredTables.singleWhere((t) => t.sqlName == 'valid_usage');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
table.converters,
|
||||||
|
contains(
|
||||||
|
isA<UsedTypeConverter>().having(
|
||||||
|
(e) => e.expression, 'expression', contains('EnumIndexConverter')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fails when used with a non-enum class', () {
|
||||||
|
final errors = state.file('package:foo/main.dart').errors.errors;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
errors,
|
||||||
|
contains(isA<MoorError>().having((e) => e.message, 'message',
|
||||||
|
allOf(contains('Not an enum'), contains('NotAnEnum')))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('parses enum columns', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
import 'enum.dart';
|
||||||
|
|
||||||
|
CREATE TABLE foo (
|
||||||
|
fruit ENUM(Fruits) NOT NULL,
|
||||||
|
another ENUM(DoesNotExist) NOT NULL
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'foo|lib/enum.dart': '''
|
||||||
|
enum Fruits {
|
||||||
|
apple, orange, banane
|
||||||
|
}
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/a.moor');
|
||||||
|
final table = file.currentResult.declaredTables.single;
|
||||||
|
final column = table.columns.singleWhere((c) => c.name.name == 'fruit');
|
||||||
|
|
||||||
|
expect(column.type, ColumnType.integer);
|
||||||
|
expect(
|
||||||
|
column.typeConverter,
|
||||||
|
isA<UsedTypeConverter>()
|
||||||
|
.having(
|
||||||
|
(e) => e.expression,
|
||||||
|
'expression',
|
||||||
|
contains('EnumIndexConverter<Fruits>'),
|
||||||
|
)
|
||||||
|
.having(
|
||||||
|
(e) => e.mappedType.getDisplayString(),
|
||||||
|
'mappedType',
|
||||||
|
'Fruits',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
file.errors.errors,
|
||||||
|
contains(
|
||||||
|
isA<MoorError>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('Type DoesNotExist could not be found'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not allow converters for enum columns', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
import 'enum.dart';
|
||||||
|
|
||||||
|
CREATE TABLE foo (
|
||||||
|
fruit ENUM(Fruits) NOT NULL MAPPED BY `MyConverter()`
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'foo|lib/enum.dart': '''
|
||||||
|
import 'package:moor/moor.dart';
|
||||||
|
|
||||||
|
enum Fruits {
|
||||||
|
apple, orange, banane
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyConverter extends TypeConverter<String, String> {}
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/a.moor');
|
||||||
|
expect(
|
||||||
|
file.errors.errors,
|
||||||
|
contains(
|
||||||
|
isA<MoorError>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains("can't apply another converter"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not allow enum types for non-enums', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
import 'enum.dart';
|
||||||
|
|
||||||
|
CREATE TABLE foo (
|
||||||
|
fruit ENUM(NotAnEnum) NOT NULL
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'foo|lib/enum.dart': '''
|
||||||
|
class NotAnEnum {}
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/a.moor');
|
||||||
|
expect(
|
||||||
|
file.errors.errors,
|
||||||
|
contains(
|
||||||
|
isA<ErrorInMoorFile>()
|
||||||
|
.having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
allOf(contains('NotAnEnum'), contains('Not an enum')),
|
||||||
|
)
|
||||||
|
.having((e) => e.span.text, 'span', 'ENUM(NotAnEnum)'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
|
@ -28,8 +28,8 @@ CREATE TABLE bar (
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
final mapper = TypeMapper();
|
final mapper = TypeMapper();
|
||||||
final engine = SqlEngine(EngineOptions(useMoorExtensions: true));
|
final engine = SqlEngine(EngineOptions(useMoorExtensions: true));
|
||||||
final step = ParseMoorStep(
|
final step = ParseMoorStep(Task(null, null, null),
|
||||||
Task(null, null, null), FoundFile(Uri.parse('foo'), FileType.moor), '');
|
FoundFile(Uri.parse('file://foo'), FileType.moor), '');
|
||||||
|
|
||||||
final parsedFoo = engine.parse(createFoo).rootNode as CreateTableStatement;
|
final parsedFoo = engine.parse(createFoo).rootNode as CreateTableStatement;
|
||||||
final foo = await CreateTableReader(parsedFoo, step).extractTable(mapper);
|
final foo = await CreateTableReader(parsedFoo, step).extractTable(mapper);
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:build/build.dart';
|
||||||
|
import 'package:build_test/build_test.dart';
|
||||||
|
import 'package:moor_generator/integrations/build.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('generator emits warning about wrong language version', () async {
|
||||||
|
final logs = StreamController<String>();
|
||||||
|
|
||||||
|
final expectation = expectLater(
|
||||||
|
logs.stream,
|
||||||
|
emitsThrough(
|
||||||
|
allOf(
|
||||||
|
contains('Dart 2.1'),
|
||||||
|
contains('Please consider raising the minimum SDK version'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await testBuilder(
|
||||||
|
moorBuilder(BuilderOptions.empty),
|
||||||
|
{
|
||||||
|
'foo|lib/a.dart': '''
|
||||||
|
// @dart = 2.1
|
||||||
|
|
||||||
|
import 'package:moor/moor.dart';
|
||||||
|
|
||||||
|
@UseMoor(tables: [])
|
||||||
|
class Database {}
|
||||||
|
''',
|
||||||
|
},
|
||||||
|
reader: await PackageAssetReader.currentIsolate(),
|
||||||
|
onLog: (log) {
|
||||||
|
logs.add(log.message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectation;
|
||||||
|
await logs.close();
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,10 +5,10 @@ import 'package:test/test.dart';
|
||||||
void main() {
|
void main() {
|
||||||
FileTracker tracker;
|
FileTracker tracker;
|
||||||
|
|
||||||
final fa = FoundFile(Uri.parse('a'), FileType.dartLibrary);
|
final fa = FoundFile(Uri.parse('file://a'), FileType.dartLibrary);
|
||||||
final fb = FoundFile(Uri.parse('b'), FileType.dartLibrary);
|
final fb = FoundFile(Uri.parse('file://b'), FileType.dartLibrary);
|
||||||
final fc = FoundFile(Uri.parse('c'), FileType.dartLibrary);
|
final fc = FoundFile(Uri.parse('file://c'), FileType.dartLibrary);
|
||||||
final fd = FoundFile(Uri.parse('d'), FileType.dartLibrary);
|
final fd = FoundFile(Uri.parse('file://d'), FileType.dartLibrary);
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
tracker = FileTracker();
|
tracker = FileTracker();
|
||||||
|
|
|
@ -48,4 +48,88 @@ class MyDatabase {}
|
||||||
{const TableUpdate('users', kind: UpdateKind.insert)}),
|
{const TableUpdate('users', kind: UpdateKind.insert)}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('finds update rules for foreign key constraint', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
CREATE TABLE a (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
bar TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE will_delete_on_delete (
|
||||||
|
col INTEGER NOT NULL REFERENCES a(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE will_update_on_delete (
|
||||||
|
col INTEGER REFERENCES a(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE unaffected_on_delete (
|
||||||
|
col INTEGER REFERENCES a(id) ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE will_update_on_update (
|
||||||
|
col INTEGER NOT NULL REFERENCES a(id) ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE unaffected_on_update (
|
||||||
|
col INTEGER NOT NULL REFERENCES a(id) ON UPDATE NO ACTION
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'foo|lib/main.dart': '''
|
||||||
|
import 'package:moor/moor.dart';
|
||||||
|
|
||||||
|
@UseMoor(include: {'a.moor'})
|
||||||
|
class MyDatabase {}
|
||||||
|
'''
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/main.dart');
|
||||||
|
final db = (file.currentResult as ParsedDartFile).declaredDatabases.single;
|
||||||
|
|
||||||
|
expect(state.file('package:foo/a.moor').errors.errors, isEmpty);
|
||||||
|
|
||||||
|
final rules = FindStreamUpdateRules(db).identifyRules();
|
||||||
|
|
||||||
|
const updateA =
|
||||||
|
TableUpdateQuery.onTableName('a', limitUpdateKind: UpdateKind.update);
|
||||||
|
const deleteA =
|
||||||
|
TableUpdateQuery.onTableName('a', limitUpdateKind: UpdateKind.delete);
|
||||||
|
|
||||||
|
TableUpdate update(String table) {
|
||||||
|
return TableUpdate(table, kind: UpdateKind.update);
|
||||||
|
}
|
||||||
|
|
||||||
|
TableUpdate delete(String table) {
|
||||||
|
return TableUpdate(table, kind: UpdateKind.delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher writePropagation(TableUpdateQuery cause, TableUpdate effect) {
|
||||||
|
return isA<WritePropagation>()
|
||||||
|
.having((e) => e.on, 'on', cause)
|
||||||
|
.having((e) => e.result, 'result', equals([effect]));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
rules.rules,
|
||||||
|
containsAll(
|
||||||
|
[
|
||||||
|
writePropagation(
|
||||||
|
deleteA,
|
||||||
|
delete('will_delete_on_delete'),
|
||||||
|
),
|
||||||
|
writePropagation(
|
||||||
|
deleteA,
|
||||||
|
update('will_update_on_delete'),
|
||||||
|
),
|
||||||
|
writePropagation(
|
||||||
|
updateA,
|
||||||
|
update('will_update_on_update'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(rules.rules, hasLength(3));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
- New `package:sqlparser/utils/find_referenced_tables.dart` library. Use it to easily find all referenced tables
|
||||||
|
in a query.
|
||||||
|
- Support [row values](https://www.sqlite.org/rowvalue.html) including warnings about misuse
|
||||||
|
|
||||||
## 0.8.1
|
## 0.8.1
|
||||||
|
|
||||||
- Support collate expressions in the new type inference ([#533](https://github.com/simolus3/moor/issues/533))
|
- Support collate expressions in the new type inference ([#533](https://github.com/simolus3/moor/issues/533))
|
||||||
|
|
|
@ -64,8 +64,8 @@ final context =
|
||||||
final select = context.root as SelectStatement;
|
final select = context.root as SelectStatement;
|
||||||
final resolvedColumns = select.resolvedColumns;
|
final resolvedColumns = select.resolvedColumns;
|
||||||
|
|
||||||
resolvedColumns.map((c) => c.name)); // id, content, id, content, 3 + 4
|
resolvedColumns.map((c) => c.name); // id, content, id, content, 3 + 4
|
||||||
resolvedColumns.map((c) => context.typeOf(c).type.type) // int, text, int, text, int, int
|
resolvedColumns.map((c) => context.typeOf(c).type.type); // int, text, int, text, int, int
|
||||||
```
|
```
|
||||||
|
|
||||||
## But why?
|
## But why?
|
||||||
|
|
|
@ -4,10 +4,36 @@ import 'package:sqlparser/sqlparser.dart';
|
||||||
// prints what columns would be returned by that statement.
|
// prints what columns would be returned by that statement.
|
||||||
void main() {
|
void main() {
|
||||||
final engine = SqlEngine()
|
final engine = SqlEngine()
|
||||||
..registerTable(frameworks)
|
..registerTableFromSql(
|
||||||
..registerTable(languages)
|
'''
|
||||||
..registerTable(frameworkToLanguage);
|
CREATE TABLE frameworks (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
popularity REAL NOT NULL
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
..registerTableFromSql(
|
||||||
|
'''
|
||||||
|
CREATE TABLE languages (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
..registerTableFromSql(
|
||||||
|
'''
|
||||||
|
CREATE TABLE uses_language (
|
||||||
|
framework INTEGER NOT NULL REFERENCES frameworks (id),
|
||||||
|
language INTEGER NOT NULL REFERENCES languages (id),
|
||||||
|
PRIMARY KEY (framework, language)
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use SqlEngine.analyze to parse a single sql statement and analyze it.
|
||||||
|
// Analysis can be used to find semantic errors, lints and inferred types of
|
||||||
|
// expressions or result columns.
|
||||||
final result = engine.analyze('''
|
final result = engine.analyze('''
|
||||||
SELECT f.* FROM frameworks f
|
SELECT f.* FROM frameworks f
|
||||||
INNER JOIN uses_language ul ON ul.framework = f.id
|
INNER JOIN uses_language ul ON ul.framework = f.id
|
||||||
|
@ -30,50 +56,11 @@ LIMIT 5 OFFSET 5 * 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// declare some tables. I know this is verbose and boring, but it's needed so
|
extension on SqlEngine {
|
||||||
// that the analyzer knows what's going on.
|
/// Utility function that parses a `CREATE TABLE` statement and registers the
|
||||||
final Table frameworks = Table(
|
/// created table to the engine.
|
||||||
name: 'frameworks',
|
void registerTableFromSql(String createTable) {
|
||||||
resolvedColumns: [
|
final stmt = parse(createTable).rootNode as CreateTableStatement;
|
||||||
TableColumn(
|
registerTable(schemaReader.read(stmt));
|
||||||
'id',
|
}
|
||||||
const ResolvedType(type: BasicType.int),
|
}
|
||||||
),
|
|
||||||
TableColumn(
|
|
||||||
'name',
|
|
||||||
const ResolvedType(type: BasicType.text),
|
|
||||||
),
|
|
||||||
TableColumn(
|
|
||||||
'popularity',
|
|
||||||
const ResolvedType(type: BasicType.real),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
final Table languages = Table(
|
|
||||||
name: 'languages',
|
|
||||||
resolvedColumns: [
|
|
||||||
TableColumn(
|
|
||||||
'id',
|
|
||||||
const ResolvedType(type: BasicType.int),
|
|
||||||
),
|
|
||||||
TableColumn(
|
|
||||||
'name',
|
|
||||||
const ResolvedType(type: BasicType.text),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
final Table frameworkToLanguage = Table(
|
|
||||||
name: 'uses_language',
|
|
||||||
resolvedColumns: [
|
|
||||||
TableColumn(
|
|
||||||
'framework',
|
|
||||||
const ResolvedType(type: BasicType.int),
|
|
||||||
),
|
|
||||||
TableColumn(
|
|
||||||
'language',
|
|
||||||
const ResolvedType(type: BasicType.int),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
|
@ -62,5 +62,6 @@ enum AnalysisErrorType {
|
||||||
compoundColumnCountMismatch,
|
compoundColumnCountMismatch,
|
||||||
cteColumnCountMismatch,
|
cteColumnCountMismatch,
|
||||||
valuesSelectCountMismatch,
|
valuesSelectCountMismatch,
|
||||||
|
rowValueMisuse,
|
||||||
other,
|
other,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ class SchemaFromCreateTable {
|
||||||
/// and `DATETIME` columns.
|
/// and `DATETIME` columns.
|
||||||
final bool moorExtensions;
|
final bool moorExtensions;
|
||||||
|
|
||||||
SchemaFromCreateTable({this.moorExtensions = false});
|
const SchemaFromCreateTable({this.moorExtensions = false});
|
||||||
|
|
||||||
Table read(TableInducingStatement stmt) {
|
Table read(TableInducingStatement stmt) {
|
||||||
if (stmt is CreateTableStatement) {
|
if (stmt is CreateTableStatement) {
|
||||||
|
@ -30,9 +30,7 @@ class SchemaFromCreateTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
TableColumn _readColumn(ColumnDefinition definition) {
|
TableColumn _readColumn(ColumnDefinition definition) {
|
||||||
final typeName = definition.typeName.toUpperCase();
|
final type = resolveColumnType(definition.typeName);
|
||||||
|
|
||||||
final type = resolveColumnType(typeName);
|
|
||||||
final nullable = !definition.constraints.any((c) => c is NotNull);
|
final nullable = !definition.constraints.any((c) => c is NotNull);
|
||||||
|
|
||||||
final resolvedType = type.withNullable(nullable);
|
final resolvedType = type.withNullable(nullable);
|
||||||
|
@ -49,7 +47,7 @@ class SchemaFromCreateTable {
|
||||||
/// [IsDateTime] hints if the type name contains `BOOL` or `DATE`,
|
/// [IsDateTime] hints if the type name contains `BOOL` or `DATE`,
|
||||||
/// respectively.
|
/// respectively.
|
||||||
/// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
|
/// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
|
||||||
ResolvedType resolveColumnType(String typeName) {
|
ResolvedType resolveColumnType(String /*?*/ typeName) {
|
||||||
if (typeName == null) {
|
if (typeName == null) {
|
||||||
return const ResolvedType(type: BasicType.blob);
|
return const ResolvedType(type: BasicType.blob);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +73,10 @@ class SchemaFromCreateTable {
|
||||||
if (upper.contains('DATE')) {
|
if (upper.contains('DATE')) {
|
||||||
return const ResolvedType(type: BasicType.int, hint: IsDateTime());
|
return const ResolvedType(type: BasicType.int, hint: IsDateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (upper.contains('ENUM')) {
|
||||||
|
return const ResolvedType(type: BasicType.int);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return const ResolvedType(type: BasicType.real);
|
return const ResolvedType(type: BasicType.real);
|
||||||
|
|
|
@ -18,6 +18,67 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
|
||||||
visitChildren(e, arg);
|
visitChildren(e, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitTuple(Tuple e, void arg) {
|
||||||
|
if (!e.usedAsRowValue) return;
|
||||||
|
|
||||||
|
bool isRowValue(Expression expr) => expr is Tuple || expr is SubQuery;
|
||||||
|
|
||||||
|
var parent = e.parent;
|
||||||
|
var isAllowed = false;
|
||||||
|
|
||||||
|
if (parent is WhenComponent && e == parent.when) {
|
||||||
|
// look at the surrounding case expression
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent is BinaryExpression) {
|
||||||
|
// Source: https://www.sqlite.org/rowvalue.html#syntax
|
||||||
|
const allowedTokens = [
|
||||||
|
TokenType.less,
|
||||||
|
TokenType.lessEqual,
|
||||||
|
TokenType.more,
|
||||||
|
TokenType.moreEqual,
|
||||||
|
TokenType.equal,
|
||||||
|
TokenType.doubleEqual,
|
||||||
|
TokenType.lessMore,
|
||||||
|
TokenType.$is,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowedTokens.contains(parent.operator.type)) {
|
||||||
|
isAllowed = true;
|
||||||
|
}
|
||||||
|
} else if (parent is BetweenExpression) {
|
||||||
|
// Allowed if all value are row values or subqueries
|
||||||
|
isAllowed = !parent.childNodes.any((e) => !isRowValue(e));
|
||||||
|
} else if (parent is CaseExpression) {
|
||||||
|
// Allowed if we have something to compare against and all comparisons
|
||||||
|
// are row values
|
||||||
|
if (parent.base == null) {
|
||||||
|
isAllowed = false;
|
||||||
|
} else {
|
||||||
|
final comparisons = <Expression>[
|
||||||
|
parent.base,
|
||||||
|
for (final branch in parent.whens) branch.when
|
||||||
|
];
|
||||||
|
|
||||||
|
isAllowed = !comparisons.any((e) => !isRowValue(e));
|
||||||
|
}
|
||||||
|
} else if (parent is InExpression) {
|
||||||
|
// In expressions are tricky. The rhs can always be a row value, but the
|
||||||
|
// lhs can only be a row value if the rhs is a subquery
|
||||||
|
isAllowed = e == parent.inside || parent.inside is SubQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
context.reportError(AnalysisError(
|
||||||
|
type: AnalysisErrorType.rowValueMisuse,
|
||||||
|
relevantNode: e,
|
||||||
|
message: 'Row values can only be used as expressions in comparisons',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
|
void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
|
||||||
final expectedColumns = e.resolvedColumns.length;
|
final expectedColumns = e.resolvedColumns.length;
|
||||||
|
|
|
@ -105,6 +105,10 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
// acts like a table for expressions in the same scope, so let's
|
// acts like a table for expressions in the same scope, so let's
|
||||||
// register it.
|
// register it.
|
||||||
if (table.as != null) {
|
if (table.as != null) {
|
||||||
|
// todo should we register a TableAlias instead? Some parts of this
|
||||||
|
// package and moor_generator might depend on this being a table
|
||||||
|
// directly (e.g. nested result sets in moor).
|
||||||
|
// Same for nested selects, joins and table-valued functions below.
|
||||||
scope.register(table.as, table);
|
scope.register(table.as, table);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,14 +2,19 @@ part of '../ast.dart';
|
||||||
|
|
||||||
/// A tuple of values, denotes in brackets. `(<expr>, ..., <expr>)`.
|
/// A tuple of values, denotes in brackets. `(<expr>, ..., <expr>)`.
|
||||||
///
|
///
|
||||||
/// Notice that this class extends [Expression] because the type inference
|
/// In sqlite, this is also called a "row value".
|
||||||
/// algorithm works best when tuples are treated as expressions. Syntactically,
|
|
||||||
/// tuples aren't expressions.
|
|
||||||
class Tuple extends Expression {
|
class Tuple extends Expression {
|
||||||
/// The expressions appearing in this tuple.
|
/// The expressions appearing in this tuple.
|
||||||
final List<Expression> expressions;
|
final List<Expression> expressions;
|
||||||
|
|
||||||
Tuple({@required this.expressions});
|
/// Whether this tuple is used as an expression, e.g. a [row value][r v].
|
||||||
|
///
|
||||||
|
/// Other tuples might appear in `VALUES` clauses.
|
||||||
|
///
|
||||||
|
/// [r v]: https://www.sqlite.org/rowvalue.html
|
||||||
|
final bool usedAsRowValue;
|
||||||
|
|
||||||
|
Tuple({@required this.expressions, this.usedAsRowValue = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ part of '../ast.dart';
|
||||||
/// https://www.sqlite.org/syntax/column-def.html
|
/// https://www.sqlite.org/syntax/column-def.html
|
||||||
class ColumnDefinition extends AstNode {
|
class ColumnDefinition extends AstNode {
|
||||||
final String columnName;
|
final String columnName;
|
||||||
final String typeName;
|
final String /*?*/ typeName;
|
||||||
final List<ColumnConstraint> constraints;
|
final List<ColumnConstraint> constraints;
|
||||||
|
|
||||||
/// The tokens there were involved in defining the type of this column.
|
/// The tokens there were involved in defining the type of this column.
|
||||||
|
|
|
@ -301,6 +301,20 @@ mixin ExpressionParser on ParserBase {
|
||||||
return SubQuery(select: selectStmt)..setSpan(left, _previous);
|
return SubQuery(select: selectStmt)..setSpan(left, _previous);
|
||||||
} else {
|
} else {
|
||||||
final expr = expression();
|
final expr = expression();
|
||||||
|
|
||||||
|
if (_matchOne(TokenType.comma)) {
|
||||||
|
// It's a row value!
|
||||||
|
final expressions = [expr];
|
||||||
|
|
||||||
|
do {
|
||||||
|
expressions.add(expression());
|
||||||
|
} while (_matchOne(TokenType.comma));
|
||||||
|
|
||||||
|
_consume(TokenType.rightParen, 'Expected a closing bracket');
|
||||||
|
return Tuple(expressions: expressions, usedAsRowValue: true)
|
||||||
|
..setSpan(left, _previous);
|
||||||
|
}
|
||||||
|
|
||||||
_consume(TokenType.rightParen, 'Expected a closing bracket');
|
_consume(TokenType.rightParen, 'Expected a closing bracket');
|
||||||
return Parentheses(left, expr, _previous)..setSpan(left, _previous);
|
return Parentheses(left, expr, _previous)..setSpan(left, _previous);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,3 +32,19 @@ extension SyntacticLengthExtension on SyntacticEntity {
|
||||||
/// The length of this entity, in characters.
|
/// The length of this entity, in characters.
|
||||||
int get length => lastPosition - firstPosition;
|
int get length => lastPosition - firstPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extension to obtain the span for a sequence of [SyntacticEntity].
|
||||||
|
extension UnionEntityExtension on Iterable<SyntacticEntity> {
|
||||||
|
/// Creates the span covered by all of the entities in this iterable.
|
||||||
|
FileSpan get span {
|
||||||
|
if (isEmpty) {
|
||||||
|
throw ArgumentError.value(this, 'this', 'Was empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
final firstSpan = first.span;
|
||||||
|
return skip(1).fold(
|
||||||
|
firstSpan,
|
||||||
|
(previousValue, entity) => previousValue.expand(entity.span),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:moor/moor.dart' show UpdateKind;
|
library utils.find_referenced_tables;
|
||||||
|
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
|
||||||
/// An AST-visitor that walks sql statements and finds all tables referenced in
|
/// An AST-visitor that walks sql statements and finds all tables referenced in
|
||||||
|
@ -40,11 +41,25 @@ class ReferencedTablesVisitor extends RecursiveVisitor<void, void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WrittenTable {
|
enum UpdateKind { insert, update, delete }
|
||||||
|
|
||||||
|
/// A write to a table as found while analyzing a statement.
|
||||||
|
class TableWrite {
|
||||||
|
/// The table that a statement might write to when run.
|
||||||
final Table table;
|
final Table table;
|
||||||
|
|
||||||
|
/// What kind of update was found (e.g. insert, update or delete).
|
||||||
final UpdateKind kind;
|
final UpdateKind kind;
|
||||||
|
|
||||||
WrittenTable(this.table, this.kind);
|
TableWrite(this.table, this.kind);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => 37 * table.hashCode + kind.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return other is TableWrite && other.table == table && other.kind == kind;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds all tables that could be affected when executing a query. In
|
/// Finds all tables that could be affected when executing a query. In
|
||||||
|
@ -56,12 +71,12 @@ class UpdatedTablesVisitor extends ReferencedTablesVisitor {
|
||||||
/// Note that this is a subset of [foundTables], since an updating tables
|
/// Note that this is a subset of [foundTables], since an updating tables
|
||||||
/// could reference tables it's not updating (e.g. with `INSERT INTO foo
|
/// could reference tables it's not updating (e.g. with `INSERT INTO foo
|
||||||
/// SELECT * FROM bar`).
|
/// SELECT * FROM bar`).
|
||||||
final Set<WrittenTable> writtenTables = {};
|
final Set<TableWrite> writtenTables = {};
|
||||||
|
|
||||||
void _addIfResolved(ResolvesToResultSet r, UpdateKind kind) {
|
void _addIfResolved(ResolvesToResultSet r, UpdateKind kind) {
|
||||||
final resolved = _toTableOrNull(r);
|
final resolved = _toTableOrNull(r);
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
writtenTables.add(WrittenTable(resolved, kind));
|
writtenTables.add(TableWrite(resolved, kind));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,3 +98,34 @@ class UpdatedTablesVisitor extends ReferencedTablesVisitor {
|
||||||
visitChildren(e, arg);
|
visitChildren(e, arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds all writes to a table that occur anywhere inside the [root] node or a
|
||||||
|
/// descendant.
|
||||||
|
///
|
||||||
|
/// The [root] node must have all its references resolved. This means that using
|
||||||
|
/// a node obtained via [SqlEngine.parse] directly won't report meaningful
|
||||||
|
/// results. Instead, use [SqlEngine.analyze] or [SqlEngine.analyzeParsed].
|
||||||
|
///
|
||||||
|
/// If you want to find all referenced tables, use [findReferencedTables]. If
|
||||||
|
/// you want to find writes (including their [UpdateKind]) and referenced
|
||||||
|
/// tables, constrct a [UpdatedTablesVisitor] manually.
|
||||||
|
/// Then, let it [RecursiveVisitor.visit] the [root] node. You can now use
|
||||||
|
/// [UpdatedTablesVisitor.writtenTables] and
|
||||||
|
/// [ReferencedTablesVisitor.foundTables]. This will only walk the ast once,
|
||||||
|
/// whereas calling this and [findReferencedTables] will require two walks.
|
||||||
|
///
|
||||||
|
Set<TableWrite> findWrittenTables(AstNode root) {
|
||||||
|
return (UpdatedTablesVisitor()..visit(root, null)).writtenTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds all tables referenced in [root] or a descendant.
|
||||||
|
///
|
||||||
|
/// The [root] node must have all its references resolved. This means that using
|
||||||
|
/// a node obtained via [SqlEngine.parse] directly won't report meaningful
|
||||||
|
/// results. Instead, use [SqlEngine.analyze] or [SqlEngine.analyzeParsed].
|
||||||
|
///
|
||||||
|
/// If you want to use both [findWrittenTables] and this on the same ast node,
|
||||||
|
/// follow the advice on [findWrittenTables] to only walk the ast once.
|
||||||
|
Set<Table> findReferencedTables(AstNode root) {
|
||||||
|
return (ReferencedTablesVisitor()..visit(root, null)).foundTables;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
description: Parses sqlite statements and performs static analysis on them
|
description: Parses sqlite statements and performs static analysis on them
|
||||||
version: 0.8.1
|
version: 0.9.0
|
||||||
homepage: https://github.com/simolus3/moor/tree/develop/sqlparser
|
homepage: https://github.com/simolus3/moor/tree/develop/sqlparser
|
||||||
#homepage: https://moor.simonbinder.eu/
|
#homepage: https://moor.simonbinder.eu/
|
||||||
issue_tracker: https://github.com/simolus3/moor/issues
|
issue_tracker: https://github.com/simolus3/moor/issues
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
SqlEngine engine;
|
||||||
|
setUp(() {
|
||||||
|
engine = SqlEngine();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when using row value in select', () {
|
||||||
|
engine.analyze('SELECT (1, 2, 3)').expectError('(1, 2, 3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('as left hand operator of in', () {
|
||||||
|
engine.analyze('SELECT (1, 2, 3) IN (4, 5, 6)').expectError('(1, 2, 3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in BETWEEN expression', () {
|
||||||
|
engine.analyze('SELECT 1 BETWEEN (1, 2, 3) AND 3').expectError('(1, 2, 3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in CASE - value', () {
|
||||||
|
engine
|
||||||
|
.analyze('SELECT CASE 1 WHEN 1 THEN (1, 2, 3) ELSE 1 END')
|
||||||
|
.expectError('(1, 2, 3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in CASE - when', () {
|
||||||
|
engine
|
||||||
|
.analyze('SELECT CASE 1 WHEN (1, 2, 3) THEN 1 ELSE 1 END')
|
||||||
|
.expectError('(1, 2, 3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in CASE - base', () {
|
||||||
|
engine
|
||||||
|
.analyze('SELECT CASE (1, 2, 3) WHEN 1 THEN 1 ELSE 1 END')
|
||||||
|
.expectError('(1, 2, 3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
group('does not generate error for valid usage', () {
|
||||||
|
test('in comparison', () {
|
||||||
|
engine.analyze('SELECT (1, 2, 3) < (?, ?, ?);').expectNoError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in IN expression (lhs)', () {
|
||||||
|
engine.analyze('SELECT (1, 2, 3) IN (VALUES(0, 1, 2))').expectNoError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in IN expression (rhs)', () {
|
||||||
|
engine.analyze('SELECT ? IN (1, 2, 3)').expectNoError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in BETWEEN expression', () {
|
||||||
|
engine.analyze('SELECT (1, 2) BETWEEN (3, 4) AND (5, 6)').expectNoError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in CASE expression', () {
|
||||||
|
engine
|
||||||
|
.analyze('SELECT CASE (1, 2) WHEN (1, 2) THEN 1 ELSE 0 END')
|
||||||
|
.expectNoError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on AnalysisContext {
|
||||||
|
void expectError(String lexeme) {
|
||||||
|
expect(
|
||||||
|
errors,
|
||||||
|
[
|
||||||
|
isA<AnalysisError>()
|
||||||
|
.having((e) => e.type, 'type', AnalysisErrorType.rowValueMisuse)
|
||||||
|
.having((e) => e.span.text, 'span.text', lexeme),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectNoError() {
|
||||||
|
expect(errors, isEmpty);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import 'package:test/test.dart';
|
||||||
void main() {
|
void main() {
|
||||||
test('isAliasForRowId', () {
|
test('isAliasForRowId', () {
|
||||||
final engine = SqlEngine();
|
final engine = SqlEngine();
|
||||||
final schemaParser = SchemaFromCreateTable();
|
const schemaParser = SchemaFromCreateTable();
|
||||||
|
|
||||||
final isAlias = {
|
final isAlias = {
|
||||||
'CREATE TABLE x (id INTEGER PRIMARY KEY)': true,
|
'CREATE TABLE x (id INTEGER PRIMARY KEY)': true,
|
||||||
|
|
|
@ -36,7 +36,7 @@ const _affinityTests = {
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('affinity from typename', () {
|
test('affinity from typename', () {
|
||||||
final resolver = SchemaFromCreateTable();
|
const resolver = SchemaFromCreateTable();
|
||||||
|
|
||||||
_affinityTests.forEach((key, value) {
|
_affinityTests.forEach((key, value) {
|
||||||
expect(resolver.columnAffinity(key), equals(value),
|
expect(resolver.columnAffinity(key), equals(value),
|
||||||
|
@ -48,7 +48,8 @@ void main() {
|
||||||
final engine = SqlEngine();
|
final engine = SqlEngine();
|
||||||
final stmt = engine.parse(createTableStmt).rootNode;
|
final stmt = engine.parse(createTableStmt).rootNode;
|
||||||
|
|
||||||
final table = SchemaFromCreateTable().read(stmt as CreateTableStatement);
|
final table =
|
||||||
|
const SchemaFromCreateTable().read(stmt as CreateTableStatement);
|
||||||
|
|
||||||
expect(table.resolvedColumns.map((c) => c.name),
|
expect(table.resolvedColumns.map((c) => c.name),
|
||||||
['id', 'email', 'score', 'display_name']);
|
['id', 'email', 'score', 'display_name']);
|
||||||
|
@ -70,7 +71,7 @@ void main() {
|
||||||
)
|
)
|
||||||
''').rootNode;
|
''').rootNode;
|
||||||
|
|
||||||
final table = SchemaFromCreateTable(moorExtensions: true)
|
final table = const SchemaFromCreateTable(moorExtensions: true)
|
||||||
.read(stmt as CreateTableStatement);
|
.read(stmt as CreateTableStatement);
|
||||||
expect(table.resolvedColumns.map((c) => c.type), const [
|
expect(table.resolvedColumns.map((c) => c.type), const [
|
||||||
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: true),
|
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: true),
|
||||||
|
@ -79,4 +80,12 @@ void main() {
|
||||||
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: false),
|
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: false),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can read columns without type name', () {
|
||||||
|
final engine = SqlEngine();
|
||||||
|
final stmt = engine.parse('CREATE TABLE foo (id);').rootNode;
|
||||||
|
|
||||||
|
final table = engine.schemaReader.read(stmt as CreateTableStatement);
|
||||||
|
expect(table.resolvedColumns.single.type.type, BasicType.blob);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:test/test.dart';
|
||||||
void main() {
|
void main() {
|
||||||
group('finds columns', () {
|
group('finds columns', () {
|
||||||
final engine = SqlEngine();
|
final engine = SqlEngine();
|
||||||
final schemaParser = SchemaFromCreateTable();
|
const schemaParser = SchemaFromCreateTable();
|
||||||
|
|
||||||
Column findWith(String createTbl, String columnName) {
|
Column findWith(String createTbl, String columnName) {
|
||||||
final stmt = engine.parse(createTbl).rootNode as CreateTableStatement;
|
final stmt = engine.parse(createTbl).rootNode as CreateTableStatement;
|
||||||
|
|
|
@ -11,8 +11,8 @@ void main() {
|
||||||
final result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
final result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
||||||
"fts5(bar , tokenize = 'porter ascii')");
|
"fts5(bar , tokenize = 'porter ascii')");
|
||||||
|
|
||||||
final table =
|
final table = const SchemaFromCreateTable()
|
||||||
SchemaFromCreateTable().read(result.root as TableInducingStatement);
|
.read(result.root as TableInducingStatement);
|
||||||
|
|
||||||
expect(table.name, 'foo');
|
expect(table.name, 'foo');
|
||||||
final columns = table.resultColumns;
|
final columns = table.resultColumns;
|
||||||
|
@ -24,8 +24,8 @@ void main() {
|
||||||
final result = engine
|
final result = engine
|
||||||
.analyze('CREATE VIRTUAL TABLE foo USING fts5(bar, baz UNINDEXED)');
|
.analyze('CREATE VIRTUAL TABLE foo USING fts5(bar, baz UNINDEXED)');
|
||||||
|
|
||||||
final table =
|
final table = const SchemaFromCreateTable()
|
||||||
SchemaFromCreateTable().read(result.root as TableInducingStatement);
|
.read(result.root as TableInducingStatement);
|
||||||
|
|
||||||
expect(table.name, 'foo');
|
expect(table.name, 'foo');
|
||||||
expect(table.resultColumns.map((c) => c.name), ['bar', 'baz']);
|
expect(table.resultColumns.map((c) => c.name), ['bar', 'baz']);
|
||||||
|
@ -39,7 +39,7 @@ void main() {
|
||||||
// add an fts5 table for the following queries
|
// add an fts5 table for the following queries
|
||||||
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
||||||
'fts5(bar, baz);');
|
'fts5(bar, baz);');
|
||||||
engine.registerTable(SchemaFromCreateTable()
|
engine.registerTable(const SchemaFromCreateTable()
|
||||||
.read(fts5Result.root as TableInducingStatement));
|
.read(fts5Result.root as TableInducingStatement));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ void main() {
|
||||||
// add an fts5 table for the following queries
|
// add an fts5 table for the following queries
|
||||||
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
||||||
'fts5(bar, baz);');
|
'fts5(bar, baz);');
|
||||||
engine.registerTable(SchemaFromCreateTable()
|
engine.registerTable(const SchemaFromCreateTable()
|
||||||
.read(fts5Result.root as TableInducingStatement));
|
.read(fts5Result.root as TableInducingStatement));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -129,11 +129,11 @@ void main() {
|
||||||
// add an fts5 table for the following queries
|
// add an fts5 table for the following queries
|
||||||
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
|
||||||
'fts5(bar, baz);');
|
'fts5(bar, baz);');
|
||||||
engine.registerTable(SchemaFromCreateTable()
|
engine.registerTable(const SchemaFromCreateTable()
|
||||||
.read(fts5Result.root as TableInducingStatement));
|
.read(fts5Result.root as TableInducingStatement));
|
||||||
|
|
||||||
final normalResult = engine.analyze('CREATE TABLE other (bar TEXT);');
|
final normalResult = engine.analyze('CREATE TABLE other (bar TEXT);');
|
||||||
engine.registerTable(SchemaFromCreateTable()
|
engine.registerTable(const SchemaFromCreateTable()
|
||||||
.read(normalResult.root as TableInducingStatement));
|
.read(normalResult.root as TableInducingStatement));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -148,6 +148,18 @@ final Map<String, Expression> _testCases = {
|
||||||
TimeConstantKind.currentTimestamp, token(TokenType.currentTimestamp)),
|
TimeConstantKind.currentTimestamp, token(TokenType.currentTimestamp)),
|
||||||
'CURRENT_DATE': TimeConstantLiteral(
|
'CURRENT_DATE': TimeConstantLiteral(
|
||||||
TimeConstantKind.currentDate, token(TokenType.currentDate)),
|
TimeConstantKind.currentDate, token(TokenType.currentDate)),
|
||||||
|
'(1, 2, 3) > (?, ?, ?)': BinaryExpression(
|
||||||
|
Tuple(expressions: [
|
||||||
|
for (var i = 1; i <= 3; i++)
|
||||||
|
NumericLiteral(i, token(TokenType.numberLiteral)),
|
||||||
|
]),
|
||||||
|
token(TokenType.more),
|
||||||
|
Tuple(expressions: [
|
||||||
|
NumberedVariable(QuestionMarkVariableToken(fakeSpan('?'), null)),
|
||||||
|
NumberedVariable(QuestionMarkVariableToken(fakeSpan('?'), null)),
|
||||||
|
NumberedVariable(QuestionMarkVariableToken(fakeSpan('?'), null)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
import 'package:sqlparser/utils/find_referenced_tables.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
SqlEngine engine;
|
||||||
|
const schemaReader = SchemaFromCreateTable();
|
||||||
|
Table users, logins;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
engine = SqlEngine();
|
||||||
|
|
||||||
|
Table addTableFromStmt(String create) {
|
||||||
|
final parsed = engine.parse(create);
|
||||||
|
final table = schemaReader.read(parsed.rootNode as CreateTableStatement);
|
||||||
|
|
||||||
|
engine.registerTable(table);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
users = addTableFromStmt('''
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
);
|
||||||
|
''');
|
||||||
|
|
||||||
|
logins = addTableFromStmt('''
|
||||||
|
CREATE TABLE logins (
|
||||||
|
user INTEGER NOT NULL REFERENCES users (id),
|
||||||
|
timestamp INT
|
||||||
|
);
|
||||||
|
''');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognizes read tables', () {
|
||||||
|
final ctx = engine.analyze('SELECT * FROM logins INNER JOIN users u '
|
||||||
|
'ON u.id = logins.user;');
|
||||||
|
expect(findReferencedTables(ctx.root), {users, logins});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves aliased tables', () {
|
||||||
|
final ctx = engine.analyze('''
|
||||||
|
CREATE TRIGGER foo AFTER INSERT ON users BEGIN
|
||||||
|
INSERT INTO logins (user, timestamp) VALUES (new.id, 0);
|
||||||
|
END;
|
||||||
|
''');
|
||||||
|
final body = (ctx.root as CreateTriggerStatement).action;
|
||||||
|
|
||||||
|
// Users referenced via "new" in body.
|
||||||
|
expect(findReferencedTables(body), contains(users));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognizes written tables', () {
|
||||||
|
final ctx = engine.analyze('INSERT INTO logins '
|
||||||
|
'SELECT id, CURRENT_TIME FROM users;');
|
||||||
|
expect(
|
||||||
|
findWrittenTables(ctx.root), {TableWrite(logins, UpdateKind.insert)});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue