Merge branch 'develop' of https://github.com/dickermoshe/drift into develop

This commit is contained in:
Moshe Dicker 2024-04-21 17:38:42 -04:00
commit 7c29925217
31 changed files with 238 additions and 69 deletions

View File

@ -9,6 +9,8 @@ import '../_shared/todo_tables.drift.dart';
class EntryWithCategory { class EntryWithCategory {
EntryWithCategory(this.entry, this.category); EntryWithCategory(this.entry, this.category);
// The classes are generated by drift for each of the tables involved in the
// join.
final TodoItem entry; final TodoItem entry;
final Category? category; final Category? category;
} }
@ -69,8 +71,6 @@ extension SelectExamples on CanUseCommonTables {
leftOuterJoin(categories, categories.id.equalsExp(todoItems.category)), leftOuterJoin(categories, categories.id.equalsExp(todoItems.category)),
]); ]);
// see next section on how to parse the result
// #enddocregion joinIntro
// #docregion results // #docregion results
return query.watch().map((rows) { return query.watch().map((rows) {
return rows.map((row) { return rows.map((row) {
@ -81,7 +81,6 @@ extension SelectExamples on CanUseCommonTables {
}).toList(); }).toList();
}); });
// #enddocregion results // #enddocregion results
// #docregion joinIntro
} }
// #enddocregion joinIntro // #enddocregion joinIntro

View File

@ -1,21 +1,19 @@
// #docregion after_generation
// #docregion before_generation // #docregion before_generation
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
// #enddocregion before_generation // #enddocregion before_generation
// #enddocregion after_generation
// #docregion open // #docregion after_generation
// These imports are necessary to open the sqlite3 database // These additional imports are necessary to open the sqlite3 database
import 'dart:io'; import 'dart:io';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
// ... the TodoItems table definition stays the same
// #enddocregion open
// #docregion before_generation // #docregion before_generation
part 'database.g.dart'; part 'database.g.dart';
@ -35,25 +33,22 @@ class TodoCategory extends Table {
} }
// #enddocregion table // #enddocregion table
// #docregion open
@DriftDatabase(tables: [TodoItems, TodoCategory]) @DriftDatabase(tables: [TodoItems, TodoCategory])
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
// #enddocregion open // #enddocregion before_generation
// #enddocregion after_generation
// After generating code, this class needs to define a `schemaVersion` getter // After generating code, this class needs to define a `schemaVersion` getter
// and a constructor telling drift where the database should be stored. // and a constructor telling drift where the database should be stored.
// These are described in the getting started guide: https://drift.simonbinder.eu/getting-started/#open // These are described in the getting started guide: https://drift.simonbinder.eu/getting-started/#open
// #enddocregion before_generation // #docregion after_generation
// #docregion open
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 1; int get schemaVersion => 1;
// #docregion before_generation // #docregion before_generation
} }
// #enddocregion before_generation, open // #enddocregion before_generation
// #docregion open
LazyDatabase _openConnection() { LazyDatabase _openConnection() {
// the LazyDatabase util lets us find the right location for the file async. // the LazyDatabase util lets us find the right location for the file async.
@ -78,7 +73,7 @@ LazyDatabase _openConnection() {
return NativeDatabase.createInBackground(file); return NativeDatabase.createInBackground(file);
}); });
} }
// #enddocregion open // #enddocregion after_generation
class WidgetsFlutterBinding { class WidgetsFlutterBinding {
static void ensureInitialized() {} static void ensureInitialized() {}

View File

@ -114,14 +114,14 @@ Of course, you can also join multiple tables:
{% include "blocks/snippet" snippets = snippets name = 'otherTodosInSameCategory' %} {% include "blocks/snippet" snippets = snippets name = 'otherTodosInSameCategory' %}
## Parsing results ### Parsing results
Calling `get()` or `watch` on a select statement with join returns a `Future` or `Stream` of Calling `get()` or `watch` on a select statement with join returns a `Future` or `Stream` of
`List<TypedResult>`, respectively. Each `TypedResult` represents a row from which data can be `List<TypedResult>`, respectively. Each `TypedResult` represents a row from which data can be
read. It contains a `rawData` getter to obtain the raw columns. But more importantly, the read. It contains a `rawData` getter to obtain the raw columns. But more importantly, the
`readTable` method can be used to read a data class from a table. `readTable` method can be used to read a data class from a table.
In the example query above, we can read the todo entry and the category from each row like this: In the example query above, we've read the todo entry and the category from each row like this:
{% include "blocks/snippet" snippets = snippets name = 'results' %} {% include "blocks/snippet" snippets = snippets name = 'results' %}

View File

@ -116,8 +116,10 @@ to determine the column type based on the declared type name.
Additionally, columns that have the type name `BOOLEAN` or `DATETIME` will have Additionally, columns that have the type name `BOOLEAN` or `DATETIME` will have
`bool` or `DateTime` as their Dart counterpart. `bool` or `DateTime` as their Dart counterpart.
Booleans are stored as `INTEGER` (either `0` or `1`). Datetimes are stored as Booleans are stored as `INTEGER` (either `0` or `1`). Datetimes are stored as
unix timestamps (`INTEGER`) or ISO-8601 (`TEXT`) depending on a configurable unix timestamps (`INTEGER`) or ISO-8601 (`TEXT`) [depending on a configurable build option]({{ '../Dart API/tables.md#datetime-options' | pageUrl }}).
build option. For integers that should be represented as a `BigInt` in Dart (i.e. to have better compatibility with large numbers when compiling to JS),
define the column with the `INT64` type.
Dart enums can automatically be stored by their index by using an `ENUM()` type Dart enums can automatically be stored by their index by using an `ENUM()` type
referencing the Dart enum class: referencing the Dart enum class:

View File

@ -81,7 +81,7 @@ to store todo items for a todo list app.
Everything there is to know about defining tables in Dart is described on the [Dart tables]({{'Dart API/tables.md' | pageUrl}}) page. Everything there is to know about defining tables in Dart is described on the [Dart tables]({{'Dart API/tables.md' | pageUrl}}) page.
If you prefer using SQL to define your tables, drift supports that too! You can read all about the [SQL API]({{ 'SQL API/index.md' | pageUrl }}) here. If you prefer using SQL to define your tables, drift supports that too! You can read all about the [SQL API]({{ 'SQL API/index.md' | pageUrl }}) here.
For now, the contents of `database.dart` are: For now, populate the contents of `database.dart` with:
{% include "blocks/snippet" snippets = snippets name = 'before_generation' %} {% include "blocks/snippet" snippets = snippets name = 'before_generation' %}
@ -97,10 +97,11 @@ After running either command, the `database.g.dart` file containing the generate
class will have been generated. class will have been generated.
You will now see errors related to missing overrides and a missing constructor. The constructor You will now see errors related to missing overrides and a missing constructor. The constructor
is responsible for telling drift how to open the database. The `schemaVersion` getter is relevant is responsible for telling drift how to open the database. The `schemaVersion` getter is relevant
for migrations after changing the database, we can leave it at `1` for now. The database class for migrations after changing the database, we can leave it at `1` for now. Update `database.dart`
now looks like this: so it now looks like this:
<a name="open">
{% include "blocks/snippet" snippets = snippets name = 'open' %} <a name="open"></a>
{% include "blocks/snippet" snippets = snippets name = 'after_generation' %}
The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store
private data on unix-like systems, which is forbidden on Android. We also use this opportunity private data on unix-like systems, which is forbidden on Android. We also use this opportunity

View File

@ -1,4 +1,4 @@
## 2.17.0-dev ## 2.17.0
- Adds `companion` entry to `DataClassName` to override the name of the - Adds `companion` entry to `DataClassName` to override the name of the
generated companion class. generated companion class.

View File

@ -140,7 +140,7 @@ abstract class _TransactionExecutor extends _BaseExecutor
if (_closed) { if (_closed) {
throw StateError( throw StateError(
"A tranaction was used after being closed. Please check that you're " "A transaction was used after being closed. Please check that you're "
'awaiting all database operations inside a `transaction` block.'); 'awaiting all database operations inside a `transaction` block.');
} }
} }

View File

@ -1,6 +1,6 @@
name: drift name: drift
description: Drift is a reactive library to store relational data in Dart and Flutter applications. description: Drift is a reactive library to store relational data in Dart and Flutter applications.
version: 2.16.0 version: 2.17.0
repository: https://github.com/simolus3/drift repository: https://github.com/simolus3/drift
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
@ -30,7 +30,7 @@ dev_dependencies:
drift_dev: any drift_dev: any
drift_testcases: drift_testcases:
path: ../extras/integration_tests/drift_testcases path: ../extras/integration_tests/drift_testcases
http: ^0.13.4 http: ^1.2.1
lints: ^3.0.0 lints: ^3.0.0
uuid: ^4.0.0 uuid: ^4.0.0
build_runner: ^2.0.0 build_runner: ^2.0.0
@ -39,7 +39,5 @@ dev_dependencies:
rxdart: ^0.27.0 rxdart: ^0.27.0
shelf: ^1.3.0 shelf: ^1.3.0
test_descriptor: ^2.0.1 test_descriptor: ^2.0.1
vm_service: ^13.0.0 vm_service: ^14.0.0
dependency_overrides:
drift_dev:
path: ../drift_dev

View File

@ -1,4 +1,4 @@
## 2.17.0-dev ## 2.17.0
- Fix drift using the wrong import alias in generated part files. - Fix drift using the wrong import alias in generated part files.
- Add the `use_sql_column_name_as_json_key` builder option. - Add the `use_sql_column_name_as_json_key` builder option.

View File

@ -1,6 +1,6 @@
name: drift_dev name: drift_dev
description: Dev-dependency for users of drift. Contains the generator and development tools. description: Dev-dependency for users of drift. Contains the generator and development tools.
version: 2.16.0 version: 2.17.0
repository: https://github.com/simolus3/drift repository: https://github.com/simolus3/drift
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
@ -30,9 +30,9 @@ dependencies:
io: ^1.0.3 io: ^1.0.3
# Drift-specific analysis and apis # Drift-specific analysis and apis
drift: '>=2.16.0 <2.17.0' drift: '>=2.17.0 <2.18.0'
sqlite3: '>=0.1.6 <3.0.0' sqlite3: '>=0.1.6 <3.0.0'
sqlparser: '^0.34.0' sqlparser: '^0.35.0'
# Dart analysis # Dart analysis
analyzer: '>=5.12.0 <7.0.0' analyzer: '>=5.12.0 <7.0.0'

View File

@ -290,4 +290,21 @@ class MyType implements CustomSqlType<String> {}
expect(column.sqlType.custom?.dartType.toString(), 'String'); expect(column.sqlType.custom?.dartType.toString(), 'String');
expect(column.sqlType.custom?.expression.toString(), 'MyType()'); expect(column.sqlType.custom?.expression.toString(), 'MyType()');
}); });
test('recognizes bigint columns', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE foo (
bar INT64 NOT NULL
);
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final table = file.analyzedElements.single as DriftTable;
final column = table.columns.single;
expect(column.sqlType.builtin, DriftSqlType.bigInt);
});
} }

View File

@ -1,7 +1,10 @@
## 3.35.0-dev ## 0.35.0
- Fix parsing binary literals. - Fix parsing binary literals.
- Expand support for `IN` expressions, they now support tuples on the left-hand
side and the shorthand syntax for table references and table-valued functions.
- Drift extensions: Allow custom class names for `CREATE VIEW` statements. - Drift extensions: Allow custom class names for `CREATE VIEW` statements.
- Drift extensions: Support the `INT64` hint for `CREATE TABLE` statements.
## 0.34.1 ## 0.34.1

View File

@ -157,7 +157,11 @@ class SchemaFromCreateTable {
final upper = typeName.toUpperCase(); final upper = typeName.toUpperCase();
if (upper.contains('INT')) { if (upper.contains('INT')) {
return const ResolvedType(type: BasicType.int); if (driftExtensions && upper.contains('INT64')) {
return const ResolvedType(type: BasicType.int, hints: [IsBigInt()]);
} else {
return const ResolvedType(type: BasicType.int);
}
} }
if (upper.contains('CHAR') || if (upper.contains('CHAR') ||
upper.contains('CLOB') || upper.contains('CLOB') ||

View File

@ -27,7 +27,7 @@ abstract class ReferenceScope {
/// All available result sets that can also be seen in child scopes. /// 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 /// Usually, this is the same list as the result sets being declared in this
/// scope. However, some exceptions apply (see e.g. [SubqueryInFromScope]). /// scope. However, some exceptions apply (see e.g. [SourceScope]).
Iterable<ResultSetAvailableInStatement> get resultSetAvailableToChildScopes => Iterable<ResultSetAvailableInStatement> get resultSetAvailableToChildScopes =>
const Iterable.empty(); const Iterable.empty();
@ -167,8 +167,8 @@ mixin _HasParentScope on ReferenceScope {
/// them in a [StatementScope] as well. /// them in a [StatementScope] as well.
/// - subqueries appearing in a `FROM` clause _can't_ see outer columns and /// - subqueries appearing in a `FROM` clause _can't_ see outer columns and
/// tables. These statements are also wrapped in a [StatementScope], but a /// tables. These statements are also wrapped in a [StatementScope], but a
/// [SubqueryInFromScope] is insertted as an intermediatet scope to prevent /// [SourceScope] is inserted as an intermediate scope to prevent the inner
/// the inner scope from seeing the outer columns. /// scope from seeing the outer columns.
class StatementScope extends ReferenceScope with _HasParentScope { class StatementScope extends ReferenceScope with _HasParentScope {
final ReferenceScope parent; final ReferenceScope parent;

View File

@ -170,6 +170,16 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
visitExcept(e, e.foreignTable, arg); visitExcept(e, e.foreignTable, arg);
} }
@override
void visitInExpression(InExpression e, ColumnResolverContext arg) {
if (e.inside case Queryable query) {
_handle(query, [], arg);
visitExcept(e, e.inside, arg);
} else {
super.visitInExpression(e, arg);
}
}
@override @override
void visitUpdateStatement(UpdateStatement e, ColumnResolverContext arg) { void visitUpdateStatement(UpdateStatement e, ColumnResolverContext arg) {
// Resolve CTEs first // Resolve CTEs first

View File

@ -199,6 +199,56 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
visitChildren(e, arg); visitChildren(e, arg);
} }
@override
void visitInExpression(InExpression e, void arg) {
final expectedColumns = switch (e.left) {
Tuple(:var expressions) => expressions.length,
_ => 1,
};
switch (e.inside) {
case Tuple tuple:
for (final element in tuple.expressions) {
final actualColumns = switch (element) {
Tuple(:var expressions) => expressions.length,
_ => 1,
};
if (expectedColumns != actualColumns) {
context.reportError(AnalysisError(
type: AnalysisErrorType.other,
message: 'Expected $expectedColumns columns in this entry, got '
'$actualColumns',
relevantNode: element,
));
}
}
case SubQuery subquery:
final columns = subquery.select.resolvedColumns;
if (columns != null && columns.length != expectedColumns) {
context.reportError(AnalysisError(
type: AnalysisErrorType.other,
message: 'The subquery must return $expectedColumns columns, '
'it returns ${columns.length}',
relevantNode: subquery,
));
}
case TableOrSubquery table:
final columns =
table.availableResultSet?.resultSet.resultSet?.resolvedColumns;
if (columns != null && columns.length != expectedColumns) {
context.reportError(AnalysisError(
type: AnalysisErrorType.other,
message: 'To be used in this `IN` expression, this table must '
'have $expectedColumns columns (it has ${columns.length}).',
relevantNode: table,
));
}
}
visitChildren(e, arg);
}
@override @override
void visitIsExpression(IsExpression e, void arg) { void visitIsExpression(IsExpression e, void arg) {
if (e.distinctFromSyntax && options.version < SqliteVersion.v3_39) { if (e.distinctFromSyntax && options.version < SqliteVersion.v3_39) {
@ -526,9 +576,9 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
isAllowed = !comparisons.any((e) => !isRowValue(e)); isAllowed = !comparisons.any((e) => !isRowValue(e));
} }
} else if (parent is InExpression) { } else if (parent is InExpression) {
// In expressions are tricky. The rhs can always be a row value, but the // For in expressions we have a more accurate analysis on whether tuples
// lhs can only be a row value if the rhs is a subquery // are allowed that looks at both the LHS and the RHS.
isAllowed = e == parent.inside || parent.inside is SubQuery; isAllowed = true;
} else if (parent is SetComponent) { } else if (parent is SetComponent) {
isAllowed = true; isAllowed = true;
} }

View File

@ -136,6 +136,14 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
visitChildren(e, arg); visitChildren(e, arg);
} }
@override
void visitInExpression(InExpression e, void arg) {
// The RHS can use everything from the parent scope, but it can't add new
// table references that would be visible to others.
e.scope = StatementScope(e.scope);
visitChildren(e, arg);
}
@override @override
void visitNumberedVariable(NumberedVariable e, void arg) { void visitNumberedVariable(NumberedVariable e, void arg) {
_foundVariables.add(e); _foundVariables.add(e);

View File

@ -427,9 +427,11 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override @override
void visitInExpression(InExpression e, TypeExpectation arg) { void visitInExpression(InExpression e, TypeExpectation arg) {
session._checkAndResolve(e, const ResolvedType.bool(), arg); session._checkAndResolve(e, const ResolvedType.bool(), arg);
session._addRelation(NullableIfSomeOtherIs(e, e.childNodes));
session._addRelation(CopyTypeFrom(e.inside, e.left, array: true)); if (e.inside case Expression inExpr) {
session._addRelation(NullableIfSomeOtherIs(e, [e.left, inExpr]));
session._addRelation(CopyTypeFrom(inExpr, e.left, array: true));
}
visitChildren(e, const NoTypeExpectation()); visitChildren(e, const NoTypeExpectation());
} }

View File

@ -41,7 +41,7 @@ abstract class TableOrSubquery extends Queryable {
/// set. /// set.
class TableReference extends TableOrSubquery class TableReference extends TableOrSubquery
with ReferenceOwner with ReferenceOwner
implements Renamable, ResolvesToResultSet { implements Renamable, ResolvesToResultSet, InExpressionTarget {
final String? schemaName; final String? schemaName;
final String tableName; final String tableName;
Token? tableNameToken; Token? tableNameToken;
@ -213,7 +213,12 @@ class UsingConstraint extends JoinConstraint {
} }
class TableValuedFunction extends Queryable class TableValuedFunction extends Queryable
implements TableOrSubquery, SqlInvocation, Renamable, ResolvesToResultSet { implements
TableOrSubquery,
SqlInvocation,
Renamable,
ResolvesToResultSet,
InExpressionTarget {
@override @override
final String name; final String name;

View File

@ -3,7 +3,7 @@ part of '../ast.dart';
/// A tuple of values, denotes in brackets. `(<expr>, ..., <expr>)`. /// A tuple of values, denotes in brackets. `(<expr>, ..., <expr>)`.
/// ///
/// In sqlite, this is also called a "row value". /// In sqlite, this is also called a "row value".
class Tuple extends Expression { class Tuple extends Expression implements InExpressionTarget {
/// The expressions appearing in this tuple. /// The expressions appearing in this tuple.
List<Expression> expressions; List<Expression> expressions;

View File

@ -180,10 +180,9 @@ class InExpression extends Expression {
/// against. From the sqlite grammar, we support [Tuple] and a [SubQuery]. /// against. From the sqlite grammar, we support [Tuple] and a [SubQuery].
/// We also support a [Variable] as syntax sugar - it will be expanded into a /// We also support a [Variable] as syntax sugar - it will be expanded into a
/// tuple of variables at runtime. /// tuple of variables at runtime.
Expression inside; InExpressionTarget inside;
InExpression({this.not = false, required this.left, required this.inside}) InExpression({this.not = false, required this.left, required this.inside});
: assert(inside is Tuple || inside is Variable || inside is SubQuery);
@override @override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) { R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
@ -191,7 +190,7 @@ class InExpression extends Expression {
} }
@override @override
List<Expression> get childNodes => [left, inside]; List<AstNode> get childNodes => [left, inside];
@override @override
void transformChildren<A>(Transformer<A> transformer, A arg) { void transformChildren<A>(Transformer<A> transformer, A arg) {
@ -200,6 +199,19 @@ class InExpression extends Expression {
} }
} }
/// Possible values for the right-hand side of an [InExpression].
///
/// Valid subclasses are:
/// - [Tuple], to check whether the LHS is equal to any of the elements in the
/// tuple.
/// - [SubQuery], to check whether the LHS is equal to any of the rows returned
/// by the subquery.
/// - [TableReference] and [TableValuedFunction], a short-hand for [SubQuery]s
/// if the table or function only return one column.
/// - [Variable] (only if drift extensions are enabled), drift's generator
/// turns this into a tuple of variables at runtime.
abstract class InExpressionTarget implements AstNode {}
class Parentheses extends Expression { class Parentheses extends Expression {
Token? openingLeft; Token? openingLeft;
Token? closingRight; Token? closingRight;

View File

@ -2,7 +2,7 @@ part of '../ast.dart';
/// A subquery, which is an expression. It is expected that the inner query /// A subquery, which is an expression. It is expected that the inner query
/// only returns one column and one row. /// only returns one column and one row.
class SubQuery extends Expression { class SubQuery extends Expression implements InExpressionTarget {
BaseSelectStatement select; BaseSelectStatement select;
SubQuery({required this.select}); SubQuery({required this.select});

View File

@ -1,6 +1,6 @@
part of '../ast.dart'; part of '../ast.dart';
abstract class Variable extends Expression { abstract class Variable extends Expression implements InExpressionTarget {
int? resolvedIndex; int? resolvedIndex;
} }

View File

@ -511,7 +511,20 @@ class Parser {
final not = _matchOne(TokenType.not); final not = _matchOne(TokenType.not);
_matchOne(TokenType.$in); _matchOne(TokenType.$in);
final inside = _variableOrNull() ?? _consumeTuple(orSubQuery: true); InExpressionTarget inside;
if (_variableOrNull() case var variable?) {
inside = variable;
} else if (_check(TokenType.leftParen)) {
inside = _consumeTuple(orSubQuery: true) as InExpressionTarget;
} else {
final target = _tableOrSubquery();
// TableOrSubquery is either a table reference, a table-valued function,
// or a Subquery. We don't support subqueries, but they can't be parsed
// here because we would have entered the tuple case above.
assert(target is! SubQuery);
inside = target as InExpressionTarget;
}
return InExpression(left: left, inside: inside, not: not) return InExpression(left: left, inside: inside, not: not)
..setSpan(left.first!, _previous); ..setSpan(left.first!, _previous);
} }

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.34.1 version: 0.35.0
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
repository: https://github.com/simolus3/drift repository: https://github.com/simolus3/drift
#homepage: https://drift.simonbinder.eu/ #homepage: https://drift.simonbinder.eu/

View File

@ -363,4 +363,10 @@ SELECT * FROM cars
expect(select.resolvedColumns?.map((e) => e.name), ['literal', 'bar']); expect(select.resolvedColumns?.map((e) => e.name), ['literal', 'bar']);
}); });
test('error for nonexisting table in IN expression', () {
final query = engine.analyze('SELECT 1 IN no_such_table');
query.expectError('no_such_table',
type: AnalysisErrorType.referencedUnknownTable);
});
} }

View File

@ -6,7 +6,8 @@ import 'utils.dart';
void main() { void main() {
late SqlEngine engine; late SqlEngine engine;
setUp(() { setUp(() {
engine = SqlEngine(); // enable json1 extension
engine = SqlEngine(EngineOptions(version: SqliteVersion.v3_38));
}); });
test('when using row value in select', () { test('when using row value in select', () {
@ -15,12 +16,6 @@ void main() {
.expectError('(1, 2, 3)', type: AnalysisErrorType.rowValueMisuse); .expectError('(1, 2, 3)', type: AnalysisErrorType.rowValueMisuse);
}); });
test('as left hand operator of in', () {
engine
.analyze('SELECT (1, 2, 3) IN (4, 5, 6)')
.expectError('(1, 2, 3)', type: AnalysisErrorType.rowValueMisuse);
});
test('in BETWEEN expression', () { test('in BETWEEN expression', () {
engine engine
.analyze('SELECT 1 BETWEEN (1, 2, 3) AND 3') .analyze('SELECT 1 BETWEEN (1, 2, 3) AND 3')
@ -68,4 +63,33 @@ void main() {
.expectNoError(); .expectNoError();
}); });
}); });
group('in expressions', () {
test('when tuple is expected', () {
engine.analyze('SELECT (1, 2) IN ((4, 5), 6)').expectError('6',
type: AnalysisErrorType.other,
message: contains('Expected 2 columns in this entry, got 1'));
});
test('when tuple is not expected', () {
engine.analyze('SELECT 1 IN ((4, 5), 6)').expectError('(4, 5)',
type: AnalysisErrorType.other,
message: contains('Expected 1 columns in this entry, got 2'));
});
test('for table reference', () {
engine
.analyze('WITH names AS (VALUES(1, 2, 3)) SELECT (1, 2) IN names')
.expectError('names',
type: AnalysisErrorType.other,
message: contains('must have 2 columns (it has 3)'));
});
test('for table-valued function', () {
engine.analyze("SELECT (1, 2) IN json_each('{}')").expectError(
"json_each('{}')",
type: AnalysisErrorType.other,
message: contains('this table must have 2 columns (it has 8)'));
});
});
} }

View File

@ -2,10 +2,10 @@ import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
extension ExpectErrors on AnalysisContext { extension ExpectErrors on AnalysisContext {
void expectError(String lexeme, {AnalysisErrorType? type}) { void expectError(String lexeme, {AnalysisErrorType? type, message}) {
expect( expect(
errors, errors,
[analysisErrorWith(lexeme: lexeme, type: type)], [analysisErrorWith(lexeme: lexeme, type: type, message: message)],
); );
} }

View File

@ -103,7 +103,7 @@ void main() {
SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions())); SqlEngine(EngineOptions(driftOptions: const DriftSqlOptions()));
final stmt = engine.parse(''' final stmt = engine.parse('''
CREATE TABLE foo ( CREATE TABLE foo (
a BOOL, b DATETIME, c DATE, d BOOLEAN NOT NULL a BOOL, b DATETIME, c DATE, d BOOLEAN NOT NULL, e INT64
) )
''').rootNode; ''').rootNode;
@ -114,6 +114,7 @@ void main() {
ResolvedType(type: BasicType.int, hints: [IsDateTime()], nullable: true), ResolvedType(type: BasicType.int, hints: [IsDateTime()], nullable: true),
ResolvedType(type: BasicType.int, hints: [IsDateTime()], nullable: true), ResolvedType(type: BasicType.int, hints: [IsDateTime()], nullable: true),
ResolvedType(type: BasicType.int, hints: [IsBoolean()], nullable: false), ResolvedType(type: BasicType.int, hints: [IsBoolean()], nullable: false),
ResolvedType(type: BasicType.int, hints: [IsBigInt()], nullable: true),
]); ]);
}); });

View File

@ -160,6 +160,22 @@ final Map<String, Expression> _testCases = {
], ],
), ),
), ),
'x IN json_each(bar)': InExpression(
left: Reference(columnName: 'x'),
inside: TableValuedFunction(
'json_each',
ExprFunctionParameters(
parameters: [
Reference(columnName: 'bar'),
],
),
),
),
'x NOT IN "table"': InExpression(
not: true,
left: Reference(columnName: 'x'),
inside: TableReference('table'),
),
'CAST(3 + 4 AS TEXT)': CastExpression( 'CAST(3 + 4 AS TEXT)': CastExpression(
BinaryExpression( BinaryExpression(
NumericLiteral(3.0), NumericLiteral(3.0),

View File

@ -485,6 +485,9 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
test('in', () { test('in', () {
testFormat('SELECT x IN (SELECT * FROM foo);'); testFormat('SELECT x IN (SELECT * FROM foo);');
testFormat('SELECT x NOT IN (SELECT * FROM foo);'); testFormat('SELECT x NOT IN (SELECT * FROM foo);');
testFormat('SELECT x IN foo');
testFormat('SELECT x IN json_each(bar)');
testFormat('SELECT x IN :array');
}); });
test('boolean literals', () { test('boolean literals', () {