Handle aliases references to rowid (#197)

This commit is contained in:
Simon Binder 2019-10-19 15:18:00 +02:00
parent 263004fe7b
commit d8226aeb23
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
10 changed files with 228 additions and 6 deletions

View File

@ -1,3 +1,6 @@
## unreleased
- Handle special `rowid`, `oid`, `__rowid__` references
## 0.3.0
- parse compound select statements
- scan comment tokens

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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