added support for CREATE VIEW in .moor files

This commit is contained in:
Hossein Yousefi 2021-02-10 16:23:51 +01:00
parent 7a2c4e7f75
commit c9269e13be
21 changed files with 332 additions and 20 deletions

View File

@ -47,6 +47,27 @@ class Index extends DatabaseSchemaEntity {
Index(this.entityName, this.createIndexStmt);
}
/// A sqlite view.
///
/// In moor, views can only be declared in `.moor` files.
///
/// For more information on views, see the [CREATE VIEW][sqlite-docs]
/// documentation from sqlite, or the [entry on sqlitetutorial.net][sql-tut].
///
/// [sqlite-docs]: https://www.sqlite.org/lang_createview.html
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/
class View extends DatabaseSchemaEntity {
@override
final String entityName;
/// The `CREATE VIEW` sql statement that can be used to create this view.
final String createViewStmt;
/// Creates an view model by the [createViewStmt] and its [entityName].
/// Mainly used by generated code.
View(this.entityName, this.createViewStmt);
}
/// An internal schema entity to run an sql statement when the database is
/// created.
///

View File

@ -491,7 +491,7 @@ abstract class _$Database extends GeneratedDatabase {
Future<int> _resetCategory(int var1) {
return customUpdate(
'UPDATE todos SET category = NULL WHERE category = ?',
variables: [Variable.withInt(var1)],
variables: [Variable<int>(var1)],
updates: {todos},
updateKind: UpdateKind.update,
);
@ -521,8 +521,8 @@ class CategoriesWithCountResult {
final String desc;
final int amount;
CategoriesWithCountResult({
this.id,
this.desc,
this.amount,
@required this.id,
@required this.desc,
@required this.amount,
});
}

View File

@ -8,7 +8,7 @@ import 'package:sqlparser/sqlparser.dart';
typedef LogFunction = void Function(dynamic message,
[Object error, StackTrace stackTrace]);
/// Base class for errors that can be presented to an user.
/// Base class for errors that can be presented to a user.
class MoorError {
final Severity severity;
final String message;

View File

@ -4,6 +4,7 @@ import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/analyzer/runner/steps.dart';
import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart';
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
import 'package:moor_generator/src/model/view.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/find_referenced_tables.dart';
@ -16,14 +17,18 @@ class EntityHandler extends BaseAnalyzer {
AnalyzeMoorStep get moorStep => step as AnalyzeMoorStep;
EntityHandler(
AnalyzeMoorStep step, this.file, List<MoorTable> availableTables)
: super(availableTables, step) {
AnalyzeMoorStep step,
this.file,
List<MoorTable> availableTables,
List<MoorView> availableViews,
) : super(availableTables, availableViews, step) {
_referenceResolver = _ReferenceResolvingVisitor(this);
}
final Map<CreateTriggerStatement, MoorTrigger> _triggers = {};
final Map<TableInducingStatement, MoorTable> _tables = {};
final Map<CreateIndexStatement, MoorIndex> _indexes = {};
final Map<CreateViewStatement, MoorView> _views = {};
_ReferenceResolvingVisitor _referenceResolver;
@ -56,6 +61,12 @@ class EntityHandler extends BaseAnalyzer {
_lint(node, 'special @create table');
entity.references.addAll(_findTables(node.statement));
} else if (entity is MoorView) {
final node =
_handleMoorDeclaration(entity, _views) as CreateViewStatement;
_lint(node, node.viewName);
entity.references.addAll(_findTables(node.query));
entity.references.addAll(_findViews(node.query));
}
}
}
@ -75,6 +86,12 @@ class EntityHandler extends BaseAnalyzer {
return tablesFinder.foundTables.map(mapper.tableToMoor);
}
Iterable<MoorView> _findViews(AstNode node) {
final tablesFinder = ReferencedTablesVisitor();
node.acceptWithoutArg(tablesFinder);
return tablesFinder.foundViews.map(mapper.viewToMoor);
}
Iterable<WrittenMoorTable> _findUpdatedTables(AstNode node) {
final finder = UpdatedTablesVisitor();
node.acceptWithoutArg(finder);

View File

@ -32,6 +32,8 @@ class MoorParser {
} else if (parsedStmt is CreateTriggerStatement) {
// the table will be resolved in the analysis step
createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file));
} else if (parsedStmt is CreateViewStatement) {
createdEntities.add(MoorView.fromMoor(parsedStmt, step.file));
} else if (parsedStmt is CreateIndexStatement) {
createdEntities.add(MoorIndex.fromMoor(parsedStmt, step.file));
} else if (parsedStmt is DeclaredStatement) {

View File

@ -3,12 +3,16 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
import 'package:moor_generator/src/model/sql_query.dart';
import 'package:moor_generator/src/model/view.dart';
import 'package:sqlparser/sqlparser.dart';
abstract class FileResult {
final List<MoorSchemaEntity> declaredEntities;
Iterable<MoorTable> get declaredTables => declaredEntities.whereType();
Iterable<MoorTable> get declaredTables =>
declaredEntities.whereType<MoorTable>();
Iterable<MoorView> get declaredViews =>
declaredEntities.whereType<MoorView>();
FileResult(this.declaredEntities);
}

View File

@ -14,6 +14,7 @@ import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
import 'package:moor_generator/src/analyzer/runner/task.dart';
import 'package:moor_generator/src/model/sql_query.dart';
import 'package:moor_generator/src/model/view.dart';
import 'package:moor_generator/src/utils/entity_reference_sorter.dart';
import 'package:source_gen/source_gen.dart';
@ -60,6 +61,10 @@ abstract class AnalyzingStep extends Step {
}
Iterable<MoorTable> _availableTables(List<FoundFile> imports) {
return _availableEntities(imports).whereType();
return _availableEntities(imports).whereType<MoorTable>();
}
Iterable<MoorView> _availableViews(List<FoundFile> imports) {
return _availableEntities(imports).whereType<MoorView>();
}
}

View File

@ -74,8 +74,9 @@ class AnalyzeDartStep extends AnalyzingStep {
.expand((f) => f.resolvedQueries);
final availableTables = availableEntities.whereType<MoorTable>().toList();
final parser =
SqlAnalyzer(this, availableTables, accessor.declaredQueries);
final availableViews = availableEntities.whereType<MoorView>().toList();
final parser = SqlAnalyzer(
this, availableTables, availableViews, accessor.declaredQueries);
parser.parse();
accessor

View File

@ -18,10 +18,15 @@ class AnalyzeMoorStep extends AnalyzingStep {
.followedBy(parseResult.declaredTables)
.toList();
final parser = SqlAnalyzer(this, availableTables, parseResult.queries)
..parse();
final availableViews = _availableViews(transitiveImports)
.followedBy(parseResult.declaredViews)
.toList();
EntityHandler(this, parseResult, availableTables).handle();
final parser =
SqlAnalyzer(this, availableTables, availableViews, parseResult.queries)
..parse();
EntityHandler(this, parseResult, availableTables, availableViews).handle();
parseResult.resolvedQueries = parser.foundQueries;
}

View File

@ -7,17 +7,19 @@ import 'package:moor_generator/src/analyzer/runner/steps.dart';
import 'package:moor_generator/src/model/sql_query.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:moor_generator/src/model/view.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
abstract class BaseAnalyzer {
final List<MoorTable> tables;
final List<MoorView> views;
final Step step;
@protected
final TypeMapper mapper;
SqlEngine _engine;
BaseAnalyzer(this.tables, this.step)
BaseAnalyzer(this.tables, this.views, this.step)
: mapper = TypeMapper(
applyTypeConvertersToVariables:
step.task.session.options.applyConvertersOnVariables,
@ -28,10 +30,24 @@ abstract class BaseAnalyzer {
if (_engine == null) {
_engine = step.task.session.spawnEngine();
tables.map(mapper.extractStructure).forEach(_engine.registerTable);
resolveViews();
views.map(mapper.extractView).forEach(_engine.registerView);
}
return _engine;
}
/// Parses the view and adds columns to its resolved columns.
@protected
void resolveViews() {
for (final view in views) {
final ctx = _engine.analyzeNode(
view.declaration.node, view.declaration.createSql);
view.parserView = const SchemaFromCreateTable(moorExtensions: true)
.readView(ctx, view.declaration.creatingStatement);
view.columns = view.parserView.resolvedColumns;
}
}
@protected
void report(AnalysisError error, {String Function() msg, Severity severity}) {
if (step.file.type == FileType.moor) {
@ -62,8 +78,9 @@ class SqlAnalyzer extends BaseAnalyzer {
final List<SqlQuery> foundQueries = [];
SqlAnalyzer(Step step, List<MoorTable> tables, this.definedQueries)
: super(tables, step);
SqlAnalyzer(Step step, List<MoorTable> tables, List<MoorView> views,
this.definedQueries)
: super(tables, views, step);
void parse() {
for (final query in definedQueries) {

View File

@ -17,6 +17,7 @@ class QueryHandler {
final TypeMapper mapper;
Set<Table> _foundTables;
Set<View> _foundViews;
List<FoundElement> _foundElements;
Iterable<FoundVariable> get _foundVariables =>
_foundElements.whereType<FoundVariable>();
@ -75,8 +76,13 @@ class QueryHandler {
final tableFinder = ReferencedTablesVisitor();
_select.acceptWithoutArg(tableFinder);
_foundTables = tableFinder.foundTables;
_foundViews = tableFinder.foundViews;
final moorTables =
_foundTables.map(mapper.tableToMoor).where((s) => s != null).toList();
final moorViews =
_foundViews.map(mapper.viewToMoor).where((s) => s != null).toList();
final moorEntities = [...moorTables, ...moorViews];
String requestedName;
if (source is DeclaredMoorQuery) {
@ -87,7 +93,7 @@ class QueryHandler {
name,
context,
_foundElements,
moorTables,
moorEntities,
_inferResultSet(),
requestedName,
);
@ -121,6 +127,12 @@ class QueryHandler {
candidatesForSingleTable.clear();
}
if (_foundViews.isNotEmpty) {
// For now we're not using the single table optimization when selecting
// from views.
candidatesForSingleTable.clear();
}
// if all columns read from the same table, and all columns in that table
// are present in the result set, we can use the data class we generate for
// that table instead of generating another class just for this result set.

View File

@ -1,6 +1,7 @@
import 'package:moor/moor.dart' as m;
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/model/sql_query.dart';
import 'package:moor_generator/src/model/view.dart';
import 'package:moor_generator/src/utils/type_converter_hint.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/find_referenced_tables.dart' as s;
@ -9,6 +10,7 @@ import 'package:sqlparser/utils/find_referenced_tables.dart' as s;
/// library.
class TypeMapper {
final Map<Table, MoorTable> _engineTablesToSpecified = {};
final Map<View, MoorView> _engineViewsToSpecified = {};
final bool applyTypeConvertersToVariables;
TypeMapper({this.applyTypeConvertersToVariables = false});
@ -87,6 +89,20 @@ class TypeMapper {
throw StateError('Unexpected type: $type');
}
/// Converts a [MoorView] into something that can be understood
/// by the sqlparser library.
View extractView(MoorView view) {
if (view.parserView != null) {
final parserView = view.parserView;
_engineViewsToSpecified[parserView] = view;
return parserView;
}
final engineView = View(name: view.name, resolvedColumns: view.columns);
engineView.setMeta<MoorView>(view);
_engineViewsToSpecified[engineView] = view;
return engineView;
}
/// Extracts variables and Dart templates from the [ctx]. Variables are
/// sorted by their ascending index. Placeholders are sorted by the position
/// they have in the query. When comparing variables and placeholders, the
@ -249,6 +265,10 @@ class TypeMapper {
return _engineTablesToSpecified[table];
}
MoorView viewToMoor(View view) {
return _engineViewsToSpecified[view];
}
WrittenMoorTable writtenToMoor(s.TableWrite table) {
final moorKind = const {
s.UpdateKind.insert: m.UpdateKind.insert,

View File

@ -11,6 +11,7 @@ part 'index.dart';
part 'special_queries.dart';
part 'tables.dart';
part 'trigger.dart';
part 'views.dart';
/// Interface for model elements that are declared somewhere.
abstract class HasDeclaration {

View File

@ -0,0 +1,35 @@
part of 'declaration.dart';
abstract class ViewDeclaration extends Declaration {}
abstract class ViewDeclarationWithSql implements ViewDeclaration {
/// The `CREATE VIEW` statement used to create this view.
String get createSql;
/// The parsed statement creating this view.
CreateViewStatement get creatingStatement;
}
class MoorViewDeclaration
implements ViewDeclaration, MoorDeclaration, ViewDeclarationWithSql {
@override
final SourceRange declaration;
@override
final CreateViewStatement node;
MoorViewDeclaration._(this.declaration, this.node);
factory MoorViewDeclaration(CreateViewStatement node, FoundFile file) {
return MoorViewDeclaration._(
SourceRange.fromNodeAndFile(node, file),
node,
);
}
@override
String get createSql => node.span.text;
@override
CreateViewStatement get creatingStatement => node;
}

View File

@ -10,3 +10,4 @@ export 'table.dart';
export 'trigger.dart';
export 'types.dart';
export 'used_type_converter.dart';
export 'view.dart';

View File

@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moor/moor.dart' show $mrjf, $mrjc, UpdateKind;
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/model/base_entity.dart';
import 'package:moor_generator/src/utils/hash.dart';
import 'package:moor_generator/src/writer/writer.dart';
import 'package:recase/recase.dart';
@ -108,7 +109,7 @@ abstract class SqlQuery {
}
class SqlSelectQuery extends SqlQuery {
final List<MoorTable> readsFrom;
final List<MoorSchemaEntity> readsFrom;
final InferredResultSet resultSet;
/// The name of the result class, as requested by the user.

View File

@ -0,0 +1,47 @@
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
import 'package:sqlparser/sqlparser.dart';
import 'base_entity.dart';
import 'declarations/declaration.dart';
import 'model.dart';
/// A parsed view
class MoorView extends MoorSchemaEntity {
@override
final MoorViewDeclaration declaration;
/// The associated view to use for the sqlparser package when analyzing
/// sql queries. Note that this field is set lazily.
View parserView;
final String name;
@override
List<MoorSchemaEntity> references = [];
List<ViewColumn> columns;
MoorView({
this.declaration,
this.name,
});
factory MoorView.fromMoor(CreateViewStatement stmt, FoundFile file) {
return MoorView(
declaration: MoorViewDeclaration(stmt, file),
name: stmt.viewName,
);
}
/// The `CREATE VIEW` statement that can be used to create this view.
String createSql(MoorOptions options) {
return declaration.formatSqlIfAvailable(options) ?? declaration.createSql;
}
@override
String get dbGetterName => dbFieldName(name);
@override
String get displayName => name;
}

View File

@ -85,6 +85,15 @@ class DatabaseWriter {
'${asDartLiteral(entity.createSql(scope.options))})',
options: scope.generationOptions,
);
} else if (entity is MoorView) {
writeMemoizedGetter(
buffer: dbScope.leaf(),
getterName: entity.dbGetterName,
returnType: 'View',
code: 'View(${asDartLiteral(entity.displayName)}, '
'${asDartLiteral(entity.createSql(scope.options))})',
options: scope.generationOptions,
);
}
}

View File

@ -0,0 +1,53 @@
@Tags(['analyzer'])
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../utils.dart';
void main() {
test('view created', () async {
final state = TestState.withContent({
'foo|lib/table.moor': '''
CREATE TABLE t (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL);
''',
'foo|lib/a.moor': '''
import 'table.moor';
CREATE VIEW random_view AS
SELECT name FROM t WHERE id % 2 = 0;
''',
});
final file = await state.analyze('package:foo/a.moor');
final view = file.currentResult.declaredViews.single;
expect(view.columns.length, equals(1));
final column = view.columns.single;
state.close();
expect(column.type.type, BasicType.text);
expect(file.errors.errors, isEmpty);
});
test('view without table', () async {
final state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE VIEW random_view AS
SELECT name FROM t WHERE id % 2 = 0;
''',
});
final file = await state.analyze('package:foo/a.moor');
state.close();
expect(
file.errors.errors,
contains(isA<MoorError>().having(
(e) => e.message,
'message',
contains('Could not find t.'),
)));
});
}

View File

@ -0,0 +1,61 @@
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:test/test.dart';
import '../utils.dart';
void main() {
test('select from view test', () async {
final state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE artists (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL
);
CREATE TABLE albums (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
artist INTEGER NOT NULL REFERENCES artists (id)
);
CREATE TABLE tracks (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
album INTEGER NOT NULL REFERENCES albums (id),
duration_seconds INTEGER NOT NULL,
was_single BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE VIEW total_duration_by_artist_view AS
SELECT a.*, SUM(tracks.duration_seconds) AS duration
FROM artists a
INNER JOIN albums ON albums.artist = a.id
INNER JOIN tracks ON tracks.album = albums.id
GROUP BY artists.id;
totalDurationByArtist:
SELECT * FROM total_duration_by_artist_view;
'''
}, options: const MoorOptions());
final file = await state.analyze('package:foo/a.moor');
final result = file.currentResult as ParsedMoorFile;
final queries = result.resolvedQueries;
expect(state.session.errorsInFileAndImports(file), isEmpty);
state.close();
final totalDurationByArtist =
queries.singleWhere((q) => q.name == 'totalDurationByArtist');
expect(
totalDurationByArtist,
returnsColumns({
'id': ColumnType.integer,
'name': ColumnType.text,
'duration': ColumnType.integer,
}),
);
});
}

View File

@ -9,7 +9,7 @@ class View extends NamedResultSet with HasMetaMixin implements HumanReadable {
@override
final List<ViewColumn> resolvedColumns;
/// The ast node that created this table
/// The ast node that created this view
final CreateViewStatement? definition;
@override