From c6f0fa27aa09bed1a92bf13a7403a223c6e410dd Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 12 Apr 2024 17:36:31 +0200 Subject: [PATCH 1/6] sqlparser: Extend support for `IN` expressions This adds support for table names and table-valued function invocations as right-hand targets in `IN` expressions. The lint for row values around `IN` expressions is updated to only report warnings if there's an actual mismatch in the amount of columns. Closes https://github.com/simolus3/drift/issues/2948 --- sqlparser/CHANGELOG.md | 2 + .../lib/src/analysis/schema/references.dart | 6 +- .../src/analysis/steps/column_resolver.dart | 10 ++++ .../src/analysis/steps/linting_visitor.dart | 56 ++++++++++++++++++- .../lib/src/analysis/steps/prepare_ast.dart | 8 +++ .../src/analysis/types/resolving_visitor.dart | 6 +- sqlparser/lib/src/ast/common/queryables.dart | 9 ++- sqlparser/lib/src/ast/common/tuple.dart | 2 +- sqlparser/lib/src/ast/expressions/simple.dart | 20 +++++-- .../lib/src/ast/expressions/subquery.dart | 2 +- .../lib/src/ast/expressions/variables.dart | 2 +- sqlparser/lib/src/reader/parser.dart | 15 ++++- .../test/analysis/column_resolver_test.dart | 6 ++ .../errors/row_value_misuse_test.dart | 38 ++++++++++--- sqlparser/test/analysis/errors/utils.dart | 4 +- sqlparser/test/parser/expression_test.dart | 16 ++++++ sqlparser/test/utils/node_to_text_test.dart | 3 + 17 files changed, 178 insertions(+), 27 deletions(-) diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index f43d53bd..eb39fc22 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,6 +1,8 @@ ## 3.35.0-dev - 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. ## 0.34.1 diff --git a/sqlparser/lib/src/analysis/schema/references.dart b/sqlparser/lib/src/analysis/schema/references.dart index 799308f0..a62ae702 100644 --- a/sqlparser/lib/src/analysis/schema/references.dart +++ b/sqlparser/lib/src/analysis/schema/references.dart @@ -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 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; diff --git a/sqlparser/lib/src/analysis/steps/column_resolver.dart b/sqlparser/lib/src/analysis/steps/column_resolver.dart index aa49629f..7295d8cf 100644 --- a/sqlparser/lib/src/analysis/steps/column_resolver.dart +++ b/sqlparser/lib/src/analysis/steps/column_resolver.dart @@ -170,6 +170,16 @@ class ColumnResolver extends RecursiveVisitor { 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 diff --git a/sqlparser/lib/src/analysis/steps/linting_visitor.dart b/sqlparser/lib/src/analysis/steps/linting_visitor.dart index 8258e1f2..a814140a 100644 --- a/sqlparser/lib/src/analysis/steps/linting_visitor.dart +++ b/sqlparser/lib/src/analysis/steps/linting_visitor.dart @@ -199,6 +199,56 @@ class LintingVisitor extends RecursiveVisitor { 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 { 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; } diff --git a/sqlparser/lib/src/analysis/steps/prepare_ast.dart b/sqlparser/lib/src/analysis/steps/prepare_ast.dart index b12f9e77..1c984721 100644 --- a/sqlparser/lib/src/analysis/steps/prepare_ast.dart +++ b/sqlparser/lib/src/analysis/steps/prepare_ast.dart @@ -136,6 +136,14 @@ class AstPreparingVisitor extends RecursiveVisitor { 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); diff --git a/sqlparser/lib/src/analysis/types/resolving_visitor.dart b/sqlparser/lib/src/analysis/types/resolving_visitor.dart index c3e843bd..9370830c 100644 --- a/sqlparser/lib/src/analysis/types/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types/resolving_visitor.dart @@ -427,9 +427,11 @@ class TypeResolver extends RecursiveVisitor { @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()); } diff --git a/sqlparser/lib/src/ast/common/queryables.dart b/sqlparser/lib/src/ast/common/queryables.dart index 45e9d8b9..9ce807c0 100644 --- a/sqlparser/lib/src/ast/common/queryables.dart +++ b/sqlparser/lib/src/ast/common/queryables.dart @@ -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; diff --git a/sqlparser/lib/src/ast/common/tuple.dart b/sqlparser/lib/src/ast/common/tuple.dart index 22357d5b..6c5ccc62 100644 --- a/sqlparser/lib/src/ast/common/tuple.dart +++ b/sqlparser/lib/src/ast/common/tuple.dart @@ -3,7 +3,7 @@ part of '../ast.dart'; /// A tuple of values, denotes in brackets. `(, ..., )`. /// /// 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 expressions; diff --git a/sqlparser/lib/src/ast/expressions/simple.dart b/sqlparser/lib/src/ast/expressions/simple.dart index 1d340dbb..2b5a4028 100644 --- a/sqlparser/lib/src/ast/expressions/simple.dart +++ b/sqlparser/lib/src/ast/expressions/simple.dart @@ -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(AstVisitor visitor, A arg) { @@ -191,7 +190,7 @@ class InExpression extends Expression { } @override - List get childNodes => [left, inside]; + List get childNodes => [left, inside]; @override void transformChildren(Transformer 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; diff --git a/sqlparser/lib/src/ast/expressions/subquery.dart b/sqlparser/lib/src/ast/expressions/subquery.dart index 042cdfa1..7b0a18c5 100644 --- a/sqlparser/lib/src/ast/expressions/subquery.dart +++ b/sqlparser/lib/src/ast/expressions/subquery.dart @@ -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}); diff --git a/sqlparser/lib/src/ast/expressions/variables.dart b/sqlparser/lib/src/ast/expressions/variables.dart index 098924f3..876fff81 100644 --- a/sqlparser/lib/src/ast/expressions/variables.dart +++ b/sqlparser/lib/src/ast/expressions/variables.dart @@ -1,6 +1,6 @@ part of '../ast.dart'; -abstract class Variable extends Expression { +abstract class Variable extends Expression implements InExpressionTarget { int? resolvedIndex; } diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index 758166c1..288d09b9 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -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); } diff --git a/sqlparser/test/analysis/column_resolver_test.dart b/sqlparser/test/analysis/column_resolver_test.dart index 403ab0a2..6108b1b3 100644 --- a/sqlparser/test/analysis/column_resolver_test.dart +++ b/sqlparser/test/analysis/column_resolver_test.dart @@ -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); + }); } diff --git a/sqlparser/test/analysis/errors/row_value_misuse_test.dart b/sqlparser/test/analysis/errors/row_value_misuse_test.dart index ff8febe0..bc133ea2 100644 --- a/sqlparser/test/analysis/errors/row_value_misuse_test.dart +++ b/sqlparser/test/analysis/errors/row_value_misuse_test.dart @@ -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)')); + }); + }); } diff --git a/sqlparser/test/analysis/errors/utils.dart b/sqlparser/test/analysis/errors/utils.dart index 3d1ec050..ee1be2a6 100644 --- a/sqlparser/test/analysis/errors/utils.dart +++ b/sqlparser/test/analysis/errors/utils.dart @@ -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)], ); } diff --git a/sqlparser/test/parser/expression_test.dart b/sqlparser/test/parser/expression_test.dart index 1076058a..66e80aaf 100644 --- a/sqlparser/test/parser/expression_test.dart +++ b/sqlparser/test/parser/expression_test.dart @@ -160,6 +160,22 @@ final Map _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), diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index f4785446..bb516eb1 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -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', () { From ca0dee4f83234b053039d23b62d9defd007e7686 Mon Sep 17 00:00:00 2001 From: Ewan Date: Sun, 14 Apr 2024 21:02:55 +0100 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20minimise=20the=20copy/think/paste?= =?UTF-8?q?=20needed=20to=20get=20the=20minimal=20impleme=E2=80=A6=20(#295?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: minimise the copy/think/paste needed to get the minimal implementation working * fix: revert tag name & add closing --------- Co-authored-by: Ewan Nisbet --- docs/lib/snippets/setup/database.dart | 23 +++++++++-------------- docs/pages/docs/setup.md | 11 ++++++----- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart index 00d175a5..3779df69 100644 --- a/docs/lib/snippets/setup/database.dart +++ b/docs/lib/snippets/setup/database.dart @@ -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() {} diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index a1379099..d1660c91 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -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: - -{% 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: + + +{% 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 From ac4947f26684ffe2cf2cbbff20fa8ac3da158efc Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Apr 2024 21:16:32 +0200 Subject: [PATCH 3/6] Support `INT64` type for bigints in drift files https://github.com/simolus3/drift/issues/2955 --- docs/pages/docs/SQL API/drift_files.md | 6 ++++-- .../analysis/resolver/drift/table_test.dart | 17 +++++++++++++++++ sqlparser/CHANGELOG.md | 1 + .../src/analysis/schema/from_create_table.dart | 6 +++++- .../analysis/schema/from_create_table_test.dart | 3 ++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/pages/docs/SQL API/drift_files.md b/docs/pages/docs/SQL API/drift_files.md index a2ff0a69..572c59ee 100644 --- a/docs/pages/docs/SQL API/drift_files.md +++ b/docs/pages/docs/SQL API/drift_files.md @@ -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: diff --git a/drift_dev/test/analysis/resolver/drift/table_test.dart b/drift_dev/test/analysis/resolver/drift/table_test.dart index cf81d1ae..bbfb7b9c 100644 --- a/drift_dev/test/analysis/resolver/drift/table_test.dart +++ b/drift_dev/test/analysis/resolver/drift/table_test.dart @@ -290,4 +290,21 @@ class MyType implements CustomSqlType {} expect(column.sqlType.custom?.dartType.toString(), 'String'); 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); + }); } diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index eb39fc22..9f9c65f0 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -4,6 +4,7 @@ - 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 diff --git a/sqlparser/lib/src/analysis/schema/from_create_table.dart b/sqlparser/lib/src/analysis/schema/from_create_table.dart index 6cc4f4fe..50d563e6 100644 --- a/sqlparser/lib/src/analysis/schema/from_create_table.dart +++ b/sqlparser/lib/src/analysis/schema/from_create_table.dart @@ -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') || diff --git a/sqlparser/test/analysis/schema/from_create_table_test.dart b/sqlparser/test/analysis/schema/from_create_table_test.dart index 84867922..c4770780 100644 --- a/sqlparser/test/analysis/schema/from_create_table_test.dart +++ b/sqlparser/test/analysis/schema/from_create_table_test.dart @@ -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), ]); }); From 11e31cc6538bb743058e7e140520910daaf7efc3 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 20 Apr 2024 15:32:17 +0200 Subject: [PATCH 4/6] Prepare 2.17 release --- drift/CHANGELOG.md | 2 +- drift/pubspec.yaml | 2 +- drift_dev/CHANGELOG.md | 2 +- drift_dev/pubspec.yaml | 6 +++--- sqlparser/CHANGELOG.md | 2 +- sqlparser/pubspec.yaml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index 90151da5..59b00974 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.17.0-dev +## 2.17.0 - Adds `companion` entry to `DataClassName` to override the name of the generated companion class. diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index ba3bf91a..7d72770a 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -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 diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index 8d6b0e3b..596fc483 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -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. diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index e4d69927..bf826fe3 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -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' diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 9f9c65f0..a9596cb3 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.35.0-dev +## 3.35.0 - Fix parsing binary literals. - Expand support for `IN` expressions, they now support tuples on the left-hand diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index fba7f470..271a4321 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -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/ From 9bcaeddb3b2dfa18769e8ddb550968e4093fc5d0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 20 Apr 2024 15:35:17 +0200 Subject: [PATCH 5/6] Upgrade dev dependencies in drift --- drift/pubspec.yaml | 4 ++-- sqlparser/CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index 7d72770a..fcbd149e 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -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 diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index a9596cb3..b3806d76 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.35.0 +## 0.35.0 - Fix parsing binary literals. - Expand support for `IN` expressions, they now support tuples on the left-hand From aba434e7e05f9288a5e2d3c2b73ef12921081021 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 21 Apr 2024 14:51:03 +0200 Subject: [PATCH 6/6] Clarify join result types in docs --- docs/lib/snippets/dart_api/select.dart | 5 ++--- docs/pages/docs/Dart API/select.md | 4 ++-- drift/lib/src/runtime/executor/helpers/engines.dart | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/lib/snippets/dart_api/select.dart b/docs/lib/snippets/dart_api/select.dart index 937f9a09..7752f78b 100644 --- a/docs/lib/snippets/dart_api/select.dart +++ b/docs/lib/snippets/dart_api/select.dart @@ -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 diff --git a/docs/pages/docs/Dart API/select.md b/docs/pages/docs/Dart API/select.md index b26f3bfe..88e308a7 100644 --- a/docs/pages/docs/Dart API/select.md +++ b/docs/pages/docs/Dart API/select.md @@ -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`, 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' %} diff --git a/drift/lib/src/runtime/executor/helpers/engines.dart b/drift/lib/src/runtime/executor/helpers/engines.dart index bb488ea7..47365c4e 100644 --- a/drift/lib/src/runtime/executor/helpers/engines.dart +++ b/drift/lib/src/runtime/executor/helpers/engines.dart @@ -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.'); } }