mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into web-v2
This commit is contained in:
commit
9f2405be26
|
@ -1,3 +1,9 @@
|
|||
## 2.8.1
|
||||
|
||||
- Performance improvement: Cache and re-use prepared statements - thanks to [@davidmartos96](https://github.com/davidmartos96/)
|
||||
- Fix a deadlock after rolling back a transaction in a remote isolate.
|
||||
- Remove unintended log messages when using `connectToDriftWorker`.
|
||||
|
||||
## 2.8.0
|
||||
|
||||
- Don't keep databases in an unusable state if the `setup` callback throws an
|
||||
|
|
|
@ -313,6 +313,11 @@ extension ComputeWithDriftIsolate<DB extends DatabaseConnectionUser> on DB {
|
|||
/// [computeWithDatabase] is beneficial when an an expensive work unit needs
|
||||
/// to use the database, or when creating the SQL statements itself is
|
||||
/// expensive.
|
||||
/// In particular, note that [computeWithDatabase] does not create a second
|
||||
/// database connection to sqlite3 - the current one is re-used. So if you're
|
||||
/// using a synchronous database connection, using this method is unlikely to
|
||||
/// take significant loads off the main isolate. For that reason, the use of
|
||||
/// `NativeDatabase.createInBackground` is encouraged.
|
||||
@experimental
|
||||
Future<Ret> computeWithDatabase<Ret>({
|
||||
required FutureOr<Ret> Function(DB) computation,
|
||||
|
|
|
@ -48,9 +48,19 @@ class NativeDatabase extends DelegatedDatabase {
|
|||
/// add custom user-defined sql functions or to provide encryption keys in
|
||||
/// SQLCipher implementations.
|
||||
/// {@endtemplate}
|
||||
factory NativeDatabase(File file,
|
||||
{bool logStatements = false, DatabaseSetup? setup}) {
|
||||
return NativeDatabase._(_NativeDelegate(file, setup), logStatements);
|
||||
factory NativeDatabase(
|
||||
File file, {
|
||||
bool logStatements = false,
|
||||
DatabaseSetup? setup,
|
||||
bool cachePreparedStatements = true,
|
||||
}) {
|
||||
return NativeDatabase._(
|
||||
_NativeDelegate(
|
||||
file,
|
||||
setup,
|
||||
cachePreparedStatements,
|
||||
),
|
||||
logStatements);
|
||||
}
|
||||
|
||||
/// Creates a database storing its result in [file].
|
||||
|
@ -102,9 +112,15 @@ class NativeDatabase extends DelegatedDatabase {
|
|||
/// Creates an in-memory database won't persist its changes on disk.
|
||||
///
|
||||
/// {@macro drift_vm_database_factory}
|
||||
factory NativeDatabase.memory(
|
||||
{bool logStatements = false, DatabaseSetup? setup}) {
|
||||
return NativeDatabase._(_NativeDelegate(null, setup), logStatements);
|
||||
factory NativeDatabase.memory({
|
||||
bool logStatements = false,
|
||||
DatabaseSetup? setup,
|
||||
bool cachePreparedStatements = true,
|
||||
}) {
|
||||
return NativeDatabase._(
|
||||
_NativeDelegate(null, setup, cachePreparedStatements),
|
||||
logStatements,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a drift executor for an opened [database] from the `sqlite3`
|
||||
|
@ -119,12 +135,20 @@ class NativeDatabase extends DelegatedDatabase {
|
|||
/// internally when running [integration tests for migrations](https://drift.simonbinder.eu/docs/advanced-features/migrations/#verifying-migrations).
|
||||
///
|
||||
/// {@macro drift_vm_database_factory}
|
||||
factory NativeDatabase.opened(Database database,
|
||||
{bool logStatements = false,
|
||||
DatabaseSetup? setup,
|
||||
bool closeUnderlyingOnClose = true}) {
|
||||
factory NativeDatabase.opened(
|
||||
Database database, {
|
||||
bool logStatements = false,
|
||||
DatabaseSetup? setup,
|
||||
bool closeUnderlyingOnClose = true,
|
||||
bool cachePreparedStatements = true,
|
||||
}) {
|
||||
return NativeDatabase._(
|
||||
_NativeDelegate.opened(database, setup, closeUnderlyingOnClose),
|
||||
_NativeDelegate.opened(
|
||||
database,
|
||||
setup,
|
||||
closeUnderlyingOnClose,
|
||||
cachePreparedStatements,
|
||||
),
|
||||
logStatements);
|
||||
}
|
||||
|
||||
|
@ -181,12 +205,24 @@ class NativeDatabase extends DelegatedDatabase {
|
|||
class _NativeDelegate extends Sqlite3Delegate<Database> {
|
||||
final File? file;
|
||||
|
||||
_NativeDelegate(this.file, DatabaseSetup? setup) : super(setup);
|
||||
_NativeDelegate(this.file, DatabaseSetup? setup, bool cachePreparedStatements)
|
||||
: super(
|
||||
setup,
|
||||
cachePreparedStatements: cachePreparedStatements,
|
||||
);
|
||||
|
||||
_NativeDelegate.opened(
|
||||
Database db, DatabaseSetup? setup, bool closeUnderlyingWhenClosed)
|
||||
: file = null,
|
||||
super.opened(db, setup, closeUnderlyingWhenClosed);
|
||||
Database db,
|
||||
DatabaseSetup? setup,
|
||||
bool closeUnderlyingWhenClosed,
|
||||
bool cachePreparedStatements,
|
||||
) : file = null,
|
||||
super.opened(
|
||||
db,
|
||||
setup,
|
||||
closeUnderlyingWhenClosed,
|
||||
cachePreparedStatements: cachePreparedStatements,
|
||||
);
|
||||
|
||||
@override
|
||||
Database openDatabase() {
|
||||
|
@ -242,6 +278,8 @@ class _NativeDelegate extends Sqlite3Delegate<Database> {
|
|||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await super.close();
|
||||
|
||||
if (closeUnderlyingWhenClosed) {
|
||||
try {
|
||||
tracker.markClosed(database);
|
||||
|
|
|
@ -209,19 +209,24 @@ class ServerImplementation implements DriftServer {
|
|||
);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case TransactionControl.commit:
|
||||
await executor.send();
|
||||
break;
|
||||
case TransactionControl.rollback:
|
||||
switch (action) {
|
||||
case TransactionControl.commit:
|
||||
await executor.send();
|
||||
// The transaction should only be released if the commit doesn't throw.
|
||||
_releaseExecutor(executorId!);
|
||||
break;
|
||||
case TransactionControl.rollback:
|
||||
// Rollbacks shouldn't fail. Other parts of drift assume the transaction
|
||||
// to be over after a rollback either way, so we always release the
|
||||
// executor in this case.
|
||||
try {
|
||||
await executor.rollback();
|
||||
break;
|
||||
default:
|
||||
assert(false, 'Unknown TransactionControl');
|
||||
}
|
||||
} finally {
|
||||
_releaseExecutor(executorId!);
|
||||
} finally {
|
||||
_releaseExecutor(executorId!);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
assert(false, 'Unknown TransactionControl');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -343,12 +343,17 @@ abstract class DatabaseConnectionUser {
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Creates a custom select statement from the given sql [query]. To run the
|
||||
/// query once, use [Selectable.get]. For an auto-updating streams, set the
|
||||
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
|
||||
/// know the query will never emit more than one row, you can also use
|
||||
/// `getSingle` and `SelectableUtils.watchSingle` which return the item
|
||||
/// directly without wrapping it into a list.
|
||||
/// Creates a custom select statement from the given sql [query].
|
||||
///
|
||||
/// The query can be run once by calling [Selectable.get].
|
||||
///
|
||||
/// For an auto-updating query stream, the [readsFrom] parameter needs to be
|
||||
/// set to the tables the SQL statement reads from - drift can't infer it
|
||||
/// automatically like for other queries constructed with its Dart API.
|
||||
/// When, [Selectable.watch] can be used to construct an updating stream.
|
||||
///
|
||||
/// For queries that are known to only return a single row,
|
||||
/// [Selectable.getSingle] and [Selectable.watchSingle] can be used as well.
|
||||
///
|
||||
/// If you use variables in your query (for instance with "?"), they will be
|
||||
/// bound to the [variables] you specify on this query.
|
||||
|
|
|
@ -80,8 +80,6 @@ class StreamQueryStore {
|
|||
final StreamController<Set<TableUpdate>> _tableUpdates =
|
||||
StreamController.broadcast(sync: true);
|
||||
|
||||
StreamQueryStore();
|
||||
|
||||
/// Creates a new stream from the select statement.
|
||||
Stream<List<Map<String, Object?>>> registerStream(
|
||||
QueryStreamFetcher fetcher) {
|
||||
|
|
|
@ -65,7 +65,10 @@ class Variable<T extends Object> extends Expression<T> {
|
|||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
if (!context.supportsVariables) {
|
||||
if (!context.supportsVariables ||
|
||||
// Workaround for https://github.com/simolus3/drift/issues/2441
|
||||
// Binding nulls on postgres is currently untyped which causes issues.
|
||||
(value == null && context.dialect == SqlDialect.postgres)) {
|
||||
// Write as constant instead.
|
||||
Constant<T>(value).writeInto(context);
|
||||
return;
|
||||
|
|
|
@ -15,6 +15,7 @@ import 'package:drift/src/runtime/exceptions.dart';
|
|||
import 'package:drift/src/runtime/executor/stream_queries.dart';
|
||||
import 'package:drift/src/runtime/types/converters.dart';
|
||||
import 'package:drift/src/runtime/types/mapping.dart';
|
||||
import 'package:drift/src/utils/async_map.dart';
|
||||
import 'package:drift/src/utils/single_transformer.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
|
|
|
@ -267,7 +267,7 @@ abstract mixin class Selectable<T>
|
|||
/// Maps this selectable by the [mapper] function.
|
||||
///
|
||||
/// Like [map] just async.
|
||||
Selectable<N> asyncMap<N>(Future<N> Function(T) mapper) {
|
||||
Selectable<N> asyncMap<N>(FutureOr<N> Function(T) mapper) {
|
||||
return _AsyncMappedSelectable<T, N>(this, mapper);
|
||||
}
|
||||
}
|
||||
|
@ -293,7 +293,7 @@ class _MappedSelectable<S, T> extends Selectable<T> {
|
|||
|
||||
class _AsyncMappedSelectable<S, T> extends Selectable<T> {
|
||||
final Selectable<S> _source;
|
||||
final Future<T> Function(S) _mapper;
|
||||
final FutureOr<T> Function(S) _mapper;
|
||||
|
||||
_AsyncMappedSelectable(this._source, this._mapper);
|
||||
|
||||
|
@ -304,11 +304,13 @@ class _AsyncMappedSelectable<S, T> extends Selectable<T> {
|
|||
|
||||
@override
|
||||
Stream<List<T>> watch() {
|
||||
return _source.watch().asyncMap(_mapResults);
|
||||
return AsyncMapPerSubscription(_source.watch())
|
||||
.asyncMapPerSubscription(_mapResults);
|
||||
}
|
||||
|
||||
Future<List<T>> _mapResults(List<S> results) async =>
|
||||
[for (final result in results) await _mapper(result)];
|
||||
Future<List<T>> _mapResults(List<S> results) async {
|
||||
return [for (final result in results) await _mapper(result)];
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixin for a [Query] that operates on a single primary table only.
|
||||
|
|
|
@ -73,7 +73,7 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
|||
key: StreamKey(query.sql, query.boundVariables),
|
||||
);
|
||||
|
||||
return database.createStream(fetcher).asyncMap(_mapResponse);
|
||||
return database.createStream(fetcher).asyncMapPerSubscription(_mapResponse);
|
||||
}
|
||||
|
||||
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
||||
|
|
|
@ -233,7 +233,7 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
|
||||
return database
|
||||
.createStream(fetcher)
|
||||
.asyncMap((rows) => _mapResponse(ctx, rows));
|
||||
.asyncMapPerSubscription((rows) => _mapResponse(ctx, rows));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@internal
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:sqlite3/common.dart';
|
||||
|
@ -24,6 +25,9 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
|||
|
||||
final void Function(DB)? _setup;
|
||||
|
||||
/// Whether prepared statements should be cached.
|
||||
final bool cachePreparedStatements;
|
||||
|
||||
/// Whether the [database] should be closed when [close] is called on this
|
||||
/// instance.
|
||||
///
|
||||
|
@ -31,13 +35,22 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
|||
/// connections to the same database.
|
||||
final bool closeUnderlyingWhenClosed;
|
||||
|
||||
final PreparedStatementsCache _preparedStmtsCache = PreparedStatementsCache();
|
||||
|
||||
/// A delegate that will call [openDatabase] to open the database.
|
||||
Sqlite3Delegate(this._setup) : closeUnderlyingWhenClosed = true;
|
||||
Sqlite3Delegate(
|
||||
this._setup, {
|
||||
required this.cachePreparedStatements,
|
||||
}) : closeUnderlyingWhenClosed = true;
|
||||
|
||||
/// A delegate using an underlying sqlite3 database object that has already
|
||||
/// been opened.
|
||||
Sqlite3Delegate.opened(
|
||||
this._database, this._setup, this.closeUnderlyingWhenClosed) {
|
||||
this._database,
|
||||
this._setup,
|
||||
this.closeUnderlyingWhenClosed, {
|
||||
required this.cachePreparedStatements,
|
||||
}) {
|
||||
_initializeDatabase();
|
||||
}
|
||||
|
||||
|
@ -68,6 +81,10 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
|||
_database?.dispose();
|
||||
_database = null;
|
||||
|
||||
// We can call clear instead of disposeAll because disposing the
|
||||
// database will also dispose all prepared statements on it.
|
||||
_preparedStmtsCache.clear();
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +102,12 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
|||
_hasInitializedDatabase = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
Future<void> close() {
|
||||
return Future(_preparedStmtsCache.disposeAll);
|
||||
}
|
||||
|
||||
/// Synchronously prepares and runs [statements] collected from a batch.
|
||||
@protected
|
||||
void runBatchSync(BatchedStatements statements) {
|
||||
|
@ -114,23 +137,32 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
|||
if (args.isEmpty) {
|
||||
database.execute(statement);
|
||||
} else {
|
||||
final stmt = database.prepare(statement, checkNoTail: true);
|
||||
try {
|
||||
stmt.execute(args);
|
||||
} finally {
|
||||
stmt.dispose();
|
||||
}
|
||||
final stmt = _getPreparedStatement(statement);
|
||||
stmt.execute(args);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
||||
final stmt = database.prepare(statement, checkNoTail: true);
|
||||
try {
|
||||
final result = stmt.select(args);
|
||||
return QueryResult.fromRows(result.toList());
|
||||
} finally {
|
||||
stmt.dispose();
|
||||
final stmt = _getPreparedStatement(statement);
|
||||
final result = stmt.select(args);
|
||||
return QueryResult.fromRows(result.toList());
|
||||
}
|
||||
|
||||
CommonPreparedStatement _getPreparedStatement(String sql) {
|
||||
if (cachePreparedStatements) {
|
||||
final cachedStmt = _preparedStmtsCache.use(sql);
|
||||
if (cachedStmt != null) {
|
||||
return cachedStmt;
|
||||
}
|
||||
|
||||
final stmt = database.prepare(sql, checkNoTail: true);
|
||||
_preparedStmtsCache.addNew(sql, stmt);
|
||||
|
||||
return stmt;
|
||||
} else {
|
||||
final stmt = database.prepare(sql, checkNoTail: true);
|
||||
return stmt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,3 +181,62 @@ class _SqliteVersionDelegate extends DynamicVersionDelegate {
|
|||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache of prepared statements to avoid having to parse SQL statements
|
||||
/// multiple time when they're used frequently.
|
||||
@internal
|
||||
class PreparedStatementsCache {
|
||||
/// The maximum amount of cached statements.
|
||||
final int maxSize;
|
||||
|
||||
// The linked map returns entries in the order in which they have been
|
||||
// inserted (with the first insertion being reported first).
|
||||
// So, we treat it as a LRU cache with `entries.last` being the MRU and
|
||||
// `entries.last` being the LRU element.
|
||||
final LinkedHashMap<String, CommonPreparedStatement> _cachedStatements =
|
||||
LinkedHashMap();
|
||||
|
||||
/// Create a cache of prepared statements with a capacity of [maxSize].
|
||||
PreparedStatementsCache({this.maxSize = 64});
|
||||
|
||||
/// Attempts to look up the cached [sql] statement, if it exists.
|
||||
///
|
||||
/// If the statement exists, it is marked as most recently used as well.
|
||||
CommonPreparedStatement? use(String sql) {
|
||||
// Remove and add the statement if it was found to move it to the end,
|
||||
// which marks it as the MRU element.
|
||||
final foundStatement = _cachedStatements.remove(sql);
|
||||
|
||||
if (foundStatement != null) {
|
||||
_cachedStatements[sql] = foundStatement;
|
||||
}
|
||||
|
||||
return foundStatement;
|
||||
}
|
||||
|
||||
/// Caches a statement that has not been cached yet for subsequent uses.
|
||||
void addNew(String sql, CommonPreparedStatement statement) {
|
||||
assert(!_cachedStatements.containsKey(sql));
|
||||
|
||||
if (_cachedStatements.length == maxSize) {
|
||||
final lru = _cachedStatements.remove(_cachedStatements.keys.first)!;
|
||||
lru.dispose();
|
||||
}
|
||||
|
||||
_cachedStatements[sql] = statement;
|
||||
}
|
||||
|
||||
/// Removes all cached statements.
|
||||
void disposeAll() {
|
||||
for (final statement in _cachedStatements.values) {
|
||||
statement.dispose();
|
||||
}
|
||||
|
||||
_cachedStatements.clear();
|
||||
}
|
||||
|
||||
/// Forgets cached statements without explicitly disposing them.
|
||||
void clear() {
|
||||
_cachedStatements.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import 'dart:async';
|
||||
|
||||
/// Extension to make the drift-specific version of [asyncMap] available.
|
||||
extension AsyncMapPerSubscription<S> on Stream<S> {
|
||||
/// A variant of [Stream.asyncMap] that forwards each subscription of the
|
||||
/// returned stream to the source (`this`).
|
||||
///
|
||||
/// The `asyncMap` implementation from the SDK uses a broadcast controller
|
||||
/// when given an input stream that [Stream.isBroadcast]. As broadcast
|
||||
/// controllers only call `onListen` once, these subscriptions aren't
|
||||
/// forwarded to the original stream.
|
||||
///
|
||||
/// Drift query streams send the current snapshot to each attaching listener,
|
||||
/// a behavior that is lost when wrapping these streams in a broadcast stream
|
||||
/// controller. Since we need the behavior of `asyncMap` internally though, we
|
||||
/// re-implement it in a simple variant that transforms each subscription
|
||||
/// individually.
|
||||
Stream<T> asyncMapPerSubscription<T>(Future<T> Function(S) mapper) {
|
||||
return Stream.multi(
|
||||
(listener) {
|
||||
late StreamSubscription<S> subscription;
|
||||
|
||||
void onData(S original) {
|
||||
subscription.pause();
|
||||
mapper(original)
|
||||
.then(listener.addSync, onError: listener.addErrorSync)
|
||||
.whenComplete(subscription.resume);
|
||||
}
|
||||
|
||||
subscription = listen(
|
||||
onData,
|
||||
onError: listener.addErrorSync,
|
||||
onDone: listener.closeSync,
|
||||
cancelOnError: false, // Determined by downstream subscription
|
||||
);
|
||||
|
||||
listener
|
||||
..onPause = subscription.pause
|
||||
..onResume = subscription.resume
|
||||
..onCancel = subscription.cancel;
|
||||
},
|
||||
isBroadcast: isBroadcast,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -56,9 +56,12 @@ class WasmDatabase extends DelegatedDatabase {
|
|||
WasmDatabaseSetup? setup,
|
||||
IndexedDbFileSystem? fileSystem,
|
||||
bool logStatements = false,
|
||||
bool cachePreparedStatements = true,
|
||||
}) {
|
||||
return WasmDatabase._(
|
||||
_WasmDelegate(sqlite3, path, setup, fileSystem), logStatements);
|
||||
_WasmDelegate(sqlite3, path, setup, fileSystem, cachePreparedStatements),
|
||||
logStatements,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates an in-memory database in the loaded [sqlite3] database.
|
||||
|
@ -66,9 +69,12 @@ class WasmDatabase extends DelegatedDatabase {
|
|||
CommonSqlite3 sqlite3, {
|
||||
WasmDatabaseSetup? setup,
|
||||
bool logStatements = false,
|
||||
bool cachePreparedStatements = true,
|
||||
}) {
|
||||
return WasmDatabase._(
|
||||
_WasmDelegate(sqlite3, null, setup, null), logStatements);
|
||||
_WasmDelegate(sqlite3, null, setup, null, cachePreparedStatements),
|
||||
logStatements,
|
||||
);
|
||||
}
|
||||
|
||||
/// For an in-depth
|
||||
|
@ -110,8 +116,15 @@ class _WasmDelegate extends Sqlite3Delegate<CommonDatabase> {
|
|||
final IndexedDbFileSystem? _fileSystem;
|
||||
|
||||
_WasmDelegate(
|
||||
this._sqlite3, this._path, WasmDatabaseSetup? setup, this._fileSystem)
|
||||
: super(setup);
|
||||
this._sqlite3,
|
||||
this._path,
|
||||
WasmDatabaseSetup? setup,
|
||||
this._fileSystem,
|
||||
bool cachePreparedStatements,
|
||||
) : super(
|
||||
setup,
|
||||
cachePreparedStatements: cachePreparedStatements,
|
||||
);
|
||||
|
||||
@override
|
||||
CommonDatabase openDatabase() {
|
||||
|
@ -163,6 +176,8 @@ class _WasmDelegate extends Sqlite3Delegate<CommonDatabase> {
|
|||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await super.close();
|
||||
|
||||
if (closeUnderlyingWhenClosed) {
|
||||
database.dispose();
|
||||
await _flush();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift
|
||||
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||
version: 2.8.0
|
||||
version: 2.8.1
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/runtime/api/runtime_api.dart';
|
||||
import 'package:drift/src/runtime/executor/stream_queries.dart';
|
||||
|
@ -177,13 +178,13 @@ void main() {
|
|||
await first.first; // will listen to stream, then cancel
|
||||
await pumpEventQueue(times: 1); // give cancel event time to propagate
|
||||
|
||||
final checkEmits =
|
||||
expectLater(second, emitsInOrder([<Object?>[], <Object?>[]]));
|
||||
final listener = StreamQueue(second);
|
||||
await expectLater(listener, emits(isEmpty));
|
||||
|
||||
db.markTablesUpdated({db.users});
|
||||
await pumpEventQueue(times: 1);
|
||||
await expectLater(listener, emits(isEmpty));
|
||||
|
||||
await checkEmits;
|
||||
await listener.cancel();
|
||||
});
|
||||
|
||||
test('same stream instance can be listened to multiple times', () async {
|
||||
|
|
|
@ -216,7 +216,7 @@ class TodoDb extends _$TodoDb {
|
|||
DriftDatabaseOptions options = const DriftDatabaseOptions();
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int schemaVersion = 1;
|
||||
}
|
||||
|
||||
@DriftAccessor(
|
||||
|
|
|
@ -1846,9 +1846,9 @@ class AllTodosWithCategoryResult extends CustomResultSet {
|
|||
|
||||
mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
$SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos;
|
||||
$CategoriesTable get categories => attachedDatabase.categories;
|
||||
$TodosTableTable get todosTable => attachedDatabase.todosTable;
|
||||
$SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos;
|
||||
$TodoWithCategoryViewView get todoWithCategoryView =>
|
||||
attachedDatabase.todoWithCategoryView;
|
||||
Selectable<TodoEntry> todosForUser({required int user}) {
|
||||
|
|
|
@ -420,6 +420,47 @@ void main() {
|
|||
expect(db.validateDatabaseSchema(), completes);
|
||||
});
|
||||
});
|
||||
|
||||
test('custom schema upgrades', () async {
|
||||
// I promised this would work in https://github.com/simolus3/drift/discussions/2436,
|
||||
// so we better make sure this keeps working.
|
||||
final underlying = sqlite3.openInMemory()
|
||||
..execute('pragma user_version = 1;');
|
||||
addTearDown(underlying.dispose);
|
||||
|
||||
const maxSchema = 10;
|
||||
final expectedException = Exception('schema upgrade!');
|
||||
|
||||
for (var currentSchema = 1; currentSchema < maxSchema; currentSchema++) {
|
||||
final db = TodoDb(NativeDatabase.opened(underlying));
|
||||
db.schemaVersion = maxSchema;
|
||||
db.migration = MigrationStrategy(
|
||||
onUpgrade: expectAsync3((m, from, to) async {
|
||||
// This upgrade callback does one step and then throws. Opening the
|
||||
// database multiple times should run the individual migrations.
|
||||
expect(from, currentSchema);
|
||||
expect(to, maxSchema);
|
||||
|
||||
await db.customStatement('CREATE TABLE t$from (id INTEGER);');
|
||||
await db.customStatement('pragma user_version = ${from + 1}');
|
||||
|
||||
if (from != to - 1) {
|
||||
// Simulate a failed upgrade
|
||||
throw expectedException;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (currentSchema != maxSchema - 1) {
|
||||
// Opening the database should throw this exception
|
||||
await expectLater(
|
||||
db.customSelect('SELECT 1').get(), throwsA(expectedException));
|
||||
} else {
|
||||
// The last migration should work
|
||||
await expectLater(db.customSelect('SELECT 1').get(), completes);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class _TestDatabase extends GeneratedDatabase {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@TestOn('vm')
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/isolate.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
@ -8,11 +10,12 @@ import '../test_utils/database_vm.dart';
|
|||
void main() {
|
||||
preferLocalSqlite3();
|
||||
|
||||
test('a failing commit does not block the whole database', () async {
|
||||
final db = _Database(NativeDatabase.memory());
|
||||
addTearDown(db.close);
|
||||
group('a failing commit does not block the whole database', () {
|
||||
Future<void> testWith(QueryExecutor executor) async {
|
||||
final db = _Database(executor);
|
||||
addTearDown(db.close);
|
||||
|
||||
await db.customStatement('''
|
||||
await db.customStatement('''
|
||||
CREATE TABLE IF NOT EXISTS todo_items (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL, content TEXT NULL,
|
||||
|
@ -22,27 +25,38 @@ CREATE TABLE IF NOT EXISTS todo_items (
|
|||
GENERATED ALWAYS AS (title || ' (' || content || ')') VIRTUAL
|
||||
);
|
||||
''');
|
||||
await db.customStatement('''
|
||||
await db.customStatement('''
|
||||
CREATE TABLE IF NOT EXISTS todo_categories (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
''');
|
||||
await db.customStatement('PRAGMA foreign_keys = ON;');
|
||||
await db.customStatement('PRAGMA foreign_keys = ON;');
|
||||
|
||||
await expectLater(
|
||||
db.transaction(() async {
|
||||
// Thanks to the deferrable clause, this statement will only cause a
|
||||
// failing COMMIT.
|
||||
await db.customStatement(
|
||||
'INSERT INTO todo_items (title, category_id) VALUES (?, ?);',
|
||||
['a', 100]);
|
||||
}),
|
||||
throwsA(isA<SqliteException>()),
|
||||
);
|
||||
await expectLater(
|
||||
db.transaction(() async {
|
||||
// Thanks to the deferrable clause, this statement will only cause a
|
||||
// failing COMMIT.
|
||||
await db.customStatement(
|
||||
'INSERT INTO todo_items (title, category_id) VALUES (?, ?);',
|
||||
['a', 100]);
|
||||
}),
|
||||
throwsA(anyOf(isA<SqliteException>(), isA<DriftRemoteException>())),
|
||||
);
|
||||
|
||||
expect(
|
||||
db.customSelect('SELECT * FROM todo_items').get(), completion(isEmpty));
|
||||
expect(db.customSelect('SELECT * FROM todo_items').get(),
|
||||
completion(isEmpty));
|
||||
}
|
||||
|
||||
test('sync client', () async {
|
||||
await testWith(NativeDatabase.memory());
|
||||
});
|
||||
|
||||
test('through isolate', () async {
|
||||
final isolate = await DriftIsolate.spawn(NativeDatabase.memory);
|
||||
|
||||
await testWith(await isolate.connect(singleClientMode: true));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:drift/native.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
for (final suspendBetweenListeners in [true, false]) {
|
||||
for (final asyncMap in [true, false]) {
|
||||
test(
|
||||
'suspendBetweenListeners=$suspendBetweenListeners, asyncMap=$asyncMap',
|
||||
() async {
|
||||
final db = TestDb();
|
||||
final select = db.customSelect('select 1');
|
||||
final stream = asyncMap
|
||||
? select.asyncMap(Future.value).watch()
|
||||
: select.map((row) => row).watch();
|
||||
|
||||
final log = <Object>[];
|
||||
stream.listen(log.add);
|
||||
if (suspendBetweenListeners) await pumpEventQueue();
|
||||
stream.listen(log.add);
|
||||
await pumpEventQueue();
|
||||
expect(log, hasLength(2));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestDb extends GeneratedDatabase {
|
||||
TestDb() : super(NativeDatabase.memory());
|
||||
@override
|
||||
final List<TableInfo> allTables = const [];
|
||||
@override
|
||||
final int schemaVersion = 1;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
@TestOn('vm')
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift/src/sqlite3/database.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../generated/todos.dart';
|
||||
import '../../test_utils/database_vm.dart';
|
||||
|
||||
void main() {
|
||||
preferLocalSqlite3();
|
||||
|
||||
test("lru/mru order and remove callback", () {
|
||||
final cache = PreparedStatementsCache(maxSize: 3);
|
||||
final database = sqlite3.openInMemory();
|
||||
addTearDown(database.dispose);
|
||||
|
||||
expect(cache.use('SELECT 1'), isNull);
|
||||
cache.addNew('SELECT 1', database.prepare('SELECT 1'));
|
||||
cache.addNew('SELECT 2', database.prepare('SELECT 2'));
|
||||
cache.addNew('SELECT 3', database.prepare('SELECT 3'));
|
||||
|
||||
expect(cache.use('SELECT 3'), isNotNull);
|
||||
expect(cache.use('SELECT 1'), isNotNull);
|
||||
|
||||
// Inserting another statement should remove #2, which is now the LRU
|
||||
cache.addNew('SELECT 4', database.prepare('SELECT 4'));
|
||||
expect(cache.use('SELECT 2'), isNull);
|
||||
expect(cache.use('SELECT 1'), isNotNull);
|
||||
});
|
||||
|
||||
test('returns new columns after recompilation', () async {
|
||||
// https://github.com/simolus3/drift/issues/2454
|
||||
final db = TodoDb(NativeDatabase.memory(cachePreparedStatements: true));
|
||||
|
||||
await db.customStatement('create table t (c1)');
|
||||
await db.customInsert('insert into t values (1)');
|
||||
|
||||
final before = await db.customSelect('select * from t').getSingle();
|
||||
expect(before.data, {'c1': 1});
|
||||
|
||||
await db.customStatement('alter table t add column c2');
|
||||
|
||||
final after = await db.customSelect('select * from t').getSingle();
|
||||
expect(after.data, {'c1': 1, 'c2': null});
|
||||
});
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
// Mocks generated by Mockito 5.4.0 from annotations
|
||||
// Mocks generated by Mockito 5.4.1 from annotations
|
||||
// in drift/test/test_utils/test_utils.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// @dart=2.19
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i4;
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
## 2.8.3
|
||||
|
||||
- Allow Dart-defined tables to reference imported tables through SQL
|
||||
[#2433](https://github.com/simolus3/drift/issues/2433).
|
||||
|
||||
## 2.8.2
|
||||
|
||||
- Fix generated to write qualified column references for Dart components in
|
||||
|
|
|
@ -63,8 +63,12 @@ class DriftAnalysisCache {
|
|||
yield found;
|
||||
|
||||
for (final imported in found.imports ?? const <Uri>[]) {
|
||||
if (seenUris.add(imported)) {
|
||||
pending.add(knownFiles[imported]!);
|
||||
// We might not have a known file for all imports of a Dart file, since
|
||||
// not all imports are drift-related there.
|
||||
final knownImport = knownFiles[imported];
|
||||
|
||||
if (seenUris.add(imported) && knownImport != null) {
|
||||
pending.add(knownImport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ class DriftAnalysisDriver {
|
|||
final DriftBackend backend;
|
||||
final DriftAnalysisCache cache = DriftAnalysisCache();
|
||||
final DriftOptions options;
|
||||
final bool _isTesting;
|
||||
|
||||
late final TypeMapping typeMapping = TypeMapping(this);
|
||||
late final ElementDeserializer deserializer = ElementDeserializer(this);
|
||||
|
@ -64,7 +65,8 @@ class DriftAnalysisDriver {
|
|||
|
||||
KnownDriftTypes? _knownTypes;
|
||||
|
||||
DriftAnalysisDriver(this.backend, this.options);
|
||||
DriftAnalysisDriver(this.backend, this.options, {bool isTesting = false})
|
||||
: _isTesting = isTesting;
|
||||
|
||||
SqlEngine newSqlEngine() {
|
||||
return SqlEngine(
|
||||
|
@ -155,12 +157,16 @@ class DriftAnalysisDriver {
|
|||
}
|
||||
|
||||
/// Runs the first step (element discovery) on a file with the given [uri].
|
||||
Future<FileState> prepareFileForAnalysis(Uri uri,
|
||||
{bool needsDiscovery = true}) async {
|
||||
Future<FileState> prepareFileForAnalysis(
|
||||
Uri uri, {
|
||||
bool needsDiscovery = true,
|
||||
bool warnIfFileDoesntExist = true,
|
||||
}) async {
|
||||
var known = cache.knownFiles[uri] ?? cache.notifyFileChanged(uri);
|
||||
|
||||
if (known.discovery == null && needsDiscovery) {
|
||||
await DiscoverStep(this, known).discover();
|
||||
await DiscoverStep(this, known)
|
||||
.discover(warnIfFileDoesntExist: warnIfFileDoesntExist);
|
||||
cache.postFileDiscoveryResults(known);
|
||||
|
||||
// todo: Mark elements that need to be analyzed again
|
||||
|
@ -184,6 +190,13 @@ class DriftAnalysisDriver {
|
|||
);
|
||||
}
|
||||
}
|
||||
} else if (state is DiscoveredDartLibrary) {
|
||||
for (final import in state.importDependencies) {
|
||||
// We might import a generated file that doesn't exist yet, that
|
||||
// should not be a user-visible error. Users will notice because the
|
||||
// import is reported as an error by the analyzer either way.
|
||||
await prepareFileForAnalysis(import, warnIfFileDoesntExist: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,6 +220,8 @@ class DriftAnalysisDriver {
|
|||
} catch (e, s) {
|
||||
if (e is! CouldNotResolveElementException) {
|
||||
backend.log.warning('Could not analyze ${discovered.ownId}', e, s);
|
||||
|
||||
if (_isTesting) rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,10 +129,17 @@ class DriftFileImport {
|
|||
class DiscoveredDartLibrary extends DiscoveredFileState {
|
||||
final LibraryElement library;
|
||||
|
||||
@override
|
||||
final List<Uri> importDependencies;
|
||||
|
||||
@override
|
||||
bool get isValidImport => true;
|
||||
|
||||
DiscoveredDartLibrary(this.library, super.locallyDefinedElements);
|
||||
DiscoveredDartLibrary(
|
||||
this.library,
|
||||
super.locallyDefinedElements,
|
||||
this.importDependencies,
|
||||
);
|
||||
}
|
||||
|
||||
class NotADartLibrary extends DiscoveredFileState {
|
||||
|
|
|
@ -46,6 +46,9 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
}
|
||||
}
|
||||
|
||||
final tableConstraints =
|
||||
await _readCustomConstraints(references, columns, element);
|
||||
|
||||
final table = DriftTable(
|
||||
discovered.ownId,
|
||||
DriftDeclaration.dartElement(element),
|
||||
|
@ -60,7 +63,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
for (final uniqueKey in uniqueKeys ?? const <Set<DriftColumn>>[])
|
||||
UniqueColumns(uniqueKey),
|
||||
],
|
||||
overrideTableConstraints: await _readCustomConstraints(columns, element),
|
||||
overrideTableConstraints: tableConstraints,
|
||||
withoutRowId: await _overrideWithoutRowId(element) ?? false,
|
||||
);
|
||||
|
||||
|
@ -272,7 +275,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
return ColumnParser(this).parse(declaration, element);
|
||||
}
|
||||
|
||||
Future<List<String>> _readCustomConstraints(
|
||||
Future<List<String>> _readCustomConstraints(Set<DriftElement> references,
|
||||
List<DriftColumn> localColumns, ClassElement element) async {
|
||||
final customConstraints =
|
||||
element.lookUpGetter('customConstraints', element.library);
|
||||
|
@ -343,6 +346,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
(msg) => DriftAnalysisError.inDartAst(element, source, msg));
|
||||
|
||||
if (table != null) {
|
||||
references.add(table);
|
||||
final missingColumns = clause.columnNames
|
||||
.map((e) => e.columnName)
|
||||
.where((e) => !table.columnBySqlName.containsKey(e));
|
||||
|
|
|
@ -51,7 +51,7 @@ class DiscoverStep {
|
|||
return result;
|
||||
}
|
||||
|
||||
Future<void> discover() async {
|
||||
Future<void> discover({required bool warnIfFileDoesntExist}) async {
|
||||
final extension = _file.extension;
|
||||
_file.discovery = UnknownFile();
|
||||
|
||||
|
@ -61,7 +61,7 @@ class DiscoverStep {
|
|||
try {
|
||||
library = await _driver.backend.readDart(_file.ownUri);
|
||||
} catch (e) {
|
||||
if (e is! NotALibraryException) {
|
||||
if (e is! NotALibraryException && warnIfFileDoesntExist) {
|
||||
// Backends are supposed to throw NotALibraryExceptions if the
|
||||
// library is a part file. For other exceptions, we better report
|
||||
// the error.
|
||||
|
@ -77,8 +77,11 @@ class DiscoverStep {
|
|||
await finder.find();
|
||||
|
||||
_file.errorsDuringDiscovery.addAll(finder.errors);
|
||||
_file.discovery =
|
||||
DiscoveredDartLibrary(library, _checkForDuplicates(finder.found));
|
||||
_file.discovery = DiscoveredDartLibrary(
|
||||
library,
|
||||
_checkForDuplicates(finder.found),
|
||||
finder.imports,
|
||||
);
|
||||
break;
|
||||
case '.drift':
|
||||
case '.moor':
|
||||
|
@ -153,6 +156,8 @@ class _FindDartElements extends RecursiveElementVisitor<void> {
|
|||
final DiscoverStep _discoverStep;
|
||||
final LibraryElement _library;
|
||||
|
||||
final List<Uri> imports = [];
|
||||
|
||||
final TypeChecker _isTable, _isView, _isTableInfo, _isDatabase, _isDao;
|
||||
|
||||
final List<Future<void>> _pendingWork = [];
|
||||
|
@ -231,6 +236,18 @@ class _FindDartElements extends RecursiveElementVisitor<void> {
|
|||
super.visitClassElement(element);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitLibraryImportElement(LibraryImportElement element) {
|
||||
final imported = element.importedLibrary;
|
||||
|
||||
if (imported != null && !imported.isInSdk) {
|
||||
_pendingWork.add(Future(() async {
|
||||
final uri = await _discoverStep._driver.backend.uriOfDart(imported);
|
||||
imports.add(uri);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
String _defaultNameForTableOrView(ClassElement definingElement) {
|
||||
return _discoverStep._driver.options.caseFromDartToSql
|
||||
.apply(definingElement.name);
|
||||
|
|
|
@ -222,7 +222,8 @@ class DartTopLevelSymbol {
|
|||
|
||||
factory DartTopLevelSymbol.topLevelElement(Element element,
|
||||
[String? elementName]) {
|
||||
assert(element.library?.topLevelElements.contains(element) == true);
|
||||
assert(element.library?.topLevelElements.contains(element) == true,
|
||||
'${element.name} is not a top-level element');
|
||||
|
||||
// We're using this to recover the right import URI when using
|
||||
// `package:build`:
|
||||
|
@ -437,7 +438,7 @@ class _AddFromAst extends GeneralizingAstVisitor<void> {
|
|||
_AddFromAst(this._builder, this._excluding);
|
||||
|
||||
void _addTopLevelReference(Element? element, Token name2) {
|
||||
if (element == null) {
|
||||
if (element == null || (element.isSynthetic && element.library == null)) {
|
||||
_builder.addText(name2.lexeme);
|
||||
} else {
|
||||
_builder.addTopLevel(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift_dev
|
||||
description: Dev-dependency for users of drift. Contains the generator and development tools.
|
||||
version: 2.8.2
|
||||
version: 2.8.3
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
|
|
@ -364,6 +364,48 @@ class WithConstraints extends Table {
|
|||
]);
|
||||
});
|
||||
|
||||
test('can resolve references from import', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/topic.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'language.dart';
|
||||
|
||||
class Topics extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get langCode => text()();
|
||||
}
|
||||
|
||||
''',
|
||||
'a|lib/video.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'topic.dart';
|
||||
|
||||
class Videos extends Table {
|
||||
IntColumn get id => integer()();
|
||||
|
||||
IntColumn get topicId => integer()();
|
||||
TextColumn get topicLang => text()();
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
'FOREIGN KEY (topic_id, topic_lang) REFERENCES topics (id, lang_code)',
|
||||
];
|
||||
}
|
||||
''',
|
||||
});
|
||||
|
||||
final state = await backend.analyze('package:a/video.dart');
|
||||
backend.expectNoErrors();
|
||||
|
||||
final table = state.analyzedElements.single as DriftTable;
|
||||
expect(
|
||||
table.references,
|
||||
contains(isA<DriftTable>()
|
||||
.having((e) => e.schemaName, 'schemaName', 'topics')));
|
||||
});
|
||||
|
||||
test('supports autoIncrement on int64 columns', () async {
|
||||
final backend = TestBackend.inTest({
|
||||
'a|lib/a.dart': '''
|
||||
|
|
|
@ -45,7 +45,7 @@ class TestBackend extends DriftBackend {
|
|||
for (final entry in sourceContents.entries)
|
||||
AssetId.parse(entry.key).uri.toString(): entry.value,
|
||||
} {
|
||||
driver = DriftAnalysisDriver(this, options);
|
||||
driver = DriftAnalysisDriver(this, options, isTesting: true);
|
||||
}
|
||||
|
||||
factory TestBackend.inTest(
|
||||
|
|
|
@ -27,7 +27,3 @@ dev_dependencies:
|
|||
flutter:
|
||||
assets:
|
||||
- test_asset.db
|
||||
|
||||
dependency_overrides:
|
||||
# Flutter's test packages don't support the latest analyzer yet.
|
||||
test_api: ^0.4.16
|
||||
|
|
|
@ -5,7 +5,8 @@ publish_to: 'none'
|
|||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.6 <3.0.0"
|
||||
sdk: ">=2.17.6 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
drift: ^2.0.2+1
|
||||
sqlcipher_flutter_libs: ^0.5.1
|
||||
|
@ -24,7 +25,3 @@ dev_dependencies:
|
|||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
dependency_overrides:
|
||||
# Flutter's test packages don't support the latest analyzer yet.
|
||||
test_api: ^0.4.16
|
||||
|
|
|
@ -24,4 +24,8 @@ Future<void> main() async {
|
|||
}
|
||||
|
||||
output.writeAsStringSync(json.encode(tracker.timings));
|
||||
|
||||
// Make sure the process exits. Otherwise, unclosed resources from the
|
||||
// benchmarks will keep the process alive.
|
||||
exit(0);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:benchmark_harness/benchmark_harness.dart' show ScoreEmitter;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'src/moor/cache_prepared_statements.dart';
|
||||
import 'src/moor/key_value_insert.dart';
|
||||
import 'src/sqlite/bind_string.dart';
|
||||
import 'src/sqlparser/parse_drift_file.dart';
|
||||
|
@ -22,6 +23,9 @@ List<Reportable> allBenchmarks(ScoreEmitter emitter) {
|
|||
// sql parser
|
||||
ParseDriftFile(emitter),
|
||||
TokenizerBenchmark(emitter),
|
||||
// prepared statements cache
|
||||
CachedPreparedStatements(emitter),
|
||||
NonCachedPreparedStatements(emitter),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:benchmarks/benchmarks.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'database.dart';
|
||||
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
const int _numQueries = 100000;
|
||||
|
||||
const Uuid uuid = Uuid();
|
||||
|
||||
Future<void> _runQueries(Database _db) async {
|
||||
// a query with a subquery so that the query planner takes some time
|
||||
const queryToBench = '''
|
||||
SELECT * FROM key_values WHERE value IN (SELECT value FROM key_values WHERE value = ?);
|
||||
''';
|
||||
|
||||
final fs = <Future>[];
|
||||
|
||||
for (var i = 0; i < _numQueries; i++) {
|
||||
fs.add(
|
||||
_db.customSelect(queryToBench, variables: [Variable(uuid.v4())]).get(),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(fs);
|
||||
}
|
||||
|
||||
class CachedPreparedStatements extends AsyncBenchmarkBase {
|
||||
final _db = Database(cachePreparedStatements: true);
|
||||
|
||||
CachedPreparedStatements(ScoreEmitter emitter)
|
||||
: super('Running $_numQueries queries (cached prepared statements)',
|
||||
emitter);
|
||||
|
||||
@override
|
||||
Future<void> setup() async {
|
||||
// Empty db so that we mostly bench the prepared statement time
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
await _runQueries(_db);
|
||||
}
|
||||
}
|
||||
|
||||
class NonCachedPreparedStatements extends AsyncBenchmarkBase {
|
||||
final _db = Database(cachePreparedStatements: false);
|
||||
|
||||
NonCachedPreparedStatements(ScoreEmitter emitter)
|
||||
: super('Running $_numQueries queries (non-cached prepared statements)',
|
||||
emitter);
|
||||
|
||||
@override
|
||||
Future<void> setup() async {
|
||||
// Empty db so that we mostly bench the prepared statement time
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
await _runQueries(_db);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,10 @@ class KeyValues extends Table {
|
|||
|
||||
@DriftDatabase(tables: [KeyValues])
|
||||
class Database extends _$Database {
|
||||
Database() : super(_obtainExecutor());
|
||||
Database({bool cachePreparedStatements = true})
|
||||
: super(_obtainExecutor(
|
||||
cachePreparedStatements: cachePreparedStatements,
|
||||
));
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
@ -25,10 +28,15 @@ class Database extends _$Database {
|
|||
|
||||
const _uuid = Uuid();
|
||||
|
||||
QueryExecutor _obtainExecutor() {
|
||||
QueryExecutor _obtainExecutor({
|
||||
required bool cachePreparedStatements,
|
||||
}) {
|
||||
final file =
|
||||
File(p.join(Directory.systemTemp.path, 'drift_benchmarks', _uuid.v4()));
|
||||
file.parent.createSync();
|
||||
|
||||
return NativeDatabase(file);
|
||||
return NativeDatabase(
|
||||
file,
|
||||
cachePreparedStatements: cachePreparedStatements,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,3 @@ dev_dependencies:
|
|||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
|
||||
dependency_overrides:
|
||||
# Flutter's test packages don't support the latest analyzer yet.
|
||||
test_api: ^0.4.16
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
## 0.30.2
|
||||
|
||||
- Fix false-positive "unknown table" errors when the same table is used in a
|
||||
join with and then without an alias.
|
||||
|
||||
## 0.30.1
|
||||
|
||||
- Report syntax error for `WITH` clauses in triggers.
|
||||
|
||||
## 0.30.0
|
||||
|
||||
- Add `previous` and `next` fields for tokens
|
||||
|
|
|
@ -69,6 +69,7 @@ enum AnalysisErrorType {
|
|||
starColumnWithoutTable,
|
||||
compoundColumnCountMismatch,
|
||||
cteColumnCountMismatch,
|
||||
circularReference,
|
||||
valuesSelectCountMismatch,
|
||||
viewColumnNamesMismatch,
|
||||
rowValueMisuse,
|
||||
|
|
|
@ -45,7 +45,8 @@ abstract class ReferenceScope {
|
|||
/// This is useful to resolve qualified references (e.g. to resolve `foo.bar`
|
||||
/// the resolver would call [resolveResultSet]("foo") and then look up the
|
||||
/// `bar` column in that result set).
|
||||
ResultSetAvailableInStatement? resolveResultSet(String name) => null;
|
||||
ResultSetAvailableInStatement? resolveResultSetForReference(String name) =>
|
||||
null;
|
||||
|
||||
/// Adds an added result set to this scope.
|
||||
///
|
||||
|
@ -126,8 +127,8 @@ mixin _HasParentScope on ReferenceScope {
|
|||
_parentScopeForLookups.resultSetAvailableToChildScopes;
|
||||
|
||||
@override
|
||||
ResultSetAvailableInStatement? resolveResultSet(String name) =>
|
||||
_parentScopeForLookups.resolveResultSet(name);
|
||||
ResultSetAvailableInStatement? resolveResultSetForReference(String name) =>
|
||||
_parentScopeForLookups.resolveResultSetForReference(name);
|
||||
|
||||
@override
|
||||
ResultSet? resolveResultSetToAdd(String name) =>
|
||||
|
@ -219,8 +220,8 @@ class StatementScope extends ReferenceScope with _HasParentScope {
|
|||
}
|
||||
|
||||
@override
|
||||
ResultSetAvailableInStatement? resolveResultSet(String name) {
|
||||
return resultSets[name] ?? parent.resolveResultSet(name);
|
||||
ResultSetAvailableInStatement? resolveResultSetForReference(String name) {
|
||||
return resultSets[name] ?? parent.resolveResultSetForReference(name);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -279,12 +280,19 @@ class StatementScope extends ReferenceScope with _HasParentScope {
|
|||
}
|
||||
}
|
||||
|
||||
/// A special intermediate scope used for subqueries appearing in a `FROM`
|
||||
/// clause so that the subquery can't see outer columns and tables being added.
|
||||
class SubqueryInFromScope extends ReferenceScope with _HasParentScope {
|
||||
/// A special intermediate scope used for nodes that don't see columns and
|
||||
/// tables added to the statement they're in.
|
||||
///
|
||||
/// An example for this are subqueries appearing in a `FROM` clause, which can't
|
||||
/// see outer columns and tables of the select statement.
|
||||
///
|
||||
/// Another example is the [InsertStatement.source] of an [InsertStatement],
|
||||
/// which cannot refer to columns of the table being inserted to of course.
|
||||
/// Things like `INSERT INTO tbl (col) VALUES (tbl.col)` are not allowed.
|
||||
class SourceScope extends ReferenceScope with _HasParentScope {
|
||||
final StatementScope enclosingStatement;
|
||||
|
||||
SubqueryInFromScope(this.enclosingStatement);
|
||||
SourceScope(this.enclosingStatement);
|
||||
|
||||
@override
|
||||
RootScope get rootScope => enclosingStatement.rootScope;
|
||||
|
@ -321,8 +329,9 @@ class MiscStatementSubScope extends ReferenceScope with _HasParentScope {
|
|||
RootScope get rootScope => parent.rootScope;
|
||||
|
||||
@override
|
||||
ResultSetAvailableInStatement? resolveResultSet(String name) {
|
||||
return additionalResultSets[name] ?? parent.resolveResultSet(name);
|
||||
ResultSetAvailableInStatement? resolveResultSetForReference(String name) {
|
||||
return additionalResultSets[name] ??
|
||||
parent.resolveResultSetForReference(name);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -348,7 +357,7 @@ class SingleTableReferenceScope extends ReferenceScope {
|
|||
RootScope get rootScope => parent.rootScope;
|
||||
|
||||
@override
|
||||
ResultSetAvailableInStatement? resolveResultSet(String name) {
|
||||
ResultSetAvailableInStatement? resolveResultSetForReference(String name) {
|
||||
if (name == addedTableName) {
|
||||
return addedTable;
|
||||
} else {
|
||||
|
|
|
@ -11,17 +11,55 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
|
||||
@override
|
||||
void visitSelectStatement(SelectStatement e, ColumnResolverContext arg) {
|
||||
// visit children first so that common table expressions are resolved
|
||||
visitChildren(e, arg);
|
||||
e.withClause?.accept(this, arg);
|
||||
_resolveSelect(e, arg);
|
||||
|
||||
// We've handled the from clause in _resolveSelect, but we still need to
|
||||
// visit other children to handle things like subquery expressions.
|
||||
for (final child in e.childNodes) {
|
||||
if (child != e.withClause && child != e.from) {
|
||||
visit(child, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCreateIndexStatement(
|
||||
CreateIndexStatement e, ColumnResolverContext arg) {
|
||||
_handle(e.on, [], arg);
|
||||
visitExcept(e, e.on, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCreateTriggerStatement(
|
||||
CreateTriggerStatement e, ColumnResolverContext arg) {
|
||||
final table = _resolveTableReference(e.onTable, arg);
|
||||
if (table == null) {
|
||||
// further analysis is not really possible without knowing the table
|
||||
super.visitCreateTriggerStatement(e, arg);
|
||||
return;
|
||||
}
|
||||
|
||||
final scope = e.statementScope;
|
||||
|
||||
// Add columns of the target table for when and update of clauses
|
||||
scope.expansionOfStarColumn = table.resolvedColumns;
|
||||
|
||||
if (e.target.introducesNew) {
|
||||
scope.addAlias(e, table, 'new');
|
||||
}
|
||||
if (e.target.introducesOld) {
|
||||
scope.addAlias(e, table, 'old');
|
||||
}
|
||||
|
||||
visitChildren(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCompoundSelectStatement(
|
||||
CompoundSelectStatement e, ColumnResolverContext arg) {
|
||||
// first, visit all children so that the compound parts have their columns
|
||||
// resolved
|
||||
visitChildren(e, arg);
|
||||
e.base.accept(this, arg);
|
||||
visitList(e.additional, arg);
|
||||
|
||||
_resolveCompoundSelect(e);
|
||||
}
|
||||
|
@ -29,29 +67,75 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
@override
|
||||
void visitValuesSelectStatement(
|
||||
ValuesSelectStatement e, ColumnResolverContext arg) {
|
||||
// visit children to resolve CTEs
|
||||
visitChildren(e, arg);
|
||||
|
||||
e.withClause?.accept(this, arg);
|
||||
_resolveValuesSelect(e);
|
||||
|
||||
// Still visit expressions because they could have subqueries that we need
|
||||
// to handle.
|
||||
visitList(e.values, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCommonTableExpression(
|
||||
CommonTableExpression e, ColumnResolverContext arg) {
|
||||
visitChildren(
|
||||
e,
|
||||
const ColumnResolverContext(referencesUseNameOfReferencedColumn: false),
|
||||
// If we have a compound select statement as a CTE, resolve the initial
|
||||
// query first because the whole CTE will have those columns in the end.
|
||||
// This allows subsequent parts of the compound select to refer to the CTE.
|
||||
final query = e.as;
|
||||
final contextForFirstChild = ColumnResolverContext(
|
||||
referencesUseNameOfReferencedColumn: false,
|
||||
inDefinitionOfCte: [
|
||||
...arg.inDefinitionOfCte,
|
||||
e.cteTableName.toLowerCase(),
|
||||
],
|
||||
);
|
||||
|
||||
final resolved = e.as.resolvedColumns;
|
||||
final names = e.columnNames;
|
||||
if (names != null && resolved != null && names.length != resolved.length) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.cteColumnCountMismatch,
|
||||
message: 'This CTE declares ${names.length} columns, but its select '
|
||||
'statement actually returns ${resolved.length}.',
|
||||
relevantNode: e,
|
||||
));
|
||||
void applyColumns(BaseSelectStatement source) {
|
||||
final resolved = source.resolvedColumns!;
|
||||
final names = e.columnNames;
|
||||
|
||||
if (names == null) {
|
||||
e.resolvedColumns = resolved;
|
||||
} else {
|
||||
if (names.length != resolved.length) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.cteColumnCountMismatch,
|
||||
message:
|
||||
'This CTE declares ${names.length} columns, but its select '
|
||||
'statement actually returns ${resolved.length}.',
|
||||
relevantNode: e.tableNameToken ?? e,
|
||||
));
|
||||
}
|
||||
|
||||
final cteColumns = names
|
||||
.map((name) => CommonTableExpressionColumn(name)..containingSet = e)
|
||||
.toList();
|
||||
for (var i = 0; i < cteColumns.length; i++) {
|
||||
if (i < resolved.length) {
|
||||
final selectColumn = resolved[i];
|
||||
cteColumns[i].innerColumn = selectColumn;
|
||||
}
|
||||
}
|
||||
e.resolvedColumns = cteColumns;
|
||||
}
|
||||
}
|
||||
|
||||
if (query is CompoundSelectStatement) {
|
||||
// The first nested select statement determines the columns of this CTE.
|
||||
query.base.accept(this, contextForFirstChild);
|
||||
applyColumns(query.base);
|
||||
|
||||
// Subsequent queries can refer to the CTE though.
|
||||
final contextForOtherChildren = ColumnResolverContext(
|
||||
referencesUseNameOfReferencedColumn: false,
|
||||
inDefinitionOfCte: arg.inDefinitionOfCte,
|
||||
);
|
||||
|
||||
visitList(query.additional, contextForOtherChildren);
|
||||
_resolveCompoundSelect(query);
|
||||
} else {
|
||||
visitChildren(e, contextForFirstChild);
|
||||
applyColumns(query);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,10 +154,9 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
}
|
||||
|
||||
@override
|
||||
void visitTableReference(TableReference e, void arg) {
|
||||
if (e.resolved == null) {
|
||||
_resolveTableReference(e);
|
||||
}
|
||||
void visitForeignKeyClause(ForeignKeyClause e, ColumnResolverContext arg) {
|
||||
_resolveTableReference(e.foreignTable, arg);
|
||||
visitExcept(e, e.foreignTable, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -100,13 +183,15 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
_resolveReturningClause(e, e.table.resultSet, arg);
|
||||
}
|
||||
|
||||
ResultSet? _addIfResolved(AstNode node, TableReference ref) {
|
||||
final table = _resolveTableReference(ref);
|
||||
if (table != null) {
|
||||
node.statementScope.expansionOfStarColumn = table.resolvedColumns;
|
||||
}
|
||||
ResultSet? _addIfResolved(
|
||||
AstNode node, TableReference ref, ColumnResolverContext arg) {
|
||||
final availableColumns = <Column>[];
|
||||
_handle(ref, availableColumns, arg);
|
||||
|
||||
return table;
|
||||
final scope = node.statementScope;
|
||||
scope.expansionOfStarColumn = availableColumns;
|
||||
|
||||
return ref.resultSet;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -114,11 +199,11 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
// Resolve CTEs first
|
||||
e.withClause?.accept(this, arg);
|
||||
|
||||
final into = _addIfResolved(e, e.table);
|
||||
_handle(e.table, [], arg);
|
||||
for (final child in e.childNodes) {
|
||||
if (child != e.withClause) visit(child, arg);
|
||||
}
|
||||
_resolveReturningClause(e, into, arg);
|
||||
_resolveReturningClause(e, e.table.resultSet, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -126,7 +211,7 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
// Resolve CTEs first
|
||||
e.withClause?.accept(this, arg);
|
||||
|
||||
final from = _addIfResolved(e, e.from);
|
||||
final from = _addIfResolved(e, e.from, arg);
|
||||
for (final child in e.childNodes) {
|
||||
if (child != e.withClause) visit(child, arg);
|
||||
}
|
||||
|
@ -168,31 +253,10 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
stmt.returnedResultSet = CustomResultSet(columns);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCreateTriggerStatement(
|
||||
CreateTriggerStatement e, ColumnResolverContext arg) {
|
||||
final table = _resolveTableReference(e.onTable);
|
||||
if (table == null) {
|
||||
// further analysis is not really possible without knowing the table
|
||||
super.visitCreateTriggerStatement(e, arg);
|
||||
return;
|
||||
}
|
||||
|
||||
final scope = e.statementScope;
|
||||
|
||||
// Add columns of the target table for when and update of clauses
|
||||
scope.expansionOfStarColumn = table.resolvedColumns;
|
||||
|
||||
if (e.target.introducesNew) {
|
||||
scope.addAlias(e, table, 'new');
|
||||
}
|
||||
if (e.target.introducesOld) {
|
||||
scope.addAlias(e, table, 'old');
|
||||
}
|
||||
|
||||
visitChildren(e, arg);
|
||||
}
|
||||
|
||||
/// Visits a [queryable] appearing in a `FROM` clause under the state [state].
|
||||
///
|
||||
/// This also adds columns contributed to the resolved source to
|
||||
/// [availableColumns], which is later used to expand `*` parameters.
|
||||
void _handle(Queryable queryable, List<Column> availableColumns,
|
||||
ColumnResolverContext state) {
|
||||
void addColumns(Iterable<Column> columns) {
|
||||
|
@ -209,41 +273,52 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
}
|
||||
}
|
||||
|
||||
final scope = queryable.scope;
|
||||
|
||||
void markAvailableResultSet(
|
||||
Queryable source, ResolvesToResultSet resultSet, String? name) {
|
||||
final added = ResultSetAvailableInStatement(source, resultSet);
|
||||
|
||||
if (source is TableOrSubquery) {
|
||||
source.availableResultSet = added;
|
||||
}
|
||||
|
||||
scope.addResolvedResultSet(name, added);
|
||||
}
|
||||
|
||||
queryable.when(
|
||||
isTable: (table) {
|
||||
final resolved = _resolveTableReference(table);
|
||||
final resolved = _resolveTableReference(table, state);
|
||||
markAvailableResultSet(
|
||||
table, resolved ?? table, table.as ?? table.tableName);
|
||||
|
||||
if (resolved != null) {
|
||||
// an error will be logged when resolved is null, so the != null check
|
||||
// is fine and avoids crashes
|
||||
addColumns(table.resultSet!.resolvedColumns!);
|
||||
}
|
||||
},
|
||||
isSelect: (select) {
|
||||
markAvailableResultSet(select, select.statement, select.as);
|
||||
|
||||
// Inside subqueries, references don't take the name of the referenced
|
||||
// column.
|
||||
final childState =
|
||||
ColumnResolverContext(referencesUseNameOfReferencedColumn: false);
|
||||
|
||||
// the inner select statement doesn't have access to columns defined in
|
||||
// the outer statements, which is why we use _resolveSelect instead of
|
||||
// passing availableColumns down to a recursive call of _handle
|
||||
final childState = ColumnResolverContext(
|
||||
referencesUseNameOfReferencedColumn: false,
|
||||
inDefinitionOfCte: state.inDefinitionOfCte,
|
||||
);
|
||||
final stmt = select.statement;
|
||||
if (stmt is CompoundSelectStatement) {
|
||||
_resolveCompoundSelect(stmt);
|
||||
} else if (stmt is SelectStatement) {
|
||||
_resolveSelect(stmt, childState);
|
||||
} else if (stmt is ValuesSelectStatement) {
|
||||
_resolveValuesSelect(stmt);
|
||||
} else {
|
||||
throw AssertionError('Unknown type of select statement: $stmt');
|
||||
}
|
||||
|
||||
visit(stmt, childState);
|
||||
addColumns(stmt.resolvedColumns!);
|
||||
},
|
||||
isJoin: (join) {
|
||||
_handle(join.primary, availableColumns, state);
|
||||
for (final query in join.joins.map((j) => j.query)) {
|
||||
_handle(query, availableColumns, state);
|
||||
isJoin: (joinClause) {
|
||||
_handle(joinClause.primary, availableColumns, state);
|
||||
for (final join in joinClause.joins) {
|
||||
_handle(join.query, availableColumns, state);
|
||||
|
||||
final constraint = join.constraint;
|
||||
if (constraint is OnConstraint) {
|
||||
visit(constraint.expression, state);
|
||||
}
|
||||
}
|
||||
},
|
||||
isTableFunction: (function) {
|
||||
|
@ -251,6 +326,9 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
.engineOptions.addedTableFunctions[function.name.toLowerCase()];
|
||||
final resolved = handler?.resolveTableValued(context, function);
|
||||
|
||||
markAvailableResultSet(
|
||||
function, resolved ?? function, function.as ?? function.name);
|
||||
|
||||
if (resolved == null) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.unknownFunction,
|
||||
|
@ -290,7 +368,8 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
Iterable<Column>? visibleColumnsForStar;
|
||||
|
||||
if (resultColumn.tableName != null) {
|
||||
final tableResolver = scope.resolveResultSet(resultColumn.tableName!);
|
||||
final tableResolver =
|
||||
scope.resolveResultSetForReference(resultColumn.tableName!);
|
||||
if (tableResolver == null) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.referencedUnknownTable,
|
||||
|
@ -361,7 +440,8 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
}
|
||||
}
|
||||
} else if (resultColumn is NestedStarResultColumn) {
|
||||
final target = scope.resolveResultSet(resultColumn.tableName);
|
||||
final target =
|
||||
scope.resolveResultSetForReference(resultColumn.tableName);
|
||||
|
||||
if (target == null) {
|
||||
context.reportError(AnalysisError(
|
||||
|
@ -458,24 +538,26 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
|||
return span;
|
||||
}
|
||||
|
||||
ResultSet? _resolveTableReference(TableReference r) {
|
||||
ResultSet? _resolveTableReference(
|
||||
TableReference r, ColumnResolverContext state) {
|
||||
// Check for circular references
|
||||
if (state.inDefinitionOfCte.contains(r.tableName.toLowerCase())) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.circularReference,
|
||||
relevantNode: r,
|
||||
message: 'Circular reference to its own CTE',
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
final scope = r.scope;
|
||||
|
||||
// Try resolving to a top-level table in the schema and to a result set that
|
||||
// may have been added to the table
|
||||
final resolvedInSchema = scope.resolveResultSetToAdd(r.tableName);
|
||||
final resolvedInQuery = scope.resolveResultSet(r.tableName);
|
||||
final createdName = r.as;
|
||||
|
||||
// Prefer using a table that has already been added if this isn't the
|
||||
// definition of the added table reference
|
||||
if (resolvedInQuery != null && resolvedInQuery.origin != r) {
|
||||
final resolved = resolvedInQuery.resultSet.resultSet;
|
||||
if (resolved != null) {
|
||||
return r.resolved =
|
||||
createdName != null ? TableAlias(resolved, createdName) : resolved;
|
||||
}
|
||||
} else if (resolvedInSchema != null) {
|
||||
if (resolvedInSchema != null) {
|
||||
return r.resolved = createdName != null
|
||||
? TableAlias(resolvedInSchema, createdName)
|
||||
: resolvedInSchema;
|
||||
|
@ -528,6 +610,13 @@ class ColumnResolverContext {
|
|||
/// column in subqueries or CTEs.
|
||||
final bool referencesUseNameOfReferencedColumn;
|
||||
|
||||
const ColumnResolverContext(
|
||||
{this.referencesUseNameOfReferencedColumn = true});
|
||||
/// The common table expressions that are currently being defined.
|
||||
///
|
||||
/// This is used to detect forbidden circular references.
|
||||
final List<String> inDefinitionOfCte;
|
||||
|
||||
const ColumnResolverContext({
|
||||
this.referencesUseNameOfReferencedColumn = true,
|
||||
this.inDefinitionOfCte = const [],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -544,4 +544,17 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
|
|||
|
||||
visitChildren(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitWithClause(WithClause e, void arg) {
|
||||
if (_isInTopLevelTriggerStatement) {
|
||||
context.reportError(AnalysisError(
|
||||
type: AnalysisErrorType.synctactic,
|
||||
relevantNode: e.withToken ?? e,
|
||||
message: 'WITH clauses cannot appear in triggers.',
|
||||
));
|
||||
}
|
||||
|
||||
visitChildren(e, arg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,12 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
|||
resolveIndexOfVariables(_foundVariables);
|
||||
}
|
||||
|
||||
@override
|
||||
void defaultInsertSource(InsertSource e, void arg) {
|
||||
e.scope = SourceScope(e.parent!.statementScope);
|
||||
visitChildren(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCreateTableStatement(CreateTableStatement e, void arg) {
|
||||
final scope = e.scope = StatementScope.forStatement(context.rootScope, e);
|
||||
|
@ -52,6 +58,7 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
|||
// query: "SELECT * FROM demo d1,
|
||||
// (SELECT * FROM demo i WHERE i.id = d1.id) d2;"
|
||||
// it won't work.
|
||||
|
||||
final isInFROM = e.parent is Queryable;
|
||||
StatementScope scope;
|
||||
|
||||
|
@ -59,7 +66,7 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
|||
final surroundingSelect = e.parents
|
||||
.firstWhere((node) => node is HasFrom)
|
||||
.scope as StatementScope;
|
||||
scope = StatementScope(SubqueryInFromScope(surroundingSelect));
|
||||
scope = StatementScope(SourceScope(surroundingSelect));
|
||||
} else {
|
||||
scope = StatementScope.forStatement(context.rootScope, e);
|
||||
}
|
||||
|
@ -107,37 +114,6 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
|||
visitChildren(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void defaultQueryable(Queryable e, void arg) {
|
||||
final scope = e.scope;
|
||||
|
||||
e.when(
|
||||
isTable: (table) {
|
||||
final added = ResultSetAvailableInStatement(table, table);
|
||||
table.availableResultSet = added;
|
||||
|
||||
scope.addResolvedResultSet(table.as ?? table.tableName, added);
|
||||
},
|
||||
isSelect: (select) {
|
||||
final added = ResultSetAvailableInStatement(select, select.statement);
|
||||
select.availableResultSet = added;
|
||||
scope.addResolvedResultSet(select.as, added);
|
||||
},
|
||||
isJoin: (join) {
|
||||
// the join can contain multiple tables. Luckily for us, all of them are
|
||||
// Queryables, so we can deal with them by visiting the children and
|
||||
// dont't need to do anything here.
|
||||
},
|
||||
isTableFunction: (function) {
|
||||
final added = ResultSetAvailableInStatement(function, function);
|
||||
function.availableResultSet = added;
|
||||
scope.addResolvedResultSet(function.as ?? function.name, added);
|
||||
},
|
||||
);
|
||||
|
||||
visitChildren(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCommonTableExpression(CommonTableExpression e, void arg) {
|
||||
StatementScope.cast(e.scope).additionalKnownTables[e.cteTableName] = e;
|
||||
|
|
|
@ -57,7 +57,7 @@ class ReferenceResolver
|
|||
if (e.entityName != null) {
|
||||
// first find the referenced table or view,
|
||||
// then use the column on that table or view.
|
||||
final entityResolver = scope.resolveResultSet(e.entityName!);
|
||||
final entityResolver = scope.resolveResultSetForReference(e.entityName!);
|
||||
final resultSet = entityResolver?.resultSet.resultSet;
|
||||
|
||||
if (resultSet == null) {
|
||||
|
|
|
@ -89,7 +89,7 @@ class JoinModel {
|
|||
}
|
||||
|
||||
// The boolean arg indicates whether a visited queryable is needed for the
|
||||
// result to have any rows (which, in particular, mean's its non-nullable)
|
||||
// result to have any rows (which, in particular, means its non-nullable)
|
||||
class _FindNonNullableJoins extends RecursiveVisitor<bool, void> {
|
||||
final List<ResultSetAvailableInStatement> nonNullable = [];
|
||||
|
||||
|
|
|
@ -49,7 +49,8 @@ class CommonTableExpression extends AstNode with ResultSet {
|
|||
Token? asToken;
|
||||
IdentifierToken? tableNameToken;
|
||||
|
||||
List<CommonTableExpressionColumn>? _cachedColumns;
|
||||
@override
|
||||
List<Column>? resolvedColumns;
|
||||
|
||||
CommonTableExpression({
|
||||
required this.cteTableName,
|
||||
|
@ -71,33 +72,6 @@ class CommonTableExpression extends AstNode with ResultSet {
|
|||
@override
|
||||
Iterable<AstNode> get childNodes => [as];
|
||||
|
||||
@override
|
||||
List<Column>? get resolvedColumns {
|
||||
final columnsOfSelect = as.resolvedColumns;
|
||||
|
||||
// we don't override column names, so just return the columns declared by
|
||||
// the select statement
|
||||
if (columnNames == null) return columnsOfSelect;
|
||||
|
||||
final cached = _cachedColumns ??= columnNames!
|
||||
.map((name) => CommonTableExpressionColumn(name)..containingSet = this)
|
||||
.toList();
|
||||
|
||||
if (columnsOfSelect != null) {
|
||||
// bind the CommonTableExpressionColumn to the real underlying column
|
||||
// returned by the select statement
|
||||
|
||||
for (var i = 0; i < cached.length; i++) {
|
||||
if (i < columnsOfSelect.length) {
|
||||
final selectColumn = columnsOfSelect[i];
|
||||
cached[i].innerColumn = selectColumn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedColumns;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get visibleToChildren => true;
|
||||
}
|
||||
|
|
|
@ -270,22 +270,18 @@ class SqlEngine {
|
|||
final node = context.root;
|
||||
node.scope = context.rootScope;
|
||||
|
||||
try {
|
||||
AstPreparingVisitor(context: context).start(node);
|
||||
AstPreparingVisitor(context: context).start(node);
|
||||
|
||||
node
|
||||
..accept(ColumnResolver(context), const ColumnResolverContext())
|
||||
..accept(ReferenceResolver(context), const ReferenceResolvingContext());
|
||||
node
|
||||
..accept(ColumnResolver(context), const ColumnResolverContext())
|
||||
..accept(ReferenceResolver(context), const ReferenceResolvingContext());
|
||||
|
||||
final session = TypeInferenceSession(context, options);
|
||||
final resolver = TypeResolver(session);
|
||||
resolver.run(node);
|
||||
context.types2 = session.results!;
|
||||
final session = TypeInferenceSession(context, options);
|
||||
final resolver = TypeResolver(session);
|
||||
resolver.run(node);
|
||||
context.types2 = session.results!;
|
||||
|
||||
node.acceptWithoutArg(LintingVisitor(options, context));
|
||||
} catch (_) {
|
||||
rethrow;
|
||||
}
|
||||
node.acceptWithoutArg(LintingVisitor(options, context));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: sqlparser
|
||||
description: Parses sqlite statements and performs static analysis on them
|
||||
version: 0.30.0
|
||||
version: 0.30.1
|
||||
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
|
||||
repository: https://github.com/simolus3/drift
|
||||
#homepage: https://drift.simonbinder.eu/
|
||||
|
|
|
@ -100,6 +100,21 @@ END;
|
|||
});
|
||||
});
|
||||
|
||||
test('resolves index', () {
|
||||
final context = engine.analyze('CREATE INDEX foo ON demo (content)');
|
||||
context.expectNoError();
|
||||
|
||||
final tableReference =
|
||||
context.root.allDescendants.whereType<TableReference>().first;
|
||||
final columnReference = context.root.allDescendants
|
||||
.whereType<IndexedColumn>()
|
||||
.first
|
||||
.expression as Reference;
|
||||
|
||||
expect(tableReference.resolved, demoTable);
|
||||
expect(columnReference.resolvedColumn, isA<AvailableColumn>());
|
||||
});
|
||||
|
||||
test("DO UPDATE action in upsert can refer to 'exluded'", () {
|
||||
final context = engine.analyze('''
|
||||
INSERT INTO demo VALUES (?, ?)
|
||||
|
@ -110,6 +125,12 @@ INSERT INTO demo VALUES (?, ?)
|
|||
expect(context.errors, isEmpty);
|
||||
});
|
||||
|
||||
test('columns in an insert cannot refer to table', () {
|
||||
engine
|
||||
.analyze('INSERT INTO demo (content) VALUES (demo.content)')
|
||||
.expectError('demo.content');
|
||||
});
|
||||
|
||||
test('columns from values statement', () {
|
||||
final context = engine.analyze("VALUES ('foo', 3), ('bar', 5)");
|
||||
|
||||
|
@ -127,6 +148,33 @@ INSERT INTO demo VALUES (?, ?)
|
|||
expect(context.errors, isEmpty);
|
||||
});
|
||||
|
||||
test('joining table with and without alias', () {
|
||||
final context = engine.analyze('''
|
||||
SELECT * FROM demo a
|
||||
JOIN demo ON demo.id = a.id
|
||||
''');
|
||||
|
||||
context.expectNoError();
|
||||
});
|
||||
|
||||
test("from clause can't use its own table aliases", () {
|
||||
final context = engine.analyze('''
|
||||
SELECT * FROM demo a
|
||||
JOIN a b ON b.id = a.id
|
||||
''');
|
||||
|
||||
expect(context.errors, [
|
||||
analysisErrorWith(
|
||||
lexeme: 'a b', type: AnalysisErrorType.referencedUnknownTable),
|
||||
analysisErrorWith(
|
||||
lexeme: 'b.id', type: AnalysisErrorType.referencedUnknownTable),
|
||||
]);
|
||||
});
|
||||
|
||||
test('can use columns from deleted table', () {
|
||||
engine.analyze('DELETE FROM demo WHERE demo.id = 2').expectNoError();
|
||||
});
|
||||
|
||||
test('gracefully handles tuples of different lengths in VALUES', () {
|
||||
final context = engine.analyze("VALUES ('foo', 3), ('bar')");
|
||||
|
||||
|
@ -270,4 +318,25 @@ INSERT INTO demo VALUES (?, ?)
|
|||
.root as SelectStatement;
|
||||
expect(cte.resolvedColumns?.map((e) => e.name), ['RoWiD']);
|
||||
});
|
||||
|
||||
test('reports error for circular reference', () {
|
||||
final query = engine.analyze('WITH x AS (SELECT * FROM x) SELECT 1;');
|
||||
expect(query.errors, [
|
||||
analysisErrorWith(lexeme: 'x', type: AnalysisErrorType.circularReference),
|
||||
]);
|
||||
});
|
||||
|
||||
test('regression test for #2453', () {
|
||||
// https://github.com/simolus3/drift/issues/2453
|
||||
engine
|
||||
..registerTableFromSql('CREATE TABLE persons (id INTEGER);')
|
||||
..registerTableFromSql('CREATE TABLE cars (driver INTEGER);');
|
||||
|
||||
final query = engine.analyze('''
|
||||
SELECT * FROM cars
|
||||
JOIN persons second_person ON second_person.id = cars.driver
|
||||
JOIN persons ON persons.id = cars.driver;
|
||||
''');
|
||||
query.expectNoError();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -80,6 +80,17 @@ void main() {
|
|||
.expectError('DEFAULT VALUES', type: AnalysisErrorType.synctactic);
|
||||
});
|
||||
|
||||
test('WITH clauses', () {
|
||||
// https://sqlite.org/lang_with.html#limitations_and_caveats
|
||||
engine.analyze('WITH x AS (SELECT 1) SELECT 2').expectNoError();
|
||||
|
||||
engine.analyze('''
|
||||
CREATE TRIGGER tgr AFTER INSERT ON demo BEGIN
|
||||
WITH x AS (SELECT 1) SELECT 2;
|
||||
END;
|
||||
''').expectError('WITH', type: AnalysisErrorType.synctactic);
|
||||
});
|
||||
|
||||
group('aliased source tables', () {
|
||||
test('insert', () {
|
||||
engine.analyze('INSERT INTO demo AS d VALUES (?, ?)').expectNoError();
|
||||
|
|
|
@ -19,17 +19,17 @@ void main() {
|
|||
final model = JoinModel.of(stmt)!;
|
||||
expect(
|
||||
model.isNullableTable(
|
||||
stmt.scope.resolveResultSet('a1')!.resultSet.resultSet!),
|
||||
stmt.scope.resolveResultSetForReference('a1')!.resultSet.resultSet!),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
model.isNullableTable(
|
||||
stmt.scope.resolveResultSet('a2')!.resultSet.resultSet!),
|
||||
stmt.scope.resolveResultSetForReference('a2')!.resultSet.resultSet!),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
model.isNullableTable(
|
||||
stmt.scope.resolveResultSet('a3')!.resultSet.resultSet!),
|
||||
stmt.scope.resolveResultSetForReference('a3')!.resultSet.resultSet!),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue