Merge pull request #1043 from HosseinYousefi/develop

added support for CREATE VIEW in .moor files
This commit is contained in:
Simon Binder 2021-02-12 14:10:21 +01:00 committed by GitHub
commit 306b2f5d55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 484 additions and 53 deletions

View File

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

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

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

View File

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

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

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

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) {
@ -79,6 +81,12 @@ class MoorParser {
decl.file = analyzedFile;
}
for (final entity in createdEntities) {
if (entity is MoorView) {
entity.file = analyzedFile;
}
}
return analyzedFile;
}
}

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

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

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

View File

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

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

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: []);
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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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