mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into analyzer-plugin
# Conflicts: # moor_generator/lib/src/parser/moor/moor_analyzer.dart
This commit is contained in:
commit
b550afd68f
|
@ -88,6 +88,21 @@ Future feelingLazy() {
|
|||
__⚠️ Caution:__ If you don't explicitly add a `where` clause on updates or deletes,
|
||||
the statement will affect all rows in the table!
|
||||
|
||||
{{% alert title="Entries, companions - why do we need all of this?" %}}
|
||||
You might have noticed that we used a `TodosCompanion` for the first update instead of
|
||||
just passing a `TodoEntry`. Moor generates the `TodoEntry` class (also called _data
|
||||
class_ for the table) to hold a __full__ row with all its data. For _partial_ data,
|
||||
prefer to use companions. In the example above, we only set the the `category` column,
|
||||
so we used a companion.
|
||||
Why is that necessary? If a field was set to `null`, we wouldn't know whether we need
|
||||
to set that column back to null in the database or if we should just leave it unchanged.
|
||||
Fields in the companions have a special `Value.absent()` state which makes this explicit.
|
||||
|
||||
Companions also have a special constructor for inserts - all columns which don't have
|
||||
a default value and aren't nullable are marked `@required` on that constructor. This makes
|
||||
companions easier to use for inserts because you know which fields to set.
|
||||
{{% /alert %}}
|
||||
|
||||
## Inserts
|
||||
You can very easily insert any valid object into tables. As some values can be absent
|
||||
(like default values that we don't have to set explicitly), we again use the
|
||||
|
|
|
@ -159,7 +159,7 @@ packages:
|
|||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
version: "1.1.6"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -173,14 +173,14 @@ packages:
|
|||
path: "../../../moor"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.6.0"
|
||||
version: "1.7.1"
|
||||
moor_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../../moor_flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.6.0"
|
||||
version: "1.7.0"
|
||||
multi_server_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -8,6 +8,7 @@ void transactionTests(TestExecutor executor) {
|
|||
test('transactions write data', () async {
|
||||
final db = Database(executor.createExecutor());
|
||||
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
await db.transaction((_) async {
|
||||
final florianId = await db.writeUser(People.florian);
|
||||
|
||||
|
@ -30,6 +31,7 @@ void transactionTests(TestExecutor executor) {
|
|||
final db = Database(executor.createExecutor());
|
||||
|
||||
try {
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
await db.transaction((_) async {
|
||||
final florianId = await db.writeUser(People.florian);
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
## 1.7.2
|
||||
- Fixed a race condition that caused the database to be opened multiple times on slower devices.
|
||||
This problem was introduced in `1.7.0` and was causing problems during migrations.
|
||||
|
||||
## 1.7.1
|
||||
- Better documentation on `getSingle` and `watchSingle` for queries.
|
||||
- Fix `INTEGER NOT NULL PRIMARY KEY` wrongly requiring a value during insert (this never affected
|
||||
|
|
|
@ -76,6 +76,10 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
|
|||
this.id = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
});
|
||||
CategoriesCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
});
|
||||
CategoriesCompanion copyWith({Value<int> id, Value<String> description}) {
|
||||
return CategoriesCompanion(
|
||||
id: id ?? this.id,
|
||||
|
@ -266,6 +270,13 @@ class RecipesCompanion extends UpdateCompanion<Recipe> {
|
|||
this.instructions = const Value.absent(),
|
||||
this.category = const Value.absent(),
|
||||
});
|
||||
RecipesCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
@required String title,
|
||||
@required String instructions,
|
||||
this.category = const Value.absent(),
|
||||
}) : title = Value(title),
|
||||
instructions = Value(instructions);
|
||||
RecipesCompanion copyWith(
|
||||
{Value<int> id,
|
||||
Value<String> title,
|
||||
|
@ -482,6 +493,12 @@ class IngredientsCompanion extends UpdateCompanion<Ingredient> {
|
|||
this.name = const Value.absent(),
|
||||
this.caloriesPer100g = const Value.absent(),
|
||||
});
|
||||
IngredientsCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
@required String name,
|
||||
@required int caloriesPer100g,
|
||||
}) : name = Value(name),
|
||||
caloriesPer100g = Value(caloriesPer100g);
|
||||
IngredientsCompanion copyWith(
|
||||
{Value<int> id, Value<String> name, Value<int> caloriesPer100g}) {
|
||||
return IngredientsCompanion(
|
||||
|
@ -688,6 +705,13 @@ class IngredientInRecipesCompanion extends UpdateCompanion<IngredientInRecipe> {
|
|||
this.ingredient = const Value.absent(),
|
||||
this.amountInGrams = const Value.absent(),
|
||||
});
|
||||
IngredientInRecipesCompanion.insert({
|
||||
@required int recipe,
|
||||
@required int ingredient,
|
||||
@required int amountInGrams,
|
||||
}) : recipe = Value(recipe),
|
||||
ingredient = Value(ingredient),
|
||||
amountInGrams = Value(amountInGrams);
|
||||
IngredientInRecipesCompanion copyWith(
|
||||
{Value<int> recipe, Value<int> ingredient, Value<int> amountInGrams}) {
|
||||
return IngredientInRecipesCompanion(
|
||||
|
@ -805,15 +829,6 @@ class $IngredientInRecipesTable extends IngredientInRecipes
|
|||
}
|
||||
}
|
||||
|
||||
class TotalWeightResult {
|
||||
final String title;
|
||||
final int totalWeight;
|
||||
TotalWeightResult({
|
||||
this.title,
|
||||
this.totalWeight,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class _$Database extends GeneratedDatabase {
|
||||
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
|
||||
$CategoriesTable _categories;
|
||||
|
@ -832,25 +847,35 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Selectable<TotalWeightResult> _totalWeightQuery(
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelectQuery(
|
||||
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
|
||||
variables: [],
|
||||
readsFrom: {recipes, ingredientInRecipes}).map(_rowToTotalWeightResult);
|
||||
}
|
||||
|
||||
Future<List<TotalWeightResult>> _totalWeight(
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelect(
|
||||
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
|
||||
variables: []).then((rows) => rows.map(_rowToTotalWeightResult).toList());
|
||||
return _totalWeightQuery(operateOn: operateOn).get();
|
||||
}
|
||||
|
||||
Stream<List<TotalWeightResult>> _watchTotalWeight() {
|
||||
return customSelectStream(
|
||||
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
|
||||
variables: [],
|
||||
readsFrom: {
|
||||
recipes,
|
||||
ingredientInRecipes
|
||||
}).map((rows) => rows.map(_rowToTotalWeightResult).toList());
|
||||
return _totalWeightQuery().watch();
|
||||
}
|
||||
|
||||
@override
|
||||
List<TableInfo> get allTables =>
|
||||
[categories, recipes, ingredients, ingredientInRecipes];
|
||||
}
|
||||
|
||||
class TotalWeightResult {
|
||||
final String title;
|
||||
final int totalWeight;
|
||||
TotalWeightResult({
|
||||
this.title,
|
||||
this.totalWeight,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ import 'package:moor/src/runtime/statements/update.dart';
|
|||
|
||||
const _zoneRootUserKey = #DatabaseConnectionUser;
|
||||
|
||||
typedef _CustomWriter<T> = Future<T> Function(
|
||||
QueryExecutor e, String sql, List<dynamic> vars);
|
||||
|
||||
/// Class that runs queries to a subset of all available queries in a database.
|
||||
///
|
||||
/// This comes in handy to structure large amounts of database code better: The
|
||||
|
@ -111,6 +114,7 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
/// although it is very likely that the user meant to call it on the
|
||||
/// [Transaction] t. We can detect this by calling the function passed to
|
||||
/// `transaction` in a forked [Zone] storing the transaction in
|
||||
@protected
|
||||
bool get topLevel => false;
|
||||
|
||||
/// We can detect when a user called methods on the wrong [QueryEngine]
|
||||
|
@ -169,32 +173,59 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
/// You can use the [updates] parameter so that moor knows which tables are
|
||||
/// affected by your query. All select streams that depend on a table
|
||||
/// specified there will then issue another query.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Future<int> customUpdate(String query,
|
||||
{List<Variable> variables = const [], Set<TableInfo> updates}) async {
|
||||
return _customWrite(query, variables, updates, (executor, sql, vars) {
|
||||
return executor.runUpdate(sql, vars);
|
||||
});
|
||||
}
|
||||
|
||||
/// Executes a custom insert statement and returns the last inserted rowid.
|
||||
///
|
||||
/// You can tell moor which tables your query is going to affect by using the
|
||||
/// [updates] parameter. Query-streams running on any of these tables will
|
||||
/// then be re-run.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Future<int> customInsert(String query,
|
||||
{List<Variable> variables = const [], Set<TableInfo> updates}) {
|
||||
return _customWrite(query, variables, updates, (executor, sql, vars) {
|
||||
return executor.runInsert(sql, vars);
|
||||
});
|
||||
}
|
||||
|
||||
/// Common logic for [customUpdate] and [customInsert] which takes care of
|
||||
/// mapping the variables, running the query and optionally informing the
|
||||
/// stream-queries.
|
||||
Future<T> _customWrite<T>(String query, List<Variable> variables,
|
||||
Set<TableInfo> updates, _CustomWriter<T> writer) async {
|
||||
final engine = _resolvedEngine;
|
||||
final executor = engine.executor;
|
||||
|
||||
final ctx = GenerationContext.fromDb(engine);
|
||||
final mappedArgs = variables.map((v) => v.mapToSimpleValue(ctx)).toList();
|
||||
|
||||
final affectedRows = await executor
|
||||
.doWhenOpened((_) => executor.runUpdate(query, mappedArgs));
|
||||
final result =
|
||||
await executor.doWhenOpened((e) => writer(e, query, mappedArgs));
|
||||
|
||||
if (updates != null) {
|
||||
await engine.streamQueries.handleTableUpdates(updates);
|
||||
}
|
||||
|
||||
return affectedRows;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Executes a custom select statement once. To use the variables, mark them
|
||||
/// with a "?" in your [query]. They will then be changed to the appropriate
|
||||
/// value.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
@Deprecated('use customSelectQuery(...).get() instead')
|
||||
Future<List<QueryRow>> customSelect(String query,
|
||||
{List<Variable> variables = const []}) async {
|
||||
return CustomSelectStatement(
|
||||
query, variables, <TableInfo>{}, _resolvedEngine)
|
||||
.get();
|
||||
return customSelectQuery(query, variables: variables).get();
|
||||
}
|
||||
|
||||
/// Creates a stream from a custom select statement.To use the variables, mark
|
||||
|
@ -202,15 +233,36 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
/// appropriate value. The stream will re-emit items when any table in
|
||||
/// [readsFrom] changes, so be sure to set it to the set of tables your query
|
||||
/// reads data from.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
@Deprecated('use customSelectQuery(...).watch() instead')
|
||||
Stream<List<QueryRow>> customSelectStream(String query,
|
||||
{List<Variable> variables = const [], Set<TableInfo> readsFrom}) {
|
||||
final tables = readsFrom ?? <TableInfo>{};
|
||||
final statement =
|
||||
CustomSelectStatement(query, variables, tables, _resolvedEngine);
|
||||
return statement.watch();
|
||||
return customSelectQuery(query, variables: variables, readsFrom: readsFrom)
|
||||
.watch();
|
||||
}
|
||||
|
||||
/// Creates a custom select statement from the given sql [query]. To run the
|
||||
/// query once, use [Selectable.get]. For an auto-updating streams, set the
|
||||
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
|
||||
/// know the query will never emit more than one row, you can also use
|
||||
/// [Selectable.getSingle] and [Selectable.watchSingle] which return the item
|
||||
/// directly or wrapping it into a list.
|
||||
///
|
||||
/// If you use variables in your query (for instance with "?"), they will be
|
||||
/// bound to the [variables] you specify on this query.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Selectable<QueryRow> customSelectQuery(String query,
|
||||
{List<Variable> variables = const [],
|
||||
Set<TableInfo> readsFrom = const {}}) {
|
||||
readsFrom ??= {};
|
||||
return CustomSelectStatement(query, variables, readsFrom, _resolvedEngine);
|
||||
}
|
||||
|
||||
/// Executes the custom sql [statement] on the database.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Future<void> customStatement(String statement) {
|
||||
return _resolvedEngine.executor.runCustom(statement);
|
||||
}
|
||||
|
@ -226,6 +278,8 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
/// might be different than that of the "global" database instance.
|
||||
/// 2. Nested transactions are not supported. Creating another transaction
|
||||
/// inside a transaction returns the parent transaction.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Future transaction(Future Function(QueryEngine transaction) action) async {
|
||||
final resolved = _resolvedEngine;
|
||||
if (resolved is Transaction) {
|
||||
|
|
|
@ -220,7 +220,6 @@ class _BeforeOpeningExecutor extends QueryExecutor
|
|||
/// work to a [DatabaseDelegate].
|
||||
class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
||||
final DatabaseDelegate delegate;
|
||||
Completer<bool> _openingCompleter;
|
||||
|
||||
@override
|
||||
bool logStatements;
|
||||
|
@ -233,6 +232,8 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
|||
@override
|
||||
SqlDialect get dialect => delegate.dialect;
|
||||
|
||||
final Lock _openingLock = Lock();
|
||||
|
||||
DelegatedDatabase(this.delegate,
|
||||
{this.logStatements, this.isSequential = false}) {
|
||||
// not using default value because it's commonly set to null
|
||||
|
@ -240,29 +241,17 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() async {
|
||||
// if we're already opening the database or if its already open, return that
|
||||
// status
|
||||
if (_openingCompleter != null) {
|
||||
return _openingCompleter.future;
|
||||
}
|
||||
Future<bool> ensureOpen() {
|
||||
return _openingLock.synchronized(() async {
|
||||
final alreadyOpen = await delegate.isOpen;
|
||||
if (alreadyOpen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final alreadyOpen = await delegate.isOpen;
|
||||
if (alreadyOpen) return true;
|
||||
|
||||
// ignore: invariant_booleans
|
||||
if (_openingCompleter != null) {
|
||||
return _openingCompleter.future;
|
||||
}
|
||||
|
||||
// not already open or opening. Open the database now!
|
||||
_openingCompleter = Completer();
|
||||
await delegate.open(databaseInfo);
|
||||
await _runMigrations();
|
||||
|
||||
_openingCompleter.complete(true);
|
||||
_openingCompleter = null;
|
||||
return true;
|
||||
await delegate.open(databaseInfo);
|
||||
await _runMigrations();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runMigrations() async {
|
||||
|
|
|
@ -67,14 +67,8 @@ class Constant<T, S extends SqlType<T>> extends Expression<T, S> {
|
|||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
// Instead of writing string literals (which we don't support because of
|
||||
// possible sql injections), just write the variable.
|
||||
if (value is String) {
|
||||
_writeVariableIntoContext(context, value);
|
||||
} else {
|
||||
final type = context.typeSystem.forDartType<T>();
|
||||
context.buffer.write(type.mapToSqlConstant(value));
|
||||
}
|
||||
final type = context.typeSystem.forDartType<T>();
|
||||
context.buffer.write(type.mapToSqlConstant(value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -120,6 +120,33 @@ abstract class Selectable<T> {
|
|||
Stream<T> watchSingle() {
|
||||
return watch().transform(singleElements());
|
||||
}
|
||||
|
||||
/// Maps this selectable by using [mapper].
|
||||
///
|
||||
/// Each entry emitted by this [Selectable] will be transformed by the
|
||||
/// [mapper] and then emitted to the selectable returned.
|
||||
Selectable<N> map<N>(N Function(T) mapper) {
|
||||
return _MappedSelectable<T, N>(this, mapper);
|
||||
}
|
||||
}
|
||||
|
||||
class _MappedSelectable<S, T> extends Selectable<T> {
|
||||
final Selectable<S> _source;
|
||||
final T Function(S) _mapper;
|
||||
|
||||
_MappedSelectable(this._source, this._mapper);
|
||||
|
||||
@override
|
||||
Future<List<T>> get() {
|
||||
return _source.get().then(_mapResults);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<T>> watch() {
|
||||
return _source.watch().map(_mapResults);
|
||||
}
|
||||
|
||||
List<T> _mapResults(List<S> results) => results.map(_mapper).toList();
|
||||
}
|
||||
|
||||
mixin SingleTableQueryMixin<T extends Table, D extends DataClass>
|
||||
|
|
|
@ -56,9 +56,13 @@ class StringType extends SqlType<String> {
|
|||
|
||||
@override
|
||||
String mapToSqlConstant(String content) {
|
||||
// TODO: implement mapToSqlConstant, we would probably have to take care
|
||||
// of sql injection vulnerabilities here
|
||||
throw UnimplementedError("Strings can't be mapped to sql literals yet");
|
||||
// From the sqlite docs: (https://www.sqlite.org/lang_expr.html)
|
||||
// A string constant is formed by enclosing the string in single quotes (').
|
||||
// A single quote within the string can be encoded by putting two single
|
||||
// quotes in a row - as in Pascal. C-style escapes using the backslash
|
||||
// character are not supported because they are not standard SQL.
|
||||
final escapedChars = content.replaceAll('\'', '\'\'');
|
||||
return "'$escapedChars'";
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: moor
|
||||
description: Moor is a safe and reactive persistence library for Dart applications
|
||||
version: 1.7.1
|
||||
version: 1.7.2
|
||||
repository: https://github.com/simolus3/moor
|
||||
homepage: https://moor.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/moor/issues
|
||||
|
@ -18,7 +18,7 @@ dependencies:
|
|||
pedantic: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
moor_generator: ^1.6.0
|
||||
moor_generator: ^1.7.0
|
||||
build_runner: '>=1.3.0 <2.0.0'
|
||||
build_test: ^0.10.8
|
||||
test: ^1.6.4
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:test_api/test_api.dart';
|
||||
|
||||
import 'data/tables/todos.dart';
|
||||
|
@ -6,10 +7,12 @@ import 'data/utils/mocks.dart';
|
|||
void main() {
|
||||
TodoDb db;
|
||||
MockExecutor executor;
|
||||
MockStreamQueries streamQueries;
|
||||
|
||||
setUp(() {
|
||||
executor = MockExecutor();
|
||||
db = TodoDb(executor);
|
||||
streamQueries = MockStreamQueries();
|
||||
db = TodoDb(executor)..streamQueries = streamQueries;
|
||||
});
|
||||
|
||||
group('compiled custom queries', () {
|
||||
|
@ -25,4 +28,23 @@ void main() {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('custom update informs stream queries', () async {
|
||||
await db.customUpdate('UPDATE tbl SET a = ?',
|
||||
variables: [Variable.withString('hi')], updates: {db.users});
|
||||
|
||||
verify(executor.runUpdate('UPDATE tbl SET a = ?', ['hi']));
|
||||
verify(streamQueries.handleTableUpdates({db.users}));
|
||||
});
|
||||
|
||||
test('custom insert', () async {
|
||||
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(32));
|
||||
|
||||
final id =
|
||||
await db.customInsert('fake insert', variables: [Variable.withInt(3)]);
|
||||
expect(id, 32);
|
||||
|
||||
// shouldn't call stream queries - we didn't set the updates parameter
|
||||
verifyNever(streamQueries.handleTableUpdates(any));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@ import 'package:moor/moor.dart';
|
|||
|
||||
part 'custom_tables.g.dart';
|
||||
|
||||
@UseMoor(include: {'tables.moor'})
|
||||
@UseMoor(
|
||||
include: {'tables.moor'},
|
||||
queries: {'writeConfig': 'REPLACE INTO config VALUES (:key, :value)'},
|
||||
)
|
||||
class CustomTablesDb extends _$CustomTablesDb {
|
||||
CustomTablesDb(QueryExecutor e) : super(e);
|
||||
|
||||
|
|
|
@ -63,6 +63,9 @@ class NoIdsCompanion extends UpdateCompanion<NoId> {
|
|||
const NoIdsCompanion({
|
||||
this.payload = const Value.absent(),
|
||||
});
|
||||
NoIdsCompanion.insert({
|
||||
@required Uint8List payload,
|
||||
}) : payload = Value(payload);
|
||||
NoIdsCompanion copyWith({Value<Uint8List> payload}) {
|
||||
return NoIdsCompanion(
|
||||
payload: payload ?? this.payload,
|
||||
|
@ -197,6 +200,10 @@ class WithDefaultsCompanion extends UpdateCompanion<WithDefault> {
|
|||
this.a = const Value.absent(),
|
||||
this.b = const Value.absent(),
|
||||
});
|
||||
WithDefaultsCompanion.insert({
|
||||
this.a = const Value.absent(),
|
||||
this.b = const Value.absent(),
|
||||
});
|
||||
WithDefaultsCompanion copyWith({Value<String> a, Value<int> b}) {
|
||||
return WithDefaultsCompanion(
|
||||
a: a ?? this.a,
|
||||
|
@ -359,6 +366,11 @@ class WithConstraintsCompanion extends UpdateCompanion<WithConstraint> {
|
|||
this.b = const Value.absent(),
|
||||
this.c = const Value.absent(),
|
||||
});
|
||||
WithConstraintsCompanion.insert({
|
||||
this.a = const Value.absent(),
|
||||
@required int b,
|
||||
this.c = const Value.absent(),
|
||||
}) : b = Value(b);
|
||||
WithConstraintsCompanion copyWith(
|
||||
{Value<String> a, Value<int> b, Value<double> c}) {
|
||||
return WithConstraintsCompanion(
|
||||
|
@ -535,6 +547,10 @@ class ConfigCompanion extends UpdateCompanion<ConfigData> {
|
|||
this.configKey = const Value.absent(),
|
||||
this.configValue = const Value.absent(),
|
||||
});
|
||||
ConfigCompanion.insert({
|
||||
@required String configKey,
|
||||
this.configValue = const Value.absent(),
|
||||
}) : configKey = Value(configKey);
|
||||
ConfigCompanion copyWith(
|
||||
{Value<String> configKey, Value<String> configValue}) {
|
||||
return ConfigCompanion(
|
||||
|
@ -694,6 +710,10 @@ class MytableCompanion extends UpdateCompanion<MytableData> {
|
|||
this.someid = const Value.absent(),
|
||||
this.sometext = const Value.absent(),
|
||||
});
|
||||
MytableCompanion.insert({
|
||||
this.someid = const Value.absent(),
|
||||
this.sometext = const Value.absent(),
|
||||
});
|
||||
MytableCompanion copyWith({Value<int> someid, Value<String> sometext}) {
|
||||
return MytableCompanion(
|
||||
someid: someid ?? this.someid,
|
||||
|
@ -792,6 +812,21 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
Config get config => _config ??= Config(this);
|
||||
Mytable _mytable;
|
||||
Mytable get mytable => _mytable ??= Mytable(this);
|
||||
Future<int> writeConfig(
|
||||
String key,
|
||||
String value,
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customInsert(
|
||||
'REPLACE INTO config VALUES (:key, :value)',
|
||||
variables: [
|
||||
Variable.withString(key),
|
||||
Variable.withString(value),
|
||||
],
|
||||
updates: {config},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<TableInfo> get allTables =>
|
||||
[noIds, withDefaults, withConstraints, config, mytable];
|
||||
|
|
|
@ -133,6 +133,13 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
|
|||
this.targetDate = const Value.absent(),
|
||||
this.category = const Value.absent(),
|
||||
});
|
||||
TodosTableCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
@required String content,
|
||||
this.targetDate = const Value.absent(),
|
||||
this.category = const Value.absent(),
|
||||
}) : content = Value(content);
|
||||
TodosTableCompanion copyWith(
|
||||
{Value<int> id,
|
||||
Value<String> title,
|
||||
|
@ -358,6 +365,10 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
|
|||
this.id = const Value.absent(),
|
||||
this.description = const Value.absent(),
|
||||
});
|
||||
CategoriesCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
@required String description,
|
||||
}) : description = Value(description);
|
||||
CategoriesCompanion copyWith({Value<int> id, Value<String> description}) {
|
||||
return CategoriesCompanion(
|
||||
id: id ?? this.id,
|
||||
|
@ -569,6 +580,14 @@ class UsersCompanion extends UpdateCompanion<User> {
|
|||
this.profilePicture = const Value.absent(),
|
||||
this.creationTime = const Value.absent(),
|
||||
});
|
||||
UsersCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
@required String name,
|
||||
this.isAwesome = const Value.absent(),
|
||||
@required Uint8List profilePicture,
|
||||
this.creationTime = const Value.absent(),
|
||||
}) : name = Value(name),
|
||||
profilePicture = Value(profilePicture);
|
||||
UsersCompanion copyWith(
|
||||
{Value<int> id,
|
||||
Value<String> name,
|
||||
|
@ -792,6 +811,11 @@ class SharedTodosCompanion extends UpdateCompanion<SharedTodo> {
|
|||
this.todo = const Value.absent(),
|
||||
this.user = const Value.absent(),
|
||||
});
|
||||
SharedTodosCompanion.insert({
|
||||
@required int todo,
|
||||
@required int user,
|
||||
}) : todo = Value(todo),
|
||||
user = Value(user);
|
||||
SharedTodosCompanion copyWith({Value<int> todo, Value<int> user}) {
|
||||
return SharedTodosCompanion(
|
||||
todo: todo ?? this.todo,
|
||||
|
@ -978,6 +1002,13 @@ class TableWithoutPKCompanion extends UpdateCompanion<TableWithoutPKData> {
|
|||
this.someFloat = const Value.absent(),
|
||||
this.custom = const Value.absent(),
|
||||
});
|
||||
TableWithoutPKCompanion.insert({
|
||||
@required int notReallyAnId,
|
||||
@required double someFloat,
|
||||
@required MyCustomObject custom,
|
||||
}) : notReallyAnId = Value(notReallyAnId),
|
||||
someFloat = Value(someFloat),
|
||||
custom = Value(custom);
|
||||
TableWithoutPKCompanion copyWith(
|
||||
{Value<int> notReallyAnId,
|
||||
Value<double> someFloat,
|
||||
|
@ -1162,6 +1193,10 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
|
|||
this.id = const Value.absent(),
|
||||
this.txt = const Value.absent(),
|
||||
});
|
||||
PureDefaultsCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
this.txt = const Value.absent(),
|
||||
});
|
||||
PureDefaultsCompanion copyWith({Value<int> id, Value<String> txt}) {
|
||||
return PureDefaultsCompanion(
|
||||
id: id ?? this.id,
|
||||
|
@ -1247,32 +1282,6 @@ class $PureDefaultsTable extends PureDefaults
|
|||
}
|
||||
}
|
||||
|
||||
class AllTodosWithCategoryResult {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final DateTime targetDate;
|
||||
final int category;
|
||||
final int catId;
|
||||
final String catDesc;
|
||||
AllTodosWithCategoryResult({
|
||||
this.id,
|
||||
this.title,
|
||||
this.content,
|
||||
this.targetDate,
|
||||
this.category,
|
||||
this.catId,
|
||||
this.catDesc,
|
||||
});
|
||||
}
|
||||
|
||||
class FindCustomResult {
|
||||
final MyCustomObject custom;
|
||||
FindCustomResult({
|
||||
this.custom,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class _$TodoDb extends GeneratedDatabase {
|
||||
_$TodoDb(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
|
||||
$TodosTableTable _todosTable;
|
||||
|
@ -1303,22 +1312,26 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<AllTodosWithCategoryResult>> allTodosWithCategory(
|
||||
Selectable<AllTodosWithCategoryResult> allTodosWithCategoryQuery(
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelect(
|
||||
'SELECT t.*, c.id as catId, c."desc" as catDesc FROM todos t INNER JOIN categories c ON c.id = t.category',
|
||||
variables: []).then((rows) => rows.map(_rowToAllTodosWithCategoryResult).toList());
|
||||
}
|
||||
|
||||
Stream<List<AllTodosWithCategoryResult>> watchAllTodosWithCategory() {
|
||||
return customSelectStream(
|
||||
return (operateOn ?? this).customSelectQuery(
|
||||
'SELECT t.*, c.id as catId, c."desc" as catDesc FROM todos t INNER JOIN categories c ON c.id = t.category',
|
||||
variables: [],
|
||||
readsFrom: {
|
||||
categories,
|
||||
todosTable
|
||||
}).map((rows) => rows.map(_rowToAllTodosWithCategoryResult).toList());
|
||||
}).map(_rowToAllTodosWithCategoryResult);
|
||||
}
|
||||
|
||||
Future<List<AllTodosWithCategoryResult>> allTodosWithCategory(
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return allTodosWithCategoryQuery(operateOn: operateOn).get();
|
||||
}
|
||||
|
||||
Stream<List<AllTodosWithCategoryResult>> watchAllTodosWithCategory() {
|
||||
return allTodosWithCategoryQuery().watch();
|
||||
}
|
||||
|
||||
Future<int> deleteTodoById(
|
||||
|
@ -1344,7 +1357,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<TodoEntry>> withIn(
|
||||
Selectable<TodoEntry> withInQuery(
|
||||
String var1,
|
||||
String var2,
|
||||
List<int> var3,
|
||||
|
@ -1353,21 +1366,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
var $highestIndex = 3;
|
||||
final expandedvar3 = $expandVar($highestIndex, var3.length);
|
||||
$highestIndex += var3.length;
|
||||
return (operateOn ?? this).customSelect(
|
||||
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
|
||||
variables: [
|
||||
Variable.withString(var1),
|
||||
Variable.withString(var2),
|
||||
for (var $ in var3) Variable.withInt($),
|
||||
]).then((rows) => rows.map(_rowToTodoEntry).toList());
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> watchWithIn(
|
||||
String var1, String var2, List<int> var3) {
|
||||
var $highestIndex = 3;
|
||||
final expandedvar3 = $expandVar($highestIndex, var3.length);
|
||||
$highestIndex += var3.length;
|
||||
return customSelectStream(
|
||||
return (operateOn ?? this).customSelectQuery(
|
||||
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
|
||||
variables: [
|
||||
Variable.withString(var1),
|
||||
|
@ -1376,29 +1375,46 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
],
|
||||
readsFrom: {
|
||||
todosTable
|
||||
}).map((rows) => rows.map(_rowToTodoEntry).toList());
|
||||
}).map(_rowToTodoEntry);
|
||||
}
|
||||
|
||||
Future<List<TodoEntry>> withIn(
|
||||
String var1,
|
||||
String var2,
|
||||
List<int> var3,
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return withInQuery(var1, var2, var3, operateOn: operateOn).get();
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> watchWithIn(
|
||||
String var1, String var2, List<int> var3) {
|
||||
return withInQuery(var1, var2, var3).watch();
|
||||
}
|
||||
|
||||
Selectable<TodoEntry> searchQuery(
|
||||
int id,
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelectQuery(
|
||||
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
|
||||
variables: [
|
||||
Variable.withInt(id),
|
||||
],
|
||||
readsFrom: {
|
||||
todosTable
|
||||
}).map(_rowToTodoEntry);
|
||||
}
|
||||
|
||||
Future<List<TodoEntry>> search(
|
||||
int id,
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelect(
|
||||
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
|
||||
variables: [
|
||||
Variable.withInt(id),
|
||||
]).then((rows) => rows.map(_rowToTodoEntry).toList());
|
||||
return searchQuery(id, operateOn: operateOn).get();
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> watchSearch(int id) {
|
||||
return customSelectStream(
|
||||
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
|
||||
variables: [
|
||||
Variable.withInt(id),
|
||||
],
|
||||
readsFrom: {
|
||||
todosTable
|
||||
}).map((rows) => rows.map(_rowToTodoEntry).toList());
|
||||
return searchQuery(id).watch();
|
||||
}
|
||||
|
||||
FindCustomResult _rowToFindCustomResult(QueryRow row) {
|
||||
|
@ -1408,20 +1424,23 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
);
|
||||
}
|
||||
|
||||
Selectable<FindCustomResult> findCustomQuery(
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelectQuery(
|
||||
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
|
||||
variables: [],
|
||||
readsFrom: {tableWithoutPK}).map(_rowToFindCustomResult);
|
||||
}
|
||||
|
||||
Future<List<FindCustomResult>> findCustom(
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelect(
|
||||
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
|
||||
variables: []).then((rows) => rows.map(_rowToFindCustomResult).toList());
|
||||
return findCustomQuery(operateOn: operateOn).get();
|
||||
}
|
||||
|
||||
Stream<List<FindCustomResult>> watchFindCustom() {
|
||||
return customSelectStream(
|
||||
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
|
||||
variables: [],
|
||||
readsFrom: {tableWithoutPK})
|
||||
.map((rows) => rows.map(_rowToFindCustomResult).toList());
|
||||
return findCustomQuery().watch();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1435,6 +1454,32 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
];
|
||||
}
|
||||
|
||||
class AllTodosWithCategoryResult {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final DateTime targetDate;
|
||||
final int category;
|
||||
final int catId;
|
||||
final String catDesc;
|
||||
AllTodosWithCategoryResult({
|
||||
this.id,
|
||||
this.title,
|
||||
this.content,
|
||||
this.targetDate,
|
||||
this.category,
|
||||
this.catId,
|
||||
this.catDesc,
|
||||
});
|
||||
}
|
||||
|
||||
class FindCustomResult {
|
||||
final MyCustomObject custom;
|
||||
FindCustomResult({
|
||||
this.custom,
|
||||
});
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
@ -1453,19 +1498,11 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<TodoEntry>> todosForUser(
|
||||
Selectable<TodoEntry> todosForUserQuery(
|
||||
int user,
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return (operateOn ?? this).customSelect(
|
||||
'SELECT t.* FROM todos t INNER JOIN shared_todos st ON st.todo = t.id INNER JOIN users u ON u.id = st.user WHERE u.id = :user',
|
||||
variables: [
|
||||
Variable.withInt(user),
|
||||
]).then((rows) => rows.map(_rowToTodoEntry).toList());
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> watchTodosForUser(int user) {
|
||||
return customSelectStream(
|
||||
return (operateOn ?? this).customSelectQuery(
|
||||
'SELECT t.* FROM todos t INNER JOIN shared_todos st ON st.todo = t.id INNER JOIN users u ON u.id = st.user WHERE u.id = :user',
|
||||
variables: [
|
||||
Variable.withInt(user),
|
||||
|
@ -1474,6 +1511,17 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
|
|||
todosTable,
|
||||
sharedTodos,
|
||||
users
|
||||
}).map((rows) => rows.map(_rowToTodoEntry).toList());
|
||||
}).map(_rowToTodoEntry);
|
||||
}
|
||||
|
||||
Future<List<TodoEntry>> todosForUser(
|
||||
int user,
|
||||
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
|
||||
QueryEngine operateOn}) {
|
||||
return todosForUserQuery(user, operateOn: operateOn).get();
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> watchTodosForUser(int user) {
|
||||
return todosForUserQuery(user).watch();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,19 +10,40 @@ typedef Future<T> _EnsureOpenAction<T>(QueryExecutor e);
|
|||
|
||||
class MockExecutor extends Mock implements QueryExecutor {
|
||||
final MockTransactionExecutor transactions = MockTransactionExecutor();
|
||||
var _opened = false;
|
||||
|
||||
MockExecutor() {
|
||||
when(runSelect(any, any)).thenAnswer((_) => Future.value([]));
|
||||
when(runUpdate(any, any)).thenAnswer((_) => Future.value(0));
|
||||
when(runDelete(any, any)).thenAnswer((_) => Future.value(0));
|
||||
when(runInsert(any, any)).thenAnswer((_) => Future.value(0));
|
||||
when(runSelect(any, any)).thenAnswer((_) {
|
||||
assert(_opened);
|
||||
return Future.value([]);
|
||||
});
|
||||
when(runUpdate(any, any)).thenAnswer((_) {
|
||||
assert(_opened);
|
||||
return Future.value(0);
|
||||
});
|
||||
when(runDelete(any, any)).thenAnswer((_) {
|
||||
assert(_opened);
|
||||
return Future.value(0);
|
||||
});
|
||||
when(runInsert(any, any)).thenAnswer((_) {
|
||||
assert(_opened);
|
||||
return Future.value(0);
|
||||
});
|
||||
when(beginTransaction()).thenAnswer((_) {
|
||||
assert(_opened);
|
||||
return transactions;
|
||||
});
|
||||
|
||||
when(doWhenOpened(any)).thenAnswer((i) {
|
||||
_opened = true;
|
||||
final action = i.positionalArguments.single as _EnsureOpenAction;
|
||||
|
||||
return action(this);
|
||||
});
|
||||
|
||||
when(beginTransaction()).thenAnswer((_) => transactions);
|
||||
when(close()).thenAnswer((_) async {
|
||||
_opened = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:test_api/test_api.dart';
|
||||
|
||||
import '../data/tables/todos.dart';
|
||||
|
||||
void main() {
|
||||
group('string literals', () {
|
||||
test('can be written as constants', () {
|
||||
testStringMapping('hello world', "'hello world'");
|
||||
});
|
||||
|
||||
test('supports escaping snigle quotes', () {
|
||||
testStringMapping('what\'s that?', "'what\'\'s that?'");
|
||||
});
|
||||
|
||||
test('other chars are not escaped', () {
|
||||
testStringMapping('\\\$"', "'\\\$\"'");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void testStringMapping(String dart, String expectedLiteral) {
|
||||
final ctx = GenerationContext.fromDb(TodoDb(null));
|
||||
final constant = Constant(dart);
|
||||
|
||||
constant.writeInto(ctx);
|
||||
|
||||
expect(ctx.sql, expectedLiteral);
|
||||
}
|
|
@ -91,14 +91,14 @@ class Database extends _$Database {
|
|||
Stream<List<CategoryWithCount>> categoriesWithCount() {
|
||||
// select all categories and load how many associated entries there are for
|
||||
// each category
|
||||
return customSelectStream(
|
||||
return customSelectQuery(
|
||||
'SELECT c.id, c.desc, '
|
||||
'(SELECT COUNT(*) FROM todos WHERE category = c.id) AS amount '
|
||||
'FROM categories c '
|
||||
'UNION ALL SELECT null, null, '
|
||||
'(SELECT COUNT(*) FROM todos WHERE category IS NULL)',
|
||||
readsFrom: {todos, categories},
|
||||
).map((rows) {
|
||||
).watch().map((rows) {
|
||||
// when we have the result set, map each row to the data class
|
||||
return rows.map((row) {
|
||||
final hasId = row.data['id'] != null;
|
||||
|
|
|
@ -7,14 +7,14 @@ packages:
|
|||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.2.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.4"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -52,14 +52,14 @@ packages:
|
|||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
version: "1.1.6"
|
||||
moor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../moor"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.6.0"
|
||||
version: "1.7.1"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -73,7 +73,7 @@ packages:
|
|||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0+1"
|
||||
version: "1.7.0"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -99,7 +99,7 @@ packages:
|
|||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6+3"
|
||||
version: "1.1.6+4"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
|
|
|
@ -1,17 +1,4 @@
|
|||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/dao_generator.dart';
|
||||
import 'package:moor_generator/src/state/options.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'package:moor_generator/src/moor_generator.dart';
|
||||
import 'package:moor_generator/src/backends/build/moor_builder.dart';
|
||||
|
||||
Builder moorBuilder(BuilderOptions options) {
|
||||
final parsedOptions = MoorOptions.fromBuilder(options.config);
|
||||
|
||||
return SharedPartBuilder(
|
||||
[
|
||||
MoorGenerator(parsedOptions),
|
||||
DaoGenerator(parsedOptions),
|
||||
],
|
||||
'moor',
|
||||
);
|
||||
}
|
||||
Builder moorBuilder(BuilderOptions options) => MoorBuilder(options);
|
||||
|
|
|
@ -1,13 +1,4 @@
|
|||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:moor_generator/src/model/used_type_converter.dart';
|
||||
import 'package:moor_generator/src/state/errors.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/parser/parser.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/utils/type_utils.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
part of 'parser.dart';
|
||||
|
||||
const String startInt = 'integer';
|
||||
const String startString = 'text';
|
||||
|
@ -16,7 +7,7 @@ const String startDateTime = 'dateTime';
|
|||
const String startBlob = 'blob';
|
||||
const String startReal = 'real';
|
||||
|
||||
final Set<String> starters = {
|
||||
const Set<String> starters = {
|
||||
startInt,
|
||||
startString,
|
||||
startBool,
|
||||
|
@ -38,29 +29,31 @@ const String _errorMessage = 'This getter does not create a valid column that '
|
|||
'can be parsed by moor. Please refer to the readme from moor to see how '
|
||||
'columns are formed. If you have any questions, feel free to raise an issue.';
|
||||
|
||||
class ColumnParser extends ParserBase {
|
||||
ColumnParser(GeneratorSession session) : super(session);
|
||||
/// Parses a single column defined in a Dart table. These columns are a chain
|
||||
/// or [MethodInvocation]s. An example getter might look like this:
|
||||
/// ```dart
|
||||
/// IntColumn get id => integer().autoIncrement()();
|
||||
/// ```
|
||||
/// The last call `()` is a [FunctionExpressionInvocation], the entries for
|
||||
/// before that (in this case `autoIncrement()` and `integer()` are a)
|
||||
/// [MethodInvocation]. We work our way through that syntax until we hit a
|
||||
/// method that starts the chain (contained in [starters]). By visiting all
|
||||
/// the invocations on our way, we can extract the constraint for the column
|
||||
/// (e.g. its name, whether it has auto increment, is a primary key and so on).
|
||||
class ColumnParser {
|
||||
final MoorDartParser base;
|
||||
|
||||
SpecifiedColumn parse(MethodDeclaration getter, Element getterElement) {
|
||||
/*
|
||||
These getters look like this: ... get id => integer().autoIncrement()();
|
||||
The last () is a FunctionExpressionInvocation, the entries before that
|
||||
(here autoIncrement and integer) are MethodInvocations.
|
||||
We go through each of the method invocations until we hit one that starts
|
||||
the chain (integer, text, boolean, etc.). From each method in the chain,
|
||||
we can extract what it means for the column (name, auto increment, PK,
|
||||
constraints...).
|
||||
*/
|
||||
ColumnParser(this.base);
|
||||
|
||||
final expr = returnExpressionOfMethod(getter);
|
||||
SpecifiedColumn parse(MethodDeclaration getter, Element element) {
|
||||
final expr = base.returnExpressionOfMethod(getter);
|
||||
|
||||
if (!(expr is FunctionExpressionInvocation)) {
|
||||
session.errors.add(MoorError(
|
||||
base.task.reportError(ErrorInDartCode(
|
||||
affectedElement: getter.declaredElement,
|
||||
message: _errorMessage,
|
||||
critical: true,
|
||||
severity: Severity.criticalError,
|
||||
));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -88,9 +81,9 @@ class ColumnParser extends ParserBase {
|
|||
switch (methodName) {
|
||||
case _methodNamed:
|
||||
if (foundExplicitName != null) {
|
||||
session.errors.add(
|
||||
MoorError(
|
||||
critical: false,
|
||||
base.task.reportError(
|
||||
ErrorInDartCode(
|
||||
severity: Severity.warning,
|
||||
affectedElement: getter.declaredElement,
|
||||
message:
|
||||
"You're setting more than one name here, the first will "
|
||||
|
@ -99,11 +92,11 @@ class ColumnParser extends ParserBase {
|
|||
);
|
||||
}
|
||||
|
||||
foundExplicitName =
|
||||
readStringLiteral(remainingExpr.argumentList.arguments.first, () {
|
||||
session.errors.add(
|
||||
MoorError(
|
||||
critical: false,
|
||||
foundExplicitName = base.readStringLiteral(
|
||||
remainingExpr.argumentList.arguments.first, () {
|
||||
base.task.reportError(
|
||||
ErrorInDartCode(
|
||||
severity: Severity.error,
|
||||
affectedElement: getter.declaredElement,
|
||||
message:
|
||||
'This table name is cannot be resolved! Please only use '
|
||||
|
@ -116,12 +109,12 @@ class ColumnParser extends ParserBase {
|
|||
break;
|
||||
case _methodWithLength:
|
||||
final args = remainingExpr.argumentList;
|
||||
final minArg = findNamedArgument(args, 'min');
|
||||
final maxArg = findNamedArgument(args, 'max');
|
||||
final minArg = base.findNamedArgument(args, 'min');
|
||||
final maxArg = base.findNamedArgument(args, 'max');
|
||||
|
||||
foundFeatures.add(LimitingTextLength.withLength(
|
||||
min: readIntLiteral(minArg, () {}),
|
||||
max: readIntLiteral(maxArg, () {}),
|
||||
min: base.readIntLiteral(minArg, () {}),
|
||||
max: base.readIntLiteral(maxArg, () {}),
|
||||
));
|
||||
break;
|
||||
case _methodAutoIncrement:
|
||||
|
@ -133,11 +126,11 @@ class ColumnParser extends ParserBase {
|
|||
nullable = true;
|
||||
break;
|
||||
case _methodCustomConstraint:
|
||||
foundCustomConstraint =
|
||||
readStringLiteral(remainingExpr.argumentList.arguments.first, () {
|
||||
session.errors.add(
|
||||
MoorError(
|
||||
critical: false,
|
||||
foundCustomConstraint = base.readStringLiteral(
|
||||
remainingExpr.argumentList.arguments.first, () {
|
||||
base.task.reportError(
|
||||
ErrorInDartCode(
|
||||
severity: Severity.warning,
|
||||
affectedElement: getter.declaredElement,
|
||||
message:
|
||||
'This constraint is cannot be resolved! Please only use '
|
||||
|
@ -190,7 +183,7 @@ class ColumnParser extends ParserBase {
|
|||
type: columnType,
|
||||
dartGetterName: getter.name.name,
|
||||
name: name,
|
||||
overriddenJsonName: _readJsonKey(getterElement),
|
||||
overriddenJsonName: _readJsonKey(element),
|
||||
customConstraints: foundCustomConstraint,
|
||||
nullable: nullable,
|
||||
features: foundFeatures,
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:moor/sqlite_keywords.dart';
|
||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_dao.dart';
|
||||
import 'package:moor_generator/src/model/specified_database.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/model/used_type_converter.dart';
|
||||
import 'package:moor_generator/src/utils/names.dart';
|
||||
import 'package:moor_generator/src/utils/type_utils.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
part 'column_parser.dart';
|
||||
part 'table_parser.dart';
|
||||
part 'use_dao_parser.dart';
|
||||
part 'use_moor_parser.dart';
|
||||
|
||||
class MoorDartParser {
|
||||
final DartTask task;
|
||||
|
||||
ColumnParser _columnParser;
|
||||
TableParser _tableParser;
|
||||
|
||||
MoorDartParser(this.task) {
|
||||
_columnParser = ColumnParser(this);
|
||||
_tableParser = TableParser(this);
|
||||
}
|
||||
|
||||
Future<SpecifiedTable> parseTable(ClassElement classElement) {
|
||||
return _tableParser.parseTable(classElement);
|
||||
}
|
||||
|
||||
Future<SpecifiedColumn> parseColumn(
|
||||
MethodDeclaration declaration, Element element) {
|
||||
return Future.value(_columnParser.parse(declaration, element));
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Expression returnExpressionOfMethod(MethodDeclaration method) {
|
||||
final body = method.body;
|
||||
|
||||
if (!(body is ExpressionFunctionBody)) {
|
||||
task.reportError(ErrorInDartCode(
|
||||
affectedElement: method.declaredElement,
|
||||
severity: Severity.criticalError,
|
||||
message:
|
||||
'This method must have an expression body (user => instead of {return ...})',
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
return (method.body as ExpressionFunctionBody).expression;
|
||||
}
|
||||
|
||||
Future<ElementDeclarationResult> loadElementDeclaration(
|
||||
Element element) async {
|
||||
final resolvedLibrary = await element.library.session
|
||||
.getResolvedLibraryByElement(element.library);
|
||||
|
||||
return resolvedLibrary.getElementDeclaration(element);
|
||||
}
|
||||
|
||||
String readStringLiteral(Expression expression, void onError()) {
|
||||
if (!(expression is StringLiteral)) {
|
||||
onError();
|
||||
} else {
|
||||
final value = (expression as StringLiteral).stringValue;
|
||||
if (value == null) {
|
||||
onError();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int readIntLiteral(Expression expression, void onError()) {
|
||||
if (!(expression is IntegerLiteral)) {
|
||||
onError();
|
||||
// ignore: avoid_returning_null
|
||||
return null;
|
||||
} else {
|
||||
return (expression as IntegerLiteral).value;
|
||||
}
|
||||
}
|
||||
|
||||
Expression findNamedArgument(ArgumentList args, String argName) {
|
||||
final argument = args.arguments.singleWhere(
|
||||
(e) => e is NamedExpression && e.name.label.name == argName,
|
||||
orElse: () => null) as NamedExpression;
|
||||
|
||||
return argument?.expression;
|
||||
}
|
||||
}
|
|
@ -1,19 +1,12 @@
|
|||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:moor_generator/src/state/errors.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/parser/parser.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/utils/names.dart';
|
||||
import 'package:moor_generator/src/utils/type_utils.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:moor/sqlite_keywords.dart';
|
||||
part of 'parser.dart';
|
||||
|
||||
class TableParser extends ParserBase {
|
||||
TableParser(GeneratorSession session) : super(session);
|
||||
/// Parses a [SpecifiedTable] from a Dart class.
|
||||
class TableParser {
|
||||
final MoorDartParser base;
|
||||
|
||||
Future<SpecifiedTable> parse(ClassElement element) async {
|
||||
TableParser(this.base);
|
||||
|
||||
Future<SpecifiedTable> parseTable(ClassElement element) async {
|
||||
final sqlName = await _parseTableName(element);
|
||||
if (sqlName == null) return null;
|
||||
|
||||
|
@ -62,13 +55,13 @@ class TableParser extends ParserBase {
|
|||
// we expect something like get tableName => "myTableName", the getter
|
||||
// must do nothing more complicated
|
||||
final tableNameDeclaration =
|
||||
await session.loadElementDeclaration(tableNameGetter);
|
||||
final returnExpr = returnExpressionOfMethod(
|
||||
await base.loadElementDeclaration(tableNameGetter);
|
||||
final returnExpr = base.returnExpressionOfMethod(
|
||||
tableNameDeclaration.node as MethodDeclaration);
|
||||
|
||||
final tableName = readStringLiteral(returnExpr, () {
|
||||
session.errors.add(MoorError(
|
||||
critical: true,
|
||||
final tableName = base.readStringLiteral(returnExpr, () {
|
||||
base.task.reportError(ErrorInDartCode(
|
||||
severity: Severity.criticalError,
|
||||
message:
|
||||
'This getter must return a string literal, and do nothing more',
|
||||
affectedElement: tableNameGetter));
|
||||
|
@ -84,11 +77,11 @@ class TableParser extends ParserBase {
|
|||
return null;
|
||||
}
|
||||
|
||||
final resolved = await session.loadElementDeclaration(primaryKeyGetter);
|
||||
final resolved = await base.loadElementDeclaration(primaryKeyGetter);
|
||||
final ast = resolved.node as MethodDeclaration;
|
||||
final body = ast.body;
|
||||
if (body is! ExpressionFunctionBody) {
|
||||
session.errors.add(MoorError(
|
||||
base.task.reportError(ErrorInDartCode(
|
||||
affectedElement: primaryKeyGetter,
|
||||
message: 'This must return a set literal using the => syntax!'));
|
||||
return null;
|
||||
|
@ -107,7 +100,7 @@ class TableParser extends ParserBase {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
session.errors.add(MoorError(
|
||||
base.task.reportError(ErrorInDartCode(
|
||||
affectedElement: primaryKeyGetter,
|
||||
message: 'This must return a set literal!'));
|
||||
}
|
||||
|
@ -120,10 +113,10 @@ class TableParser extends ParserBase {
|
|||
.where((field) => isColumn(field.type) && field.getter != null);
|
||||
|
||||
return Future.wait(columns.map((field) async {
|
||||
final resolved = await session.loadElementDeclaration(field.getter);
|
||||
final resolved = await base.loadElementDeclaration(field.getter);
|
||||
final node = resolved.node as MethodDeclaration;
|
||||
|
||||
return await session.parseColumn(node, field.getter);
|
||||
return await base.parseColumn(node, field.getter);
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:moor_generator/src/model/specified_dao.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
part of 'parser.dart';
|
||||
|
||||
class UseDaoParser {
|
||||
final GeneratorSession session;
|
||||
final DartTask dartTask;
|
||||
|
||||
UseDaoParser(this.session);
|
||||
UseDaoParser(this.dartTask);
|
||||
|
||||
/// If [element] has a `@UseDao` annotation, parses the database model
|
||||
/// declared by that class and the referenced tables.
|
||||
|
@ -24,11 +21,11 @@ class UseDaoParser {
|
|||
?.map((e) => e.toStringValue()) ??
|
||||
{};
|
||||
|
||||
final parsedTables = await session.parseTables(tableTypes, element);
|
||||
parsedTables.addAll(await session.resolveIncludes(includes));
|
||||
final parsedTables = await dartTask.parseTables(tableTypes, element);
|
||||
parsedTables.addAll(await dartTask.resolveIncludes(includes));
|
||||
|
||||
final parsedQueries =
|
||||
await session.parseQueries(queryStrings, parsedTables);
|
||||
await dartTask.parseQueries(queryStrings, parsedTables);
|
||||
|
||||
return SpecifiedDao(element, parsedTables, parsedQueries);
|
||||
}
|
|
@ -1,13 +1,9 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:moor_generator/src/model/specified_database.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
part of 'parser.dart';
|
||||
|
||||
class UseMoorParser {
|
||||
final GeneratorSession session;
|
||||
final DartTask task;
|
||||
|
||||
UseMoorParser(this.session);
|
||||
UseMoorParser(this.task);
|
||||
|
||||
/// If [element] has a `@UseMoor` annotation, parses the database model
|
||||
/// declared by that class and the referenced tables.
|
||||
|
@ -25,11 +21,10 @@ class UseMoorParser {
|
|||
?.map((e) => e.toStringValue()) ??
|
||||
{};
|
||||
|
||||
final parsedTables = await session.parseTables(tableTypes, element);
|
||||
parsedTables.addAll(await session.resolveIncludes(includes));
|
||||
final parsedTables = await task.parseTables(tableTypes, element);
|
||||
parsedTables.addAll(await task.resolveIncludes(includes));
|
||||
|
||||
final parsedQueries =
|
||||
await session.parseQueries(queryStrings, parsedTables);
|
||||
final parsedQueries = await task.parseQueries(queryStrings, parsedTables);
|
||||
final daoTypes = _readDaoTypes(annotation);
|
||||
|
||||
return SpecifiedDatabase(element, parsedTables, daoTypes, parsedQueries);
|
|
@ -0,0 +1,89 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
|
||||
typedef LogFunction = void Function(dynamic message,
|
||||
[Object error, StackTrace stackTrace]);
|
||||
|
||||
/// Base class for errors that can be presented to an user.
|
||||
class MoorError {
|
||||
final Severity severity;
|
||||
final String message;
|
||||
|
||||
MoorError({@required this.severity, this.message});
|
||||
|
||||
bool get isError =>
|
||||
severity == Severity.criticalError || severity == Severity.error;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Error: $message';
|
||||
}
|
||||
|
||||
void writeDescription(LogFunction log) {
|
||||
log(message);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorInDartCode extends MoorError {
|
||||
final Element affectedElement;
|
||||
|
||||
ErrorInDartCode(
|
||||
{String message,
|
||||
this.affectedElement,
|
||||
Severity severity = Severity.warning})
|
||||
: super(severity: severity, message: message);
|
||||
|
||||
@override
|
||||
void writeDescription(LogFunction log) {
|
||||
if (affectedElement != null) {
|
||||
final span = spanForElement(affectedElement);
|
||||
log(span.message(message));
|
||||
} else {
|
||||
log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorInMoorFile extends MoorError {
|
||||
final FileSpan span;
|
||||
|
||||
ErrorInMoorFile(
|
||||
{@required this.span,
|
||||
String message,
|
||||
Severity severity = Severity.warning})
|
||||
: super(message: message, severity: severity);
|
||||
|
||||
@override
|
||||
void writeDescription(LogFunction log) {
|
||||
log(span.message(message));
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorSink {
|
||||
final List<MoorError> _errors = [];
|
||||
UnmodifiableListView<MoorError> get errors => UnmodifiableListView(_errors);
|
||||
|
||||
void report(MoorError error) {
|
||||
_errors.add(error);
|
||||
}
|
||||
}
|
||||
|
||||
enum Severity {
|
||||
/// A severe error. We might not be able to generate correct or consistent
|
||||
/// code when errors with these severity are present.
|
||||
criticalError,
|
||||
|
||||
/// An error. The generated code won't have major problems, but might cause
|
||||
/// runtime errors. For instance, this is used when we get sql that has
|
||||
/// semantic errors.
|
||||
error,
|
||||
|
||||
/// A warning is used when the code affected is technically valid, but
|
||||
/// unlikely to do what the user expects.
|
||||
warning,
|
||||
info,
|
||||
hint
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
|
||||
/// Inputs coming from an external system (such as the analyzer, the build
|
||||
/// package, etc.) that will be further analyzed by moor.
|
||||
abstract class Input {
|
||||
final String path;
|
||||
|
||||
Input(this.path);
|
||||
}
|
||||
|
||||
/// Input for Dart files that have already been analyzed.
|
||||
class DartInput extends Input {
|
||||
final LibraryElement library;
|
||||
|
||||
DartInput(String path, this.library) : super(path);
|
||||
}
|
||||
|
||||
/// Input for a `.moor` file
|
||||
class MoorInput extends Input {
|
||||
final String content;
|
||||
|
||||
MoorInput(String path, this.content) : super(path);
|
||||
}
|
|
@ -1,38 +1,18 @@
|
|||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
|
||||
import 'package:moor_generator/src/model/used_type_converter.dart';
|
||||
import 'package:moor_generator/src/utils/names.dart';
|
||||
import 'package:moor_generator/src/utils/string_escaper.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
/*
|
||||
We're in the process of defining what a .moor file could actually look like.
|
||||
At the moment, we only support "CREATE TABLE" statements:
|
||||
``` // content of a .moor file
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
)
|
||||
```
|
||||
|
||||
In the future, we'd also like to support
|
||||
- import statements between moor files
|
||||
- import statements from moor files referencing tables declared via the Dart DSL
|
||||
- declaring statements in these files, similar to how compiled statements work
|
||||
with the annotation.
|
||||
*/
|
||||
|
||||
class ParsedMoorFile {
|
||||
final List<CreateTable> declaredTables;
|
||||
|
||||
ParsedMoorFile(this.declaredTables);
|
||||
}
|
||||
|
||||
class CreateTable {
|
||||
class CreateTableReader {
|
||||
/// The AST of this `CREATE TABLE` statement.
|
||||
final ParseResult ast;
|
||||
|
||||
CreateTableReader(this.ast);
|
||||
|
||||
SpecifiedTable extractTable(TypeMapper mapper) {
|
||||
final table =
|
||||
SchemaFromCreateTable().read(ast.rootNode as CreateTableStatement);
|
||||
|
@ -47,6 +27,7 @@ class CreateTable {
|
|||
final dartName = ReCase(sqlName).camelCase;
|
||||
final constraintWriter = StringBuffer();
|
||||
final moorType = mapper.resolvedToMoor(column.type);
|
||||
UsedTypeConverter converter;
|
||||
String defaultValue;
|
||||
|
||||
for (var constraint in column.constraints) {
|
||||
|
@ -65,6 +46,12 @@ class CreateTable {
|
|||
defaultValue = '$expressionName(${asDartLiteral(sqlDefault)})';
|
||||
}
|
||||
|
||||
if (constraint is MappedBy) {
|
||||
converter = _readTypeConverter(constraint);
|
||||
// don't write MAPPED BY constraints when creating the table
|
||||
continue;
|
||||
}
|
||||
|
||||
if (constraintWriter.isNotEmpty) {
|
||||
constraintWriter.write(' ');
|
||||
}
|
||||
|
@ -79,6 +66,7 @@ class CreateTable {
|
|||
features: features,
|
||||
customConstraints: constraintWriter.toString(),
|
||||
defaultArgument: defaultValue,
|
||||
typeConverter: converter,
|
||||
);
|
||||
|
||||
foundColumns[column.name] = parsed;
|
||||
|
@ -114,5 +102,8 @@ class CreateTable {
|
|||
);
|
||||
}
|
||||
|
||||
CreateTable(this.ast);
|
||||
UsedTypeConverter _readTypeConverter(MappedBy mapper) {
|
||||
// todo we need to somehow parse the dart expression and check types
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
|
||||
/// Resolves the type of Dart expressions given as a string. The
|
||||
/// [importStatements] are used to discover types.
|
||||
///
|
||||
/// The way this works is that we create a fake file for the analyzer. That file
|
||||
/// has the following content:
|
||||
/// ```
|
||||
/// import 'package:moor/moor.dart'; // always imported
|
||||
/// // all import statements
|
||||
///
|
||||
/// var expr = $expression;
|
||||
/// ```
|
||||
///
|
||||
/// We can then obtain the type of an expression by reading the inferred type
|
||||
/// of the top-level `expr` variable in that source.
|
||||
class InlineDartResolver {
|
||||
final List<String> importStatements = [];
|
||||
final MoorTask task;
|
||||
|
||||
InlineDartResolver(this.task);
|
||||
|
||||
Future<DartType> resolveDartTypeOf(String expression) async {
|
||||
final template = _createDartTemplate(expression);
|
||||
final unit = await task.backendTask.parseSource(template);
|
||||
|
||||
final declaration = unit.declarations.single as TopLevelVariableDeclaration;
|
||||
return declaration.variables.variables.single.initializer.staticType;
|
||||
}
|
||||
|
||||
String _createDartTemplate(String expression) {
|
||||
final fakeDart = StringBuffer();
|
||||
|
||||
fakeDart.write("import 'package:moor/moor.dart';\n");
|
||||
for (var import in importStatements) {
|
||||
fakeDart.write("import '$import';\n");
|
||||
}
|
||||
|
||||
fakeDart.write('var expr = $expression;\n');
|
||||
|
||||
return fakeDart.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/moor/create_table_reader.dart';
|
||||
import 'package:moor_generator/src/analyzer/results.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
class MoorParser {
|
||||
final MoorTask task;
|
||||
|
||||
MoorParser(this.task);
|
||||
|
||||
Future<ParsedMoorFile> parseAndAnalyze() {
|
||||
final results =
|
||||
SqlEngine(useMoorExtensions: true).parseMultiple(task.content);
|
||||
|
||||
final createdReaders = <CreateTableReader>[];
|
||||
|
||||
for (var parsedStmt in results) {
|
||||
if (parsedStmt.rootNode is ImportStatement) {
|
||||
final importStmt = (parsedStmt.rootNode) as ImportStatement;
|
||||
task.inlineDartResolver.importStatements.add(importStmt.importedFile);
|
||||
} else if (parsedStmt.rootNode is CreateTableStatement) {
|
||||
createdReaders.add(CreateTableReader(parsedStmt));
|
||||
} else {
|
||||
task.reportError(ErrorInMoorFile(
|
||||
span: parsedStmt.rootNode.span,
|
||||
message: 'At the moment, only CREATE TABLE statements are supported'
|
||||
'in .moor files'));
|
||||
}
|
||||
}
|
||||
|
||||
// all results have the same list of errors
|
||||
final sqlErrors = results.isEmpty ? <ParsingError>[] : results.first.errors;
|
||||
|
||||
for (var error in sqlErrors) {
|
||||
task.reportError(ErrorInMoorFile(
|
||||
span: error.token.span,
|
||||
message: error.message,
|
||||
));
|
||||
}
|
||||
|
||||
final createdTables =
|
||||
createdReaders.map((r) => r.extractTable(task.mapper)).toList();
|
||||
final parsedFile = ParsedMoorFile(createdTables);
|
||||
|
||||
return Future.value(parsedFile);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:moor_generator/src/model/specified_dao.dart';
|
||||
import 'package:moor_generator/src/model/specified_database.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
|
||||
abstract class ParsedFile {}
|
||||
|
||||
class ParsedDartFile extends ParsedFile {
|
||||
final LibraryElement library;
|
||||
|
||||
final List<SpecifiedTable> declaredTables;
|
||||
final List<SpecifiedDao> declaredDaos;
|
||||
final List<SpecifiedDatabase> declaredDatabases;
|
||||
|
||||
ParsedDartFile(
|
||||
{@required this.library,
|
||||
this.declaredTables = const [],
|
||||
this.declaredDaos = const [],
|
||||
this.declaredDatabases = const []});
|
||||
}
|
||||
|
||||
class ParsedMoorFile extends ParsedFile {
|
||||
final List<SpecifiedTable> declaredTables;
|
||||
|
||||
ParsedMoorFile(this.declaredTables);
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:moor/moor.dart' show Table;
|
||||
import 'package:moor_generator/src/analyzer/dart/parser.dart';
|
||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/moor/inline_dart_resolver.dart';
|
||||
import 'package:moor_generator/src/analyzer/moor/parser.dart';
|
||||
import 'package:moor_generator/src/analyzer/results.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/sql_parser.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/specified_dao.dart';
|
||||
import 'package:moor_generator/src/model/specified_database.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
/// Will store cached data about files that have already been analyzed.
|
||||
class MoorSession {
|
||||
MoorSession();
|
||||
|
||||
Future<DartTask> startDartTask(BackendTask backendTask, {Uri uri}) async {
|
||||
final input = uri ?? backendTask.entrypoint;
|
||||
final library = await backendTask.resolveDart(input);
|
||||
return DartTask(this, backendTask, library);
|
||||
}
|
||||
|
||||
Future<MoorTask> startMoorTask(BackendTask backendTask, {Uri uri}) async {
|
||||
final input = uri ?? backendTask.entrypoint;
|
||||
final source = await backendTask.readMoor(input);
|
||||
return MoorTask(backendTask, this, source);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to parse and analyze a single file.
|
||||
abstract class FileTask<R extends ParsedFile> {
|
||||
final BackendTask backendTask;
|
||||
final MoorSession session;
|
||||
|
||||
final ErrorSink errors = ErrorSink();
|
||||
|
||||
FileTask(this.backendTask, this.session);
|
||||
|
||||
void reportError(MoorError error) => errors.report(error);
|
||||
|
||||
FutureOr<R> compute();
|
||||
|
||||
void printErrors() {
|
||||
final foundErrors = errors.errors;
|
||||
if (foundErrors.isNotEmpty) {
|
||||
final log = backendTask.log;
|
||||
|
||||
log.warning('There were some errors while running '
|
||||
'moor_generator on ${backendTask.entrypoint}:');
|
||||
|
||||
for (var error in foundErrors) {
|
||||
final printer = error.isError ? log.warning : log.info;
|
||||
error.writeDescription(printer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Session used to parse a Dart file and extract table information.
|
||||
class DartTask extends FileTask<ParsedDartFile> {
|
||||
static const tableTypeChecker = const TypeChecker.fromRuntime(Table);
|
||||
|
||||
final LibraryElement library;
|
||||
MoorDartParser _parser;
|
||||
MoorDartParser get parser => _parser;
|
||||
|
||||
DartTask(MoorSession session, BackendTask task, this.library)
|
||||
: super(task, session) {
|
||||
_parser = MoorDartParser(this);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<ParsedDartFile> compute() {
|
||||
// TODO: implement compute
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Parses a [SpecifiedDatabase] from the [ClassElement] which was annotated
|
||||
/// with `@UseMoor` and the [annotation] reader that reads the `@UseMoor`
|
||||
/// annotation.
|
||||
Future<SpecifiedDatabase> parseDatabase(
|
||||
ClassElement element, ConstantReader annotation) {
|
||||
return UseMoorParser(this).parseDatabase(element, annotation);
|
||||
}
|
||||
|
||||
/// Parses a [SpecifiedDao] from a class declaration that has a `UseDao`
|
||||
/// [annotation].
|
||||
Future<SpecifiedDao> parseDao(
|
||||
ClassElement element, ConstantReader annotation) {
|
||||
return UseDaoParser(this).parseDao(element, annotation);
|
||||
}
|
||||
|
||||
/// Resolves a [SpecifiedTable] for the class of each [DartType] in [types].
|
||||
/// The [initializedBy] element should be the piece of code that caused the
|
||||
/// parsing (e.g. the database class that is annotated with `@UseMoor`). This
|
||||
/// will allow for more descriptive error messages.
|
||||
Future<List<SpecifiedTable>> parseTables(
|
||||
Iterable<DartType> types, Element initializedBy) {
|
||||
return Future.wait(types.map((type) {
|
||||
if (!tableTypeChecker.isAssignableFrom(type.element)) {
|
||||
reportError(ErrorInDartCode(
|
||||
severity: Severity.criticalError,
|
||||
message: 'The type $type is not a moor table',
|
||||
affectedElement: initializedBy,
|
||||
));
|
||||
return null;
|
||||
} else {
|
||||
return parser.parseTable(type.element as ClassElement);
|
||||
}
|
||||
})).then((list) {
|
||||
// only keep tables that were resolved successfully
|
||||
return List.from(list.where((t) => t != null));
|
||||
});
|
||||
}
|
||||
|
||||
/// Reads all tables declared in sql by a `.moor` file in [paths].
|
||||
Future<List<SpecifiedTable>> resolveIncludes(Iterable<String> paths) {
|
||||
return Stream.fromFutures(paths.map(
|
||||
(path) => session.startMoorTask(backendTask, uri: Uri.parse(path))))
|
||||
.asyncMap((task) async {
|
||||
final result = await task.compute();
|
||||
|
||||
// add errors from nested task to this task as well.
|
||||
task.errors.errors.forEach(reportError);
|
||||
|
||||
return result;
|
||||
})
|
||||
.expand((file) => file.declaredTables)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<SqlQuery>> parseQueries(
|
||||
Map<DartObject, DartObject> fromAnnotation,
|
||||
List<SpecifiedTable> availableTables) {
|
||||
// no queries declared, so there is no point in starting a sql engine
|
||||
if (fromAnnotation.isEmpty) return Future.value([]);
|
||||
|
||||
final parser = SqlParser(this, availableTables, fromAnnotation)..parse();
|
||||
|
||||
return Future.value(parser.foundQueries);
|
||||
}
|
||||
}
|
||||
|
||||
class MoorTask extends FileTask<ParsedMoorFile> {
|
||||
final String content;
|
||||
final TypeMapper mapper = TypeMapper();
|
||||
/* late final */ InlineDartResolver inlineDartResolver;
|
||||
|
||||
MoorTask(BackendTask task, MoorSession session, this.content)
|
||||
: super(task, session) {
|
||||
inlineDartResolver = InlineDartResolver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<ParsedMoorFile> compute() {
|
||||
final parser = MoorParser(this);
|
||||
return parser.parseAndAnalyze();
|
||||
}
|
||||
}
|
|
@ -52,4 +52,10 @@ class UpdatedTablesVisitor extends RecursiveVisitor<void> {
|
|||
_addIfResolved(e.table);
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInsertStatement(InsertStatement e) {
|
||||
_addIfResolved(e.table);
|
||||
visitChildren(e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
import '../query_handler.dart';
|
||||
|
||||
class Linter {
|
||||
final QueryHandler handler;
|
||||
final List<AnalysisError> lints = [];
|
||||
|
||||
Linter(this.handler);
|
||||
|
||||
void reportLints() {
|
||||
handler.context.root.accept(_LintingVisitor(this));
|
||||
}
|
||||
}
|
||||
|
||||
class _LintingVisitor extends RecursiveVisitor<void> {
|
||||
final Linter linter;
|
||||
|
||||
_LintingVisitor(this.linter);
|
||||
|
||||
@override
|
||||
void visitInsertStatement(InsertStatement e) {
|
||||
final targeted = e.resolvedTargetColumns;
|
||||
if (targeted == null) return;
|
||||
|
||||
// First, check that the amount of values matches the declaration.
|
||||
e.source.when(
|
||||
isValues: (values) {
|
||||
for (var tuple in values.values) {
|
||||
if (tuple.expressions.length != targeted.length) {
|
||||
linter.lints.add(AnalysisError(
|
||||
type: AnalysisErrorType.other,
|
||||
message: 'Expected tuple to have ${targeted.length} values',
|
||||
relevantNode: tuple,
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelect: (select) {
|
||||
final columns = select.stmt.resolvedColumns;
|
||||
|
||||
if (columns.length != targeted.length) {
|
||||
linter.lints.add(AnalysisError(
|
||||
type: AnalysisErrorType.other,
|
||||
message: 'This select statement should return ${targeted.length} '
|
||||
'columns, but actually returns ${columns.length}',
|
||||
relevantNode: select.stmt,
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// second, check that no required columns are left out
|
||||
final specifiedTable =
|
||||
linter.handler.mapper.tableToMoor(e.table.resolved as Table);
|
||||
final required =
|
||||
specifiedTable.columns.where((c) => c.requiredDuringInsert).toList();
|
||||
|
||||
if (required.isNotEmpty && e.source is DefaultValues) {
|
||||
linter.lints.add(AnalysisError(
|
||||
type: AnalysisErrorType.other,
|
||||
message: 'This table has columns without default values, so defaults '
|
||||
'can\'t be used for insert.',
|
||||
relevantNode: e.table,
|
||||
));
|
||||
} else {
|
||||
final notPresent = required.where((c) => !targeted
|
||||
.any((t) => t.name.toUpperCase() == c.name.name.toUpperCase()));
|
||||
|
||||
if (notPresent.isNotEmpty) {
|
||||
final msg = notPresent.join(', ');
|
||||
|
||||
linter.lints.add(AnalysisError(
|
||||
type: AnalysisErrorType.other,
|
||||
message: 'Some columns are required but not present here. Expected '
|
||||
'values for $msg.',
|
||||
relevantNode: e.source.childNodes.first,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:moor_generator/src/model/used_type_converter.dart';
|
||||
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||
import 'package:moor_generator/src/utils/type_converter_hint.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||
|
||||
import 'affected_tables_visitor.dart';
|
||||
import 'lints/linter.dart';
|
||||
|
||||
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
|
||||
/// generator package by determining its type, return columns, variables and so
|
||||
/// on.
|
||||
class QueryHandler {
|
||||
final String name;
|
||||
final AnalysisContext context;
|
||||
|
@ -19,14 +23,25 @@ class QueryHandler {
|
|||
QueryHandler(this.name, this.context, this.mapper);
|
||||
|
||||
SqlQuery handle() {
|
||||
final root = context.root;
|
||||
_foundVariables = mapper.extractVariables(context);
|
||||
|
||||
_verifyNoSkippedIndexes();
|
||||
final query = _mapToMoor();
|
||||
|
||||
final linter = Linter(this);
|
||||
linter.reportLints();
|
||||
query.lints = linter.lints;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
SqlQuery _mapToMoor() {
|
||||
final root = context.root;
|
||||
if (root is SelectStatement) {
|
||||
return _handleSelect();
|
||||
} else if (root is UpdateStatement || root is DeleteStatement) {
|
||||
} else if (root is UpdateStatement ||
|
||||
root is DeleteStatement ||
|
||||
root is InsertStatement) {
|
||||
return _handleUpdate();
|
||||
} else {
|
||||
throw StateError(
|
||||
|
@ -39,8 +54,11 @@ class QueryHandler {
|
|||
context.root.accept(updatedFinder);
|
||||
_foundTables = updatedFinder.foundTables;
|
||||
|
||||
final isInsert = context.root is InsertStatement;
|
||||
|
||||
return UpdatingQuery(name, context, _foundVariables,
|
||||
_foundTables.map(mapper.tableToMoor).toList());
|
||||
_foundTables.map(mapper.tableToMoor).toList(),
|
||||
isInsert: isInsert);
|
||||
}
|
||||
|
||||
SqlSelectQuery _handleSelect() {
|
|
@ -1,16 +1,16 @@
|
|||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/state/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:moor_generator/src/parser/sql/query_handler.dart';
|
||||
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||
|
||||
class SqlParser {
|
||||
final List<SpecifiedTable> tables;
|
||||
final GeneratorSession session;
|
||||
final FileTask task;
|
||||
final Map<DartObject, DartObject> definedQueries;
|
||||
|
||||
final TypeMapper _mapper = TypeMapper();
|
||||
|
@ -18,7 +18,7 @@ class SqlParser {
|
|||
|
||||
final List<SqlQuery> foundQueries = [];
|
||||
|
||||
SqlParser(this.session, this.tables, this.definedQueries);
|
||||
SqlParser(this.task, this.tables, this.definedQueries);
|
||||
|
||||
void _spawnEngine() {
|
||||
_engine = SqlEngine();
|
||||
|
@ -36,23 +36,34 @@ class SqlParser {
|
|||
try {
|
||||
context = _engine.analyze(sql);
|
||||
} catch (e, s) {
|
||||
session.errors.add(MoorError(
|
||||
critical: true,
|
||||
message: 'Error while trying to parse $sql: $e, $s'));
|
||||
task.reportError(MoorError(
|
||||
severity: Severity.criticalError,
|
||||
message: 'Error while trying to parse $key: $e, $s'));
|
||||
return;
|
||||
}
|
||||
|
||||
for (var error in context.errors) {
|
||||
session.errors.add(MoorError(
|
||||
message: 'The sql query $sql is invalid: $error',
|
||||
task.reportError(MoorError(
|
||||
severity: Severity.warning,
|
||||
message: 'The sql query $key is invalid: $error',
|
||||
));
|
||||
}
|
||||
|
||||
try {
|
||||
foundQueries.add(QueryHandler(name, context, _mapper).handle());
|
||||
} catch (e, s) {
|
||||
log.warning('Error while generating APIs for ${context.sql}', e, s);
|
||||
log.warning('Error while generating APIs for $key', e, s);
|
||||
}
|
||||
});
|
||||
|
||||
// report lints
|
||||
for (var query in foundQueries) {
|
||||
for (var lint in query.lints) {
|
||||
task.reportError(MoorError(
|
||||
severity: Severity.warning,
|
||||
message: 'Lint for ${query.name}: $lint',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
|
||||
/// A backend for the moor generator.
|
||||
///
|
||||
/// Currently, we only have a backend based on the build package, but we can
|
||||
/// extend this to a backend for an analyzer plugin or a standalone tool.
|
||||
abstract class Backend {
|
||||
final MoorSession session = MoorSession();
|
||||
}
|
||||
|
||||
/// Used to analyze a single file via ([entrypoint]). The other methods can be
|
||||
/// used to read imports used by the other files.
|
||||
abstract class BackendTask {
|
||||
Uri get entrypoint;
|
||||
Logger get log;
|
||||
|
||||
Future<LibraryElement> resolveDart(Uri uri);
|
||||
Future<CompilationUnit> parseSource(String dart);
|
||||
Future<String> readMoor(Uri uri);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:analyzer/analyzer.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart' hide log;
|
||||
import 'package:build/build.dart' as build show log;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moor_generator/src/backends/backend.dart';
|
||||
|
||||
class BuildBackend extends Backend {
|
||||
BuildBackendTask createTask(BuildStep step) {
|
||||
return BuildBackendTask(step);
|
||||
}
|
||||
}
|
||||
|
||||
class BuildBackendTask extends BackendTask {
|
||||
final BuildStep step;
|
||||
|
||||
BuildBackendTask(this.step);
|
||||
|
||||
@override
|
||||
Uri get entrypoint => step.inputId.uri;
|
||||
|
||||
AssetId _resolve(Uri uri) {
|
||||
return AssetId.resolve(uri.toString(), from: step.inputId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readMoor(Uri uri) {
|
||||
return step.readAsString(_resolve(uri));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LibraryElement> resolveDart(Uri uri) {
|
||||
return step.resolver.libraryFor(_resolve(uri));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CompilationUnit> parseSource(String dart) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Logger get log => build.log;
|
||||
}
|
|
@ -1,24 +1,20 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor_generator/src/state/generator_state.dart';
|
||||
import 'package:moor_generator/src/state/options.dart';
|
||||
import 'package:moor_generator/src/writer/query_writer.dart';
|
||||
import 'package:moor_generator/src/writer/result_set_writer.dart';
|
||||
import 'package:moor_generator/src/backends/build/moor_builder.dart';
|
||||
import 'package:moor_generator/src/writer/queries/query_writer.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
import 'model/sql_query.dart';
|
||||
|
||||
class DaoGenerator extends GeneratorForAnnotation<UseDao> {
|
||||
final MoorOptions options;
|
||||
|
||||
DaoGenerator(this.options);
|
||||
class DaoGenerator extends GeneratorForAnnotation<UseDao>
|
||||
implements BaseGenerator {
|
||||
@override
|
||||
MoorBuilder builder;
|
||||
|
||||
@override
|
||||
generateForAnnotatedElement(
|
||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
||||
final state = useState(() => GeneratorState(options));
|
||||
final session = state.startSession(buildStep);
|
||||
final task = await builder.createDartTask(buildStep);
|
||||
|
||||
if (element is! ClassElement) {
|
||||
throw InvalidGenerationSourceError(
|
||||
|
@ -27,7 +23,7 @@ class DaoGenerator extends GeneratorForAnnotation<UseDao> {
|
|||
}
|
||||
|
||||
final targetClass = element as ClassElement;
|
||||
final parsedDao = await session.parseDao(targetClass, annotation);
|
||||
final parsedDao = await task.parseDao(targetClass, annotation);
|
||||
|
||||
final dbType = targetClass.supertype;
|
||||
if (dbType.name != 'DatabaseAccessor') {
|
||||
|
@ -46,33 +42,27 @@ class DaoGenerator extends GeneratorForAnnotation<UseDao> {
|
|||
}
|
||||
|
||||
// finally, we can write the mixin
|
||||
final buffer = StringBuffer();
|
||||
final writer = Writer(builder.options);
|
||||
final classScope = writer.child();
|
||||
|
||||
final daoName = targetClass.displayName;
|
||||
|
||||
buffer.write('mixin _\$${daoName}Mixin on '
|
||||
classScope.leaf().write('mixin _\$${daoName}Mixin on '
|
||||
'DatabaseAccessor<${dbImpl.displayName}> {\n');
|
||||
|
||||
for (var table in parsedDao.tables) {
|
||||
final infoType = table.tableInfoName;
|
||||
final getterName = table.tableFieldName;
|
||||
buffer.write('$infoType get $getterName => db.$getterName;\n');
|
||||
classScope.leaf().write('$infoType get $getterName => db.$getterName;\n');
|
||||
}
|
||||
|
||||
final writtenMappingMethods = <String>{};
|
||||
for (var query in parsedDao.queries) {
|
||||
QueryWriter(query, session, writtenMappingMethods).writeInto(buffer);
|
||||
QueryWriter(query, classScope.child(), writtenMappingMethods).write();
|
||||
}
|
||||
|
||||
buffer.write('}');
|
||||
classScope.leaf().write('}');
|
||||
|
||||
// if the queries introduced additional classes, also write those
|
||||
for (final query in parsedDao.queries) {
|
||||
if (query is SqlSelectQuery && query.resultSet.matchingTable == null) {
|
||||
ResultSetWriter(query).write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
return writer.writeGenerated();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/backends/build/moor_builder.dart';
|
||||
import 'package:moor_generator/src/writer/database_writer.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
class MoorGenerator extends GeneratorForAnnotation<UseMoor>
|
||||
implements BaseGenerator {
|
||||
@override
|
||||
MoorBuilder builder;
|
||||
|
||||
@override
|
||||
generateForAnnotatedElement(
|
||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
||||
final task = await builder.createDartTask(buildStep);
|
||||
|
||||
if (element is! ClassElement) {
|
||||
task.reportError(ErrorInDartCode(
|
||||
severity: Severity.criticalError,
|
||||
message: 'This annotation can only be used on classes',
|
||||
affectedElement: element,
|
||||
));
|
||||
}
|
||||
|
||||
final database =
|
||||
await task.parseDatabase(element as ClassElement, annotation);
|
||||
|
||||
task.printErrors();
|
||||
|
||||
if (database.tables.isEmpty) return '';
|
||||
|
||||
final writer = Writer(builder.options);
|
||||
writer
|
||||
.leaf()
|
||||
.write('// ignore_for_file: unnecessary_brace_in_string_interps\n');
|
||||
|
||||
DatabaseWriter(database, writer.child()).write();
|
||||
|
||||
return writer.writeGenerated();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
import 'package:moor_generator/src/backends/build/build_backend.dart';
|
||||
import 'package:moor_generator/src/backends/build/generators/dao_generator.dart';
|
||||
import 'package:moor_generator/src/backends/build/generators/moor_generator.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
part 'options.dart';
|
||||
|
||||
class MoorBuilder extends SharedPartBuilder {
|
||||
final BuildBackend backend = BuildBackend();
|
||||
final MoorOptions options;
|
||||
|
||||
factory MoorBuilder(BuilderOptions options) {
|
||||
final parsedOptions = MoorOptions.fromBuilder(options.config);
|
||||
|
||||
final generators = <Generator>[
|
||||
MoorGenerator(),
|
||||
DaoGenerator(),
|
||||
];
|
||||
|
||||
final builder = MoorBuilder._(generators, 'moor', parsedOptions);
|
||||
|
||||
for (var generator in generators.cast<BaseGenerator>()) {
|
||||
generator.builder = builder;
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
MoorBuilder._(List<Generator> generators, String name, this.options)
|
||||
: super(generators, name);
|
||||
|
||||
Future<DartTask> createDartTask(BuildStep step) async {
|
||||
final backendTask = backend.createTask(step);
|
||||
return await backend.session
|
||||
.startDartTask(backendTask, uri: step.inputId.uri);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseGenerator {
|
||||
MoorBuilder builder;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
part of 'moor_builder.dart';
|
||||
|
||||
class MoorOptions {
|
||||
final bool generateFromJsonStringConstructor;
|
||||
|
|
@ -143,6 +143,14 @@ class SpecifiedColumn {
|
|||
ColumnType.real: 'GeneratedRealColumn',
|
||||
}[type];
|
||||
|
||||
/// Whether this column is required for insert statements, meaning that a
|
||||
/// non-absent value must be provided for an insert statement to be valid.
|
||||
bool get requiredDuringInsert {
|
||||
final aliasForPk = type == ColumnType.integer &&
|
||||
features.any((f) => f is PrimaryKey || f is AutoIncrement);
|
||||
return !nullable && defaultArgument == null && !aliasForPk;
|
||||
}
|
||||
|
||||
/// The class inside the moor library that represents the same sql type as
|
||||
/// this column.
|
||||
String get sqlTypeName => sqlTypes[type];
|
||||
|
|
|
@ -10,6 +10,8 @@ final _leadingDigits = RegExp(r'^\d*');
|
|||
abstract class SqlQuery {
|
||||
final String name;
|
||||
final AnalysisContext fromContext;
|
||||
List<AnalysisError> lints;
|
||||
|
||||
String get sql => fromContext.sql;
|
||||
|
||||
/// The variables that appear in the [sql] query. We support three kinds of
|
||||
|
@ -51,9 +53,11 @@ class SqlSelectQuery extends SqlQuery {
|
|||
|
||||
class UpdatingQuery extends SqlQuery {
|
||||
final List<SpecifiedTable> updates;
|
||||
final bool isInsert;
|
||||
|
||||
UpdatingQuery(String name, AnalysisContext fromContext,
|
||||
List<FoundVariable> variables, this.updates)
|
||||
List<FoundVariable> variables, this.updates,
|
||||
{this.isInsert = false})
|
||||
: super(name, fromContext, variables);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/state/errors.dart';
|
||||
import 'package:moor_generator/src/state/generator_state.dart';
|
||||
import 'package:moor_generator/src/state/options.dart';
|
||||
import 'package:moor_generator/src/writer/database_writer.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
class MoorGenerator extends GeneratorForAnnotation<UseMoor> {
|
||||
final MoorOptions options;
|
||||
MoorGenerator(this.options);
|
||||
|
||||
@override
|
||||
generateForAnnotatedElement(
|
||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
||||
final state = useState(() => GeneratorState(options));
|
||||
final session = state.startSession(buildStep);
|
||||
|
||||
if (element is! ClassElement) {
|
||||
session.errors.add(MoorError(
|
||||
critical: true,
|
||||
message: 'This annotation can only be used on classes',
|
||||
affectedElement: element,
|
||||
));
|
||||
}
|
||||
|
||||
final database =
|
||||
await session.parseDatabase(element as ClassElement, annotation);
|
||||
|
||||
if (session.errors.errors.isNotEmpty) {
|
||||
print('Warning: There were some errors while running '
|
||||
'moor_generator on ${buildStep.inputId.path}:');
|
||||
|
||||
for (var error in session.errors.errors) {
|
||||
print(error.message);
|
||||
|
||||
if (error.affectedElement != null) {
|
||||
final span = spanForElement(error.affectedElement);
|
||||
print('${span.start.toolString}\n${span.highlight()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (database.tables.isEmpty) return '';
|
||||
|
||||
final buffer = StringBuffer()
|
||||
..write('// ignore_for_file: unnecessary_brace_in_string_interps\n');
|
||||
|
||||
DatabaseWriter(database, session).write(buffer);
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import 'package:moor_generator/src/parser/moor/parsed_moor_file.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
/// Parses and analyzes the experimental `.moor` files containing sql
|
||||
/// statements.
|
||||
class MoorAnalyzer {
|
||||
/// Content of the `.moor` file we're analyzing.
|
||||
final String content;
|
||||
|
||||
MoorAnalyzer(this.content);
|
||||
|
||||
Future<MoorParsingResult> analyze() {
|
||||
final engine = SqlEngine();
|
||||
final tokens = engine.tokenize(content);
|
||||
final results = SqlEngine().parseMultiple(tokens, content);
|
||||
|
||||
final createdTables = <CreateTable>[];
|
||||
final errors = <MoorParsingError>[];
|
||||
|
||||
for (var parsedStmt in results) {
|
||||
if (parsedStmt.rootNode is CreateTableStatement) {
|
||||
createdTables.add(CreateTable(parsedStmt));
|
||||
} else {
|
||||
errors.add(
|
||||
MoorParsingError(
|
||||
parsedStmt.rootNode.span,
|
||||
message:
|
||||
'At the moment, only CREATE TABLE statements are supported in .moor files',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// all results have the same list of errors
|
||||
final sqlErrors = results.isEmpty ? <ParsingError>[] : results.first.errors;
|
||||
|
||||
for (var error in sqlErrors) {
|
||||
errors.add(MoorParsingError(error.token.span, message: error.message));
|
||||
}
|
||||
|
||||
final parsedFile = ParsedMoorFile(createdTables);
|
||||
|
||||
return Future.value(MoorParsingResult(parsedFile, errors));
|
||||
}
|
||||
}
|
||||
|
||||
class MoorParsingResult {
|
||||
final ParsedMoorFile parsedFile;
|
||||
final List<MoorParsingError> errors;
|
||||
|
||||
MoorParsingResult(this.parsedFile, this.errors);
|
||||
}
|
||||
|
||||
class MoorParsingError {
|
||||
final FileSpan span;
|
||||
final String message;
|
||||
|
||||
MoorParsingError(this.span, {this.message});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return span.message(message, color: true);
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:moor_generator/src/state/errors.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
|
||||
class Parser {
|
||||
List<SpecifiedTable> specifiedTables;
|
||||
|
||||
void init() async {}
|
||||
}
|
||||
|
||||
class ParserBase {
|
||||
final GeneratorSession session;
|
||||
|
||||
ParserBase(this.session);
|
||||
|
||||
Expression returnExpressionOfMethod(MethodDeclaration method) {
|
||||
final body = method.body;
|
||||
|
||||
if (!(body is ExpressionFunctionBody)) {
|
||||
session.errors.add(MoorError(
|
||||
affectedElement: method.declaredElement,
|
||||
critical: true,
|
||||
message:
|
||||
'This method must have an expression body (use => instead of {return ...})'));
|
||||
return null;
|
||||
}
|
||||
|
||||
return (method.body as ExpressionFunctionBody).expression;
|
||||
}
|
||||
|
||||
String readStringLiteral(Expression expression, void onError()) {
|
||||
if (!(expression is StringLiteral)) {
|
||||
onError();
|
||||
} else {
|
||||
final value = (expression as StringLiteral).stringValue;
|
||||
if (value == null) {
|
||||
onError();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int readIntLiteral(Expression expression, void onError()) {
|
||||
if (!(expression is IntegerLiteral)) {
|
||||
onError();
|
||||
// ignore: avoid_returning_null
|
||||
return null;
|
||||
} else {
|
||||
return (expression as IntegerLiteral).value;
|
||||
}
|
||||
}
|
||||
|
||||
Expression findNamedArgument(ArgumentList args, String argName) {
|
||||
final argument = args.arguments.singleWhere(
|
||||
(e) => e is NamedExpression && e.name.label.name == argName,
|
||||
orElse: () => null) as NamedExpression;
|
||||
|
||||
return argument?.expression;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
|
||||
class MoorError {
|
||||
final bool critical;
|
||||
final String message;
|
||||
final Element affectedElement;
|
||||
|
||||
MoorError({this.critical = false, this.message, this.affectedElement});
|
||||
}
|
||||
|
||||
class ErrorStore {
|
||||
final List<MoorError> errors = [];
|
||||
|
||||
void add(MoorError error) => errors.add(error);
|
||||
|
||||
bool get hasCriticalError => errors.any((e) => e.critical);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
GeneratorState _state;
|
||||
|
||||
/// Uses the created instance of the generator state or creates one via the
|
||||
/// [create] callback if necessary.
|
||||
GeneratorState useState(GeneratorState Function() create) {
|
||||
return _state ??= create();
|
||||
}
|
||||
|
||||
class GeneratorState {
|
||||
final MoorOptions options;
|
||||
|
||||
final Map<DartType, Future<SpecifiedTable>> _foundTables = {};
|
||||
final tableTypeChecker = const TypeChecker.fromRuntime(Table);
|
||||
|
||||
GeneratorState(this.options);
|
||||
|
||||
GeneratorSession startSession(BuildStep step) {
|
||||
return GeneratorSession(this, step);
|
||||
}
|
||||
|
||||
/// Parses the [SpecifiedTable] from a [type]. As this operation is very
|
||||
/// expensive, we always try to only perform it once.
|
||||
///
|
||||
/// The [resolve] function is responsible for performing the actual analysis
|
||||
/// and it will be called when the [type] has not yet been resolved.
|
||||
Future<SpecifiedTable> parseTable(
|
||||
DartType type, Future<SpecifiedTable> Function() resolve) {
|
||||
return _foundTables.putIfAbsent(type, resolve);
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_dao.dart';
|
||||
import 'package:moor_generator/src/model/specified_database.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:moor_generator/src/parser/column_parser.dart';
|
||||
import 'package:moor_generator/src/parser/moor/moor_analyzer.dart';
|
||||
import 'package:moor_generator/src/parser/sql/sql_parser.dart';
|
||||
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
|
||||
import 'package:moor_generator/src/parser/table_parser.dart';
|
||||
import 'package:moor_generator/src/parser/use_dao_parser.dart';
|
||||
import 'package:moor_generator/src/parser/use_moor_parser.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
|
||||
import 'errors.dart';
|
||||
import 'generator_state.dart';
|
||||
import 'options.dart';
|
||||
import 'writer.dart';
|
||||
|
||||
class GeneratorSession {
|
||||
final GeneratorState state;
|
||||
final ErrorStore errors = ErrorStore();
|
||||
final BuildStep step;
|
||||
|
||||
final Writer writer = Writer();
|
||||
|
||||
TableParser _tableParser;
|
||||
ColumnParser _columnParser;
|
||||
|
||||
MoorOptions get options => state.options;
|
||||
|
||||
GeneratorSession(this.state, this.step) {
|
||||
_tableParser = TableParser(this);
|
||||
_columnParser = ColumnParser(this);
|
||||
}
|
||||
|
||||
Future<ElementDeclarationResult> loadElementDeclaration(
|
||||
Element element) async {
|
||||
final resolvedLibrary = await element.library.session
|
||||
.getResolvedLibraryByElement(element.library);
|
||||
|
||||
return resolvedLibrary.getElementDeclaration(element);
|
||||
}
|
||||
|
||||
/// Parses a [SpecifiedDatabase] from the [ClassElement] which was annotated
|
||||
/// with `@UseMoor` and the [annotation] reader that reads the `@UseMoor`
|
||||
/// annotation.
|
||||
Future<SpecifiedDatabase> parseDatabase(
|
||||
ClassElement element, ConstantReader annotation) {
|
||||
return UseMoorParser(this).parseDatabase(element, annotation);
|
||||
}
|
||||
|
||||
/// Parses a [SpecifiedDao] from a class declaration that has a `UseDao`
|
||||
/// [annotation].
|
||||
Future<SpecifiedDao> parseDao(
|
||||
ClassElement element, ConstantReader annotation) {
|
||||
return UseDaoParser(this).parseDao(element, annotation);
|
||||
}
|
||||
|
||||
/// Resolves a [SpecifiedTable] for the class of each [DartType] in [types].
|
||||
/// The [initializedBy] element should be the piece of code that caused the
|
||||
/// parsing (e.g. the database class that is annotated with `@UseMoor`). This
|
||||
/// will allow for more descriptive error messages.
|
||||
Future<List<SpecifiedTable>> parseTables(
|
||||
Iterable<DartType> types, Element initializedBy) {
|
||||
return Future.wait(types.map((type) {
|
||||
if (!state.tableTypeChecker.isAssignableFrom(type.element)) {
|
||||
errors.add(MoorError(
|
||||
critical: true,
|
||||
message: 'The type $type is not a moor table',
|
||||
affectedElement: initializedBy,
|
||||
));
|
||||
return null;
|
||||
} else {
|
||||
return _tableParser.parse(type.element as ClassElement);
|
||||
}
|
||||
})).then((list) => List.from(list)); // make growable
|
||||
}
|
||||
|
||||
Future<List<SpecifiedTable>> resolveIncludes(Iterable<String> paths) async {
|
||||
final mapper = TypeMapper();
|
||||
final foundTables = <SpecifiedTable>[];
|
||||
|
||||
for (var path in paths) {
|
||||
final asset = AssetId.resolve(path, from: step.inputId);
|
||||
String content;
|
||||
try {
|
||||
content = await step.readAsString(asset);
|
||||
} catch (e) {
|
||||
errors.add(MoorError(
|
||||
critical: true,
|
||||
message: 'The included file $path could not be found'));
|
||||
}
|
||||
|
||||
final parsed = await MoorAnalyzer(content).analyze();
|
||||
foundTables.addAll(
|
||||
parsed.parsedFile.declaredTables.map((t) => t.extractTable(mapper)));
|
||||
|
||||
for (var parseError in parsed.errors) {
|
||||
errors.add(MoorError(message: "Can't parse sql in $path: $parseError"));
|
||||
}
|
||||
}
|
||||
|
||||
return foundTables;
|
||||
}
|
||||
|
||||
/// Parses a column from a getter [e] declared inside a table class and its
|
||||
/// resolved AST node [m].
|
||||
Future<SpecifiedColumn> parseColumn(MethodDeclaration m, Element e) {
|
||||
return Future.value(_columnParser.parse(m, e));
|
||||
}
|
||||
|
||||
Future<List<SqlQuery>> parseQueries(
|
||||
Map<DartObject, DartObject> fromAnnotation,
|
||||
List<SpecifiedTable> availableTables) {
|
||||
// no queries declared, so there is no point in starting a sql engine
|
||||
if (fromAnnotation.isEmpty) return Future.value([]);
|
||||
|
||||
final parser = SqlParser(this, availableTables, fromAnnotation)..parse();
|
||||
|
||||
return Future.value(parser.foundQueries);
|
||||
}
|
||||
}
|
|
@ -1,34 +1,28 @@
|
|||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/writer/query_writer.dart';
|
||||
import 'package:moor_generator/src/writer/result_set_writer.dart';
|
||||
import 'package:moor_generator/src/writer/queries/query_writer.dart';
|
||||
import 'package:moor_generator/src/writer/tables/table_writer.dart';
|
||||
import 'package:moor_generator/src/writer/utils/memoized_getter.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:moor_generator/src/model/specified_database.dart';
|
||||
import 'package:moor_generator/src/writer/table_writer.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class DatabaseWriter {
|
||||
final SpecifiedDatabase db;
|
||||
final GeneratorSession session;
|
||||
final Scope scope;
|
||||
|
||||
DatabaseWriter(this.db, this.session);
|
||||
DatabaseWriter(this.db, this.scope);
|
||||
|
||||
void write(StringBuffer buffer) {
|
||||
void write() {
|
||||
// Write referenced tables
|
||||
for (final table in db.tables) {
|
||||
TableWriter(table, session).writeInto(buffer);
|
||||
}
|
||||
|
||||
// Write additional classes to hold the result of custom queries
|
||||
for (final query in db.queries) {
|
||||
if (query is SqlSelectQuery && query.resultSet.matchingTable == null) {
|
||||
ResultSetWriter(query).write(buffer);
|
||||
}
|
||||
TableWriter(table, scope.child()).writeInto();
|
||||
}
|
||||
|
||||
// Write the database class
|
||||
final dbScope = scope.child();
|
||||
|
||||
final className = '_\$${db.fromClass.name}';
|
||||
buffer.write('abstract class $className extends GeneratedDatabase {\n'
|
||||
dbScope.leaf().write(
|
||||
'abstract class $className extends GeneratedDatabase {\n'
|
||||
'$className(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); \n');
|
||||
|
||||
final tableGetters = <String>[];
|
||||
|
@ -38,7 +32,7 @@ class DatabaseWriter {
|
|||
final tableClassName = table.tableInfoName;
|
||||
|
||||
writeMemoizedGetter(
|
||||
buffer: buffer,
|
||||
buffer: dbScope.leaf(),
|
||||
getterName: table.tableFieldName,
|
||||
returnType: tableClassName,
|
||||
code: '$tableClassName(this)',
|
||||
|
@ -52,7 +46,7 @@ class DatabaseWriter {
|
|||
final databaseImplName = db.fromClass.name;
|
||||
|
||||
writeMemoizedGetter(
|
||||
buffer: buffer,
|
||||
buffer: dbScope.leaf(),
|
||||
getterName: getterName,
|
||||
returnType: typeName,
|
||||
code: '$typeName(this as $databaseImplName)',
|
||||
|
@ -62,11 +56,11 @@ class DatabaseWriter {
|
|||
// Write implementation for query methods
|
||||
final writtenMappingMethods = <String>{};
|
||||
for (var query in db.queries) {
|
||||
QueryWriter(query, session, writtenMappingMethods).writeInto(buffer);
|
||||
QueryWriter(query, dbScope.child(), writtenMappingMethods).write();
|
||||
}
|
||||
|
||||
// Write List of tables, close bracket for class
|
||||
buffer
|
||||
dbScope.leaf()
|
||||
..write('@override\nList<TableInfo> get allTables => [')
|
||||
..write(tableGetters.join(','))
|
||||
..write('];\n}');
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:math' show max;
|
||||
|
||||
import 'package:moor_generator/src/backends/build/moor_builder.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/utils/string_escaper.dart';
|
||||
import 'package:moor_generator/src/writer/queries/result_set_writer.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
|
@ -16,13 +18,18 @@ const highestAssignedIndexVar = '\$highestIndex';
|
|||
/// should be included in a generated database or dao class.
|
||||
class QueryWriter {
|
||||
final SqlQuery query;
|
||||
final GeneratorSession session;
|
||||
final Scope scope;
|
||||
SqlSelectQuery get _select => query as SqlSelectQuery;
|
||||
UpdatingQuery get _update => query as UpdatingQuery;
|
||||
|
||||
MoorOptions get options => scope.writer.options;
|
||||
StringBuffer _buffer;
|
||||
|
||||
final Set<String> _writtenMappingMethods;
|
||||
|
||||
QueryWriter(this.query, this.session, this._writtenMappingMethods);
|
||||
QueryWriter(this.query, this.scope, this._writtenMappingMethods) {
|
||||
_buffer = scope.leaf();
|
||||
}
|
||||
|
||||
/// The expanded sql that we insert into queries whenever an array variable
|
||||
/// appears. For the query "SELECT * FROM t WHERE x IN ?", we generate
|
||||
|
@ -36,31 +43,42 @@ class QueryWriter {
|
|||
return 'expanded${v.dartParameterName}';
|
||||
}
|
||||
|
||||
void writeInto(StringBuffer buffer) {
|
||||
void write() {
|
||||
if (query is SqlSelectQuery) {
|
||||
_writeSelect(buffer);
|
||||
final select = query as SqlSelectQuery;
|
||||
if (select.resultSet.matchingTable == null) {
|
||||
// query needs its own result set - write that now
|
||||
final buffer = scope.findScopeOfLevel(DartScope.library).leaf();
|
||||
ResultSetWriter(select).write(buffer);
|
||||
}
|
||||
_writeSelect();
|
||||
} else if (query is UpdatingQuery) {
|
||||
_writeUpdatingQuery(buffer);
|
||||
_writeUpdatingQuery();
|
||||
}
|
||||
}
|
||||
|
||||
void _writeSelect(StringBuffer buffer) {
|
||||
_writeMapping(buffer);
|
||||
_writeOneTimeReader(buffer);
|
||||
_writeStreamReader(buffer);
|
||||
void _writeSelect() {
|
||||
_writeMapping();
|
||||
_writeSelectStatementCreator();
|
||||
_writeOneTimeReader();
|
||||
_writeStreamReader();
|
||||
}
|
||||
|
||||
String _nameOfMappingMethod() {
|
||||
return '_rowTo${_select.resultClassName}';
|
||||
}
|
||||
|
||||
String _nameOfCreationMethod() {
|
||||
return '${_select.name}Query';
|
||||
}
|
||||
|
||||
/// Writes a mapping method that turns a "QueryRow" into the desired custom
|
||||
/// return type.
|
||||
void _writeMapping(StringBuffer buffer) {
|
||||
void _writeMapping() {
|
||||
// avoid writing mapping methods twice if the same result class is written
|
||||
// more than once.
|
||||
if (!_writtenMappingMethods.contains(_nameOfMappingMethod())) {
|
||||
buffer
|
||||
_buffer
|
||||
..write('${_select.resultClassName} ${_nameOfMappingMethod()}')
|
||||
..write('(QueryRow row) {\n')
|
||||
..write('return ${_select.resultClassName}(');
|
||||
|
@ -79,85 +97,97 @@ class QueryWriter {
|
|||
code = '$field.mapToDart($code)';
|
||||
}
|
||||
|
||||
buffer.write('$fieldName: $code,');
|
||||
_buffer.write('$fieldName: $code,');
|
||||
}
|
||||
|
||||
buffer.write(');\n}\n');
|
||||
_buffer.write(');\n}\n');
|
||||
_writtenMappingMethods.add(_nameOfMappingMethod());
|
||||
}
|
||||
}
|
||||
|
||||
void _writeOneTimeReader(StringBuffer buffer) {
|
||||
buffer.write('Future<List<${_select.resultClassName}>> ${query.name}(');
|
||||
_writeParameters(buffer);
|
||||
buffer.write(') {\n');
|
||||
_writeExpandedDeclarations(buffer);
|
||||
buffer
|
||||
..write('return (operateOn ?? this).') // use custom engine, if set
|
||||
..write('customSelect(${_queryCode()},');
|
||||
_writeVariables(buffer);
|
||||
buffer
|
||||
..write(')')
|
||||
..write(
|
||||
'.then((rows) => rows.map(${_nameOfMappingMethod()}).toList());\n')
|
||||
..write('\n}\n');
|
||||
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
|
||||
/// of the custom query.
|
||||
void _writeSelectStatementCreator() {
|
||||
final returnType = 'Selectable<${_select.resultClassName}>';
|
||||
final methodName = _nameOfCreationMethod();
|
||||
|
||||
_buffer.write('$returnType $methodName(');
|
||||
_writeParameters();
|
||||
_buffer.write(') {\n');
|
||||
|
||||
_writeExpandedDeclarations();
|
||||
_buffer
|
||||
..write('return (operateOn ?? this).')
|
||||
..write('customSelectQuery(${_queryCode()}, ');
|
||||
_writeVariables();
|
||||
_buffer.write(', ');
|
||||
_writeReadsFrom();
|
||||
|
||||
_buffer.write(').map(');
|
||||
_buffer.write(_nameOfMappingMethod());
|
||||
_buffer.write(');\n}\n');
|
||||
}
|
||||
|
||||
void _writeStreamReader(StringBuffer buffer) {
|
||||
// turning the query name into pascal case will remove underscores
|
||||
/*
|
||||
Future<List<AllTodosWithCategoryResult>> allTodos(String name,
|
||||
{QueryEngine overrideEngine}) {
|
||||
return _allTodosWithCategoryQuery(name, engine: overrideEngine).get();
|
||||
}
|
||||
*/
|
||||
|
||||
void _writeOneTimeReader() {
|
||||
_buffer.write('Future<List<${_select.resultClassName}>> ${query.name}(');
|
||||
_writeParameters();
|
||||
_buffer..write(') {\n')..write('return ${_nameOfCreationMethod()}(');
|
||||
_writeUseParameters();
|
||||
_buffer.write(').get();\n}\n');
|
||||
}
|
||||
|
||||
void _writeStreamReader() {
|
||||
final upperQueryName = ReCase(query.name).pascalCase;
|
||||
|
||||
String methodName;
|
||||
if (session.options.fixPrivateWatchMethods && query.name.startsWith('_')) {
|
||||
// turning the query name into pascal case will remove underscores, add the
|
||||
// "private" modifier back in if needed
|
||||
if (scope.writer.options.fixPrivateWatchMethods &&
|
||||
query.name.startsWith('_')) {
|
||||
methodName = '_watch$upperQueryName';
|
||||
} else {
|
||||
methodName = 'watch$upperQueryName';
|
||||
}
|
||||
|
||||
buffer.write('Stream<List<${_select.resultClassName}>> $methodName(');
|
||||
// don't supply an engine override parameter because select streams cannot
|
||||
// be used in transaction or similar context, only on the main database
|
||||
// engine.
|
||||
_writeParameters(buffer, dontOverrideEngine: true);
|
||||
buffer.write(') {\n');
|
||||
|
||||
_writeExpandedDeclarations(buffer);
|
||||
buffer..write('return customSelectStream(${_queryCode()},');
|
||||
|
||||
_writeVariables(buffer);
|
||||
buffer.write(',');
|
||||
_writeReadsFrom(buffer);
|
||||
|
||||
buffer
|
||||
..write(')')
|
||||
..write('.map((rows) => rows.map(${_nameOfMappingMethod()}).toList());\n')
|
||||
..write('\n}\n');
|
||||
_buffer.write('Stream<List<${_select.resultClassName}>> $methodName(');
|
||||
_writeParameters(dontOverrideEngine: true);
|
||||
_buffer..write(') {\n')..write('return ${_nameOfCreationMethod()}(');
|
||||
_writeUseParameters(dontUseEngine: true);
|
||||
_buffer.write(').watch();\n}\n');
|
||||
}
|
||||
|
||||
void _writeUpdatingQuery(StringBuffer buffer) {
|
||||
void _writeUpdatingQuery() {
|
||||
/*
|
||||
Future<int> test() {
|
||||
return customUpdate('', variables: [], updates: {});
|
||||
}
|
||||
*/
|
||||
buffer.write('Future<int> ${query.name}(');
|
||||
_writeParameters(buffer);
|
||||
buffer.write(') {\n');
|
||||
final implName = _update.isInsert ? 'customInsert' : 'customUpdate';
|
||||
|
||||
_writeExpandedDeclarations(buffer);
|
||||
buffer
|
||||
_buffer.write('Future<int> ${query.name}(');
|
||||
_writeParameters();
|
||||
_buffer.write(') {\n');
|
||||
|
||||
_writeExpandedDeclarations();
|
||||
_buffer
|
||||
..write('return (operateOn ?? this).')
|
||||
..write('customUpdate(${_queryCode()},');
|
||||
..write('$implName(${_queryCode()},');
|
||||
|
||||
_writeVariables(buffer);
|
||||
buffer.write(',');
|
||||
_writeUpdates(buffer);
|
||||
_writeVariables();
|
||||
_buffer.write(',');
|
||||
_writeUpdates();
|
||||
|
||||
buffer..write(',);\n}\n');
|
||||
_buffer..write(',);\n}\n');
|
||||
}
|
||||
|
||||
void _writeParameters(StringBuffer buffer,
|
||||
{bool dontOverrideEngine = false}) {
|
||||
void _writeParameters({bool dontOverrideEngine = false}) {
|
||||
final paramList = query.variables.map((v) {
|
||||
var dartType = dartTypeNames[v.type];
|
||||
if (v.isArray) {
|
||||
|
@ -166,17 +196,28 @@ class QueryWriter {
|
|||
return '$dartType ${v.dartParameterName}';
|
||||
}).join(', ');
|
||||
|
||||
buffer.write(paramList);
|
||||
_buffer.write(paramList);
|
||||
|
||||
// write named optional parameter to configure the query engine used to
|
||||
// execute the statement,
|
||||
if (!dontOverrideEngine) {
|
||||
if (query.variables.isNotEmpty) buffer.write(', ');
|
||||
buffer.write('{@Deprecated(${asDartLiteral(queryEngineWarningDesc)}) '
|
||||
if (query.variables.isNotEmpty) _buffer.write(', ');
|
||||
_buffer.write('{@Deprecated(${asDartLiteral(queryEngineWarningDesc)}) '
|
||||
'QueryEngine operateOn}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes code that uses the parameters as declared by [_writeParameters],
|
||||
/// assuming that for each parameter, a variable with the same name exists
|
||||
/// in the current scope.
|
||||
void _writeUseParameters({bool dontUseEngine = false}) {
|
||||
_buffer.write(query.variables.map((v) => v.dartParameterName).join(', '));
|
||||
if (!dontUseEngine) {
|
||||
if (query.variables.isNotEmpty) _buffer.write(', ');
|
||||
_buffer.write('operateOn: operateOn');
|
||||
}
|
||||
}
|
||||
|
||||
// Some notes on parameters and generating query code:
|
||||
// We expand array parameters to multiple variables at runtime (see the
|
||||
// documentation of FoundVariable and SqlQuery for further discussion).
|
||||
|
@ -189,7 +230,7 @@ class QueryWriter {
|
|||
// "vars" variable twice. To do this, a local var called "$currentVarIndex"
|
||||
// keeps track of the highest variable number assigned.
|
||||
|
||||
void _writeExpandedDeclarations(StringBuffer buffer) {
|
||||
void _writeExpandedDeclarations() {
|
||||
var indexCounterWasDeclared = false;
|
||||
var highestIndexBeforeArray = 0;
|
||||
|
||||
|
@ -200,12 +241,12 @@ class QueryWriter {
|
|||
// add +1 because that's going to be the first index of the expanded
|
||||
// array
|
||||
final firstVal = highestIndexBeforeArray + 1;
|
||||
buffer.write('var $highestAssignedIndexVar = $firstVal;');
|
||||
_buffer.write('var $highestAssignedIndexVar = $firstVal;');
|
||||
indexCounterWasDeclared = true;
|
||||
}
|
||||
|
||||
// final expandedvar1 = $expandVar(<startIndex>, <amount>);
|
||||
buffer
|
||||
_buffer
|
||||
..write('final ')
|
||||
..write(_expandedName(variable))
|
||||
..write(' = ')
|
||||
|
@ -216,7 +257,7 @@ class QueryWriter {
|
|||
..write('.length);\n');
|
||||
|
||||
// increase highest index for the next array
|
||||
buffer
|
||||
_buffer
|
||||
..write('$highestAssignedIndexVar += ')
|
||||
..write(variable.dartParameterName)
|
||||
..write('.length;');
|
||||
|
@ -228,8 +269,8 @@ class QueryWriter {
|
|||
}
|
||||
}
|
||||
|
||||
void _writeVariables(StringBuffer buffer) {
|
||||
buffer..write('variables: [');
|
||||
void _writeVariables() {
|
||||
_buffer..write('variables: [');
|
||||
|
||||
for (var variable in query.variables) {
|
||||
// for a regular variable: Variable.withInt(x),
|
||||
|
@ -238,15 +279,15 @@ class QueryWriter {
|
|||
final name = variable.dartParameterName;
|
||||
|
||||
if (variable.isArray) {
|
||||
buffer.write('for (var \$ in $name) $constructor(\$)');
|
||||
_buffer.write('for (var \$ in $name) $constructor(\$)');
|
||||
} else {
|
||||
buffer.write('$constructor($name)');
|
||||
_buffer.write('$constructor($name)');
|
||||
}
|
||||
|
||||
buffer.write(',');
|
||||
_buffer.write(',');
|
||||
}
|
||||
|
||||
buffer..write(']');
|
||||
_buffer..write(']');
|
||||
}
|
||||
|
||||
/// Returns a Dart string literal representing the query after variables have
|
||||
|
@ -267,7 +308,7 @@ class QueryWriter {
|
|||
.singleWhere((f) => f.variable.resolvedIndex == sqlVar.resolvedIndex);
|
||||
if (!moorVar.isArray) continue;
|
||||
|
||||
// write everything that comes before this var into the buffer
|
||||
// write everything that comes before this var into the_buffer
|
||||
final currentIndex = sqlVar.firstPosition;
|
||||
final queryPart = query.sql.substring(lastIndex, currentIndex);
|
||||
buffer.write(escapeForDart(queryPart));
|
||||
|
@ -283,13 +324,13 @@ class QueryWriter {
|
|||
return buffer.toString();
|
||||
}
|
||||
|
||||
void _writeReadsFrom(StringBuffer buffer) {
|
||||
void _writeReadsFrom() {
|
||||
final from = _select.readsFrom.map((t) => t.tableFieldName).join(', ');
|
||||
buffer..write('readsFrom: {')..write(from)..write('}');
|
||||
_buffer..write('readsFrom: {')..write(from)..write('}');
|
||||
}
|
||||
|
||||
void _writeUpdates(StringBuffer buffer) {
|
||||
void _writeUpdates() {
|
||||
final from = _update.updates.map((t) => t.tableFieldName).join(', ');
|
||||
buffer..write('updates: {')..write(from)..write('}');
|
||||
_buffer..write('updates: {')..write(from)..write('}');
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
|
||||
/// Writes a class holding the result of an sql query into Dart.
|
||||
class ResultSetWriter {
|
||||
final SqlSelectQuery query;
|
||||
|
|
@ -1,25 +1,30 @@
|
|||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/writer/utils/hash_code.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
class DataClassWriter {
|
||||
final SpecifiedTable table;
|
||||
final GeneratorSession session;
|
||||
final Scope scope;
|
||||
|
||||
DataClassWriter(this.table, this.session);
|
||||
StringBuffer _buffer;
|
||||
|
||||
void writeInto(StringBuffer buffer) {
|
||||
buffer.write(
|
||||
DataClassWriter(this.table, this.scope) {
|
||||
_buffer = scope.leaf();
|
||||
}
|
||||
|
||||
void write() {
|
||||
_buffer.write(
|
||||
'class ${table.dartTypeName} extends DataClass implements Insertable<${table.dartTypeName}> {\n');
|
||||
|
||||
// write individual fields
|
||||
for (var column in table.columns) {
|
||||
buffer.write('final ${column.dartTypeName} ${column.dartGetterName}; \n');
|
||||
_buffer
|
||||
.write('final ${column.dartTypeName} ${column.dartGetterName}; \n');
|
||||
}
|
||||
|
||||
// write constructor with named optional fields
|
||||
buffer
|
||||
_buffer
|
||||
..write(table.dartTypeName)
|
||||
..write('({')
|
||||
..write(table.columns.map((column) {
|
||||
|
@ -32,27 +37,27 @@ class DataClassWriter {
|
|||
..write('});');
|
||||
|
||||
// Also write parsing factory
|
||||
_writeMappingConstructor(buffer);
|
||||
_writeMappingConstructor();
|
||||
|
||||
// And a serializer and deserializer method
|
||||
_writeFromJson(buffer);
|
||||
_writeToJson(buffer);
|
||||
_writeCompanionOverride(buffer);
|
||||
_writeFromJson();
|
||||
_writeToJson();
|
||||
_writeCompanionOverride();
|
||||
|
||||
// And a convenience method to copy data from this class.
|
||||
_writeCopyWith(buffer);
|
||||
_writeCopyWith();
|
||||
|
||||
_writeToString(buffer);
|
||||
_writeHashCode(buffer);
|
||||
_writeToString();
|
||||
_writeHashCode();
|
||||
|
||||
// override ==
|
||||
// return identical(this, other) || (other is DataClass && other.id == id && ...)
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\nbool operator ==(other) => ')
|
||||
..write('identical(this, other) || (other is ${table.dartTypeName}');
|
||||
|
||||
if (table.columns.isNotEmpty) {
|
||||
buffer
|
||||
_buffer
|
||||
..write('&&')
|
||||
..write(table.columns.map((c) {
|
||||
final getter = c.dartGetterName;
|
||||
|
@ -62,13 +67,13 @@ class DataClassWriter {
|
|||
}
|
||||
|
||||
// finish overrides method and class declaration
|
||||
buffer.write(');\n}');
|
||||
_buffer.write(');\n}');
|
||||
}
|
||||
|
||||
void _writeMappingConstructor(StringBuffer buffer) {
|
||||
void _writeMappingConstructor() {
|
||||
final dataClassName = table.dartTypeName;
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write('factory $dataClassName.fromData')
|
||||
..write('(Map<String, dynamic> data, GeneratedDatabase db, ')
|
||||
..write('{String prefix}) {\n')
|
||||
|
@ -82,12 +87,12 @@ class DataClassWriter {
|
|||
final resolver = '${ReCase(usedType).camelCase}Type';
|
||||
dartTypeToResolver[usedType] = resolver;
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
.write('final $resolver = db.typeSystem.forDartType<$usedType>();\n');
|
||||
}
|
||||
|
||||
// finally, the mighty constructor invocation:
|
||||
buffer.write('return $dataClassName(');
|
||||
_buffer.write('return $dataClassName(');
|
||||
|
||||
for (var column in table.columns) {
|
||||
// id: intType.mapFromDatabaseResponse(data["id])
|
||||
|
@ -106,16 +111,16 @@ class DataClassWriter {
|
|||
loadType = '$loaded.mapToDart($loadType)';
|
||||
}
|
||||
|
||||
buffer.write('$getter: $loadType,');
|
||||
_buffer.write('$getter: $loadType,');
|
||||
}
|
||||
|
||||
buffer.write(');}\n');
|
||||
_buffer.write(');}\n');
|
||||
}
|
||||
|
||||
void _writeFromJson(StringBuffer buffer) {
|
||||
void _writeFromJson() {
|
||||
final dataClassName = table.dartTypeName;
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write('factory $dataClassName.fromJson('
|
||||
'Map<String, dynamic> json,'
|
||||
'{ValueSerializer serializer = const ValueSerializer.defaults()}'
|
||||
|
@ -127,14 +132,14 @@ class DataClassWriter {
|
|||
final jsonKey = column.jsonKey;
|
||||
final type = column.dartTypeName;
|
||||
|
||||
buffer.write("$getter: serializer.fromJson<$type>(json['$jsonKey']),");
|
||||
_buffer.write("$getter: serializer.fromJson<$type>(json['$jsonKey']),");
|
||||
}
|
||||
|
||||
buffer.write(');}\n');
|
||||
_buffer.write(');}\n');
|
||||
|
||||
if (session.options.generateFromJsonStringConstructor) {
|
||||
if (scope.writer.options.generateFromJsonStringConstructor) {
|
||||
// also generate a constructor that only takes a json string
|
||||
buffer.write('factory $dataClassName.fromJsonString(String encodedJson, '
|
||||
_buffer.write('factory $dataClassName.fromJsonString(String encodedJson, '
|
||||
'{ValueSerializer serializer = const ValueSerializer.defaults()}) => '
|
||||
'$dataClassName.fromJson('
|
||||
'DataClass.parseJson(encodedJson) as Map<String, dynamic>, '
|
||||
|
@ -142,8 +147,8 @@ class DataClassWriter {
|
|||
}
|
||||
}
|
||||
|
||||
void _writeToJson(StringBuffer buffer) {
|
||||
buffer.write('@override Map<String, dynamic> toJson('
|
||||
void _writeToJson() {
|
||||
_buffer.write('@override Map<String, dynamic> toJson('
|
||||
'{ValueSerializer serializer = const ValueSerializer.defaults()}) {'
|
||||
'\n return {');
|
||||
|
||||
|
@ -153,40 +158,40 @@ class DataClassWriter {
|
|||
final needsThis = getter == 'serializer';
|
||||
final value = needsThis ? 'this.$getter' : getter;
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
.write("'$name': serializer.toJson<${column.dartTypeName}>($value),");
|
||||
}
|
||||
|
||||
buffer.write('};}');
|
||||
_buffer.write('};}');
|
||||
}
|
||||
|
||||
void _writeCopyWith(StringBuffer buffer) {
|
||||
void _writeCopyWith() {
|
||||
final dataClassName = table.dartTypeName;
|
||||
|
||||
buffer.write('$dataClassName copyWith({');
|
||||
_buffer.write('$dataClassName copyWith({');
|
||||
for (var i = 0; i < table.columns.length; i++) {
|
||||
final column = table.columns[i];
|
||||
final last = i == table.columns.length - 1;
|
||||
|
||||
buffer.write('${column.dartTypeName} ${column.dartGetterName}');
|
||||
_buffer.write('${column.dartTypeName} ${column.dartGetterName}');
|
||||
if (!last) {
|
||||
buffer.write(',');
|
||||
_buffer.write(',');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.write('}) => $dataClassName(');
|
||||
_buffer.write('}) => $dataClassName(');
|
||||
|
||||
for (var column in table.columns) {
|
||||
// we also have a method parameter called like the getter, so we can use
|
||||
// field: field ?? this.field
|
||||
final getter = column.dartGetterName;
|
||||
buffer.write('$getter: $getter ?? this.$getter,');
|
||||
_buffer.write('$getter: $getter ?? this.$getter,');
|
||||
}
|
||||
|
||||
buffer.write(');');
|
||||
_buffer.write(');');
|
||||
}
|
||||
|
||||
void _writeToString(StringBuffer buffer) {
|
||||
void _writeToString() {
|
||||
/*
|
||||
@override
|
||||
String toString() {
|
||||
|
@ -198,7 +203,7 @@ class DataClassWriter {
|
|||
}
|
||||
*/
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\nString toString() {')
|
||||
..write("return (StringBuffer('${table.dartTypeName}(')");
|
||||
|
||||
|
@ -206,36 +211,36 @@ class DataClassWriter {
|
|||
final column = table.columns[i];
|
||||
final getterName = column.dartGetterName;
|
||||
|
||||
buffer.write("..write('$getterName: \$$getterName");
|
||||
if (i != table.columns.length - 1) buffer.write(', ');
|
||||
_buffer.write("..write('$getterName: \$$getterName");
|
||||
if (i != table.columns.length - 1) _buffer.write(', ');
|
||||
|
||||
buffer.write("')");
|
||||
_buffer.write("')");
|
||||
}
|
||||
|
||||
buffer..write("..write(')')).toString();")..write('\}\n');
|
||||
_buffer..write("..write(')')).toString();")..write('\}\n');
|
||||
}
|
||||
|
||||
void _writeHashCode(StringBuffer buffer) {
|
||||
buffer.write('@override\n int get hashCode => ');
|
||||
void _writeHashCode() {
|
||||
_buffer.write('@override\n int get hashCode => ');
|
||||
|
||||
final fields = table.columns.map((c) => c.dartGetterName).toList();
|
||||
HashCodeWriter().writeHashCode(fields, buffer);
|
||||
buffer.write(';');
|
||||
HashCodeWriter().writeHashCode(fields, _buffer);
|
||||
_buffer.write(';');
|
||||
}
|
||||
|
||||
void _writeCompanionOverride(StringBuffer buffer) {
|
||||
void _writeCompanionOverride() {
|
||||
// T createCompanion<T extends UpdateCompanion>(bool nullToAbsent)
|
||||
|
||||
final companionClass = table.updateCompanionName;
|
||||
buffer.write('@override\nT createCompanion<T extends UpdateCompanion'
|
||||
_buffer.write('@override\nT createCompanion<T extends UpdateCompanion'
|
||||
'<${table.dartTypeName}>>('
|
||||
'bool nullToAbsent) {\n return $companionClass(');
|
||||
|
||||
for (var column in table.columns) {
|
||||
final getter = column.dartGetterName;
|
||||
buffer.write('$getter: $getter == null && nullToAbsent ? '
|
||||
_buffer.write('$getter: $getter == null && nullToAbsent ? '
|
||||
'const Value.absent() : Value($getter),');
|
||||
}
|
||||
buffer.write(') as T;}\n');
|
||||
_buffer.write(') as T;}\n');
|
||||
}
|
||||
}
|
|
@ -1,33 +1,37 @@
|
|||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:moor_generator/src/utils/string_escaper.dart';
|
||||
import 'package:moor_generator/src/writer/data_class_writer.dart';
|
||||
import 'package:moor_generator/src/writer/update_companion_writer.dart';
|
||||
import 'package:moor_generator/src/writer/utils.dart';
|
||||
import 'package:moor_generator/src/writer/tables/data_class_writer.dart';
|
||||
import 'package:moor_generator/src/writer/tables/update_companion_writer.dart';
|
||||
import 'package:moor_generator/src/writer/utils/memoized_getter.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
|
||||
class TableWriter {
|
||||
final SpecifiedTable table;
|
||||
final GeneratorSession session;
|
||||
final Scope scope;
|
||||
|
||||
TableWriter(this.table, this.session);
|
||||
StringBuffer _buffer;
|
||||
|
||||
void writeInto(StringBuffer buffer) {
|
||||
writeDataClass(buffer);
|
||||
writeTableInfoClass(buffer);
|
||||
TableWriter(this.table, this.scope);
|
||||
|
||||
void writeInto() {
|
||||
writeDataClass();
|
||||
writeTableInfoClass();
|
||||
}
|
||||
|
||||
void writeDataClass(StringBuffer buffer) {
|
||||
DataClassWriter(table, session).writeInto(buffer);
|
||||
UpdateCompanionWriter(table, session).writeInto(buffer);
|
||||
void writeDataClass() {
|
||||
DataClassWriter(table, scope.child()).write();
|
||||
UpdateCompanionWriter(table, scope.child()).write();
|
||||
}
|
||||
|
||||
void writeTableInfoClass(StringBuffer buffer) {
|
||||
void writeTableInfoClass() {
|
||||
_buffer = scope.leaf();
|
||||
|
||||
final dataClass = table.dartTypeName;
|
||||
final tableDslName = table.fromClass?.name ?? 'Table';
|
||||
|
||||
// class UsersTable extends Users implements TableInfo<Users, User> {
|
||||
buffer
|
||||
_buffer
|
||||
..write('class ${table.tableInfoName} extends $tableDslName '
|
||||
'with TableInfo<${table.tableInfoName}, $dataClass> {\n')
|
||||
// should have a GeneratedDatabase reference that is set in the constructor
|
||||
|
@ -37,15 +41,15 @@ class TableWriter {
|
|||
|
||||
// Generate the columns
|
||||
for (var column in table.columns) {
|
||||
_writeColumnVerificationMeta(buffer, column);
|
||||
_writeColumnGetter(buffer, column);
|
||||
_writeColumnVerificationMeta(column);
|
||||
_writeColumnGetter(column);
|
||||
}
|
||||
|
||||
// Generate $columns, $tableName, asDslTable getters
|
||||
final columnsWithGetters =
|
||||
table.columns.map((c) => c.dartGetterName).join(', ');
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write(
|
||||
'@override\nList<GeneratedColumn> get \$columns => [$columnsWithGetters];\n')
|
||||
..write('@override\n${table.tableInfoName} get asDslTable => this;\n')
|
||||
|
@ -54,33 +58,33 @@ class TableWriter {
|
|||
..write(
|
||||
'@override\nfinal String actualTableName = \'${table.sqlName}\';\n');
|
||||
|
||||
_writeValidityCheckMethod(buffer);
|
||||
_writePrimaryKeyOverride(buffer);
|
||||
_writeValidityCheckMethod();
|
||||
_writePrimaryKeyOverride();
|
||||
|
||||
_writeMappingMethod(buffer);
|
||||
_writeReverseMappingMethod(buffer);
|
||||
_writeMappingMethod();
|
||||
_writeReverseMappingMethod();
|
||||
|
||||
_writeAliasGenerator(buffer);
|
||||
_writeAliasGenerator();
|
||||
|
||||
_writeConvertersAsStaticFields(buffer);
|
||||
_overrideFieldsIfNeeded(buffer);
|
||||
_writeConvertersAsStaticFields();
|
||||
_overrideFieldsIfNeeded();
|
||||
|
||||
// close class
|
||||
buffer.write('}');
|
||||
_buffer.write('}');
|
||||
}
|
||||
|
||||
void _writeConvertersAsStaticFields(StringBuffer buffer) {
|
||||
void _writeConvertersAsStaticFields() {
|
||||
for (var converter in table.converters) {
|
||||
final typeName = converter.typeOfConverter.displayName;
|
||||
final code = converter.expression.toSource();
|
||||
buffer..write('static $typeName ${converter.fieldName} = $code;');
|
||||
_buffer..write('static $typeName ${converter.fieldName} = $code;');
|
||||
}
|
||||
}
|
||||
|
||||
void _writeMappingMethod(StringBuffer buffer) {
|
||||
void _writeMappingMethod() {
|
||||
final dataClassName = table.dartTypeName;
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write(
|
||||
'@override\n$dataClassName map(Map<String, dynamic> data, {String tablePrefix}) {\n')
|
||||
..write(
|
||||
|
@ -90,15 +94,15 @@ class TableWriter {
|
|||
..write('}\n');
|
||||
}
|
||||
|
||||
void _writeReverseMappingMethod(StringBuffer buffer) {
|
||||
void _writeReverseMappingMethod() {
|
||||
// Map<String, Variable> entityToSql(covariant UpdateCompanion<D> instance)
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\nMap<String, Variable> entityToSql('
|
||||
'${table.updateCompanionName} d) {\n')
|
||||
..write('final map = <String, Variable> {};');
|
||||
|
||||
for (var column in table.columns) {
|
||||
buffer.write('if (d.${column.dartGetterName}.present) {');
|
||||
_buffer.write('if (d.${column.dartGetterName}.present) {');
|
||||
final mapSetter = 'map[${asDartLiteral(column.name.name)}] = '
|
||||
'Variable<${column.variableTypeName}, ${column.sqlTypeName}>';
|
||||
|
||||
|
@ -106,26 +110,26 @@ class TableWriter {
|
|||
// apply type converter before writing the variable
|
||||
final converter = column.typeConverter;
|
||||
final fieldName = '${table.tableInfoName}.${converter.fieldName}';
|
||||
buffer
|
||||
_buffer
|
||||
..write('final converter = $fieldName;\n')
|
||||
..write(mapSetter)
|
||||
..write('(converter.mapToSql(d.${column.dartGetterName}.value));');
|
||||
} else {
|
||||
// no type converter. Write variable directly
|
||||
buffer
|
||||
_buffer
|
||||
..write(mapSetter)
|
||||
..write('(')
|
||||
..write('d.${column.dartGetterName}.value')
|
||||
..write(');');
|
||||
}
|
||||
|
||||
buffer.write('}');
|
||||
_buffer.write('}');
|
||||
}
|
||||
|
||||
buffer.write('return map; \n}\n');
|
||||
_buffer.write('return map; \n}\n');
|
||||
}
|
||||
|
||||
void _writeColumnGetter(StringBuffer buffer, SpecifiedColumn column) {
|
||||
void _writeColumnGetter(SpecifiedColumn column) {
|
||||
final isNullable = column.nullable;
|
||||
final additionalParams = <String, String>{};
|
||||
final expressionBuffer = StringBuffer();
|
||||
|
@ -176,7 +180,7 @@ class TableWriter {
|
|||
expressionBuffer.write(');');
|
||||
|
||||
writeMemoizedGetterWithBody(
|
||||
buffer: buffer,
|
||||
buffer: _buffer,
|
||||
getterName: column.dartGetterName,
|
||||
returnType: column.implColumnTypeName,
|
||||
code: expressionBuffer.toString(),
|
||||
|
@ -186,16 +190,15 @@ class TableWriter {
|
|||
);
|
||||
}
|
||||
|
||||
void _writeColumnVerificationMeta(
|
||||
StringBuffer buffer, SpecifiedColumn column) {
|
||||
void _writeColumnVerificationMeta(SpecifiedColumn column) {
|
||||
// final VerificationMeta _targetDateMeta = const VerificationMeta('targetDate');
|
||||
buffer
|
||||
_buffer
|
||||
..write('final VerificationMeta ${_fieldNameForColumnMeta(column)} = ')
|
||||
..write("const VerificationMeta('${column.dartGetterName}');\n");
|
||||
}
|
||||
|
||||
void _writeValidityCheckMethod(StringBuffer buffer) {
|
||||
buffer
|
||||
void _writeValidityCheckMethod() {
|
||||
_buffer
|
||||
..write('@override\nVerificationContext validateIntegrity'
|
||||
'(${table.updateCompanionName} d, {bool isInserting = false}) {\n')
|
||||
..write('final context = VerificationContext();\n');
|
||||
|
@ -207,13 +210,13 @@ class TableWriter {
|
|||
if (column.typeConverter != null) {
|
||||
// dont't verify custom columns, we assume that the user knows what
|
||||
// they're doing
|
||||
buffer
|
||||
_buffer
|
||||
..write(
|
||||
'context.handle($metaName, const VerificationResult.success());');
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write('if (d.$getterName.present) {\n')
|
||||
..write('context.handle('
|
||||
'$metaName, '
|
||||
|
@ -222,15 +225,15 @@ class TableWriter {
|
|||
..write('context.missing($metaName);\n')
|
||||
..write('}\n');
|
||||
}
|
||||
buffer.write('return context;\n}\n');
|
||||
_buffer.write('return context;\n}\n');
|
||||
}
|
||||
|
||||
String _fieldNameForColumnMeta(SpecifiedColumn column) {
|
||||
return '_${column.dartGetterName}Meta';
|
||||
}
|
||||
|
||||
void _writePrimaryKeyOverride(StringBuffer buffer) {
|
||||
buffer.write('@override\nSet<GeneratedColumn> get \$primaryKey => ');
|
||||
void _writePrimaryKeyOverride() {
|
||||
_buffer.write('@override\nSet<GeneratedColumn> get \$primaryKey => ');
|
||||
var primaryKey = table.primaryKey;
|
||||
|
||||
// If there is an auto increment column, that forms the primary key. The
|
||||
|
@ -239,37 +242,37 @@ class TableWriter {
|
|||
primaryKey ??= table.columns.where((c) => c.hasAI).toSet();
|
||||
|
||||
if (primaryKey.isEmpty) {
|
||||
buffer.write('<GeneratedColumn>{};');
|
||||
_buffer.write('<GeneratedColumn>{};');
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.write('{');
|
||||
_buffer.write('{');
|
||||
final pkList = primaryKey.toList();
|
||||
for (var i = 0; i < pkList.length; i++) {
|
||||
final pk = pkList[i];
|
||||
|
||||
buffer.write(pk.dartGetterName);
|
||||
_buffer.write(pk.dartGetterName);
|
||||
if (i != pkList.length - 1) {
|
||||
buffer.write(', ');
|
||||
_buffer.write(', ');
|
||||
}
|
||||
}
|
||||
buffer.write('};\n');
|
||||
_buffer.write('};\n');
|
||||
}
|
||||
|
||||
void _writeAliasGenerator(StringBuffer buffer) {
|
||||
void _writeAliasGenerator() {
|
||||
final typeName = table.tableInfoName;
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\n')
|
||||
..write('$typeName createAlias(String alias) {\n')
|
||||
..write('return $typeName(_db, alias);')
|
||||
..write('}');
|
||||
}
|
||||
|
||||
void _overrideFieldsIfNeeded(StringBuffer buffer) {
|
||||
void _overrideFieldsIfNeeded() {
|
||||
if (table.overrideWithoutRowId != null) {
|
||||
final value = table.overrideWithoutRowId ? 'true' : 'false';
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\n')
|
||||
..write('final bool withoutRowId = $value;\n');
|
||||
}
|
||||
|
@ -278,14 +281,14 @@ class TableWriter {
|
|||
final value =
|
||||
table.overrideTableConstraints.map(asDartLiteral).join(', ');
|
||||
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\n')
|
||||
..write('final List<String> customConstraints = const [$value];\n');
|
||||
}
|
||||
|
||||
if (table.overrideDontWriteConstraints != null) {
|
||||
final value = table.overrideDontWriteConstraints ? 'true' : 'false';
|
||||
buffer
|
||||
_buffer
|
||||
..write('@override\n')
|
||||
..write('final bool dontWriteConstraints = $value;\n');
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/writer/writer.dart';
|
||||
|
||||
class UpdateCompanionWriter {
|
||||
final SpecifiedTable table;
|
||||
final Scope scope;
|
||||
|
||||
StringBuffer _buffer;
|
||||
|
||||
UpdateCompanionWriter(this.table, this.scope) {
|
||||
_buffer = scope.leaf();
|
||||
}
|
||||
|
||||
void write() {
|
||||
_buffer.write('class ${table.updateCompanionName} '
|
||||
'extends UpdateCompanion<${table.dartTypeName}> {\n');
|
||||
_writeFields();
|
||||
_writeConstructor();
|
||||
_writeInsertConstructor();
|
||||
_writeCopyWith();
|
||||
|
||||
_buffer.write('}\n');
|
||||
}
|
||||
|
||||
void _writeFields() {
|
||||
for (var column in table.columns) {
|
||||
_buffer.write('final Value<${column.dartTypeName}>'
|
||||
' ${column.dartGetterName};\n');
|
||||
}
|
||||
}
|
||||
|
||||
void _writeConstructor() {
|
||||
_buffer.write('const ${table.updateCompanionName}({');
|
||||
|
||||
for (var column in table.columns) {
|
||||
_buffer.write('this.${column.dartGetterName} = const Value.absent(),');
|
||||
}
|
||||
|
||||
_buffer.write('});\n');
|
||||
}
|
||||
|
||||
/// Writes a special `.insert` constructor. All columns which may not be
|
||||
/// absent during insert are marked `@required` here. Also, we don't need to
|
||||
/// use value wrappers here - `Value.absent` simply isn't an option.
|
||||
void _writeInsertConstructor() {
|
||||
final requiredColumns = <SpecifiedColumn>{};
|
||||
|
||||
// can't be constant because we use initializers (this.a = Value(a)).
|
||||
// for a parameter a which is only potentially constant.
|
||||
_buffer.write('${table.updateCompanionName}.insert({');
|
||||
|
||||
// Say we had two required columns a and c, and an optional column b.
|
||||
// .insert({
|
||||
// @required String a,
|
||||
// this.b = const Value.absent(),
|
||||
// @required String b}): a = Value(a), b = Value(b);
|
||||
// We don't need to use this. for the initializers, Dart figures that out.
|
||||
|
||||
for (var column in table.columns) {
|
||||
final param = column.dartGetterName;
|
||||
|
||||
if (column.requiredDuringInsert) {
|
||||
requiredColumns.add(column);
|
||||
|
||||
_buffer.write('@required ${column.dartTypeName} $param,');
|
||||
} else {
|
||||
_buffer.write('this.$param = const Value.absent(),');
|
||||
}
|
||||
}
|
||||
_buffer.write('})');
|
||||
|
||||
var first = true;
|
||||
for (var required in requiredColumns) {
|
||||
if (first) {
|
||||
_buffer.write(': ');
|
||||
first = false;
|
||||
} else {
|
||||
_buffer.write(', ');
|
||||
}
|
||||
|
||||
final param = required.dartGetterName;
|
||||
_buffer.write('$param = Value($param)');
|
||||
}
|
||||
|
||||
_buffer.write(';\n');
|
||||
}
|
||||
|
||||
void _writeCopyWith() {
|
||||
_buffer.write('${table.updateCompanionName} copyWith({');
|
||||
var first = true;
|
||||
for (var column in table.columns) {
|
||||
if (!first) {
|
||||
_buffer.write(', ');
|
||||
}
|
||||
first = false;
|
||||
_buffer.write('Value<${column.dartTypeName}> ${column.dartGetterName}');
|
||||
}
|
||||
|
||||
_buffer
|
||||
..write('}) {\n') //
|
||||
..write('return ${table.updateCompanionName}(');
|
||||
for (var column in table.columns) {
|
||||
final name = column.dartGetterName;
|
||||
_buffer.write('$name: $name ?? this.$name,');
|
||||
}
|
||||
_buffer.write(');\n}\n');
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
|
||||
class UpdateCompanionWriter {
|
||||
final SpecifiedTable table;
|
||||
final GeneratorSession session;
|
||||
|
||||
UpdateCompanionWriter(this.table, this.session);
|
||||
|
||||
void writeInto(StringBuffer buffer) {
|
||||
buffer.write('class ${table.updateCompanionName} '
|
||||
'extends UpdateCompanion<${table.dartTypeName}> {\n');
|
||||
_writeFields(buffer);
|
||||
_writeConstructor(buffer);
|
||||
_writeCopyWith(buffer);
|
||||
|
||||
buffer.write('}\n');
|
||||
}
|
||||
|
||||
void _writeFields(StringBuffer buffer) {
|
||||
for (var column in table.columns) {
|
||||
buffer.write('final Value<${column.dartTypeName}>'
|
||||
' ${column.dartGetterName};\n');
|
||||
}
|
||||
}
|
||||
|
||||
void _writeConstructor(StringBuffer buffer) {
|
||||
buffer.write('const ${table.updateCompanionName}({');
|
||||
|
||||
for (var column in table.columns) {
|
||||
buffer.write('this.${column.dartGetterName} = const Value.absent(),');
|
||||
}
|
||||
|
||||
buffer.write('});\n');
|
||||
}
|
||||
|
||||
void _writeCopyWith(StringBuffer buffer) {
|
||||
buffer.write('${table.updateCompanionName} copyWith({');
|
||||
var first = true;
|
||||
for (var column in table.columns) {
|
||||
if (!first) {
|
||||
buffer.write(', ');
|
||||
}
|
||||
first = false;
|
||||
buffer.write('Value<${column.dartTypeName}> ${column.dartGetterName}');
|
||||
}
|
||||
|
||||
buffer
|
||||
..write('}) {\n') //
|
||||
..write('return ${table.updateCompanionName}(');
|
||||
for (var column in table.columns) {
|
||||
final name = column.dartGetterName;
|
||||
buffer.write('$name: $name ?? this.$name,');
|
||||
}
|
||||
buffer.write(');\n}\n');
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:moor_generator/src/backends/build/moor_builder.dart';
|
||||
|
||||
/// Manages a tree structure which we use to generate code.
|
||||
///
|
||||
|
@ -9,7 +10,12 @@ import 'package:meta/meta.dart';
|
|||
/// [StringBuffer] to the generators that will get ugly to manage, but when
|
||||
/// passing a [Scope] we will always be able to write code in a parent scope.
|
||||
class Writer {
|
||||
final Scope _root = Scope(parent: null);
|
||||
/* late final */ Scope _root;
|
||||
final MoorOptions options;
|
||||
|
||||
Writer(this.options) {
|
||||
_root = Scope(parent: null, writer: this);
|
||||
}
|
||||
|
||||
String writeGenerated() => _leafNodes(_root).join();
|
||||
|
||||
|
@ -41,8 +47,13 @@ abstract class _Node {
|
|||
/// we just pass a single [StringBuffer] around, this is annoying to manage.
|
||||
class Scope extends _Node {
|
||||
final List<_Node> _children = [];
|
||||
final DartScope scope;
|
||||
final Writer writer;
|
||||
|
||||
Scope({@required Scope parent}) : super(parent);
|
||||
Scope({@required Scope parent, Writer writer})
|
||||
: scope = parent?.scope?.nextLevel ?? DartScope.library,
|
||||
writer = writer ?? parent?.writer,
|
||||
super(parent);
|
||||
|
||||
Scope get root {
|
||||
var found = this;
|
||||
|
@ -52,6 +63,19 @@ class Scope extends _Node {
|
|||
return found;
|
||||
}
|
||||
|
||||
Iterable<Scope> get _thisAndParents sync* {
|
||||
var scope = this;
|
||||
do {
|
||||
yield scope;
|
||||
scope = scope.parent;
|
||||
} while (scope != null);
|
||||
}
|
||||
|
||||
Scope findScopeOfLevel(DartScope level) {
|
||||
return _thisAndParents
|
||||
.firstWhere((scope) => scope.scope.isSuperScope(level));
|
||||
}
|
||||
|
||||
Scope child() {
|
||||
final child = Scope(parent: this);
|
||||
_children.add(child);
|
||||
|
@ -70,3 +94,27 @@ class _LeafNode extends _Node {
|
|||
|
||||
_LeafNode(Scope parent) : super(parent);
|
||||
}
|
||||
|
||||
class DartScope {
|
||||
static const DartScope library = DartScope._(0);
|
||||
static const DartScope topLevelMember = DartScope._(1);
|
||||
static const DartScope inner = DartScope._(2);
|
||||
|
||||
static const List<DartScope> values = [library, topLevelMember, inner];
|
||||
|
||||
final int _id;
|
||||
|
||||
const DartScope._(this._id);
|
||||
|
||||
DartScope get nextLevel {
|
||||
if (_id == values.length - 1) {
|
||||
// already in innermost level
|
||||
return this;
|
||||
}
|
||||
return values[_id + 1];
|
||||
}
|
||||
|
||||
bool isSuperScope(DartScope other) {
|
||||
return other._id >= _id;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ dependencies:
|
|||
source_gen: ^0.9.4
|
||||
source_span: ^1.5.5
|
||||
build: ^1.1.0
|
||||
logging: '>=0.11.0 <1.0.0'
|
||||
build_config: '>=0.3.1 <1.0.0'
|
||||
moor: ^1.7.1
|
||||
meta: ^1.1.0
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/analyzer/dart/parser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../utils/test_backend.dart';
|
||||
|
||||
void main() {
|
||||
test('return expression of methods', () async {
|
||||
final backend = TestBackend({
|
||||
AssetId.parse('test_lib|lib/main.dart'): r'''
|
||||
class Test {
|
||||
String get getter => 'foo';
|
||||
String function() => 'bar';
|
||||
String invalid() {
|
||||
return 'baz';
|
||||
}
|
||||
}
|
||||
'''
|
||||
});
|
||||
|
||||
final backendTask =
|
||||
backend.startTask(Uri.parse('package:test_lib/main.dart'));
|
||||
final dartTask = await backend.session.startDartTask(backendTask);
|
||||
final parser = MoorDartParser(dartTask);
|
||||
|
||||
Future<MethodDeclaration> _loadDeclaration(Element element) async {
|
||||
final declaration = await parser.loadElementDeclaration(element);
|
||||
return declaration.node as MethodDeclaration;
|
||||
}
|
||||
|
||||
void _verifyReturnExpressionMatches(Element element, String source) async {
|
||||
final node = await _loadDeclaration(element);
|
||||
expect(parser.returnExpressionOfMethod(node).toSource(), source);
|
||||
}
|
||||
|
||||
final testClass = dartTask.library.getType('Test');
|
||||
|
||||
_verifyReturnExpressionMatches(testClass.getGetter('getter'), "'foo'");
|
||||
_verifyReturnExpressionMatches(testClass.getMethod('function'), "'bar'");
|
||||
|
||||
final invalidDecl = await _loadDeclaration(testClass.getMethod('invalid'));
|
||||
expect(parser.returnExpressionOfMethod(invalidDecl), isNull);
|
||||
expect(dartTask.errors.errors, isNotEmpty);
|
||||
|
||||
backend.finish();
|
||||
});
|
||||
}
|
|
@ -1,86 +1,81 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:moor_generator/src/analyzer/dart/parser.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
import 'package:moor_generator/src/model/specified_column.dart';
|
||||
import 'package:moor_generator/src/model/specified_table.dart';
|
||||
import 'package:moor_generator/src/state/generator_state.dart';
|
||||
import 'package:moor_generator/src/state/options.dart';
|
||||
import 'package:moor_generator/src/parser/table_parser.dart';
|
||||
import 'package:moor_generator/src/state/session.dart';
|
||||
import 'package:test_api/test_api.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() async {
|
||||
LibraryElement testLib;
|
||||
GeneratorState state;
|
||||
GeneratorSession session;
|
||||
import '../../utils/test_backend.dart';
|
||||
|
||||
setUpAll(() async {
|
||||
testLib = await resolveSource(r'''
|
||||
library test_parser;
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
class TableWithCustomName extends Table {
|
||||
@override
|
||||
String get tableName => "my-fancy-table"
|
||||
}
|
||||
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text().named("user_name").withLength(min: 6, max: 32)();
|
||||
TextColumn get onlyMax => text().withLength(max: 100)();
|
||||
|
||||
DateTimeColumn get defaults => dateTime().withDefault(currentDate)();
|
||||
}
|
||||
|
||||
class CustomPrimaryKey extends Table {
|
||||
void main() {
|
||||
TestBackend backend;
|
||||
DartTask dartTask;
|
||||
MoorDartParser parser;
|
||||
setUpAll(() {
|
||||
backend = TestBackend({
|
||||
AssetId.parse('test_lib|lib/main.dart'): r'''
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
class TableWithCustomName extends Table {
|
||||
@override String get tableName => 'my-fancy-table';
|
||||
}
|
||||
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text().named("user_name").withLength(min: 6, max: 32)();
|
||||
TextColumn get onlyMax => text().withLength(max: 100)();
|
||||
|
||||
DateTimeColumn get defaults => dateTime().withDefault(currentDate)();
|
||||
}
|
||||
|
||||
class CustomPrimaryKey extends Table {
|
||||
IntColumn get partA => integer()();
|
||||
IntColumn get partB => integer().customConstraint('custom')();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {partA, partB};
|
||||
}
|
||||
|
||||
class WrongName extends Table {
|
||||
|
||||
String constructTableName() {
|
||||
return "my-table-name";
|
||||
}
|
||||
|
||||
@override
|
||||
}
|
||||
|
||||
class WrongName extends Table {
|
||||
String constructTableName() => 'my-table-name';
|
||||
String get tableName => constructTableName();
|
||||
}
|
||||
''', (r) => r.findLibraryByName('test_parser'));
|
||||
}
|
||||
'''
|
||||
});
|
||||
});
|
||||
tearDownAll(() {
|
||||
backend.finish();
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
state = useState(() => GeneratorState(const MoorOptions.defaults()));
|
||||
session = state.startSession(null);
|
||||
setUp(() async {
|
||||
final task = backend.startTask(Uri.parse('package:test_lib/main.dart'));
|
||||
dartTask = await backend.session.startDartTask(task);
|
||||
parser = MoorDartParser(dartTask);
|
||||
});
|
||||
|
||||
Future<SpecifiedTable> parse(String name) {
|
||||
return TableParser(session).parse(testLib.getType(name));
|
||||
Future<SpecifiedTable> parse(String name) async {
|
||||
return parser.parseTable(dartTask.library.getType(name));
|
||||
}
|
||||
|
||||
group('SQL table name', () {
|
||||
test('should parse correctly when valid', () async {
|
||||
group('table names', () {
|
||||
test('use overridden name', () async {
|
||||
final parsed = await parse('TableWithCustomName');
|
||||
expect(parsed.sqlName, equals('my-fancy-table'));
|
||||
});
|
||||
|
||||
test('should use class name if table name is not specified', () async {
|
||||
test('use re-cased class name', () async {
|
||||
final parsed = await parse('Users');
|
||||
expect(parsed.sqlName, equals('users'));
|
||||
});
|
||||
|
||||
test('should not parse for complex methods', () async {
|
||||
await TableParser(session).parse(testLib.getType('WrongName'));
|
||||
|
||||
expect(session.errors.errors, isNotEmpty);
|
||||
await parse('WrongName');
|
||||
expect(dartTask.errors.errors, isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('Columns', () {
|
||||
test('should use field name if no name has been set explicitely', () async {
|
||||
test('should use field name if no name has been set explicitly', () async {
|
||||
final table = await parse('Users');
|
||||
final idColumn =
|
||||
table.columns.singleWhere((col) => col.name.name == 'id');
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:moor_generator/src/parser/moor/moor_analyzer.dart';
|
||||
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
|
||||
import 'package:moor_generator/src/analyzer/moor/parser.dart';
|
||||
import 'package:moor_generator/src/analyzer/session.dart';
|
||||
import 'package:test_api/test_api.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -11,13 +11,13 @@ CREATE TABLE users(
|
|||
''';
|
||||
|
||||
test('extracts table structure from .moor files', () async {
|
||||
final analyzer = MoorAnalyzer(content);
|
||||
final result = await analyzer.analyze();
|
||||
final task = MoorTask(null, null, content);
|
||||
final analyzer = MoorParser(task);
|
||||
final result = await analyzer.parseAndAnalyze();
|
||||
|
||||
expect(result.errors, isEmpty);
|
||||
expect(task.errors.errors, isEmpty);
|
||||
|
||||
final table =
|
||||
result.parsedFile.declaredTables.single.extractTable(TypeMapper());
|
||||
final table = result.declaredTables.single;
|
||||
|
||||
expect(table.sqlName, 'users');
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:moor_generator/src/parser/sql/query_handler.dart';
|
||||
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:build_test/build_test.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moor_generator/src/backends/backend.dart';
|
||||
|
||||
class TestBackend extends Backend {
|
||||
final Map<AssetId, String> fakeContent;
|
||||
Resolver _resolver;
|
||||
|
||||
final Completer _initCompleter = Completer();
|
||||
final Completer _finish = Completer();
|
||||
|
||||
/// Future that completes when this backend is ready, which happens when all
|
||||
/// input files have been parsed and analyzed by the Dart analyzer.
|
||||
Future get _ready => _initCompleter.future;
|
||||
|
||||
TestBackend(this.fakeContent) {
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
resolveSources(fakeContent.map((k, v) => MapEntry(k.toString(), v)), (r) {
|
||||
_resolver = r;
|
||||
_initCompleter.complete();
|
||||
return _finish.future;
|
||||
});
|
||||
}
|
||||
|
||||
BackendTask startTask(Uri uri) {
|
||||
return _TestBackendTask(this, uri);
|
||||
}
|
||||
|
||||
void finish() {
|
||||
_finish.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class _TestBackendTask extends BackendTask {
|
||||
final TestBackend backend;
|
||||
|
||||
@override
|
||||
final Uri entrypoint;
|
||||
|
||||
@override
|
||||
Logger get log => null;
|
||||
|
||||
_TestBackendTask(this.backend, this.entrypoint);
|
||||
|
||||
@override
|
||||
Future<String> readMoor(Uri path) async {
|
||||
await backend._ready;
|
||||
return backend.fakeContent[AssetId.resolve(path.toString())];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LibraryElement> resolveDart(Uri path) async {
|
||||
await backend._ready;
|
||||
return await backend._resolver.libraryFor(AssetId.resolve(path.toString()));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CompilationUnit> parseSource(String dart) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -65,10 +65,10 @@ package to generate type-safe methods from sql.
|
|||
Most on this list is just not supported yet because I didn't found a use case for
|
||||
them yet. If you need them, just leave an issue and I'll try to implement them soon.
|
||||
|
||||
- For now, `INSERT` statements are not supported, but they will be soon.
|
||||
- Compound select statements (`UNION` / `INTERSECT`) are not supported yet
|
||||
- Common table expressions are not supported
|
||||
- Some advanced expressions, like `CAST`s aren't supported yet.
|
||||
- An `UPSERT` clause is not yet supported on insert statements
|
||||
|
||||
If you run into parsing errors with what you think is valid sql, please create an issue.
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
import 'dart:math' show min, max;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/src/reader/tokenizer/token.dart';
|
||||
|
||||
|
|
|
@ -7,24 +7,54 @@ class AnalysisError {
|
|||
|
||||
AnalysisError({@required this.type, this.message, this.relevantNode});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
/// The relevant portion of the source code that caused this error. Some AST
|
||||
/// nodes don't have a span, in that case this error is going to be null.
|
||||
SourceSpan get span {
|
||||
final first = relevantNode?.first?.span;
|
||||
final last = relevantNode?.last?.span;
|
||||
|
||||
if (first != null && last != null) {
|
||||
final span = first.expand(last);
|
||||
return span.message(message ?? type.toString(), color: true);
|
||||
return first.expand(last);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final msgSpan = span;
|
||||
if (msgSpan != null) {
|
||||
return msgSpan.message(message ?? type.toString(), color: true);
|
||||
} else {
|
||||
return 'Error: $type: $message at $relevantNode';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnresolvedReferenceError extends AnalysisError {
|
||||
/// The attempted reference that couldn't be resolved
|
||||
final String reference;
|
||||
|
||||
/// A list of alternative references that would be available for [reference].
|
||||
final Iterable<String> available;
|
||||
|
||||
UnresolvedReferenceError(
|
||||
{@required AnalysisErrorType type,
|
||||
this.reference,
|
||||
this.available,
|
||||
AstNode relevantNode})
|
||||
: super(type: type, relevantNode: relevantNode);
|
||||
|
||||
@override
|
||||
String get message {
|
||||
return 'Could not find $reference. Available are: ${available.join(', ')}';
|
||||
}
|
||||
}
|
||||
|
||||
enum AnalysisErrorType {
|
||||
referencedUnknownTable,
|
||||
referencedUnknownColumn,
|
||||
ambiguousReference,
|
||||
|
||||
unknownFunction,
|
||||
other,
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ mixin Referencable {}
|
|||
/// many things, basically only tables.
|
||||
///
|
||||
/// For instance: "SELECT *, 1 AS d, (SELECT id FROM demo WHERE id = out.id) FROM demo AS out;"
|
||||
/// is a valid sql query when the demo table as an id column. However,
|
||||
/// is a valid sql query when the demo table has an id column. However,
|
||||
/// "SELECT *, 1 AS d, (SELECT id FROM demo WHERE id = d) FROM demo AS out;" is
|
||||
/// not, the "d" referencable is not visible for the child select statement.
|
||||
mixin VisibleToChildren on Referencable {}
|
||||
|
@ -79,12 +79,20 @@ class ReferenceScope {
|
|||
/// Returns everything that is in scope and a subtype of [T].
|
||||
List<T> allOf<T>() {
|
||||
var scope = this;
|
||||
var isInCurrentScope = true;
|
||||
final collected = <T>[];
|
||||
|
||||
while (scope != null) {
|
||||
collected.addAll(
|
||||
scope._references.values.expand((list) => list).whereType<T>());
|
||||
var foundValues =
|
||||
scope._references.values.expand((list) => list).whereType<T>();
|
||||
|
||||
if (!isInCurrentScope) {
|
||||
foundValues = foundValues.whereType<VisibleToChildren>().cast();
|
||||
}
|
||||
|
||||
collected.addAll(foundValues);
|
||||
scope = scope.parent;
|
||||
isInCurrentScope = false;
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,14 @@ class ColumnResolver extends RecursiveVisitor<void> {
|
|||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInsertStatement(InsertStatement e) {
|
||||
final table = _resolveTableReference(e.table);
|
||||
visitChildren(e);
|
||||
e.scope.availableColumns = table.resolvedColumns;
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitDeleteStatement(DeleteStatement e) {
|
||||
final table = _resolveTableReference(e.from);
|
||||
|
@ -115,10 +123,13 @@ class ColumnResolver extends RecursiveVisitor<void> {
|
|||
Table _resolveTableReference(TableReference r) {
|
||||
final scope = r.scope;
|
||||
final resolvedTable = scope.resolve<Table>(r.tableName, orElse: () {
|
||||
context.reportError(AnalysisError(
|
||||
final available = scope.allOf<Table>().map((t) => t.name);
|
||||
|
||||
context.reportError(UnresolvedReferenceError(
|
||||
type: AnalysisErrorType.referencedUnknownTable,
|
||||
relevantNode: r,
|
||||
message: 'The table ${r.tableName} could not be found',
|
||||
reference: r.tableName,
|
||||
available: available,
|
||||
));
|
||||
});
|
||||
return r.resolved = resolvedTable;
|
||||
|
|
|
@ -8,6 +8,10 @@ class ReferenceResolver extends RecursiveVisitor<void> {
|
|||
|
||||
@override
|
||||
void visitReference(Reference e) {
|
||||
if (e.resolved != null) {
|
||||
return super.visitReference(e);
|
||||
}
|
||||
|
||||
final scope = e.scope;
|
||||
|
||||
if (e.tableName != null) {
|
||||
|
@ -65,7 +69,7 @@ class ReferenceResolver extends RecursiveVisitor<void> {
|
|||
|
||||
@override
|
||||
void visitAggregateExpression(AggregateExpression e) {
|
||||
if (e.windowName != null) {
|
||||
if (e.windowName != null && e.resolved == null) {
|
||||
final resolved = e.scope.resolve<NamedWindowDeclaration>(e.windowName);
|
||||
e.resolved = resolved;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
part of '../analysis.dart';
|
||||
|
||||
/// Resolves the type of columns in a select statement and the type of
|
||||
/// expressions appearing in a select statement.
|
||||
/// Resolves types for all nodes in the AST which can have a type. This includes
|
||||
/// expressions, variables and so on. For select statements, we also try to
|
||||
/// figure out what types they return.
|
||||
class TypeResolvingVisitor extends RecursiveVisitor<void> {
|
||||
final AnalysisContext context;
|
||||
TypeResolver get types => context.types;
|
||||
|
@ -19,4 +20,29 @@ class TypeResolvingVisitor extends RecursiveVisitor<void> {
|
|||
|
||||
super.visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInsertStatement(InsertStatement e) {
|
||||
// resolve target columns - this is easy, as we should have the table
|
||||
// structure available.
|
||||
e.targetColumns.forEach(types.resolveExpression);
|
||||
|
||||
// if the insert statement has a VALUES source, we can now infer the type
|
||||
// for those expressions by comparing with the target column.
|
||||
if (e.source is ValuesSource) {
|
||||
final targetTypes = e.resolvedTargetColumns.map(context.typeOf).toList();
|
||||
final source = e.source as ValuesSource;
|
||||
|
||||
for (var tuple in source.values) {
|
||||
final expressions = tuple.expressions;
|
||||
for (var i = 0; i < min(expressions.length, targetTypes.length); i++) {
|
||||
if (i < targetTypes.length) {
|
||||
context.types.markResult(expressions[i], targetTypes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitChildren(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@ class TypeResolver {
|
|||
return calculated;
|
||||
}
|
||||
|
||||
/// Manually writes the [result] for the [Typeable] [t].
|
||||
void markResult(Typeable t, ResolveResult result) {
|
||||
_results.putIfAbsent(t, () => result);
|
||||
}
|
||||
|
||||
ResolveResult resolveOrInfer(Typeable t) {
|
||||
if (t is Column) {
|
||||
return resolveColumn(t);
|
||||
|
|
|
@ -21,11 +21,14 @@ part 'expressions/subquery.dart';
|
|||
part 'expressions/tuple.dart';
|
||||
part 'expressions/variables.dart';
|
||||
|
||||
part 'moor/import_statement.dart';
|
||||
|
||||
part 'schema/column_definition.dart';
|
||||
part 'schema/table_definition.dart';
|
||||
|
||||
part 'statements/create_table.dart';
|
||||
part 'statements/delete.dart';
|
||||
part 'statements/insert.dart';
|
||||
part 'statements/select.dart';
|
||||
part 'statements/statement.dart';
|
||||
part 'statements/update.dart';
|
||||
|
@ -133,6 +136,7 @@ abstract class AstNode {
|
|||
abstract class AstVisitor<T> {
|
||||
T visitSelectStatement(SelectStatement e);
|
||||
T visitResultColumn(ResultColumn e);
|
||||
T visitInsertStatement(InsertStatement e);
|
||||
T visitDeleteStatement(DeleteStatement e);
|
||||
T visitUpdateStatement(UpdateStatement e);
|
||||
T visitCreateTableStatement(CreateTableStatement e);
|
||||
|
@ -172,6 +176,8 @@ abstract class AstVisitor<T> {
|
|||
|
||||
T visitNumberedVariable(NumberedVariable e);
|
||||
T visitNamedVariable(ColonNamedVariable e);
|
||||
|
||||
T visitMoorImportStatement(ImportStatement e);
|
||||
}
|
||||
|
||||
/// Visitor that walks down the entire tree, visiting all children in order.
|
||||
|
@ -248,6 +254,9 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
|
|||
@override
|
||||
T visitSelectStatement(SelectStatement e) => visitChildren(e);
|
||||
|
||||
@override
|
||||
T visitInsertStatement(InsertStatement e) => visitChildren(e);
|
||||
|
||||
@override
|
||||
T visitDeleteStatement(DeleteStatement e) => visitChildren(e);
|
||||
|
||||
|
@ -281,6 +290,9 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
|
|||
@override
|
||||
T visitFrameSpec(FrameSpec e) => visitChildren(e);
|
||||
|
||||
@override
|
||||
T visitMoorImportStatement(ImportStatement e) => visitChildren(e);
|
||||
|
||||
@protected
|
||||
T visitChildren(AstNode e) {
|
||||
for (var child in e.childNodes) {
|
||||
|
|
|
@ -11,6 +11,8 @@ class Reference extends Expression with ReferenceOwner {
|
|||
final String tableName;
|
||||
final String columnName;
|
||||
|
||||
Column get resolvedColumn => resolved as Column;
|
||||
|
||||
Reference({this.tableName, this.columnName});
|
||||
|
||||
@override
|
||||
|
@ -23,4 +25,13 @@ class Reference extends Expression with ReferenceOwner {
|
|||
bool contentEquals(Reference other) {
|
||||
return other.tableName == tableName && other.columnName == columnName;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (tableName != null) {
|
||||
return '$tableName.$columnName';
|
||||
} else {
|
||||
return columnName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
part of '../ast.dart';
|
||||
|
||||
class ImportStatement extends Statement {
|
||||
Token importToken;
|
||||
StringLiteralToken importString;
|
||||
final String importedFile;
|
||||
|
||||
ImportStatement(this.importedFile);
|
||||
|
||||
@override
|
||||
T accept<T>(AstVisitor<T> visitor) {}
|
||||
|
||||
@override
|
||||
final Iterable<AstNode> childNodes = const [];
|
||||
|
||||
@override
|
||||
bool contentEquals(ImportStatement other) {
|
||||
return other.importedFile == importedFile;
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ abstract class ColumnConstraint extends AstNode {
|
|||
T Function(Default) isDefault,
|
||||
T Function(CollateConstraint) collate,
|
||||
T Function(ForeignKeyColumnConstraint) foreignKey,
|
||||
T Function(MappedBy) mappedBy,
|
||||
}) {
|
||||
if (this is NotNull) {
|
||||
return notNull?.call(this as NotNull);
|
||||
|
@ -55,6 +56,8 @@ abstract class ColumnConstraint extends AstNode {
|
|||
return collate?.call(this as CollateConstraint);
|
||||
} else if (this is ForeignKeyColumnConstraint) {
|
||||
return foreignKey?.call(this as ForeignKeyColumnConstraint);
|
||||
} else if (this is MappedBy) {
|
||||
return mappedBy?.call(this as MappedBy);
|
||||
} else {
|
||||
throw Exception('Did not expect $runtimeType as a ColumnConstraint');
|
||||
}
|
||||
|
@ -164,3 +167,20 @@ class ForeignKeyColumnConstraint extends ColumnConstraint {
|
|||
@override
|
||||
Iterable<AstNode> get childNodes => [clause];
|
||||
}
|
||||
|
||||
/// A `MAPPED BY` constraint, which is only parsed for moor files. It can be
|
||||
/// used to declare a type converter for this column.
|
||||
class MappedBy extends ColumnConstraint {
|
||||
/// The Dart expression creating the type converter we use to map this token.
|
||||
final InlineDartToken mapper;
|
||||
|
||||
MappedBy(String name, this.mapper) : super(name);
|
||||
|
||||
@override
|
||||
bool _equalToConstraint(MappedBy other) {
|
||||
return other.mapper.dartCode == mapper.dartCode;
|
||||
}
|
||||
|
||||
@override
|
||||
final Iterable<AstNode> childNodes = const [];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
part of '../ast.dart';
|
||||
|
||||
enum InsertMode {
|
||||
insert,
|
||||
replace,
|
||||
insertOrReplace,
|
||||
insertOrRollback,
|
||||
insertOrAbort,
|
||||
insertOrFail,
|
||||
insertOrIgnore
|
||||
}
|
||||
|
||||
class InsertStatement extends Statement with CrudStatement {
|
||||
final InsertMode mode;
|
||||
final TableReference table;
|
||||
final List<Reference> targetColumns;
|
||||
final InsertSource source;
|
||||
|
||||
List<Column> get resolvedTargetColumns {
|
||||
if (targetColumns.isNotEmpty) {
|
||||
return targetColumns.map((c) => c.resolvedColumn).toList();
|
||||
} else {
|
||||
// no columns declared - assume all columns from the table
|
||||
return table.resultSet?.resolvedColumns;
|
||||
}
|
||||
}
|
||||
|
||||
// todo parse upsert clauses
|
||||
|
||||
InsertStatement(
|
||||
{this.mode = InsertMode.insert,
|
||||
@required this.table,
|
||||
@required this.targetColumns,
|
||||
@required this.source});
|
||||
|
||||
@override
|
||||
T accept<T>(AstVisitor<T> visitor) => visitor.visitInsertStatement(this);
|
||||
|
||||
@override
|
||||
Iterable<AstNode> get childNodes sync* {
|
||||
yield table;
|
||||
yield* targetColumns;
|
||||
yield* source.childNodes;
|
||||
}
|
||||
|
||||
@override
|
||||
bool contentEquals(InsertStatement other) {
|
||||
return other.mode == mode && other.source.runtimeType == source.runtimeType;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class InsertSource {
|
||||
Iterable<AstNode> get childNodes;
|
||||
|
||||
const InsertSource();
|
||||
|
||||
T when<T>(
|
||||
{T Function(ValuesSource) isValues,
|
||||
T Function(SelectInsertSource) isSelect,
|
||||
T Function(DefaultValues) isDefaults}) {
|
||||
if (this is ValuesSource) {
|
||||
return isValues?.call(this as ValuesSource);
|
||||
} else if (this is SelectInsertSource) {
|
||||
return isSelect?.call(this as SelectInsertSource);
|
||||
} else if (this is DefaultValues) {
|
||||
return isDefaults?.call(this as DefaultValues);
|
||||
} else {
|
||||
throw StateError('Did not expect $runtimeType as InsertSource');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses a list of values for an insert statement (`VALUES (a, b, c)`).
|
||||
class ValuesSource extends InsertSource {
|
||||
final List<TupleExpression> values;
|
||||
|
||||
ValuesSource(this.values);
|
||||
|
||||
@override
|
||||
Iterable<AstNode> get childNodes => values;
|
||||
}
|
||||
|
||||
/// Inserts the rows returned by [stmt].
|
||||
class SelectInsertSource extends InsertSource {
|
||||
final SelectStatement stmt;
|
||||
|
||||
SelectInsertSource(this.stmt);
|
||||
|
||||
@override
|
||||
Iterable<AstNode> get childNodes => [stmt];
|
||||
}
|
||||
|
||||
/// Use `DEFAULT VALUES` for an insert statement.
|
||||
class DefaultValues extends InsertSource {
|
||||
const DefaultValues();
|
||||
|
||||
@override
|
||||
final Iterable<AstNode> childNodes = const [];
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
part of '../ast.dart';
|
||||
|
||||
abstract class Statement extends AstNode {}
|
||||
abstract class Statement extends AstNode {
|
||||
Token semicolon;
|
||||
}
|
||||
|
||||
/// Marker mixin for statements that read from an existing table structure.
|
||||
mixin CrudStatement on Statement {}
|
||||
|
|
|
@ -8,7 +8,12 @@ class SqlEngine {
|
|||
/// All tables registered with [registerTable].
|
||||
final List<Table> knownTables = [];
|
||||
|
||||
SqlEngine();
|
||||
/// Moor extends the sql grammar a bit to support type converters and other
|
||||
/// features. Enabling this flag will make this engine parse sql with these
|
||||
/// extensions enabled.
|
||||
final bool useMoorExtensions;
|
||||
|
||||
SqlEngine({this.useMoorExtensions = false});
|
||||
|
||||
/// Registers the [table], which means that it can later be used in sql
|
||||
/// statements.
|
||||
|
@ -28,7 +33,7 @@ class SqlEngine {
|
|||
/// Tokenizes the [source] into a list list [Token]s. Each [Token] contains
|
||||
/// information about where it appears in the [source] and a [TokenType].
|
||||
List<Token> tokenize(String source) {
|
||||
final scanner = Scanner(source);
|
||||
final scanner = Scanner(source, scanMoorTokens: useMoorExtensions);
|
||||
final tokens = scanner.scanTokens();
|
||||
|
||||
if (scanner.errors.isNotEmpty) {
|
||||
|
@ -41,7 +46,7 @@ class SqlEngine {
|
|||
/// Parses the [sql] statement into an AST-representation.
|
||||
ParseResult parse(String sql) {
|
||||
final tokens = tokenize(sql);
|
||||
final parser = Parser(tokens);
|
||||
final parser = Parser(tokens, useMoor: useMoorExtensions);
|
||||
|
||||
final stmt = parser.statement();
|
||||
return ParseResult._(stmt, tokens, parser.errors, sql);
|
||||
|
|
|
@ -130,9 +130,11 @@ mixin CrudParser on ParserBase {
|
|||
if (_matchOne(TokenType.identifier)) {
|
||||
// ignore the schema name, it's not supported. Besides that, we're on the
|
||||
// first branch in the diagram here
|
||||
final tableName = (_previous as IdentifierToken).identifier;
|
||||
final firstToken = _previous as IdentifierToken;
|
||||
final tableName = firstToken.identifier;
|
||||
final alias = _as();
|
||||
return TableReference(tableName, alias?.identifier);
|
||||
return TableReference(tableName, alias?.identifier)
|
||||
..setSpan(firstToken, _previous);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -366,6 +368,75 @@ mixin CrudParser on ParserBase {
|
|||
or: failureMode, table: table, set: set, where: where);
|
||||
}
|
||||
|
||||
InsertStatement _insertStmt() {
|
||||
if (!_match(const [TokenType.insert, TokenType.replace])) return null;
|
||||
|
||||
final firstToken = _previous;
|
||||
InsertMode insertMode;
|
||||
if (_previous.type == TokenType.insert) {
|
||||
// insert modes can have a failure clause (INSERT OR xxx)
|
||||
if (_matchOne(TokenType.or)) {
|
||||
const tokensToModes = {
|
||||
TokenType.replace: InsertMode.insertOrReplace,
|
||||
TokenType.rollback: InsertMode.insertOrRollback,
|
||||
TokenType.abort: InsertMode.insertOrAbort,
|
||||
TokenType.fail: InsertMode.insertOrFail,
|
||||
TokenType.ignore: InsertMode.insertOrIgnore
|
||||
};
|
||||
|
||||
if (_match(tokensToModes.keys)) {
|
||||
insertMode = tokensToModes[_previous.type];
|
||||
} else {
|
||||
_error(
|
||||
'After the INSERT OR, expected an insert mode (REPLACE, ROLLBACK, etc.)');
|
||||
}
|
||||
} else {
|
||||
insertMode = InsertMode.insert;
|
||||
}
|
||||
} else {
|
||||
// is it wasn't an insert, it must have been a replace
|
||||
insertMode = InsertMode.replace;
|
||||
}
|
||||
assert(insertMode != null);
|
||||
_consume(TokenType.into, 'Expected INSERT INTO');
|
||||
|
||||
final table = _tableReference();
|
||||
final targetColumns = <Reference>[];
|
||||
|
||||
if (_matchOne(TokenType.leftParen)) {
|
||||
do {
|
||||
final columnRef = _consumeIdentifier('Expected a column');
|
||||
targetColumns.add(Reference(columnName: columnRef.identifier));
|
||||
} while (_matchOne(TokenType.comma));
|
||||
|
||||
_consume(TokenType.rightParen,
|
||||
'Expected clpsing parenthesis after column list');
|
||||
}
|
||||
final source = _insertSource();
|
||||
|
||||
return InsertStatement(
|
||||
mode: insertMode,
|
||||
table: table,
|
||||
targetColumns: targetColumns,
|
||||
source: source,
|
||||
)..setSpan(firstToken, _previous);
|
||||
}
|
||||
|
||||
InsertSource _insertSource() {
|
||||
if (_matchOne(TokenType.$values)) {
|
||||
final values = <TupleExpression>[];
|
||||
do {
|
||||
values.add(_consumeTuple());
|
||||
} while (_matchOne(TokenType.comma));
|
||||
return ValuesSource(values);
|
||||
} else if (_matchOne(TokenType.$default)) {
|
||||
_consume(TokenType.$values, 'Expected DEFAULT VALUES');
|
||||
return const DefaultValues();
|
||||
} else {
|
||||
return SelectInsertSource(select());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
WindowDefinition _windowDefinition() {
|
||||
_consume(TokenType.leftParen, 'Expected opening parenthesis');
|
||||
|
|
|
@ -326,8 +326,10 @@ mixin ExpressionParser on ParserBase {
|
|||
break;
|
||||
case TokenType.colon:
|
||||
final colon = token;
|
||||
final identifier = _consume(TokenType.identifier,
|
||||
'Expected an identifier for the named variable') as IdentifierToken;
|
||||
final identifier = _consumeIdentifier(
|
||||
'Expected an identifier for the named variable',
|
||||
lenient: true);
|
||||
|
||||
final content = identifier.identifier;
|
||||
return ColonNamedVariable(':$content')..setSpan(colon, identifier);
|
||||
default:
|
||||
|
@ -391,4 +393,20 @@ mixin ExpressionParser on ParserBase {
|
|||
windowName: windowName,
|
||||
)..setSpan(name, _previous);
|
||||
}
|
||||
|
||||
@override
|
||||
TupleExpression _consumeTuple() {
|
||||
final firstToken =
|
||||
_consume(TokenType.leftParen, 'Expected opening parenthesis for tuple');
|
||||
final expressions = <Expression>[];
|
||||
|
||||
do {
|
||||
expressions.add(expression());
|
||||
} while (_matchOne(TokenType.comma));
|
||||
|
||||
_consume(TokenType.rightParen, 'Expected right parenthesis to close tuple');
|
||||
|
||||
return TupleExpression(expressions: expressions)
|
||||
..setSpan(firstToken, _previous);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,9 +43,13 @@ class ParsingError implements Exception {
|
|||
abstract class ParserBase {
|
||||
final List<Token> tokens;
|
||||
final List<ParsingError> errors = [];
|
||||
|
||||
/// Whether to enable the extensions moor makes to the sql grammar.
|
||||
final bool enableMoorExtensions;
|
||||
|
||||
int _current = 0;
|
||||
|
||||
ParserBase(this.tokens);
|
||||
ParserBase(this.tokens, this.enableMoorExtensions);
|
||||
|
||||
bool get _isAtEnd => _peek.type == TokenType.eof;
|
||||
Token get _peek => tokens[_current];
|
||||
|
@ -119,12 +123,19 @@ abstract class ParserBase {
|
|||
_error(message);
|
||||
}
|
||||
|
||||
IdentifierToken _consumeIdentifier(String message) {
|
||||
/// Consumes an identifier. If [lenient] is true and the next token is not
|
||||
/// an identifier but rather a [KeywordToken], that token will be converted
|
||||
/// to an identifier.
|
||||
IdentifierToken _consumeIdentifier(String message, {bool lenient = false}) {
|
||||
if (lenient && _peek is KeywordToken) {
|
||||
return (_advance() as KeywordToken).convertToIdentifier();
|
||||
}
|
||||
return _consume(TokenType.identifier, message) as IdentifierToken;
|
||||
}
|
||||
|
||||
// Common operations that we are referenced very often
|
||||
Expression expression();
|
||||
TupleExpression _consumeTuple();
|
||||
|
||||
/// Parses a [SelectStatement], or returns null if there is no select token
|
||||
/// after the current position.
|
||||
|
@ -140,33 +151,66 @@ abstract class ParserBase {
|
|||
WindowDefinition _windowDefinition();
|
||||
}
|
||||
|
||||
// todo better error handling and synchronisation, like it's done here:
|
||||
// https://craftinginterpreters.com/parsing-expressions.html#synchronizing-a-recursive-descent-parser
|
||||
|
||||
class Parser extends ParserBase
|
||||
with ExpressionParser, SchemaParser, CrudParser {
|
||||
Parser(List<Token> tokens) : super(tokens);
|
||||
Parser(List<Token> tokens, {bool useMoor = false}) : super(tokens, useMoor);
|
||||
|
||||
Statement statement({bool expectEnd = true}) {
|
||||
final first = _peek;
|
||||
final stmt = select() ?? _deleteStmt() ?? _update() ?? _createTable();
|
||||
var stmt = select() ??
|
||||
_deleteStmt() ??
|
||||
_update() ??
|
||||
_insertStmt() ??
|
||||
_createTable();
|
||||
|
||||
if (enableMoorExtensions) {
|
||||
stmt ??= _import();
|
||||
}
|
||||
|
||||
if (stmt == null) {
|
||||
_error('Expected a sql statement to start here');
|
||||
}
|
||||
|
||||
_matchOne(TokenType.semicolon);
|
||||
if (_matchOne(TokenType.semicolon)) {
|
||||
stmt.semicolon = _previous;
|
||||
}
|
||||
|
||||
if (!_isAtEnd && expectEnd) {
|
||||
_error('Expected the statement to finish here');
|
||||
}
|
||||
return stmt..setSpan(first, _previous);
|
||||
}
|
||||
|
||||
ImportStatement _import() {
|
||||
if (_matchOne(TokenType.import)) {
|
||||
final importToken = _previous;
|
||||
final import = _consume(TokenType.stringLiteral,
|
||||
'Expected import file as a string literal (single quoted)')
|
||||
as StringLiteralToken;
|
||||
|
||||
return ImportStatement(import.value)
|
||||
..importToken = importToken
|
||||
..importString = import;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Statement> statements() {
|
||||
final stmts = <Statement>[];
|
||||
while (!_isAtEnd) {
|
||||
stmts.add(statement(expectEnd: false));
|
||||
try {
|
||||
stmts.add(statement(expectEnd: false));
|
||||
} on ParsingError catch (_) {
|
||||
// the error is added to the list errors, so ignore. We skip to the next
|
||||
// semicolon to parse the next statement.
|
||||
_synchronize();
|
||||
}
|
||||
}
|
||||
return stmts;
|
||||
}
|
||||
|
||||
void _synchronize() {
|
||||
// fast-forward to the token after th next semicolon
|
||||
while (!_isAtEnd && _advance().type != TokenType.semicolon) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@ mixin SchemaParser on ParserBase {
|
|||
ifNotExists = true;
|
||||
}
|
||||
|
||||
final tableIdentifier = _consumeIdentifier('Expected a table name');
|
||||
final tableIdentifier =
|
||||
_consumeIdentifier('Expected a table name', lenient: true);
|
||||
|
||||
// we don't currently support CREATE TABLE x AS SELECT ... statements
|
||||
_consume(
|
||||
|
@ -153,6 +154,15 @@ mixin SchemaParser on ParserBase {
|
|||
return ForeignKeyColumnConstraint(resolvedName, clause)
|
||||
..setSpan(first, _previous);
|
||||
}
|
||||
if (enableMoorExtensions && _matchOne(TokenType.mapped)) {
|
||||
_consume(TokenType.by, 'Expected a MAPPED BY constraint');
|
||||
|
||||
final dartExpr = _consume(
|
||||
TokenType.inlineDart, 'Expected Dart expression in backticks');
|
||||
|
||||
return MappedBy(resolvedName, dartExpr as InlineDartToken)
|
||||
..setSpan(first, _previous);
|
||||
}
|
||||
|
||||
// no known column constraint matched. If orNull is set and we're not
|
||||
// guaranteed to be in a constraint clause (started with CONSTRAINT), we
|
||||
|
@ -204,7 +214,8 @@ mixin SchemaParser on ParserBase {
|
|||
|
||||
String _constraintNameOrNull() {
|
||||
if (_matchOne(TokenType.constraint)) {
|
||||
final name = _consumeIdentifier('Expect a name for the constraint here');
|
||||
final name = _consumeIdentifier('Expect a name for the constraint here',
|
||||
lenient: true);
|
||||
return name.identifier;
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -4,6 +4,9 @@ import 'package:sqlparser/src/reader/tokenizer/utils.dart';
|
|||
|
||||
class Scanner {
|
||||
final String source;
|
||||
|
||||
/// Whether to scan tokens that are only relevant for moor.
|
||||
final bool scanMoorTokens;
|
||||
final SourceFile _file;
|
||||
|
||||
final List<Token> tokens = [];
|
||||
|
@ -21,7 +24,8 @@ class Scanner {
|
|||
return _file.location(_currentOffset);
|
||||
}
|
||||
|
||||
Scanner(this.source) : _file = SourceFile.fromString(source);
|
||||
Scanner(this.source, {this.scanMoorTokens = false})
|
||||
: _file = SourceFile.fromString(source);
|
||||
|
||||
List<Token> scanTokens() {
|
||||
while (!_isAtEnd) {
|
||||
|
@ -131,6 +135,13 @@ class Scanner {
|
|||
// todo sqlite also allows string literals with double ticks, we don't
|
||||
_identifier(escapedInQuotes: true);
|
||||
break;
|
||||
case '`':
|
||||
if (scanMoorTokens) {
|
||||
_inlineDart();
|
||||
} else {
|
||||
_unexpectedToken();
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\n':
|
||||
|
@ -143,12 +154,16 @@ class Scanner {
|
|||
} else if (canStartColumnName(char)) {
|
||||
_identifier();
|
||||
} else {
|
||||
errors.add(TokenizerError('Unexpected character.', _currentLocation));
|
||||
_unexpectedToken();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _unexpectedToken() {
|
||||
errors.add(TokenizerError('Unexpected character.', _currentLocation));
|
||||
}
|
||||
|
||||
String _nextChar() {
|
||||
_currentOffset++;
|
||||
return source.substring(_currentOffset - 1, _currentOffset);
|
||||
|
@ -306,10 +321,29 @@ class Scanner {
|
|||
// not escaped, so it could be a keyword
|
||||
final text = _currentSpan.text.toUpperCase();
|
||||
if (keywords.containsKey(text)) {
|
||||
_addToken(keywords[text]);
|
||||
tokens.add(KeywordToken(keywords[text], _currentSpan));
|
||||
} else if (scanMoorTokens && moorKeywords.containsKey(text)) {
|
||||
tokens.add(KeywordToken(moorKeywords[text], _currentSpan));
|
||||
} else {
|
||||
tokens.add(IdentifierToken(false, _currentSpan));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _inlineDart() {
|
||||
// inline starts with a `, we just need to find the matching ` that
|
||||
// terminates this token.
|
||||
while (_peek() != '`' && !_isAtEnd) {
|
||||
_nextChar();
|
||||
}
|
||||
|
||||
if (_isAtEnd) {
|
||||
errors.add(
|
||||
TokenizerError('Unterminated inline Dart code', _currentLocation));
|
||||
} else {
|
||||
// consume the `
|
||||
_nextChar();
|
||||
tokens.add(InlineDartToken(_currentSpan));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ enum TokenType {
|
|||
select,
|
||||
delete,
|
||||
update,
|
||||
insert,
|
||||
into,
|
||||
distinct,
|
||||
all,
|
||||
from,
|
||||
|
@ -124,6 +126,7 @@ enum TokenType {
|
|||
unique,
|
||||
check,
|
||||
$default,
|
||||
$values,
|
||||
conflict,
|
||||
references,
|
||||
cascade,
|
||||
|
@ -133,10 +136,17 @@ enum TokenType {
|
|||
|
||||
semicolon,
|
||||
eof,
|
||||
|
||||
/// Moor specific token, used to declare a type converters
|
||||
mapped,
|
||||
inlineDart,
|
||||
import,
|
||||
}
|
||||
|
||||
const Map<String, TokenType> keywords = {
|
||||
'SELECT': TokenType.select,
|
||||
'INSERT': TokenType.insert,
|
||||
'INTO': TokenType.into,
|
||||
'COLLATE': TokenType.collate,
|
||||
'DISTINCT': TokenType.distinct,
|
||||
'UPDATE': TokenType.update,
|
||||
|
@ -224,6 +234,12 @@ const Map<String, TokenType> keywords = {
|
|||
'OTHERS': TokenType.others,
|
||||
'TIES': TokenType.ties,
|
||||
'WINDOW': TokenType.window,
|
||||
'VALUES': TokenType.$values,
|
||||
};
|
||||
|
||||
const Map<String, TokenType> moorKeywords = {
|
||||
'MAPPED': TokenType.mapped,
|
||||
'IMPORT': TokenType.import,
|
||||
};
|
||||
|
||||
class Token {
|
||||
|
@ -254,6 +270,11 @@ class IdentifierToken extends Token {
|
|||
/// Whether this identifier was escaped by putting it in "double ticks".
|
||||
final bool escaped;
|
||||
|
||||
/// Whether this identifier token is synthetic. We sometimes convert
|
||||
/// [KeywordToken]s to identifiers if they're unambiguous, in which case
|
||||
/// [synthetic] will be true on this token because it was not scanned as such.
|
||||
final bool synthetic;
|
||||
|
||||
String get identifier {
|
||||
if (escaped) {
|
||||
return lexeme.substring(1, lexeme.length - 1);
|
||||
|
@ -262,10 +283,37 @@ class IdentifierToken extends Token {
|
|||
}
|
||||
}
|
||||
|
||||
const IdentifierToken(this.escaped, FileSpan span)
|
||||
const IdentifierToken(this.escaped, FileSpan span, {this.synthetic = false})
|
||||
: super(TokenType.identifier, span);
|
||||
}
|
||||
|
||||
/// Inline Dart appearing in a create table statement. Only parsed when the moor
|
||||
/// extensions are enabled. Dart code is wrapped in backticks.
|
||||
class InlineDartToken extends Token {
|
||||
InlineDartToken(FileSpan span) : super(TokenType.inlineDart, span);
|
||||
|
||||
String get dartCode {
|
||||
// strip the backticks
|
||||
return lexeme.substring(1, lexeme.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for tokens that are keywords. We use this special class without any
|
||||
/// additional properties to ease syntax highlighting, as it allows us to find
|
||||
/// the keywords easily.
|
||||
class KeywordToken extends Token {
|
||||
/// Whether this token has been used as an identifier while parsing.
|
||||
bool isIdentifier;
|
||||
|
||||
KeywordToken(TokenType type, FileSpan span) : super(type, span);
|
||||
|
||||
IdentifierToken convertToIdentifier() {
|
||||
isIdentifier = true;
|
||||
|
||||
return IdentifierToken(false, span, synthetic: false);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenizerError {
|
||||
final String message;
|
||||
final SourceLocation location;
|
||||
|
|
|
@ -46,6 +46,19 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('handles VALUES clause in insert statements', () {
|
||||
final engine = SqlEngine()..registerTable(demoTable);
|
||||
final context = engine.analyze('INSERT INTO demo VALUES (?, ?), (?, ?)');
|
||||
|
||||
final variables =
|
||||
context.root.allDescendants.whereType<Variable>().toList();
|
||||
|
||||
expect(context.typeOf(variables[0]), ResolveResult(id.type));
|
||||
expect(context.typeOf(variables[1]), ResolveResult(content.type));
|
||||
expect(context.typeOf(variables[2]), ResolveResult(id.type));
|
||||
expect(context.typeOf(variables[3]), ResolveResult(content.type));
|
||||
});
|
||||
|
||||
test('handles nth_value', () {
|
||||
final ctx = SqlEngine().analyze("SELECT nth_value('string', ?1) = ?2");
|
||||
final variables = ctx.root.allDescendants.whereType<Variable>().iterator;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/src/ast/ast.dart';
|
||||
import 'package:sqlparser/src/utils/ast_equality.dart';
|
||||
import 'package:test_core/test_core.dart';
|
||||
|
||||
import '../common_data.dart';
|
||||
|
@ -115,4 +116,24 @@ void main() {
|
|||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('parses MAPPED BY expressions when in moor mode', () {
|
||||
const stmt = 'CREATE TABLE a (b NOT NULL MAPPED BY `Mapper()` PRIMARY KEY)';
|
||||
final parsed = SqlEngine(useMoorExtensions: true).parse(stmt).rootNode;
|
||||
|
||||
enforceEqual(
|
||||
parsed,
|
||||
CreateTableStatement(tableName: 'a', columns: [
|
||||
ColumnDefinition(
|
||||
columnName: 'b',
|
||||
typeName: null,
|
||||
constraints: [
|
||||
NotNull(null),
|
||||
MappedBy(null, inlineDart('Mapper()')),
|
||||
PrimaryKeyColumn(null),
|
||||
],
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
void main() {
|
||||
test('parses insert statements', () {
|
||||
testStatement(
|
||||
'INSERT OR REPLACE INTO tbl (a, b, c) VALUES (d, e, f)',
|
||||
InsertStatement(
|
||||
mode: InsertMode.insertOrReplace,
|
||||
table: TableReference('tbl', null),
|
||||
targetColumns: [
|
||||
Reference(columnName: 'a'),
|
||||
Reference(columnName: 'b'),
|
||||
Reference(columnName: 'c'),
|
||||
],
|
||||
source: ValuesSource([
|
||||
TupleExpression(expressions: [
|
||||
Reference(columnName: 'd'),
|
||||
Reference(columnName: 'e'),
|
||||
Reference(columnName: 'f'),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('insert statement with default values', () {
|
||||
testStatement(
|
||||
'INSERT INTO tbl DEFAULT VALUES',
|
||||
InsertStatement(
|
||||
mode: InsertMode.insert,
|
||||
table: TableReference('tbl', null),
|
||||
targetColumns: const [],
|
||||
source: const DefaultValues(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('insert statement with select as source', () {
|
||||
testStatement(
|
||||
'REPLACE INTO tbl SELECT * FROM tbl',
|
||||
InsertStatement(
|
||||
mode: InsertMode.replace,
|
||||
table: TableReference('tbl', null),
|
||||
targetColumns: const [],
|
||||
source: SelectInsertSource(
|
||||
SelectStatement(
|
||||
columns: [StarResultColumn(null)],
|
||||
from: [TableReference('tbl', null)],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/src/reader/parser/parser.dart';
|
||||
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
|
||||
import 'package:sqlparser/src/utils/ast_equality.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('can parse multiple statements', () {
|
||||
final sql = 'UPDATE tbl SET a = b; SELECT * FROM tbl;';
|
||||
final tokens = Scanner(sql).scanTokens();
|
||||
final statements = Parser(tokens).statements();
|
||||
|
||||
enforceEqual(
|
||||
statements[0],
|
||||
UpdateStatement(
|
||||
table: TableReference('tbl', null),
|
||||
set: [
|
||||
SetComponent(
|
||||
column: Reference(columnName: 'a'),
|
||||
expression: Reference(columnName: 'b'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
enforceEqual(
|
||||
statements[1],
|
||||
SelectStatement(
|
||||
columns: [StarResultColumn(null)],
|
||||
from: [TableReference('tbl', null)],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('recovers from invalid statements', () {
|
||||
final sql = 'UPDATE tbl SET a = * d; SELECT * FROM tbl;';
|
||||
final tokens = Scanner(sql).scanTokens();
|
||||
final statements = Parser(tokens).statements();
|
||||
|
||||
expect(statements, hasLength(1));
|
||||
enforceEqual(
|
||||
statements[0],
|
||||
SelectStatement(
|
||||
columns: [StarResultColumn(null)],
|
||||
from: [TableReference('tbl', null)],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('parses import directives in moor mode', () {
|
||||
final sql = r'''
|
||||
import 'test.dart';
|
||||
SELECT * FROM tbl;
|
||||
''';
|
||||
|
||||
final tokens = Scanner(sql, scanMoorTokens: true).scanTokens();
|
||||
final statements = Parser(tokens, useMoor: true).statements();
|
||||
|
||||
expect(statements, hasLength(2));
|
||||
|
||||
final parsedImport = statements[0] as ImportStatement;
|
||||
enforceEqual(parsedImport, ImportStatement('test.dart'));
|
||||
expect(parsedImport.importToken, tokens[0]);
|
||||
expect(parsedImport.importString, tokens[1]);
|
||||
expect(parsedImport.semicolon, tokens[2]);
|
||||
});
|
||||
}
|
|
@ -10,6 +10,11 @@ Token token(TokenType type) {
|
|||
return Token(type, null);
|
||||
}
|
||||
|
||||
InlineDartToken inlineDart(String dartCode) {
|
||||
final fakeFile = SourceFile.fromString('`$dartCode`');
|
||||
return InlineDartToken(fakeFile.span(0));
|
||||
}
|
||||
|
||||
IdentifierToken identifier(String content) {
|
||||
final fakeFile = SourceFile.fromString(content);
|
||||
return IdentifierToken(false, fakeFile.span(0));
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
|
||||
import 'package:sqlparser/src/reader/tokenizer/token.dart';
|
||||
|
||||
void main() {
|
||||
test('parses moor specific tokens', () {
|
||||
final part = 'c INTEGER MAPPED BY `const Mapper()` NOT NULL';
|
||||
final scanner = Scanner(part, scanMoorTokens: true);
|
||||
final tokens = scanner.scanTokens();
|
||||
|
||||
expect(scanner.errors, isEmpty);
|
||||
expect(tokens.map((t) => t.type), [
|
||||
TokenType.identifier, // c
|
||||
TokenType.identifier, // INTEGER
|
||||
TokenType.mapped,
|
||||
TokenType.by,
|
||||
TokenType.inlineDart, // `const Mapper()`
|
||||
TokenType.not,
|
||||
TokenType.$null,
|
||||
TokenType.eof,
|
||||
]);
|
||||
|
||||
expect(
|
||||
tokens.whereType<InlineDartToken>().single.dartCode, 'const Mapper()');
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue