From d86d7ab7e38eaef0a8e381e17b3f12bdc78023f3 Mon Sep 17 00:00:00 2001 From: Markus Richter <8398165+mqus@users.noreply.github.com> Date: Wed, 13 May 2020 14:29:11 +0200 Subject: [PATCH] Parser is ready, work on analyzer was started. --- sqlparser/lib/src/analysis/analysis.dart | 1 + sqlparser/lib/src/analysis/schema/column.dart | 67 +++++++++++ sqlparser/lib/src/analysis/schema/view.dart | 104 ++++++++++++++++++ sqlparser/lib/src/ast/ast.dart | 1 + .../lib/src/ast/statements/create_view.dart | 37 +++++++ sqlparser/lib/src/ast/visitor.dart | 6 + sqlparser/lib/src/reader/parser/schema.dart | 45 ++++++++ sqlparser/lib/src/reader/tokenizer/token.dart | 2 + 8 files changed, 263 insertions(+) create mode 100644 sqlparser/lib/src/analysis/schema/view.dart create mode 100644 sqlparser/lib/src/ast/statements/create_view.dart diff --git a/sqlparser/lib/src/analysis/analysis.dart b/sqlparser/lib/src/analysis/analysis.dart index 944f1ce8..9de7d220 100644 --- a/sqlparser/lib/src/analysis/analysis.dart +++ b/sqlparser/lib/src/analysis/analysis.dart @@ -17,6 +17,7 @@ part 'schema/column.dart'; part 'schema/from_create_table.dart'; part 'schema/references.dart'; part 'schema/table.dart'; +part 'schema/view.dart'; part 'steps/column_resolver.dart'; part 'steps/linting_visitor.dart'; part 'steps/prepare_ast.dart'; diff --git a/sqlparser/lib/src/analysis/schema/column.dart b/sqlparser/lib/src/analysis/schema/column.dart index 32ad1fe2..2399a1a7 100644 --- a/sqlparser/lib/src/analysis/schema/column.dart +++ b/sqlparser/lib/src/analysis/schema/column.dart @@ -103,6 +103,73 @@ class TableColumn extends Column { } } +/// A column that is part of a table. +class ViewColumn extends Column { + @override + final String name; + + /// The type of this column, which is available before any resolution happens + /// (we know ii from the table). + ResolvedType get type => _type; + ResolvedType _type; + + /// The column constraints set on this column. + /// + /// This only works columns where [hasDefinition] is true, otherwise this + /// getter will throw. The columns in a `CREATE TABLE` statement always have + /// a definition, but those from a `CREATE VIRTUAL TABLE` likely don't. + /// + /// See also: + /// - https://www.sqlite.org/syntax/column-constraint.html + List get constraints => definition.constraints; + + /// The definition in the AST that was used to create this column model. + final ColumnDefinition definition; + + /// Whether this column has a definition from the ast. + bool get hasDefinition => definition != null; + + /// The table this column belongs to. + View view; + + ViewColumn(this.name, this._type, {this.definition}); + + /// Applies a type hint to this column. + /// + /// The [hint] will then be reflected in the [type]. + void applyTypeHint(TypeHint hint) { + _type = _type?.copyWith(hint: hint); + } + + /// Whether this column is an alias for the rowid, as defined in + /// https://www.sqlite.org/lang_createtable.html#rowid + /// + /// To summarize, a column is an alias for the rowid if all of the following + /// conditions are met: + /// - the table has a primary key that consists of exactly one (this) column + /// - the column is declared to be an integer + /// - if this column has a [PrimaryKeyColumn], the [OrderingMode] of that + /// constraint is not [OrderingMode.descending]. + bool isAliasForRowId() { + //TODO is this applicable to views? + if (definition == null || + view == null || + type?.type != BasicType.int || + view.withoutRowId) { + return false; + } + + return false; + } + + @override + String humanReadableDescription() { + return '$name in ${view.humanReadableDescription()}'; + } +} + + + /// Refers to the special "rowid", "oid" or "_rowid_" column defined for tables /// that weren't created with an `WITHOUT ROWID` clause. class RowId extends TableColumn { diff --git a/sqlparser/lib/src/analysis/schema/view.dart b/sqlparser/lib/src/analysis/schema/view.dart new file mode 100644 index 00000000..ca2daf83 --- /dev/null +++ b/sqlparser/lib/src/analysis/schema/view.dart @@ -0,0 +1,104 @@ +part of '../analysis.dart'; + +/// A database view. The information stored here will be used to resolve +/// references and for type inference. +class View with HasMetaMixin implements HumanReadable, ResolvesToResultSet { + /// The name of this table, as it appears in sql statements. This should be + /// the raw name, not an escaped version. + /// + /// To obtain an escaped name, use [escapedName]. + final String name; + + /// If [name] is a reserved sql keyword, wraps it in double ticks. Otherwise + /// just returns the [name] directly. + String get escapedName { + return isKeywordLexeme(name) ? '"$name"' : name; + } + + final List resolvedColumns; + + /// Filter the [resolvedColumns] for those that are + /// [Column.includedInResults]. + List get resultColumns => + resolvedColumns.where((c) => c.includedInResults).toList(); + + /// Whether this table was created with an "WITHOUT ROWID" modifier + final bool withoutRowId; + + /// Additional constraints set on this table. + final List tableConstraints; + + /// The ast node that created this table + final TableInducingStatement definition; + + @override + bool get visibleToChildren => true; + + ViewColumn _rowIdColumn; + + /// Constructs a table from the known [name] and [resolvedColumns]. + View( + {@required this.name, + this.resolvedColumns, + this.withoutRowId = false, + this.tableConstraints = const [], + this.definition}) { + for (final column in resolvedColumns) { + column.view = this; + + if (_rowIdColumn == null && column.isAliasForRowId()) { + _rowIdColumn = column; + } + } + } + + @override + Column findColumn(String name) { +// final defaultSearch = super.findColumn(name); +// if (defaultSearch != null) return defaultSearch; +// +// // handle aliases to rowids, see https://www.sqlite.org/lang_createtable.html#rowid +// if (aliasesForRowId.contains(name.toLowerCase()) && !withoutRowId) { +// return _rowIdColumn ?? RowId() +// ..table = this; +// } + return null; + } + + @override + String humanReadableDescription() { + return name; + } + + @override + // TODO: implement resultSet + ResultSet get resultSet => null; +} +// +//class TableAlias implements ResultSet, HumanReadable { +// final ResultSet delegate; +// final String alias; +// +// TableAlias(this.delegate, this.alias); +// +// @override +// List get resolvedColumns => delegate.resolvedColumns; +// +// @override +// Column findColumn(String name) => delegate.findColumn(name); +// +// @override +// ResultSet get resultSet => this; +// +// @override +// bool get visibleToChildren => delegate.visibleToChildren; +// +// @override +// String humanReadableDescription() { +// final delegateDescription = delegate is HumanReadable +// ? (delegate as HumanReadable).humanReadableDescription() +// : delegate.toString(); +// +// return '$alias (alias to $delegateDescription)'; +// } +//} diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart index 869a5d43..214b61eb 100644 --- a/sqlparser/lib/src/ast/ast.dart +++ b/sqlparser/lib/src/ast/ast.dart @@ -34,6 +34,7 @@ part 'statements/block.dart'; part 'statements/create_table.dart'; part 'statements/create_index.dart'; part 'statements/create_trigger.dart'; +part 'statements/create_view.dart'; part 'statements/delete.dart'; part 'statements/insert.dart'; part 'statements/select.dart'; diff --git a/sqlparser/lib/src/ast/statements/create_view.dart b/sqlparser/lib/src/ast/statements/create_view.dart new file mode 100644 index 00000000..eaf77e68 --- /dev/null +++ b/sqlparser/lib/src/ast/statements/create_view.dart @@ -0,0 +1,37 @@ +part of '../ast.dart'; + +/// A "CREATE VIEW" statement, see https://sqlite.org/lang_createview.html +class CreateViewStatement extends Statement implements CreatingStatement { + final bool ifNotExists; + + final String viewName; + IdentifierToken viewNameToken; + + final BaseSelectStatement query; + + final List columns; + + CreateViewStatement( + {this.ifNotExists = false, + @required this.viewName, + this.columns, + @required this.query}); + + @override + String get createdName => viewName; + + @override + R accept(AstVisitor visitor, A arg) { + return visitor.visitCreateViewStatement(this, arg); + } + + @override + Iterable get childNodes => [query]; + + @override + bool contentEquals(CreateViewStatement other) { + return other.ifNotExists == ifNotExists && + other.viewName == viewName && + const ListEquality().equals(other.columns,columns); + } +} diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index 88893dd1..124b1d00 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -13,6 +13,7 @@ abstract class AstVisitor { R visitCreateVirtualTableStatement(CreateVirtualTableStatement e, A arg); R visitCreateTriggerStatement(CreateTriggerStatement e, A arg); R visitCreateIndexStatement(CreateIndexStatement e, A arg); + R visitCreateViewStatement(CreateViewStatement e, A arg); R visitWithClause(WithClause e, A arg); R visitUpsertClause(UpsertClause e, A arg); @@ -122,6 +123,11 @@ class RecursiveVisitor implements AstVisitor { return visitTableInducingStatement(e, arg); } + @override + R visitCreateViewStatement(CreateViewStatement e, A arg) { + return visitStatement(e, arg); + } + @override R visitCreateTriggerStatement(CreateTriggerStatement e, A arg) { return visitSchemaStatement(e, arg); diff --git a/sqlparser/lib/src/reader/parser/schema.dart b/sqlparser/lib/src/reader/parser/schema.dart index 0f543876..206fbe5d 100644 --- a/sqlparser/lib/src/reader/parser/schema.dart +++ b/sqlparser/lib/src/reader/parser/schema.dart @@ -10,6 +10,8 @@ mixin SchemaParser on ParserBase { return _createTrigger(); } else if (_check(TokenType.unique) || _check(TokenType.$index)) { return _createIndex(); + } else if (_check(TokenType.view)) { + return _createView(); } return null; @@ -247,6 +249,37 @@ mixin SchemaParser on ParserBase { ..triggerNameToken = trigger; } + /// Parses a [CreateViewStatement]. The `CREATE` token must have already been + /// accepted. + CreateViewStatement _createView() { + final create = _previous; + assert(create.type == TokenType.create); + + if (!_matchOne(TokenType.view)) return null; + + final ifNotExists = _ifNotExists(); + final name = _consumeIdentifier('Expected a name for this index'); + + List columnNames = null; + if (_matchOne(TokenType.leftParen)) { + columnNames = _columnNames(); + _consume(TokenType.rightParen, 'Expected closing bracket'); + } + + _consume(TokenType.as, 'Expected AS SELECT'); + + final query = select(); + + return CreateViewStatement( + ifNotExists: ifNotExists, + viewName: name.identifier, + columns: columnNames, + query: query, + ) + ..viewNameToken = name + ..setSpan(create, _previous); + } + /// Parses a [CreateIndexStatement]. The `CREATE` token must have already been /// accepted. CreateIndexStatement _createIndex() { @@ -288,6 +321,18 @@ mixin SchemaParser on ParserBase { ..setSpan(create, _previous); } + @override + List _columnNames() { + final columns = []; + do { + final colName = _consumeIdentifier('Expected a name for this column'); + + columns.add(colName.identifier); + } while (_matchOne(TokenType.comma)); + + return columns; + } + @override List _indexedColumns() { final indexes = []; diff --git a/sqlparser/lib/src/reader/tokenizer/token.dart b/sqlparser/lib/src/reader/tokenizer/token.dart index 71d1ba4d..a97033eb 100644 --- a/sqlparser/lib/src/reader/tokenizer/token.dart +++ b/sqlparser/lib/src/reader/tokenizer/token.dart @@ -141,6 +141,7 @@ enum TokenType { unique, update, using, + view, virtual, when, where, @@ -262,6 +263,7 @@ const Map keywords = { 'UPDATE': TokenType.update, 'USING': TokenType.using, 'VALUES': TokenType.$values, + 'VIEW': TokenType.view, 'VIRTUAL': TokenType.virtual, 'WHEN': TokenType.when, 'WHERE': TokenType.where,