Merge remote-tracking branch 'origin/develop' into geopoly

This commit is contained in:
Simon Binder 2024-04-22 22:56:16 +02:00
commit fefae6dd44
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
42 changed files with 350 additions and 102 deletions

View File

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

View File

@ -1,21 +1,19 @@
// #docregion after_generation
// #docregion before_generation
import 'package:drift/drift.dart';
// #enddocregion before_generation
// #enddocregion after_generation
// #docregion open
// These imports are necessary to open the sqlite3 database
// #docregion after_generation
// These additional imports are necessary to open the sqlite3 database
import 'dart:io';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
// ... the TodoItems table definition stays the same
// #enddocregion open
// #docregion before_generation
part 'database.g.dart';
@ -27,25 +25,22 @@ class TodoItems extends Table {
IntColumn get category => integer().nullable()();
}
// #enddocregion table
// #docregion open
@DriftDatabase(tables: [TodoItems])
class AppDatabase extends _$AppDatabase {
// #enddocregion open
// #enddocregion before_generation
// #enddocregion after_generation
// After generating code, this class needs to define a `schemaVersion` getter
// 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
// #enddocregion before_generation
// #docregion open
// #docregion after_generation
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
// #docregion before_generation
}
// #enddocregion before_generation, open
// #docregion open
// #enddocregion before_generation
LazyDatabase _openConnection() {
// the LazyDatabase util lets us find the right location for the file async.
@ -70,7 +65,7 @@ LazyDatabase _openConnection() {
return NativeDatabase.createInBackground(file);
});
}
// #enddocregion open
// #enddocregion after_generation
class WidgetsFlutterBinding {
static void ensureInitialized() {}

View File

@ -114,14 +114,14 @@ Of course, you can also join multiple tables:
{% 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
`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
`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' %}

View File

@ -30,6 +30,9 @@ There are a couple of things that should be kept in mind when working with trans
on the transaction after it has been closed! This can cause data loss or runtime crashes.
Drift contains some runtime checks against this misuse and will throw an exception when a transaction
is used after being closed.
A transaction is active during all asynchronous calls made in a `transaction` block, so transactions
also can't schedule timers or other operations using the database (as those would try to use the
transaction after the main `transaction` block has completed).
2. __Different behavior of stream queries__: Inside a `transaction` callback, stream queries behave
differently. If you're creating streams inside a transaction, check the next section to learn how
they behave.

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
`bool` or `DateTime` as their Dart counterpart.
Booleans are stored as `INTEGER` (either `0` or `1`). Datetimes are stored as
unix timestamps (`INTEGER`) or ISO-8601 (`TEXT`) depending on a configurable
build option.
unix timestamps (`INTEGER`) or ISO-8601 (`TEXT`) [depending on a configurable build option]({{ '../Dart API/tables.md#datetime-options' | pageUrl }}).
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
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.
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' %}
@ -97,10 +97,11 @@ After running either command, the `database.g.dart` file containing the generate
class will have been generated.
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
for migrations after changing the database, we can leave it at `1` for now. The database class
now looks like this:
<a name="open">
{% include "blocks/snippet" snippets = snippets name = 'open' %}
for migrations after changing the database, we can leave it at `1` for now. Update `database.dart`
so it now looks like this:
<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
private data on unix-like systems, which is forbidden on Android. We also use this opportunity

View File

@ -1,4 +1,11 @@
## 2.17.0-dev
## 2.18.0-dev
- Add `AggregateFunctionExpression` to write custom [aggregate function](https://www.sqlite.org/lang_aggfunc.html)
invocations in the Dart query builder.
- The `json_group_array` and `jsonb_group_array` functions now contain an `orderBy`
and `filter` parameter.
## 2.17.0
- Adds `companion` entry to `DataClassName` to override the name of the
generated companion class.

View File

@ -144,8 +144,13 @@ extension JsonExtensions on Expression<String> {
/// all emails in that folder.
/// This string could be turned back into a list with
/// `(json.decode(row.read(subjects)!) as List).cast<String>()`.
Expression<String> jsonGroupArray(Expression value) {
return FunctionCallExpression('json_group_array', [value]);
Expression<String> jsonGroupArray(
Expression value, {
OrderBy? orderBy,
Expression<bool>? filter,
}) {
return AggregateFunctionExpression('json_group_array', [value],
orderBy: orderBy, filter: filter);
}
/// Returns a binary representation of a JSON array containing the result of
@ -153,8 +158,13 @@ Expression<String> jsonGroupArray(Expression value) {
///
/// See [jsonGroupArray], the variant of this function returning a textual
/// description, for more details and an example.
Expression<Uint8List> jsonbGroupArray(Expression value) {
return FunctionCallExpression('jsonb_group_array', [value]);
Expression<Uint8List> jsonbGroupArray(
Expression value, {
OrderBy? orderBy,
Expression<bool>? filter,
}) {
return AggregateFunctionExpression('jsonb_group_array', [value],
orderBy: orderBy, filter: filter);
}
List<Expression> _groupObjectArgs(Map<Expression<String>, Expression> values) {

View File

@ -140,7 +140,7 @@ abstract class _TransactionExecutor extends _BaseExecutor
if (_closed) {
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.');
}
}

View File

@ -12,7 +12,7 @@ part of '../query_builder.dart';
/// This is equivalent to the `COUNT(*) FILTER (WHERE filter)` sql function. The
/// filter will be omitted if null.
Expression<int> countAll({Expression<bool>? filter}) {
return _AggregateExpression('COUNT', const [_StarFunctionParameter()],
return AggregateFunctionExpression('COUNT', const [_StarFunctionParameter()],
filter: filter);
}
@ -26,7 +26,7 @@ extension BaseAggregate<DT extends Object> on Expression<DT> {
/// counted twice.
/// {@macro drift_aggregate_filter}
Expression<int> count({bool distinct = false, Expression<bool>? filter}) {
return _AggregateExpression('COUNT', [this],
return AggregateFunctionExpression('COUNT', [this],
filter: filter, distinct: distinct);
}
@ -35,14 +35,14 @@ extension BaseAggregate<DT extends Object> on Expression<DT> {
/// If there are no non-null values in the group, returns null.
/// {@macro drift_aggregate_filter}
Expression<DT> max({Expression<bool>? filter}) =>
_AggregateExpression('MAX', [this], filter: filter);
AggregateFunctionExpression('MAX', [this], filter: filter);
/// Return the minimum of all non-null values in this group.
///
/// If there are no non-null values in the group, returns null.
/// {@macro drift_aggregate_filter}
Expression<DT> min({Expression<bool>? filter}) =>
_AggregateExpression('MIN', [this], filter: filter);
AggregateFunctionExpression('MIN', [this], filter: filter);
/// Returns the concatenation of all non-null values in the current group,
/// joined by the [separator].
@ -71,7 +71,7 @@ extension BaseAggregate<DT extends Object> on Expression<DT> {
'Cannot use groupConcat with distinct: true and a custom separator');
}
return _AggregateExpression(
return AggregateFunctionExpression(
'GROUP_CONCAT',
[
this,
@ -89,21 +89,21 @@ extension ArithmeticAggregates<DT extends num> on Expression<DT> {
///
/// {@macro drift_aggregate_filter}
Expression<double> avg({Expression<bool>? filter}) =>
_AggregateExpression('AVG', [this], filter: filter);
AggregateFunctionExpression('AVG', [this], filter: filter);
/// Return the maximum of all non-null values in this group.
///
/// If there are no non-null values in the group, returns null.
/// {@macro drift_aggregate_filter}
Expression<DT> max({Expression<bool>? filter}) =>
_AggregateExpression('MAX', [this], filter: filter);
AggregateFunctionExpression('MAX', [this], filter: filter);
/// Return the minimum of all non-null values in this group.
///
/// If there are no non-null values in the group, returns null.
/// {@macro drift_aggregate_filter}
Expression<DT> min({Expression<bool>? filter}) =>
_AggregateExpression('MIN', [this], filter: filter);
AggregateFunctionExpression('MIN', [this], filter: filter);
/// Calculate the sum of all non-null values in the group.
///
@ -115,7 +115,7 @@ extension ArithmeticAggregates<DT extends num> on Expression<DT> {
/// value and doesn't throw an overflow exception.
/// {@macro drift_aggregate_filter}
Expression<DT> sum({Expression<bool>? filter}) =>
_AggregateExpression('SUM', [this], filter: filter);
AggregateFunctionExpression('SUM', [this], filter: filter);
/// Calculate the sum of all non-null values in the group.
///
@ -123,7 +123,7 @@ extension ArithmeticAggregates<DT extends num> on Expression<DT> {
/// uses floating-point values internally.
/// {@macro drift_aggregate_filter}
Expression<double> total({Expression<bool>? filter}) =>
_AggregateExpression('TOTAL', [this], filter: filter);
AggregateFunctionExpression('TOTAL', [this], filter: filter);
}
/// Provides aggregate functions that are available for BigInt expressions.
@ -197,16 +197,41 @@ extension DateTimeAggregate on Expression<DateTime> {
}
}
class _AggregateExpression<D extends Object> extends Expression<D> {
/// An expression invoking an [aggregate function](https://www.sqlite.org/lang_aggfunc.html).
///
/// Aggregate functions, like `count()` or `sum()` collapse the entire data set
/// (or a partition of it, if `GROUP BY` is used) into a single value.
///
/// Drift exposes direct bindings to most aggregate functions (e.g. via
/// [BaseAggregate.count]). This class is useful when writing custom aggregate
/// function invocations.
final class AggregateFunctionExpression<D extends Object>
extends Expression<D> {
/// The name of the aggregate function to invoke.
final String functionName;
final bool distinct;
final List<FunctionParameter> parameter;
/// Whether only distinct rows should be passed to the function.
final bool distinct;
/// The arguments to pass to the function.
final List<FunctionParameter> arguments;
/// The order in which rows of the current group should be passed to the
/// aggregate function.
final OrderBy? orderBy;
/// An optional filter clause only passing rows matching this condition into
/// the function.
final Where? filter;
_AggregateExpression(this.functionName, this.parameter,
{Expression<bool>? filter, this.distinct = false})
: filter = filter != null ? Where(filter) : null;
/// Creates an aggregate function expression from the syntactic components.
AggregateFunctionExpression(
this.functionName,
this.arguments, {
Expression<bool>? filter,
this.distinct = false,
this.orderBy,
}) : filter = filter != null ? Where(filter) : null;
@override
final Precedence precedence = Precedence.primary;
@ -220,7 +245,11 @@ class _AggregateExpression<D extends Object> extends Expression<D> {
if (distinct) {
context.buffer.write('DISTINCT ');
}
_writeCommaSeparated(context, parameter);
_writeCommaSeparated(context, arguments);
if (orderBy case final orderBy?) {
context.writeWhitespace();
orderBy.writeInto(context);
}
context.buffer.write(')');
if (filter != null) {
@ -233,20 +262,20 @@ class _AggregateExpression<D extends Object> extends Expression<D> {
@override
int get hashCode {
return Object.hash(functionName, distinct,
const ListEquality<Object?>().hash(parameter), filter);
const ListEquality<Object?>().hash(arguments), orderBy, filter);
}
@override
bool operator ==(Object other) {
if (!identical(this, other) && other.runtimeType != runtimeType) {
if (!identical(this, other) && other is! AggregateFunctionExpression<D>) {
return false;
}
// ignore: test_types_in_equals
final typedOther = other as _AggregateExpression;
final typedOther = other as AggregateFunctionExpression<D>;
return typedOther.functionName == functionName &&
typedOther.distinct == distinct &&
const ListEquality<Object?>().equals(typedOther.parameter, parameter) &&
const ListEquality<Object?>().equals(typedOther.arguments, arguments) &&
typedOther.orderBy == orderBy &&
typedOther.filter == filter;
}
}

View File

@ -1,6 +1,6 @@
name: drift
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
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
@ -30,7 +30,7 @@ dev_dependencies:
drift_dev: any
drift_testcases:
path: ../extras/integration_tests/drift_testcases
http: ^0.13.4
http: ^1.2.1
lints: ^3.0.0
uuid: ^4.0.0
build_runner: ^2.0.0
@ -39,4 +39,4 @@ dev_dependencies:
rxdart: ^0.27.0
shelf: ^1.3.0
test_descriptor: ^2.0.1
vm_service: ^13.0.0
vm_service: ^14.0.0

View File

@ -101,15 +101,17 @@ void main() {
db.todosTable, db.todosTable.category.equalsExp(db.categories.id))
]);
final stringArray = jsonGroupArray(db.todosTable.id);
final binaryArray = jsonbGroupArray(db.todosTable.id).json();
final stringArray = jsonGroupArray(db.todosTable.id,
orderBy: OrderBy([OrderingTerm.desc(db.todosTable.id)]));
final binaryArray = jsonbGroupArray(db.todosTable.id,
orderBy: OrderBy([OrderingTerm.asc(db.todosTable.id)])).json();
query
..groupBy([db.categories.id])
..addColumns([stringArray, binaryArray]);
final row = await query.getSingle();
expect(json.decode(row.read(stringArray)!), unorderedEquals([1, 3]));
expect(json.decode(row.read(binaryArray)!), unorderedEquals([1, 3]));
expect(json.decode(row.read(stringArray)!), [3, 1]);
expect(json.decode(row.read(binaryArray)!), [1, 3]);
});
test('json_group_object', () async {

View File

@ -42,6 +42,17 @@ void main() {
test('aggregates', () {
expect(jsonGroupArray(column), generates('json_group_array(col)'));
expect(
jsonGroupArray(
column,
orderBy: OrderBy([OrderingTerm.desc(column)]),
filter: column.length.isBiggerOrEqualValue(10),
),
generates(
'json_group_array(col ORDER BY col DESC) FILTER (WHERE LENGTH(col) >= ?)',
[10],
),
);
expect(
jsonGroupObject({
Variable('foo'): column,
@ -84,6 +95,17 @@ void main() {
test('aggregates', () {
expect(jsonbGroupArray(column), generates('jsonb_group_array(col)'));
expect(
jsonbGroupArray(
column,
orderBy: OrderBy([OrderingTerm.desc(column)]),
filter: column.length.isBiggerOrEqualValue(10),
),
generates(
'jsonb_group_array(col ORDER BY col DESC) FILTER (WHERE LENGTH(col) >= ?)',
[10],
),
);
expect(
jsonbGroupObject({
Variable('foo'): column,

View File

@ -107,7 +107,7 @@ class _GeneratesSqlMatcher extends Matcher {
matches = false;
}
final argsMatchState = <String, Object?>{};
final argsMatchState = <Object?, Object?>{};
if (_matchVariables != null &&
!_matchVariables.matches(ctx.boundVariables, argsMatchState)) {
matchState['vars'] = ctx.boundVariables;

View File

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

View File

@ -1,6 +1,6 @@
name: drift_dev
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
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
@ -30,9 +30,9 @@ dependencies:
io: ^1.0.3
# 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'
sqlparser: '^0.34.0'
sqlparser: '^0.35.0'
# Dart analysis
analyzer: '>=5.12.0 <7.0.0'

View File

@ -295,4 +295,21 @@ class MyType implements CustomSqlType<String> {}
expect(custom.expression.toString(), 'MyType()');
}
});
test('recognizes bigint columns', () async {
final state = await 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,6 +1,8 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
inhibit_all_warnings!
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -1,5 +1,7 @@
platform :osx, '10.14'
inhibit_all_warnings!
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -40,8 +40,8 @@ SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
sqlite3: fd89671d969f3e73efe503ce203e28b016b58f68
sqlite3_flutter_libs: 00a50503d69f7ab0fe85a5ff25b33082f4df4ce9
sqlite3_flutter_libs: 01f6f1a7e23e5b22dcbeb49fddab75ecfc1de530
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
PODFILE CHECKSUM: 54c2ee7490cf98371b4d49ae3114180468e1140c
COCOAPODS: 1.12.1
COCOAPODS: 1.14.3

View File

@ -259,7 +259,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,7 +1,10 @@
## 3.35.0-dev
## 0.35.0
- 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: Support the `INT64` hint for `CREATE TABLE` statements.
## 0.34.1

View File

@ -157,7 +157,11 @@ class SchemaFromCreateTable {
final upper = typeName.toUpperCase();
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') ||
upper.contains('CLOB') ||

View File

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

View File

@ -170,6 +170,16 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
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
void visitUpdateStatement(UpdateStatement e, ColumnResolverContext arg) {
// Resolve CTEs first

View File

@ -199,6 +199,56 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
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
void visitIsExpression(IsExpression e, void arg) {
if (e.distinctFromSyntax && options.version < SqliteVersion.v3_39) {
@ -526,9 +576,9 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
isAllowed = !comparisons.any((e) => !isRowValue(e));
}
} else if (parent is InExpression) {
// In expressions are tricky. The rhs can always be a row value, but the
// lhs can only be a row value if the rhs is a subquery
isAllowed = e == parent.inside || parent.inside is SubQuery;
// For in expressions we have a more accurate analysis on whether tuples
// are allowed that looks at both the LHS and the RHS.
isAllowed = true;
} else if (parent is SetComponent) {
isAllowed = true;
}

View File

@ -136,6 +136,14 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
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
void visitNumberedVariable(NumberedVariable e, void arg) {
_foundVariables.add(e);

View File

@ -427,9 +427,11 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override
void visitInExpression(InExpression e, TypeExpectation 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());
}

View File

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

View File

@ -3,7 +3,7 @@ part of '../ast.dart';
/// A tuple of values, denotes in brackets. `(<expr>, ..., <expr>)`.
///
/// 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.
List<Expression> expressions;

View File

@ -180,10 +180,9 @@ class InExpression extends Expression {
/// 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
/// tuple of variables at runtime.
Expression inside;
InExpressionTarget inside;
InExpression({this.not = false, required this.left, required this.inside})
: assert(inside is Tuple || inside is Variable || inside is SubQuery);
InExpression({this.not = false, required this.left, required this.inside});
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
@ -191,7 +190,7 @@ class InExpression extends Expression {
}
@override
List<Expression> get childNodes => [left, inside];
List<AstNode> get childNodes => [left, inside];
@override
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 {
Token? openingLeft;
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
/// only returns one column and one row.
class SubQuery extends Expression {
class SubQuery extends Expression implements InExpressionTarget {
BaseSelectStatement select;
SubQuery({required this.select});

View File

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

View File

@ -511,7 +511,20 @@ class Parser {
final not = _matchOne(TokenType.not);
_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)
..setSpan(left.first!, _previous);
}

View File

@ -1,6 +1,6 @@
name: sqlparser
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
repository: https://github.com/simolus3/drift
#homepage: https://drift.simonbinder.eu/

View File

@ -363,4 +363,10 @@ SELECT * FROM cars
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() {
late SqlEngine engine;
setUp(() {
engine = SqlEngine();
// enable json1 extension
engine = SqlEngine(EngineOptions(version: SqliteVersion.v3_38));
});
test('when using row value in select', () {
@ -15,12 +16,6 @@ void main() {
.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', () {
engine
.analyze('SELECT 1 BETWEEN (1, 2, 3) AND 3')
@ -68,4 +63,33 @@ void main() {
.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';
extension ExpectErrors on AnalysisContext {
void expectError(String lexeme, {AnalysisErrorType? type}) {
void expectError(String lexeme, {AnalysisErrorType? type, message}) {
expect(
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()));
final stmt = engine.parse('''
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;
@ -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: [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(
BinaryExpression(
NumericLiteral(3.0),

View File

@ -485,6 +485,9 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
test('in', () {
testFormat('SELECT x 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', () {