Support @create-queries in moor files

This commit is contained in:
Simon Binder 2020-01-03 22:15:40 +01:00
parent 00c1d2a2e7
commit 373ad320c4
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
14 changed files with 163 additions and 27 deletions

View File

@ -76,6 +76,8 @@ class Migrator {
await createTrigger(entity);
} else if (entity is Index) {
await createIndex(entity);
} else if (entity is OnCreateQuery) {
await issueCustomQuery(entity.sql, const []);
} else {
throw AssertionError('Unknown entity: $entity');
}

View File

@ -46,3 +46,26 @@ class Index extends DatabaseSchemaEntity {
/// Mainly used by generated code.
Index(this.entityName, this.createIndexStmt);
}
/// An internal schema entity to run an sql statement when the database is
/// created.
///
/// The generator uses this entity to implement `@create` statements in moor
/// files:
/// ```sql
/// CREATE TABLE users (name TEXT);
///
/// @create: INSERT INTO users VALUES ('Bob');
/// ```
/// A [OnCreateQuery] is emitted for each `@create` statement in an included
/// moor file.
class OnCreateQuery extends DatabaseSchemaEntity {
/// The sql statement that should be run in the default `onCreate` clause.
final String sql;
/// Create a query that will be run in the default `onCreate` migration.
OnCreateQuery(this.sql);
@override
String get entityName => r'$internal$';
}

View File

@ -1224,6 +1224,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
valueIdx,
withDefaults,
myTrigger,
OnCreateQuery('INSERT INTO config VALUES (\'key\', \'values\')'),
noIds,
withConstraints,
mytable,

View File

@ -40,6 +40,8 @@ readMultiple: SELECT * FROM config WHERE config_key IN ? ORDER BY $clause;
readDynamic: SELECT * FROM config WHERE $predicate;
findValidJsons: SELECT * FROM config WHERE json_valid(config_value);
@create: INSERT INTO config VALUES ('key', 'values');
multiple: SELECT * FROM with_constraints c
INNER JOIN with_defaults d
ON d.a = c.a AND d.b = c.b

View File

@ -37,6 +37,8 @@ END;''';
const _createValueIndex =
'CREATE INDEX IF NOT EXISTS value_idx ON config (config_value);';
const _defaultInsert = "INSERT INTO config VALUES ('key', 'values')";
void main() {
// see ../data/tables/tables.moor
test('creates everything as specified in .moor files', () async {
@ -53,6 +55,7 @@ void main() {
verify(mockQueryExecutor.call(_createEmail, []));
verify(mockQueryExecutor.call(_createMyTrigger, []));
verify(mockQueryExecutor.call(_createValueIndex, []));
verify(mockQueryExecutor.call(_defaultInsert, []));
});
test('can create trigger manually', () async {

View File

@ -40,26 +40,38 @@ class EntityHandler extends BaseAnalyzer {
_handleMoorDeclaration(entity, _triggers) as CreateTriggerStatement;
// triggers can have complex statements, so run the linter on them
final context = engine.analyzeNode(node, file.parseResult.sql);
context.errors.forEach(report);
final linter = Linter(context, mapper);
linter.reportLints();
reportLints(linter.lints, name: entity.displayName);
_lint(node, entity.displayName);
// find additional tables that might be referenced in the body
final tablesFinder = ReferencedTablesVisitor();
node.action.acceptWithoutArg(tablesFinder);
final tablesFromBody = tablesFinder.foundTables.map(mapper.tableToMoor);
entity.bodyReferences.addAll(tablesFromBody);
entity.bodyReferences.addAll(_findTables(node.action));
} else if (entity is MoorIndex) {
entity.table = null;
_handleMoorDeclaration<MoorIndexDeclaration>(entity, _indexes);
} else if (entity is SpecialQuery) {
final node = (entity.declaration as MoorSpecialQueryDeclaration).node;
_lint(node, 'special @create table');
entity.references.addAll(_findTables(node.statement));
}
}
}
void _lint(AstNode node, String displayName) {
final context = engine.analyzeNode(node, file.parseResult.sql);
context.errors.forEach(report);
final linter = Linter(context, mapper);
linter.reportLints();
reportLints(linter.lints, name: displayName);
}
Iterable<MoorTable> _findTables(AstNode node) {
final tablesFinder = ReferencedTablesVisitor();
node.acceptWithoutArg(tablesFinder);
return tablesFinder.foundTables.map(mapper.tableToMoor);
}
AstNode _handleMoorDeclaration<T extends MoorDeclaration>(
HasDeclaration e,
Map<AstNode, HasDeclaration> map,

View File

@ -37,6 +37,19 @@ class MoorParser {
} else if (parsedStmt is DeclaredStatement) {
if (parsedStmt.isRegularQuery) {
queryDeclarations.add(DeclaredMoorQuery.fromStatement(parsedStmt));
} else {
final identifier =
parsedStmt.identifier as SpecialStatementIdentifier;
if (identifier.specialName != 'create') {
step.reportError(
ErrorInMoorFile(
span: identifier.nameToken.span,
message: 'Only @create is supported at the moment.',
),
);
} else {
createdEntities.add(SpecialQuery.fromMoor(parsedStmt, step.file));
}
}
}
}

View File

@ -17,5 +17,7 @@ abstract class MoorSchemaEntity implements HasDeclaration {
String get displayName;
/// The getter in a generated database accessor referring to this model.
///
/// Returns null for entities that shouldn't have a getter.
String get dbGetterName;
}

View File

@ -6,6 +6,7 @@ import 'package:sqlparser/sqlparser.dart';
part 'columns.dart';
part 'database.dart';
part 'index.dart';
part 'special_queries.dart';
part 'tables.dart';
part 'trigger.dart';

View File

@ -0,0 +1,17 @@
part of 'declaration.dart';
abstract class SpecialQueryDeclaration extends Declaration {}
class MoorSpecialQueryDeclaration
implements MoorDeclaration, SpecialQueryDeclaration {
@override
final SourceRange declaration;
@override
final DeclaredStatement node;
MoorSpecialQueryDeclaration._(this.declaration, this.node);
MoorSpecialQueryDeclaration.fromNodeAndFile(this.node, FoundFile file)
: declaration = SourceRange.fromNodeAndFile(node, file);
}

View File

@ -4,6 +4,7 @@ export 'database.dart';
export 'declarations/declaration.dart';
export 'index.dart';
export 'sources.dart';
export 'special_queries.dart';
export 'sql_query.dart';
export 'table.dart';
export 'trigger.dart';

View File

@ -0,0 +1,36 @@
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
import 'package:sqlparser/sqlparser.dart';
import 'model.dart';
enum SpecialQueryMode {
atCreate,
}
/// A special query, such as the ones executes when the database was created.
///
/// Those are generated from `@created:` queries in moor files.
class SpecialQuery implements MoorSchemaEntity {
final String sql;
final SpecialQueryMode mode;
@override
final SpecialQueryDeclaration declaration;
SpecialQuery(this.sql, this.declaration,
[this.mode = SpecialQueryMode.atCreate]);
factory SpecialQuery.fromMoor(DeclaredStatement stmt, FoundFile file) {
return SpecialQuery(stmt.statement.span.text,
MoorSpecialQueryDeclaration.fromNodeAndFile(stmt, file));
}
@override
String get dbGetterName => null;
@override
String get displayName =>
throw UnsupportedError("Special queries don't have a name");
@override
List<MoorTable> references = [];
}

View File

@ -34,7 +34,11 @@ class DatabaseWriter {
final entityGetters = <MoorSchemaEntity, String>{};
for (final entity in db.entities) {
entityGetters[entity] = entity.dbGetterName;
final getterName = entity.dbGetterName;
if (getterName != null) {
entityGetters[entity] = entity.dbGetterName;
}
if (entity is MoorTable) {
final tableClassName = entity.tableInfoName;
@ -92,7 +96,13 @@ class DatabaseWriter {
..write('=> [');
schemaScope
..write(db.entities.map((e) => entityGetters[e]).join(', '))
..write(db.entities.map((e) {
if (e is SpecialQuery) {
return 'OnCreateQuery(${asDartLiteral(e.sql)})';
}
return entityGetters[e];
}).join(', '))
// close list literal, getter and finally the class
..write('];\n}');
}

View File

@ -27,8 +27,19 @@ void main() {
});
group('warns about insert column count mismatch', () {
TestState state;
Future<void> expectError() async {
final file = await state.analyze('package:foo/a.moor');
expect(
file.errors.errors,
contains(const TypeMatcher<MoorError>().having(
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
);
}
test('in top-level queries', () async {
final state = TestState.withContent({
state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -38,17 +49,11 @@ CREATE TABLE foo (
test: INSERT INTO foo VALUES (?)
''',
});
final file = await state.analyze('package:foo/a.moor');
expect(
file.errors.errors,
contains(const TypeMatcher<MoorError>().having(
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
);
await expectError();
});
test('in CREATE TRIGGER statements', () async {
final state = TestState.withContent({
state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -60,13 +65,21 @@ CREATE TRIGGER my_trigger AFTER DELETE ON foo BEGIN
END;
''',
});
await expectError();
});
final file = await state.analyze('package:foo/a.moor');
expect(
file.errors.errors,
contains(const TypeMatcher<MoorError>().having(
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
);
test('in @create statements', () async {
state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE foo (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
context VARCHAR
);
@create: INSERT INTO foo VALUES (old.context);
''',
});
await expectError();
});
});
}