Merge branch 'develop'

# Conflicts:
#	moor/CHANGELOG.md
#	moor/pubspec.yaml
#	moor/test/batch_test.dart
This commit is contained in:
Simon Binder 2020-05-18 21:16:43 +02:00
commit ec1072dce8
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
87 changed files with 1897 additions and 223 deletions

View File

@ -82,6 +82,10 @@ weight = 1
name = "GitHub"
weight = 110
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

View File

@ -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`
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!
* `data_class_to_companions` (defaults to `true`): Controls whether moor will write the `toCompanion` method in generated
data classes.
## Available extensions
@ -90,12 +92,14 @@ We currently support the following extensions:
## 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
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
You can disable some default moor features and reduce the amount of generated code with the following options:
- `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.
- `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

View File

@ -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
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
Since moor 2.4, type converters can also be used inside moor files.
@ -92,3 +134,16 @@ CREATE TABLE users (
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.

View File

@ -69,9 +69,9 @@ Future<void> writeShoppingCart(CartWithItems entry) {
.go();
// And write the new ones
await into(shoppingCartEntries).insertAll([
for (var item in entry.items) ShoppingCartEntry(shoppingCart: cart.id, item: item.id)
]);
for (final item in entry.items) {
await into(shoppingCartEntries).insert(ShoppingCartEntry(shoppingCart: cart.id, item: item.id));
}
});
}
```

View File

@ -181,6 +181,8 @@ Future<void> insertMultipleEntries() async{
TodosCompanion.insert(
title: 'Another entry',
content: 'More content',
// columns that aren't required for inserts are still wrapped in a Value:
category: Value(3),
),
// ...
]);

View File

@ -1,6 +1,6 @@
---
title: "Welcome to Moor"
linkTitle: "Documentation"
linkTitle: "Documentation & Guides"
weight: 20
menu:
main:

View File

@ -104,3 +104,10 @@ Firebase is a very good option when
- your data model can be expressed as documents instead of relations
- 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.

View File

@ -98,9 +98,12 @@ class Database extends _$Database {
/// It will be set in the onUpgrade callback. Null if no migration occurred
int schemaVersionChangedTo;
MigrationStrategy overrideMigration;
@override
MigrationStrategy get migration {
return MigrationStrategy(
return overrideMigration ??
MigrationStrategy(
onCreate: (m) async {
await m.createTable(users);
if (schemaVersion >= 2) {
@ -123,7 +126,8 @@ class Database extends _$Database {
// make sure that transactions can be used in the beforeOpen callback.
await transaction(() async {
await batch((batch) {
batch.insertAll(users, [people.dash, people.duke, people.gopher]);
batch.insertAll(
users, [people.dash, people.duke, people.gopher]);
});
});
}

View File

@ -1,6 +1,7 @@
import 'package:test/test.dart';
import 'package:tests/data/sample_data.dart' as people;
import 'package:tests/database/database.dart';
import 'package:tests/tests.dart';
import 'suite.dart';
@ -31,6 +32,19 @@ void migrationTests(TestExecutor executor) {
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 {
var database = Database(executor.createConnection(), schemaVersion: 2);
await database.executor.ensureOpen(database); // Create the database

View File

@ -24,4 +24,10 @@ void runAllTests(TestExecutor executor) {
migrationTests(executor);
customObjectTests(executor);
transactionTests(executor);
test('can close database without interacting with it', () async {
final connection = executor.createConnection();
await connection.executor.close();
});
}

View File

@ -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
- Fix update statements not escaping column names ([#539](https://github.com/simolus3/moor/issues/539))

View File

@ -34,6 +34,15 @@ class Category extends DataClass implements Insertable<Category> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -230,6 +239,20 @@ class Recipe extends DataClass implements Insertable<Recipe> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -481,6 +504,16 @@ class Ingredient extends DataClass implements Insertable<Ingredient> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -708,6 +741,19 @@ class IngredientInRecipe extends DataClass
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;

View File

@ -81,3 +81,33 @@ Expression<num> sqlAcos(Expression<num> value) {
Expression<num> sqlAtan(Expression<num> 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),
]);
}
}

View File

@ -62,6 +62,14 @@ abstract class Table {
@protected
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.
/// Example (inside the body of a table class):
/// ```

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
@ -59,6 +60,25 @@ abstract class DataClass {
abstract class UpdateCompanion<D extends DataClass> implements Insertable<D> {
/// Constant constructor so that generated companion classes can be constant.
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.

View File

@ -291,7 +291,12 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
@override
Future<void> 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
QueryDelegate get impl => _base.impl;
@override
bool get logStatements => _base.logStatements;
}

View File

@ -15,6 +15,9 @@ class Variable<T> extends Expression<T> {
@override
Precedence get precedence => Precedence.primary;
@override
int get hashCode => value.hashCode;
/// Constructs a new variable from the [value].
const Variable(this.value);
@ -68,6 +71,11 @@ class Variable<T> extends Expression<T> {
@override
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

View File

@ -176,6 +176,26 @@ class Migrator {
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
/// escape the [name] parameter.
Future<void> deleteTable(String name) async {
@ -220,3 +240,36 @@ class OpeningDetails {
/// Used internally by moor when opening a database.
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();
},
);
}
}

View File

@ -68,9 +68,10 @@ mixin TableInfo<TableDsl extends Table, D extends DataClass> on Table
'evaluated by a database engine.');
}
final context = GenerationContext(SqlTypeSystem.defaultInstance, null);
final rawValues = asColumnMap
.cast<String, Variable>()
.map((key, value) => MapEntry(key, value.value));
.map((key, value) => MapEntry(key, value.mapToSimpleValue(context)));
return map(rawValues);
}

View File

@ -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].
///
/// 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) {
assert(
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
/// 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 {
final ctx = constructQuery();

View File

@ -20,3 +20,23 @@ abstract class TypeConverter<D, S> {
/// nullable.
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;
}
}

View File

@ -1,6 +1,6 @@
name: moor
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
homepage: https://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues

View File

@ -52,7 +52,7 @@ void main() {
'INSERT INTO todos (content) VALUES (?)',
'UPDATE users SET 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 todos WHERE id = ?;',
],

View File

@ -11,7 +11,12 @@ class Config extends DataClass implements Insertable<Config> {
final String configKey;
final String configValue;
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,
{String prefix}) {
final effectivePrefix = prefix ?? '';
@ -24,6 +29,9 @@ class Config extends DataClass implements Insertable<Config> {
.mapFromDatabaseResponse(data['${effectivePrefix}config_value']),
syncState: ConfigTable.$converter0.mapToDart(intType
.mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])),
syncStateImplicit: ConfigTable.$converter1.mapToDart(
intType.mapFromDatabaseResponse(
data['${effectivePrefix}sync_state_implicit'])),
);
}
@override
@ -39,9 +47,31 @@ class Config extends DataClass implements Insertable<Config> {
final converter = ConfigTable.$converter0;
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;
}
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -49,6 +79,8 @@ class Config extends DataClass implements Insertable<Config> {
configKey: serializer.fromJson<String>(json['config_key']),
configValue: serializer.fromJson<String>(json['config_value']),
syncState: serializer.fromJson<SyncType>(json['sync_state']),
syncStateImplicit:
serializer.fromJson<SyncType>(json['sync_state_implicit']),
);
}
factory Config.fromJsonString(String encodedJson,
@ -62,71 +94,88 @@ class Config extends DataClass implements Insertable<Config> {
'config_key': serializer.toJson<String>(configKey),
'config_value': serializer.toJson<String>(configValue),
'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(
configKey: configKey ?? this.configKey,
configValue: configValue ?? this.configValue,
syncState: syncState ?? this.syncState,
syncStateImplicit: syncStateImplicit ?? this.syncStateImplicit,
);
@override
String toString() {
return (StringBuffer('Config(')
..write('configKey: $configKey, ')
..write('configValue: $configValue, ')
..write('syncState: $syncState')
..write('syncState: $syncState, ')
..write('syncStateImplicit: $syncStateImplicit')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(
configKey.hashCode, $mrjc(configValue.hashCode, syncState.hashCode)));
configKey.hashCode,
$mrjc(configValue.hashCode,
$mrjc(syncState.hashCode, syncStateImplicit.hashCode))));
@override
bool operator ==(dynamic other) =>
identical(this, other) ||
(other is Config &&
other.configKey == this.configKey &&
other.configValue == this.configValue &&
other.syncState == this.syncState);
other.syncState == this.syncState &&
other.syncStateImplicit == this.syncStateImplicit);
}
class ConfigCompanion extends UpdateCompanion<Config> {
final Value<String> configKey;
final Value<String> configValue;
final Value<SyncType> syncState;
final Value<SyncType> syncStateImplicit;
const ConfigCompanion({
this.configKey = const Value.absent(),
this.configValue = const Value.absent(),
this.syncState = const Value.absent(),
this.syncStateImplicit = const Value.absent(),
});
ConfigCompanion.insert({
@required String configKey,
this.configValue = const Value.absent(),
this.syncState = const Value.absent(),
this.syncStateImplicit = const Value.absent(),
}) : configKey = Value(configKey);
static Insertable<Config> custom({
Expression<String> configKey,
Expression<String> configValue,
Expression<int> syncState,
Expression<int> syncStateImplicit,
}) {
return RawValuesInsertable({
if (configKey != null) 'config_key': configKey,
if (configValue != null) 'config_value': configValue,
if (syncState != null) 'sync_state': syncState,
if (syncStateImplicit != null) 'sync_state_implicit': syncStateImplicit,
});
}
ConfigCompanion copyWith(
{Value<String> configKey,
Value<String> configValue,
Value<SyncType> syncState}) {
Value<SyncType> syncState,
Value<SyncType> syncStateImplicit}) {
return ConfigCompanion(
configKey: configKey ?? this.configKey,
configValue: configValue ?? this.configValue,
syncState: syncState ?? this.syncState,
syncStateImplicit: syncStateImplicit ?? this.syncStateImplicit,
);
}
@ -143,6 +192,11 @@ class ConfigCompanion extends UpdateCompanion<Config> {
final converter = ConfigTable.$converter0;
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;
}
}
@ -177,8 +231,19 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
$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
List<GeneratedColumn> get $columns => [configKey, configValue, syncState];
List<GeneratedColumn> get $columns =>
[configKey, configValue, syncState, syncStateImplicit];
@override
ConfigTable get asDslTable => this;
@override
@ -203,6 +268,7 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
data['config_value'], _configValueMeta));
}
context.handle(_syncStateMeta, const VerificationResult.success());
context.handle(_syncStateImplicitMeta, const VerificationResult.success());
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> $converter1 =
const EnumIndexConverter<SyncType>(SyncType.values);
@override
bool get dontWriteConstraints => true;
}
@ -250,6 +318,13 @@ class WithDefault extends DataClass implements Insertable<WithDefault> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -415,6 +490,14 @@ class NoId extends DataClass implements Insertable<NoId> {
return map;
}
NoIdsCompanion toCompanion(bool nullToAbsent) {
return NoIdsCompanion(
payload: payload == null && nullToAbsent
? const Value.absent()
: Value(payload),
);
}
factory NoId.fromJson(Map<String, dynamic> json,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -569,6 +652,14 @@ class WithConstraint extends DataClass implements Insertable<WithConstraint> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -786,6 +877,22 @@ class MytableData extends DataClass implements Insertable<MytableData> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -1024,6 +1131,16 @@ class EMail extends DataClass implements Insertable<EMail> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -1237,6 +1354,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
configKey: row.readString('config_key'),
configValue: row.readString('config_value'),
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'),
configValue: row.readString('config_value'),
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 configValue;
final SyncType syncState;
final SyncType syncStateImplicit;
ReadRowIdResult({
this.rowid,
this.configKey,
this.configValue,
this.syncState,
this.syncStateImplicit,
});
@override
int get hashCode => $mrjf($mrjc(
rowid.hashCode,
$mrjc(configKey.hashCode,
$mrjc(configValue.hashCode, syncState.hashCode))));
$mrjc(
configKey.hashCode,
$mrjc(configValue.hashCode,
$mrjc(syncState.hashCode, syncStateImplicit.hashCode)))));
@override
bool operator ==(dynamic other) =>
identical(this, other) ||
@ -1436,5 +1561,6 @@ class ReadRowIdResult {
other.rowid == this.rowid &&
other.configKey == this.configKey &&
other.configValue == this.configValue &&
other.syncState == this.syncState);
other.syncState == this.syncState &&
other.syncStateImplicit == this.syncStateImplicit);
}

View File

@ -20,7 +20,8 @@ CREATE TABLE with_constraints (
create table config (
config_key TEXT not null primary key,
config_value TEXT,
sync_state INTEGER MAPPED BY `const SyncTypeConverter()`
sync_state INTEGER MAPPED BY `const SyncTypeConverter()`,
sync_state_implicit ENUM(SyncType)
) AS "Config";
CREATE INDEX IF NOT EXISTS value_idx ON config (config_value);

View File

@ -33,8 +33,12 @@ class Users extends Table with AutoIncrement {
class Categories extends Table with AutoIncrement {
TextColumn get description =>
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 {
IntColumn get todo => integer()();
IntColumn get user => integer()();

View File

@ -58,6 +58,23 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -319,7 +336,9 @@ class $TodosTableTable extends TodosTable
class Category extends DataClass implements Insertable<Category> {
final int id;
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,
{String prefix}) {
final effectivePrefix = prefix ?? '';
@ -329,6 +348,8 @@ class Category extends DataClass implements Insertable<Category> {
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
description:
stringType.mapFromDatabaseResponse(data['${effectivePrefix}desc']),
priority: $CategoriesTable.$converter0.mapToDart(
intType.mapFromDatabaseResponse(data['${effectivePrefix}priority'])),
);
}
@override
@ -340,15 +361,32 @@ class Category extends DataClass implements Insertable<Category> {
if (!nullToAbsent || description != null) {
map['desc'] = Variable<String>(description);
}
if (!nullToAbsent || priority != null) {
final converter = $CategoriesTable.$converter0;
map['priority'] = Variable<int>(converter.mapToSql(priority));
}
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return Category(
id: serializer.fromJson<int>(json['id']),
description: serializer.fromJson<String>(json['description']),
priority: serializer.fromJson<CategoryPriority>(json['priority']),
);
}
factory Category.fromJsonString(String encodedJson,
@ -362,57 +400,72 @@ class Category extends DataClass implements Insertable<Category> {
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'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,
description: description ?? this.description,
priority: priority ?? this.priority,
);
@override
String toString() {
return (StringBuffer('Category(')
..write('id: $id, ')
..write('description: $description')
..write('description: $description, ')
..write('priority: $priority')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(id.hashCode, description.hashCode));
int get hashCode =>
$mrjf($mrjc(id.hashCode, $mrjc(description.hashCode, priority.hashCode)));
@override
bool operator ==(dynamic other) =>
identical(this, other) ||
(other is Category &&
other.id == this.id &&
other.description == this.description);
other.description == this.description &&
other.priority == this.priority);
}
class CategoriesCompanion extends UpdateCompanion<Category> {
final Value<int> id;
final Value<String> description;
final Value<CategoryPriority> priority;
const CategoriesCompanion({
this.id = const Value.absent(),
this.description = const Value.absent(),
this.priority = const Value.absent(),
});
CategoriesCompanion.insert({
this.id = const Value.absent(),
@required String description,
this.priority = const Value.absent(),
}) : description = Value(description);
static Insertable<Category> custom({
Expression<int> id,
Expression<String> description,
Expression<int> priority,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
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(
id: id ?? this.id,
description: description ?? this.description,
priority: priority ?? this.priority,
);
}
@ -425,6 +478,10 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
if (description.present) {
map['desc'] = Variable<String>(description.value);
}
if (priority.present) {
final converter = $CategoriesTable.$converter0;
map['priority'] = Variable<int>(converter.mapToSql(priority.value));
}
return map;
}
}
@ -454,8 +511,17 @@ class $CategoriesTable extends Categories
$customConstraints: 'NOT NULL UNIQUE');
}
final VerificationMeta _priorityMeta = const VerificationMeta('priority');
GeneratedIntColumn _priority;
@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
$CategoriesTable get asDslTable => this;
@override
@ -476,6 +542,7 @@ class $CategoriesTable extends Categories
} else if (isInserting) {
context.missing(_descriptionMeta);
}
context.handle(_priorityMeta, const VerificationResult.success());
return context;
}
@ -491,6 +558,9 @@ class $CategoriesTable extends Categories
$CategoriesTable createAlias(String alias) {
return $CategoriesTable(_db, alias);
}
static TypeConverter<CategoryPriority, int> $converter0 =
const EnumIndexConverter<CategoryPriority>(CategoryPriority.values);
}
class User extends DataClass implements Insertable<User> {
@ -545,6 +615,22 @@ class User extends DataClass implements Insertable<User> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -828,6 +914,13 @@ class SharedTodo extends DataClass implements Insertable<SharedTodo> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -1027,6 +1120,19 @@ class TableWithoutPKData extends DataClass
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
@ -1252,6 +1358,13 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
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,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;

View File

@ -71,6 +71,45 @@ void main() {
expect(recovered.isAwesome, user.isAwesome);
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 {

View File

@ -1,7 +1,10 @@
@TestOn('vm')
import 'package:moor/extensions/moor_ffi.dart';
import 'package:moor/src/runtime/query_builder/query_builder.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:test/test.dart';
import '../data/tables/todos.dart';
import '../data/utils/expect_generated.dart';
void main() {
@ -19,4 +22,38 @@ void main() {
test('asin', () => expect(sqlAsin(a), generates('asin(a)')));
test('acos', () => expect(sqlAcos(a), generates('acos(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),
);
});
}

View File

@ -218,4 +218,16 @@ void main() {
));
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],
));
});
}

View File

@ -23,7 +23,8 @@ void main() {
verify(executor.runSelect(
'SELECT t.id AS "t.id", t.title AS "t.title", '
'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;',
argThat(isEmpty)));
});
@ -43,6 +44,7 @@ void main() {
't.category': 3,
'c.id': 3,
'c.desc': 'description',
'c.priority': 2,
}
]);
});
@ -65,7 +67,13 @@ void main() {
));
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));
});
@ -167,20 +175,29 @@ void main() {
when(executor.runSelect(any, any)).thenAnswer((_) async {
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();
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;',
[],
));
expect(result.readTable(categories),
equals(Category(id: 3, description: 'Description')));
expect(
result.readTable(categories),
equals(
Category(
id: 3,
description: 'Description',
priority: CategoryPriority.medium,
),
),
);
expect(result.read(descriptionLength), 11);
});
@ -205,20 +222,28 @@ void main() {
when(executor.runSelect(any, any)).thenAnswer((_) async {
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();
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 '
'GROUP BY c.id HAVING COUNT(t.id) >= ?;',
[10]));
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);
});

View File

@ -19,7 +19,8 @@ const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS with_constraints ('
const _createConfig = 'CREATE TABLE IF NOT EXISTS config ('
'config_key VARCHAR not null primary key, '
'config_value VARCHAR, '
'sync_state INTEGER);';
'sync_state INTEGER, '
'sync_state_implicit INTEGER);';
const _createMyTable = 'CREATE TABLE IF NOT EXISTS mytable ('
'someid INTEGER NOT NULL PRIMARY KEY, '

View File

@ -28,7 +28,8 @@ void main() {
verify(mockExecutor.runCustom(
'CREATE TABLE IF NOT EXISTS categories '
'(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(
@ -81,6 +82,18 @@ void main() {
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 {
await db.createMigrator().addColumn(db.users, db.users.isAwesome);

View File

@ -156,4 +156,27 @@ void main() {
..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,
),
);
});
}

View File

@ -39,7 +39,8 @@ void main() {
id: Value(3),
name: Value('hi'),
profilePicture: Value.absent(),
isAwesome: Value(true),
// false for https://github.com/simolus3/moor/issues/559
isAwesome: Value(false),
);
final user = db.users.mapFromCompanion(companion);
@ -49,7 +50,7 @@ void main() {
id: 3,
name: 'hi',
profilePicture: null,
isAwesome: true,
isAwesome: false,
creationTime: null,
),
);

View File

@ -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
- Provide mathematical functions in sql (`pow`, `power`, `sin`, `cos`, `tan`, `asin`, `atan`, `acos`, `sqrt`)

View File

@ -93,6 +93,33 @@ void _regexpImpl(Pointer<FunctionContext> ctx, int argCount,
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) {
final powImplPointer =
Pointer.fromFunction<sqlite3_function_handler>(_powImpl);
@ -117,4 +144,11 @@ void _registerOn(Database db) {
db.createFunction('regexp', 2, Pointer.fromFunction(_regexpImpl),
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);
}

View File

@ -1,5 +1,6 @@
import 'dart:ffi';
import 'dart:io';
import 'dart:math';
import 'package:meta/meta.dart';
@ -23,7 +24,25 @@ final OpenDynamicLibrary open = OpenDynamicLibrary._();
DynamicLibrary _defaultOpen() {
if (Platform.isLinux || Platform.isAndroid) {
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) {
// todo: Consider including sqlite3 in the build and use DynamicLibrary.

View File

@ -1,6 +1,6 @@
name: moor_ffi
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
issue_tracker: https://github.com/simolus3/moor/issues

View File

@ -9,15 +9,19 @@ void main() {
setUp(() => db = Database.memory()..enableMoorFfiFunctions());
tearDown(() => db.close());
group('pow', () {
dynamic _resultOfPow(String a, String b) {
final stmt = db.prepare('SELECT pow($a, $b) AS r;');
dynamic selectSingle(String expression) {
final stmt = db.prepare('SELECT $expression AS r;');
final rows = stmt.select();
stmt.close();
return rows.single['r'];
}
group('pow', () {
dynamic _resultOfPow(String a, String b) {
return selectSingle('pow($a, $b)');
}
test('returns null when any argument is null', () {
expect(_resultOfPow('null', 'null'), isNull);
expect(_resultOfPow('3', 'null'), isNull);
@ -105,6 +109,26 @@ void main() {
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

View File

@ -1,3 +1,7 @@
## 3.1.0
- Respect foreign key constraints when calculating the stream update graph
## 3.0.0
Generate code for moor 3.0. This most notably includes custom companions and nested result sets.

View File

@ -1,6 +1,7 @@
part of 'parser.dart';
const String startInt = 'integer';
const String startEnum = 'intEnum';
const String startString = 'text';
const String startBool = 'boolean';
const String startDateTime = 'dateTime';
@ -9,6 +10,7 @@ const String startReal = 'real';
const Set<String> starters = {
startInt,
startEnum,
startString,
startBool,
startDateTime,
@ -185,6 +187,28 @@ class ColumnParser {
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) {
base.step.reportError(
ErrorInDartCode(
@ -217,6 +241,7 @@ class ColumnParser {
startBool: ColumnType.boolean,
startString: ColumnType.text,
startInt: ColumnType.integer,
startEnum: ColumnType.integer,
startDateTime: ColumnType.datetime,
startBlob: ColumnType.blob,
startReal: ColumnType.real,

View File

@ -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/src/analyzer/errors.dart';
import 'package:analyzer/dart/element/type.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/backends/backend.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/utils/names.dart';
@ -15,18 +18,25 @@ class CreateTableReader {
/// The AST of this `CREATE TABLE` statement.
final TableInducingStatement stmt;
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 {
Table table;
try {
table = SchemaFromCreateTable(moorExtensions: true).read(stmt);
table = _schemaReader.read(stmt);
} catch (e) {
step.reportError(ErrorInMoorFile(
span: stmt.tableNameToken.span,
message: 'Could not extract schema information for this table: $e',
));
return null;
}
final foundColumns = <String, MoorColumn>{};
@ -43,6 +53,33 @@ class CreateTableReader {
String defaultValue;
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
// can't read the constraints.
final constraints = column.hasDefinition
@ -64,6 +101,18 @@ class CreateTableReader {
}
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);
// don't write MAPPED BY constraints when creating the table, they're
// a convenience feature by the compiler
@ -156,4 +205,30 @@ class CreateTableReader {
return UsedTypeConverter(
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;
}
}

View File

@ -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/runner/results.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/query_analyzer.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/find_referenced_tables.dart';
/// Handles `REFERENCES` clauses in tables by resolving their columns and
/// reporting errors if they don't exist. Further, sets the

View File

@ -27,7 +27,8 @@ class MoorParser {
final importStmt = parsedStmt;
importStatements.add(importStmt);
} else if (parsedStmt is TableInducingStatement) {
createdReaders.add(CreateTableReader(parsedStmt, step));
createdReaders
.add(CreateTableReader(parsedStmt, step, importStatements));
} else if (parsedStmt is CreateTriggerStatement) {
// the table will be resolved in the analysis step
createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file));
@ -62,7 +63,10 @@ class MoorParser {
}
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(

View File

@ -65,11 +65,14 @@ class MoorOptions {
@JsonKey(name: 'eagerly_load_dart_ast', defaultValue: false)
final bool eagerlyLoadDartAst;
@JsonKey(name: 'data_class_to_companions', defaultValue: true)
final bool dataClassToCompanions;
/// Whether the [module] has been enabled in this configuration.
bool hasModule(SqlModule module) => modules.contains(module);
const MoorOptions(
{this.generateFromJsonStringConstructor = false,
const MoorOptions({
this.generateFromJsonStringConstructor = false,
this.overrideHashAndEqualsInResultSets = false,
this.compactQueryMethods = false,
this.skipVerificationCode = false,
@ -78,7 +81,9 @@ class MoorOptions {
this.generateConnectConstructor = false,
this.legacyTypeInference = false,
this.eagerlyLoadDartAst = false,
this.modules = const []});
this.dataClassToCompanions = true,
this.modules = const [],
});
factory MoorOptions.fromJson(Map<String, dynamic> json) =>
_$MoorOptionsFromJson(json);

View File

@ -18,7 +18,8 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
'generate_connect_constructor',
'legacy_type_inference',
'sqlite_modules',
'eagerly_load_dart_ast'
'eagerly_load_dart_ast',
'data_class_to_companions'
]);
final val = MoorOptions(
generateFromJsonStringConstructor: $checkedConvert(
@ -50,6 +51,9 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
eagerlyLoadDartAst:
$checkedConvert(json, 'eagerly_load_dart_ast', (v) => v as bool) ??
false,
dataClassToCompanions:
$checkedConvert(json, 'data_class_to_companions', (v) => v as bool) ??
true,
modules: $checkedConvert(
json,
'sqlite_modules',
@ -69,8 +73,9 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
'useColumnNameAsJsonKeyWhenDefinedInMoorFile':
'use_column_name_as_json_key_when_defined_in_moor_file',
'generateConnectConstructor': 'generate_connect_constructor',
'useExperimentalInference': 'use_experimental_inference',
'legacyTypeInference': 'legacy_type_inference',
'eagerlyLoadDartAst': 'eagerly_load_dart_ast',
'dataClassToCompanions': 'data_class_to_companions',
'modules': 'sqlite_modules'
});
}

View File

@ -111,7 +111,7 @@ class FoundFile {
FileState state = FileState.dirty;
final ErrorSink errors = ErrorSink();
FoundFile(this.uri, this.type);
FoundFile(this.uri, this.type) : assert(uri.isAbsolute);
String get shortName => uri.pathSegments.last;

View File

@ -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/utils/type_converter_hint.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:sqlparser/utils/find_referenced_tables.dart';
import 'affected_tables_visitor.dart';
import 'lints/linter.dart';
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this

View File

@ -1,8 +1,9 @@
import 'package:moor/moor.dart' as m;
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/utils/type_converter_hint.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
/// library.
@ -224,7 +225,13 @@ class TypeMapper {
return _engineTablesToSpecified[table];
}
WrittenMoorTable writtenToMoor(WrittenTable table) {
return WrittenMoorTable(tableToMoor(table.table), table.kind);
WrittenMoorTable writtenToMoor(s.TableWrite table) {
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);
}
}

View File

@ -15,6 +15,9 @@ const _ignoredLints = [
'lines_longer_than_80_chars',*/
];
const _targetMajorVersion = 2;
const _targetMinorVersion = 6;
class MoorGenerator extends Generator implements BaseGenerator {
@override
MoorBuilder builder;
@ -33,6 +36,23 @@ class MoorGenerator extends Generator implements BaseGenerator {
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();
}
}

View File

@ -1,4 +1,7 @@
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:moor_generator/src/backends/backend.dart';
@ -14,7 +17,7 @@ class CommonBackend extends Backend {
final absolute = driver.absolutePath(Uri.parse(import), base: base);
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);
}
@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
Future<bool> exists(Uri uri) {
return Future.value(driver.doesFileExist(uri.path));

View File

@ -108,7 +108,7 @@ class MoorDriver implements AnalysisDriverGeneric {
session.notifyTaskFinished(task);
} catch (e, s) {
Logger.root.warning(
'Error while working on ${mostImportantFile.file.uri}', e, s);
'Error while working on ${mostImportantFile.file.uri}: ', e, s);
_tracker.removePending(mostImportantFile);
}
}

View File

@ -12,9 +12,11 @@ class MoorCompletingContributor implements CompletionContributor {
final autoComplete = request.parsedMoor.parseResult.autoCompleteEngine;
final results = autoComplete.suggestCompletions(request.offset);
// todo: Fix calculation in sqlparser. Then, set offset to results.anchor
// and length to results.lengthBefore
collector
..offset = results.anchor
..length = results.lengthBefore;
..offset = request.offset // should be results.anchor
..length = 0; // should be results.lengthBefore
for (final suggestion in results.suggestions) {
collector.addSuggestion(CompletionSuggestion(

View File

@ -102,10 +102,12 @@ class _NavigationVisitor extends RecursiveVisitor<void, void> {
if (resolved is Table && resolved != null) {
final declaration = resolved.meta<MoorTable>()?.declaration;
if (declaration != null) {
_reportForSpan(
e.span, ElementKind.CLASS, locationOfDeclaration(declaration));
}
}
}
visitChildren(e, arg);
}

View File

@ -1,3 +1,4 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:meta/meta.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.
String get fieldName => '\$converter$index';
UsedTypeConverter(
{@required this.expression,
UsedTypeConverter({
@required this.expression,
@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';
}
}

View File

@ -10,7 +10,69 @@ class FindStreamUpdateRules {
StreamQueryUpdateRules identifyRules() {
final rules = <UpdateRule>[];
for (final trigger in db.entities.whereType<MoorTrigger>()) {
for (final entity in db.entities) {
if (entity is MoorTrigger) {
_writeRulesForTrigger(entity, rules);
} else if (entity is MoorTable) {
_writeRulesForTable(entity, 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) {
@ -34,7 +96,4 @@ class FindStreamUpdateRules {
),
);
}
return StreamQueryUpdateRules(rules);
}
}

View File

@ -40,6 +40,9 @@ class DataClassWriter {
_writeMappingConstructor();
_writeToColumnsOverride();
if (scope.options.dataClassToCompanions) {
_writeToCompanion();
}
// And a serializer and deserializer method
_writeFromJson();
@ -214,6 +217,30 @@ class DataClassWriter {
_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() {
/*
@override

View File

@ -1,6 +1,6 @@
name: moor_generator
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
homepage: https://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues
@ -23,7 +23,7 @@ dependencies:
# Moor-specific analysis
moor: ^3.0.0
sqlparser: ^0.8.0
sqlparser: ^0.9.0
# Dart analysis
analyzer: '^0.39.0'

View File

@ -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')))),
);
});
}

View File

@ -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)'),
),
);
});
}

View File

@ -28,8 +28,8 @@ CREATE TABLE bar (
Future<void> main() async {
final mapper = TypeMapper();
final engine = SqlEngine(EngineOptions(useMoorExtensions: true));
final step = ParseMoorStep(
Task(null, null, null), FoundFile(Uri.parse('foo'), FileType.moor), '');
final step = ParseMoorStep(Task(null, null, null),
FoundFile(Uri.parse('file://foo'), FileType.moor), '');
final parsedFoo = engine.parse(createFoo).rootNode as CreateTableStatement;
final foo = await CreateTableReader(parsedFoo, step).extractTable(mapper);

View File

@ -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();
});
}

View File

@ -5,10 +5,10 @@ import 'package:test/test.dart';
void main() {
FileTracker tracker;
final fa = FoundFile(Uri.parse('a'), FileType.dartLibrary);
final fb = FoundFile(Uri.parse('b'), FileType.dartLibrary);
final fc = FoundFile(Uri.parse('c'), FileType.dartLibrary);
final fd = FoundFile(Uri.parse('d'), FileType.dartLibrary);
final fa = FoundFile(Uri.parse('file://a'), FileType.dartLibrary);
final fb = FoundFile(Uri.parse('file://b'), FileType.dartLibrary);
final fc = FoundFile(Uri.parse('file://c'), FileType.dartLibrary);
final fd = FoundFile(Uri.parse('file://d'), FileType.dartLibrary);
setUp(() {
tracker = FileTracker();

View File

@ -48,4 +48,88 @@ class MyDatabase {}
{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));
});
}

View File

@ -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
- Support collate expressions in the new type inference ([#533](https://github.com/simolus3/moor/issues/533))

View File

@ -64,8 +64,8 @@ final context =
final select = context.root as SelectStatement;
final resolvedColumns = select.resolvedColumns;
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) => c.name); // id, content, id, content, 3 + 4
resolvedColumns.map((c) => context.typeOf(c).type.type); // int, text, int, text, int, int
```
## But why?

View File

@ -4,10 +4,36 @@ import 'package:sqlparser/sqlparser.dart';
// prints what columns would be returned by that statement.
void main() {
final engine = SqlEngine()
..registerTable(frameworks)
..registerTable(languages)
..registerTable(frameworkToLanguage);
..registerTableFromSql(
'''
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('''
SELECT f.* FROM frameworks f
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
// that the analyzer knows what's going on.
final Table frameworks = Table(
name: 'frameworks',
resolvedColumns: [
TableColumn(
'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),
),
],
);
extension on SqlEngine {
/// Utility function that parses a `CREATE TABLE` statement and registers the
/// created table to the engine.
void registerTableFromSql(String createTable) {
final stmt = parse(createTable).rootNode as CreateTableStatement;
registerTable(schemaReader.read(stmt));
}
}

View File

@ -62,5 +62,6 @@ enum AnalysisErrorType {
compoundColumnCountMismatch,
cteColumnCountMismatch,
valuesSelectCountMismatch,
rowValueMisuse,
other,
}

View File

@ -6,7 +6,7 @@ class SchemaFromCreateTable {
/// and `DATETIME` columns.
final bool moorExtensions;
SchemaFromCreateTable({this.moorExtensions = false});
const SchemaFromCreateTable({this.moorExtensions = false});
Table read(TableInducingStatement stmt) {
if (stmt is CreateTableStatement) {
@ -30,9 +30,7 @@ class SchemaFromCreateTable {
}
TableColumn _readColumn(ColumnDefinition definition) {
final typeName = definition.typeName.toUpperCase();
final type = resolveColumnType(typeName);
final type = resolveColumnType(definition.typeName);
final nullable = !definition.constraints.any((c) => c is NotNull);
final resolvedType = type.withNullable(nullable);
@ -49,7 +47,7 @@ class SchemaFromCreateTable {
/// [IsDateTime] hints if the type name contains `BOOL` or `DATE`,
/// respectively.
/// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
ResolvedType resolveColumnType(String typeName) {
ResolvedType resolveColumnType(String /*?*/ typeName) {
if (typeName == null) {
return const ResolvedType(type: BasicType.blob);
}
@ -75,6 +73,10 @@ class SchemaFromCreateTable {
if (upper.contains('DATE')) {
return const ResolvedType(type: BasicType.int, hint: IsDateTime());
}
if (upper.contains('ENUM')) {
return const ResolvedType(type: BasicType.int);
}
}
return const ResolvedType(type: BasicType.real);

View File

@ -18,6 +18,67 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
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
void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
final expectedColumns = e.resolvedColumns.length;

View File

@ -105,6 +105,10 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
// acts like a table for expressions in the same scope, so let's
// register it.
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);
}
},

View File

@ -2,14 +2,19 @@ part of '../ast.dart';
/// A tuple of values, denotes in brackets. `(<expr>, ..., <expr>)`.
///
/// Notice that this class extends [Expression] because the type inference
/// algorithm works best when tuples are treated as expressions. Syntactically,
/// tuples aren't expressions.
/// In sqlite, this is also called a "row value".
class Tuple extends Expression {
/// The expressions appearing in this tuple.
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
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {

View File

@ -3,7 +3,7 @@ part of '../ast.dart';
/// https://www.sqlite.org/syntax/column-def.html
class ColumnDefinition extends AstNode {
final String columnName;
final String typeName;
final String /*?*/ typeName;
final List<ColumnConstraint> constraints;
/// The tokens there were involved in defining the type of this column.

View File

@ -301,6 +301,20 @@ mixin ExpressionParser on ParserBase {
return SubQuery(select: selectStmt)..setSpan(left, _previous);
} else {
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');
return Parentheses(left, expr, _previous)..setSpan(left, _previous);
}

View File

@ -32,3 +32,19 @@ extension SyntacticLengthExtension on SyntacticEntity {
/// The length of this entity, in characters.
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),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:moor/moor.dart' show UpdateKind;
library utils.find_referenced_tables;
import 'package:sqlparser/sqlparser.dart';
/// 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;
/// What kind of update was found (e.g. insert, update or delete).
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
@ -56,12 +71,12 @@ class UpdatedTablesVisitor extends ReferencedTablesVisitor {
/// 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
/// SELECT * FROM bar`).
final Set<WrittenTable> writtenTables = {};
final Set<TableWrite> writtenTables = {};
void _addIfResolved(ResolvesToResultSet r, UpdateKind kind) {
final resolved = _toTableOrNull(r);
if (resolved != null) {
writtenTables.add(WrittenTable(resolved, kind));
writtenTables.add(TableWrite(resolved, kind));
}
}
@ -83,3 +98,34 @@ class UpdatedTablesVisitor extends ReferencedTablesVisitor {
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;
}

View File

@ -1,6 +1,6 @@
name: sqlparser
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://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues

View File

@ -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);
}
}

View File

@ -4,7 +4,7 @@ import 'package:test/test.dart';
void main() {
test('isAliasForRowId', () {
final engine = SqlEngine();
final schemaParser = SchemaFromCreateTable();
const schemaParser = SchemaFromCreateTable();
final isAlias = {
'CREATE TABLE x (id INTEGER PRIMARY KEY)': true,

View File

@ -36,7 +36,7 @@ const _affinityTests = {
void main() {
test('affinity from typename', () {
final resolver = SchemaFromCreateTable();
const resolver = SchemaFromCreateTable();
_affinityTests.forEach((key, value) {
expect(resolver.columnAffinity(key), equals(value),
@ -48,7 +48,8 @@ void main() {
final engine = SqlEngine();
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),
['id', 'email', 'score', 'display_name']);
@ -70,7 +71,7 @@ void main() {
)
''').rootNode;
final table = SchemaFromCreateTable(moorExtensions: true)
final table = const SchemaFromCreateTable(moorExtensions: true)
.read(stmt as CreateTableStatement);
expect(table.resolvedColumns.map((c) => c.type), const [
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: true),
@ -79,4 +80,12 @@ void main() {
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);
});
}

View File

@ -4,7 +4,7 @@ import 'package:test/test.dart';
void main() {
group('finds columns', () {
final engine = SqlEngine();
final schemaParser = SchemaFromCreateTable();
const schemaParser = SchemaFromCreateTable();
Column findWith(String createTbl, String columnName) {
final stmt = engine.parse(createTbl).rootNode as CreateTableStatement;

View File

@ -11,8 +11,8 @@ void main() {
final result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
"fts5(bar , tokenize = 'porter ascii')");
final table =
SchemaFromCreateTable().read(result.root as TableInducingStatement);
final table = const SchemaFromCreateTable()
.read(result.root as TableInducingStatement);
expect(table.name, 'foo');
final columns = table.resultColumns;
@ -24,8 +24,8 @@ void main() {
final result = engine
.analyze('CREATE VIRTUAL TABLE foo USING fts5(bar, baz UNINDEXED)');
final table =
SchemaFromCreateTable().read(result.root as TableInducingStatement);
final table = const SchemaFromCreateTable()
.read(result.root as TableInducingStatement);
expect(table.name, 'foo');
expect(table.resultColumns.map((c) => c.name), ['bar', 'baz']);
@ -39,7 +39,7 @@ void main() {
// add an fts5 table for the following queries
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
'fts5(bar, baz);');
engine.registerTable(SchemaFromCreateTable()
engine.registerTable(const SchemaFromCreateTable()
.read(fts5Result.root as TableInducingStatement));
});
@ -83,7 +83,7 @@ void main() {
// add an fts5 table for the following queries
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
'fts5(bar, baz);');
engine.registerTable(SchemaFromCreateTable()
engine.registerTable(const SchemaFromCreateTable()
.read(fts5Result.root as TableInducingStatement));
});
@ -129,11 +129,11 @@ void main() {
// add an fts5 table for the following queries
final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
'fts5(bar, baz);');
engine.registerTable(SchemaFromCreateTable()
engine.registerTable(const SchemaFromCreateTable()
.read(fts5Result.root as TableInducingStatement));
final normalResult = engine.analyze('CREATE TABLE other (bar TEXT);');
engine.registerTable(SchemaFromCreateTable()
engine.registerTable(const SchemaFromCreateTable()
.read(normalResult.root as TableInducingStatement));
});

View File

@ -148,6 +148,18 @@ final Map<String, Expression> _testCases = {
TimeConstantKind.currentTimestamp, token(TokenType.currentTimestamp)),
'CURRENT_DATE': TimeConstantLiteral(
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() {

View File

@ -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)});
});
}