Merge remote-tracking branch 'origin/develop' into jaspr-docs

This commit is contained in:
Simon Binder
2025-10-18 00:02:01 +02:00
61 changed files with 1527 additions and 229 deletions
+4
View File
@@ -308,6 +308,10 @@ users table contains a lot of rows:
<Snippet href="/lib/src/snippets/dart_api/tables.dart" name="index" />
To specify the ordering mode for a column, you can use an `IndexedColumn` instance:
{{ load_snippet('index_ordering','lib/snippets/dart_api/tables.dart.excerpt.json') }}
#### SQL-based index
If you need more options in your index, for instance to define partial indexes,
+23
View File
@@ -194,6 +194,8 @@ We currently support the following extensions:
- `spellfix1`: Assumes that the [spellfix1](https://www.sqlite.org/spellfix1.html)
module is available. Note that this is not the case for most sqlite3 builds,
including the ones shipping with `sqlite3_flutter_libs`.
- `powersync`: Assumes that the PowerSync SQLite extension is available, allowing references to e.g.
`powersync_crud` in `CREATE TABLE` statements.
### Known custom functions
@@ -226,6 +228,27 @@ Each type consists of an arbitrary word used to determine [column affinity](http
with drift also supporting `DATETIME` and `BOOLEAN` as type hints. Then, the
optional `NULL` keyword can be used to indicate whether the type is nullable.
### External tables
In addition to `known_functions`, you can also define external tables that are assumed
to be available to the database without being managed by drift.
```yaml
targets:
$default:
builders:
drift_dev:
options:
sql:
dialect: sqlite
options:
known_tables:
- "CREATE TABLE external_resource (id INTEGER PRIMARY KEY, another TEXT)"
```
Here, references to `external_resource` would not cause a warning. Drift would not generate
code for the `external_resource` table though.
## Recommended options
In general, we recommend using the default options.
+6 -1
View File
@@ -32,11 +32,16 @@ The generated `User` class will then have a `preferences` column of type
the object in `select`, `update` and `insert` statements. This feature
also works with [compiled custom queries]("/queries/custom").
!!! warning "Caution with equality"
!!! warning "Common type converter issues"
If your converter returns an object that is not comparable by value, the generated dataclass will not
be comparable by value. Consider implementing `==` and `hashCode` on those classes.
Also note that drift generates `part` files by default, which currently can't have their own imports.
For this reason, if you define tables in another file than your database file, you'll have to manually
add imports to type converters to that file.
[Modular code generation](generation_options/modular.md) can fix this issue too.
Since applying type converters for JSON conversion is so common, drift provides a helper
for that. For instance, we could declare the type converter as a field in the
`Preferences` class:
@@ -202,6 +202,17 @@ class Users extends Table {
}
// #enddocregion index
// #docregion index_ordering
@TableIndex(
name: 'log_entries_at',
columns: {IndexedColumn(#loggedAt, orderBy: OrderingMode.desc)},
)
class LogEntries extends Table {
IntColumn get id => integer().autoIncrement()();
DateTimeColumn get loggedAt => dateTime()();
}
// #enddocregion index_ordering
// #docregion indexsql
@TableIndex.sql('''
CREATE INDEX pending_orders ON orders (creation_time)
+7
View File
@@ -1,3 +1,10 @@
## 2.29.0-dev
- Allow specifying ordering for columns in `@TableIndex` annotation.
- Add `DelegatedDatabase.externalExecutor`, allowing transactions started
externally to be used with drift.
- Add `Batch.insertFromSelect` to insert rows from a select statement in a batch.
## 2.28.2
- Legacy wasm database: Fix cast issue when compiling with `dart2wasm`.
+1 -1
View File
@@ -31,7 +31,7 @@ targets:
named_parameters: true
fatal_warnings: true
sql:
dialect: sqlite
dialects: [sqlite, postgres]
options:
version: "3.37"
modules:
+4 -2
View File
@@ -699,8 +699,10 @@ abstract class _$Database extends GeneratedDatabase {
$TodoCategoryItemCountView(this);
late final $TodoItemWithCategoryNameViewView customViewName =
$TodoItemWithCategoryNameViewView(this);
late final Index itemTitle =
Index('item_title', 'CREATE INDEX item_title ON todo_items (title)');
late final Index itemTitle = Index.byDialect('item_title', {
SqlDialect.sqlite: 'CREATE INDEX item_title ON todo_items (title)',
SqlDialect.postgres: 'CREATE INDEX item_title ON todo_items (title)',
});
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
+24 -1
View File
@@ -289,11 +289,16 @@ final class TableIndex {
/// The columns of the table that should be part of the index.
///
/// This can either be a [Symbol] for the default options or an
/// [IndexedColumn] instance.
///
/// Columns are referenced with a [Symbol] of their getter name used in the
/// column definition. For instance, a table declaring a column as
/// `IntColumn get nextUpdateSnapshot => ...()` could reference this column
/// using `#nextUpdateSnapshot`.
final Set<Symbol> columns;
///
/// To further control options of the index, use an [IndexedColumn] instance.
final Set<Object /*IndexedColumn | Symbol*/ > columns;
/// As an alternative to [name], [unique] and [columns], a `CREATE INDEX` SQL
/// statement defining the index.
@@ -324,6 +329,24 @@ final class TableIndex {
columns = const {};
}
/// A column that can appear in a [TableIndex].
final class IndexedColumn {
/// The columns of the table that should be part of the index.
///
/// Columns are referenced with a [Symbol] of their getter name used in the
/// column definition. For instance, a table declaring a column as
/// `IntColumn get nextUpdateSnapshot => ...()` could reference this column
/// using `#nextUpdateSnapshot`.
final Symbol columnName;
/// The [OrderingMode] to use for the index, controlling the order of values
/// in the b-tree.
final OrderingMode? orderBy;
/// Creates an indexed column from a [Symbol] and index options.
const IndexedColumn(this.columnName, {required this.orderBy});
}
/// A class to be used as an annotation on [Table] classes to customize the
/// name for the data class that will be generated for the table class. The data
/// class is a dart object that will be used to represent a row in the table.
+26
View File
@@ -53,6 +53,32 @@ class Batch {
_addContext(context);
}
/// Inserts rows from the [select] statement.
///
/// This method creates an `INSERT INTO SELECT` statement in SQL which will
/// insert a row into this table for each row returned by the [select]
/// statement.
///
/// The [columns] map describes which column from the select statement should
/// be written into which column of the table. The keys of the map are the
/// target column, and values are expressions added to the select statement.
///
/// See also:
/// - [InsertStatement.insertFromSelect], which would be used outside a
/// [Batch].
void insertFromSelect<T extends Table, D>(
TableInfo<T, D> table, BaseSelectStatement select,
{required Map<Column, Expression> columns,
InsertMode? mode,
UpsertClause<T, D>? onConflict}) {
_addUpdate(table, UpdateKind.insert);
final actualMode = mode ?? InsertMode.insert;
final context = InsertStatement<T, D>(_user, table).createContextFromSelect(
select, columns, actualMode,
onConflict: onConflict);
_addContext(context);
}
/// Inserts all [rows] into the [table].
///
/// All fields in a row that don't have a default value or auto-increment
+5 -2
View File
@@ -34,8 +34,11 @@ class DatabaseConnection implements QueryExecutor {
this.connectionData,
bool closeStreamsSynchronously = false,
}) : streamQueries = streamQueries ??
StreamQueryStore(
closeStreamsSynchronously: closeStreamsSynchronously);
switch (executor) {
DatabaseConnection() => executor.streamQueries,
_ => StreamQueryStore(
closeStreamsSynchronously: closeStreamsSynchronously)
};
/// Constructs a [DatabaseConnection] from the [QueryExecutor] by using the
/// default type system and a new [StreamQueryStore].
@@ -0,0 +1,23 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:path/path.dart' as p;
/// True if exporting is supported on this platform, false otherwise.
final bool isExportSupported = true;
/// Exports contents of the [database] as a [Uint8List] representing its main
/// file.
Future<Uint8List> exportDatabase(GeneratedDatabase database) async {
final destination = p.join(Directory.systemTemp.path,
"drift-export-${DateTime.now().toUtc().millisecondsSinceEpoch}.tmp");
await database.exclusively(() async {
await database.customStatement('VACUUM INTO ?;', [destination]);
});
final file = File(destination);
final rawBytes = await file.readAsBytes();
await file.delete();
return rawBytes;
}
@@ -0,0 +1,13 @@
import 'package:drift/drift.dart';
/// True if exporting is supported on this platform, false otherwise.
final bool isExportSupported = false;
/// Exports contents of the [database] as a [Uint8List] representing its main
/// file.
Future<Uint8List> exportDatabase(GeneratedDatabase database) async {
// We currently only support this for native databases. We should also be able
// to support web fairly easily after migrating to sqlite3_web.
throw UnsupportedError(
'Exporting databases it not supported on this platform');
}
@@ -7,6 +7,8 @@ import 'package:drift/src/remote/protocol.dart';
import 'package:drift/src/runtime/executor/transactions.dart';
import 'package:meta/meta.dart';
import 'platform_unsupported.dart' if (dart.library.io) 'platform_native.dart';
import '../api/runtime_api.dart';
import 'devtools.dart';
@@ -25,6 +27,17 @@ class DriftServiceExtension {
final tracked = TrackedDatabase.all.firstWhere((e) => e.id == databaseId);
switch (action) {
case 'get-supported-features':
return {
'isExportSupported': isExportSupported,
};
case 'download':
final exported = await exportDatabase(tracked.database);
return {
'database': tracked.database.runtimeType.toString(),
'data': base64.encode(exported),
};
case 'subscribe-to-tables':
final stream = tracked.database.tableUpdates();
final id = _subscriptionId++;
@@ -448,6 +448,16 @@ class DelegatedDatabase extends _BaseExecutor {
{bool? logStatements, this.isSequential = false})
: logStatements = logStatements ?? false;
/// Returns a [QueryExecutor] delegating calls to another [QueryDelegate].
///
/// This is intended for scenarios where an opened external database
/// connection is shared with drift. In those cases, this allows starting
/// transactions outside of drift but then running drift statements against
/// those external transactions.
QueryExecutor externalExecutor(QueryDelegate delegate) {
return _IndependentExecutor(impl: delegate, db: this);
}
@override
Future<bool> ensureOpen(QueryExecutorUser user) {
return _openingLock.synchronized(() async {
@@ -550,6 +560,32 @@ class DelegatedDatabase extends _BaseExecutor {
}
}
final class _IndependentExecutor extends _BaseExecutor {
@override
final QueryDelegate impl;
final DelegatedDatabase db;
_IndependentExecutor({
required this.impl,
required this.db,
});
@override
SqlDialect get dialect => db.dialect;
@override
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
return db.beginTransactionInContext(context);
}
@override
Future<bool> ensureOpen(QueryExecutorUser user) async {
_ensureOpenCalled = true;
return true;
}
}
/// Inside a `beforeOpen` callback, all drift apis must be available. At the
/// same time, the `beforeOpen` callback must complete before any query sent
/// outside of a `beforeOpen` callback can run. We do this by introducing a
@@ -178,7 +178,7 @@ class StreamQueryStore {
_isShuttingDown = true;
for (final stream in _activeKeyStreams.values) {
stream.close();
await stream.close();
}
// awaiting this is fine - the stream is never exposed to users and we don't
// pause any subscriptions on it.
@@ -211,7 +211,7 @@ class QueryStream<Rows extends Object> {
final queryListener = _QueryStreamListener(listener);
if (_isClosed) {
listener.closeSync();
listener.close();
return;
}
@@ -360,12 +360,13 @@ class QueryStream<Rows extends Object> {
}
}
void close() {
Future<void> close() async {
_isClosed = true;
for (final listener in _listeners) {
listener.controller.close();
}
final listenersDone = Future.wait(
[for (final listener in _listeners) listener.controller.close()]);
_listeners.clear();
await listenersDone;
}
}
@@ -34,7 +34,7 @@ extension WriteDefinition on GenerationContext {
String pickForDialect(Map<SqlDialect, String> sql) {
assert(
sql.containsKey(dialect),
'Tried running SQL optimized for the following dialects: ${sql.keys.join}. '
'Tried running SQL optimized for the following dialects: ${sql.keys.join(', ')}. '
'However, the database is running $dialect. Has that dialect been added '
'to the `dialects` drift builder option?',
);
@@ -95,45 +95,8 @@ class InsertStatement<T extends Table, D> {
InsertMode mode = InsertMode.insert,
UpsertClause<T, D>? onConflict,
}) async {
// To be able to reference columns by names instead of by their index like
// normally done with `INSERT INTO SELECT`, we use a CTE. The final SQL
// statement will look like this:
// WITH source AS $select INSERT INTO $table (...) SELECT ... FROM source
final ctx = GenerationContext.fromDb(database);
const sourceCte = '_source';
ctx.buffer.write('WITH $sourceCte AS (');
select.writeInto(ctx);
ctx.buffer.write(') ');
final columnNameToSelectColumnName = <String, String>{};
columns.forEach((key, value) {
final name = select._nameForColumn(value);
if (name == null) {
throw ArgumentError.value(
value,
'column',
'This column passd to insertFromSelect() was not added to the '
'source select statement.');
}
columnNameToSelectColumnName[key.name] = name;
});
mode.writeInto(ctx);
ctx.buffer
..write(' INTO ${ctx.identifier(table.aliasedName)} (')
..write(columnNameToSelectColumnName.keys.map(ctx.identifier).join(', '))
..write(') SELECT ')
..write(
columnNameToSelectColumnName.values.map(ctx.identifier).join(', '))
..write(' FROM $sourceCte');
if (onConflict != null) {
// Resolve parsing ambiguity (a `ON` from the conflict clause could also
// be parsed as a join).
ctx.buffer.write(' WHERE TRUE');
_writeOnConflict(ctx, mode, null, onConflict);
}
final ctx =
createContextFromSelect(select, columns, mode, onConflict: onConflict);
return await database.withCurrentExecutor((e) async {
await e.runInsert(ctx.sql, ctx.boundVariables);
@@ -276,6 +239,57 @@ class InsertStatement<T extends Table, D> {
return ctx;
}
/// Creates a [GenerationContext] which contains the sql necessary to run an
/// insert from the [select] statement with the [mode].
///
/// This method is used internally by drift. Consider using [insertFromSelect]
/// instead.
GenerationContext createContextFromSelect(BaseSelectStatement select,
Map<Column, Expression> columns, InsertMode mode,
{UpsertClause<T, D>? onConflict}) {
// To be able to reference columns by names instead of by their index like
// normally done with `INSERT INTO SELECT`, we use a CTE. The final SQL
// statement will look like this:
// WITH source AS $select INSERT INTO $table (...) SELECT ... FROM source
final ctx = GenerationContext.fromDb(database);
const sourceCte = '_source';
ctx.buffer.write('WITH $sourceCte AS (');
select.writeInto(ctx);
ctx.buffer.write(') ');
final columnNameToSelectColumnName = <String, String>{};
columns.forEach((key, value) {
final name = select._nameForColumn(value);
if (name == null) {
throw ArgumentError.value(
value,
'column',
'This column passd to insertFromSelect() was not added to the '
'source select statement.');
}
columnNameToSelectColumnName[key.name] = name;
});
mode.writeInto(ctx);
ctx.buffer
..write(' INTO ${ctx.identifier(table.aliasedName)} (')
..write(columnNameToSelectColumnName.keys.map(ctx.identifier).join(', '))
..write(') SELECT ')
..write(
columnNameToSelectColumnName.values.map(ctx.identifier).join(', '))
..write(' FROM $sourceCte');
if (onConflict != null) {
// Resolve parsing ambiguity (a `ON` from the conflict clause could also
// be parsed as a join).
ctx.buffer.write(' WHERE TRUE');
_writeOnConflict(ctx, mode, null, onConflict);
}
return ctx;
}
void _writeOnConflict(
GenerationContext ctx,
InsertMode mode,
+87
View File
@@ -241,4 +241,91 @@ void main() {
verify(executor.beginTransaction()).called(1);
});
group('insert from select', () {
test('with simple select statement', () async {
final query = db.select(db.categories);
await db.batch((b) {
b.insertFromSelect(db.categories, query, columns: {
db.categories.description: db.categories.description,
db.categories.priority: db.categories.priority,
});
});
verify(executor.transactions.runBatched(BatchedStatements(
[
('WITH _source AS (SELECT * FROM "categories") INSERT INTO "categories" '
'("desc", "priority") SELECT "desc", "priority" FROM _source')
],
[
ArgumentsForBatchedStatement(0, []),
],
)));
});
test('with join', () async {
final amountOfTodos = db.todosTable.id.count();
final newDescription = db.categories.description + amountOfTodos.cast();
final query = db.selectOnly(db.todosTable)
..join([
innerJoin(
db.categories, db.categories.id.equalsExp(db.todosTable.category))
])
..groupBy([db.categories.id])
..addColumns([newDescription, db.categories.priority]);
await db.batch((b) {
b.insertFromSelect(db.categories, query, columns: {
db.categories.description: newDescription,
db.categories.priority: db.categories.priority,
});
});
verify(executor.transactions.runBatched(BatchedStatements(
[
('WITH _source AS (SELECT '
'"categories"."desc" || CAST(COUNT("todos"."id") AS TEXT) AS "c0", '
'"categories"."priority" AS "categories.priority" '
'FROM "todos" '
'INNER JOIN "categories" ON "categories"."id" = "todos"."category" '
'GROUP BY "categories"."id") '
'INSERT INTO "categories" ("desc", "priority") '
'SELECT "c0", "categories.priority" FROM _source')
],
[
ArgumentsForBatchedStatement(0, []),
],
)));
});
test('with on conflict clause', () async {
final query = db.select(db.categories);
await db.batch((b) {
b.insertFromSelect(
db.categories,
query,
columns: {
db.categories.description: db.categories.description,
db.categories.priority: db.categories.priority,
},
onConflict: DoUpdate<$CategoriesTable, Category>(
(old) => CategoriesCompanion.custom(
description: old.description,
),
),
);
});
verify(executor.transactions.runBatched(BatchedStatements(
[
('WITH _source AS (SELECT * FROM "categories") INSERT INTO "categories" '
'("desc", "priority") SELECT "desc", "priority" FROM _source WHERE TRUE'
' ON CONFLICT("id") DO UPDATE SET "desc" = "desc"')
],
[
ArgumentsForBatchedStatement(0, []),
],
)));
});
});
}
@@ -92,6 +92,11 @@ void main() {
'INNER JOIN "categories" "t1" '
'ON "t1"."id" = "t0"."category"',
[]));
verify(mockExecutor.runCustom(
'CREATE INDEX categories_desc ON categories ("desc" DESC, priority)',
[],
));
});
test('creates individual tables', () async {
+15
View File
@@ -234,6 +234,21 @@ void main() {
subscription.resume();
await subscription.cancel();
}, skip: 'testing out awaited streams');
test('closing database waits for streams', () async {
final stream = db.select(db.users).watch();
final subscription = stream.listen((_) {})..pause();
var closed = false;
db.close().then((_) => closed = true);
await pumpEventQueue();
expect(closed, isFalse);
subscription.resume();
await subscription.cancel();
await pumpEventQueue();
expect(closed, isTrue);
});
group('stream keys', () {
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:drift/backends.dart';
import 'package:drift/drift.dart';
import 'package:drift/src/runtime/api/runtime_api.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
@@ -17,6 +18,15 @@ class _FakeExecutorUser extends QueryExecutorUser {
int get schemaVersion => 1;
}
class _EmptyDb extends GeneratedDatabase {
_EmptyDb(super.c);
@override
final List<TableInfo> allTables = const [];
@override
final int schemaVersion = 1;
}
MockDatabaseDelegate _mockDelegate() {
final delegate = MockDatabaseDelegate();
provideDummy<TransactionDelegate>(const NoTransactionDelegate());
@@ -390,4 +400,28 @@ void main() {
verify(delegate.close()).called(1);
});
});
test('scoped external blocks', () async {
final rawDb = DelegatedDatabase(delegate, isSequential: true);
final db = _EmptyDb(rawDb);
await db.customSelect('SELECT 1').get();
final secondDelegate = _mockDelegate();
await db.runConnectionZoned(
_CustomExecutor(db, rawDb.externalExecutor(secondDelegate)),
() async {
await db.customStatement("SELECT 'inner'");
},
);
verify(secondDelegate.runCustom("SELECT 'inner'", []));
verifyNever(delegate.runCustom("SELECT 'inner'", []));
});
}
final class _CustomExecutor extends DatabaseConnectionUser {
@override
final GeneratedDatabase attachedDatabase;
_CustomExecutor(this.attachedDatabase, super.executor);
}
@@ -218,7 +218,13 @@ abstract class _$_GeopolyTestDatabase extends GeneratedDatabase {
late final GeopolyTest geopolyTest = GeopolyTest(this);
Selectable<double?> area(int var1) {
return customSelect(
'SELECT geopoly_area(_shape) AS _c0 FROM geopoly_test WHERE "rowid" = ?1',
switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT geopoly_area(_shape) AS _c0 FROM geopoly_test WHERE "rowid" = ?1',
SqlDialect.postgres ||
_ =>
'SELECT geopoly_area(_shape) AS _c0 FROM geopoly_test WHERE "rowid" = \$1',
},
variables: [
Variable<int>(var1)
],
+7
View File
@@ -57,6 +57,13 @@ class Users extends Table with AutoIncrement {
}
@DataClassName('Category')
@TableIndex(
name: 'categories_desc',
columns: {
IndexedColumn(#description, orderBy: OrderingMode.desc),
#priority,
},
)
class Categories extends Table with AutoIncrement {
late final description =
text().named('desc').customConstraint('NOT NULL UNIQUE')();
+50 -18
View File
@@ -659,13 +659,15 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
static const VerificationMeta _isAwesomeMeta =
const VerificationMeta('isAwesome');
@override
late final GeneratedColumn<bool> isAwesome = GeneratedColumn<bool>(
'is_awesome', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("is_awesome" IN (0, 1))'),
defaultValue: const Constant(true));
late final GeneratedColumn<bool> isAwesome =
GeneratedColumn<bool>('is_awesome', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
SqlDialect.sqlite: 'CHECK ("is_awesome" IN (0, 1))',
SqlDialect.postgres: '',
}),
defaultValue: const Constant(true));
static const VerificationMeta _profilePictureMeta =
const VerificationMeta('profilePicture');
@override
@@ -1687,12 +1689,14 @@ class $TableWithEveryColumnTypeTable extends TableWithEveryColumnType
.withConverter<RowId>($TableWithEveryColumnTypeTable.$converterid);
static const VerificationMeta _aBoolMeta = const VerificationMeta('aBool');
@override
late final GeneratedColumn<bool> aBool = GeneratedColumn<bool>(
'a_bool', aliasedName, true,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("a_bool" IN (0, 1))'));
late final GeneratedColumn<bool> aBool =
GeneratedColumn<bool>('a_bool', aliasedName, true,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
SqlDialect.sqlite: 'CHECK ("a_bool" IN (0, 1))',
SqlDialect.postgres: '',
}));
static const VerificationMeta _aDateTimeMeta =
const VerificationMeta('aDateTime');
@override
@@ -3327,6 +3331,12 @@ abstract class _$TodoDb extends GeneratedDatabase {
$CategoryTodoCountViewView(this);
late final $TodoWithCategoryViewView todoWithCategoryView =
$TodoWithCategoryViewView(this);
late final Index categoriesDesc = Index.byDialect('categories_desc', {
SqlDialect.sqlite:
'CREATE INDEX categories_desc ON categories ("desc" DESC, priority)',
SqlDialect.postgres:
'CREATE INDEX categories_desc ON categories ("desc" DESC, priority)',
});
late final SomeDao someDao = SomeDao(this as TodoDb);
Selectable<AllTodosWithCategoryResult> allTodosWithCategory() {
return customSelect(
@@ -3354,7 +3364,10 @@ abstract class _$TodoDb extends GeneratedDatabase {
Future<int> deleteTodoById(RowId var1) {
return customUpdate(
'DELETE FROM todos WHERE id = ?1',
switch (executor.dialect) {
SqlDialect.sqlite => 'DELETE FROM todos WHERE id = ?1',
SqlDialect.postgres || _ => 'DELETE FROM todos WHERE id = \$1',
},
variables: [Variable<int>($TodosTableTable.$converterid.toSql(var1))],
updates: {todosTable},
updateKind: UpdateKind.delete,
@@ -3366,7 +3379,13 @@ abstract class _$TodoDb extends GeneratedDatabase {
final expandedvar3 = $expandVar($arrayStartIndex, var3.length);
$arrayStartIndex += var3.length;
return customSelect(
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
SqlDialect.postgres ||
_ =>
'SELECT * FROM todos WHERE title = \$2 OR id IN ($expandedvar3) OR title = \$1',
},
variables: [
Variable<String>(var1),
Variable<String>(var2),
@@ -3380,7 +3399,13 @@ abstract class _$TodoDb extends GeneratedDatabase {
Selectable<TodoEntry> search({required RowId id}) {
return customSelect(
'SELECT * FROM todos WHERE CASE WHEN -1 = ?1 THEN 1 ELSE id = ?1 END',
switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT * FROM todos WHERE CASE WHEN -1 = ?1 THEN 1 ELSE id = ?1 END',
SqlDialect.postgres ||
_ =>
'SELECT * FROM todos WHERE CASE WHEN -1 = \$1 THEN 1 ELSE id = \$1 END',
},
variables: [
Variable<int>($TodosTableTable.$converterid.toSql(id))
],
@@ -3417,7 +3442,8 @@ abstract class _$TodoDb extends GeneratedDatabase {
store,
listing,
categoryTodoCountView,
todoWithCategoryView
todoWithCategoryView,
categoriesDesc
];
}
@@ -5977,7 +6003,13 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
attachedDatabase.todoWithCategoryView;
Selectable<TodoEntry> todosForUser({required RowId user}) {
return customSelect(
'SELECT t.* FROM todos AS t INNER JOIN shared_todos AS st ON st.todo = t.id INNER JOIN users AS u ON u.id = st.user WHERE u.id = ?1',
switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT t.* FROM todos AS t INNER JOIN shared_todos AS st ON st.todo = t.id INNER JOIN users AS u ON u.id = st.user WHERE u.id = ?1',
SqlDialect.postgres ||
_ =>
'SELECT t.* FROM todos AS t INNER JOIN shared_todos AS st ON st.todo = t.id INNER JOIN users AS u ON u.id = st."user" WHERE u.id = \$1',
},
variables: [
Variable<int>($UsersTable.$converterid.toSql(user))
],
+109 -86
View File
@@ -210,8 +210,8 @@ class _Fake$TodoWithCategoryViewView_16 extends _i1.SmartFake
);
}
class _FakeSomeDao_17 extends _i1.SmartFake implements _i3.SomeDao {
_FakeSomeDao_17(
class _FakeIndex_17 extends _i1.SmartFake implements _i2.Index {
_FakeIndex_17(
Object parent,
Invocation parentInvocation,
) : super(
@@ -220,9 +220,19 @@ class _FakeSomeDao_17 extends _i1.SmartFake implements _i3.SomeDao {
);
}
class _FakeGeneratedDatabase_18 extends _i1.SmartFake
class _FakeSomeDao_18 extends _i1.SmartFake implements _i3.SomeDao {
_FakeSomeDao_18(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
class _FakeGeneratedDatabase_19 extends _i1.SmartFake
implements _i2.GeneratedDatabase {
_FakeGeneratedDatabase_18(
_FakeGeneratedDatabase_19(
Object parent,
Invocation parentInvocation,
) : super(
@@ -231,9 +241,9 @@ class _FakeGeneratedDatabase_18 extends _i1.SmartFake
);
}
class _FakeStreamQueryUpdateRules_19 extends _i1.SmartFake
class _FakeStreamQueryUpdateRules_20 extends _i1.SmartFake
implements _i2.StreamQueryUpdateRules {
_FakeStreamQueryUpdateRules_19(
_FakeStreamQueryUpdateRules_20(
Object parent,
Invocation parentInvocation,
) : super(
@@ -242,9 +252,9 @@ class _FakeStreamQueryUpdateRules_19 extends _i1.SmartFake
);
}
class _FakeDatabaseConnection_20 extends _i1.SmartFake
class _FakeDatabaseConnection_21 extends _i1.SmartFake
implements _i2.DatabaseConnection {
_FakeDatabaseConnection_20(
_FakeDatabaseConnection_21(
Object parent,
Invocation parentInvocation,
) : super(
@@ -253,8 +263,8 @@ class _FakeDatabaseConnection_20 extends _i1.SmartFake
);
}
class _FakeQueryExecutor_21 extends _i1.SmartFake implements _i2.QueryExecutor {
_FakeQueryExecutor_21(
class _FakeQueryExecutor_22 extends _i1.SmartFake implements _i2.QueryExecutor {
_FakeQueryExecutor_22(
Object parent,
Invocation parentInvocation,
) : super(
@@ -263,9 +273,9 @@ class _FakeQueryExecutor_21 extends _i1.SmartFake implements _i2.QueryExecutor {
);
}
class _FakeStreamQueryStore_22 extends _i1.SmartFake
class _FakeStreamQueryStore_23 extends _i1.SmartFake
implements _i4.StreamQueryStore {
_FakeStreamQueryStore_22(
_FakeStreamQueryStore_23(
Object parent,
Invocation parentInvocation,
) : super(
@@ -274,9 +284,9 @@ class _FakeStreamQueryStore_22 extends _i1.SmartFake
);
}
class _FakeDatabaseConnectionUser_23 extends _i1.SmartFake
class _FakeDatabaseConnectionUser_24 extends _i1.SmartFake
implements _i2.DatabaseConnectionUser {
_FakeDatabaseConnectionUser_23(
_FakeDatabaseConnectionUser_24(
Object parent,
Invocation parentInvocation,
) : super(
@@ -285,8 +295,8 @@ class _FakeDatabaseConnectionUser_23 extends _i1.SmartFake
);
}
class _FakeSelectable_24<T> extends _i1.SmartFake implements _i2.Selectable<T> {
_FakeSelectable_24(
class _FakeSelectable_25<T> extends _i1.SmartFake implements _i2.Selectable<T> {
_FakeSelectable_25(
Object parent,
Invocation parentInvocation,
) : super(
@@ -295,8 +305,8 @@ class _FakeSelectable_24<T> extends _i1.SmartFake implements _i2.Selectable<T> {
);
}
class _FakeMigrator_25 extends _i1.SmartFake implements _i2.Migrator {
_FakeMigrator_25(
class _FakeMigrator_26 extends _i1.SmartFake implements _i2.Migrator {
_FakeMigrator_26(
Object parent,
Invocation parentInvocation,
) : super(
@@ -305,8 +315,8 @@ class _FakeMigrator_25 extends _i1.SmartFake implements _i2.Migrator {
);
}
class _FakeFuture_26<T1> extends _i1.SmartFake implements _i5.Future<T1> {
_FakeFuture_26(
class _FakeFuture_27<T1> extends _i1.SmartFake implements _i5.Future<T1> {
_FakeFuture_27(
Object parent,
Invocation parentInvocation,
) : super(
@@ -315,9 +325,9 @@ class _FakeFuture_26<T1> extends _i1.SmartFake implements _i5.Future<T1> {
);
}
class _FakeInsertStatement_27<T1 extends _i2.Table, D1> extends _i1.SmartFake
class _FakeInsertStatement_28<T1 extends _i2.Table, D1> extends _i1.SmartFake
implements _i2.InsertStatement<T1, D1> {
_FakeInsertStatement_27(
_FakeInsertStatement_28(
Object parent,
Invocation parentInvocation,
) : super(
@@ -326,9 +336,9 @@ class _FakeInsertStatement_27<T1 extends _i2.Table, D1> extends _i1.SmartFake
);
}
class _FakeUpdateStatement_28<T extends _i2.Table, D> extends _i1.SmartFake
class _FakeUpdateStatement_29<T extends _i2.Table, D> extends _i1.SmartFake
implements _i2.UpdateStatement<T, D> {
_FakeUpdateStatement_28(
_FakeUpdateStatement_29(
Object parent,
Invocation parentInvocation,
) : super(
@@ -337,9 +347,9 @@ class _FakeUpdateStatement_28<T extends _i2.Table, D> extends _i1.SmartFake
);
}
class _FakeSimpleSelectStatement_29<T1 extends _i2.HasResultSet, D>
class _FakeSimpleSelectStatement_30<T1 extends _i2.HasResultSet, D>
extends _i1.SmartFake implements _i2.SimpleSelectStatement<T1, D> {
_FakeSimpleSelectStatement_29(
_FakeSimpleSelectStatement_30(
Object parent,
Invocation parentInvocation,
) : super(
@@ -348,9 +358,9 @@ class _FakeSimpleSelectStatement_29<T1 extends _i2.HasResultSet, D>
);
}
class _FakeJoinedSelectStatement_30<FirstT extends _i2.HasResultSet, FirstD>
class _FakeJoinedSelectStatement_31<FirstT extends _i2.HasResultSet, FirstD>
extends _i1.SmartFake implements _i2.JoinedSelectStatement<FirstT, FirstD> {
_FakeJoinedSelectStatement_30(
_FakeJoinedSelectStatement_31(
Object parent,
Invocation parentInvocation,
) : super(
@@ -359,9 +369,9 @@ class _FakeJoinedSelectStatement_30<FirstT extends _i2.HasResultSet, FirstD>
);
}
class _FakeBaseSelectStatement_31<Row> extends _i1.SmartFake
class _FakeBaseSelectStatement_32<Row> extends _i1.SmartFake
implements _i2.BaseSelectStatement<Row> {
_FakeBaseSelectStatement_31(
_FakeBaseSelectStatement_32(
Object parent,
Invocation parentInvocation,
) : super(
@@ -370,9 +380,9 @@ class _FakeBaseSelectStatement_31<Row> extends _i1.SmartFake
);
}
class _FakeDeleteStatement_32<T1 extends _i2.Table, D1> extends _i1.SmartFake
class _FakeDeleteStatement_33<T1 extends _i2.Table, D1> extends _i1.SmartFake
implements _i2.DeleteStatement<T1, D1> {
_FakeDeleteStatement_32(
_FakeDeleteStatement_33(
Object parent,
Invocation parentInvocation,
) : super(
@@ -381,9 +391,9 @@ class _FakeDeleteStatement_32<T1 extends _i2.Table, D1> extends _i1.SmartFake
);
}
class _FakeGenerationContext_33 extends _i1.SmartFake
class _FakeGenerationContext_34 extends _i1.SmartFake
implements _i2.GenerationContext {
_FakeGenerationContext_33(
_FakeGenerationContext_34(
Object parent,
Invocation parentInvocation,
) : super(
@@ -653,14 +663,27 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
) as _i3.$TodoWithCategoryViewView);
@override
_i2.Index get categoriesDesc => (super.noSuchMethod(
Invocation.getter(#categoriesDesc),
returnValue: _FakeIndex_17(
this,
Invocation.getter(#categoriesDesc),
),
returnValueForMissingStub: _FakeIndex_17(
this,
Invocation.getter(#categoriesDesc),
),
) as _i2.Index);
@override
_i3.SomeDao get someDao => (super.noSuchMethod(
Invocation.getter(#someDao),
returnValue: _FakeSomeDao_17(
returnValue: _FakeSomeDao_18(
this,
Invocation.getter(#someDao),
),
returnValueForMissingStub: _FakeSomeDao_17(
returnValueForMissingStub: _FakeSomeDao_18(
this,
Invocation.getter(#someDao),
),
@@ -684,11 +707,11 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
@override
_i2.GeneratedDatabase get attachedDatabase => (super.noSuchMethod(
Invocation.getter(#attachedDatabase),
returnValue: _FakeGeneratedDatabase_18(
returnValue: _FakeGeneratedDatabase_19(
this,
Invocation.getter(#attachedDatabase),
),
returnValueForMissingStub: _FakeGeneratedDatabase_18(
returnValueForMissingStub: _FakeGeneratedDatabase_19(
this,
Invocation.getter(#attachedDatabase),
),
@@ -697,11 +720,11 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
@override
_i2.StreamQueryUpdateRules get streamUpdateRules => (super.noSuchMethod(
Invocation.getter(#streamUpdateRules),
returnValue: _FakeStreamQueryUpdateRules_19(
returnValue: _FakeStreamQueryUpdateRules_20(
this,
Invocation.getter(#streamUpdateRules),
),
returnValueForMissingStub: _FakeStreamQueryUpdateRules_19(
returnValueForMissingStub: _FakeStreamQueryUpdateRules_20(
this,
Invocation.getter(#streamUpdateRules),
),
@@ -710,11 +733,11 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
@override
_i2.DatabaseConnection get connection => (super.noSuchMethod(
Invocation.getter(#connection),
returnValue: _FakeDatabaseConnection_20(
returnValue: _FakeDatabaseConnection_21(
this,
Invocation.getter(#connection),
),
returnValueForMissingStub: _FakeDatabaseConnection_20(
returnValueForMissingStub: _FakeDatabaseConnection_21(
this,
Invocation.getter(#connection),
),
@@ -736,11 +759,11 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
@override
_i2.QueryExecutor get executor => (super.noSuchMethod(
Invocation.getter(#executor),
returnValue: _FakeQueryExecutor_21(
returnValue: _FakeQueryExecutor_22(
this,
Invocation.getter(#executor),
),
returnValueForMissingStub: _FakeQueryExecutor_21(
returnValueForMissingStub: _FakeQueryExecutor_22(
this,
Invocation.getter(#executor),
),
@@ -749,11 +772,11 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
@override
_i4.StreamQueryStore get streamQueries => (super.noSuchMethod(
Invocation.getter(#streamQueries),
returnValue: _FakeStreamQueryStore_22(
returnValue: _FakeStreamQueryStore_23(
this,
Invocation.getter(#streamQueries),
),
returnValueForMissingStub: _FakeStreamQueryStore_22(
returnValueForMissingStub: _FakeStreamQueryStore_23(
this,
Invocation.getter(#streamQueries),
),
@@ -762,11 +785,11 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
@override
_i2.DatabaseConnectionUser get resolvedEngine => (super.noSuchMethod(
Invocation.getter(#resolvedEngine),
returnValue: _FakeDatabaseConnectionUser_23(
returnValue: _FakeDatabaseConnectionUser_24(
this,
Invocation.getter(#resolvedEngine),
),
returnValueForMissingStub: _FakeDatabaseConnectionUser_23(
returnValueForMissingStub: _FakeDatabaseConnectionUser_24(
this,
Invocation.getter(#resolvedEngine),
),
@@ -779,7 +802,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#allTodosWithCategory,
[],
),
returnValue: _FakeSelectable_24<_i3.AllTodosWithCategoryResult>(
returnValue: _FakeSelectable_25<_i3.AllTodosWithCategoryResult>(
this,
Invocation.method(
#allTodosWithCategory,
@@ -787,7 +810,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
),
returnValueForMissingStub:
_FakeSelectable_24<_i3.AllTodosWithCategoryResult>(
_FakeSelectable_25<_i3.AllTodosWithCategoryResult>(
this,
Invocation.method(
#allTodosWithCategory,
@@ -821,7 +844,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
var3,
],
),
returnValue: _FakeSelectable_24<_i3.TodoEntry>(
returnValue: _FakeSelectable_25<_i3.TodoEntry>(
this,
Invocation.method(
#withIn,
@@ -832,7 +855,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
],
),
),
returnValueForMissingStub: _FakeSelectable_24<_i3.TodoEntry>(
returnValueForMissingStub: _FakeSelectable_25<_i3.TodoEntry>(
this,
Invocation.method(
#withIn,
@@ -853,7 +876,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
[],
{#id: id},
),
returnValue: _FakeSelectable_24<_i3.TodoEntry>(
returnValue: _FakeSelectable_25<_i3.TodoEntry>(
this,
Invocation.method(
#search,
@@ -861,7 +884,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
{#id: id},
),
),
returnValueForMissingStub: _FakeSelectable_24<_i3.TodoEntry>(
returnValueForMissingStub: _FakeSelectable_25<_i3.TodoEntry>(
this,
Invocation.method(
#search,
@@ -877,14 +900,14 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#findCustom,
[],
),
returnValue: _FakeSelectable_24<_i3.MyCustomObject>(
returnValue: _FakeSelectable_25<_i3.MyCustomObject>(
this,
Invocation.method(
#findCustom,
[],
),
),
returnValueForMissingStub: _FakeSelectable_24<_i3.MyCustomObject>(
returnValueForMissingStub: _FakeSelectable_25<_i3.MyCustomObject>(
this,
Invocation.method(
#findCustom,
@@ -899,14 +922,14 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#createMigrator,
[],
),
returnValue: _FakeMigrator_25(
returnValue: _FakeMigrator_26(
this,
Invocation.method(
#createMigrator,
[],
),
),
returnValueForMissingStub: _FakeMigrator_25(
returnValueForMissingStub: _FakeMigrator_26(
this,
Invocation.method(
#createMigrator,
@@ -1038,7 +1061,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#doWhenOpened,
@@ -1055,7 +1078,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#doWhenOpened,
@@ -1072,14 +1095,14 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#into,
[table],
),
returnValue: _FakeInsertStatement_27<T, D>(
returnValue: _FakeInsertStatement_28<T, D>(
this,
Invocation.method(
#into,
[table],
),
),
returnValueForMissingStub: _FakeInsertStatement_27<T, D>(
returnValueForMissingStub: _FakeInsertStatement_28<T, D>(
this,
Invocation.method(
#into,
@@ -1096,14 +1119,14 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#update,
[table],
),
returnValue: _FakeUpdateStatement_28<Tbl, R>(
returnValue: _FakeUpdateStatement_29<Tbl, R>(
this,
Invocation.method(
#update,
[table],
),
),
returnValueForMissingStub: _FakeUpdateStatement_28<Tbl, R>(
returnValueForMissingStub: _FakeUpdateStatement_29<Tbl, R>(
this,
Invocation.method(
#update,
@@ -1123,7 +1146,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
[table],
{#distinct: distinct},
),
returnValue: _FakeSimpleSelectStatement_29<T, R>(
returnValue: _FakeSimpleSelectStatement_30<T, R>(
this,
Invocation.method(
#select,
@@ -1131,7 +1154,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
{#distinct: distinct},
),
),
returnValueForMissingStub: _FakeSimpleSelectStatement_29<T, R>(
returnValueForMissingStub: _FakeSimpleSelectStatement_30<T, R>(
this,
Invocation.method(
#select,
@@ -1152,7 +1175,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
[table],
{#distinct: distinct},
),
returnValue: _FakeJoinedSelectStatement_30<T, R>(
returnValue: _FakeJoinedSelectStatement_31<T, R>(
this,
Invocation.method(
#selectOnly,
@@ -1160,7 +1183,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
{#distinct: distinct},
),
),
returnValueForMissingStub: _FakeJoinedSelectStatement_30<T, R>(
returnValueForMissingStub: _FakeJoinedSelectStatement_31<T, R>(
this,
Invocation.method(
#selectOnly,
@@ -1178,14 +1201,14 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#selectExpressions,
[columns],
),
returnValue: _FakeBaseSelectStatement_31<_i2.TypedResult>(
returnValue: _FakeBaseSelectStatement_32<_i2.TypedResult>(
this,
Invocation.method(
#selectExpressions,
[columns],
),
),
returnValueForMissingStub: _FakeBaseSelectStatement_31<_i2.TypedResult>(
returnValueForMissingStub: _FakeBaseSelectStatement_32<_i2.TypedResult>(
this,
Invocation.method(
#selectExpressions,
@@ -1202,14 +1225,14 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#delete,
[table],
),
returnValue: _FakeDeleteStatement_32<T, D>(
returnValue: _FakeDeleteStatement_33<T, D>(
this,
Invocation.method(
#delete,
[table],
),
),
returnValueForMissingStub: _FakeDeleteStatement_32<T, D>(
returnValueForMissingStub: _FakeDeleteStatement_33<T, D>(
this,
Invocation.method(
#delete,
@@ -1295,7 +1318,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#readsFrom: readsFrom,
},
),
returnValue: _FakeSelectable_24<_i2.QueryRow>(
returnValue: _FakeSelectable_25<_i2.QueryRow>(
this,
Invocation.method(
#customSelect,
@@ -1306,7 +1329,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
},
),
),
returnValueForMissingStub: _FakeSelectable_24<_i2.QueryRow>(
returnValueForMissingStub: _FakeSelectable_25<_i2.QueryRow>(
this,
Invocation.method(
#customSelect,
@@ -1334,7 +1357,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#readsFrom: readsFrom,
},
),
returnValue: _FakeSelectable_24<_i2.QueryRow>(
returnValue: _FakeSelectable_25<_i2.QueryRow>(
this,
Invocation.method(
#customSelectQuery,
@@ -1345,7 +1368,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
},
),
),
returnValueForMissingStub: _FakeSelectable_24<_i2.QueryRow>(
returnValueForMissingStub: _FakeSelectable_25<_i2.QueryRow>(
this,
Invocation.method(
#customSelectQuery,
@@ -1397,7 +1420,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#transaction,
@@ -1416,7 +1439,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#transaction,
@@ -1443,7 +1466,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#exclusively,
@@ -1460,7 +1483,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#exclusively,
@@ -1502,7 +1525,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#runWithInterceptor,
@@ -1521,7 +1544,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
),
(T v) => _i5.Future<T>.value(v),
) ??
_FakeFuture_26<T>(
_FakeFuture_27<T>(
this,
Invocation.method(
#runWithInterceptor,
@@ -1546,7 +1569,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
#startIndex: startIndex,
},
),
returnValue: _FakeGenerationContext_33(
returnValue: _FakeGenerationContext_34(
this,
Invocation.method(
#$write,
@@ -1557,7 +1580,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
},
),
),
returnValueForMissingStub: _FakeGenerationContext_33(
returnValueForMissingStub: _FakeGenerationContext_34(
this,
Invocation.method(
#$write,
@@ -1585,7 +1608,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
],
{#startIndex: startIndex},
),
returnValue: _FakeGenerationContext_33(
returnValue: _FakeGenerationContext_34(
this,
Invocation.method(
#$writeInsertable,
@@ -1596,7 +1619,7 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
{#startIndex: startIndex},
),
),
returnValueForMissingStub: _FakeGenerationContext_33(
returnValueForMissingStub: _FakeGenerationContext_34(
this,
Invocation.method(
#$writeInsertable,
@@ -2,9 +2,11 @@
@TestOn('vm')
library;
import 'package:async/async.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:drift/src/runtime/cancellation_zone.dart';
import 'package:drift/src/runtime/executor/stream_queries.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.dart';
@@ -47,6 +49,26 @@ void main() {
driftDb.select(driftDb.categories).get(), completion(hasLength(1)));
});
test('DatabaseConnection constructor can wrap inner', () async {
final raw = NativeDatabase.memory();
final streams = StreamQueryStore();
final db = TodoDb(
DatabaseConnection(DatabaseConnection(raw, streamQueries: streams)));
await db
.into(db.categories)
.insert(CategoriesCompanion.insert(description: 'description'));
final query = StreamQueue(db.categories.all().watch());
await expectLater(query, emits(hasLength(1)));
await raw.runCustom('DELETE FROM categories');
streams.handleTableUpdates({TableUpdate('categories')});
await expectLater(query, emits(isEmpty));
await query.cancel();
await db.close();
});
group('nested transactions', () {
test(
'outer transaction does not see inner writes after rollback',
@@ -92,6 +92,7 @@ void main() {
test('can be used in a query stream', () async {
final stream = StreamQueue(db.readView().watch());
addTearDown(stream.cancel);
const entry = Config(
configKey: 'another_key',
configValue: DriftAny('value'),
@@ -12,6 +12,7 @@ void main() {
addTearDown(db.close);
final query = StreamQueue(db.select(db.myView).watch());
addTearDown(query.cancel);
await expectLater(query, emits(isEmpty));
await db.into(db.config).insert(ConfigCompanion.insert(
+1
View File
@@ -263,6 +263,7 @@ void main() {
);
});
stream.cancel();
await db.close();
}
+7
View File
@@ -1,3 +1,10 @@
## 2.29.0-dev
- Fix invalid schema code for views.
- Add the `known_tables` option. It allows defining external tables assumed to be available
without including them in the drift-managed schema.
- Make-migrations: Add `--no-test` option to disable generating tests.
## 2.28.3
- Remove dependencies on discontinued build packages.
@@ -86,6 +86,8 @@ class DriftAnalysisDriver {
if (options.hasModule(SqlModule.spellfix1))
const Spellfix1Extension(),
if (options.hasModule(SqlModule.geopoly)) const GeopolyExtension(),
if (options.hasModule(SqlModule.powersync))
const PowerSyncSqliteExtension(),
],
version: options.sqliteVersion,
),
+43 -1
View File
@@ -4,7 +4,15 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart'
show BasicType, ResolvedType, SchemaFromCreateTable, SqliteVersion;
show
BasicType,
EngineOptions,
ResolvedType,
SchemaFromCreateTable,
SqlEngine,
SqliteVersion,
Table,
TableInducingStatement;
import 'package:string_scanner/string_scanner.dart';
part '../generated/analysis/options.g.dart';
@@ -281,10 +289,14 @@ class SqliteAnalysisOptions {
final Map<String, KnownSqliteFunction> knownFunctions;
@_TableFromSql()
final List<Table> knownTables;
const SqliteAnalysisOptions({
this.modules = const [],
this.version,
this.knownFunctions = const {},
this.knownTables = const [],
});
factory SqliteAnalysisOptions.fromJson(Map json) {
@@ -294,6 +306,32 @@ class SqliteAnalysisOptions {
Map<String, Object?> toJson() => _$SqliteAnalysisOptionsToJson(this);
}
final class _TableFromSql extends JsonConverter<Table, String> {
const _TableFromSql();
@override
Table fromJson(String json) {
final engine = SqlEngine(EngineOptions(version: SqliteVersion.current));
final result = engine.parse(json);
if (result.errors.isNotEmpty) {
throw ArgumentError.value(json,
'Not a valid CREATE TABLE statement: ${result.errors.join('\n')}');
}
final root = result.rootNode;
if (root is! TableInducingStatement) {
throw ArgumentError.value(json, 'Not a valid CREATE TABLE statement');
}
return SchemaFromCreateTable().read(root);
}
@override
String toJson(Table object) {
throw UnsupportedError('Unused');
}
}
class KnownSqliteFunction {
final List<ResolvedType> argumentTypes;
final ResolvedType returnType;
@@ -451,6 +489,10 @@ enum SqlModule {
/// Enables the dbstat table providing insights into the disk state occupied
/// by certain tables.
dbstat,
/// Enables static analysis support for PowerSync, providing the
/// `powersync_crud` virtual table.
powersync,
}
/// The possible values for the case of the table and column names.
@@ -30,16 +30,39 @@ class DartIndexResolver extends LocalElementResolver<DiscoveredDartIndex> {
final unique = computed?.getField('unique')?.toBoolValue() ?? false;
final columns = <DriftColumn>[];
final columns = <DriftIndexedColumn>[];
final referencedColumns = computed?.getField('columns')?.toSetValue();
for (final column in referencedColumns ?? const <DartObject>{}) {
final columnName = column.toSymbolValue();
// Column can either be a Symbol or an IndexedColumn instance.
String? columnName;
OrderingMode? orderBy;
if (column.toSymbolValue() case final symbol?) {
columnName = symbol;
} else {
columnName = column.getField('columnName')?.toSymbolValue();
if (column.getField('orderBy')?.getField('_name')?.toStringValue()
case final orderByName?) {
orderBy = switch (orderByName) {
'asc' => OrderingMode.ascending,
'desc' => OrderingMode.descending,
_ => null,
};
}
}
if (columnName == null) {
reportError(DriftAnalysisError.forDartElement(
discovered.dartElement,
'Unknown entry in columns array, should be a symbols or an IndexedColumn instance.',
));
}
final tableColumn =
table?.columns.firstWhereOrNull((c) => c.nameInDart == columnName);
if (tableColumn != null) {
columns.add(tableColumn);
columns.add(DriftIndexedColumn(column: tableColumn, orderBy: orderBy));
} else {
reportError(DriftAnalysisError.forDartElement(
discovered.dartElement,
@@ -10,10 +10,17 @@ class DriftOptionsExtension implements Extension {
@override
void register(SqlEngine engine) {
final knownFunctions = options.sqliteOptions?.knownFunctions;
final knownTables = options.sqliteOptions?.knownTables;
if (knownFunctions != null) {
engine.registerFunctionHandler(_CustomFunctions(knownFunctions));
}
if (knownTables != null) {
for (final knownTable in knownTables) {
engine.registerTable(knownTable);
}
}
}
}
@@ -182,7 +182,7 @@ class DriftResolver {
return resolveReferencedElement(owner, existing.ownId);
} else {
return InvalidReferenceResult(
InvalidReferenceError.noElementWichSuchName,
InvalidReferenceError.noElementWithSuchName,
'The referenced element, ${element.name3}, is not understood by drift.',
);
}
@@ -214,7 +214,7 @@ class DriftResolver {
if (candidates.isEmpty) {
return InvalidReferenceResult(
InvalidReferenceError.noElementWichSuchName,
InvalidReferenceError.noElementWithSuchName,
'`$reference` could not be found in any import.',
);
} else if (candidates.length > 1) {
@@ -248,6 +248,17 @@ abstract class LocalElementResolver<T extends DiscoveredElement> {
DriftAnalysisError Function(String msg) createError,
) async {
final result = await resolver.resolveReference(discovered.ownId, reference);
if (result
case InvalidReferenceResult(
error: InvalidReferenceError.noElementWithSuchName
)) {
final knownTables = resolver.driver.options.sqliteOptions?.knownTables;
if (knownTables != null && knownTables.any((e) => e.name == reference)) {
// This table is external, no need to emit a warning.
return null;
}
}
return handleReferenceResult(result, createError);
}
@@ -355,11 +366,11 @@ abstract class LocalElementResolver<T extends DiscoveredElement> {
Future<DriftElement> resolve();
}
abstract class ResolveReferencedElementResult {
sealed class ResolveReferencedElementResult {
const ResolveReferencedElementResult();
}
class ResolvedReferenceFound extends ResolveReferencedElementResult {
final class ResolvedReferenceFound extends ResolveReferencedElementResult {
final DriftElement element;
ResolvedReferenceFound(this.element);
@@ -370,24 +381,24 @@ enum InvalidReferenceError {
/// Reported by [DriftResolver.resolveReference] when no element with the
/// given name exists in transitive imports.
noElementWichSuchName,
noElementWithSuchName,
/// Reported by [DriftResolver.resolveReference] when more than one element
/// with the queried name was found.
ambigiousElements,
}
class InvalidReferenceResult extends ResolveReferencedElementResult {
final class InvalidReferenceResult extends ResolveReferencedElementResult {
final InvalidReferenceError error;
final String message;
InvalidReferenceResult(this.error, this.message);
}
class ReferencedElementCouldNotBeResolved
final class ReferencedElementCouldNotBeResolved
extends ResolveReferencedElementResult {}
class ReferencesItself extends ResolveReferencedElementResult {
final class ReferencesItself extends ResolveReferencedElementResult {
const ReferencesItself();
}
+13 -3
View File
@@ -17,7 +17,7 @@ class DriftIndex extends DriftSchemaElement {
/// This list is empty for indices created in SQL because it can't represent
/// all expressions being indexed. It is useful for Dart-defined indices to
/// implement [createStatementForDartDefinition].
List<DriftColumn> indexedColumns;
List<DriftIndexedColumn> indexedColumns;
/// Whethet the index has been declared to be unique.
final bool unique;
@@ -69,10 +69,20 @@ class DriftIndex extends DriftSchemaElement {
unique: unique,
columns: [
for (final column in indexedColumns)
IndexedColumn(Reference(columnName: column.nameInSql))
IndexedColumn(
Reference(columnName: column.column.nameInSql),
column.orderBy,
)
],
);
}
}
sealed class DriftIndexDefintion {}
final class DriftIndexedColumn {
final DriftColumn column;
final OrderingMode? orderBy;
DriftIndexedColumn({required this.column, required this.orderBy});
}
sealed class DriftIndexDefinition {}
+16 -4
View File
@@ -2,7 +2,7 @@ import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
import 'package:sqlparser/sqlparser.dart' show ReferenceAction;
import 'package:sqlparser/sqlparser.dart' show OrderingMode, ReferenceAction;
import 'driver/driver.dart';
import 'driver/state.dart';
@@ -75,7 +75,11 @@ class ElementSerializer {
'type': 'index',
'sql': element.createStmt,
'columns': [
for (final column in element.indexedColumns) column.nameInSql,
for (final column in element.indexedColumns)
{
'column': column.column.nameInSql,
'order_by': column.orderBy?.name,
},
],
'unique': element.unique,
};
@@ -594,8 +598,16 @@ class ElementDeserializer {
table: onTable,
createStmt: json['sql'] as String?,
indexedColumns: [
for (final entry in json['columns'] as List)
onTable!.columnBySqlName[entry as String]!,
for (final entry
in (json['columns'] as List).cast<Map<String, Object?>>())
DriftIndexedColumn(
column: onTable!.columnBySqlName[entry['column'] as String]!,
orderBy: switch (entry['order_by']) {
null => null,
final orderBy =>
OrderingMode.values.byName(orderBy as String),
},
),
],
unique: json['unique'] as bool,
);
@@ -17,6 +17,11 @@ import 'package:recase/recase.dart';
class MakeMigrationCommand extends DriftCommand {
MakeMigrationCommand(super.cli) {
argParser.registerExportSchemaStartupCodeOption();
argParser.addFlag(
'test',
help: 'Whether to generate a test file template for migrations',
defaultsTo: true,
);
}
@override
@@ -96,6 +101,7 @@ targets:
cli.exit('`test_dir` must be a relative path. Remove the leading slash');
}
final generateTests = argResults?.flag('test') ?? true;
final dumpGeneratedSchemaCode = argResults?.exportSchemaStartupCode;
/// The root directory where test files for all databases are stored
@@ -142,8 +148,11 @@ targets:
await writer.writeStepsFile();
// Write the generated test databases
await writer.writeTestDatabases();
// Write the generated test
await writer.writeTest();
// Write the generated test (unless that option has been disabled).
if (generateTests) {
await writer.writeTest();
}
await writer.flush();
writer.suggestDataMigrationTest();
}
@@ -211,6 +211,7 @@ const _$SqlModuleEnumMap = {
SqlModule.spellfix1: 'spellfix1',
SqlModule.geopoly: 'geopoly',
SqlModule.dbstat: 'dbstat',
SqlModule.powersync: 'powersync',
};
const _$CaseFromDartToSqlEnumMap = {
@@ -270,7 +271,12 @@ SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['modules', 'version', 'known_functions'],
allowedKeys: const [
'modules',
'version',
'known_functions',
'known_tables'
],
);
final val = SqliteAnalysisOptions(
modules: $checkedConvert(
@@ -292,10 +298,20 @@ SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
k as String, KnownSqliteFunction.fromJson(e as String)),
) ??
const {}),
knownTables: $checkedConvert(
'known_tables',
(v) =>
(v as List<dynamic>?)
?.map((e) => const _TableFromSql().fromJson(e as String))
.toList() ??
const []),
);
return val;
},
fieldKeyMap: const {'knownFunctions': 'known_functions'},
fieldKeyMap: const {
'knownFunctions': 'known_functions',
'knownTables': 'known_tables'
},
);
Map<String, dynamic> _$SqliteAnalysisOptionsToJson(
@@ -306,6 +322,8 @@ Map<String, dynamic> _$SqliteAnalysisOptionsToJson(
instance.version, const _SqliteVersionConverter().toJson),
'known_functions':
instance.knownFunctions.map((k, e) => MapEntry(k, e.toJson())),
'known_tables':
instance.knownTables.map(const _TableFromSql().toJson).toList(),
};
Value? _$JsonConverterFromJson<Json, Value>(
@@ -162,7 +162,11 @@ class SchemaWriter {
'sql': entity.createStmt,
'unique': entity.unique,
'columns': [
for (final column in entity.indexedColumns) column.nameInSql,
for (final column in entity.indexedColumns)
{
'column': column.column.nameInSql,
'order_by': column.orderBy?.name,
},
],
};
} else if (entity is DriftView) {
@@ -446,13 +450,33 @@ class SchemaReader {
final sql = content['sql'] as String?;
if (_version.supportsDartIndex) {
DriftIndexedColumn readColumn(Object serialized) {
if (serialized case final String name) {
// Older versions used to write index columns by name.
return DriftIndexedColumn(
column: on.columnBySqlName[name]!,
orderBy: null,
);
} else {
// Newer schemas encode {name, order_by}.
serialized as Map<String, Object?>;
return DriftIndexedColumn(
column: on.columnBySqlName[serialized['column'] as String]!,
orderBy: switch (serialized['order_by']) {
null => null,
final ordering => OrderingMode.values.byName(ordering as String),
},
);
}
}
final index = DriftIndex(
_id(name),
_declaration,
table: on,
indexedColumns: [
for (final col in content['columns'] as List)
on.columnBySqlName[col]!,
readColumn(col as Object),
],
unique: content['unique'] as bool,
createStmt: sql,
@@ -478,7 +502,11 @@ class SchemaReader {
unique: stmt.unique,
indexedColumns: [
for (final column in stmt.columns)
on.columnBySqlName[(column.expression as Reference).columnName]!,
DriftIndexedColumn(
column: on.columnBySqlName[
(column.expression as Reference).columnName]!,
orderBy: column.ordering,
)
],
)..parsedStatement = stmt;
}
@@ -586,7 +614,13 @@ class SchemaReader {
_declaration,
columns: [
for (final column in content['columns'] as Iterable)
_readColumn(column as Map<String, dynamic>)
_readColumn(
column as Map<String, dynamic>,
// Don't parse column constraints. The serialized format includes a
// generated_as DSL feature for each view column, but that should be
// ignored because we're parsing views as SQL instead.
parseColumnConstraints: false,
)
],
source: SqlViewSource(content['sql'] as String),
customParentClass: null,
@@ -617,7 +651,8 @@ class SchemaReader {
static final _dialectByName = SqlDialect.values.asNameMap();
DriftColumn _readColumn(Map<String, dynamic> data) {
DriftColumn _readColumn(Map<String, dynamic> data,
{bool parseColumnConstraints = true}) {
final name = data['name'] as String;
final columnType =
_SerializeSqlType.deserialize(data['moor_type'] as String);
@@ -629,7 +664,7 @@ class SchemaReader {
final dslFeatures = <DriftColumnConstraint?>[
for (final feature in data['dsl_features'] as List<dynamic>)
_columnFeature(feature),
if (parseColumnConstraints) _columnFeature(feature),
if (dialectAwareConstraints != null)
DefaultConstraintsFromSchemaFile(null, dialectSpecific: {
for (final MapEntry(:key, :value)
+1 -1
View File
@@ -37,7 +37,7 @@ dependencies:
# Drift-specific analysis and apis
drift: ">=2.28.0 <2.29.0"
sqlite3: ^2.4.6
sqlparser: ^0.41.2
sqlparser: ^0.42.0-0
# Dart analysis
# Note: When changing this to a lower bound of 8.x, please re-enable
+36
View File
@@ -116,4 +116,40 @@ sqlite:
expect(() => KnownSqliteFunction.fromJson('int (boolean, )'), throws);
});
});
group('external tables', () {
final options = parse('''
sqlite:
known_tables:
- "CREATE TABLE external (foo TEXT, bar INTEGER) STRICT;"
''');
test('available in drift file', () async {
final test = await TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE internal (
foo TEXT REFERENCES external(foo)
);
''',
}, options: options);
await test.analyze('package:a/a.drift');
test.expectNoErrors();
});
test('available in dart file', () async {
final test = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
class Internal extends Table {
TextColumn get foo => text().nullable().customConstraint('REFERENCES external(foo)')();
}
''',
}, options: options);
await test.analyze('package:a/a.dart');
test.expectNoErrors();
});
});
}
@@ -0,0 +1,115 @@
import 'package:test/test.dart';
import 'test_utils.dart';
void main() {
test('analyzes broken fts5 query', () async {
// Regression test for https://github.com/simolus3/drift/issues/3674. The
// broken "SELECT highlighted_text" in the FinalRankedIDs query used to get
// resolved against itself due to a broken scope, which then causes an
// infinite loop in drift. So if this can be analyzed in finite time we can
// consider the bug fixed.
final state = await TestBackend.inTest({
'a|lib/query.drift': '''
dictionary_search_fts5_drift:
-- Use CTEs to find, categorize, and rank all matching IDs
WITH RankedIDs AS (
-- Find matches in 'term' (Kanji) - Column Priority 1
SELECT
TB3T.id as term_bank_id,
rank,
TT.term as matched_text,
highlight(term_fts, 0, '', '') AS matched_text,
-- Categorize the match type
CASE
WHEN TT.term = :query THEN 1 -- 1. Exact Match
WHEN TT.term LIKE :query || '%' THEN 2 -- 2. Prefix Match
ELSE 3 -- 3. Infix/N-gram/Other
END AS match_type_priority,
1 AS match_column_priority
FROM term_fts AS FTS
JOIN term_bank_v3_table AS TB3T ON FTS.rowid = TB3T.term_id
JOIN term_table AS TT ON FTS.rowid = TT.id
WHERE term_fts MATCH :fts_query
UNION ALL
-- Find matches in 'reading' - Column Priority 2
SELECT
TB3T.id as term_bank_id,
rank,
RT.reading as matched_text,
highlight(reading_fts, 0, '', '') AS highlighted_text,
CASE
WHEN RT.reading = :query THEN 1
WHEN RT.reading LIKE :query || '%' THEN 2
ELSE 3
END AS match_type_priority,
2 AS match_column_priority
FROM reading_fts AS FTS
JOIN term_bank_v3_table AS TB3T ON FTS.rowid = TB3T.reading_id
JOIN reading_table AS RT ON FTS.rowid = RT.id
WHERE reading_fts MATCH :fts_query
UNION ALL
-- Find matches in 'definition' - Column Priority 3
SELECT
R.term_bank_id,
rank,
DT.definition as matched_text,
highlight(definition_fts, 0, '', '') AS highlighted_text,
CASE
WHEN DT.definition = :query THEN 1
WHEN DT.definition LIKE :query || '%' THEN 2
ELSE 3
END AS match_type_priority,
3 AS match_column_priority
FROM definition_fts AS FTS
JOIN term_bank_v3_x_definition_table AS R ON FTS.rowid = R.definition_id
JOIN definition_table AS DT ON FTS.rowid = DT.id
WHERE definition_fts MATCH :fts_query
),
FinalRankedIDs AS (
-- For each term, select the single best match based on the priority system.
SELECT
term_bank_id,
rank as best_rank,
match_type_priority,
match_column_priority,
highlighted_text
FROM (
SELECT
*,
ROW_NUMBER() OVER(
PARTITION BY term_bank_id
ORDER BY
match_type_priority, -- First, by the quality of the match
match_column_priority, -- Then, by which column was matched
rank -- Finally, by FTS5's own relevance score
) as rn
FROM RankedIDs
)
WHERE rn = 1
)
-- Finally, join the best-ranked IDs with the main view to get the full data
SELECT
R.best_rank as fts5_rank,
highlighted_text,
R.match_type_priority,
R.match_column_priority,
V.*
FROM term_bank_v3_search_view AS V
JOIN FinalRankedIDs AS R ON V.id = R.term_bank_id
ORDER BY
R.match_type_priority, -- 1. Sort by Match Type (Exact > Prefix > Other)
R.match_column_priority, -- 2. Sort by Match Column (Kanji > Reading > Definition)
V.popularity DESC, -- 4. Sort by result frequency/popularity
R.best_rank; -- 3. Sort by FTS5 relevance
''',
});
await state.driver.fullyAnalyze(Uri.parse('package:a/query.drift'));
});
}
@@ -1,5 +1,6 @@
import 'package:build_test/build_test.dart';
import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../../../utils.dart';
@@ -32,13 +33,13 @@ class MyTable extends Table {
expect(indexA.table, table);
expect(indexA.unique, false);
expect(indexA.indexedColumns, [table.columnBySqlName['a']]);
expect(indexA.indexedColumns, [indexedColumn('a')]);
expect(indexBC.table, table);
expect(indexBC.unique, true);
expect(indexBC.indexedColumns, [
table.columnBySqlName['b'],
table.columnBySqlName['c'],
indexedColumn('b'),
indexedColumn('c'),
]);
});
@@ -61,6 +62,28 @@ class MyTable extends Table {
]);
});
test('reads ordering mode', () async {
final backend = await TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
@TableIndex(columns: {IndexedColumn(#a, orderBy: OrderingMode.desc)}, name: 'tbl_a')
class MyTable extends Table {
IntColumn get a => integer()();
TextColumn get b => text()();
TextColumn get c => text()();
}
''',
});
final file = await backend.analyze('package:a/a.dart');
backend.expectNoErrors();
final indexA = file.analysis[file.id('tbl_a')]!.result as DriftIndex;
expect(indexA.indexedColumns,
[indexedColumn('a', ordering: OrderingMode.descending)]);
});
group('SQL', () {
test('can create index', () async {
final results = await emulateDriftBuild(
@@ -124,3 +147,10 @@ class MyTable extends Table {
});
});
}
TypeMatcher<DriftIndexedColumn> indexedColumn(String name,
{OrderingMode? ordering}) {
return isA<DriftIndexedColumn>()
.having((e) => e.column.nameInSql, 'nameInSql', name)
.having((e) => e.orderBy, 'orderBy', ordering);
}
@@ -0,0 +1,58 @@
import 'package:drift_dev/src/analysis/options.dart';
import 'package:test/test.dart';
import '../../test_utils.dart';
void main() {
test('can define triggers referencing powersync vtab', () async {
final state = await TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE todo_lists (
id TEXT NOT NULL PRIMARY KEY,
created_by TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT
) STRICT;
CREATE TRIGGER todo_lists_insert
AFTER INSERT ON todo_lists
FOR EACH ROW
BEGIN
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PUT', NEW.id, 'todo_lists', json_object(
'created_by', NEW.created_by,
'title', NEW.title,
'content', NEW.content
));
END;
CREATE TRIGGER todo_lists_update
AFTER UPDATE ON todo_lists
FOR EACH ROW
BEGIN
SELECT CASE
WHEN (OLD.id != NEW.id)
THEN RAISE (FAIL, 'Cannot update id')
END;
-- TODO: You may want to replace the json_object with a powersync_diff call of the old and new values, or
-- use your own diff logic to avoid marking unchanged columns as updated.
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PATCH', NEW.id, 'todo_lists', json_object(
'created_by', NEW.created_by,
'title', NEW.title,
'content', NEW.content
));
END;
CREATE TRIGGER todo_lists_delete
AFTER DELETE ON todo_lists
FOR EACH ROW
BEGIN
INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'todo_lists');
END;
'''
}, options: DriftOptions.defaults(modules: [SqlModule.powersync]));
await state.analyze('package:a/main.drift');
state.expectNoErrors();
});
}
@@ -76,6 +76,53 @@ targets:
.file('app/lib/db.steps.dart', IsValidDartFile(anything))
.validate();
});
test('without tests', () async {
project = await TestDriftProject.create([
d.dir('lib', [d.file('db.dart', _dbContent)]),
d.file('build.yaml', """
targets:
\$default:
builders:
drift_dev:
options:
databases:
my_database: lib/db.dart""")
]);
await project.runDriftCli(['make-migrations']);
expect(
d
.file('app/drift_schemas/my_database/drift_schema_v1.json')
.io
.existsSync(),
true);
// No other files should be created for 1st version
expect(d.file('app/test').io.existsSync(), false);
// Change the db schema and bump the version
File(p.join(project.root.path, 'lib/db.dart'))
.writeAsStringSync(_dbWithNewColumnBump);
await project.runDriftCli(['make-migrations', '--no-test']);
expect(
d
.file('app/drift_schemas/my_database/drift_schema_v2.json')
.io
.existsSync(),
true);
// Test files should be created
await d.dir('app/test/drift/my_database', [
// No test file, option was disabled
d.nothing('migration_test.dart'),
d.file('generated/schema.dart', IsValidDartFile(anything)),
d.file('generated/schema_v1.dart', IsValidDartFile(anything)),
d.file('generated/schema_v2.dart', IsValidDartFile(anything)),
]).validate();
// Steps file should be created
await d
.file('app/lib/db.steps.dart', IsValidDartFile(anything))
.validate();
});
test('schema_dir is respected', () async {
project = await TestDriftProject.create([
d.dir('lib', [d.file('db.dart', _dbContent)]),
@@ -198,8 +198,92 @@ class Database {}
'sql':
'CREATE VIEW IF NOT EXISTS "my_view" ("id", "id1", "id2") AS SELECT "t0"."id" AS "id", "t1"."id" AS "id1", "t2"."id" AS "id2" FROM "my_table" "t0" INNER JOIN "my_table" "t1" ON "t1"."id" = "t0"."id" INNER JOIN "my_table" "t2" ON "t2"."id" = "t0"."id"',
'dart_info_name': r'$MyViewView',
'columns': anything,
'columns': everyElement(containsPair('dsl_features', [
// Views should include generated_as information about each column.
containsPair('generated_as', anything),
])),
});
final reader = SchemaReader.readJson(schemaJson);
final fakeBuildConfig = runInBuildConfigZone(() {
return BuildConfig(buildTargets: {});
}, 'drift_dev', []);
final generated = await GenerateUtils.generateSchemaCode(
DriftDevCli()
..project = DriftProject(fakeBuildConfig, Directory(sandbox)),
1,
ExportedSchema(reader.entities.toList(), {}),
false,
false,
);
// The generated_as constraint is not applicable for views when we generate
// columns, so it shouldn't be generated there.
expect(generated, isNot(contains('GeneratedAs')));
});
test('can export Dart-defined index', () async {
final backend = await TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
@TableIndex(name: 'tbl_name', columns: {IndexedColumn(#name, orderBy: OrderingMode.desc)})
class MyTable extends Table {
IntColumn get id => integer()();
TextColumn get name => text()();
}
@DriftDatabase(tables: [MyTable])
class Database {}
''',
});
final file = await backend.analyze('package:a/main.dart');
backend.expectNoErrors();
final db = file.fileAnalysis!.resolvedDatabases.values.single;
final schemaJson =
await SchemaWriter(db.availableElements).createSchemaJson();
final serializedIndex = (schemaJson['entities'] as List)[1];
expect(serializedIndex['data'], {
'on': 0,
'name': 'tbl_name',
'sql': null,
'unique': false,
'columns': [
{'column': 'name', 'order_by': 'descending'}
]
});
final newFormat = schemaJson;
// Older versions of drift_dev used to only include the column name in the
// schema and did not support specifying an ordering mode.
final oldFormat = json.decode(json.encode(newFormat));
oldFormat['entities'][1]['data']['columns'] = ['name'];
for (final format in [oldFormat, newFormat]) {
final reader = SchemaReader.readJson(format as Map<String, Object?>);
final fakeBuildConfig = runInBuildConfigZone(() {
return BuildConfig(buildTargets: {});
}, 'drift_dev', []);
final generated = await GenerateUtils.generateSchemaCode(
DriftDevCli()
..project = DriftProject(fakeBuildConfig, Directory(sandbox)),
1,
ExportedSchema(reader.entities.toList(), {}),
false,
false,
);
if (format == newFormat) {
expect(generated,
contains('CREATE INDEX tbl_name ON my_table (name DESC)'));
} else {
expect(generated, contains('CREATE INDEX tbl_name ON my_table (name)'));
}
}
});
group('generates correct datetime mode', () {
+1 -1
View File
@@ -17,7 +17,7 @@ environment:
sdk: ">=3.5.0 <4.0.0"
dependencies:
drift: ^2.21.0
drift: ^2.28.2
flutter:
sdk: flutter
meta: ^1.3.0
+1
View File
@@ -136,6 +136,7 @@ void main() {
// Should be reflected here!
await expectLater(stream, emits(hasLength(1)));
stream.cancel();
await database.close();
});
@@ -1,4 +1,5 @@
import 'package:devtools_app_shared/utils.dart';
import 'package:drift_devtools_extension/src/download_button.dart';
import 'package:drift_devtools_extension/src/schema_validator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -23,6 +24,11 @@ final loadedDatabase = FutureProvider.autoDispose((ref) async {
return null;
});
final supportedFeatures = FutureProvider.autoDispose((ref) async {
final db = await ref.watch(loadedDatabase.future);
return await db?.getSupportedFeatures();
});
class DatabaseDetails extends ConsumerStatefulWidget {
const DatabaseDetails({super.key});
@@ -44,6 +50,7 @@ class _DatabaseDetailsState extends ConsumerState<DatabaseDetails> {
@override
Widget build(BuildContext context) {
final database = ref.watch(loadedDatabase);
final features = ref.watch(supportedFeatures);
return database.when(
loading: () => const Center(child: CircularProgressIndicator()),
@@ -54,38 +61,50 @@ class _DatabaseDetailsState extends ConsumerState<DatabaseDetails> {
final textTheme = theme.textTheme;
return Theme(
data: theme.copyWith(
scrollbarTheme: const ScrollbarThemeData(
thumbVisibility: WidgetStatePropertyAll(true),
data: theme.copyWith(
scrollbarTheme: const ScrollbarThemeData(
thumbVisibility: WidgetStatePropertyAll(true),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Padding(
padding: EdgeInsets.all(8),
child: DatabaseSchemaCheck(),
),
const Padding(
padding: EdgeInsets.all(8),
child: Row(
children: [
ClearDatabaseButton(),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Padding(
padding: EdgeInsets.all(8),
child: DatabaseSchemaCheck(),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text('Database viewer', style: textTheme.headlineMedium),
],
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
DownloadDatabaseButton(
isEnabled: features.asData
?.value?["isExportSupported"] as bool? ??
false,
),
],
),
),
),
Expanded(child: DatabaseViewer(database: database)),
],
),
);
const Padding(
padding: EdgeInsets.all(8),
child: Row(
children: [
ClearDatabaseButton(),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text('Database viewer',
style: textTheme.headlineMedium),
],
),
),
Expanded(child: DatabaseViewer(database: database)),
],
));
} else {
return const SizedBox.shrink();
}
@@ -0,0 +1,63 @@
import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:web/web.dart';
import 'details.dart';
class DownloadDatabaseButton extends ConsumerStatefulWidget {
final bool isEnabled;
const DownloadDatabaseButton({
super.key,
this.isEnabled = false,
});
@override
ConsumerState<DownloadDatabaseButton> createState() =>
_DownloadDatabaseButtonState();
}
class _DownloadDatabaseButtonState
extends ConsumerState<DownloadDatabaseButton> {
Future<void>? _pendingDownload;
void saveFile(String databaseName, String encodedData) {
// adapted from https://github.com/flutter/devtools/blob/98a52a86231144d8ed31cf9ab7e2165a8f23f925/packages/devtools_app/lib/src/shared/config_specific/import_export/_export_web.dart#L16
// ideally, this would use ExportController. see: https://github.com/flutter/devtools/issues/9451
final element = document.createElement('a') as HTMLAnchorElement;
element.setAttribute(
'href', "data:application/octet-stream;base64,$encodedData");
element.setAttribute('download', "$databaseName.sqlite");
element.style.display = 'none';
(document.body as HTMLBodyElement).append(element);
element.click();
element.remove();
}
@override
Widget build(BuildContext context) {
return DevToolsButton(
tooltip: widget.isEnabled
? null
: "Downloading is not enabled on this platform",
onPressed: _pendingDownload != null || !widget.isEnabled
? null
: () {
setState(() {
_pendingDownload = Future(() async {
final database = ref.read(loadedDatabase);
final res = await database.value!.download();
saveFile(res["database"]!, res["data"]!);
})
.whenComplete(() => setState(() => _pendingDownload = null))
.then((_) {});
});
},
label: 'Download database',
icon: Icons.download,
);
}
}
@@ -84,6 +84,17 @@ class RemoteDatabase {
await _driftRequest('clear');
}
Future<Map<String, dynamic>> getSupportedFeatures() async {
final res = await _driftRequest('get-supported-features');
return (res as Map).cast();
}
Future<Map<String, String>> download() async {
final res = await _driftRequest('download');
return (res as Map).cast();
}
Future<int> _newTableSubscription() async {
final result = await _driftRequest('subscribe-to-tables');
return result as int;
@@ -34,6 +34,7 @@ void main() {
});
_addCallbackForWebDriver('open', _open);
_addCallbackForWebDriver('close', (arg) async {
await tableUpdates?.cancel();
await openedDatabase?.close();
return null;
});
+5
View File
@@ -1,3 +1,8 @@
## 0.42.0-dev
- Fix unresolved references in CTEs resolving against the scope of the main query.
- Static analysis support for the PowerSync SQLite extension.
## 0.41.2
- Improve error message on unknown columns when it looks like the identifier
+1
View File
@@ -10,6 +10,7 @@ export 'src/engine/module/fts5.dart' show Fts5Extension, Fts5Table;
export 'src/engine/module/geopoly.dart' show GeopolyExtension;
export 'src/engine/module/json1.dart' show Json1Extension;
export 'src/engine/module/math.dart' show BuiltInMathExtension;
export 'src/engine/module/powersync.dart' show PowerSyncSqliteExtension;
export 'src/engine/module/rtree.dart' show RTreeExtension;
export 'src/engine/module/spellfix1.dart' show Spellfix1Extension;
export 'src/engine/module/module.dart';
@@ -173,6 +173,9 @@ mixin _HasParentScope on ReferenceScope {
class StatementScope extends ReferenceScope with _HasParentScope {
final ReferenceScope parent;
/// Only used for debugging purposes.
final AstNode statement;
@override
get _parentScopeForLookups => parent;
@@ -206,7 +209,7 @@ class StatementScope extends ReferenceScope with _HasParentScope {
@override
List<Column>? expansionOfStarColumn;
StatementScope(this.parent);
StatementScope(this.parent, this.statement);
@override
Iterable<ResultSetAvailableInStatement> get resultSetAvailableToChildScopes {
@@ -286,7 +289,7 @@ class StatementScope extends ReferenceScope with _HasParentScope {
}
factory StatementScope.forStatement(RootScope root, Statement statement) {
return StatementScope(statement.optionalScope ?? root);
return StatementScope(statement.optionalScope ?? root, statement);
}
static StatementScope cast(ReferenceScope other) {
@@ -75,14 +75,18 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
// (SELECT * FROM demo i WHERE i.id = d1.id) d2;"
// it won't work.
final isInFROM = e.parent is Queryable;
StatementScope scope;
if (isInFROM) {
if (e.parent is Queryable) {
final surroundingSelect = e.parents
.firstWhere((node) => node is HasFrom)
.scope as StatementScope;
scope = StatementScope(SourceScope(surroundingSelect));
scope = StatementScope(SourceScope(surroundingSelect), e);
} else if (e.parent case final CommonTableExpression cte) {
final withClause = cte.parent!;
assert(withClause is WithClause);
scope = StatementScope(
SourceScope(withClause.parent!.scope as StatementScope), e);
} else {
scope = StatementScope.forStatement(context.rootScope, e);
}
@@ -140,7 +144,7 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
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);
e.scope = StatementScope(e.scope, e);
visitChildren(e, arg);
}
@@ -0,0 +1,95 @@
import 'package:sqlparser/src/analysis/types/types.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/engine/sql_engine.dart';
import '../../analysis/analysis.dart';
import 'module.dart';
/// Provides static analysis support for the [PowerSync SQLite core extension].
///
/// This provides the `powersync_crud` eponymous virtual table to store local
/// mutation as well as some JSON / UUID helper functions.
///
/// [PowerSync SQLite core extension]: https://github.com/powersync-ja/powersync-sqlite-core/
final class PowerSyncSqliteExtension implements Extension {
const PowerSyncSqliteExtension();
@override
void register(SqlEngine engine) {
engine
..registerTable(_powersyncCrudLegacy)
..registerTable(_powersyncCrud)
..registerFunctionHandler(const _PowerSyncFunctionHandler());
}
}
final class _PowerSyncFunctionHandler implements FunctionHandler {
const _PowerSyncFunctionHandler();
@override
Set<String> get functionNames => {
'powersync_diff', // (TEXT, TEXT) -> TEXT
'powersync_client_id', // () -> TEXT
'powersync_in_sync_operation', // () -> BOOLEAN
'gen_random_uuid', // () -> TEXT
'uuid', // () -> TEXT
};
@override
ResolveResult inferArgumentType(
TypeInferenceSession session, SqlInvocation call, Expression argument) {
switch (call.name.toLowerCase()) {
case 'powersync_diff':
return ResolveResult(_text);
default:
return ResolveResult.unknown();
}
}
@override
ResolveResult inferReturnType(TypeInferenceSession session,
SqlInvocation call, List<Typeable> expandedArgs) {
switch (call.name.toLowerCase()) {
case 'powersync_diff':
case 'powersync_client_id':
case 'gen_random_uuid':
case 'uuid':
return const ResolveResult(_text);
case 'powersync_in_sync_operation':
return const ResolveResult(ResolvedType.bool());
default:
return ResolveResult.unknown();
}
}
@override
void reportErrors(SqlInvocation call, AnalysisContext context) {}
}
/// `CREATE TABLE powersync_crud_(data TEXT, options INT HIDDEN);`
///
/// Source: https://github.com/powersync-ja/powersync-sqlite-core/blob/637fda0f1d84c46964736a4f8c59fab5ae27e304/crates/core/src/crud_vtab.rs#L26
Table get _powersyncCrudLegacy {
return Table(name: 'powersync_crud', isVirtual: true, resolvedColumns: [
TableColumn('data', _text),
TableColumn('options', _int, isHidden: true),
]);
}
/// `CREATE TABLE powersync_crud(op TEXT, id TEXT, type TEXT, data TEXT, old_values TEXT, metadata TEXT, options INT HIDDEN);`
///
/// Source: https://github.com/powersync-ja/powersync-sqlite-core/blob/637fda0f1d84c46964736a4f8c59fab5ae27e304/crates/core/src/crud_vtab.rs#L27C6-L27C126
Table get _powersyncCrud {
return Table(name: 'powersync_crud', isVirtual: true, resolvedColumns: [
TableColumn('op', _text),
TableColumn('id', _text),
TableColumn('type', _text),
TableColumn('data', _text),
TableColumn('old_values', _text),
TableColumn('metadata', _text),
TableColumn('options', _int, isHidden: true),
]);
}
const _text = ResolvedType(type: BasicType.text);
const _int = ResolvedType(type: BasicType.int);
+1 -1
View File
@@ -1,6 +1,6 @@
name: sqlparser
description: Parses sqlite statements and performs static analysis on them
version: 0.41.2
version: 0.42.0-dev
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
repository: https://github.com/simolus3/drift
#homepage: https://drift.simonbinder.eu/
@@ -450,4 +450,16 @@ CREATE TABLE routes (
message: contains('Note: Double-quotes define an identifier in SQL.'),
);
});
test("common table expressions don't see columns from main query", () {
final result = SqlEngine().analyze('''
WITH users AS (SELECT 1, bar)
SELECT *, 'foo' as bar FROM users;
''');
result.expectError(
'bar',
type: AnalysisErrorType.referencedUnknownColumn,
);
});
}
@@ -0,0 +1,99 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../../analysis/data.dart';
import '../../analysis/errors/utils.dart';
void main() {
final options = EngineOptions(
version: SqliteVersion.current,
enabledExtensions: [const PowerSyncSqliteExtension()]);
late SqlEngine engine;
setUp(() {
engine = SqlEngine(options)..registerTableFromSql('''
CREATE TABLE todo_lists (
id TEXT NOT NULL PRIMARY KEY,
created_by TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT
) STRICT;
''');
});
group('crud vtab via triggers', () {
// This captures suggestions from https://docs.powersync.com/usage/use-case-examples/raw-tables#capture-local-writes-with-triggers
test('insert', () {
final result = engine.analyze('''
CREATE TRIGGER todo_lists_insert
AFTER INSERT ON todo_lists
FOR EACH ROW
BEGIN
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PUT', NEW.id, 'todo_lists', json_object(
'created_by', NEW.created_by,
'title', NEW.title,
'content', NEW.content
));
END;
''');
result.expectNoError();
});
test('update', () {
final result = engine.analyze('''
CREATE TRIGGER todo_lists_update
AFTER UPDATE ON todo_lists
FOR EACH ROW
BEGIN
SELECT CASE
WHEN (OLD.id != NEW.id)
THEN RAISE (FAIL, 'Cannot update id')
END;
-- TODO: You may want to replace the json_object with a powersync_diff call of the old and new values, or
-- use your own diff logic to avoid marking unchanged columns as updated.
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PATCH', NEW.id, 'todo_lists', json_object(
'created_by', NEW.created_by,
'title', NEW.title,
'content', NEW.content
));
END;
''');
result.expectNoError();
});
test('delete', () {
final result = engine.analyze('''
CREATE TRIGGER todo_lists_delete
AFTER DELETE ON todo_lists
FOR EACH ROW
BEGIN
INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'todo_lists');
END;
''');
result.expectNoError();
});
});
test('infers function return type', () {
final functions = {
"powersync_diff('{}', '{}')": BasicType.text,
'powersync_client_id()': BasicType.text,
'powersync_in_sync_operation()': BasicType.int,
'gen_random_uuid()': BasicType.text,
'uuid()': BasicType.text,
};
functions.forEach((call, type) {
final analyzed = engine.analyze('SELECT $call;');
analyzed.expectNoError();
final [column] = (analyzed.root as SelectStatement).resolvedColumns!;
expect(analyzed.typeOf(column).type?.type, type,
reason: '$call should resolve to $type');
});
});
}