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
|
## 2.8.0
|
||||||
|
|
||||||
- Don't keep databases in an unusable state if the `setup` callback throws an
|
- 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
|
/// [computeWithDatabase] is beneficial when an an expensive work unit needs
|
||||||
/// to use the database, or when creating the SQL statements itself is
|
/// to use the database, or when creating the SQL statements itself is
|
||||||
/// expensive.
|
/// 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
|
@experimental
|
||||||
Future<Ret> computeWithDatabase<Ret>({
|
Future<Ret> computeWithDatabase<Ret>({
|
||||||
required FutureOr<Ret> Function(DB) computation,
|
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
|
/// add custom user-defined sql functions or to provide encryption keys in
|
||||||
/// SQLCipher implementations.
|
/// SQLCipher implementations.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
factory NativeDatabase(File file,
|
factory NativeDatabase(
|
||||||
{bool logStatements = false, DatabaseSetup? setup}) {
|
File file, {
|
||||||
return NativeDatabase._(_NativeDelegate(file, setup), logStatements);
|
bool logStatements = false,
|
||||||
|
DatabaseSetup? setup,
|
||||||
|
bool cachePreparedStatements = true,
|
||||||
|
}) {
|
||||||
|
return NativeDatabase._(
|
||||||
|
_NativeDelegate(
|
||||||
|
file,
|
||||||
|
setup,
|
||||||
|
cachePreparedStatements,
|
||||||
|
),
|
||||||
|
logStatements);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a database storing its result in [file].
|
/// 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.
|
/// Creates an in-memory database won't persist its changes on disk.
|
||||||
///
|
///
|
||||||
/// {@macro drift_vm_database_factory}
|
/// {@macro drift_vm_database_factory}
|
||||||
factory NativeDatabase.memory(
|
factory NativeDatabase.memory({
|
||||||
{bool logStatements = false, DatabaseSetup? setup}) {
|
bool logStatements = false,
|
||||||
return NativeDatabase._(_NativeDelegate(null, setup), logStatements);
|
DatabaseSetup? setup,
|
||||||
|
bool cachePreparedStatements = true,
|
||||||
|
}) {
|
||||||
|
return NativeDatabase._(
|
||||||
|
_NativeDelegate(null, setup, cachePreparedStatements),
|
||||||
|
logStatements,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a drift executor for an opened [database] from the `sqlite3`
|
/// 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).
|
/// internally when running [integration tests for migrations](https://drift.simonbinder.eu/docs/advanced-features/migrations/#verifying-migrations).
|
||||||
///
|
///
|
||||||
/// {@macro drift_vm_database_factory}
|
/// {@macro drift_vm_database_factory}
|
||||||
factory NativeDatabase.opened(Database database,
|
factory NativeDatabase.opened(
|
||||||
{bool logStatements = false,
|
Database database, {
|
||||||
|
bool logStatements = false,
|
||||||
DatabaseSetup? setup,
|
DatabaseSetup? setup,
|
||||||
bool closeUnderlyingOnClose = true}) {
|
bool closeUnderlyingOnClose = true,
|
||||||
|
bool cachePreparedStatements = true,
|
||||||
|
}) {
|
||||||
return NativeDatabase._(
|
return NativeDatabase._(
|
||||||
_NativeDelegate.opened(database, setup, closeUnderlyingOnClose),
|
_NativeDelegate.opened(
|
||||||
|
database,
|
||||||
|
setup,
|
||||||
|
closeUnderlyingOnClose,
|
||||||
|
cachePreparedStatements,
|
||||||
|
),
|
||||||
logStatements);
|
logStatements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,12 +205,24 @@ class NativeDatabase extends DelegatedDatabase {
|
||||||
class _NativeDelegate extends Sqlite3Delegate<Database> {
|
class _NativeDelegate extends Sqlite3Delegate<Database> {
|
||||||
final File? file;
|
final File? file;
|
||||||
|
|
||||||
_NativeDelegate(this.file, DatabaseSetup? setup) : super(setup);
|
_NativeDelegate(this.file, DatabaseSetup? setup, bool cachePreparedStatements)
|
||||||
|
: super(
|
||||||
|
setup,
|
||||||
|
cachePreparedStatements: cachePreparedStatements,
|
||||||
|
);
|
||||||
|
|
||||||
_NativeDelegate.opened(
|
_NativeDelegate.opened(
|
||||||
Database db, DatabaseSetup? setup, bool closeUnderlyingWhenClosed)
|
Database db,
|
||||||
: file = null,
|
DatabaseSetup? setup,
|
||||||
super.opened(db, setup, closeUnderlyingWhenClosed);
|
bool closeUnderlyingWhenClosed,
|
||||||
|
bool cachePreparedStatements,
|
||||||
|
) : file = null,
|
||||||
|
super.opened(
|
||||||
|
db,
|
||||||
|
setup,
|
||||||
|
closeUnderlyingWhenClosed,
|
||||||
|
cachePreparedStatements: cachePreparedStatements,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Database openDatabase() {
|
Database openDatabase() {
|
||||||
|
@ -242,6 +278,8 @@ class _NativeDelegate extends Sqlite3Delegate<Database> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
await super.close();
|
||||||
|
|
||||||
if (closeUnderlyingWhenClosed) {
|
if (closeUnderlyingWhenClosed) {
|
||||||
try {
|
try {
|
||||||
tracker.markClosed(database);
|
tracker.markClosed(database);
|
||||||
|
|
|
@ -209,20 +209,25 @@ class ServerImplementation implements DriftServer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case TransactionControl.commit:
|
case TransactionControl.commit:
|
||||||
await executor.send();
|
await executor.send();
|
||||||
|
// The transaction should only be released if the commit doesn't throw.
|
||||||
|
_releaseExecutor(executorId!);
|
||||||
break;
|
break;
|
||||||
case TransactionControl.rollback:
|
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();
|
await executor.rollback();
|
||||||
|
} finally {
|
||||||
|
_releaseExecutor(executorId!);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
assert(false, 'Unknown TransactionControl');
|
assert(false, 'Unknown TransactionControl');
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
_releaseExecutor(executorId!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _releaseExecutor(int id) {
|
void _releaseExecutor(int id) {
|
||||||
|
|
|
@ -343,12 +343,17 @@ abstract class DatabaseConnectionUser {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a custom select statement from the given sql [query]. To run the
|
/// Creates a custom select statement from the given sql [query].
|
||||||
/// query once, use [Selectable.get]. For an auto-updating streams, set the
|
///
|
||||||
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
|
/// The query can be run once by calling [Selectable.get].
|
||||||
/// know the query will never emit more than one row, you can also use
|
///
|
||||||
/// `getSingle` and `SelectableUtils.watchSingle` which return the item
|
/// For an auto-updating query stream, the [readsFrom] parameter needs to be
|
||||||
/// directly without wrapping it into a list.
|
/// 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
|
/// If you use variables in your query (for instance with "?"), they will be
|
||||||
/// bound to the [variables] you specify on this query.
|
/// bound to the [variables] you specify on this query.
|
||||||
|
|
|
@ -80,8 +80,6 @@ class StreamQueryStore {
|
||||||
final StreamController<Set<TableUpdate>> _tableUpdates =
|
final StreamController<Set<TableUpdate>> _tableUpdates =
|
||||||
StreamController.broadcast(sync: true);
|
StreamController.broadcast(sync: true);
|
||||||
|
|
||||||
StreamQueryStore();
|
|
||||||
|
|
||||||
/// Creates a new stream from the select statement.
|
/// Creates a new stream from the select statement.
|
||||||
Stream<List<Map<String, Object?>>> registerStream(
|
Stream<List<Map<String, Object?>>> registerStream(
|
||||||
QueryStreamFetcher fetcher) {
|
QueryStreamFetcher fetcher) {
|
||||||
|
|
|
@ -65,7 +65,10 @@ class Variable<T extends Object> extends Expression<T> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void writeInto(GenerationContext context) {
|
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.
|
// Write as constant instead.
|
||||||
Constant<T>(value).writeInto(context);
|
Constant<T>(value).writeInto(context);
|
||||||
return;
|
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/executor/stream_queries.dart';
|
||||||
import 'package:drift/src/runtime/types/converters.dart';
|
import 'package:drift/src/runtime/types/converters.dart';
|
||||||
import 'package:drift/src/runtime/types/mapping.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:drift/src/utils/single_transformer.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
|
|
@ -267,7 +267,7 @@ abstract mixin class Selectable<T>
|
||||||
/// Maps this selectable by the [mapper] function.
|
/// Maps this selectable by the [mapper] function.
|
||||||
///
|
///
|
||||||
/// Like [map] just async.
|
/// 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);
|
return _AsyncMappedSelectable<T, N>(this, mapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,7 +293,7 @@ class _MappedSelectable<S, T> extends Selectable<T> {
|
||||||
|
|
||||||
class _AsyncMappedSelectable<S, T> extends Selectable<T> {
|
class _AsyncMappedSelectable<S, T> extends Selectable<T> {
|
||||||
final Selectable<S> _source;
|
final Selectable<S> _source;
|
||||||
final Future<T> Function(S) _mapper;
|
final FutureOr<T> Function(S) _mapper;
|
||||||
|
|
||||||
_AsyncMappedSelectable(this._source, this._mapper);
|
_AsyncMappedSelectable(this._source, this._mapper);
|
||||||
|
|
||||||
|
@ -304,11 +304,13 @@ class _AsyncMappedSelectable<S, T> extends Selectable<T> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<T>> watch() {
|
Stream<List<T>> watch() {
|
||||||
return _source.watch().asyncMap(_mapResults);
|
return AsyncMapPerSubscription(_source.watch())
|
||||||
|
.asyncMapPerSubscription(_mapResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<T>> _mapResults(List<S> results) async =>
|
Future<List<T>> _mapResults(List<S> results) async {
|
||||||
[for (final result in results) await _mapper(result)];
|
return [for (final result in results) await _mapper(result)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mixin for a [Query] that operates on a single primary table only.
|
/// 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),
|
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) {
|
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
||||||
|
|
|
@ -233,7 +233,7 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
|
|
||||||
return database
|
return database
|
||||||
.createStream(fetcher)
|
.createStream(fetcher)
|
||||||
.asyncMap((rows) => _mapResponse(ctx, rows));
|
.asyncMapPerSubscription((rows) => _mapResponse(ctx, rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
@internal
|
@internal
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:sqlite3/common.dart';
|
import 'package:sqlite3/common.dart';
|
||||||
|
@ -24,6 +25,9 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
|
|
||||||
final void Function(DB)? _setup;
|
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
|
/// Whether the [database] should be closed when [close] is called on this
|
||||||
/// instance.
|
/// instance.
|
||||||
///
|
///
|
||||||
|
@ -31,13 +35,22 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
/// connections to the same database.
|
/// connections to the same database.
|
||||||
final bool closeUnderlyingWhenClosed;
|
final bool closeUnderlyingWhenClosed;
|
||||||
|
|
||||||
|
final PreparedStatementsCache _preparedStmtsCache = PreparedStatementsCache();
|
||||||
|
|
||||||
/// A delegate that will call [openDatabase] to open the database.
|
/// 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
|
/// A delegate using an underlying sqlite3 database object that has already
|
||||||
/// been opened.
|
/// been opened.
|
||||||
Sqlite3Delegate.opened(
|
Sqlite3Delegate.opened(
|
||||||
this._database, this._setup, this.closeUnderlyingWhenClosed) {
|
this._database,
|
||||||
|
this._setup,
|
||||||
|
this.closeUnderlyingWhenClosed, {
|
||||||
|
required this.cachePreparedStatements,
|
||||||
|
}) {
|
||||||
_initializeDatabase();
|
_initializeDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +81,10 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
_database?.dispose();
|
_database?.dispose();
|
||||||
_database = null;
|
_database = null;
|
||||||
|
|
||||||
|
// We can call clear instead of disposeAll because disposing the
|
||||||
|
// database will also dispose all prepared statements on it.
|
||||||
|
_preparedStmtsCache.clear();
|
||||||
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +102,12 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
_hasInitializedDatabase = true;
|
_hasInitializedDatabase = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
Future<void> close() {
|
||||||
|
return Future(_preparedStmtsCache.disposeAll);
|
||||||
|
}
|
||||||
|
|
||||||
/// Synchronously prepares and runs [statements] collected from a batch.
|
/// Synchronously prepares and runs [statements] collected from a batch.
|
||||||
@protected
|
@protected
|
||||||
void runBatchSync(BatchedStatements statements) {
|
void runBatchSync(BatchedStatements statements) {
|
||||||
|
@ -114,23 +137,32 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
|
||||||
if (args.isEmpty) {
|
if (args.isEmpty) {
|
||||||
database.execute(statement);
|
database.execute(statement);
|
||||||
} else {
|
} else {
|
||||||
final stmt = database.prepare(statement, checkNoTail: true);
|
final stmt = _getPreparedStatement(statement);
|
||||||
try {
|
|
||||||
stmt.execute(args);
|
stmt.execute(args);
|
||||||
} finally {
|
|
||||||
stmt.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
||||||
final stmt = database.prepare(statement, checkNoTail: true);
|
final stmt = _getPreparedStatement(statement);
|
||||||
try {
|
|
||||||
final result = stmt.select(args);
|
final result = stmt.select(args);
|
||||||
return QueryResult.fromRows(result.toList());
|
return QueryResult.fromRows(result.toList());
|
||||||
} finally {
|
}
|
||||||
stmt.dispose();
|
|
||||||
|
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();
|
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,
|
WasmDatabaseSetup? setup,
|
||||||
IndexedDbFileSystem? fileSystem,
|
IndexedDbFileSystem? fileSystem,
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
|
bool cachePreparedStatements = true,
|
||||||
}) {
|
}) {
|
||||||
return WasmDatabase._(
|
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.
|
/// Creates an in-memory database in the loaded [sqlite3] database.
|
||||||
|
@ -66,9 +69,12 @@ class WasmDatabase extends DelegatedDatabase {
|
||||||
CommonSqlite3 sqlite3, {
|
CommonSqlite3 sqlite3, {
|
||||||
WasmDatabaseSetup? setup,
|
WasmDatabaseSetup? setup,
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
|
bool cachePreparedStatements = true,
|
||||||
}) {
|
}) {
|
||||||
return WasmDatabase._(
|
return WasmDatabase._(
|
||||||
_WasmDelegate(sqlite3, null, setup, null), logStatements);
|
_WasmDelegate(sqlite3, null, setup, null, cachePreparedStatements),
|
||||||
|
logStatements,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For an in-depth
|
/// For an in-depth
|
||||||
|
@ -110,8 +116,15 @@ class _WasmDelegate extends Sqlite3Delegate<CommonDatabase> {
|
||||||
final IndexedDbFileSystem? _fileSystem;
|
final IndexedDbFileSystem? _fileSystem;
|
||||||
|
|
||||||
_WasmDelegate(
|
_WasmDelegate(
|
||||||
this._sqlite3, this._path, WasmDatabaseSetup? setup, this._fileSystem)
|
this._sqlite3,
|
||||||
: super(setup);
|
this._path,
|
||||||
|
WasmDatabaseSetup? setup,
|
||||||
|
this._fileSystem,
|
||||||
|
bool cachePreparedStatements,
|
||||||
|
) : super(
|
||||||
|
setup,
|
||||||
|
cachePreparedStatements: cachePreparedStatements,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CommonDatabase openDatabase() {
|
CommonDatabase openDatabase() {
|
||||||
|
@ -163,6 +176,8 @@ class _WasmDelegate extends Sqlite3Delegate<CommonDatabase> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
await super.close();
|
||||||
|
|
||||||
if (closeUnderlyingWhenClosed) {
|
if (closeUnderlyingWhenClosed) {
|
||||||
database.dispose();
|
database.dispose();
|
||||||
await _flush();
|
await _flush();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: drift
|
name: drift
|
||||||
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
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
|
repository: https://github.com/simolus3/drift
|
||||||
homepage: https://drift.simonbinder.eu/
|
homepage: https://drift.simonbinder.eu/
|
||||||
issue_tracker: https://github.com/simolus3/drift/issues
|
issue_tracker: https://github.com/simolus3/drift/issues
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/src/runtime/api/runtime_api.dart';
|
import 'package:drift/src/runtime/api/runtime_api.dart';
|
||||||
import 'package:drift/src/runtime/executor/stream_queries.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 first.first; // will listen to stream, then cancel
|
||||||
await pumpEventQueue(times: 1); // give cancel event time to propagate
|
await pumpEventQueue(times: 1); // give cancel event time to propagate
|
||||||
|
|
||||||
final checkEmits =
|
final listener = StreamQueue(second);
|
||||||
expectLater(second, emitsInOrder([<Object?>[], <Object?>[]]));
|
await expectLater(listener, emits(isEmpty));
|
||||||
|
|
||||||
db.markTablesUpdated({db.users});
|
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 {
|
test('same stream instance can be listened to multiple times', () async {
|
||||||
|
|
|
@ -216,7 +216,7 @@ class TodoDb extends _$TodoDb {
|
||||||
DriftDatabaseOptions options = const DriftDatabaseOptions();
|
DriftDatabaseOptions options = const DriftDatabaseOptions();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int schemaVersion = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@DriftAccessor(
|
@DriftAccessor(
|
||||||
|
|
|
@ -1846,9 +1846,9 @@ class AllTodosWithCategoryResult extends CustomResultSet {
|
||||||
|
|
||||||
mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
|
mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$UsersTable get users => attachedDatabase.users;
|
||||||
$SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos;
|
|
||||||
$CategoriesTable get categories => attachedDatabase.categories;
|
$CategoriesTable get categories => attachedDatabase.categories;
|
||||||
$TodosTableTable get todosTable => attachedDatabase.todosTable;
|
$TodosTableTable get todosTable => attachedDatabase.todosTable;
|
||||||
|
$SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos;
|
||||||
$TodoWithCategoryViewView get todoWithCategoryView =>
|
$TodoWithCategoryViewView get todoWithCategoryView =>
|
||||||
attachedDatabase.todoWithCategoryView;
|
attachedDatabase.todoWithCategoryView;
|
||||||
Selectable<TodoEntry> todosForUser({required int user}) {
|
Selectable<TodoEntry> todosForUser({required int user}) {
|
||||||
|
|
|
@ -420,6 +420,47 @@ void main() {
|
||||||
expect(db.validateDatabaseSchema(), completes);
|
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 {
|
class _TestDatabase extends GeneratedDatabase {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
@TestOn('vm')
|
@TestOn('vm')
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/isolate.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
@ -8,8 +10,9 @@ import '../test_utils/database_vm.dart';
|
||||||
void main() {
|
void main() {
|
||||||
preferLocalSqlite3();
|
preferLocalSqlite3();
|
||||||
|
|
||||||
test('a failing commit does not block the whole database', () async {
|
group('a failing commit does not block the whole database', () {
|
||||||
final db = _Database(NativeDatabase.memory());
|
Future<void> testWith(QueryExecutor executor) async {
|
||||||
|
final db = _Database(executor);
|
||||||
addTearDown(db.close);
|
addTearDown(db.close);
|
||||||
|
|
||||||
await db.customStatement('''
|
await db.customStatement('''
|
||||||
|
@ -38,11 +41,22 @@ CREATE TABLE IF NOT EXISTS todo_categories (
|
||||||
'INSERT INTO todo_items (title, category_id) VALUES (?, ?);',
|
'INSERT INTO todo_items (title, category_id) VALUES (?, ?);',
|
||||||
['a', 100]);
|
['a', 100]);
|
||||||
}),
|
}),
|
||||||
throwsA(isA<SqliteException>()),
|
throwsA(anyOf(isA<SqliteException>(), isA<DriftRemoteException>())),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(db.customSelect('SELECT * FROM todo_items').get(),
|
||||||
db.customSelect('SELECT * FROM todo_items').get(), completion(isEmpty));
|
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.
|
// in drift/test/test_utils/test_utils.dart.
|
||||||
// Do not manually edit this file.
|
// Do not manually edit this file.
|
||||||
|
|
||||||
|
// @dart=2.19
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
import 'dart:async' as _i4;
|
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
|
## 2.8.2
|
||||||
|
|
||||||
- Fix generated to write qualified column references for Dart components in
|
- Fix generated to write qualified column references for Dart components in
|
||||||
|
|
|
@ -63,8 +63,12 @@ class DriftAnalysisCache {
|
||||||
yield found;
|
yield found;
|
||||||
|
|
||||||
for (final imported in found.imports ?? const <Uri>[]) {
|
for (final imported in found.imports ?? const <Uri>[]) {
|
||||||
if (seenUris.add(imported)) {
|
// We might not have a known file for all imports of a Dart file, since
|
||||||
pending.add(knownFiles[imported]!);
|
// 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 DriftBackend backend;
|
||||||
final DriftAnalysisCache cache = DriftAnalysisCache();
|
final DriftAnalysisCache cache = DriftAnalysisCache();
|
||||||
final DriftOptions options;
|
final DriftOptions options;
|
||||||
|
final bool _isTesting;
|
||||||
|
|
||||||
late final TypeMapping typeMapping = TypeMapping(this);
|
late final TypeMapping typeMapping = TypeMapping(this);
|
||||||
late final ElementDeserializer deserializer = ElementDeserializer(this);
|
late final ElementDeserializer deserializer = ElementDeserializer(this);
|
||||||
|
@ -64,7 +65,8 @@ class DriftAnalysisDriver {
|
||||||
|
|
||||||
KnownDriftTypes? _knownTypes;
|
KnownDriftTypes? _knownTypes;
|
||||||
|
|
||||||
DriftAnalysisDriver(this.backend, this.options);
|
DriftAnalysisDriver(this.backend, this.options, {bool isTesting = false})
|
||||||
|
: _isTesting = isTesting;
|
||||||
|
|
||||||
SqlEngine newSqlEngine() {
|
SqlEngine newSqlEngine() {
|
||||||
return SqlEngine(
|
return SqlEngine(
|
||||||
|
@ -155,12 +157,16 @@ class DriftAnalysisDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the first step (element discovery) on a file with the given [uri].
|
/// Runs the first step (element discovery) on a file with the given [uri].
|
||||||
Future<FileState> prepareFileForAnalysis(Uri uri,
|
Future<FileState> prepareFileForAnalysis(
|
||||||
{bool needsDiscovery = true}) async {
|
Uri uri, {
|
||||||
|
bool needsDiscovery = true,
|
||||||
|
bool warnIfFileDoesntExist = true,
|
||||||
|
}) async {
|
||||||
var known = cache.knownFiles[uri] ?? cache.notifyFileChanged(uri);
|
var known = cache.knownFiles[uri] ?? cache.notifyFileChanged(uri);
|
||||||
|
|
||||||
if (known.discovery == null && needsDiscovery) {
|
if (known.discovery == null && needsDiscovery) {
|
||||||
await DiscoverStep(this, known).discover();
|
await DiscoverStep(this, known)
|
||||||
|
.discover(warnIfFileDoesntExist: warnIfFileDoesntExist);
|
||||||
cache.postFileDiscoveryResults(known);
|
cache.postFileDiscoveryResults(known);
|
||||||
|
|
||||||
// todo: Mark elements that need to be analyzed again
|
// 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) {
|
} catch (e, s) {
|
||||||
if (e is! CouldNotResolveElementException) {
|
if (e is! CouldNotResolveElementException) {
|
||||||
backend.log.warning('Could not analyze ${discovered.ownId}', e, s);
|
backend.log.warning('Could not analyze ${discovered.ownId}', e, s);
|
||||||
|
|
||||||
|
if (_isTesting) rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,10 +129,17 @@ class DriftFileImport {
|
||||||
class DiscoveredDartLibrary extends DiscoveredFileState {
|
class DiscoveredDartLibrary extends DiscoveredFileState {
|
||||||
final LibraryElement library;
|
final LibraryElement library;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final List<Uri> importDependencies;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get isValidImport => true;
|
bool get isValidImport => true;
|
||||||
|
|
||||||
DiscoveredDartLibrary(this.library, super.locallyDefinedElements);
|
DiscoveredDartLibrary(
|
||||||
|
this.library,
|
||||||
|
super.locallyDefinedElements,
|
||||||
|
this.importDependencies,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotADartLibrary extends DiscoveredFileState {
|
class NotADartLibrary extends DiscoveredFileState {
|
||||||
|
|
|
@ -46,6 +46,9 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final tableConstraints =
|
||||||
|
await _readCustomConstraints(references, columns, element);
|
||||||
|
|
||||||
final table = DriftTable(
|
final table = DriftTable(
|
||||||
discovered.ownId,
|
discovered.ownId,
|
||||||
DriftDeclaration.dartElement(element),
|
DriftDeclaration.dartElement(element),
|
||||||
|
@ -60,7 +63,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
||||||
for (final uniqueKey in uniqueKeys ?? const <Set<DriftColumn>>[])
|
for (final uniqueKey in uniqueKeys ?? const <Set<DriftColumn>>[])
|
||||||
UniqueColumns(uniqueKey),
|
UniqueColumns(uniqueKey),
|
||||||
],
|
],
|
||||||
overrideTableConstraints: await _readCustomConstraints(columns, element),
|
overrideTableConstraints: tableConstraints,
|
||||||
withoutRowId: await _overrideWithoutRowId(element) ?? false,
|
withoutRowId: await _overrideWithoutRowId(element) ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -272,7 +275,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
||||||
return ColumnParser(this).parse(declaration, element);
|
return ColumnParser(this).parse(declaration, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> _readCustomConstraints(
|
Future<List<String>> _readCustomConstraints(Set<DriftElement> references,
|
||||||
List<DriftColumn> localColumns, ClassElement element) async {
|
List<DriftColumn> localColumns, ClassElement element) async {
|
||||||
final customConstraints =
|
final customConstraints =
|
||||||
element.lookUpGetter('customConstraints', element.library);
|
element.lookUpGetter('customConstraints', element.library);
|
||||||
|
@ -343,6 +346,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
||||||
(msg) => DriftAnalysisError.inDartAst(element, source, msg));
|
(msg) => DriftAnalysisError.inDartAst(element, source, msg));
|
||||||
|
|
||||||
if (table != null) {
|
if (table != null) {
|
||||||
|
references.add(table);
|
||||||
final missingColumns = clause.columnNames
|
final missingColumns = clause.columnNames
|
||||||
.map((e) => e.columnName)
|
.map((e) => e.columnName)
|
||||||
.where((e) => !table.columnBySqlName.containsKey(e));
|
.where((e) => !table.columnBySqlName.containsKey(e));
|
||||||
|
|
|
@ -51,7 +51,7 @@ class DiscoverStep {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> discover() async {
|
Future<void> discover({required bool warnIfFileDoesntExist}) async {
|
||||||
final extension = _file.extension;
|
final extension = _file.extension;
|
||||||
_file.discovery = UnknownFile();
|
_file.discovery = UnknownFile();
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class DiscoverStep {
|
||||||
try {
|
try {
|
||||||
library = await _driver.backend.readDart(_file.ownUri);
|
library = await _driver.backend.readDart(_file.ownUri);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is! NotALibraryException) {
|
if (e is! NotALibraryException && warnIfFileDoesntExist) {
|
||||||
// Backends are supposed to throw NotALibraryExceptions if the
|
// Backends are supposed to throw NotALibraryExceptions if the
|
||||||
// library is a part file. For other exceptions, we better report
|
// library is a part file. For other exceptions, we better report
|
||||||
// the error.
|
// the error.
|
||||||
|
@ -77,8 +77,11 @@ class DiscoverStep {
|
||||||
await finder.find();
|
await finder.find();
|
||||||
|
|
||||||
_file.errorsDuringDiscovery.addAll(finder.errors);
|
_file.errorsDuringDiscovery.addAll(finder.errors);
|
||||||
_file.discovery =
|
_file.discovery = DiscoveredDartLibrary(
|
||||||
DiscoveredDartLibrary(library, _checkForDuplicates(finder.found));
|
library,
|
||||||
|
_checkForDuplicates(finder.found),
|
||||||
|
finder.imports,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case '.drift':
|
case '.drift':
|
||||||
case '.moor':
|
case '.moor':
|
||||||
|
@ -153,6 +156,8 @@ class _FindDartElements extends RecursiveElementVisitor<void> {
|
||||||
final DiscoverStep _discoverStep;
|
final DiscoverStep _discoverStep;
|
||||||
final LibraryElement _library;
|
final LibraryElement _library;
|
||||||
|
|
||||||
|
final List<Uri> imports = [];
|
||||||
|
|
||||||
final TypeChecker _isTable, _isView, _isTableInfo, _isDatabase, _isDao;
|
final TypeChecker _isTable, _isView, _isTableInfo, _isDatabase, _isDao;
|
||||||
|
|
||||||
final List<Future<void>> _pendingWork = [];
|
final List<Future<void>> _pendingWork = [];
|
||||||
|
@ -231,6 +236,18 @@ class _FindDartElements extends RecursiveElementVisitor<void> {
|
||||||
super.visitClassElement(element);
|
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) {
|
String _defaultNameForTableOrView(ClassElement definingElement) {
|
||||||
return _discoverStep._driver.options.caseFromDartToSql
|
return _discoverStep._driver.options.caseFromDartToSql
|
||||||
.apply(definingElement.name);
|
.apply(definingElement.name);
|
||||||
|
|
|
@ -222,7 +222,8 @@ class DartTopLevelSymbol {
|
||||||
|
|
||||||
factory DartTopLevelSymbol.topLevelElement(Element element,
|
factory DartTopLevelSymbol.topLevelElement(Element element,
|
||||||
[String? elementName]) {
|
[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
|
// We're using this to recover the right import URI when using
|
||||||
// `package:build`:
|
// `package:build`:
|
||||||
|
@ -437,7 +438,7 @@ class _AddFromAst extends GeneralizingAstVisitor<void> {
|
||||||
_AddFromAst(this._builder, this._excluding);
|
_AddFromAst(this._builder, this._excluding);
|
||||||
|
|
||||||
void _addTopLevelReference(Element? element, Token name2) {
|
void _addTopLevelReference(Element? element, Token name2) {
|
||||||
if (element == null) {
|
if (element == null || (element.isSynthetic && element.library == null)) {
|
||||||
_builder.addText(name2.lexeme);
|
_builder.addText(name2.lexeme);
|
||||||
} else {
|
} else {
|
||||||
_builder.addTopLevel(
|
_builder.addTopLevel(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
description: Dev-dependency for users of drift. Contains the generator and development tools.
|
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
|
repository: https://github.com/simolus3/drift
|
||||||
homepage: https://drift.simonbinder.eu/
|
homepage: https://drift.simonbinder.eu/
|
||||||
issue_tracker: https://github.com/simolus3/drift/issues
|
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 {
|
test('supports autoIncrement on int64 columns', () async {
|
||||||
final backend = TestBackend.inTest({
|
final backend = TestBackend.inTest({
|
||||||
'a|lib/a.dart': '''
|
'a|lib/a.dart': '''
|
||||||
|
|
|
@ -45,7 +45,7 @@ class TestBackend extends DriftBackend {
|
||||||
for (final entry in sourceContents.entries)
|
for (final entry in sourceContents.entries)
|
||||||
AssetId.parse(entry.key).uri.toString(): entry.value,
|
AssetId.parse(entry.key).uri.toString(): entry.value,
|
||||||
} {
|
} {
|
||||||
driver = DriftAnalysisDriver(this, options);
|
driver = DriftAnalysisDriver(this, options, isTesting: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory TestBackend.inTest(
|
factory TestBackend.inTest(
|
||||||
|
|
|
@ -27,7 +27,3 @@ dev_dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
- test_asset.db
|
- 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
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.6 <3.0.0"
|
sdk: ">=2.17.6 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
drift: ^2.0.2+1
|
drift: ^2.0.2+1
|
||||||
sqlcipher_flutter_libs: ^0.5.1
|
sqlcipher_flutter_libs: ^0.5.1
|
||||||
|
@ -24,7 +25,3 @@ dev_dependencies:
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
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));
|
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:benchmark_harness/benchmark_harness.dart' show ScoreEmitter;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'src/moor/cache_prepared_statements.dart';
|
||||||
import 'src/moor/key_value_insert.dart';
|
import 'src/moor/key_value_insert.dart';
|
||||||
import 'src/sqlite/bind_string.dart';
|
import 'src/sqlite/bind_string.dart';
|
||||||
import 'src/sqlparser/parse_drift_file.dart';
|
import 'src/sqlparser/parse_drift_file.dart';
|
||||||
|
@ -22,6 +23,9 @@ List<Reportable> allBenchmarks(ScoreEmitter emitter) {
|
||||||
// sql parser
|
// sql parser
|
||||||
ParseDriftFile(emitter),
|
ParseDriftFile(emitter),
|
||||||
TokenizerBenchmark(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])
|
@DriftDatabase(tables: [KeyValues])
|
||||||
class Database extends _$Database {
|
class Database extends _$Database {
|
||||||
Database() : super(_obtainExecutor());
|
Database({bool cachePreparedStatements = true})
|
||||||
|
: super(_obtainExecutor(
|
||||||
|
cachePreparedStatements: cachePreparedStatements,
|
||||||
|
));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 1;
|
||||||
|
@ -25,10 +28,15 @@ class Database extends _$Database {
|
||||||
|
|
||||||
const _uuid = Uuid();
|
const _uuid = Uuid();
|
||||||
|
|
||||||
QueryExecutor _obtainExecutor() {
|
QueryExecutor _obtainExecutor({
|
||||||
|
required bool cachePreparedStatements,
|
||||||
|
}) {
|
||||||
final file =
|
final file =
|
||||||
File(p.join(Directory.systemTemp.path, 'drift_benchmarks', _uuid.v4()));
|
File(p.join(Directory.systemTemp.path, 'drift_benchmarks', _uuid.v4()));
|
||||||
file.parent.createSync();
|
file.parent.createSync();
|
||||||
|
|
||||||
return NativeDatabase(file);
|
return NativeDatabase(
|
||||||
|
file,
|
||||||
|
cachePreparedStatements: cachePreparedStatements,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,3 @@ dev_dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
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
|
## 0.30.0
|
||||||
|
|
||||||
- Add `previous` and `next` fields for tokens
|
- Add `previous` and `next` fields for tokens
|
||||||
|
|
|
@ -69,6 +69,7 @@ enum AnalysisErrorType {
|
||||||
starColumnWithoutTable,
|
starColumnWithoutTable,
|
||||||
compoundColumnCountMismatch,
|
compoundColumnCountMismatch,
|
||||||
cteColumnCountMismatch,
|
cteColumnCountMismatch,
|
||||||
|
circularReference,
|
||||||
valuesSelectCountMismatch,
|
valuesSelectCountMismatch,
|
||||||
viewColumnNamesMismatch,
|
viewColumnNamesMismatch,
|
||||||
rowValueMisuse,
|
rowValueMisuse,
|
||||||
|
|
|
@ -45,7 +45,8 @@ abstract class ReferenceScope {
|
||||||
/// This is useful to resolve qualified references (e.g. to resolve `foo.bar`
|
/// This is useful to resolve qualified references (e.g. to resolve `foo.bar`
|
||||||
/// the resolver would call [resolveResultSet]("foo") and then look up the
|
/// the resolver would call [resolveResultSet]("foo") and then look up the
|
||||||
/// `bar` column in that result set).
|
/// `bar` column in that result set).
|
||||||
ResultSetAvailableInStatement? resolveResultSet(String name) => null;
|
ResultSetAvailableInStatement? resolveResultSetForReference(String name) =>
|
||||||
|
null;
|
||||||
|
|
||||||
/// Adds an added result set to this scope.
|
/// Adds an added result set to this scope.
|
||||||
///
|
///
|
||||||
|
@ -126,8 +127,8 @@ mixin _HasParentScope on ReferenceScope {
|
||||||
_parentScopeForLookups.resultSetAvailableToChildScopes;
|
_parentScopeForLookups.resultSetAvailableToChildScopes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ResultSetAvailableInStatement? resolveResultSet(String name) =>
|
ResultSetAvailableInStatement? resolveResultSetForReference(String name) =>
|
||||||
_parentScopeForLookups.resolveResultSet(name);
|
_parentScopeForLookups.resolveResultSetForReference(name);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ResultSet? resolveResultSetToAdd(String name) =>
|
ResultSet? resolveResultSetToAdd(String name) =>
|
||||||
|
@ -219,8 +220,8 @@ class StatementScope extends ReferenceScope with _HasParentScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ResultSetAvailableInStatement? resolveResultSet(String name) {
|
ResultSetAvailableInStatement? resolveResultSetForReference(String name) {
|
||||||
return resultSets[name] ?? parent.resolveResultSet(name);
|
return resultSets[name] ?? parent.resolveResultSetForReference(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -279,12 +280,19 @@ class StatementScope extends ReferenceScope with _HasParentScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A special intermediate scope used for subqueries appearing in a `FROM`
|
/// A special intermediate scope used for nodes that don't see columns and
|
||||||
/// clause so that the subquery can't see outer columns and tables being added.
|
/// tables added to the statement they're in.
|
||||||
class SubqueryInFromScope extends ReferenceScope with _HasParentScope {
|
///
|
||||||
|
/// 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;
|
final StatementScope enclosingStatement;
|
||||||
|
|
||||||
SubqueryInFromScope(this.enclosingStatement);
|
SourceScope(this.enclosingStatement);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RootScope get rootScope => enclosingStatement.rootScope;
|
RootScope get rootScope => enclosingStatement.rootScope;
|
||||||
|
@ -321,8 +329,9 @@ class MiscStatementSubScope extends ReferenceScope with _HasParentScope {
|
||||||
RootScope get rootScope => parent.rootScope;
|
RootScope get rootScope => parent.rootScope;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ResultSetAvailableInStatement? resolveResultSet(String name) {
|
ResultSetAvailableInStatement? resolveResultSetForReference(String name) {
|
||||||
return additionalResultSets[name] ?? parent.resolveResultSet(name);
|
return additionalResultSets[name] ??
|
||||||
|
parent.resolveResultSetForReference(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -348,7 +357,7 @@ class SingleTableReferenceScope extends ReferenceScope {
|
||||||
RootScope get rootScope => parent.rootScope;
|
RootScope get rootScope => parent.rootScope;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ResultSetAvailableInStatement? resolveResultSet(String name) {
|
ResultSetAvailableInStatement? resolveResultSetForReference(String name) {
|
||||||
if (name == addedTableName) {
|
if (name == addedTableName) {
|
||||||
return addedTable;
|
return addedTable;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,17 +11,55 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitSelectStatement(SelectStatement e, ColumnResolverContext arg) {
|
void visitSelectStatement(SelectStatement e, ColumnResolverContext arg) {
|
||||||
// visit children first so that common table expressions are resolved
|
e.withClause?.accept(this, arg);
|
||||||
visitChildren(e, arg);
|
|
||||||
_resolveSelect(e, 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
|
@override
|
||||||
void visitCompoundSelectStatement(
|
void visitCompoundSelectStatement(
|
||||||
CompoundSelectStatement e, ColumnResolverContext arg) {
|
CompoundSelectStatement e, ColumnResolverContext arg) {
|
||||||
// first, visit all children so that the compound parts have their columns
|
e.base.accept(this, arg);
|
||||||
// resolved
|
visitList(e.additional, arg);
|
||||||
visitChildren(e, arg);
|
|
||||||
|
|
||||||
_resolveCompoundSelect(e);
|
_resolveCompoundSelect(e);
|
||||||
}
|
}
|
||||||
|
@ -29,30 +67,76 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
@override
|
@override
|
||||||
void visitValuesSelectStatement(
|
void visitValuesSelectStatement(
|
||||||
ValuesSelectStatement e, ColumnResolverContext arg) {
|
ValuesSelectStatement e, ColumnResolverContext arg) {
|
||||||
// visit children to resolve CTEs
|
e.withClause?.accept(this, arg);
|
||||||
visitChildren(e, arg);
|
|
||||||
|
|
||||||
_resolveValuesSelect(e);
|
_resolveValuesSelect(e);
|
||||||
|
|
||||||
|
// Still visit expressions because they could have subqueries that we need
|
||||||
|
// to handle.
|
||||||
|
visitList(e.values, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitCommonTableExpression(
|
void visitCommonTableExpression(
|
||||||
CommonTableExpression e, ColumnResolverContext arg) {
|
CommonTableExpression e, ColumnResolverContext arg) {
|
||||||
visitChildren(
|
// If we have a compound select statement as a CTE, resolve the initial
|
||||||
e,
|
// query first because the whole CTE will have those columns in the end.
|
||||||
const ColumnResolverContext(referencesUseNameOfReferencedColumn: false),
|
// 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;
|
void applyColumns(BaseSelectStatement source) {
|
||||||
|
final resolved = source.resolvedColumns!;
|
||||||
final names = e.columnNames;
|
final names = e.columnNames;
|
||||||
if (names != null && resolved != null && names.length != resolved.length) {
|
|
||||||
|
if (names == null) {
|
||||||
|
e.resolvedColumns = resolved;
|
||||||
|
} else {
|
||||||
|
if (names.length != resolved.length) {
|
||||||
context.reportError(AnalysisError(
|
context.reportError(AnalysisError(
|
||||||
type: AnalysisErrorType.cteColumnCountMismatch,
|
type: AnalysisErrorType.cteColumnCountMismatch,
|
||||||
message: 'This CTE declares ${names.length} columns, but its select '
|
message:
|
||||||
|
'This CTE declares ${names.length} columns, but its select '
|
||||||
'statement actually returns ${resolved.length}.',
|
'statement actually returns ${resolved.length}.',
|
||||||
relevantNode: e,
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -70,10 +154,9 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitTableReference(TableReference e, void arg) {
|
void visitForeignKeyClause(ForeignKeyClause e, ColumnResolverContext arg) {
|
||||||
if (e.resolved == null) {
|
_resolveTableReference(e.foreignTable, arg);
|
||||||
_resolveTableReference(e);
|
visitExcept(e, e.foreignTable, arg);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -100,13 +183,15 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
_resolveReturningClause(e, e.table.resultSet, arg);
|
_resolveReturningClause(e, e.table.resultSet, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultSet? _addIfResolved(AstNode node, TableReference ref) {
|
ResultSet? _addIfResolved(
|
||||||
final table = _resolveTableReference(ref);
|
AstNode node, TableReference ref, ColumnResolverContext arg) {
|
||||||
if (table != null) {
|
final availableColumns = <Column>[];
|
||||||
node.statementScope.expansionOfStarColumn = table.resolvedColumns;
|
_handle(ref, availableColumns, arg);
|
||||||
}
|
|
||||||
|
|
||||||
return table;
|
final scope = node.statementScope;
|
||||||
|
scope.expansionOfStarColumn = availableColumns;
|
||||||
|
|
||||||
|
return ref.resultSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -114,11 +199,11 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
// Resolve CTEs first
|
// Resolve CTEs first
|
||||||
e.withClause?.accept(this, arg);
|
e.withClause?.accept(this, arg);
|
||||||
|
|
||||||
final into = _addIfResolved(e, e.table);
|
_handle(e.table, [], arg);
|
||||||
for (final child in e.childNodes) {
|
for (final child in e.childNodes) {
|
||||||
if (child != e.withClause) visit(child, arg);
|
if (child != e.withClause) visit(child, arg);
|
||||||
}
|
}
|
||||||
_resolveReturningClause(e, into, arg);
|
_resolveReturningClause(e, e.table.resultSet, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -126,7 +211,7 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
// Resolve CTEs first
|
// Resolve CTEs first
|
||||||
e.withClause?.accept(this, arg);
|
e.withClause?.accept(this, arg);
|
||||||
|
|
||||||
final from = _addIfResolved(e, e.from);
|
final from = _addIfResolved(e, e.from, arg);
|
||||||
for (final child in e.childNodes) {
|
for (final child in e.childNodes) {
|
||||||
if (child != e.withClause) visit(child, arg);
|
if (child != e.withClause) visit(child, arg);
|
||||||
}
|
}
|
||||||
|
@ -168,31 +253,10 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
stmt.returnedResultSet = CustomResultSet(columns);
|
stmt.returnedResultSet = CustomResultSet(columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// Visits a [queryable] appearing in a `FROM` clause under the state [state].
|
||||||
void visitCreateTriggerStatement(
|
///
|
||||||
CreateTriggerStatement e, ColumnResolverContext arg) {
|
/// This also adds columns contributed to the resolved source to
|
||||||
final table = _resolveTableReference(e.onTable);
|
/// [availableColumns], which is later used to expand `*` parameters.
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handle(Queryable queryable, List<Column> availableColumns,
|
void _handle(Queryable queryable, List<Column> availableColumns,
|
||||||
ColumnResolverContext state) {
|
ColumnResolverContext state) {
|
||||||
void addColumns(Iterable<Column> columns) {
|
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(
|
queryable.when(
|
||||||
isTable: (table) {
|
isTable: (table) {
|
||||||
final resolved = _resolveTableReference(table);
|
final resolved = _resolveTableReference(table, state);
|
||||||
|
markAvailableResultSet(
|
||||||
|
table, resolved ?? table, table.as ?? table.tableName);
|
||||||
|
|
||||||
if (resolved != null) {
|
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!);
|
addColumns(table.resultSet!.resolvedColumns!);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSelect: (select) {
|
isSelect: (select) {
|
||||||
|
markAvailableResultSet(select, select.statement, select.as);
|
||||||
|
|
||||||
// Inside subqueries, references don't take the name of the referenced
|
// Inside subqueries, references don't take the name of the referenced
|
||||||
// column.
|
// column.
|
||||||
final childState =
|
final childState = ColumnResolverContext(
|
||||||
ColumnResolverContext(referencesUseNameOfReferencedColumn: false);
|
referencesUseNameOfReferencedColumn: false,
|
||||||
|
inDefinitionOfCte: state.inDefinitionOfCte,
|
||||||
// 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 stmt = select.statement;
|
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!);
|
addColumns(stmt.resolvedColumns!);
|
||||||
},
|
},
|
||||||
isJoin: (join) {
|
isJoin: (joinClause) {
|
||||||
_handle(join.primary, availableColumns, state);
|
_handle(joinClause.primary, availableColumns, state);
|
||||||
for (final query in join.joins.map((j) => j.query)) {
|
for (final join in joinClause.joins) {
|
||||||
_handle(query, availableColumns, state);
|
_handle(join.query, availableColumns, state);
|
||||||
|
|
||||||
|
final constraint = join.constraint;
|
||||||
|
if (constraint is OnConstraint) {
|
||||||
|
visit(constraint.expression, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isTableFunction: (function) {
|
isTableFunction: (function) {
|
||||||
|
@ -251,6 +326,9 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
.engineOptions.addedTableFunctions[function.name.toLowerCase()];
|
.engineOptions.addedTableFunctions[function.name.toLowerCase()];
|
||||||
final resolved = handler?.resolveTableValued(context, function);
|
final resolved = handler?.resolveTableValued(context, function);
|
||||||
|
|
||||||
|
markAvailableResultSet(
|
||||||
|
function, resolved ?? function, function.as ?? function.name);
|
||||||
|
|
||||||
if (resolved == null) {
|
if (resolved == null) {
|
||||||
context.reportError(AnalysisError(
|
context.reportError(AnalysisError(
|
||||||
type: AnalysisErrorType.unknownFunction,
|
type: AnalysisErrorType.unknownFunction,
|
||||||
|
@ -290,7 +368,8 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
Iterable<Column>? visibleColumnsForStar;
|
Iterable<Column>? visibleColumnsForStar;
|
||||||
|
|
||||||
if (resultColumn.tableName != null) {
|
if (resultColumn.tableName != null) {
|
||||||
final tableResolver = scope.resolveResultSet(resultColumn.tableName!);
|
final tableResolver =
|
||||||
|
scope.resolveResultSetForReference(resultColumn.tableName!);
|
||||||
if (tableResolver == null) {
|
if (tableResolver == null) {
|
||||||
context.reportError(AnalysisError(
|
context.reportError(AnalysisError(
|
||||||
type: AnalysisErrorType.referencedUnknownTable,
|
type: AnalysisErrorType.referencedUnknownTable,
|
||||||
|
@ -361,7 +440,8 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (resultColumn is NestedStarResultColumn) {
|
} else if (resultColumn is NestedStarResultColumn) {
|
||||||
final target = scope.resolveResultSet(resultColumn.tableName);
|
final target =
|
||||||
|
scope.resolveResultSetForReference(resultColumn.tableName);
|
||||||
|
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
context.reportError(AnalysisError(
|
context.reportError(AnalysisError(
|
||||||
|
@ -458,24 +538,26 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
|
||||||
return span;
|
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;
|
final scope = r.scope;
|
||||||
|
|
||||||
// Try resolving to a top-level table in the schema and to a result set that
|
// Try resolving to a top-level table in the schema and to a result set that
|
||||||
// may have been added to the table
|
// may have been added to the table
|
||||||
final resolvedInSchema = scope.resolveResultSetToAdd(r.tableName);
|
final resolvedInSchema = scope.resolveResultSetToAdd(r.tableName);
|
||||||
final resolvedInQuery = scope.resolveResultSet(r.tableName);
|
|
||||||
final createdName = r.as;
|
final createdName = r.as;
|
||||||
|
|
||||||
// Prefer using a table that has already been added if this isn't the
|
if (resolvedInSchema != null) {
|
||||||
// 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) {
|
|
||||||
return r.resolved = createdName != null
|
return r.resolved = createdName != null
|
||||||
? TableAlias(resolvedInSchema, createdName)
|
? TableAlias(resolvedInSchema, createdName)
|
||||||
: resolvedInSchema;
|
: resolvedInSchema;
|
||||||
|
@ -528,6 +610,13 @@ class ColumnResolverContext {
|
||||||
/// column in subqueries or CTEs.
|
/// column in subqueries or CTEs.
|
||||||
final bool referencesUseNameOfReferencedColumn;
|
final bool referencesUseNameOfReferencedColumn;
|
||||||
|
|
||||||
const ColumnResolverContext(
|
/// The common table expressions that are currently being defined.
|
||||||
{this.referencesUseNameOfReferencedColumn = true});
|
///
|
||||||
|
/// 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);
|
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);
|
resolveIndexOfVariables(_foundVariables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void defaultInsertSource(InsertSource e, void arg) {
|
||||||
|
e.scope = SourceScope(e.parent!.statementScope);
|
||||||
|
visitChildren(e, arg);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitCreateTableStatement(CreateTableStatement e, void arg) {
|
void visitCreateTableStatement(CreateTableStatement e, void arg) {
|
||||||
final scope = e.scope = StatementScope.forStatement(context.rootScope, e);
|
final scope = e.scope = StatementScope.forStatement(context.rootScope, e);
|
||||||
|
@ -52,6 +58,7 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
// query: "SELECT * FROM demo d1,
|
// query: "SELECT * FROM demo d1,
|
||||||
// (SELECT * FROM demo i WHERE i.id = d1.id) d2;"
|
// (SELECT * FROM demo i WHERE i.id = d1.id) d2;"
|
||||||
// it won't work.
|
// it won't work.
|
||||||
|
|
||||||
final isInFROM = e.parent is Queryable;
|
final isInFROM = e.parent is Queryable;
|
||||||
StatementScope scope;
|
StatementScope scope;
|
||||||
|
|
||||||
|
@ -59,7 +66,7 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
final surroundingSelect = e.parents
|
final surroundingSelect = e.parents
|
||||||
.firstWhere((node) => node is HasFrom)
|
.firstWhere((node) => node is HasFrom)
|
||||||
.scope as StatementScope;
|
.scope as StatementScope;
|
||||||
scope = StatementScope(SubqueryInFromScope(surroundingSelect));
|
scope = StatementScope(SourceScope(surroundingSelect));
|
||||||
} else {
|
} else {
|
||||||
scope = StatementScope.forStatement(context.rootScope, e);
|
scope = StatementScope.forStatement(context.rootScope, e);
|
||||||
}
|
}
|
||||||
|
@ -107,37 +114,6 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
visitChildren(e, arg);
|
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
|
@override
|
||||||
void visitCommonTableExpression(CommonTableExpression e, void arg) {
|
void visitCommonTableExpression(CommonTableExpression e, void arg) {
|
||||||
StatementScope.cast(e.scope).additionalKnownTables[e.cteTableName] = e;
|
StatementScope.cast(e.scope).additionalKnownTables[e.cteTableName] = e;
|
||||||
|
|
|
@ -57,7 +57,7 @@ class ReferenceResolver
|
||||||
if (e.entityName != null) {
|
if (e.entityName != null) {
|
||||||
// first find the referenced table or view,
|
// first find the referenced table or view,
|
||||||
// then use the column on that 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;
|
final resultSet = entityResolver?.resultSet.resultSet;
|
||||||
|
|
||||||
if (resultSet == null) {
|
if (resultSet == null) {
|
||||||
|
|
|
@ -89,7 +89,7 @@ class JoinModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The boolean arg indicates whether a visited queryable is needed for the
|
// 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> {
|
class _FindNonNullableJoins extends RecursiveVisitor<bool, void> {
|
||||||
final List<ResultSetAvailableInStatement> nonNullable = [];
|
final List<ResultSetAvailableInStatement> nonNullable = [];
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,8 @@ class CommonTableExpression extends AstNode with ResultSet {
|
||||||
Token? asToken;
|
Token? asToken;
|
||||||
IdentifierToken? tableNameToken;
|
IdentifierToken? tableNameToken;
|
||||||
|
|
||||||
List<CommonTableExpressionColumn>? _cachedColumns;
|
@override
|
||||||
|
List<Column>? resolvedColumns;
|
||||||
|
|
||||||
CommonTableExpression({
|
CommonTableExpression({
|
||||||
required this.cteTableName,
|
required this.cteTableName,
|
||||||
|
@ -71,33 +72,6 @@ class CommonTableExpression extends AstNode with ResultSet {
|
||||||
@override
|
@override
|
||||||
Iterable<AstNode> get childNodes => [as];
|
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
|
@override
|
||||||
bool get visibleToChildren => true;
|
bool get visibleToChildren => true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,7 +270,6 @@ class SqlEngine {
|
||||||
final node = context.root;
|
final node = context.root;
|
||||||
node.scope = context.rootScope;
|
node.scope = context.rootScope;
|
||||||
|
|
||||||
try {
|
|
||||||
AstPreparingVisitor(context: context).start(node);
|
AstPreparingVisitor(context: context).start(node);
|
||||||
|
|
||||||
node
|
node
|
||||||
|
@ -283,9 +282,6 @@ class SqlEngine {
|
||||||
context.types2 = session.results!;
|
context.types2 = session.results!;
|
||||||
|
|
||||||
node.acceptWithoutArg(LintingVisitor(options, context));
|
node.acceptWithoutArg(LintingVisitor(options, context));
|
||||||
} catch (_) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
description: Parses sqlite statements and performs static analysis on them
|
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
|
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
|
||||||
repository: https://github.com/simolus3/drift
|
repository: https://github.com/simolus3/drift
|
||||||
#homepage: https://drift.simonbinder.eu/
|
#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'", () {
|
test("DO UPDATE action in upsert can refer to 'exluded'", () {
|
||||||
final context = engine.analyze('''
|
final context = engine.analyze('''
|
||||||
INSERT INTO demo VALUES (?, ?)
|
INSERT INTO demo VALUES (?, ?)
|
||||||
|
@ -110,6 +125,12 @@ INSERT INTO demo VALUES (?, ?)
|
||||||
expect(context.errors, isEmpty);
|
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', () {
|
test('columns from values statement', () {
|
||||||
final context = engine.analyze("VALUES ('foo', 3), ('bar', 5)");
|
final context = engine.analyze("VALUES ('foo', 3), ('bar', 5)");
|
||||||
|
|
||||||
|
@ -127,6 +148,33 @@ INSERT INTO demo VALUES (?, ?)
|
||||||
expect(context.errors, isEmpty);
|
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', () {
|
test('gracefully handles tuples of different lengths in VALUES', () {
|
||||||
final context = engine.analyze("VALUES ('foo', 3), ('bar')");
|
final context = engine.analyze("VALUES ('foo', 3), ('bar')");
|
||||||
|
|
||||||
|
@ -270,4 +318,25 @@ INSERT INTO demo VALUES (?, ?)
|
||||||
.root as SelectStatement;
|
.root as SelectStatement;
|
||||||
expect(cte.resolvedColumns?.map((e) => e.name), ['RoWiD']);
|
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);
|
.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', () {
|
group('aliased source tables', () {
|
||||||
test('insert', () {
|
test('insert', () {
|
||||||
engine.analyze('INSERT INTO demo AS d VALUES (?, ?)').expectNoError();
|
engine.analyze('INSERT INTO demo AS d VALUES (?, ?)').expectNoError();
|
||||||
|
|
|
@ -19,17 +19,17 @@ void main() {
|
||||||
final model = JoinModel.of(stmt)!;
|
final model = JoinModel.of(stmt)!;
|
||||||
expect(
|
expect(
|
||||||
model.isNullableTable(
|
model.isNullableTable(
|
||||||
stmt.scope.resolveResultSet('a1')!.resultSet.resultSet!),
|
stmt.scope.resolveResultSetForReference('a1')!.resultSet.resultSet!),
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
model.isNullableTable(
|
model.isNullableTable(
|
||||||
stmt.scope.resolveResultSet('a2')!.resultSet.resultSet!),
|
stmt.scope.resolveResultSetForReference('a2')!.resultSet.resultSet!),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
model.isNullableTable(
|
model.isNullableTable(
|
||||||
stmt.scope.resolveResultSet('a3')!.resultSet.resultSet!),
|
stmt.scope.resolveResultSetForReference('a3')!.resultSet.resultSet!),
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue