mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
Merge remote-tracking branch 'origin/develop' into jaspr-docs
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -31,7 +31,7 @@ targets:
|
||||
named_parameters: true
|
||||
fatal_warnings: true
|
||||
sql:
|
||||
dialect: sqlite
|
||||
dialects: [sqlite, postgres]
|
||||
options:
|
||||
version: "3.37"
|
||||
modules:
|
||||
|
||||
@@ -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?>>();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
],
|
||||
|
||||
@@ -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')();
|
||||
|
||||
@@ -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))
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -263,6 +263,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
stream.cancel();
|
||||
await db.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user