Prepare parser for schema support

This commit is contained in:
Simon Binder 2021-07-22 21:32:53 +02:00
parent f23cf9f426
commit aa75cbaa19
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
18 changed files with 188 additions and 97 deletions

View File

@ -42,13 +42,14 @@ abstract class TableOrSubquery extends Queryable {
class TableReference extends TableOrSubquery
with ReferenceOwner
implements Renamable, ResolvesToResultSet {
final String? schemaName;
final String tableName;
Token? tableNameToken;
@override
final String? as;
TableReference(this.tableName, [this.as]);
TableReference(this.tableName, {this.as, this.schemaName});
@override
Iterable<AstNode> get childNodes => const [];

View File

@ -8,6 +8,11 @@ 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 {
/// An optional schema name.
///
/// When this is non-null, [entityName] will not be null either.
final String? schemaName;
/// Entity can be either a table or a view.
final String? entityName;
final String columnName;
@ -17,7 +22,11 @@ class Reference extends Expression with ReferenceOwner {
Column? get resolvedColumn => resolved as Column?;
Reference({this.entityName, required this.columnName});
Reference({this.entityName, this.schemaName, required this.columnName})
: assert(
entityName != null || schemaName == null,
'When setting a schemaName, entityName must not be null either.',
);
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
@ -32,10 +41,16 @@ class Reference extends Expression with ReferenceOwner {
@override
String toString() {
if (entityName != null) {
return 'Reference to the column $entityName.$columnName';
} else {
return 'Reference to the column $columnName';
final result = StringBuffer();
if (schemaName != null) {
result..write(schemaName)..write('.');
}
if (entityName != null) {
result..write(entityName)..write('.');
}
result.write(columnName);
return result.toString();
}
}

View File

@ -751,32 +751,7 @@ class Parser {
return CastExpression(operand, typeName)..setSpan(first, _previous);
} else if (_checkIdentifier()) {
final first = _consumeIdentifier(
'This error message should never be displayed. Please report.');
// could be table.column, function(...) or just column
if (_matchOne(TokenType.dot)) {
final second =
_consumeIdentifier('Expected a column name here', lenient: true);
return Reference(
entityName: first.identifier, columnName: second.identifier)
..setSpan(first, second);
} else if (_matchOne(TokenType.leftParen)) {
// regular function invocation
final parameters = _functionParameters();
final rightParen = _consume(TokenType.rightParen,
'Expected closing bracket after argument list');
if (_peek.type == TokenType.filter || _peek.type == TokenType.over) {
return _aggregate(first, parameters);
}
return FunctionExpression(
name: first.identifier, parameters: parameters)
..setSpan(first, rightParen);
} else {
return Reference(columnName: first.identifier)..setSpan(first, first);
}
return _referenceOrFunctionCall();
}
if (_peek is KeywordToken) {
@ -787,6 +762,55 @@ class Parser {
}
}
Expression _referenceOrFunctionCall() {
final first = _consumeIdentifier(
'This error message should never be displayed. Please report.');
// An expression starting with an identifier could be three things:
// - a simple reference: "foo"
// - a reference with a table: "foo.bar"
// - a reference with a table and a schema: "foo.bar.baz"
// - a function call: "foo()"
if (_matchOne(TokenType.dot)) {
// Ok, we're down to two here. it's either a table or a schema ref
final second = _consumeIdentifier('Expected a column or table name here',
lenient: true);
if (_matchOne(TokenType.dot)) {
// Three identifiers, that's a schema reference
final third =
_consumeIdentifier('Expected a column name here', lenient: true);
return Reference(
schemaName: first.identifier,
entityName: second.identifier,
columnName: third.identifier,
)..setSpan(first, third);
} else {
// Two identifiers only, so we have a table-based reference
return Reference(
entityName: first.identifier,
columnName: second.identifier,
)..setSpan(first, second);
}
} else if (_matchOne(TokenType.leftParen)) {
// We have something like "foo(" -> that's a function!
final parameters = _functionParameters();
final rightParen = _consume(
TokenType.rightParen, 'Expected closing bracket after argument list');
if (_peek.type == TokenType.filter || _peek.type == TokenType.over) {
return _aggregate(first, parameters);
}
return FunctionExpression(name: first.identifier, parameters: parameters)
..setSpan(first, rightParen);
} else {
// Ok, just a regular reference then
return Reference(columnName: first.identifier)..setSpan(first, first);
}
}
Variable? _variableOrNull() {
if (_matchOne(TokenType.questionMarkVariable)) {
return NumberedVariable(_previous as QuestionMarkVariableToken)
@ -1171,19 +1195,6 @@ class Parser {
return null;
}
TableReference _tableReference() {
_suggestHint(const TableNameDescription());
// ignore the schema name, it's not supported. Besides that, we're on the
// first branch in the diagram here https://www.sqlite.org/syntax/table-or-subquery.html
final firstToken = _consumeIdentifier('Expected a table reference');
final tableName = firstToken.identifier;
final alias = _as();
return TableReference(tableName, alias?.identifier)
..setSpan(firstToken, _previous)
..tableNameToken = firstToken;
}
JoinClause? _joinClause(TableOrSubquery start) {
var operator = _parseJoinOperator();
if (operator == null) {
@ -2424,15 +2435,37 @@ class Parser {
return null;
}
TableReference _tableReference({bool allowAlias = true}) {
_suggestHint(const TableNameDescription());
final first = _consumeIdentifier('Expected table or schema name here');
IdentifierToken? second;
IdentifierToken? as;
if (_matchOne(TokenType.dot)) {
second = _consumeIdentifier('Expected a table name here');
}
if (allowAlias) {
as = _as();
}
final tableNameToken = second ?? first;
return TableReference(
tableNameToken.identifier,
as: as?.identifier,
schemaName: second == null ? null : first.identifier,
)
..setSpan(first, _previous)
..tableNameToken = tableNameToken;
}
ForeignKeyClause _foreignKeyClause() {
// https://www.sqlite.org/syntax/foreign-key-clause.html
_consume(TokenType.references, 'Expected REFERENCES');
final firstToken = _previous;
final foreignTable = _consumeIdentifier('Expected a table name');
final foreignTableName = TableReference(foreignTable.identifier, null)
..setSpan(foreignTable, foreignTable);
final foreignTable = _tableReference(allowAlias: false);
final columnNames = _listColumnsInParentheses(allowEmpty: true);
ReferenceAction? onDelete, onUpdate;
@ -2472,7 +2505,7 @@ class Parser {
}
return ForeignKeyClause(
foreignTable: foreignTableName,
foreignTable: foreignTable,
columnNames: columnNames,
onUpdate: onUpdate,
onDelete: onDelete,

View File

@ -547,7 +547,8 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
void visitReference(Reference e, void arg) {
final current = _currentAs<Reference>(e);
_assert(
current.entityName == e.entityName &&
current.schemaName == e.schemaName &&
current.entityName == e.entityName &&
current.columnName == e.columnName,
e);
_checkChildren(e);
@ -628,7 +629,11 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
@override
void visitTableReference(TableReference e, void arg) {
final current = _currentAs<TableReference>(e);
_assert(current.tableName == e.tableName && current.as == e.as, e);
_assert(
current.schemaName == e.schemaName &&
current.tableName == e.tableName &&
current.as == e.as,
e);
_checkChildren(e);
}

View File

@ -964,14 +964,22 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
@override
void visitReference(Reference e, void arg) {
var hasTable = false;
if (e.entityName != null) {
hasTable = true;
_identifier(e.entityName!, spaceAfter: false);
var didWriteSpaceBefore = false;
if (e.schemaName != null) {
_identifier(e.schemaName!, spaceAfter: false);
_symbol('.');
didWriteSpaceBefore = true;
}
if (e.entityName != null) {
_identifier(e.entityName!,
spaceAfter: false, spaceBefore: !didWriteSpaceBefore);
_symbol('.');
didWriteSpaceBefore = true;
}
_identifier(e.columnName, spaceAfter: true, spaceBefore: !hasTable);
_identifier(e.columnName,
spaceAfter: true, spaceBefore: !didWriteSpaceBefore);
}
@override
@ -1131,7 +1139,12 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
@override
void visitTableReference(TableReference e, void arg) {
_identifier(e.tableName);
if (e.schemaName != null) {
_identifier(e.schemaName!, spaceAfter: false);
_symbol('.');
}
_identifier(e.tableName, spaceBefore: e.schemaName == null);
if (e.as != null) {
_keyword(TokenType.as);
_identifier(e.as!);

View File

@ -77,7 +77,7 @@ void main() {
ForeignKeyColumnConstraint(
null,
ForeignKeyClause(
foreignTable: TableReference('some', null),
foreignTable: TableReference('some'),
columnNames: [Reference(columnName: 'thing')],
onUpdate: ReferenceAction.cascade,
onDelete: ReferenceAction.setNull,
@ -107,7 +107,7 @@ void main() {
Reference(columnName: 'email'),
],
clause: ForeignKeyClause(
foreignTable: TableReference('another', null),
foreignTable: TableReference('another'),
columnNames: [
Reference(columnName: 'a'),
Reference(columnName: 'b'),

View File

@ -9,7 +9,7 @@ void main() {
testStatement(
'DELETE FROM tbl WHERE id = 5',
DeleteStatement(
from: TableReference('tbl', null),
from: TableReference('tbl'),
where: BinaryExpression(
Reference(columnName: 'id'),
token(TokenType.equal),
@ -26,7 +26,7 @@ void main() {
testStatement(
'DELETE FROM tbl RETURNING *;',
DeleteStatement(
from: TableReference('tbl', null),
from: TableReference('tbl'),
returning: Returning([StarResultColumn()]),
),
);

View File

@ -106,7 +106,7 @@ final Map<String, Expression> _testCases = {
ExistsExpression(
select: SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('demo', null),
from: TableReference('demo'),
),
),
),
@ -141,7 +141,7 @@ final Map<String, Expression> _testCases = {
expression: Reference(columnName: 'col'),
)
],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
),
),
@ -192,6 +192,13 @@ final Map<String, Expression> _testCases = {
'RAISE(IGNORE)': RaiseExpression(RaiseKind.ignore),
"RAISE(ROLLBACK, 'Not allowed')":
RaiseExpression(RaiseKind.rollback, 'Not allowed'),
'foo': Reference(columnName: 'foo'),
'foo.bar': Reference(entityName: 'foo', columnName: 'bar'),
'foo.bar.baz': Reference(
schemaName: 'foo',
entityName: 'bar',
columnName: 'baz',
),
};
void main() {

View File

@ -9,7 +9,7 @@ void main() {
r'SELECT * FROM tbl LIMIT $limit',
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
limit: DartLimitPlaceholder(name: 'limit'),
),
moorMode: true,
@ -21,7 +21,7 @@ void main() {
r'SELECT * FROM tbl LIMIT $amount OFFSET 3',
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
limit: Limit(
count: DartExpressionPlaceholder(name: 'amount'),
offsetSeparator: token(TokenType.offset),
@ -37,7 +37,7 @@ void main() {
r'SELECT * FROM tbl ORDER BY $term, $expr DESC',
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
orderBy: OrderBy(
terms: [
DartOrderingTermPlaceholder(name: 'term'),
@ -57,7 +57,7 @@ void main() {
r'SELECT * FROM tbl ORDER BY $order',
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
orderBy: DartOrderByPlaceholder(name: 'order'),
),
moorMode: true,

View File

@ -9,7 +9,7 @@ void main() {
'INSERT OR REPLACE INTO tbl (a, b, c) VALUES (d, e, f)',
InsertStatement(
mode: InsertMode.insertOrReplace,
table: TableReference('tbl', null),
table: TableReference('tbl'),
targetColumns: [
Reference(columnName: 'a'),
Reference(columnName: 'b'),
@ -31,7 +31,7 @@ void main() {
'INSERT INTO tbl DEFAULT VALUES',
InsertStatement(
mode: InsertMode.insert,
table: TableReference('tbl', null),
table: TableReference('tbl'),
targetColumns: const [],
source: DefaultValues(),
),
@ -43,12 +43,12 @@ void main() {
'REPLACE INTO tbl SELECT * FROM tbl',
InsertStatement(
mode: InsertMode.replace,
table: TableReference('tbl', null),
table: TableReference('tbl'),
targetColumns: const [],
source: SelectInsertSource(
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
),
),

View File

@ -46,7 +46,7 @@ void main() {
ForeignKeyColumnConstraint(
null,
ForeignKeyClause(
foreignTable: TableReference('other', null),
foreignTable: TableReference('other'),
columnNames: [
Reference(columnName: 'location'),
],
@ -62,7 +62,7 @@ void main() {
SimpleName('all'),
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
where: DartExpressionPlaceholder(name: 'predicate'),
),
),
@ -70,7 +70,7 @@ void main() {
SpecialStatementIdentifier('special'),
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
),
DeclaredStatement(
@ -104,7 +104,7 @@ void main() {
SimpleName('nested'),
SelectStatement(
columns: [NestedStarResultColumn('foo')],
from: TableReference('tbl', 'foo'),
from: TableReference('tbl', as: 'foo'),
),
as: 'MyResultSet',
),

View File

@ -16,7 +16,7 @@ void main() {
DeclaredStatement(
SimpleName('a'),
UpdateStatement(
table: TableReference('tbl', null),
table: TableReference('tbl'),
set: [
SetComponent(
column: Reference(columnName: 'a'),
@ -29,7 +29,7 @@ void main() {
SimpleName('b'),
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
),
]),
@ -48,7 +48,7 @@ void main() {
SimpleName('b'),
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
),
);
@ -78,7 +78,7 @@ void main() {
SimpleName('query'),
SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
),
);

View File

@ -47,7 +47,7 @@ void main() {
and n.folderId = :selectedFolderId;
''',
SelectStatement(
from: TableReference('notes', 'n'),
from: TableReference('notes', as: 'n'),
columns: [StarResultColumn()],
where: BinaryExpression(
caseExpr,
@ -69,7 +69,7 @@ void main() {
END;
''',
SelectStatement(
from: TableReference('notes', 'n'),
from: TableReference('notes', as: 'n'),
columns: [StarResultColumn()],
where: BinaryExpression(
folderExpr,

View File

@ -10,7 +10,7 @@ void main() {
CompoundSelectStatement(
base: SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
additional: [
CompoundSelectPart(

View File

@ -16,7 +16,13 @@ void main() {
final stmt =
SqlEngine().parse('SELECT * FROM tbl').rootNode as SelectStatement;
enforceEqual(stmt.from!, TableReference('tbl', null));
_enforceFrom(stmt, TableReference('tbl'));
});
test('schema name and alias', () {
final stmt = SqlEngine().parse('SELECT * FROM main.tbl foo').rootNode
as SelectStatement;
_enforceFrom(stmt, TableReference('tbl', schemaName: 'main', as: 'foo'));
});
test('from more than one table', () {
@ -27,11 +33,11 @@ void main() {
_enforceFrom(
stmt,
JoinClause(
primary: TableReference('tbl', 'test'),
primary: TableReference('tbl', as: 'test'),
joins: [
Join(
operator: JoinOperator.comma,
query: TableReference('table2', null),
query: TableReference('table2'),
),
],
),
@ -46,11 +52,11 @@ void main() {
_enforceFrom(
stmt,
JoinClause(
primary: TableReference('tbl', 'test'),
primary: TableReference('tbl', as: 'test'),
joins: [
Join(
operator: JoinOperator.comma,
query: TableReference('table2', null),
query: TableReference('table2'),
constraint: OnConstraint(
expression: BooleanLiteral.withTrue(token(TokenType.$true)),
),
@ -69,14 +75,14 @@ void main() {
_enforceFrom(
stmt,
JoinClause(
primary: TableReference('table1', null),
primary: TableReference('table1'),
joins: [
Join(
operator: JoinOperator.comma,
query: SelectStatementAsSource(
statement: SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('table2', null),
from: TableReference('table2'),
where: Reference(columnName: 'a'),
),
as: 'inner',
@ -132,16 +138,16 @@ void main() {
_enforceFrom(
stmt,
JoinClause(
primary: TableReference('table1', null),
primary: TableReference('table1'),
joins: [
Join(
operator: JoinOperator.inner,
query: TableReference('table2', null),
query: TableReference('table2'),
constraint: UsingConstraint(columnNames: ['test']),
),
Join(
operator: JoinOperator.leftOuter,
query: TableReference('table3', null),
query: TableReference('table3'),
constraint: OnConstraint(
expression: BooleanLiteral.withTrue(token(TokenType.$true)),
),

View File

@ -32,16 +32,16 @@ final Map<String, AstNode> testCases = {
expression: SubQuery(
select: SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('table2', null),
from: TableReference('table2'),
),
),
),
],
from: TableReference('tbl', null),
from: TableReference('tbl'),
),
'SELECT * FROM tbl WHERE id IN ()': SelectStatement(
columns: [StarResultColumn(null)],
from: TableReference('tbl', null),
from: TableReference('tbl'),
where: InExpression(
left: Reference(columnName: 'id'),
inside: Tuple(expressions: []),

View File

@ -6,7 +6,7 @@ import 'utils.dart';
final Map<String, AstNode> testCases = {
'UPDATE OR ROLLBACK tbl SET a = NULL, b = c WHERE d': UpdateStatement(
or: FailureMode.rollback,
table: TableReference('tbl', null),
table: TableReference('tbl'),
set: [
SetComponent(
column: Reference(columnName: 'a'),

View File

@ -221,6 +221,11 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
''');
});
test('table references', () {
testFormat('SELECT * FROM foo');
testFormat('SELECT * FROM main.foo');
});
test('limit', () {
testFormat('SELECT * FROM foo LIMIT 3, 4');
testFormat('SELECT * FROM foo LIMIT 4 OFFSET 3');
@ -379,6 +384,12 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
test('unary expression', () {
testFormat('SELECT -(+(~3));');
});
test('references', () {
testFormat('SELECT foo');
testFormat('SELECT foo.bar');
testFormat('SELECT foo.bar.baz');
});
});
group('moor', () {