Merge branch 'develop' into analyzer-plugin

# Conflicts:
#	moor_generator/lib/src/parser/moor/moor_analyzer.dart
This commit is contained in:
Simon Binder 2019-09-07 11:31:34 +02:00
commit b550afd68f
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
95 changed files with 2644 additions and 1083 deletions

View File

@ -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

View File

@ -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:

View File

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

View File

@ -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

View File

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

View File

@ -12,6 +12,9 @@ import 'package:moor/src/runtime/statements/update.dart';
const _zoneRootUserKey = #DatabaseConnectionUser;
typedef _CustomWriter<T> = Future<T> Function(
QueryExecutor e, String sql, List<dynamic> vars);
/// Class that runs queries to a subset of all available queries in a database.
///
/// This comes in handy to structure large amounts of database code better: The
@ -111,6 +114,7 @@ mixin QueryEngine on DatabaseConnectionUser {
/// although it is very likely that the user meant to call it on the
/// [Transaction] t. We can detect this by calling the function passed to
/// `transaction` in a forked [Zone] storing the transaction in
@protected
bool get topLevel => false;
/// We can detect when a user called methods on the wrong [QueryEngine]
@ -169,32 +173,59 @@ mixin QueryEngine on DatabaseConnectionUser {
/// You can use the [updates] parameter so that moor knows which tables are
/// affected by your query. All select streams that depend on a table
/// specified there will then issue another query.
@protected
@visibleForTesting
Future<int> customUpdate(String query,
{List<Variable> variables = const [], Set<TableInfo> updates}) async {
return _customWrite(query, variables, updates, (executor, sql, vars) {
return executor.runUpdate(sql, vars);
});
}
/// Executes a custom insert statement and returns the last inserted rowid.
///
/// You can tell moor which tables your query is going to affect by using the
/// [updates] parameter. Query-streams running on any of these tables will
/// then be re-run.
@protected
@visibleForTesting
Future<int> customInsert(String query,
{List<Variable> variables = const [], Set<TableInfo> updates}) {
return _customWrite(query, variables, updates, (executor, sql, vars) {
return executor.runInsert(sql, vars);
});
}
/// Common logic for [customUpdate] and [customInsert] which takes care of
/// mapping the variables, running the query and optionally informing the
/// stream-queries.
Future<T> _customWrite<T>(String query, List<Variable> variables,
Set<TableInfo> updates, _CustomWriter<T> writer) async {
final engine = _resolvedEngine;
final executor = engine.executor;
final ctx = GenerationContext.fromDb(engine);
final mappedArgs = variables.map((v) => v.mapToSimpleValue(ctx)).toList();
final affectedRows = await executor
.doWhenOpened((_) => executor.runUpdate(query, mappedArgs));
final result =
await executor.doWhenOpened((e) => writer(e, query, mappedArgs));
if (updates != null) {
await engine.streamQueries.handleTableUpdates(updates);
}
return affectedRows;
return result;
}
/// Executes a custom select statement once. To use the variables, mark them
/// with a "?" in your [query]. They will then be changed to the appropriate
/// value.
@protected
@visibleForTesting
@Deprecated('use customSelectQuery(...).get() instead')
Future<List<QueryRow>> customSelect(String query,
{List<Variable> variables = const []}) async {
return CustomSelectStatement(
query, variables, <TableInfo>{}, _resolvedEngine)
.get();
return customSelectQuery(query, variables: variables).get();
}
/// Creates a stream from a custom select statement.To use the variables, mark
@ -202,15 +233,36 @@ mixin QueryEngine on DatabaseConnectionUser {
/// appropriate value. The stream will re-emit items when any table in
/// [readsFrom] changes, so be sure to set it to the set of tables your query
/// reads data from.
@protected
@visibleForTesting
@Deprecated('use customSelectQuery(...).watch() instead')
Stream<List<QueryRow>> customSelectStream(String query,
{List<Variable> variables = const [], Set<TableInfo> readsFrom}) {
final tables = readsFrom ?? <TableInfo>{};
final statement =
CustomSelectStatement(query, variables, tables, _resolvedEngine);
return statement.watch();
return customSelectQuery(query, variables: variables, readsFrom: readsFrom)
.watch();
}
/// Creates a custom select statement from the given sql [query]. To run the
/// query once, use [Selectable.get]. For an auto-updating streams, set the
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
/// know the query will never emit more than one row, you can also use
/// [Selectable.getSingle] and [Selectable.watchSingle] which return the item
/// directly or wrapping it into a list.
///
/// If you use variables in your query (for instance with "?"), they will be
/// bound to the [variables] you specify on this query.
@protected
@visibleForTesting
Selectable<QueryRow> customSelectQuery(String query,
{List<Variable> variables = const [],
Set<TableInfo> readsFrom = const {}}) {
readsFrom ??= {};
return CustomSelectStatement(query, variables, readsFrom, _resolvedEngine);
}
/// Executes the custom sql [statement] on the database.
@protected
@visibleForTesting
Future<void> customStatement(String statement) {
return _resolvedEngine.executor.runCustom(statement);
}
@ -226,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) {

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -1,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

View File

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

View File

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

View File

@ -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];

View File

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

View File

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

View File

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

View File

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

View File

@ -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:

View File

@ -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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,4 +52,10 @@ class UpdatedTablesVisitor extends RecursiveVisitor<void> {
_addIfResolved(e.table);
visitChildren(e);
}
@override
void visitInsertStatement(InsertStatement e) {
_addIfResolved(e.table);
visitChildren(e);
}
}

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
part of 'moor_builder.dart';
class MoorOptions {
final bool generateFromJsonStringConstructor;

View File

@ -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];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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';

View File

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

View File

@ -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.

View File

@ -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';

View File

@ -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,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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 [];
}

View File

@ -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 [];
}

View File

@ -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 {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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