Merge branch 'simolus3:develop' into rtree

This commit is contained in:
Fabian Freund 2022-08-23 16:40:53 +03:00 committed by GitHub
commit 135433a692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 278 additions and 70 deletions

View File

@ -1,3 +1,9 @@
## 0.23.2-dev
- Support resolving the `fts5vocab` module when `fts5` is enabled - thanks to
[@FaFre](https://github.com/FaFre).
- Improve references resolving across subqueries.
## 0.23.1 ## 0.23.1
- Gracefully handle tokenizer errors related to `@` or `$` variables. - Gracefully handle tokenizer errors related to `@` or `$` variables.

View File

@ -24,6 +24,13 @@ mixin Referencable {
abstract class ReferenceScope { abstract class ReferenceScope {
RootScope get rootScope; RootScope get rootScope;
/// All available result sets that can also be seen in child scopes.
///
/// Usually, this is the same list as the result sets being declared in this
/// scope. However, some exceptions apply (see e.g. [SubqueryInFromScope]).
Iterable<ResultSetAvailableInStatement> get resultSetAvailableToChildScopes =>
const Iterable.empty();
/// The list of column to which a `*` would expand to. /// The list of column to which a `*` would expand to.
/// ///
/// This is not necessary the same list of columns that could be resolved /// This is not necessary the same list of columns that could be resolved
@ -101,6 +108,30 @@ class RootScope extends ReferenceScope {
final Map<String, Module> knownModules = CaseInsensitiveMap(); final Map<String, Module> knownModules = CaseInsensitiveMap();
} }
mixin _HasParentScope on ReferenceScope {
ReferenceScope get _parentScopeForLookups;
@override
RootScope get rootScope => _parentScopeForLookups.rootScope;
@override
Iterable<ResultSetAvailableInStatement> get resultSetAvailableToChildScopes =>
_parentScopeForLookups.resultSetAvailableToChildScopes;
@override
ResultSetAvailableInStatement? resolveResultSet(String name) =>
_parentScopeForLookups.resolveResultSet(name);
@override
ResultSet? resolveResultSetToAdd(String name) =>
_parentScopeForLookups.resolveResultSetToAdd(name);
@override
List<Column> resolveUnqualifiedReference(String columnName,
{bool allowReferenceToResultColumn = false}) =>
_parentScopeForLookups.resolveUnqualifiedReference(columnName);
}
/// A scope used by statements. /// A scope used by statements.
/// ///
/// Tables added from `FROM` clauses are added to [resultSets], CTEs are added /// Tables added from `FROM` clauses are added to [resultSets], CTEs are added
@ -118,9 +149,12 @@ class RootScope extends ReferenceScope {
/// [SubqueryInFromScope] is insertted as an intermediatet scope to prevent /// [SubqueryInFromScope] is insertted as an intermediatet scope to prevent
/// the inner scope from seeing the outer columns. /// the inner scope from seeing the outer columns.
class StatementScope extends ReferenceScope { class StatementScope extends ReferenceScope with _HasParentScope {
final ReferenceScope parent; final ReferenceScope parent;
@override
get _parentScopeForLookups => parent;
/// Additional tables (that haven't necessarily been added in a `FROM` clause /// Additional tables (that haven't necessarily been added in a `FROM` clause
/// that are only visible in this scope). /// that are only visible in this scope).
/// ///
@ -153,29 +187,17 @@ class StatementScope extends ReferenceScope {
StatementScope(this.parent); StatementScope(this.parent);
StatementScope? get parentStatementScope { @override
final parent = this.parent; Iterable<ResultSetAvailableInStatement> get resultSetAvailableToChildScopes {
if (parent is StatementScope) { return allAvailableResultSets;
return parent;
} else if (parent is MiscStatementSubScope) {
return parent.parent;
} else {
return null;
}
} }
/// All result sets available in this and parent scopes. /// All result sets available in this and parent scopes.
Iterable<ResultSetAvailableInStatement> get allAvailableResultSets { Iterable<ResultSetAvailableInStatement> get allAvailableResultSets {
final here = resultSets.values; final here = resultSets.values;
final parent = parentStatementScope; return parent.resultSetAvailableToChildScopes.followedBy(here);
return parent != null
? here.followedBy(parent.allAvailableResultSets)
: here;
} }
@override
RootScope get rootScope => parent.rootScope;
@override @override
void addAlias(AstNode origin, ResultSet resultSet, String alias) { void addAlias(AstNode origin, ResultSet resultSet, String alias) {
final createdAlias = TableAlias(resultSet, alias); final createdAlias = TableAlias(resultSet, alias);
@ -183,22 +205,20 @@ class StatementScope extends ReferenceScope {
resultSets[alias] = ResultSetAvailableInStatement(origin, createdAlias); resultSets[alias] = ResultSetAvailableInStatement(origin, createdAlias);
} }
@override
ResultSetAvailableInStatement? resolveResultSet(String name) {
return resultSets[name] ?? parentStatementScope?.resolveResultSet(name);
}
@override @override
void addResolvedResultSet( void addResolvedResultSet(
String? name, ResultSetAvailableInStatement resultSet) { String? name, ResultSetAvailableInStatement resultSet) {
resultSets[name] = resultSet; resultSets[name] = resultSet;
} }
@override
ResultSetAvailableInStatement? resolveResultSet(String name) {
return resultSets[name] ?? parent.resolveResultSet(name);
}
@override @override
ResultSet? resolveResultSetToAdd(String name) { ResultSet? resolveResultSetToAdd(String name) {
return additionalKnownTables[name] ?? return additionalKnownTables[name] ?? parent.resolveResultSetToAdd(name);
parentStatementScope?.resolveResultSetToAdd(name) ??
rootScope.knownTables[name];
} }
@override @override
@ -212,14 +232,7 @@ class StatementScope extends ReferenceScope {
} }
} }
StatementScope? currentScope = this; final available = resultSets.values;
// Search scopes for a matching column in an added result set. If a column
// reference is found in a closer scope, it takes precedence over outer
// scopes. However, it's an error if two columns with the same name are
// found in the same scope.
while (currentScope != null) {
final available = currentScope.resultSets.values;
final sourceColumns = <Column>{}; final sourceColumns = <Column>{};
final availableColumns = <AvailableColumn>[]; final availableColumns = <AvailableColumn>[];
@ -237,20 +250,12 @@ class StatementScope extends ReferenceScope {
} }
if (availableColumns.isEmpty) { if (availableColumns.isEmpty) {
currentScope = currentScope.parentStatementScope; return parent.resolveUnqualifiedReference(columnName);
if (currentScope == null) {
// Reached the outermost scope without finding a reference target.
return const [];
}
continue;
} else { } else {
return availableColumns; return availableColumns;
} }
} }
return const [];
}
factory StatementScope.forStatement(RootScope root, Statement statement) { factory StatementScope.forStatement(RootScope root, Statement statement) {
return StatementScope(statement.optionalScope ?? root); return StatementScope(statement.optionalScope ?? root);
} }
@ -269,13 +274,24 @@ class StatementScope extends ReferenceScope {
/// A special intermediate scope used for subqueries appearing in a `FROM` /// A special intermediate scope used for subqueries appearing in a `FROM`
/// clause so that the subquery can't see outer columns and tables being added. /// clause so that the subquery can't see outer columns and tables being added.
class SubqueryInFromScope extends ReferenceScope { class SubqueryInFromScope extends ReferenceScope with _HasParentScope {
final StatementScope enclosingStatement; final StatementScope enclosingStatement;
SubqueryInFromScope(this.enclosingStatement); SubqueryInFromScope(this.enclosingStatement);
@override @override
RootScope get rootScope => enclosingStatement.rootScope; RootScope get rootScope => enclosingStatement.rootScope;
// This scope can't see elements from the enclosing statement, but it can see
// elements from grandparents.
@override
ReferenceScope get _parentScopeForLookups => enclosingStatement.parent;
@override
ResultSet? resolveResultSetToAdd(String name) {
// CTEs from the enclosing statement are also available here
return enclosingStatement.resolveResultSetToAdd(name);
}
} }
/// A rarely used sub-scope for AST nodes that belong to a statement, but may /// A rarely used sub-scope for AST nodes that belong to a statement, but may
@ -283,9 +299,12 @@ class SubqueryInFromScope extends ReferenceScope {
/// ///
/// For instance, the body of an `ON CONFLICT DO UPDATE`-clause may refer to a /// For instance, the body of an `ON CONFLICT DO UPDATE`-clause may refer to a
/// table alias `excluded` to get access to a conflicting table. /// table alias `excluded` to get access to a conflicting table.
class MiscStatementSubScope extends ReferenceScope { class MiscStatementSubScope extends ReferenceScope with _HasParentScope {
final StatementScope parent; final StatementScope parent;
@override
get _parentScopeForLookups => parent;
final Map<String?, ResultSetAvailableInStatement> additionalResultSets = final Map<String?, ResultSetAvailableInStatement> additionalResultSets =
CaseInsensitiveMap(); CaseInsensitiveMap();
@ -304,12 +323,6 @@ class MiscStatementSubScope extends ReferenceScope {
String? name, ResultSetAvailableInStatement resultSet) { String? name, ResultSetAvailableInStatement resultSet) {
additionalResultSets[name] = resultSet; additionalResultSets[name] = resultSet;
} }
@override
List<Column> resolveUnqualifiedReference(String columnName,
{bool allowReferenceToResultColumn = false}) {
return parent.resolveUnqualifiedReference(columnName);
}
} }
/// A reference scope that only allows a single added result set. /// A reference scope that only allows a single added result set.

View File

@ -6,6 +6,7 @@ class Fts5Extension implements Extension {
@override @override
void register(SqlEngine engine) { void register(SqlEngine engine) {
engine.registerModule(_Fts5Module()); engine.registerModule(_Fts5Module());
engine.registerModule(_Fts5VocabModule());
engine.registerFunctionHandler(const _Fts5Functions()); engine.registerFunctionHandler(const _Fts5Functions());
} }
} }
@ -35,6 +36,59 @@ class _Fts5Module extends Module {
} }
} }
class _Fts5VocabModule extends Module {
_Fts5VocabModule() : super('fts5vocab');
@override
Table parseTable(CreateVirtualTableStatement stmt) {
if (stmt.argumentContent.length < 2 || stmt.argumentContent.length > 3) {
throw ArgumentError('''
fts5vocab table requires at least
two arguments (<referenced fts5 table name>, <type>)
and maximum three arguments when using an attached database
''');
}
final type = stmt.argumentContent.last.replaceAll(RegExp(r'''["' ]'''), '');
switch (type) {
case 'row':
return Table(
name: stmt.tableName,
resolvedColumns: [
TableColumn('term', const ResolvedType(type: BasicType.text)),
TableColumn('doc', const ResolvedType(type: BasicType.int)),
TableColumn('cnt', const ResolvedType(type: BasicType.int)),
],
definition: stmt,
isVirtual: true);
case 'col':
return Table(
name: stmt.tableName,
resolvedColumns: [
TableColumn('term', const ResolvedType(type: BasicType.text)),
TableColumn('col', const ResolvedType(type: BasicType.text)),
TableColumn('doc', const ResolvedType(type: BasicType.int)),
TableColumn('cnt', const ResolvedType(type: BasicType.int)),
],
definition: stmt,
isVirtual: true);
case 'instance':
return Table(
name: stmt.tableName,
resolvedColumns: [
TableColumn('term', const ResolvedType(type: BasicType.text)),
TableColumn('doc', const ResolvedType(type: BasicType.int)),
TableColumn('col', const ResolvedType(type: BasicType.text)),
TableColumn('offset', const ResolvedType(type: BasicType.int)),
],
definition: stmt,
isVirtual: true);
default:
throw ArgumentError('Unknown fts5vocab table type');
}
}
}
class _Fts5Table extends Table { class _Fts5Table extends Table {
_Fts5Table( _Fts5Table(
{required String name, {required String name,

View File

@ -1,6 +1,6 @@
name: sqlparser name: sqlparser
description: Parses sqlite statements and performs static analysis on them description: Parses sqlite statements and performs static analysis on them
version: 0.23.1 version: 0.23.2
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
#homepage: https://drift.simonbinder.eu/ #homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues issue_tracker: https://github.com/simolus3/drift/issues

View File

@ -163,7 +163,8 @@ void main() {
}); });
}); });
test('resolves sub-queries', () { group('sub-queries', () {
test('are resolved', () {
final engine = SqlEngine()..registerTable(demoTable); final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze( final context = engine.analyze(
@ -172,6 +173,70 @@ void main() {
expect(context.errors, isEmpty); expect(context.errors, isEmpty);
}); });
test('cannot refer to outer tables if used in FROM', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze(
'SELECT d.* FROM demo d, (SELECT * FROM demo WHERE id = d.id);');
context.expectError('d.id',
type: AnalysisErrorType.referencedUnknownTable);
});
test('can refer to CTEs if used in FROM', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('WITH cte AS (SELECT * FROM demo) '
'SELECT d.* FROM demo d, (SELECT * FROM cte);');
expect(context.errors, isEmpty);
});
test('can nest and see outer tables if that is a subquery', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('''
SELECT
(SELECT *
FROM
demo "inner",
(SELECT * FROM demo WHERE "inner".id = "outer".id)
)
FROM demo "outer";
''');
// note that "outer".id is visible and should not report an error
context.expectError('"inner".id',
type: AnalysisErrorType.referencedUnknownTable);
});
test('nested via FROM cannot see outer result sets', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('''
SELECT *
FROM
demo "outer",
(SELECT * FROM demo "inner",
(SELECT * FROM demo WHERE "inner".id = "outer".id))
''');
expect(
context.errors,
[
analysisErrorWith(
lexeme: '"inner".id',
type: AnalysisErrorType.referencedUnknownTable,
),
analysisErrorWith(
lexeme: '"outer".id',
type: AnalysisErrorType.referencedUnknownTable,
),
],
);
});
});
test('resolves sub-queries as data sources', () { test('resolves sub-queries as data sources', () {
final engine = SqlEngine() final engine = SqlEngine()
..registerTable(demoTable) ..registerTable(demoTable)

View File

@ -188,4 +188,32 @@ WHERE EXISTS(SELECT *
expect(result.errors, isEmpty); expect(result.errors, isEmpty);
}); });
test('regression test for #2010', () {
// https://github.com/simolus3/drift/issues/2010
final engine = SqlEngine()
..registerTableFromSql('CREATE TABLE place_hierarchy (feature_id INT);')
..registerTableFromSql('CREATE TABLE place_name (feature_id INT);')
..registerTableFromSql('CREATE TABLE place (feature_id INT);');
final result = engine.analyze('''
SELECT
(
SELECT
true
FROM (
SELECT
'test' AS name
FROM
place_hierarchy ph
where
ph.feature_id = p.feature_id
) second_select
) first_select
FROM place_name matches
INNER JOIN place p ON p.feature_id = matches.feature_id;
''');
expect(result.errors, isEmpty);
});
} }

View File

@ -20,6 +20,48 @@ void main() {
expect(columns.single.name, 'bar'); expect(columns.single.name, 'bar');
}); });
group('creating fts5vocab tables', () {
final engine = SqlEngine(_fts5Options);
test('can create fts5vocab instance table', () {
final result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
'fts5vocab(bar, instance)');
final table = const SchemaFromCreateTable()
.read(result.root as TableInducingStatement);
expect(table.name, 'foo');
final columns = table.resultColumns;
expect(columns, hasLength(4));
expect(columns.map((e) => e.name), contains('offset'));
});
test('can create fts5vocab row table', () {
final result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
'fts5vocab(bar, row)');
final table = const SchemaFromCreateTable()
.read(result.root as TableInducingStatement);
expect(table.name, 'foo');
final columns = table.resultColumns;
expect(columns, hasLength(3));
});
test('can create fts5vocab col table', () {
final result = engine.analyze('CREATE VIRTUAL TABLE foo USING '
'fts5vocab(bar, col)');
final table = const SchemaFromCreateTable()
.read(result.root as TableInducingStatement);
expect(table.name, 'foo');
final columns = table.resultColumns;
expect(columns, hasLength(4));
expect(columns.map((e) => e.name), contains('col'));
});
});
test('handles the UNINDEXED column option', () { test('handles the UNINDEXED column option', () {
final result = engine final result = engine
.analyze('CREATE VIRTUAL TABLE foo USING fts5(bar, baz UNINDEXED)'); .analyze('CREATE VIRTUAL TABLE foo USING fts5(bar, baz UNINDEXED)');