mirror of https://github.com/AMT-Cheif/drift.git
Handle aliases references to rowid (#197)
This commit is contained in:
parent
263004fe7b
commit
d8226aeb23
|
@ -1,3 +1,6 @@
|
|||
## unreleased
|
||||
- Handle special `rowid`, `oid`, `__rowid__` references
|
||||
|
||||
## 0.3.0
|
||||
- parse compound select statements
|
||||
- scan comment tokens
|
||||
|
|
|
@ -30,6 +30,53 @@ class TableColumn extends Column {
|
|||
Table table;
|
||||
|
||||
TableColumn(this.name, this.type, {this.definition});
|
||||
|
||||
/// 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() {
|
||||
if (definition == null ||
|
||||
table == null ||
|
||||
type?.type != BasicType.int ||
|
||||
table.withoutRowId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to check whether this column is a primary key, which could happen
|
||||
// because of a table or a column constraint
|
||||
for (var tableConstraint in table.tableConstraints.whereType<KeyClause>()) {
|
||||
if (!tableConstraint.isPrimaryKey) continue;
|
||||
|
||||
final columns = tableConstraint.indexedColumns;
|
||||
if (columns.length == 1 && columns.single.columnName == name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// option 2: This column has a primary key constraint
|
||||
for (var primaryConstraint in constraints.whereType<PrimaryKeyColumn>()) {
|
||||
if (primaryConstraint.mode == OrderingMode.descending) return false;
|
||||
|
||||
// additional restriction: Column type must be exactly "INTEGER"
|
||||
return definition.typeName == 'INTEGER';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// note that such alias is always called "rowid" in the result set -
|
||||
// "SELECT oid FROM table" yields a sinle column called "rowid"
|
||||
RowId() : super('rowid', const ResolvedType(type: BasicType.int));
|
||||
}
|
||||
|
||||
/// A column that is created by an expression. For instance, in the select
|
||||
|
@ -44,6 +91,18 @@ class ExpressionColumn extends Column {
|
|||
ExpressionColumn({@required this.name, this.expression});
|
||||
}
|
||||
|
||||
/// A column that is created by a reference expression. The difference to an
|
||||
/// [ExpressionColumn] is that the correct case of the column name depends on
|
||||
/// the resolved reference.
|
||||
class ReferenceExpressionColumn extends ExpressionColumn {
|
||||
Reference get reference => expression as Reference;
|
||||
|
||||
@override
|
||||
String get name => reference.resolvedColumn.name;
|
||||
|
||||
ReferenceExpressionColumn(Reference ref) : super(name: null, expression: ref);
|
||||
}
|
||||
|
||||
/// The result column of a [CompoundSelectStatement].
|
||||
class CompoundSelectColumn extends Column {
|
||||
/// The column in [CompoundSelectStatement.base] each of the
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
part of '../analysis.dart';
|
||||
|
||||
/// The aliases which can be used to refer to the rowid of a table. See
|
||||
/// https://www.sqlite.org/lang_createtable.html#rowid
|
||||
const aliasesForRowId = ['rowid', 'oid', '_rowid_'];
|
||||
|
||||
/// Something that will resolve to an [ResultSet] when referred to via
|
||||
/// the [ReferenceScope].
|
||||
abstract class ResolvesToResultSet with Referencable {
|
||||
|
@ -39,6 +43,8 @@ class Table with ResultSet, VisibleToChildren, HasMetaMixin {
|
|||
/// The ast node that created this table
|
||||
final CreateTableStatement definition;
|
||||
|
||||
TableColumn _rowIdColumn;
|
||||
|
||||
/// Constructs a table from the known [name] and [resolvedColumns].
|
||||
Table(
|
||||
{@required this.name,
|
||||
|
@ -48,6 +54,22 @@ class Table with ResultSet, VisibleToChildren, HasMetaMixin {
|
|||
this.definition}) {
|
||||
for (var column in resolvedColumns) {
|
||||
column.table = 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();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,14 +126,23 @@ class ColumnResolver extends RecursiveVisitor<void> {
|
|||
usedColumns.addAll(availableColumns);
|
||||
}
|
||||
} else if (resultColumn is ExpressionResultColumn) {
|
||||
final name = _nameOfResultColumn(resultColumn);
|
||||
final column =
|
||||
ExpressionColumn(name: name, expression: resultColumn.expression);
|
||||
final expression = resultColumn.expression;
|
||||
Column column;
|
||||
String name;
|
||||
|
||||
if (expression is Reference) {
|
||||
column = ReferenceExpressionColumn(expression);
|
||||
if (resultColumn.as != null) name = resultColumn.as;
|
||||
} else {
|
||||
name = _nameOfResultColumn(resultColumn);
|
||||
column =
|
||||
ExpressionColumn(name: name, expression: resultColumn.expression);
|
||||
}
|
||||
|
||||
usedColumns.add(column);
|
||||
|
||||
// make this column available if there is no other with the same name
|
||||
if (!availableColumns.any((c) => c.name == name)) {
|
||||
if (name != null && !availableColumns.any((c) => c.name == name)) {
|
||||
availableColumns.add(column);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,16 @@ class ReferenceResolver extends RecursiveVisitor<void> {
|
|||
e.resolved = column;
|
||||
}
|
||||
}
|
||||
} else if (aliasesForRowId.contains(e.columnName.toLowerCase())) {
|
||||
// special case for aliases to a rowid
|
||||
final column = _resolveRowIdAlias(e);
|
||||
|
||||
if (column == null) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.referencedUnknownColumn, relevantNode: e));
|
||||
} else {
|
||||
e.resolved = column;
|
||||
}
|
||||
} else {
|
||||
// find any column with the referenced name.
|
||||
// todo special case for USING (...) in joins?
|
||||
|
@ -67,6 +77,24 @@ class ReferenceResolver extends RecursiveVisitor<void> {
|
|||
visitChildren(e);
|
||||
}
|
||||
|
||||
Column _resolveRowIdAlias(Reference e) {
|
||||
// to resolve those aliases when they're not bound to a table, the
|
||||
// surrounding select statement may only read from one table
|
||||
final select = e.parents.firstWhere((node) => node is SelectStatement,
|
||||
orElse: () => null) as SelectStatement;
|
||||
|
||||
if (select == null) return null;
|
||||
if (select.from.length != 1 || select.from.single is! TableReference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final table = (select.from.single as TableReference).resolved as Table;
|
||||
if (table == null) return null;
|
||||
|
||||
// table.findColumn contains logic to resolve row id aliases
|
||||
return table.findColumn(e.columnName);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAggregateExpression(AggregateExpression e) {
|
||||
if (e.windowName != null && e.resolved == null) {
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
final id = TableColumn('id', const ResolvedType(type: BasicType.int));
|
||||
final id = TableColumn(
|
||||
'id',
|
||||
const ResolvedType(type: BasicType.int),
|
||||
definition: ColumnDefinition(
|
||||
columnName: 'id',
|
||||
typeName: 'INTEGER',
|
||||
constraints: [PrimaryKeyColumn(null)],
|
||||
),
|
||||
);
|
||||
final content =
|
||||
TableColumn('content', const ResolvedType(type: BasicType.text));
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ void main() {
|
|||
final engine = SqlEngine()..registerTable(demoTable);
|
||||
|
||||
final context =
|
||||
engine.analyze('SELECT id, d.content, *, 3 + 4 FROM demo AS d');
|
||||
engine.analyze('SELECT id, d.content, *, 3 + 4 FROM demo AS d '
|
||||
'WHERE _rowid_ = 3');
|
||||
|
||||
final select = context.root as SelectStatement;
|
||||
final resolvedColumns = select.resolvedColumns;
|
||||
|
@ -35,6 +36,9 @@ void main() {
|
|||
expect((firstColumn.expression as Reference).resolved, id);
|
||||
expect((secondColumn.expression as Reference).resolved, content);
|
||||
expect(from.resolved, demoTable);
|
||||
|
||||
final where = select.where as BinaryExpression;
|
||||
expect((where.left as Reference).resolved, id);
|
||||
});
|
||||
|
||||
test('resolves the column for order by clauses', () {
|
||||
|
@ -60,6 +64,28 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
group('reports correct column name for rowid aliases', () {
|
||||
final engine = SqlEngine()
|
||||
..registerTable(demoTable)
|
||||
..registerTable(anotherTable);
|
||||
|
||||
test('when virtual id', () {
|
||||
final context = engine.analyze('SELECT oid, _rowid_ FROM tbl');
|
||||
final select = context.root as SelectStatement;
|
||||
final resolvedColumns = select.resolvedColumns;
|
||||
|
||||
expect(resolvedColumns.map((c) => c.name), ['rowid', 'rowid']);
|
||||
});
|
||||
|
||||
test('when alias to actual column', () {
|
||||
final context = engine.analyze('SELECT oid, _rowid_ FROM demo');
|
||||
final select = context.root as SelectStatement;
|
||||
final resolvedColumns = select.resolvedColumns;
|
||||
|
||||
expect(resolvedColumns.map((c) => c.name), ['id', 'id']);
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves sub-queries', () {
|
||||
final engine = SqlEngine()..registerTable(demoTable);
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('isAliasForRowId', () {
|
||||
final engine = SqlEngine();
|
||||
final schemaParser = SchemaFromCreateTable();
|
||||
|
||||
final isAlias = {
|
||||
'CREATE TABLE x (id INTEGER PRIMARY KEY)': true,
|
||||
'CREATE TABLE x (id INTEGER PRIMARY KEY) WITHOUT ROWID': false,
|
||||
'CREATE TABLE x (id BIGINT PRIMARY KEY)': false,
|
||||
'CREATE TABLE x (id INTEGER PRIMARY KEY DESC)': false,
|
||||
'CREATE TABLE x (id INTEGER)': false,
|
||||
'CREATE TABLE x (id INTEGER, PRIMARY KEY (id))': true,
|
||||
};
|
||||
|
||||
isAlias.forEach((createTblString, isAlias) {
|
||||
final parsed =
|
||||
engine.parse(createTblString).rootNode as CreateTableStatement;
|
||||
final table = schemaParser.read(parsed);
|
||||
|
||||
expect(
|
||||
(table.findColumn('id') as TableColumn).isAliasForRowId(),
|
||||
isAlias,
|
||||
reason: '$createTblString: id is an alias? $isAlias',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('finds columns', () {
|
||||
final engine = SqlEngine();
|
||||
final schemaParser = SchemaFromCreateTable();
|
||||
|
||||
Column findWith(String createTbl, String columnName) {
|
||||
final stmt = (engine.parse(createTbl).rootNode) as CreateTableStatement;
|
||||
final table = schemaParser.read(stmt);
|
||||
return table.findColumn(columnName);
|
||||
}
|
||||
|
||||
test('when declared in table', () {
|
||||
expect(findWith('CREATE TABLE x (__rowid__ VARCHAR)', '__rowid__'),
|
||||
isA<TableColumn>());
|
||||
});
|
||||
|
||||
test('when alias to rowid', () {
|
||||
final column = findWith('CREATE TABLE x (id INTEGER PRIMARY KEY)', 'oid');
|
||||
expect(column.name, 'id');
|
||||
expect(column, isA<TableColumn>());
|
||||
});
|
||||
|
||||
test('when virtual rowid column', () {
|
||||
final column = findWith('CREATE TABLE x (id VARCHAR)', 'oid');
|
||||
expect(column, isA<RowId>());
|
||||
});
|
||||
|
||||
test('when not found', () {
|
||||
final column = findWith(
|
||||
'CREATE TABLE x (id INTEGER PRIMARY KEY) WITHOUT ROWID', 'oid');
|
||||
expect(column, isNull);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue