mirror of https://github.com/AMT-Cheif/drift.git
Merge pull request #1043 from HosseinYousefi/develop
added support for CREATE VIEW in .moor files
This commit is contained in:
commit
306b2f5d55
|
@ -327,9 +327,8 @@ At the moment, the following statements can appear in a `.moor` file.
|
|||
|
||||
- `import 'other.moor'`: Import all tables and queries declared in the other file
|
||||
into the current file.
|
||||
- DDL statements: You can put `CREATE TABLE`, `CREATE INDEX` and `CREATE TRIGGER` statements
|
||||
into moor files. Views are not currently supported, but [#162](https://github.com/simolus3/moor/issues/162)
|
||||
tracks support for them.
|
||||
- DDL statements: You can put `CREATE TABLE`, `CREATE VIEW`, `CREATE INDEX` and `CREATE TRIGGER` statements
|
||||
into moor files.
|
||||
- Query statements: We support `INSERT`, `SELECT`, `UPDATE` and `DELETE` statements.
|
||||
|
||||
All imports must come before DDL statements, and those must come before named queries.
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -28,7 +28,7 @@ dev_dependencies:
|
|||
path: ^1.8.0-nullsafety.3
|
||||
build_runner: ^1.10.4
|
||||
test: ^1.16.0-nullsafety
|
||||
mockito: ^5.0.0-nullsafety
|
||||
mockito: ^5.0.0-nullsafety.7
|
||||
rxdart: ^0.26.0-nullsafety
|
||||
|
||||
dependency_overrides:
|
||||
|
|
|
@ -136,6 +136,6 @@ DatabaseConnection createConnection(QueryExecutor executor,
|
|||
|
||||
extension on Mock {
|
||||
T _nsm<T>(Invocation invocation, Object? returnValue) {
|
||||
return noSuchMethod(invocation, returnValue) as T;
|
||||
return noSuchMethod(invocation, returnValue: returnValue) as T;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:moor_generator/moor_generator.dart';
|
|||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
||||
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/utils/find_referenced_tables.dart';
|
||||
|
@ -16,8 +15,13 @@ 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,
|
||||
) :
|
||||
// we'll analyze views later, so pass an empty list for now. Otherwise
|
||||
// the incomplete views would be added to the engine.
|
||||
super(availableTables, const [], step) {
|
||||
_referenceResolver = _ReferenceResolvingVisitor(this);
|
||||
}
|
||||
|
||||
|
@ -62,11 +66,7 @@ class EntityHandler extends BaseAnalyzer {
|
|||
|
||||
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);
|
||||
lintContext(context, displayName);
|
||||
}
|
||||
|
||||
Iterable<MoorTable> _findTables(AstNode node) {
|
||||
|
|
|
@ -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) {
|
||||
|
@ -79,6 +81,12 @@ class MoorParser {
|
|||
decl.file = analyzedFile;
|
||||
}
|
||||
|
||||
for (final entity in createdEntities) {
|
||||
if (entity is MoorView) {
|
||||
entity.file = analyzedFile;
|
||||
}
|
||||
}
|
||||
|
||||
return analyzedFile;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@ import 'package:moor_generator/src/analyzer/sql_queries/custom_result_class.dart
|
|||
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/analyzer/view/view_analyzer.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 +62,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>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -18,11 +18,18 @@ 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();
|
||||
|
||||
ViewAnalyzer(this, availableTables, availableViews).resolve();
|
||||
|
||||
final parser =
|
||||
SqlAnalyzer(this, availableTables, availableViews, parseResult.queries)
|
||||
..parse();
|
||||
|
||||
parseResult.resolvedQueries = parser.foundQueries;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,20 +4,23 @@ import 'package:moor_generator/moor_generator.dart';
|
|||
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||
import 'package:moor_generator/src/analyzer/runner/file_graph.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/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 +31,21 @@ abstract class BaseAnalyzer {
|
|||
if (_engine == null) {
|
||||
_engine = step.task.session.spawnEngine();
|
||||
tables.map(mapper.extractStructure).forEach(_engine.registerTable);
|
||||
views.map(mapper.extractView).forEach(_engine.registerView);
|
||||
}
|
||||
return _engine;
|
||||
}
|
||||
|
||||
@protected
|
||||
void lintContext(AnalysisContext context, String displayName) {
|
||||
context.errors.forEach(report);
|
||||
|
||||
// Additional, moor-specific analysis
|
||||
final linter = Linter(context, mapper);
|
||||
linter.reportLints();
|
||||
reportLints(linter.lints, name: displayName);
|
||||
}
|
||||
|
||||
@protected
|
||||
void report(AnalysisError error, {String Function() msg, Severity severity}) {
|
||||
if (step.file.type == FileType.moor) {
|
||||
|
@ -62,8 +76,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) {
|
||||
|
|
|
@ -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 since we don't have view data classes yet.
|
||||
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.
|
||||
|
|
|
@ -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: []);
|
||||
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,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
||||
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
|
||||
import 'package:moor_generator/src/model/table.dart';
|
||||
import 'package:moor_generator/src/model/view.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
class ViewAnalyzer extends BaseAnalyzer {
|
||||
final List<MoorView> viewsToAnalyze;
|
||||
|
||||
ViewAnalyzer(Step step, List<MoorTable> tables, this.viewsToAnalyze)
|
||||
: // We're about to analyze views and add them to the engine, but don't
|
||||
// add the unfinished views right away
|
||||
super(tables, const [], step);
|
||||
|
||||
List<MoorView> _viewsOrder;
|
||||
Set<MoorView> _resolvedViews;
|
||||
|
||||
/// Resolves all the views in topological order.
|
||||
void resolve() {
|
||||
_viewsOrder = [];
|
||||
_resolvedViews = {};
|
||||
// Topologically sorting all the views.
|
||||
for (final view in viewsToAnalyze) {
|
||||
if (!_resolvedViews.contains(view)) {
|
||||
_topologicalSort(view);
|
||||
}
|
||||
}
|
||||
|
||||
// Going through the topologically sorted list and analyzing each view.
|
||||
for (final view in _viewsOrder) {
|
||||
// Registering every table dependency.
|
||||
for (final referencedEntity in view.references) {
|
||||
if (referencedEntity is MoorTable) {
|
||||
engine.registerTable(mapper.extractStructure(referencedEntity));
|
||||
}
|
||||
}
|
||||
final ctx =
|
||||
engine.analyzeNode(view.declaration.node, view.file.parseResult.sql);
|
||||
lintContext(ctx, view.name);
|
||||
|
||||
view.parserView = const SchemaFromCreateTable(moorExtensions: true)
|
||||
.readView(ctx, view.declaration.creatingStatement);
|
||||
engine.registerView(mapper.extractView(view));
|
||||
}
|
||||
}
|
||||
|
||||
void _topologicalSort(MoorView view) {
|
||||
_resolvedViews.add(view);
|
||||
for (final referencedEntity in view.references) {
|
||||
if (referencedEntity is MoorView) {
|
||||
if (!_resolvedViews.contains(referencedEntity)) {
|
||||
_topologicalSort(referencedEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
_viewsOrder.add(view);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -10,3 +10,4 @@ export 'table.dart';
|
|||
export 'trigger.dart';
|
||||
export 'types.dart';
|
||||
export 'used_type_converter.dart';
|
||||
export 'view.dart';
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:moor_generator/src/analyzer/options.dart';
|
||||
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
|
||||
import 'package:moor_generator/src/analyzer/runner/results.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;
|
||||
|
||||
ParsedMoorFile file;
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
List<MoorSchemaEntity> references = [];
|
||||
|
||||
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;
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
@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.parserView.resolvedColumns.length, equals(1));
|
||||
final column = view.parserView.resolvedColumns.single;
|
||||
|
||||
state.close();
|
||||
|
||||
expect(column.type.type, BasicType.text);
|
||||
|
||||
expect(file.errors.errors, isEmpty);
|
||||
});
|
||||
|
||||
test('view created from another view', () 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 parent_view AS
|
||||
SELECT id, name FROM t WHERE id % 2 = 0;
|
||||
|
||||
CREATE VIEW child_view AS
|
||||
SELECT name FROM parent_view;
|
||||
''',
|
||||
});
|
||||
|
||||
final file = await state.analyze('package:foo/a.moor');
|
||||
final parentView = file.currentResult.declaredViews
|
||||
.singleWhere((element) => element.name == 'parent_view');
|
||||
final childView = file.currentResult.declaredViews
|
||||
.singleWhere((element) => element.name == 'child_view');
|
||||
expect(parentView.parserView.resolvedColumns.length, equals(2));
|
||||
expect(childView.parserView.resolvedColumns.length, equals(1));
|
||||
final column = childView.parserView.resolvedColumns.single;
|
||||
|
||||
state.close();
|
||||
|
||||
expect(file.errors.errors, isEmpty);
|
||||
expect(column.type.type, BasicType.text);
|
||||
});
|
||||
|
||||
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.'),
|
||||
)));
|
||||
});
|
||||
|
||||
test('does not allow nested columns', () async {
|
||||
final state = TestState.withContent({
|
||||
'foo|lib/a.moor': '''
|
||||
CREATE TABLE foo (bar INTEGER NOT NULL PRIMARY KEY);
|
||||
|
||||
CREATE VIEW v AS SELECT foo.** FROM foo;
|
||||
''',
|
||||
});
|
||||
|
||||
final file = await state.analyze('package:foo/a.moor');
|
||||
|
||||
state.close();
|
||||
|
||||
expect(
|
||||
file.errors.errors,
|
||||
contains(isA<MoorError>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains('Nested star columns may only appear in a top-level select '
|
||||
'query.'),
|
||||
)));
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -194,7 +194,8 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
|
|||
message: 'Unknown table: ${resultColumn.tableName}',
|
||||
relevantNode: resultColumn,
|
||||
));
|
||||
})!;
|
||||
});
|
||||
if (tableResolver == null) continue;
|
||||
|
||||
visibleColumnsForStar = tableResolver.resultSet!.resolvedColumns;
|
||||
} else {
|
||||
|
|
|
@ -79,7 +79,7 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
|
|||
// Primary key clauses may only include simple columns
|
||||
for (final column in e.columns) {
|
||||
final expr = column.expression;
|
||||
if (expr is! Reference || expr.tableName != null) {
|
||||
if (expr is! Reference || expr.entityName != null) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.synctactic,
|
||||
message: 'Only column names can be used in a PRIMARY KEY clause',
|
||||
|
|
|
@ -32,6 +32,19 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCreateViewStatement(CreateViewStatement e, void arg) {
|
||||
final scope = e.scope = e.scope.createChild();
|
||||
final registeredView = scope.resolve(e.viewName) as View?;
|
||||
if (registeredView != null) {
|
||||
scope.availableColumns = registeredView.resolvedColumns;
|
||||
for (final column in registeredView.resolvedColumns) {
|
||||
print(column.name);
|
||||
scope.register(column.name, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitSelectStatement(SelectStatement e, void arg) {
|
||||
// a select statement can appear as a sub query which has its own scope, so
|
||||
|
|
|
@ -36,15 +36,16 @@ class ReferenceResolver extends RecursiveVisitor<void, void> {
|
|||
|
||||
final scope = e.scope;
|
||||
|
||||
if (e.tableName != null) {
|
||||
// first find the referenced table, then use the column on that table.
|
||||
final tableResolver = scope.resolve<ResolvesToResultSet>(e.tableName!);
|
||||
final resultSet = tableResolver?.resultSet;
|
||||
if (e.entityName != null) {
|
||||
// first find the referenced table or view,
|
||||
// then use the column on that table or view.
|
||||
final entityResolver = scope.resolve<ResolvesToResultSet>(e.entityName!);
|
||||
final resultSet = entityResolver?.resultSet;
|
||||
|
||||
if (resultSet == null) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.referencedUnknownTable,
|
||||
message: 'Unknown table: ${e.tableName}',
|
||||
message: 'Unknown table or view: ${e.entityName}',
|
||||
relevantNode: e,
|
||||
));
|
||||
} else {
|
||||
|
|
|
@ -8,12 +8,13 @@ part of '../ast.dart';
|
|||
/// 2 * c AS d FROM table", the "c" after the "2 *" is a reference that refers
|
||||
/// to the expression "COUNT(*)".
|
||||
class Reference extends Expression with ReferenceOwner {
|
||||
final String? tableName;
|
||||
/// Entity can be either a table or a view.
|
||||
final String? entityName;
|
||||
final String columnName;
|
||||
|
||||
Column? get resolvedColumn => resolved as Column?;
|
||||
|
||||
Reference({this.tableName, required this.columnName});
|
||||
Reference({this.entityName, required this.columnName});
|
||||
|
||||
@override
|
||||
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||
|
@ -28,8 +29,8 @@ class Reference extends Expression with ReferenceOwner {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
if (tableName != null) {
|
||||
return 'Reference to the column $tableName.$columnName';
|
||||
if (entityName != null) {
|
||||
return 'Reference to the column $entityName.$columnName';
|
||||
} else {
|
||||
return 'Reference to the column $columnName';
|
||||
}
|
||||
|
|
|
@ -721,7 +721,7 @@ class Parser {
|
|||
final second =
|
||||
_consumeIdentifier('Expected a column name here', lenient: true);
|
||||
return Reference(
|
||||
tableName: first.identifier, columnName: second.identifier)
|
||||
entityName: first.identifier, columnName: second.identifier)
|
||||
..setSpan(first, second);
|
||||
} else if (_matchOne(TokenType.leftParen)) {
|
||||
// regular function invocation
|
||||
|
|
|
@ -514,7 +514,8 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
void visitReference(Reference e, void arg) {
|
||||
final current = _currentAs<Reference>(e);
|
||||
_assert(
|
||||
current.tableName == e.tableName && current.columnName == e.columnName,
|
||||
current.entityName == e.entityName &&
|
||||
current.columnName == e.columnName,
|
||||
e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
|
|
@ -909,9 +909,9 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
@override
|
||||
void visitReference(Reference e, void arg) {
|
||||
var hasTable = false;
|
||||
if (e.tableName != null) {
|
||||
if (e.entityName != null) {
|
||||
hasTable = true;
|
||||
_identifier(e.tableName!, spaceAfter: false);
|
||||
_identifier(e.entityName!, spaceAfter: false);
|
||||
_symbol('.');
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ void main() {
|
|||
BinaryExpression(
|
||||
NumericLiteral(3, token(TokenType.numberLiteral)),
|
||||
token(TokenType.star),
|
||||
Reference(tableName: 'd', columnName: 'id'),
|
||||
Reference(entityName: 'd', columnName: 'id'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -86,7 +86,7 @@ void main() {
|
|||
onTable: TableReference('tbl'),
|
||||
when: IsExpression(
|
||||
false,
|
||||
Reference(tableName: 'new', columnName: 'foo'),
|
||||
Reference(entityName: 'new', columnName: 'foo'),
|
||||
NullLiteral(token(TokenType.$null)),
|
||||
),
|
||||
action: _block,
|
||||
|
|
|
@ -18,20 +18,20 @@ void main() {
|
|||
),
|
||||
then: IsExpression(
|
||||
true,
|
||||
Reference(tableName: 'n', columnName: 'nextReviewTime'),
|
||||
Reference(entityName: 'n', columnName: 'nextReviewTime'),
|
||||
NullLiteral(token(TokenType.$null)),
|
||||
),
|
||||
),
|
||||
],
|
||||
elseExpr: IsExpression(
|
||||
false,
|
||||
Reference(tableName: 'n', columnName: 'nextReviewTime'),
|
||||
Reference(entityName: 'n', columnName: 'nextReviewTime'),
|
||||
NullLiteral(token(TokenType.$null)),
|
||||
),
|
||||
);
|
||||
|
||||
final folderExpr = BinaryExpression(
|
||||
Reference(tableName: 'n', columnName: 'folderId'),
|
||||
Reference(entityName: 'n', columnName: 'folderId'),
|
||||
token(TokenType.equal),
|
||||
ColonNamedVariable(_colon(':selectedFolderId')),
|
||||
);
|
||||
|
|
|
@ -162,7 +162,7 @@ WHERE json_each.value LIKE '704-%';
|
|||
distinct: true,
|
||||
columns: [
|
||||
ExpressionResultColumn(
|
||||
expression: Reference(tableName: 'user', columnName: 'name'),
|
||||
expression: Reference(entityName: 'user', columnName: 'name'),
|
||||
),
|
||||
],
|
||||
from: JoinClause(
|
||||
|
@ -173,14 +173,14 @@ WHERE json_each.value LIKE '704-%';
|
|||
query: TableValuedFunction(
|
||||
'json_each',
|
||||
ExprFunctionParameters(parameters: [
|
||||
Reference(tableName: 'user', columnName: 'phone')
|
||||
Reference(entityName: 'user', columnName: 'phone')
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
where: StringComparisonExpression(
|
||||
left: Reference(tableName: 'json_each', columnName: 'value'),
|
||||
left: Reference(entityName: 'json_each', columnName: 'value'),
|
||||
operator: token(TokenType.like),
|
||||
right: StringLiteral(stringLiteral('704-%')),
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue