Support rowid resolution outside of selects

This fixes #754
This commit is contained in:
Simon Binder 2020-08-10 10:46:33 +02:00
parent 5cbc331dda
commit a037de6621
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 109 additions and 13 deletions

View File

@ -1,7 +1,7 @@
## 3.3.1
- Support changing `onData` handlers for query streams.
This fixes a bug ocurring when using `queryStream.first` multiple times.
This fixes a bug occurring when using `queryStream.first` multiple times.
## 3.3.0

View File

@ -187,8 +187,10 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
relevantNode: e.table,
));
} else {
final notPresent = required.where((c) => !targeted
.any((t) => t.name.toUpperCase() == c.name.name.toUpperCase()));
final notPresent = required.where((c) {
return !targeted
.any((t) => t?.name?.toUpperCase() == c.name.name.toUpperCase());
});
if (notPresent.isNotEmpty) {
final msg = notPresent.map((c) => c.name.name).join(', ');

View File

@ -0,0 +1,36 @@
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:test/test.dart';
import '../utils.dart';
void main() {
// Regression test for https://github.com/simolus3/moor/issues/754
test('supports fts5 tables with external content', () async {
final state = TestState.withContent({
'foo|lib/a.moor': '''
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b TEXT, c TEXT);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');
-- Triggers to keep the FTS index up to date.
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN
INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN
INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
''',
}, options: const MoorOptions(modules: [SqlModule.fts5]));
final result = await state.analyze('package:foo/a.moor');
// The generator used to crash while analyzing, so consider the test passed
// if it can analyze the file and sees that there aren't any errors.
expect(result.errors.errors, isEmpty);
});
}

View File

@ -8,11 +8,21 @@ class SchemaFromCreateTable {
const SchemaFromCreateTable({this.moorExtensions = false});
/// Reads a [Table] schema from the [stmt] inducing a table (either a
/// [CreateTableStatement] or a [CreateVirtualTableStatement]).
///
/// This method might throw an exception if the table could not be read.
Table read(TableInducingStatement stmt) {
if (stmt is CreateTableStatement) {
return _readCreateTable(stmt);
} else if (stmt is CreateVirtualTableStatement) {
final module = stmt.scope.resolve<Module>(stmt.moduleName);
if (module == null) {
throw CantReadSchemaException('Unknown module "${stmt.moduleName}", '
'did you register it?');
}
return module.parseTable(stmt);
}
@ -122,3 +132,15 @@ class SchemaFromCreateTable {
@visibleForTesting
BasicType columnAffinity(String typeName) => resolveColumnType(typeName).type;
}
/// Thrown when a table schema could not be read.
class CantReadSchemaException implements Exception {
final String message;
CantReadSchemaException(this.message);
@override
String toString() {
return 'Could not read table schema: $message';
}
}

View File

@ -84,16 +84,17 @@ class ReferenceResolver extends RecursiveVisitor<void, void> {
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;
// surrounding statement may only read from one table
final stmt = e.enclosingOfType<HasPrimarySource>();
if (select == null) return null;
if (select.from is! TableReference) {
if (stmt == null) return null;
final from = stmt.table;
if (from is! TableReference) {
return null;
}
final table = (select.from as TableReference).resolved as Table;
final table = (from as TableReference).resolved as Table;
if (table == null) return null;
// table.findColumn contains logic to resolve row id aliases

View File

@ -113,6 +113,19 @@ abstract class AstNode with HasMetaMixin implements SyntacticEntity {
yield* allDescendants;
}
/// Finds the first element in [selfAndParents] of the type [T].
///
/// Returns `null` if there's no node of type [T] surrounding this ast node.
T /*?*/ enclosingOfType<T extends AstNode>() {
for (final element in selfAndParents) {
if (element is T) {
return element;
}
}
return null;
}
/// The [ReferenceScope], which contains available tables, column references
/// and functions for this node.
ReferenceScope get scope {

View File

@ -1,10 +1,14 @@
part of '../ast.dart';
class DeleteStatement extends CrudStatement implements StatementWithWhere {
class DeleteStatement extends CrudStatement
implements StatementWithWhere, HasPrimarySource {
TableReference from;
@override
Expression where;
@override
TableReference get table => from;
DeleteStatement({WithClause withClause, @required this.from, this.where})
: super._(withClause);

View File

@ -10,8 +10,9 @@ enum InsertMode {
insertOrIgnore
}
class InsertStatement extends CrudStatement {
class InsertStatement extends CrudStatement implements HasPrimarySource {
final InsertMode mode;
@override
TableReference table;
final List<Reference> targetColumns;
InsertSource source;

View File

@ -14,7 +14,7 @@ abstract class BaseSelectStatement extends CrudStatement with ResultSet {
abstract class SelectStatementNoCompound implements BaseSelectStatement {}
class SelectStatement extends BaseSelectStatement
implements StatementWithWhere, SelectStatementNoCompound {
implements StatementWithWhere, SelectStatementNoCompound, HasPrimarySource {
final bool distinct;
final List<ResultColumn> columns;
Queryable /*?*/ from;
@ -27,6 +27,9 @@ class SelectStatement extends BaseSelectStatement
OrderByBase orderBy;
LimitBase limit;
@override
Queryable get table => from;
SelectStatement(
{WithClause withClause,
this.distinct = false,

View File

@ -12,6 +12,18 @@ abstract class CrudStatement extends Statement {
CrudStatement._(this.withClause);
}
/// Interfaces for statements that have a primary source table on which they
/// operate.
/// This includes delete, update and insert statements. It also includes the
/// common [SelectStatement], but not compound select statements or `VALUES`
/// statements.
abstract class HasPrimarySource extends Statement {
/// The primary table this statement operates on. This is the part after the
/// `FROM` for select and delete statements, the part after the `INTO` for
/// inserts and the name after the `UPDATE` for updates.
Queryable get table;
}
/// Interface for statements that have a primary where clause (select, update,
/// delete).
abstract class StatementWithWhere extends Statement implements HasWhereClause {}

View File

@ -16,8 +16,10 @@ const Map<TokenType, FailureMode> _tokensToMode = {
TokenType.ignore: FailureMode.ignore,
};
class UpdateStatement extends CrudStatement implements StatementWithWhere {
class UpdateStatement extends CrudStatement
implements StatementWithWhere, HasPrimarySource {
final FailureMode or;
@override
TableReference table;
final List<SetComponent> set;
@override