mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
Deleting more old code
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/backends.dart';
|
||||
import 'package:drift/drift.dart' show OpeningDetails;
|
||||
|
||||
/// A query executor is responsible for executing statements on a database and
|
||||
/// return their results in a raw form.
|
||||
///
|
||||
/// This is an internal api of drift, which can break often. If you want to
|
||||
/// implement custom database backends, consider using the new `backends` API.
|
||||
/// The [NativeDatabase implementation](https://github.com/simolus3/drift/blob/develop/drift/lib/src/sqlite3/database.dart)
|
||||
/// might be useful as a reference. If you want to write your own database
|
||||
/// engine to use with drift and run into issues, please consider creating an
|
||||
/// issue.
|
||||
///
|
||||
/// If you want to wrap an existing [QueryExecutor], e.g. to change its
|
||||
/// behavior for some methods or to add logs in a custom format, consider using
|
||||
/// the [`QueryInterceptor` API](https://drift.simonbinder.eu/docs/examples/tracing/).
|
||||
abstract class QueryExecutor {
|
||||
/// The [SqlDialect] to use for this database engine.
|
||||
SqlDialect get dialect;
|
||||
|
||||
/// Opens the executor, if it has not yet been opened.
|
||||
Future<bool> ensureOpen(QueryExecutorUser user);
|
||||
|
||||
/// Runs a select statement with the given variables and returns the raw
|
||||
/// results.
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args);
|
||||
|
||||
/// Runs an insert statement with the given variables. Returns the row id or
|
||||
/// the auto_increment id of the inserted row.
|
||||
Future<int> runInsert(String statement, List<Object?> args);
|
||||
|
||||
/// Runs an update statement with the given variables and returns how many
|
||||
/// rows where affected.
|
||||
Future<int> runUpdate(String statement, List<Object?> args);
|
||||
|
||||
/// Runs an delete statement and returns how many rows where affected.
|
||||
Future<int> runDelete(String statement, List<Object?> args);
|
||||
|
||||
/// Runs a custom SQL statement without any variables. The result of that
|
||||
/// statement will be ignored.
|
||||
Future<void> runCustom(String statement, [List<Object?>? args]);
|
||||
|
||||
/// Prepares and runs [statements].
|
||||
///
|
||||
/// Running them doesn't need to happen in a transaction. When using drift's
|
||||
/// batch api, drift will call this method from a transaction either way. This
|
||||
/// method mainly exists to save duplicate parsing costs, allowing each
|
||||
/// statement to be prepared only once.
|
||||
Future<void> runBatched(BatchedStatements statements);
|
||||
|
||||
/// Starts a [TransactionExecutor].
|
||||
TransactionExecutor beginTransaction();
|
||||
|
||||
/// Returns a new [QueryExecutor] that, when first opened, takes an exclusive
|
||||
/// lock over `this` executor and prevents queries from running until it is
|
||||
/// closed.
|
||||
///
|
||||
/// The difference between this and [beginTransaction] is that this does not
|
||||
/// start a database transaction. The [QueryExecutor] returned by
|
||||
/// [beginExclusive] can be used to start a transaction with
|
||||
/// [beginTransaction].
|
||||
QueryExecutor beginExclusive();
|
||||
|
||||
/// Closes this database connection and releases all resources associated with
|
||||
/// it. Implementations should also handle [close] calls in a state where the
|
||||
/// database isn't open.
|
||||
Future<void> close() async {
|
||||
// no-op per default for backwards compatibility
|
||||
}
|
||||
}
|
||||
|
||||
/// Callbacks passed to [QueryExecutor.ensureOpen] to run schema migrations when
|
||||
/// the database is first opened.
|
||||
abstract class QueryExecutorUser {
|
||||
/// The schema version to set on the database when it's opened.
|
||||
int get schemaVersion;
|
||||
|
||||
/// A callbacks that runs after the database connection has been established,
|
||||
/// but before any other query is sent.
|
||||
///
|
||||
/// The query executor will wait for this future to complete before running
|
||||
/// any other query. Queries running on the [executor] are an exception to
|
||||
/// this, they can be used to run migrations.
|
||||
/// No matter how often [QueryExecutor.ensureOpen] is called, this method will
|
||||
/// not be called more than once.
|
||||
Future<void> beforeOpen(QueryExecutor executor, OpeningDetails details);
|
||||
}
|
||||
|
||||
const _equality = ListEquality<Object?>();
|
||||
|
||||
/// Stores information needed to run batched statements in the order they were
|
||||
/// issued without preparing statements multiple times.
|
||||
class BatchedStatements {
|
||||
/// All sql statements that need to be prepared.
|
||||
///
|
||||
/// A statement might run multiple times with different arguments.
|
||||
final List<String> statements;
|
||||
|
||||
/// Stores which sql statement should be run with what arguments.
|
||||
final List<ArgumentsForBatchedStatement> arguments;
|
||||
|
||||
/// Creates a collection of batched statements by splitting the sql and the
|
||||
/// bound arguments.
|
||||
BatchedStatements(this.statements, this.arguments);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(_equality.hash(statements), _equality.hash(arguments));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BatchedStatements &&
|
||||
_equality.equals(other.statements, statements) &&
|
||||
_equality.equals(other.arguments, arguments);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BatchedStatements($statements, $arguments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Instruction to run a batched sql statement with the arguments provided.
|
||||
class ArgumentsForBatchedStatement {
|
||||
/// Index of the sql statement in the [BatchedStatements.statements] of the
|
||||
/// [BatchedStatements] containing this argument set.
|
||||
final int statementIndex;
|
||||
|
||||
/// Bound arguments for the referenced statement.
|
||||
final List<Object?> arguments;
|
||||
|
||||
/// Used internally by drift.
|
||||
ArgumentsForBatchedStatement(this.statementIndex, this.arguments);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(statementIndex, _equality);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ArgumentsForBatchedStatement &&
|
||||
other.statementIndex == statementIndex &&
|
||||
_equality.equals(other.arguments, arguments);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArgumentsForBatchedStatement($statementIndex, $arguments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// A [QueryExecutor] that runs multiple queries atomically.
|
||||
abstract class TransactionExecutor extends QueryExecutor {
|
||||
/// Whether this transaction executor supports nesting transactions by calling
|
||||
/// [beginTransaction] on it.
|
||||
bool get supportsNestedTransactions;
|
||||
|
||||
/// Completes the transaction. No further queries may be sent to to this
|
||||
/// [QueryExecutor] after this method was called.
|
||||
///
|
||||
/// This may be called before [ensureOpen] was awaited, implementations must
|
||||
/// support this. That state implies that no query was sent, so it should be
|
||||
/// a no-op.
|
||||
Future<void> send();
|
||||
|
||||
/// Cancels this transaction. No further queries may be sent ot this
|
||||
/// [QueryExecutor] after this method was called.
|
||||
///
|
||||
/// This may be called before [ensureOpen] was awaited, implementations must
|
||||
/// support this. That state implies that no query was sent, so it should be
|
||||
/// a no-op.
|
||||
Future<void> rollback();
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
import 'dart:async' show FutureOr;
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/runtime/executor/helpers/results.dart';
|
||||
|
||||
String _defaultSavepoint(int depth) => 'SAVEPOINT s$depth';
|
||||
|
||||
String _defaultRelease(int depth) => 'RELEASE s$depth';
|
||||
|
||||
String _defaultRollbackToSavepoint(int depth) => 'ROLLBACK TO s$depth';
|
||||
|
||||
/// An interface that supports sending database queries. Used as a backend for
|
||||
/// drift.
|
||||
///
|
||||
/// Database implementations should support the following types both for
|
||||
/// variables and result sets:
|
||||
/// - [int]
|
||||
/// - [double]
|
||||
/// - [String]
|
||||
/// - [Uint8List]
|
||||
abstract class DatabaseDelegate extends QueryDelegate {
|
||||
/// Whether the database managed by this delegate is in a transaction at the
|
||||
/// moment. This field is only set when the [transactionDelegate] is a
|
||||
/// [NoTransactionDelegate], because in that case transactions are run on
|
||||
/// this delegate.
|
||||
bool isInTransaction = false;
|
||||
|
||||
/// Returns an appropriate class to resolve the current schema version in
|
||||
/// this database.
|
||||
///
|
||||
/// Common implementations will be:
|
||||
/// - [NoVersionDelegate] for databases without a schema version (such as an
|
||||
/// MySql server we connect to)
|
||||
/// - [OnOpenVersionDelegate] for databases whose schema version can only be
|
||||
/// set while opening it (such as sqflite)
|
||||
/// - [DynamicVersionDelegate] for databases where drift can set the schema
|
||||
/// version at any time (used for the web and VM implementation)
|
||||
DbVersionDelegate get versionDelegate;
|
||||
|
||||
/// The way this database engine starts transactions.
|
||||
TransactionDelegate get transactionDelegate;
|
||||
|
||||
/// A future that completes with `true` when this database is open and with
|
||||
/// `false` when its not. The future may never complete with an error or with
|
||||
/// null. It should return relatively quickly, as drift queries it before each
|
||||
/// statement it sends to the database.
|
||||
FutureOr<bool> get isOpen;
|
||||
|
||||
/// Opens the database. Drift will only call this when [isOpen] has returned
|
||||
/// false before. Further, drift will not attempt to open a database multiple
|
||||
/// times, so you don't have to worry about a connection being created
|
||||
/// multiple times.
|
||||
///
|
||||
/// The [QueryExecutorUser] is the user-defined database annotated with
|
||||
/// [DriftDatabase]. It might be useful to read the
|
||||
/// [QueryExecutorUser.schemaVersion] if that information is required while
|
||||
/// opening the database.
|
||||
Future<void> open(QueryExecutorUser db);
|
||||
|
||||
/// Closes this database. When the future completes, all resources used
|
||||
/// by this database should have been disposed.
|
||||
Future<void> close() async {
|
||||
// default no-op implementation
|
||||
}
|
||||
|
||||
/// Callback from drift after the database has been fully opened and all
|
||||
/// migrations ran.
|
||||
void notifyDatabaseOpened(OpeningDetails details) {
|
||||
// default no-op
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface which can execute sql statements.
|
||||
abstract class QueryDelegate {
|
||||
/// Prepares and executes the [statement], binding the variables to [args].
|
||||
/// Its safe to assume that the [statement] is a select statement, the
|
||||
/// [QueryResult] that it returns should be returned from here.
|
||||
///
|
||||
/// If the statement can't be executed, an exception should be thrown. See
|
||||
/// the class documentation of [DatabaseDelegate] on what types are supported.
|
||||
Future<QueryResult> runSelect(String statement, List<Object?> args);
|
||||
|
||||
/// Prepares and executes the [statement] with the variables bound to [args].
|
||||
/// The statement will either be an `UPDATE` or `DELETE` statement.
|
||||
///
|
||||
/// If the statement completes successfully, the amount of changed rows should
|
||||
/// be returned, or `0` if no rows where updated. Should throw if the
|
||||
/// statement can't be executed.
|
||||
Future<int> runUpdate(String statement, List<Object?> args);
|
||||
|
||||
/// Prepares and executes the [statement] with the variables bound to [args].
|
||||
/// The statement will be an `INSERT` statement.
|
||||
///
|
||||
/// If the statement completes successfully, the insert id of the row can be
|
||||
/// returned. If that information is not available, `null` can be returned.
|
||||
/// The method should throw if the statement can't be executed.
|
||||
Future<int> runInsert(String statement, List<Object?> args);
|
||||
|
||||
/// Runs a custom [statement] with the given [args]. Ignores all results, but
|
||||
/// throws when the statement can't be executed.
|
||||
Future<void> runCustom(String statement, List<Object?> args);
|
||||
|
||||
/// Runs multiple [statements] without having to prepare the same statement
|
||||
/// multiple times.
|
||||
///
|
||||
/// See also:
|
||||
/// - [QueryExecutor.runBatched].
|
||||
Future<void> runBatched(BatchedStatements statements) async {
|
||||
// default, inefficient implementation
|
||||
for (final application in statements.arguments) {
|
||||
final sql = statements.statements[application.statementIndex];
|
||||
|
||||
await runCustom(sql, application.arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface to start and manage transactions.
|
||||
sealed class TransactionDelegate {
|
||||
/// Const constructor on superclass
|
||||
const TransactionDelegate();
|
||||
}
|
||||
|
||||
/// A [TransactionDelegate] for database APIs which don't already support
|
||||
/// creating transactions. Drift will send a `BEGIN TRANSACTION` statement at
|
||||
/// the beginning, then block the database, and finally send a `COMMIT`
|
||||
/// statement at the end.
|
||||
final class NoTransactionDelegate extends TransactionDelegate {
|
||||
/// The statement that starts a transaction on this database engine.
|
||||
final String start;
|
||||
|
||||
/// The statement that commits a transaction on this database engine.
|
||||
final String commit;
|
||||
|
||||
/// The statement that will perform a rollback of a transaction on this
|
||||
/// database engine.
|
||||
final String rollback;
|
||||
|
||||
/// The statement that will create a savepoint for a given depth of a transaction
|
||||
/// on this database engine.
|
||||
final String Function(int depth) savepoint;
|
||||
|
||||
/// The statement that will release a savepoint for a given depth of a transaction
|
||||
/// on this database engine.
|
||||
final String Function(int depth) release;
|
||||
|
||||
/// The statement that will perform a rollback to a savepoint for a given depth
|
||||
/// of a transaction on this database engine.
|
||||
final String Function(int depth) rollbackToSavepoint;
|
||||
|
||||
/// Construct a transaction delegate indicating that native transactions
|
||||
/// aren't supported and need to be emulated by issuing statements and
|
||||
/// locking the database.
|
||||
const NoTransactionDelegate({
|
||||
this.start = 'BEGIN TRANSACTION',
|
||||
this.commit = 'COMMIT TRANSACTION',
|
||||
this.rollback = 'ROLLBACK TRANSACTION',
|
||||
this.savepoint = _defaultSavepoint,
|
||||
this.release = _defaultRelease,
|
||||
this.rollbackToSavepoint = _defaultRollbackToSavepoint,
|
||||
});
|
||||
}
|
||||
|
||||
/// A [TransactionDelegate] for database APIs which do support creating and
|
||||
/// managing transactions themselves.
|
||||
abstract class SupportedTransactionDelegate extends TransactionDelegate {
|
||||
/// Constant constructor on superclass
|
||||
const SupportedTransactionDelegate();
|
||||
|
||||
/// Whether [startTransaction] will ensure further requests to the parent
|
||||
/// database are delayed until the callback completes.
|
||||
///
|
||||
/// When this returns `false`, drift will manage a lock internally to ensure
|
||||
/// statements are only sent to the transaction while its active.
|
||||
///
|
||||
/// For implementations that support being in a transaction and outside of a
|
||||
/// transaction concurrently, this should return `true`.
|
||||
bool get managesLockInternally => true;
|
||||
|
||||
/// Start a transaction, which we assume implements [QueryDelegate], and call
|
||||
/// [run] with the transaction.
|
||||
///
|
||||
/// If [run] completes with an error, rollback. Otherwise, commit.
|
||||
///
|
||||
/// The returned future should complete once the transaction has been commited
|
||||
/// or was rolled back.
|
||||
FutureOr<void> startTransaction(Future Function(QueryDelegate) run);
|
||||
|
||||
/// An optional method used to implement nested transactions.
|
||||
///
|
||||
/// If the underlying database API supports nested transactions, this can be
|
||||
/// used to expose that functionality to drift. The method will only be called
|
||||
/// in [startTransaction] callbacks, and is otherwise expected to have a
|
||||
/// similiar behavior: `outer` is the delegate passed to the callback in
|
||||
/// [startTransaction], and `block` is the function that should run in a
|
||||
/// nested transaction.
|
||||
/// If it throws, the nested transaction should be rolled back.
|
||||
FutureOr<void> Function(
|
||||
QueryDelegate outer,
|
||||
Future<void> Function(QueryDelegate) block,
|
||||
)? get startNested => null;
|
||||
}
|
||||
|
||||
/// A [TransactionDelegate] for database APIs that have it's own transaction
|
||||
/// function
|
||||
@Deprecated('Use SupportedTransactionDelegate instead')
|
||||
abstract class WrappedTransactionDelegate extends SupportedTransactionDelegate {
|
||||
/// Constant constructor on superclass
|
||||
const WrappedTransactionDelegate();
|
||||
|
||||
@override
|
||||
bool get managesLockInternally => false;
|
||||
|
||||
@override
|
||||
FutureOr<void> startTransaction(Future Function(QueryDelegate p1) run) async {
|
||||
await runInTransaction(run);
|
||||
}
|
||||
|
||||
/// Start a transaction, which we assume implements [QueryDelegate], and call
|
||||
/// [run] with the transaction.
|
||||
///
|
||||
/// If [run] completes with an error, rollback. Otherwise, commit.
|
||||
Future runInTransaction(Future Function(QueryDelegate) run);
|
||||
}
|
||||
|
||||
/// An interface that supports setting the database version.
|
||||
sealed class DbVersionDelegate {
|
||||
/// Constant constructor on superclass
|
||||
const DbVersionDelegate();
|
||||
}
|
||||
|
||||
/// A database that doesn't support setting schema versions.
|
||||
final class NoVersionDelegate extends DbVersionDelegate {
|
||||
/// Delegate indicating that the underlying database does not support schema
|
||||
/// versions.
|
||||
const NoVersionDelegate();
|
||||
}
|
||||
|
||||
/// A database that only support setting the schema version while being opened.
|
||||
final class OnOpenVersionDelegate extends DbVersionDelegate {
|
||||
/// Function that returns with the current schema version.
|
||||
final Future<int> Function() loadSchemaVersion;
|
||||
|
||||
/// See [OnOpenVersionDelegate].
|
||||
const OnOpenVersionDelegate(this.loadSchemaVersion);
|
||||
}
|
||||
|
||||
/// A database that supports setting the schema version at any time.
|
||||
abstract class DynamicVersionDelegate extends DbVersionDelegate {
|
||||
/// See [DynamicVersionDelegate]
|
||||
const DynamicVersionDelegate();
|
||||
|
||||
/// Load the current schema version stored in this database.
|
||||
Future<int> get schemaVersion;
|
||||
|
||||
/// Writes the schema [version] to the database.
|
||||
Future<void> setSchemaVersion(int version);
|
||||
}
|
||||
@@ -1,627 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../../../utils/synchronized.dart';
|
||||
import '../../cancellation_zone.dart';
|
||||
import 'delegates.dart';
|
||||
|
||||
abstract class _BaseExecutor extends QueryExecutor {
|
||||
final Lock _lock = Lock();
|
||||
|
||||
/// When a transaction is active in this executor and we're using statement
|
||||
/// based transactions (`BEGIN` and `COMMIT`), statements _not_ targetting the
|
||||
/// transaction need to wait for the transaction to be completed before being
|
||||
/// sent. This is also true for databases which otherwise aren't sequential.
|
||||
int _waitingChildExecutors = 0;
|
||||
|
||||
QueryDelegate get impl;
|
||||
|
||||
bool get isSequential => false;
|
||||
|
||||
bool get logStatements => false;
|
||||
|
||||
/// Used to provide better error messages when calling operations without
|
||||
/// calling [ensureOpen] before.
|
||||
bool _ensureOpenCalled = false;
|
||||
|
||||
/// Whether this executor has explicitly been closed.
|
||||
bool _closed = false;
|
||||
|
||||
bool _debugCheckIsOpen() {
|
||||
if (!_ensureOpenCalled) {
|
||||
throw StateError('''
|
||||
Tried to run an operation without first calling QueryExecutor.ensureOpen()!
|
||||
|
||||
If you're seeing this exception from a drift database, it may indicate a bug in
|
||||
drift itself. Please consider opening an issue with the stack trace and details
|
||||
on how to reproduce this.''');
|
||||
}
|
||||
|
||||
if (_closed) {
|
||||
throw StateError('''
|
||||
This database or transaction runner has already been closed and may not be used
|
||||
anymore.
|
||||
|
||||
If this is happening in a transaction, you might be using the transaction
|
||||
without awaiting every statement in it.''');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<T> _synchronized<T>(Future<T> Function() action,
|
||||
{bool abortIfCancelled = true}) {
|
||||
if (isSequential || _waitingChildExecutors > 0) {
|
||||
return _lock.synchronized(() async {
|
||||
if (abortIfCancelled) checkIfCancelled();
|
||||
return await action();
|
||||
});
|
||||
} else {
|
||||
// support multiple operations in parallel, so just run right away
|
||||
return action();
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String sql, List<Object?> args) {
|
||||
if (logStatements) {
|
||||
driftRuntimeOptions.debugPrint('Drift: Sent $sql with args $args');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args) async {
|
||||
final result = await _synchronized(() {
|
||||
assert(_debugCheckIsOpen());
|
||||
_log(statement, args);
|
||||
return impl.runSelect(statement, args);
|
||||
});
|
||||
return result.asMap.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List<Object?> args) {
|
||||
return _synchronized(() {
|
||||
assert(_debugCheckIsOpen());
|
||||
_log(statement, args);
|
||||
return impl.runUpdate(statement, args);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List<Object?> args) {
|
||||
return _synchronized(() {
|
||||
assert(_debugCheckIsOpen());
|
||||
_log(statement, args);
|
||||
return impl.runUpdate(statement, args);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) {
|
||||
return _synchronized(() {
|
||||
assert(_debugCheckIsOpen());
|
||||
_log(statement, args);
|
||||
return impl.runInsert(statement, args);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List<Object?>? args]) {
|
||||
return _synchronized(() {
|
||||
assert(_debugCheckIsOpen());
|
||||
final resolvedArgs = args ?? const [];
|
||||
_log(statement, resolvedArgs);
|
||||
return impl.runCustom(statement, resolvedArgs);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) {
|
||||
return _synchronized(() {
|
||||
assert(_debugCheckIsOpen());
|
||||
if (logStatements) {
|
||||
driftRuntimeOptions
|
||||
.debugPrint('Drift: Executing $statements in a batch');
|
||||
}
|
||||
return impl.runBatched(statements);
|
||||
});
|
||||
}
|
||||
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context);
|
||||
|
||||
@override
|
||||
QueryExecutor beginExclusive() {
|
||||
return _ExclusiveExecutor(this);
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
return beginTransactionInContext(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TransactionExecutor extends _BaseExecutor
|
||||
implements TransactionExecutor {
|
||||
final DelegatedDatabase _db;
|
||||
|
||||
_TransactionExecutor(this._db);
|
||||
|
||||
void _checkCanOpen() {
|
||||
_ensureOpenCalled = true;
|
||||
|
||||
if (_closed) {
|
||||
throw StateError(
|
||||
"A transaction was used after being closed. Please check that you're "
|
||||
'awaiting all database operations inside a `transaction` block.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
|
||||
throw UnsupportedError("Nested transactions aren't supported.");
|
||||
}
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => _db.dialect;
|
||||
|
||||
@override
|
||||
bool get logStatements => _db.logStatements;
|
||||
|
||||
@override
|
||||
bool get isSequential => _db.isSequential;
|
||||
|
||||
@override
|
||||
bool get supportsNestedTransactions => false;
|
||||
}
|
||||
|
||||
/// A transaction implementation that sends `BEGIN` and `COMMIT` statements
|
||||
/// over the direct database implementation and blocks the main database for the
|
||||
/// duration of the transaction.
|
||||
class _StatementBasedTransactionExecutor extends _TransactionExecutor {
|
||||
final NoTransactionDelegate _delegate;
|
||||
Completer<bool>? _opened;
|
||||
final Completer<void> _done = Completer();
|
||||
|
||||
final _BaseExecutor _parent;
|
||||
|
||||
/// This value is greater than zero for nested transactions.
|
||||
///
|
||||
/// Nested transactions are implemented with savepoints created when the
|
||||
/// nested transaction is opened, allowing it to be rolled back with `ROLLBACK
|
||||
/// TO savepoint` without impacting the outer transaction.
|
||||
final int depth;
|
||||
|
||||
final String _startCommand;
|
||||
final String _commitCommand;
|
||||
final String _rollbackCommand;
|
||||
|
||||
// ignore: no_leading_underscores_for_local_identifiers
|
||||
_StatementBasedTransactionExecutor(super._db, this._parent, this._delegate)
|
||||
: _startCommand = _delegate.start,
|
||||
_commitCommand = _delegate.commit,
|
||||
_rollbackCommand = _delegate.rollback,
|
||||
depth = 0;
|
||||
|
||||
_StatementBasedTransactionExecutor.nested(
|
||||
super._db, this._parent, this._delegate, this.depth)
|
||||
: _startCommand = _delegate.savepoint(depth),
|
||||
_commitCommand = _delegate.release(depth),
|
||||
_rollbackCommand = _delegate.rollbackToSavepoint(depth);
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
_checkCanOpen();
|
||||
var opened = _opened;
|
||||
|
||||
if (opened == null) {
|
||||
opened = _opened = Completer();
|
||||
// Block the main database or the parent transaction while this
|
||||
// transaction is active.
|
||||
final parent = _parent;
|
||||
parent._waitingChildExecutors++;
|
||||
|
||||
unawaited(parent._synchronized(abortIfCancelled: false, () async {
|
||||
try {
|
||||
checkIfCancelled();
|
||||
await runCustom(_startCommand);
|
||||
_db.delegate.isInTransaction = true;
|
||||
_opened!.complete(true);
|
||||
} catch (e, s) {
|
||||
_opened!.completeError(e, s);
|
||||
|
||||
_release();
|
||||
}
|
||||
|
||||
// release the database lock after the transaction completes
|
||||
await _done.future;
|
||||
}).whenComplete(() => parent._waitingChildExecutors--));
|
||||
}
|
||||
|
||||
return opened.future;
|
||||
}
|
||||
|
||||
@override
|
||||
QueryDelegate get impl => _db.delegate;
|
||||
|
||||
@override
|
||||
bool get supportsNestedTransactions => true;
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
|
||||
return _StatementBasedTransactionExecutor.nested(
|
||||
_db, context, _delegate, depth + 1);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send() async {
|
||||
// don't do anything if the transaction completes before it was opened
|
||||
if (!_ensureOpenCalled) return;
|
||||
|
||||
await runCustom(_commitCommand, const []);
|
||||
_release();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rollback() async {
|
||||
if (!_ensureOpenCalled) return;
|
||||
|
||||
try {
|
||||
await runCustom(_rollbackCommand, const []);
|
||||
} finally {
|
||||
// Note: When send() is called and throws an exception, we don't mark this
|
||||
// transaction is closed (as the commit should either be retried or the
|
||||
// whole transaction should be aborted).
|
||||
// When aborting fails too, something is seriously wrong already. Let's
|
||||
// at least make sure that we don't block the rest of the db by pretending
|
||||
// the transaction is still open.
|
||||
_release();
|
||||
}
|
||||
}
|
||||
|
||||
void _release() {
|
||||
if (depth == 0) {
|
||||
_db.delegate.isInTransaction = false;
|
||||
}
|
||||
|
||||
_done.complete();
|
||||
_closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class _WrappingTransactionExecutor extends _TransactionExecutor {
|
||||
static final _artificialRollback =
|
||||
Exception('artificial exception to rollback the transaction');
|
||||
|
||||
@override
|
||||
late QueryDelegate impl;
|
||||
final SupportedTransactionDelegate _delegate;
|
||||
|
||||
/// If this is a nested transaction, the parent [QueryDelegate] to pass to
|
||||
/// [SupportedTransactionDelegate.startNested].
|
||||
final _WrappingTransactionExecutor? parentTransaction;
|
||||
|
||||
// We're doing some async hacks for database implementations which manage
|
||||
// transactions for us (e.g. sqflite where we do `transaction((t) => ...)`)
|
||||
// and can only use the transaction in that callback.
|
||||
// Since drift's executor API works somewhat differently, our callback starts
|
||||
// a completer which we await in that callback. Outside of that callback, we
|
||||
// use the transaction and finally complete the completer with a bogus value
|
||||
// or with an exception if we want to commit or rollback the transaction.
|
||||
//
|
||||
// This works fine, but there's a rare problem since `ensureOpen` is called by
|
||||
// the first operation _inside_ drift's `transaction` block, NOT by the
|
||||
// transaction block itself. In particular, if that first operation is a
|
||||
// select, the zone calling `ensureOpen` is a cancellable error zone. This
|
||||
// means that, in the case of a rollback (sent from an outer zone), an error
|
||||
// event would cross error zone boundaries. This is blocked by Dart's async
|
||||
// implementation, which replaces it with an uncaught error handler.
|
||||
// We _do_ want to handle those errors though, so we make sure that this
|
||||
// wrapping hack in `ensureOpen` runs in the zone that created this
|
||||
// transaction runner and not in the zone that does the first operation.
|
||||
final Zone _createdIn = Zone.current;
|
||||
|
||||
final Completer<void> _completerForCallback = Completer();
|
||||
Completer<void>? _opened, _finished;
|
||||
|
||||
_WrappingTransactionExecutor(super.db, this._delegate,
|
||||
{this.parentTransaction});
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
_checkCanOpen();
|
||||
var opened = _opened;
|
||||
_ensureOpenCalled = true;
|
||||
|
||||
if (opened == null) {
|
||||
_opened = opened = Completer();
|
||||
_createdIn.run(() {
|
||||
Future<void> launchTransaction() async {
|
||||
Future<void> transactionCallback(QueryDelegate transaction) async {
|
||||
opened!.complete();
|
||||
impl = transaction;
|
||||
await _completerForCallback.future;
|
||||
}
|
||||
|
||||
final result = switch (parentTransaction) {
|
||||
null => _delegate.startTransaction(transactionCallback),
|
||||
final parent => Future(() async {
|
||||
await parent.ensureOpen(user);
|
||||
await _delegate.startNested!(parent.impl, transactionCallback);
|
||||
}),
|
||||
};
|
||||
|
||||
if (result is Future) {
|
||||
_finished = Completer()
|
||||
..complete(
|
||||
// ignore: void_checks
|
||||
result
|
||||
// Ignore the exception caused by [rollback] which may be
|
||||
// rethrown by startTransaction
|
||||
.onError<Exception>((error, stackTrace) => null,
|
||||
test: (e) => e == _artificialRollback)
|
||||
// Consider this transaction closed after the call completes
|
||||
// This may happen without send/rollback being called in
|
||||
// case there's an exception when opening the transaction.
|
||||
.whenComplete(() => _closed = true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (_delegate.managesLockInternally) {
|
||||
return launchTransaction();
|
||||
} else {
|
||||
return _db._synchronized(launchTransaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The opened completer is never completed if `startTransaction` throws
|
||||
// before our callback is invoked (probably becaue `BEGIN` threw an
|
||||
// exception). In that case, _finished will complete with that error though.
|
||||
return Future.any([opened.future, if (_finished != null) _finished!.future])
|
||||
.then((value) => true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send() async {
|
||||
// don't do anything if the transaction completes before it was opened
|
||||
if (_opened == null || _closed) return;
|
||||
|
||||
_completerForCallback.complete();
|
||||
_closed = true;
|
||||
await _finished?.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rollback() async {
|
||||
// Note: This may be called after send() if send() throws (that is, the
|
||||
// transaction can't be completed). But if completing fails, we assume that
|
||||
// the transaction will implicitly be rolled back the underlying connection
|
||||
// (it's not like we could explicitly roll it back, we only have one
|
||||
// callback to implement).
|
||||
if (_opened == null || _closed) return;
|
||||
|
||||
_completerForCallback.completeError(_artificialRollback);
|
||||
_closed = true;
|
||||
await _finished?.future;
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
|
||||
if (_delegate.startNested != null) {
|
||||
return _WrappingTransactionExecutor(_db, _delegate,
|
||||
parentTransaction: this);
|
||||
} else {
|
||||
throw UnsupportedError('Nested transactions');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get supportsNestedTransactions => _delegate.startNested != null;
|
||||
}
|
||||
|
||||
/// A database engine (implements [QueryExecutor]) that delegates the relevant
|
||||
/// work to a [DatabaseDelegate].
|
||||
class DelegatedDatabase extends _BaseExecutor {
|
||||
/// The [DatabaseDelegate] to send queries to.
|
||||
final DatabaseDelegate delegate;
|
||||
(Object, StackTrace)? _migrationError;
|
||||
|
||||
@override
|
||||
bool logStatements;
|
||||
|
||||
@override
|
||||
final bool isSequential;
|
||||
|
||||
@override
|
||||
QueryDelegate get impl => delegate;
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => SqlDialect.sqlite;
|
||||
|
||||
final Lock _openingLock = Lock();
|
||||
|
||||
/// Constructs a delegated database by providing the [delegate].
|
||||
DelegatedDatabase(this.delegate,
|
||||
{bool? logStatements, this.isSequential = false})
|
||||
: logStatements = logStatements ?? false;
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
return _openingLock.synchronized(() async {
|
||||
if (_closed) {
|
||||
return Future.error(StateError(
|
||||
"Can't re-open a database after closing it. Please create a new "
|
||||
'database connection and open that instead.'));
|
||||
}
|
||||
|
||||
// If we have been unable to run migrations, the database is likely in an
|
||||
// inconsistent state and we should prevent subsequent operations on it.
|
||||
if (_migrationError case (var err, var trace)?) {
|
||||
Error.throwWithStackTrace(err, trace);
|
||||
}
|
||||
|
||||
final alreadyOpen = await delegate.isOpen;
|
||||
if (alreadyOpen) {
|
||||
_ensureOpenCalled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
await delegate.open(user);
|
||||
_ensureOpenCalled = true;
|
||||
|
||||
try {
|
||||
await _runMigrations(user);
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
_migrationError = (e, s);
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runMigrations(QueryExecutorUser user) async {
|
||||
final versionDelegate = delegate.versionDelegate;
|
||||
int? oldVersion;
|
||||
final currentVersion = user.schemaVersion;
|
||||
|
||||
if (versionDelegate is NoVersionDelegate) {
|
||||
// this one is easy. There is no version mechanism, so we don't run any
|
||||
// migrations. Assume database is on latest version.
|
||||
oldVersion = user.schemaVersion;
|
||||
} else if (versionDelegate is OnOpenVersionDelegate) {
|
||||
// version has already been set during open
|
||||
oldVersion = await versionDelegate.loadSchemaVersion();
|
||||
} else if (versionDelegate is DynamicVersionDelegate) {
|
||||
oldVersion = await versionDelegate.schemaVersion;
|
||||
// Note: We only update the schema version after migrations ran
|
||||
} else {
|
||||
throw Exception('Invalid delegate: $delegate. The versionDelegate getter '
|
||||
'must not subclass DBVersionDelegate directly');
|
||||
}
|
||||
|
||||
if (oldVersion == 0) {
|
||||
// some database implementations use version 0 to indicate that the
|
||||
// database was just created. We normalize that to null.
|
||||
oldVersion = null;
|
||||
}
|
||||
|
||||
final openingDetails = OpeningDetails(oldVersion, currentVersion);
|
||||
await user.beforeOpen(_BeforeOpeningExecutor(this), openingDetails);
|
||||
|
||||
if (versionDelegate is DynamicVersionDelegate &&
|
||||
oldVersion != currentVersion) {
|
||||
// set version now, after migrations ran successfully
|
||||
await versionDelegate.setSchemaVersion(currentVersion);
|
||||
}
|
||||
|
||||
delegate.notifyDatabaseOpened(openingDetails);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
|
||||
switch (delegate.transactionDelegate) {
|
||||
case NoTransactionDelegate noTransactionDelegate:
|
||||
return _StatementBasedTransactionExecutor(
|
||||
this, context, noTransactionDelegate);
|
||||
case SupportedTransactionDelegate supported:
|
||||
return _WrappingTransactionExecutor(this, supported);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
return _openingLock.synchronized(() {
|
||||
if (_ensureOpenCalled && !_closed) {
|
||||
_closed = true;
|
||||
|
||||
// Make sure the other methods throw an exception when used after
|
||||
// close()
|
||||
_ensureOpenCalled = false;
|
||||
return delegate.close();
|
||||
} else {
|
||||
// User never attempted to open the database, so this is a no-op.
|
||||
return Future.value();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Inside a `beforeOpen` callback, all drift apis must be available. At the
|
||||
/// same time, the `beforeOpen` callback must complete before any query sent
|
||||
/// outside of a `beforeOpen` callback can run. We do this by introducing a
|
||||
/// special executor that delegates all work to the original executor, but
|
||||
/// without blocking on `ensureOpen`
|
||||
class _BeforeOpeningExecutor extends _BaseExecutor {
|
||||
final DelegatedDatabase _base;
|
||||
|
||||
_BeforeOpeningExecutor(this._base);
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
|
||||
return _base.beginTransactionInContext(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(_) {
|
||||
_ensureOpenCalled = true;
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
QueryDelegate get impl => _base.impl;
|
||||
|
||||
@override
|
||||
bool get logStatements => _base.logStatements;
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => _base.dialect;
|
||||
}
|
||||
|
||||
final class _ExclusiveExecutor extends _BaseExecutor {
|
||||
final _BaseExecutor _outer;
|
||||
Completer<bool>? _opened;
|
||||
final Completer<void> _completer = Completer();
|
||||
|
||||
_ExclusiveExecutor(this._outer);
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => _outer.dialect;
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
if (_opened case var opened?) {
|
||||
return opened.future;
|
||||
} else {
|
||||
_ensureOpenCalled = true;
|
||||
final opened = _opened = Completer<bool>();
|
||||
_outer._waitingChildExecutors++;
|
||||
_outer._synchronized(() async {
|
||||
opened.complete(true);
|
||||
|
||||
// Keep the outer database locked until this statement completes.
|
||||
await _completer.future;
|
||||
_outer._waitingChildExecutors--;
|
||||
});
|
||||
|
||||
return opened.future;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
QueryDelegate get impl => _outer.impl;
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransactionInContext(_BaseExecutor context) {
|
||||
return _outer.beginTransactionInContext(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_completer.complete();
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/// A result from an select statement.
|
||||
class QueryResult {
|
||||
/// Names of the columns returned by the select statement.
|
||||
final List<String> columnNames;
|
||||
|
||||
/// The data returned by the select statement. Each list represents a row,
|
||||
/// which has the data in the same order as [columnNames].
|
||||
final List<List<Object?>> rows;
|
||||
|
||||
final Map<String, int> _columnIndexes;
|
||||
|
||||
/// Constructs a [QueryResult] by specifying the order of column names in
|
||||
/// [columnNames] and the associated data in [rows].
|
||||
QueryResult(this.columnNames, this.rows)
|
||||
: _columnIndexes = {
|
||||
for (var column in columnNames)
|
||||
column: columnNames.lastIndexOf(column)
|
||||
};
|
||||
|
||||
/// Converts the [rows] into [columnNames] and raw data [QueryResult.rows].
|
||||
/// We assume that each map in [rows] has the same keys.
|
||||
factory QueryResult.fromRows(List<Map<String, dynamic>> rows) {
|
||||
if (rows.isEmpty) {
|
||||
return QueryResult(const [], const []);
|
||||
}
|
||||
|
||||
final keys = rows.first.keys.toList();
|
||||
final mappedRows = [
|
||||
for (var row in rows) [for (var key in keys) row[key]]
|
||||
];
|
||||
|
||||
return QueryResult(keys, mappedRows);
|
||||
}
|
||||
|
||||
/// Returns a "list of maps" representation of this result set. Each map has
|
||||
/// the same keys - the [columnNames]. The values are the actual values in
|
||||
/// the row.
|
||||
Iterable<Map<String, dynamic>> get asMap {
|
||||
return rows.map((row) {
|
||||
return {
|
||||
for (var column in columnNames) column: row[_columnIndexes[column]!],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/runtime/executor/stream_queries.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Runs multiple statements transactionally.
|
||||
@internal
|
||||
class Transaction extends DatabaseConnectionUser {
|
||||
final DatabaseConnectionUser _parent;
|
||||
|
||||
@override
|
||||
// ignore: invalid_use_of_visible_for_overriding_member
|
||||
GeneratedDatabase get attachedDatabase => _parent.attachedDatabase;
|
||||
|
||||
/// Constructs a transaction executor from the [_parent] engine and the
|
||||
/// underlying [executor].
|
||||
Transaction(this._parent, TransactionExecutor executor)
|
||||
: super.delegate(
|
||||
_parent,
|
||||
executor: executor,
|
||||
streamQueries: _TransactionStreamStore(_parent.streamQueries),
|
||||
);
|
||||
|
||||
/// Instructs the underlying executor to execute this instructions. Batched
|
||||
/// table updates will also be send to the stream query store.
|
||||
Future<void> complete() async {
|
||||
await (executor as TransactionExecutor).send();
|
||||
}
|
||||
|
||||
/// Closes all streams created in this transactions and applies table updates
|
||||
/// to the main stream store.
|
||||
Future<void> disposeChildStreams() async {
|
||||
final streams = streamQueries as _TransactionStreamStore;
|
||||
await streams._dispatchAndClose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Special query engine to run the [MigrationStrategy.beforeOpen] callback.
|
||||
///
|
||||
/// To use this api, drift users should use the [MigrationStrategy.beforeOpen]
|
||||
/// parameter inside the [GeneratedDatabase.migration] getter.
|
||||
@internal
|
||||
class BeforeOpenRunner extends DatabaseConnectionUser {
|
||||
final DatabaseConnectionUser _parent;
|
||||
|
||||
@override
|
||||
// ignore: invalid_use_of_visible_for_overriding_member
|
||||
GeneratedDatabase get attachedDatabase => _parent.attachedDatabase;
|
||||
|
||||
/// Creates a [BeforeOpenRunner] from a [DatabaseConnectionUser] and the
|
||||
/// special [executor] running the queries.
|
||||
BeforeOpenRunner(this._parent, QueryExecutor executor)
|
||||
: super.delegate(_parent, executor: executor);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A subquery allows reading from another complex query in a join.
|
||||
///
|
||||
/// An existing query can be constructed via [DatabaseConnectionUser.select] or
|
||||
/// [DatabaseConnectionUser.selectOnly] and then wrapped in [Subquery] to be
|
||||
/// used in another query.
|
||||
///
|
||||
/// For instance, assuming database storing todo items with optional categories
|
||||
/// (through a reference from todo items to categories), this query uses a
|
||||
/// subquery to count how many of the top-10 todo items (by length) are in each
|
||||
/// category:
|
||||
///
|
||||
/// ```dart
|
||||
/// final longestTodos = Subquery(
|
||||
/// select(todosTable)
|
||||
/// ..orderBy([(row) => OrderingTerm.desc(row.title.length)])
|
||||
/// ..limit(10),
|
||||
/// 's',
|
||||
/// );
|
||||
///
|
||||
/// final itemCount = subquery.ref(todosTable.id).count();
|
||||
/// final query = select(categories).join([
|
||||
/// innerJoin(
|
||||
/// longestTodos,
|
||||
/// subquery.ref(todosTable.category).equalsExp(categories.id),
|
||||
/// useColumns: false,
|
||||
/// )])
|
||||
/// ..groupBy([categories.id])
|
||||
/// ..addColumns([itemCount]);
|
||||
/// ```
|
||||
///
|
||||
/// Note that the column from the subquery (here, the id of a todo entry) is not
|
||||
/// directly available in the outer query, it needs to be accessed through
|
||||
/// [Subquery.ref].
|
||||
/// Columns added to the top-level query (via [ref]) can be accessed directly
|
||||
/// through [TypedResult.read]. When columns from a subquery are added to the
|
||||
/// top-level select as well, [TypedResult.readTable] can be used to read an
|
||||
/// entire row from the subquery. It returns a nested [TypedResult] for the
|
||||
/// subquery.
|
||||
///
|
||||
/// See also: [subqueryExpression], for subqueries which only return one row and
|
||||
/// one column.
|
||||
class Subquery<Row> extends ResultSetImplementation<Subquery, Row>
|
||||
implements HasResultSet {
|
||||
/// The inner [select] statement of this subquery.
|
||||
final BaseSelectStatement<Row> select;
|
||||
@override
|
||||
final String entityName;
|
||||
|
||||
/// Creates a subqery from the inner [select] statement forming the base of
|
||||
/// the subquery and a unique name of this subquery in the statement being
|
||||
/// executed.
|
||||
Subquery(this.select, this.entityName);
|
||||
|
||||
/// Makes a column from the subquery available to the outer select statement.
|
||||
///
|
||||
/// For instance, consider a complex column like `subqueryContentLength` being
|
||||
/// added into a subquery:
|
||||
///
|
||||
/// ```dart
|
||||
/// final subqueryContentLength = todoEntries.content.length.sum();
|
||||
/// final subquery = Subquery(
|
||||
/// db.selectOnly(todoEntries)
|
||||
/// ..addColumns([todoEntries.category, subqueryContentLength])
|
||||
/// ..groupBy([todoEntries.category]),
|
||||
/// 's');
|
||||
/// ```
|
||||
///
|
||||
/// When the `subqueryContentLength` column gets written, drift will write
|
||||
/// the actual `SUM()` expression which is only valid in the subquery itself.
|
||||
/// When an outer query joining the subqery wants to read the column, it needs
|
||||
/// to refer to that expression by name. This is what [ref] is doing:
|
||||
///
|
||||
/// ```dart
|
||||
/// final readableLength = subquery.ref(subqueryContentLength);
|
||||
/// final query = selectOnly(categories)
|
||||
/// ..addColumns([categories.id, readableLength])
|
||||
/// ..join([
|
||||
/// innerJoin(subquery,
|
||||
/// subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// Here, [ref] is called two times: Once to obtain a column selected by the
|
||||
/// outer query and once as a join condition.
|
||||
///
|
||||
/// [ref] needs to be used every time a column from a subquery is used in an
|
||||
/// outer query, regardless of the context.
|
||||
Expression<T> ref<T extends Object>(Expression<T> inner) {
|
||||
final name = select._nameForColumn(inner);
|
||||
if (name == null) {
|
||||
throw ArgumentError(
|
||||
'The source select statement does not contain that column');
|
||||
}
|
||||
|
||||
return columnsByName[name]!.dartCast();
|
||||
}
|
||||
|
||||
@override
|
||||
late final List<GeneratedColumn<Object>> $columns = [
|
||||
for (final (expr, name) in select._expandedColumns)
|
||||
GeneratedColumn(
|
||||
name,
|
||||
entityName,
|
||||
true,
|
||||
type: expr.driftSqlType,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
late final Map<String, GeneratedColumn<Object>> columnsByName = {
|
||||
for (final column in $columns) column.name: column,
|
||||
};
|
||||
|
||||
@override
|
||||
Subquery get asDslTable => this;
|
||||
|
||||
@override
|
||||
DatabaseConnectionUser get attachedDatabase => (select as Query).database;
|
||||
|
||||
@override
|
||||
FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return select._mapRow(data.withoutPrefix(tablePrefix));
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../../dsl/dsl.dart';
|
||||
import '../../api/runtime_api.dart';
|
||||
import '../../utils.dart';
|
||||
import '../query_builder.dart';
|
||||
|
||||
/// In sqlite3, a table-valued function is a function that resolves to a result
|
||||
/// set, meaning that it can be selected from.
|
||||
///
|
||||
/// For more information on table-valued functions in general, visit their
|
||||
/// [documentation](https://sqlite.org/vtab.html#tabfunc2) on the sqlite website.
|
||||
///
|
||||
/// This class is meant to be extended for each table-valued function, so that
|
||||
/// the [Self] type parameter points to the actual implementation class. The
|
||||
/// class must also implement [createAlias] correctly (ensuring that every
|
||||
/// column has its [GeneratedColumn.tableName] set to the [aliasedName]).
|
||||
///
|
||||
/// For an example of a table-valued function in drift, see the
|
||||
/// `JsonTableFunction` in `package:drift/json1.dart`. It makes the `json_each`
|
||||
/// and `json_tree` table-valued functions available to drift.
|
||||
@experimental
|
||||
abstract base class TableValuedFunction<Self extends ResultSetImplementation>
|
||||
extends ResultSetImplementation<Self, TypedResult>
|
||||
implements HasResultSet, Component {
|
||||
final String _functionName;
|
||||
|
||||
/// The arguments passed to the table-valued function.
|
||||
final List<Expression> arguments;
|
||||
|
||||
@override
|
||||
final DatabaseConnectionUser attachedDatabase;
|
||||
|
||||
@override
|
||||
final List<GeneratedColumn<Object>> $columns;
|
||||
|
||||
@override
|
||||
final String aliasedName;
|
||||
|
||||
/// Constructor for table-valued functions.
|
||||
///
|
||||
/// This takes the [attachedDatabase] (used to interpret results), the name
|
||||
/// of the function as well as arguments passed to it and finally the schema
|
||||
/// of the table (in the form of [columns]).
|
||||
TableValuedFunction(
|
||||
this.attachedDatabase, {
|
||||
required String functionName,
|
||||
required this.arguments,
|
||||
required List<GeneratedColumn> columns,
|
||||
String? alias,
|
||||
}) : _functionName = functionName,
|
||||
$columns = columns,
|
||||
aliasedName = alias ?? functionName;
|
||||
|
||||
@override
|
||||
Self get asDslTable => this as Self;
|
||||
|
||||
@override
|
||||
late final Map<String, GeneratedColumn<Object>> columnsByName = {
|
||||
for (final column in $columns) column.name: column,
|
||||
};
|
||||
|
||||
@override
|
||||
String get entityName => _functionName;
|
||||
|
||||
@override
|
||||
FutureOr<TypedResult> map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final row = QueryRow(data.withoutPrefix(tablePrefix), attachedDatabase);
|
||||
return TypedResult(
|
||||
const {},
|
||||
row,
|
||||
{
|
||||
for (final column in $columns)
|
||||
column: attachedDatabase.typeMapping
|
||||
.read(column.type, row.data[column.name]),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer
|
||||
..write(_functionName)
|
||||
..write('(');
|
||||
|
||||
var first = true;
|
||||
for (final argument in arguments) {
|
||||
if (!first) {
|
||||
context.buffer.write(', ');
|
||||
}
|
||||
|
||||
argument.writeInto(context);
|
||||
first = false;
|
||||
}
|
||||
|
||||
context.buffer.write(')');
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A where clause in a select, update or delete statement.
|
||||
class Where extends Component {
|
||||
/// The expression that determines whether a given row should be included in
|
||||
/// the result.
|
||||
final Expression<bool> predicate;
|
||||
|
||||
/// Construct a [Where] clause from its [predicate].
|
||||
Where(this.predicate);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('WHERE ');
|
||||
predicate.writeInto(context);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => predicate.hashCode * 7;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is Where && other.predicate == predicate;
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Defines extension functions to express comparisons in sql
|
||||
extension ComparableExpr<DT extends Comparable<dynamic>> on Expression<DT> {
|
||||
/// Returns an expression that is true if this expression is strictly bigger
|
||||
/// than the other expression.
|
||||
Expression<bool> isBiggerThan(Expression<DT> other) {
|
||||
return _Comparison(this, _ComparisonOperator.more, other);
|
||||
}
|
||||
|
||||
/// Returns an expression that is true if this expression is strictly bigger
|
||||
/// than the other value.
|
||||
Expression<bool> isBiggerThanValue(DT other) {
|
||||
return isBiggerThan(variable(other));
|
||||
}
|
||||
|
||||
/// Returns an expression that is true if this expression is bigger than or
|
||||
/// equal to he other expression.
|
||||
Expression<bool> isBiggerOrEqual(Expression<DT> other) {
|
||||
return _Comparison(this, _ComparisonOperator.moreOrEqual, other);
|
||||
}
|
||||
|
||||
/// Returns an expression that is true if this expression is bigger than or
|
||||
/// equal to he other value.
|
||||
Expression<bool> isBiggerOrEqualValue(DT other) {
|
||||
return isBiggerOrEqual(variable(other));
|
||||
}
|
||||
|
||||
/// Returns an expression that is true if this expression is strictly smaller
|
||||
/// than the other expression.
|
||||
Expression<bool> isSmallerThan(Expression<DT> other) {
|
||||
return _Comparison(this, _ComparisonOperator.less, other);
|
||||
}
|
||||
|
||||
/// Returns an expression that is true if this expression is strictly smaller
|
||||
/// than the other value.
|
||||
Expression<bool> isSmallerThanValue(DT other) =>
|
||||
isSmallerThan(variable(other));
|
||||
|
||||
/// Returns an expression that is true if this expression is smaller than or
|
||||
/// equal to he other expression.
|
||||
Expression<bool> isSmallerOrEqual(Expression<DT> other) {
|
||||
return _Comparison(this, _ComparisonOperator.lessOrEqual, other);
|
||||
}
|
||||
|
||||
/// Returns an expression that is true if this expression is smaller than or
|
||||
/// equal to he other value.
|
||||
Expression<bool> isSmallerOrEqualValue(DT other) {
|
||||
return isSmallerOrEqual(variable(other));
|
||||
}
|
||||
|
||||
/// Returns an expression evaluating to true if this expression is between
|
||||
/// [lower] and [higher] (both inclusive).
|
||||
///
|
||||
/// If [not] is set, the expression will be negated. To compare this
|
||||
/// expression against two values, see
|
||||
Expression<bool> isBetween(Expression<DT> lower, Expression<DT> higher,
|
||||
{bool not = false}) {
|
||||
return _BetweenExpression(
|
||||
target: this, lower: lower, higher: higher, not: not);
|
||||
}
|
||||
|
||||
/// Returns an expression evaluating to true if this expression is between
|
||||
/// [lower] and [higher] (both inclusive).
|
||||
///
|
||||
/// If [not] is set, the expression will be negated.
|
||||
Expression<bool> isBetweenValues(DT lower, DT higher, {bool not = false}) {
|
||||
return _BetweenExpression(
|
||||
target: this,
|
||||
lower: variable(lower),
|
||||
higher: variable(higher),
|
||||
not: not,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BetweenExpression extends Expression<bool> {
|
||||
final Expression target;
|
||||
|
||||
// https://www.sqlite.org/lang_expr.html#between
|
||||
@override
|
||||
final Precedence precedence = Precedence.comparisonEq;
|
||||
|
||||
/// Whether to negate this between expression
|
||||
final bool not;
|
||||
|
||||
final Expression lower;
|
||||
final Expression higher;
|
||||
|
||||
_BetweenExpression(
|
||||
{required this.target,
|
||||
required this.lower,
|
||||
required this.higher,
|
||||
this.not = false});
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
var target = this.target;
|
||||
var lower = this.lower;
|
||||
var higher = this.higher;
|
||||
|
||||
// We don't want to compare datetime values lexicographically, so we convert
|
||||
// them to a comparable unit
|
||||
if (context.typeMapping.storeDateTimesAsText) {
|
||||
if (target is Expression<DateTime>) target = target.julianday;
|
||||
if (lower is Expression<DateTime>) lower = lower.julianday;
|
||||
if (higher is Expression<DateTime>) higher = higher.julianday;
|
||||
}
|
||||
|
||||
writeInner(context, target);
|
||||
|
||||
if (not) context.buffer.write(' NOT');
|
||||
context.buffer.write(' BETWEEN ');
|
||||
|
||||
writeInner(context, lower);
|
||||
context.buffer.write(' AND ');
|
||||
writeInner(context, higher);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(target, lower, higher, not);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _BetweenExpression &&
|
||||
other.target == target &&
|
||||
other.not == not &&
|
||||
other.lower == lower &&
|
||||
other.higher == higher;
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
const _equality = ListEquality<Object?>();
|
||||
|
||||
/// Base class for everything that can be used as a function parameter in sql.
|
||||
///
|
||||
/// Most prominently, this includes [Expression]s.
|
||||
///
|
||||
/// Used internally by drift.
|
||||
abstract class FunctionParameter implements Component {}
|
||||
|
||||
/// Any sql expression that evaluates to some generic value. This does not
|
||||
/// include queries (which might evaluate to multiple values) but individual
|
||||
/// columns, functions and operators.
|
||||
///
|
||||
/// To obtain the result of an [Expression], add it as a result column to a
|
||||
/// [JoinedSelectStatement], e.g. through [DatabaseConnectionUser.selectOnly]:
|
||||
///
|
||||
/// ```dart
|
||||
/// Expression<int> countUsers = users.id.count();
|
||||
///
|
||||
/// // Add the expression to a select statement to evaluate it.
|
||||
/// final query = selectOnly(users)..addColumns([countUsers]);
|
||||
/// final row = await query.getSingle();
|
||||
///
|
||||
/// // Use .read() on a row to read expressions.
|
||||
/// final amountOfUsers = query.read(counUsers);
|
||||
/// ```
|
||||
///
|
||||
/// It's important that all subclasses properly implement [hashCode] and
|
||||
/// [==].
|
||||
abstract class Expression<D extends Object> implements FunctionParameter {
|
||||
/// Constant constructor so that subclasses can be constant.
|
||||
const Expression();
|
||||
|
||||
/// The precedence of this expression. This can be used to automatically put
|
||||
/// parentheses around expressions as needed.
|
||||
Precedence get precedence => Precedence.unknown;
|
||||
|
||||
/// Whether this expression is a literal. Some use-sites need to put
|
||||
/// parentheses around non-literals.
|
||||
bool get isLiteral => false;
|
||||
|
||||
/// Whether this expression is equal to the given expression.
|
||||
///
|
||||
/// This generates an equals operator in SQL. To perform a comparison
|
||||
/// sensitive to `NULL` values, use [isExp] instead.
|
||||
Expression<bool> equalsExp(Expression<D> compare) =>
|
||||
_Comparison.equal(this, compare);
|
||||
|
||||
/// Whether this column is equal to the given value, which must have a fitting
|
||||
/// type. The [compare] value will be written
|
||||
/// as a variable using prepared statements, so there is no risk of
|
||||
/// an SQL-injection.
|
||||
///
|
||||
/// This method only supports comparing the value of the column to non-
|
||||
/// nullable values and translates to a direct `=` comparison in SQL.
|
||||
/// To compare this column to `null`, use [isValue].
|
||||
Expression<bool> equals(D compare) =>
|
||||
_Comparison.equal(this, variable(compare));
|
||||
|
||||
/// Compares the value of this column to [compare] or `null`.
|
||||
///
|
||||
/// When [compare] is null, this generates an `IS NULL` expression in SQL.
|
||||
/// For non-null values, an [equals] expression is generated.
|
||||
/// This means that, for this method, two null values are considered equal.
|
||||
/// This deviates from the usual notion in SQL that doesn't allow comparing
|
||||
/// `NULL` values with equals.
|
||||
Expression<bool> equalsNullable(D? compare) {
|
||||
if (compare == null) {
|
||||
return isNull();
|
||||
} else {
|
||||
return equals(compare);
|
||||
}
|
||||
}
|
||||
|
||||
/// Casts this expression to an expression of [D].
|
||||
///
|
||||
/// Calling [dartCast] will not affect the generated sql. In particular, it
|
||||
/// will __NOT__ generate a `CAST` expression in sql. To generate a `CAST`
|
||||
/// in sql, use [cast].
|
||||
///
|
||||
/// This method is used internally by drift.
|
||||
Expression<D2> dartCast<D2 extends Object>({CustomSqlType<D2>? customType}) {
|
||||
return _DartCastExpression<D, D2>(this, customType);
|
||||
}
|
||||
|
||||
/// Generates a `CAST(expression AS TYPE)` expression.
|
||||
///
|
||||
/// Note that this does not do a meaningful conversion for drift-only types
|
||||
/// like `bool` or `DateTime`. Both would simply generate a `CAST AS INT`
|
||||
/// expression.
|
||||
///
|
||||
/// The optional [type] parameter can be used to specify the SQL type to cast
|
||||
/// to. This is mainly useful for [CustomSqlType]s. For types supported by
|
||||
/// drift, [DriftSqlType.forType] will be used as a default.
|
||||
Expression<D2> cast<D2 extends Object>([BaseSqlType<D2>? type]) {
|
||||
return _CastInSqlExpression<D, D2>(
|
||||
this, type ?? DriftSqlType.forType<D2>());
|
||||
}
|
||||
|
||||
/// Generates an `IS` expression in SQL, comparing this expression with the
|
||||
/// Dart [value].
|
||||
///
|
||||
/// This is the SQL method most closely resembling the [Object.==] operator in
|
||||
/// Dart. When this expression and [value] are both non-null, this is the same
|
||||
/// as [equals]. Two `NULL` values are considered equal as well.
|
||||
Expression<bool> isValue(D value) {
|
||||
return isExp(variable(value));
|
||||
}
|
||||
|
||||
/// Generates an `IS NOT` expression in SQL, comparing this expression with
|
||||
/// the Dart [value].
|
||||
///
|
||||
/// This the inverse of [isValue].
|
||||
Expression<bool> isNotValue(D value) {
|
||||
return isNotExp(variable(value));
|
||||
}
|
||||
|
||||
/// Expression that is true if the inner expression resolves to a null value.
|
||||
Expression<bool> isNull() => isExp(const Constant(null));
|
||||
|
||||
/// Expression that is true if the inner expression resolves to a non-null
|
||||
/// value.
|
||||
Expression<bool> isNotNull() => isNotExp(const Constant(null));
|
||||
|
||||
/// Generates an `IS` expression in SQL, comparing this expression with the
|
||||
/// [other] expression.
|
||||
///
|
||||
/// This is the SQL method most closely resembling the [Object.==] operator in
|
||||
/// Dart. When this expression and [other] are both non-null, this is the same
|
||||
/// as [equalsExp]. Two `NULL` values are considered equal as well.
|
||||
Expression<bool> isExp(Expression<D> other) {
|
||||
return BaseInfixOperator(this, 'IS', other,
|
||||
precedence: Precedence.comparisonEq);
|
||||
}
|
||||
|
||||
/// Generates an `IS NOT` expression in SQL, comparing this expression with
|
||||
/// the [other] expression.
|
||||
///
|
||||
/// This the inverse of [isExp].
|
||||
Expression<bool> isNotExp(Expression<D> other) {
|
||||
return BaseInfixOperator(this, 'IS NOT', other,
|
||||
precedence: Precedence.comparisonEq);
|
||||
}
|
||||
|
||||
/// An expression that is true if `this` resolves to any of the values in
|
||||
/// [values].
|
||||
Expression<bool> isIn(Iterable<D> values) {
|
||||
return isInExp([for (final value in values) variable(value)]);
|
||||
}
|
||||
|
||||
/// An expression that is true if `this` does not resolve to any of the values
|
||||
/// in [values].
|
||||
Expression<bool> isNotIn(Iterable<D> values) {
|
||||
return isNotInExp([for (final value in values) variable(value)]);
|
||||
}
|
||||
|
||||
/// An expression that evaluates to `true` if this expression resolves to a
|
||||
/// value that one of the [expressions] resolve to as well.
|
||||
///
|
||||
/// For an "is in" comparison with values, use [isIn].
|
||||
Expression<bool> isInExp(List<Expression<D>> expressions) {
|
||||
if (expressions.isEmpty) {
|
||||
return Constant(false);
|
||||
}
|
||||
|
||||
return _InExpression(this, expressions, false);
|
||||
}
|
||||
|
||||
/// An expression that evaluates to `true` if this expression does not resolve
|
||||
/// to any value that the [expressions] resolve to.
|
||||
///
|
||||
/// For an "is not in" comparison with values, use [isNotIn].
|
||||
Expression<bool> isNotInExp(List<Expression<D>> expressions) {
|
||||
if (expressions.isEmpty) {
|
||||
return Constant(true);
|
||||
}
|
||||
|
||||
return _InExpression(this, expressions, true);
|
||||
}
|
||||
|
||||
/// An expression checking whether `this` is included in any row of the
|
||||
/// provided [select] statement.
|
||||
///
|
||||
/// The [select] statement may only have one column.
|
||||
Expression<bool> isInQuery(BaseSelectStatement select) {
|
||||
_checkSubquery(select);
|
||||
return _InSelectExpression(select, this, false);
|
||||
}
|
||||
|
||||
/// An expression checking whether `this` is _not_ included in any row of the
|
||||
/// provided [select] statement.
|
||||
///
|
||||
/// The [select] statement may only have one column.
|
||||
Expression<bool> isNotInQuery(BaseSelectStatement select) {
|
||||
_checkSubquery(select);
|
||||
return _InSelectExpression(select, this, true);
|
||||
}
|
||||
|
||||
/// A `CASE WHEN` construct using the current expression as a base.
|
||||
///
|
||||
/// The expression on which [caseMatch] is invoked will be used as a base and
|
||||
/// compared against the keys in [when]. If an equal key is found in the map,
|
||||
/// the expression returned evaluates to the respective value.
|
||||
/// If no matching keys are found in [when], the [orElse] expression is
|
||||
/// evaluated and returned. If no [orElse] expression is provided, `NULL` will
|
||||
/// be returned instead.
|
||||
///
|
||||
/// For example, consider this expression mapping numerical weekdays to their
|
||||
/// name:
|
||||
///
|
||||
/// ```dart
|
||||
/// final weekday = myTable.createdOnWeekDay;
|
||||
/// weekday.caseMatch<String>(
|
||||
/// when: {
|
||||
/// Constant(1): Constant('Monday'),
|
||||
/// Constant(2): Constant('Tuesday'),
|
||||
/// Constant(3): Constant('Wednesday'),
|
||||
/// Constant(4): Constant('Thursday'),
|
||||
/// Constant(5): Constant('Friday'),
|
||||
/// Constant(6): Constant('Saturday'),
|
||||
/// Constant(7): Constant('Sunday'),
|
||||
/// },
|
||||
/// orElse: Constant('(unknown)'),
|
||||
/// );
|
||||
/// ```
|
||||
Expression<T> caseMatch<T extends Object>({
|
||||
required Map<Expression<D>, Expression<T>> when,
|
||||
Expression<T>? orElse,
|
||||
}) {
|
||||
return CaseWhenExpressionWithBase<D, T>(
|
||||
this,
|
||||
cases: when.entries.map((e) => CaseWhen(e.key, then: e.value)),
|
||||
orElse: orElse,
|
||||
);
|
||||
}
|
||||
|
||||
/// Evaluates to `this` if [predicate] is true, otherwise evaluates to [ifFalse].
|
||||
Expression<T> iif<T extends Object>(
|
||||
Expression<bool> predicate, Expression<T> ifFalse) {
|
||||
return FunctionCallExpression<T>('IIF', [predicate, this, ifFalse]);
|
||||
}
|
||||
|
||||
/// Returns `null` if [matcher] is equal to this expression, `this` otherwise.
|
||||
Expression<D> nullIf(Expression<D> matcher) {
|
||||
return FunctionCallExpression('NULLIF', [this, matcher]);
|
||||
}
|
||||
|
||||
/// Writes this expression into the [GenerationContext], assuming that there's
|
||||
/// an outer expression with [precedence]. If the [Expression.precedence] of
|
||||
/// `this` expression is lower, it will be wrap}ped in
|
||||
///
|
||||
/// See also:
|
||||
/// - [Component.writeInto], which doesn't take any precedence relation into
|
||||
/// account.
|
||||
void writeAroundPrecedence(GenerationContext context, Precedence precedence) {
|
||||
if (this.precedence <= precedence) {
|
||||
context.buffer.write('(');
|
||||
writeInto(context);
|
||||
context.buffer.write(')');
|
||||
} else {
|
||||
writeInto(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// If this [Expression] wraps an [inner] expression, this utility method can
|
||||
/// be used inside [writeInto] to write that inner expression while wrapping
|
||||
/// it in parentheses if necessary.
|
||||
@protected
|
||||
void writeInner(GenerationContext ctx, Expression inner) {
|
||||
assert(precedence != Precedence.unknown,
|
||||
"Expressions with unknown precedence shouldn't have inner expressions");
|
||||
inner.writeAroundPrecedence(ctx, precedence);
|
||||
}
|
||||
|
||||
/// The [BaseSqlType] backing this expression.
|
||||
///
|
||||
/// This is a recognized [DriftSqlType] for all expressions for which a custom
|
||||
/// type has not explicitly been set.
|
||||
BaseSqlType<D> get driftSqlType => DriftSqlType.forType();
|
||||
|
||||
/// Chains all [predicates] together into a single expression that will
|
||||
/// evaluate to `true` iff any of the [predicates] evaluates to `true`.
|
||||
///
|
||||
/// The [ifEmpty] value will be used when no predicates have been passed to
|
||||
/// [or]. By default, `false` is returned.
|
||||
static Expression<bool> or(
|
||||
Iterable<Expression<bool>> predicates, {
|
||||
Expression<bool> ifEmpty = const Constant(false),
|
||||
}) {
|
||||
if (predicates.isEmpty) {
|
||||
return ifEmpty;
|
||||
}
|
||||
|
||||
return predicates.reduce((value, element) => value | element);
|
||||
}
|
||||
|
||||
/// Chains all [predicates] together into a single expression that will
|
||||
/// evaluate to `true` iff all of the [predicates] evaluates to `true`.
|
||||
///
|
||||
/// The [ifEmpty] value will be used when no predicates have been passed to
|
||||
/// [or]. By default, `true` is returned.
|
||||
static Expression<bool> and(
|
||||
Iterable<Expression<bool>> predicates, {
|
||||
Expression<bool> ifEmpty = const Constant(true),
|
||||
}) {
|
||||
if (predicates.isEmpty) {
|
||||
return ifEmpty;
|
||||
}
|
||||
|
||||
return predicates.reduce((value, element) => value & element);
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the possible comparison operators that can appear in a
|
||||
/// [_Comparison].
|
||||
enum _ComparisonOperator {
|
||||
/// '<' in sql
|
||||
less('<'),
|
||||
|
||||
/// '<=' in sql
|
||||
lessOrEqual('<='),
|
||||
|
||||
/// '=' in sql
|
||||
equal('='),
|
||||
|
||||
/// '>=' in sql
|
||||
moreOrEqual('>='),
|
||||
|
||||
/// '>' in sql
|
||||
more('>');
|
||||
|
||||
final String operator;
|
||||
|
||||
const _ComparisonOperator(this.operator);
|
||||
}
|
||||
|
||||
/// An expression that compares two child expressions.
|
||||
class _Comparison extends InfixOperator<bool> {
|
||||
@override
|
||||
final Expression left;
|
||||
@override
|
||||
final Expression right;
|
||||
|
||||
/// The operator to use for this comparison
|
||||
final _ComparisonOperator op;
|
||||
|
||||
@override
|
||||
String get operator => op.operator;
|
||||
|
||||
@override
|
||||
Precedence get precedence {
|
||||
if (op == _ComparisonOperator.equal) {
|
||||
return Precedence.comparisonEq;
|
||||
} else {
|
||||
return Precedence.comparison;
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a comparison from the [left] and [right] expressions to compare
|
||||
/// and the [ComparisonOperator] [op].
|
||||
_Comparison(this.left, this.op, this.right);
|
||||
|
||||
/// Like [Comparison(left, op, right)], but uses [_ComparisonOperator.equal].
|
||||
_Comparison.equal(this.left, this.right) : op = _ComparisonOperator.equal;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
// Most values can be compared directly, but date time values need to be
|
||||
// brought into a comparable format if they're stored as text (since we
|
||||
// don't want to compare datetimes lexicographically).
|
||||
final left = this.left;
|
||||
final right = this.right;
|
||||
|
||||
if (left is Expression<DateTime> &&
|
||||
right is Expression<DateTime> &&
|
||||
context.typeMapping.storeDateTimesAsText) {
|
||||
// Compare julianday values instead of texts
|
||||
writeInner(context, left.julianday);
|
||||
context.writeWhitespace();
|
||||
context.buffer.write(operator);
|
||||
context.writeWhitespace();
|
||||
writeInner(context, right.julianday);
|
||||
} else {
|
||||
super.writeInto(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _UnaryMinus<DT extends Object> extends Expression<DT> {
|
||||
final Expression<DT> inner;
|
||||
|
||||
_UnaryMinus(this.inner);
|
||||
|
||||
@override
|
||||
Precedence get precedence => Precedence.unary;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('-');
|
||||
writeInner(context, inner);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => inner.hashCode * 5;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _UnaryMinus && other.inner == inner;
|
||||
}
|
||||
}
|
||||
|
||||
class _DartCastExpression<D1 extends Object, D2 extends Object>
|
||||
extends Expression<D2> {
|
||||
final Expression<D1> inner;
|
||||
final CustomSqlType<D2>? _customSqlType;
|
||||
|
||||
const _DartCastExpression(this.inner, this._customSqlType);
|
||||
|
||||
@override
|
||||
BaseSqlType<D2> get driftSqlType => _customSqlType ?? super.driftSqlType;
|
||||
|
||||
@override
|
||||
Precedence get precedence => inner.precedence;
|
||||
|
||||
@override
|
||||
bool get isLiteral => inner.isLiteral;
|
||||
|
||||
@override
|
||||
void writeAroundPrecedence(GenerationContext context, Precedence precedence) {
|
||||
// This helps avoid parentheses if the inner expression has a precedence
|
||||
// that is computed dynamically.
|
||||
return inner.writeAroundPrecedence(context, precedence);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
return inner.writeInto(context);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => inner.hashCode * 7;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _DartCastExpression && other.inner == inner;
|
||||
}
|
||||
}
|
||||
|
||||
class _CastInSqlExpression<D1 extends Object, D2 extends Object>
|
||||
extends Expression<D2> {
|
||||
final Expression<D1> inner;
|
||||
final BaseSqlType<D2> targetType;
|
||||
|
||||
@override
|
||||
Precedence get precedence => Precedence.primary;
|
||||
|
||||
@override
|
||||
BaseSqlType<D2> get driftSqlType => targetType;
|
||||
|
||||
const _CastInSqlExpression(this.inner, this.targetType);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
// ignore: unrelated_type_equality_checks
|
||||
if (targetType == DriftSqlType.any) {
|
||||
inner.writeInto(context); // No need to cast
|
||||
}
|
||||
|
||||
final String typeName;
|
||||
|
||||
if (context.dialect == SqlDialect.mariadb) {
|
||||
// MariaDB has a weird cast syntax that uses different type names than the
|
||||
// ones used in a create table statement.
|
||||
|
||||
// ignore: unnecessary_cast
|
||||
typeName = switch (targetType) {
|
||||
DriftSqlType.int ||
|
||||
DriftSqlType.bigInt ||
|
||||
DriftSqlType.bool =>
|
||||
'INTEGER',
|
||||
DriftSqlType.string => 'CHAR',
|
||||
DriftSqlType.double => 'DOUBLE',
|
||||
DriftSqlType.blob => 'BINARY',
|
||||
DriftSqlType.dateTime => 'DATETIME',
|
||||
DriftSqlType.any => '',
|
||||
CustomSqlType() ||
|
||||
DialectAwareSqlType() =>
|
||||
targetType.sqlTypeName(context),
|
||||
};
|
||||
} else {
|
||||
typeName = targetType.sqlTypeName(context);
|
||||
}
|
||||
|
||||
context.buffer.write('CAST(');
|
||||
inner.writeInto(context);
|
||||
context.buffer.write(' AS $typeName)');
|
||||
}
|
||||
}
|
||||
|
||||
void _checkSubquery(BaseSelectStatement statement) {
|
||||
final columns = statement._expandedColumns.length;
|
||||
if (columns != 1) {
|
||||
throw ArgumentError.value(statement, 'statement',
|
||||
'Must return exactly one column (actually returns $columns)');
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a subquery expression from the given [statement].
|
||||
///
|
||||
/// The statement, which can be created via [DatabaseConnectionUser.select] in
|
||||
/// a database class, must return exactly one row with exactly one column.
|
||||
Expression<R> subqueryExpression<R extends Object>(
|
||||
BaseSelectStatement statement) {
|
||||
_checkSubquery(statement);
|
||||
return _SubqueryExpression<R>(statement);
|
||||
}
|
||||
|
||||
class _SubqueryExpression<R extends Object> extends Expression<R> {
|
||||
final BaseSelectStatement statement;
|
||||
|
||||
_SubqueryExpression(this.statement);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('(');
|
||||
statement.writeInto(context);
|
||||
context.buffer.write(')');
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => statement.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _SubqueryExpression && other.statement == statement;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
sealed class _BaseInExpression extends Expression<bool> {
|
||||
final Expression _expression;
|
||||
final bool _not;
|
||||
|
||||
_BaseInExpression(this._expression, this._not);
|
||||
|
||||
@override
|
||||
Precedence get precedence => Precedence.comparisonEq;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
writeInner(context, _expression);
|
||||
|
||||
if (_not) {
|
||||
context.buffer.write(' NOT');
|
||||
}
|
||||
context.buffer.write(' IN (');
|
||||
|
||||
_writeValues(context);
|
||||
context.buffer.write(')');
|
||||
}
|
||||
|
||||
void _writeValues(GenerationContext context);
|
||||
}
|
||||
|
||||
final class _InExpression<T extends Object> extends _BaseInExpression {
|
||||
final List<Expression<T>> _values;
|
||||
|
||||
_InExpression(Expression expression, this._values, bool not)
|
||||
: super(expression, not);
|
||||
|
||||
@override
|
||||
void _writeValues(GenerationContext context) {
|
||||
var first = true;
|
||||
for (final value in _values) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
context.buffer.write(', ');
|
||||
}
|
||||
|
||||
value.writeInto(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(_expression, _equality, _not);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _InExpression &&
|
||||
other._expression == _expression &&
|
||||
_equality.equals(other._values, _values) &&
|
||||
other._not == _not;
|
||||
}
|
||||
}
|
||||
|
||||
final class _InSelectExpression extends _BaseInExpression {
|
||||
final BaseSelectStatement _select;
|
||||
|
||||
_InSelectExpression(this._select, Expression expression, bool not)
|
||||
: super(expression, not);
|
||||
|
||||
@override
|
||||
void _writeValues(GenerationContext context) {
|
||||
_select.writeInto(context);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(_expression, _select, _not);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _InSelectExpression &&
|
||||
other._expression == _expression &&
|
||||
other._select == _select &&
|
||||
other._not == _not;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
@internal
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../query_builder.dart';
|
||||
|
||||
/// An expression that looks like "$a operator $b", where $a and $b itself
|
||||
/// are expressions and the operator is any string.
|
||||
abstract class InfixOperator<D extends Object> extends Expression<D> {
|
||||
/// The left-hand side of this expression
|
||||
Expression get left;
|
||||
|
||||
/// The right-hand side of this expresion
|
||||
Expression get right;
|
||||
|
||||
/// The sql operator to write
|
||||
String get operator;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
writeInner(context, left);
|
||||
context.writeWhitespace();
|
||||
context.buffer.write(operator);
|
||||
context.writeWhitespace();
|
||||
writeInner(context, right);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(left, right, operator);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is InfixOperator &&
|
||||
other.left == left &&
|
||||
other.right == right &&
|
||||
other.operator == operator;
|
||||
}
|
||||
}
|
||||
|
||||
/// A basic binary expression with an infix operator.
|
||||
class BaseInfixOperator<D extends Object> extends InfixOperator<D> {
|
||||
@override
|
||||
final Expression left;
|
||||
|
||||
@override
|
||||
final String operator;
|
||||
|
||||
@override
|
||||
final Expression right;
|
||||
|
||||
@override
|
||||
final Precedence precedence;
|
||||
|
||||
/// Create an infix operator with the child expressions, the operator and the
|
||||
/// assumed precedence.
|
||||
BaseInfixOperator(this.left, this.operator, this.right,
|
||||
{this.precedence = Precedence.unknown});
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
// ignoring the lint because we can't have parameterized factories
|
||||
// ignore_for_file: prefer_constructors_over_static_methods
|
||||
|
||||
/// An expression that represents the value of a dart object encoded to sql
|
||||
/// using prepared statements.
|
||||
final class Variable<T extends Object> extends Expression<T> {
|
||||
/// The Dart value that will be sent to the database
|
||||
final T? value;
|
||||
final UserDefinedSqlType<T>? _customType;
|
||||
|
||||
// note that we keep the identity hash/equals here because each variable would
|
||||
// get its own index in sqlite and is thus different.
|
||||
|
||||
@override
|
||||
Precedence get precedence => Precedence.primary;
|
||||
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
|
||||
@override
|
||||
BaseSqlType<T> get driftSqlType => _customType ?? super.driftSqlType;
|
||||
|
||||
/// Constructs a new variable from the [value].
|
||||
///
|
||||
/// For variables of [CustomSqlType]s, the `type` can also be provided as a
|
||||
/// parameter to control how the value is mapped to SQL.
|
||||
const Variable(this.value, [this._customType]);
|
||||
|
||||
/// Creates a variable that holds the specified boolean.
|
||||
static Variable<bool> withBool(bool value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Creates a variable that holds the specified int.
|
||||
static Variable<int> withInt(int value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Creates a variable that holds the specified BigInt.
|
||||
static Variable<BigInt> withBigInt(BigInt value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Creates a variable that holds the specified string.
|
||||
static Variable<String> withString(String value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Creates a variable that holds the specified date.
|
||||
static Variable<DateTime> withDateTime(DateTime value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Creates a variable that holds the specified data blob.
|
||||
static Variable<Uint8List> withBlob(Uint8List value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Creates a variable that holds the specified floating point value.
|
||||
static Variable<double> withReal(double value) {
|
||||
return Variable(value);
|
||||
}
|
||||
|
||||
/// Maps [value] to something that should be understood by the underlying
|
||||
/// database engine. For instance, a [DateTime] will me mapped to its unix
|
||||
/// timestamp.
|
||||
dynamic mapToSimpleValue(GenerationContext context) {
|
||||
return BaseSqlType.mapToSqlParameter<T>(context, _customType, value);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
if (!context.supportsVariables ||
|
||||
// Workaround for https://github.com/simolus3/drift/issues/2441
|
||||
// Binding nulls on postgres is currently untyped which causes issues.
|
||||
(value == null && context.dialect == SqlDialect.postgres)) {
|
||||
// Write as constant instead.
|
||||
Constant<T>(value).writeInto(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var explicitStart = context.explicitVariableIndex;
|
||||
|
||||
var mark = '?';
|
||||
var suffix = '';
|
||||
if (context.dialect == SqlDialect.postgres) {
|
||||
explicitStart = 1;
|
||||
mark = r'$';
|
||||
}
|
||||
|
||||
if (explicitStart != null) {
|
||||
context.buffer
|
||||
..write(mark)
|
||||
..write(explicitStart + context.amountOfVariables)
|
||||
..write(suffix);
|
||||
context.introduceVariable(
|
||||
this,
|
||||
mapToSimpleValue(context),
|
||||
);
|
||||
} else {
|
||||
context.buffer.write(mark);
|
||||
context.introduceVariable(this, mapToSimpleValue(context));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'Variable($value)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is Variable && other.value == value;
|
||||
}
|
||||
}
|
||||
|
||||
/// An expression that represents the value of a dart object encoded to sql
|
||||
/// by writing them into the sql statements. For most cases, consider using
|
||||
/// [Variable] instead.
|
||||
final class Constant<T extends Object> extends Expression<T> {
|
||||
/// The value that will be converted to an sql literal.
|
||||
final T? value;
|
||||
|
||||
final UserDefinedSqlType<T>? _customType;
|
||||
|
||||
/// Constructs a new constant (sql literal) holding the [value].
|
||||
const Constant(this.value, [this._customType]);
|
||||
|
||||
@override
|
||||
Precedence get precedence => Precedence.primary;
|
||||
|
||||
@override
|
||||
BaseSqlType<T> get driftSqlType => _customType ?? super.driftSqlType;
|
||||
|
||||
@override
|
||||
bool get isLiteral => true;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
return context.buffer
|
||||
.write(BaseSqlType.mapToSqlLiteral(context, _customType, value));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other.runtimeType == runtimeType &&
|
||||
// ignore: test_types_in_equals
|
||||
(other as Constant<T>).value == value;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'Constant($value)';
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
part of 'query_builder.dart';
|
||||
|
||||
/// Contains information about a query while it's being constructed.
|
||||
class GenerationContext {
|
||||
/// Whether the query obtained by this context operates on multiple tables.
|
||||
///
|
||||
/// If it does, columns should prefix their table name to avoid ambiguous
|
||||
/// queries.
|
||||
bool hasMultipleTables = false;
|
||||
|
||||
/// When set to a non-null value, [Variable]s in this context will generate
|
||||
/// explicit indices starting at [explicitVariableIndex].
|
||||
int? explicitVariableIndex;
|
||||
|
||||
/// When set to an entity name (view or table), generated column in that
|
||||
/// entity definition will written into query as expression
|
||||
String? generatingForView;
|
||||
|
||||
/// All tables that the generated query reads from.
|
||||
final List<ResultSetImplementation> watchedTables = [];
|
||||
|
||||
/// The options to use when mapping values from and to the database.
|
||||
@Deprecated('Use typeMapping instead')
|
||||
final DriftDatabaseOptions options;
|
||||
|
||||
/// The [SqlTypes] configuration used for mapping values to the database.
|
||||
final SqlTypes typeMapping;
|
||||
|
||||
/// The [SqlDialect] that should be respected when generating the query.
|
||||
SqlDialect get dialect => executor?.executor.dialect ?? SqlDialect.sqlite;
|
||||
|
||||
/// The actual [DatabaseConnectionUser] that's going to execute the generated
|
||||
/// query.
|
||||
final DatabaseConnectionUser? executor;
|
||||
|
||||
/// Whether variables are supported and can be written as `?` to be bound
|
||||
/// later.
|
||||
///
|
||||
/// This is almost always the case, but not in a `CREATE VIEW` statement.
|
||||
final bool supportsVariables;
|
||||
|
||||
final List<dynamic> _boundVariables = [];
|
||||
|
||||
/// The values of [introducedVariables] that will be sent to the underlying
|
||||
/// engine.
|
||||
List<dynamic> get boundVariables => _boundVariables;
|
||||
|
||||
/// All variables ("?" in sql) that were added to this context.
|
||||
final List<Variable> introducedVariables = [];
|
||||
|
||||
/// Returns the amount of variables that have been introduced when writing
|
||||
/// this query.
|
||||
int get amountOfVariables => boundVariables.length;
|
||||
|
||||
/// The string buffer contains the sql query as it's being constructed.
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
|
||||
/// Gets the generated sql statement
|
||||
String get sql => buffer.toString();
|
||||
|
||||
/// The variable indices occupied by this generation context.
|
||||
///
|
||||
/// SQL variables are 1-indexed, so a context with three variables would
|
||||
/// cover the variables `1`, `2` and `3` by default.
|
||||
Iterable<int> get variableIndices {
|
||||
final start = explicitVariableIndex ?? 1;
|
||||
return Iterable.generate(amountOfVariables, (i) => start + i);
|
||||
}
|
||||
|
||||
/// Constructs a [GenerationContext] by copying the relevant fields from the
|
||||
/// database.
|
||||
GenerationContext.fromDb(DatabaseConnectionUser this.executor,
|
||||
{this.supportsVariables = true})
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
: options = executor.options,
|
||||
typeMapping = executor.typeMapping;
|
||||
|
||||
/// Constructs a custom [GenerationContext] by setting the fields manually.
|
||||
/// See [GenerationContext.fromDb] for a more convenient factory.
|
||||
GenerationContext(this.options, this.executor,
|
||||
{this.supportsVariables = true})
|
||||
: typeMapping = options
|
||||
.createTypeMapping(executor?.executor.dialect ?? SqlDialect.sqlite);
|
||||
|
||||
/// Introduces a variable that will be sent to the database engine. Whenever
|
||||
/// this method is called, a question mark should be added to the [buffer] so
|
||||
/// that the prepared statement can be executed with the variable. The value
|
||||
/// must be a type that is supported by the sqflite library. A list of
|
||||
/// supported types can be found [here](https://github.com/tekartik/sqflite#supported-sqlite-types).
|
||||
void introduceVariable(Variable v, dynamic value) {
|
||||
introducedVariables.add(v);
|
||||
_boundVariables.add(value);
|
||||
}
|
||||
|
||||
/// Shortcut to add a single space to the buffer because it's used very often.
|
||||
void writeWhitespace() => buffer.write(' ');
|
||||
|
||||
/// Turns [columnName] into a safe SQL identifier by wrapping it in double
|
||||
/// quotes, or backticks depending on the dialect.
|
||||
String identifier(String columnName) => dialect.escape(columnName);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
@internal
|
||||
library;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../types/mapping.dart';
|
||||
import 'query_builder.dart';
|
||||
|
||||
/// Internal utilities for building queries that aren't exported.
|
||||
extension WriteDefinition on GenerationContext {
|
||||
/// Writes the result set to this context, suitable to implement `FROM`
|
||||
/// clauses and joins.
|
||||
void writeResultSet(ResultSetImplementation resultSet) {
|
||||
switch (resultSet) {
|
||||
case Subquery(:final select):
|
||||
buffer.write('(');
|
||||
select.writeInto(this);
|
||||
buffer
|
||||
..write(') ')
|
||||
..write(resultSet.aliasedName);
|
||||
case TableValuedFunction():
|
||||
resultSet.writeInto(this);
|
||||
|
||||
if (resultSet.aliasedName != resultSet.entityName) {
|
||||
buffer.write(' ${resultSet.aliasedName}');
|
||||
}
|
||||
default:
|
||||
buffer.write(resultSet.tableWithAlias);
|
||||
watchedTables.add(resultSet);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a suitable SQL string in [sql] based on the current dialect.
|
||||
String pickForDialect(Map<SqlDialect, String> sql) {
|
||||
assert(
|
||||
sql.containsKey(dialect),
|
||||
'Tried running SQL optimized for the following dialects: ${sql.keys.join}. '
|
||||
'However, the database is running $dialect. Has that dialect been added '
|
||||
'to the `dialects` drift builder option?',
|
||||
);
|
||||
|
||||
final found = sql[dialect];
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
|
||||
return sql.values.first; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilities to derive other expressions with a type compatible to `this`
|
||||
/// expression.
|
||||
extension WithTypes<T extends Object> on Expression<T> {
|
||||
/// Creates a variable with a matching [driftSqlType].
|
||||
Variable<T> variable(T? value) {
|
||||
return switch (driftSqlType) {
|
||||
UserDefinedSqlType<T> custom => Variable(value, custom),
|
||||
_ => Variable(value),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
// Mega compilation unit that includes all Dart apis related to generating SQL
|
||||
// at runtime.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/internal/versioned_schema.dart';
|
||||
import 'package:drift/src/dsl/dsl.dart';
|
||||
import 'package:drift/src/runtime/api/options.dart';
|
||||
import 'package:drift/src/runtime/api/runtime_api.dart';
|
||||
import 'package:drift/src/runtime/data_class.dart';
|
||||
import 'package:drift/src/runtime/data_verification.dart';
|
||||
import 'package:drift/src/runtime/exceptions.dart';
|
||||
import 'package:drift/src/runtime/executor/stream_queries.dart';
|
||||
import 'package:drift/src/runtime/types/converters.dart';
|
||||
import 'package:drift/src/runtime/types/mapping.dart';
|
||||
import 'package:drift/src/utils/async_map.dart';
|
||||
import 'package:drift/src/utils/single_transformer.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../query_builder/schema/entities.dart';
|
||||
import '../../utils/async.dart';
|
||||
import '../database/db_base.dart';
|
||||
import '../utils.dart';
|
||||
// New files should not be part of this mega library, which we're trying to
|
||||
// split up.
|
||||
|
||||
import 'expressions/case_when.dart';
|
||||
import 'expressions/internal.dart';
|
||||
import 'helpers.dart';
|
||||
|
||||
export 'components/table_valued_function.dart';
|
||||
export 'expressions/bitwise.dart';
|
||||
export 'expressions/case_when.dart';
|
||||
export 'on_table.dart';
|
||||
|
||||
part 'components/group_by.dart';
|
||||
part 'components/join.dart';
|
||||
part 'components/limit.dart';
|
||||
part 'components/order_by.dart';
|
||||
part 'components/subquery.dart';
|
||||
part 'components/where.dart';
|
||||
part 'expressions/aggregate.dart';
|
||||
part 'expressions/algebra.dart';
|
||||
part 'expressions/bools.dart';
|
||||
part 'expressions/comparable.dart';
|
||||
part 'expressions/custom.dart';
|
||||
part 'expressions/datetimes.dart';
|
||||
part 'expressions/exists.dart';
|
||||
part 'expressions/expression.dart';
|
||||
part 'expressions/in.dart';
|
||||
part 'expressions/null_check.dart';
|
||||
part 'expressions/text.dart';
|
||||
part 'expressions/variables.dart';
|
||||
part 'expressions/window.dart';
|
||||
part 'generation_context.dart';
|
||||
part 'migration.dart';
|
||||
part 'schema/column_impl.dart';
|
||||
part 'schema/entities.dart';
|
||||
part 'schema/table_info.dart';
|
||||
part 'schema/view_info.dart';
|
||||
part 'statements/delete.dart';
|
||||
part 'statements/insert.dart';
|
||||
part 'statements/query.dart';
|
||||
part 'statements/select/custom_select.dart';
|
||||
part 'statements/select/select.dart';
|
||||
part 'statements/select/select_with_join.dart';
|
||||
part 'statements/update.dart';
|
||||
|
||||
/// A component is anything that can appear in a sql query.
|
||||
abstract class Component {
|
||||
/// Default, constant constructor.
|
||||
const Component();
|
||||
|
||||
/// Writes this component into the [context] by writing to its
|
||||
/// [GenerationContext.buffer] or by introducing bound variables. When writing
|
||||
/// into the buffer, no whitespace around the this component should be
|
||||
/// introduced. When a component consists of multiple composed component, it's
|
||||
/// responsible for introducing whitespace between its child components.
|
||||
void writeInto(GenerationContext context);
|
||||
}
|
||||
|
||||
/// Writes all [components] into the [context], separated by commas.
|
||||
void _writeCommaSeparated(
|
||||
GenerationContext context, Iterable<Component> components) {
|
||||
var first = true;
|
||||
for (final element in components) {
|
||||
if (!first) {
|
||||
context.buffer.write(', ');
|
||||
}
|
||||
element.writeInto(context);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// An enumeration of database systems supported by drift. Only
|
||||
/// [SqlDialect.sqlite] is officially supported, all others are in an
|
||||
/// experimental state at the moment.
|
||||
enum SqlDialect {
|
||||
/// Use sqlite's sql dialect. This is the default option and the only
|
||||
/// officially supported dialect at the moment.
|
||||
sqlite(
|
||||
booleanType: 'INTEGER',
|
||||
textType: 'TEXT',
|
||||
integerType: 'INTEGER',
|
||||
realType: 'REAL',
|
||||
blobType: 'BLOB',
|
||||
),
|
||||
|
||||
/// (currently unsupported)
|
||||
@Deprecated('Use mariadb instead, even when talking to a MySQL database')
|
||||
mysql(
|
||||
booleanType: '',
|
||||
textType: '',
|
||||
integerType: '',
|
||||
blobType: '',
|
||||
realType: '',
|
||||
),
|
||||
|
||||
/// PostgreSQL (currently supported in an experimental state)
|
||||
postgres(
|
||||
booleanType: 'boolean',
|
||||
textType: 'text',
|
||||
integerType: 'bigint',
|
||||
blobType: 'bytea',
|
||||
realType: 'float8',
|
||||
),
|
||||
|
||||
/// MariaDB (currently supported in an experimental state)
|
||||
mariadb(
|
||||
booleanType: 'BOOLEAN',
|
||||
textType: 'TEXT',
|
||||
integerType: 'BIGINT',
|
||||
blobType: 'BLOB',
|
||||
realType: 'DOUBLE',
|
||||
escapeChar: '`',
|
||||
supportsIndexedParameters: false,
|
||||
);
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store booleans.
|
||||
final String booleanType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store strings.
|
||||
final String textType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store 64-bit
|
||||
/// integers.
|
||||
final String integerType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store doubles.
|
||||
final String realType;
|
||||
|
||||
/// The type to use in `CAST`s and column definitions to store blobs (as
|
||||
/// a [Uint8List] in Dart).
|
||||
final String blobType;
|
||||
|
||||
/// The character used to wrap identifiers to distinguish them from keywords.
|
||||
///
|
||||
/// This is a double quote character in ANSI SQL, but MariaDB uses backticks
|
||||
/// by default.
|
||||
final String escapeChar;
|
||||
|
||||
/// Whether this dialect supports indexed parameters.
|
||||
///
|
||||
/// For dialects that support this features, an explicit index can be given
|
||||
/// for parameters, even if it doesn't match the order of occurrences in the
|
||||
/// given statement (e.g. `INSERT INTO foo VALUES (?1, ?2, ?3, ?4)`).
|
||||
/// In dialects without this feature, every syntactic occurrence of a variable
|
||||
/// introduces a new logical variable with a new index, variables also can't
|
||||
/// be re-used.
|
||||
final bool supportsIndexedParameters;
|
||||
|
||||
/// Escapes [identifier] by wrapping it in [escapeChar].
|
||||
String escape(String identifier) => '$escapeChar$identifier$escapeChar';
|
||||
|
||||
const SqlDialect({
|
||||
required this.booleanType,
|
||||
required this.textType,
|
||||
required this.integerType,
|
||||
required this.realType,
|
||||
required this.blobType,
|
||||
this.escapeChar = '"',
|
||||
this.supportsIndexedParameters = true,
|
||||
});
|
||||
|
||||
/// For dialects that don't support named or explicitly-indexed variables,
|
||||
/// translates a variable assignment to avoid using that feature.
|
||||
///
|
||||
/// For instance, the SQL snippet `WHERE x = :a OR y = :a` would be translated
|
||||
/// to `WHERE x = ? OR y = ?`. Then, [original] would contain the value for
|
||||
/// the single variable and [syntacticOccurences] would contain two values
|
||||
/// (`1` and `1`) referencing the original variable.
|
||||
List<Variable> desugarDuplicateVariables(
|
||||
List<Variable> original,
|
||||
List<int> syntacticOccurences,
|
||||
) {
|
||||
if (supportsIndexedParameters) return original;
|
||||
|
||||
return [
|
||||
for (final occurence in syntacticOccurences)
|
||||
// Variables in SQL are 1-indexed
|
||||
original[occurence - 1],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
const VerificationResult _invalidNull = VerificationResult.failure(
|
||||
"This column is not nullable and doesn't have a default value. "
|
||||
"Null fields thus can't be inserted.");
|
||||
|
||||
/// Implementation for a [Column] declared on a table.
|
||||
class GeneratedColumn<T extends Object> extends Column<T> {
|
||||
/// The sql name of this column.
|
||||
final String $name; // todo: Remove, replace with `name`
|
||||
|
||||
/// The name of the table that contains this column
|
||||
final String tableName;
|
||||
|
||||
/// Whether null values are allowed for this column.
|
||||
final bool $nullable;
|
||||
|
||||
/// Default constraints generated by drift.
|
||||
final void Function(GenerationContext)? _defaultConstraints;
|
||||
|
||||
/// Custom constraints that have been specified for this column.
|
||||
///
|
||||
/// Some constraints, like `NOT NULL` or checks for booleans, are generated by
|
||||
/// drift by default.
|
||||
/// Constraints can also be overridden with [BuildColumn.customConstraint],
|
||||
/// in which case the drift constraints will not be applied.
|
||||
final String? $customConstraints;
|
||||
|
||||
/// The default expression to be used during inserts when no value has been
|
||||
/// specified. Can be null if no default value is set.
|
||||
final Expression<T>? defaultValue;
|
||||
|
||||
/// A `CHECK` column constraint present on this column.
|
||||
///
|
||||
/// These constraints are evaluated as a boolean during inserts or upserts.
|
||||
/// When they evaluate to `false`, the causing statement is rejected.
|
||||
///
|
||||
/// Note that this field isn't always set: `CHECK` constraints for tables
|
||||
/// defined in `.drift` files are written as raw constraints during build
|
||||
/// time.
|
||||
/// This field is defined as a lazy function because the check constraint
|
||||
/// typically depends on the column itself.
|
||||
final Expression<bool> Function()? check;
|
||||
|
||||
/// Additional checks performed on values before inserts or updates.
|
||||
final VerificationResult Function(T?, VerificationMeta)? additionalChecks;
|
||||
|
||||
/// The sql type to use for this column.
|
||||
final BaseSqlType<T> type;
|
||||
|
||||
/// If this column is generated (that is, it is a SQL expression of other)
|
||||
/// columns, contains information about how to generate this column.
|
||||
final GeneratedAs? generatedAs;
|
||||
|
||||
/// Whether a value is required for this column when inserting a new row.
|
||||
final bool requiredDuringInsert;
|
||||
|
||||
/// Whether this column has an `AUTOINCREMENT` primary key constraint that was
|
||||
/// created by drift.
|
||||
final bool hasAutoIncrement;
|
||||
|
||||
@override
|
||||
String get name => $name;
|
||||
|
||||
@override
|
||||
BaseSqlType<T> get driftSqlType => type;
|
||||
|
||||
/// Used by generated code.
|
||||
GeneratedColumn(
|
||||
this.$name,
|
||||
this.tableName,
|
||||
this.$nullable, {
|
||||
this.clientDefault,
|
||||
required this.type,
|
||||
void Function(GenerationContext)? defaultConstraints,
|
||||
this.$customConstraints,
|
||||
this.defaultValue,
|
||||
this.additionalChecks,
|
||||
this.requiredDuringInsert = false,
|
||||
this.generatedAs,
|
||||
this.check,
|
||||
this.hasAutoIncrement = false,
|
||||
}) : _defaultConstraints = defaultConstraints;
|
||||
|
||||
/// Applies a type converter to this column.
|
||||
///
|
||||
/// This is mainly used by the generator.
|
||||
GeneratedColumnWithTypeConverter<D, T> withConverter<D>(
|
||||
TypeConverter<D, T?> converter) {
|
||||
return GeneratedColumnWithTypeConverter._(
|
||||
converter,
|
||||
$name,
|
||||
tableName,
|
||||
$nullable,
|
||||
clientDefault,
|
||||
type,
|
||||
_defaultConstraints,
|
||||
$customConstraints,
|
||||
defaultValue,
|
||||
additionalChecks,
|
||||
requiredDuringInsert,
|
||||
generatedAs,
|
||||
check,
|
||||
hasAutoIncrement,
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes the definition of this column, as defined
|
||||
/// [here](https://www.sqlite.org/syntax/column-def.html), into the given
|
||||
/// buffer.
|
||||
void writeColumnDefinition(GenerationContext into) {
|
||||
final isSerial = into.dialect == SqlDialect.postgres && hasAutoIncrement;
|
||||
final escapedName = escapedNameFor(into.dialect);
|
||||
|
||||
if (isSerial) {
|
||||
into.buffer.write('$escapedName bigserial PRIMARY KEY NOT NULL');
|
||||
} else {
|
||||
into.buffer.write('$escapedName ${type.sqlTypeName(into)}');
|
||||
}
|
||||
|
||||
if ($customConstraints == null) {
|
||||
if (!isSerial) {
|
||||
into.buffer.write($nullable ? ' NULL' : ' NOT NULL');
|
||||
}
|
||||
|
||||
final defaultValue = this.defaultValue;
|
||||
if (defaultValue != null) {
|
||||
into.buffer.write(' DEFAULT ');
|
||||
|
||||
// we need to write brackets if the default value is not a literal.
|
||||
// see https://www.sqlite.org/syntax/column-constraint.html
|
||||
final writeBrackets = !defaultValue.isLiteral;
|
||||
|
||||
if (writeBrackets) into.buffer.write('(');
|
||||
defaultValue.writeInto(into);
|
||||
if (writeBrackets) into.buffer.write(')');
|
||||
}
|
||||
|
||||
final generated = generatedAs;
|
||||
if (generated != null) {
|
||||
into.buffer.write(' GENERATED ALWAYS AS (');
|
||||
generated.generatedAs.writeInto(into);
|
||||
into.buffer
|
||||
..write(') ')
|
||||
..write(generated.stored ? 'STORED' : 'VIRTUAL');
|
||||
}
|
||||
|
||||
final checkExpr = check?.call();
|
||||
if (checkExpr != null) {
|
||||
into.buffer.write(' CHECK(');
|
||||
checkExpr.writeInto(into);
|
||||
into.buffer.write(')');
|
||||
}
|
||||
|
||||
// these custom constraints refer to builtin constraints from drift
|
||||
if (!isSerial && _defaultConstraints != null) {
|
||||
_defaultConstraints(into);
|
||||
}
|
||||
} else if ($customConstraints?.isNotEmpty == true) {
|
||||
into.buffer
|
||||
..write(' ')
|
||||
..write($customConstraints);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context, {bool ignoreEscape = false}) {
|
||||
if (generatedAs != null && context.generatingForView == tableName) {
|
||||
generatedAs!.generatedAs.writeInto(context);
|
||||
} else {
|
||||
if (context.hasMultipleTables) {
|
||||
context.buffer
|
||||
..write(context.identifier(tableName))
|
||||
..write('.');
|
||||
}
|
||||
context.buffer
|
||||
.write(ignoreEscape ? $name : escapedNameFor(context.dialect));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the given value fits into this column. The default
|
||||
/// implementation only checks for nullability, but subclasses might enforce
|
||||
/// additional checks. For instance, a text column might verify that a text
|
||||
/// has a certain length.
|
||||
VerificationResult isAcceptableValue(T? value, VerificationMeta meta) {
|
||||
final nullOk = $nullable;
|
||||
if (!nullOk && value == null) {
|
||||
return _invalidNull;
|
||||
} else {
|
||||
return additionalChecks?.call(value, meta) ??
|
||||
const VerificationResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
/// A more general version of [isAcceptableValue] that supports any sql
|
||||
/// expression.
|
||||
///
|
||||
/// The default implementation will not perform any check if [value] is not
|
||||
/// a [Variable].
|
||||
VerificationResult isAcceptableOrUnknown(
|
||||
Expression value, VerificationMeta meta) {
|
||||
if (value is Variable) {
|
||||
return isAcceptableValue(value.value as T?, meta);
|
||||
} else {
|
||||
return const VerificationResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(tableName, $name);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
|
||||
// ignore: test_types_in_equals
|
||||
final typedOther = other as GeneratedColumn;
|
||||
return typedOther.tableName == tableName && typedOther.$name == $name;
|
||||
}
|
||||
|
||||
Variable _evaluateClientDefault() {
|
||||
return variable(clientDefault!());
|
||||
}
|
||||
|
||||
/// A value for [additionalChecks] validating allowed text lengths.
|
||||
///
|
||||
/// Used by generated code.
|
||||
static VerificationResult Function(String?, VerificationMeta) checkTextLength(
|
||||
{int? minTextLength, int? maxTextLength}) {
|
||||
return (value, meta) {
|
||||
if (value == null) return const VerificationResult.success();
|
||||
|
||||
final length = value.length;
|
||||
if (minTextLength != null && minTextLength > length) {
|
||||
return VerificationResult.failure(
|
||||
'Must at least be $minTextLength characters long.');
|
||||
}
|
||||
if (maxTextLength != null && maxTextLength < length) {
|
||||
return VerificationResult.failure(
|
||||
'Must at most be $maxTextLength characters long.');
|
||||
}
|
||||
|
||||
return const VerificationResult.success();
|
||||
};
|
||||
}
|
||||
|
||||
/// A helper method to make creating [defaultConstraints] simpler. Used when
|
||||
/// the constraint does not depend on the dialect.
|
||||
///
|
||||
/// Used by generated code.
|
||||
static void Function(GenerationContext) constraintIsAlways(
|
||||
String constraint) =>
|
||||
(context) => context.buffer
|
||||
..write(' ')
|
||||
..write(constraint);
|
||||
|
||||
/// A helper method to make creating [defaultConstraints] simpler. Used when
|
||||
/// the constraint depends on the dialect.
|
||||
///
|
||||
/// Used by generated code.
|
||||
static void Function(GenerationContext) constraintsDependsOnDialect(
|
||||
Map<SqlDialect, String> constraints,
|
||||
) =>
|
||||
(context) {
|
||||
final constraint = constraints[context.dialect];
|
||||
|
||||
if (constraint == null || constraint.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.buffer
|
||||
..write(' ')
|
||||
..write(constraint);
|
||||
};
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A sqlite index on columns or expressions.
|
||||
///
|
||||
/// For more information on triggers, see the [CREATE TRIGGER][sqlite-docs]
|
||||
/// documentation from sqlite, or the [entry on sqlitetutorial.net][sql-tut].
|
||||
///
|
||||
/// [sqlite-docs]: https://www.sqlite.org/lang_createindex.html
|
||||
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-index/
|
||||
class Index extends DatabaseSchemaEntity {
|
||||
@override
|
||||
final String entityName;
|
||||
|
||||
/// The `CREATE INDEX` sql statement that can be used to create this index.
|
||||
@Deprecated('Use createStatementsByDialect instead')
|
||||
String get createIndexStmt => createStatementsByDialect.values.first;
|
||||
|
||||
/// The `CREATE INDEX` SQL statements used to create this index, accessible
|
||||
/// for each dialect enabled when generating code.
|
||||
final Map<SqlDialect, String> createStatementsByDialect;
|
||||
|
||||
/// Creates an index model by the [createIndexStmt] and its [entityName].
|
||||
/// Mainly used by generated code.
|
||||
Index(this.entityName, String createIndexStmt)
|
||||
: createStatementsByDialect = {SqlDialect.sqlite: createIndexStmt};
|
||||
|
||||
/// Creates an index model by its [entityName] used in the schema and the
|
||||
/// `CREATE INDEX` statements for each supported dialect.
|
||||
Index.byDialect(this.entityName, this.createStatementsByDialect);
|
||||
}
|
||||
|
||||
/// An internal schema entity to run an sql statement when the database is
|
||||
/// created.
|
||||
///
|
||||
/// The generator uses this entity to implement `@create` statements in drift
|
||||
/// files:
|
||||
/// ```sql
|
||||
/// CREATE TABLE users (name TEXT);
|
||||
///
|
||||
/// @create: INSERT INTO users VALUES ('Bob');
|
||||
/// ```
|
||||
/// A [OnCreateQuery] is emitted for each `@create` statement in an included
|
||||
/// drift file.
|
||||
class OnCreateQuery extends DatabaseSchemaEntity {
|
||||
/// The sql statement that should be run in the default `onCreate` clause.
|
||||
@Deprecated('Use sqlByDialect instead')
|
||||
String get sql => sqlByDialect.values.first;
|
||||
|
||||
/// The SQL statement to run, indexed by the dialect used in the database.
|
||||
final Map<SqlDialect, String> sqlByDialect;
|
||||
|
||||
/// Create a query that will be run in the default `onCreate` migration.
|
||||
OnCreateQuery(String sql) : this.byDialect({SqlDialect.sqlite: sql});
|
||||
|
||||
/// Creates the entity of a query to run in the default `onCreate` migration.
|
||||
///
|
||||
/// The migrator will lookup a suitable query from the [sqlByDialect] map.
|
||||
OnCreateQuery.byDialect(this.sqlByDialect);
|
||||
|
||||
@override
|
||||
String get entityName => r'$internal$';
|
||||
}
|
||||
|
||||
/// Interface for schema entities that have a result set.
|
||||
///
|
||||
/// [Tbl] is the generated Dart class which implements [ResultSetImplementation]
|
||||
/// and the user-defined [Table] class. [Row] is the class used to hold a result
|
||||
/// row.
|
||||
abstract class ResultSetImplementation<Tbl, Row> extends DatabaseSchemaEntity {
|
||||
/// The generated database instance that this view or table is attached to.
|
||||
@internal
|
||||
DatabaseConnectionUser get attachedDatabase;
|
||||
|
||||
/// The (potentially aliased) name of this table or view.
|
||||
///
|
||||
/// If no alias is active, this is the same as [entityName].
|
||||
String get aliasedName => entityName;
|
||||
|
||||
/// Type system sugar. Implementations are likely to inherit from both
|
||||
/// [TableInfo] and [Tbl] and can thus just return their instance.
|
||||
Tbl get asDslTable;
|
||||
|
||||
/// All columns from this table or view.
|
||||
List<GeneratedColumn> get $columns;
|
||||
|
||||
/// Maps the given row returned by the database into the fitting data class.
|
||||
FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix});
|
||||
|
||||
/// Creates an alias of this table or view that will write the name [alias]
|
||||
/// when used in a query.
|
||||
ResultSetImplementation<Tbl, Row> createAlias(String alias) =>
|
||||
_AliasResultSet(alias, this);
|
||||
|
||||
/// Gets all [$columns] in this table or view, indexed by their (non-escaped)
|
||||
/// name.
|
||||
Map<String, GeneratedColumn> get columnsByName;
|
||||
}
|
||||
|
||||
class _AliasResultSet<Tbl, Row> extends ResultSetImplementation<Tbl, Row> {
|
||||
final String _alias;
|
||||
final ResultSetImplementation<Tbl, Row> _inner;
|
||||
|
||||
_AliasResultSet(this._alias, this._inner);
|
||||
|
||||
@override
|
||||
DatabaseConnectionUser get attachedDatabase => _inner.attachedDatabase;
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => _inner.$columns;
|
||||
|
||||
@override
|
||||
String get aliasedName => _alias;
|
||||
|
||||
@override
|
||||
ResultSetImplementation<Tbl, Row> createAlias(String alias) {
|
||||
return _AliasResultSet(alias, _inner);
|
||||
}
|
||||
|
||||
@override
|
||||
String get entityName => _inner.entityName;
|
||||
|
||||
@override
|
||||
FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return _inner.map(data, tablePrefix: tablePrefix);
|
||||
}
|
||||
|
||||
@override
|
||||
Tbl get asDslTable => _inner.asDslTable;
|
||||
|
||||
@override
|
||||
Map<String, GeneratedColumn<Object>> get columnsByName =>
|
||||
_inner.columnsByName;
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Base class for generated table classes.
|
||||
///
|
||||
/// Drift generates a subclass of [TableInfo] for each table used in a database.
|
||||
/// This classes contains information about the table's schema (e.g. its
|
||||
/// [primaryKey] or [$columns]).
|
||||
///
|
||||
/// [TableDsl] is the original table class written by the user. For tables
|
||||
/// defined in drift files, this is the table implementation class itself.
|
||||
/// [D] is the type of the data class generated from the table.
|
||||
///
|
||||
/// To obtain an instance of this class, use a table getter from the database.
|
||||
mixin TableInfo<TableDsl extends Table, D> on Table
|
||||
implements DatabaseSchemaEntity, ResultSetImplementation<TableDsl, D> {
|
||||
@override
|
||||
TableDsl get asDslTable => this as TableDsl;
|
||||
|
||||
/// The primary key of this table. Can be empty if no custom primary key has
|
||||
/// been specified.
|
||||
///
|
||||
/// Additional to the [Table.primaryKey] columns declared by an user, this
|
||||
/// also contains auto-increment integers, which are primary key by default.
|
||||
Set<GeneratedColumn> get $primaryKey => const {};
|
||||
|
||||
// ensure the primaryKey getter is consistent with $primaryKey, which can
|
||||
// contain additional columns.
|
||||
@override
|
||||
Set<Column> get primaryKey => $primaryKey;
|
||||
|
||||
/// The unique key of this table. Can be empty if no custom primary key has
|
||||
/// been specified.
|
||||
///
|
||||
/// Additional to the [Table.primaryKey] columns declared by an user, this
|
||||
/// also contains auto-increment integers, which are primary key by default.
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => const [];
|
||||
|
||||
@override
|
||||
String get aliasedName => entityName;
|
||||
|
||||
/// The name of the table in the database. Unlike [aliasedName], this can not
|
||||
/// be aliased.
|
||||
String get actualTableName;
|
||||
|
||||
@override
|
||||
String get entityName => actualTableName;
|
||||
|
||||
Map<String, GeneratedColumn>? _columnsByName;
|
||||
|
||||
@override
|
||||
Map<String, GeneratedColumn> get columnsByName {
|
||||
return _columnsByName ??= {
|
||||
for (final column in $columns) column.$name: column
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates that the given entity can be inserted into this table, meaning
|
||||
/// that it respects all constraints (nullability, text length, etc.).
|
||||
VerificationContext validateIntegrity(Insertable<D> instance,
|
||||
{bool isInserting = false}) {
|
||||
// default behavior when users chose to not verify the integrity (build time
|
||||
// option)
|
||||
return const VerificationContext.notEnabled();
|
||||
}
|
||||
|
||||
/// Converts a [companion] to the real model class, [D].
|
||||
///
|
||||
/// Values that are [Value.absent] in the companion will be set to `null`.
|
||||
/// The [database] instance is used so that the raw values from the companion
|
||||
/// can properly be interpreted as the high-level Dart values exposed by the
|
||||
/// data class.
|
||||
Future<D> mapFromCompanion(
|
||||
Insertable<D> companion, DatabaseConnectionUser database) async {
|
||||
final asColumnMap = companion.toColumns(false);
|
||||
|
||||
if (asColumnMap.values.any((e) => e is! Variable)) {
|
||||
throw ArgumentError('The companion $companion cannot be transformed '
|
||||
'into a dataclass as it contains expressions that need to be '
|
||||
'evaluated by a database engine.');
|
||||
}
|
||||
|
||||
final context = GenerationContext.fromDb(database);
|
||||
final rawValues = asColumnMap
|
||||
.cast<String, Variable>()
|
||||
.map((key, value) => MapEntry(key, value.mapToSimpleValue(context)));
|
||||
|
||||
return map(rawValues);
|
||||
}
|
||||
|
||||
@override
|
||||
TableInfo<TableDsl, D> createAlias(String alias);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
// tables are singleton instances except for aliases
|
||||
if (other is TableInfo) {
|
||||
return other.runtimeType == runtimeType &&
|
||||
other.aliasedName == aliasedName;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(aliasedName, actualTableName);
|
||||
}
|
||||
|
||||
/// Static extension members for generated table classes.
|
||||
///
|
||||
/// Most of these are accessed internally by drift or by generated code.
|
||||
extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
|
||||
/// Like [map], but from a [row] instead of the low-level map.
|
||||
Future<D> mapFromRow(QueryRow row, {String? tablePrefix}) async {
|
||||
return map(row.data, tablePrefix: tablePrefix);
|
||||
}
|
||||
|
||||
/// Like [mapFromRow], but returns null if a non-nullable column of this table
|
||||
/// is null in [row].
|
||||
Future<D?> mapFromRowOrNull(QueryRow row, {String? tablePrefix}) {
|
||||
final resolvedPrefix = tablePrefix == null ? '' : '$tablePrefix.';
|
||||
|
||||
final notInRow = $columns
|
||||
.where((c) => !c.$nullable)
|
||||
.any((e) => row.data['$resolvedPrefix${e.$name}'] == null);
|
||||
|
||||
if (notInRow) return Future.value(null);
|
||||
|
||||
return mapFromRow(row, tablePrefix: tablePrefix);
|
||||
}
|
||||
|
||||
/// Like [mapFromRow], but maps columns from the result through [alias].
|
||||
///
|
||||
/// This is used internally by drift to support mapping to a table from a
|
||||
/// select statement with different column names. For instance, for:
|
||||
///
|
||||
/// ```sql
|
||||
/// CREATE TABLE tbl (foo, bar);
|
||||
///
|
||||
/// query: SELECT foo AS c1, bar AS c2 FROM tbl;
|
||||
/// ```
|
||||
///
|
||||
/// Drift would generate code to call this method with `'c1': 'foo'` and
|
||||
/// `'c2': 'bar'` in [alias].
|
||||
Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async {
|
||||
return await map({
|
||||
for (final entry in row.data.entries) alias[entry.key]!: entry.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A sqlite view.
|
||||
///
|
||||
/// In drift, views can only be declared in `.drift` files.
|
||||
///
|
||||
/// For more information on views, see the [CREATE VIEW][sqlite-docs]
|
||||
/// documentation from sqlite, or the [entry on sqlitetutorial.net][sql-tut].
|
||||
///
|
||||
/// [sqlite-docs]: https://www.sqlite.org/lang_createview.html
|
||||
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/
|
||||
abstract class ViewInfo<Self extends HasResultSet, Row>
|
||||
implements ResultSetImplementation<Self, Row> {
|
||||
@override
|
||||
String get entityName;
|
||||
|
||||
/// The `CREATE VIEW` sql statement that can be used to create this view.
|
||||
///
|
||||
/// This will be null if the view was defined in Dart.
|
||||
@Deprecated('Use createViewStatements instead')
|
||||
String? get createViewStmt => createViewStatements?.values.first;
|
||||
|
||||
/// The `CREATE VIEW` sql statement that can be used to create this view,
|
||||
/// depending on the dialect used by the current database.
|
||||
///
|
||||
/// This will be null if the view was defined in Dart.
|
||||
Map<SqlDialect, String>? get createViewStatements;
|
||||
|
||||
/// Predefined query from `View.as()`
|
||||
///
|
||||
/// This will be null if the view was defined in a `.drift` file.
|
||||
Query? get query;
|
||||
|
||||
/// All tables that this view reads from.
|
||||
///
|
||||
/// If this view reads from other views, the [readTables] of that view are
|
||||
/// also included in this [readTables] set.
|
||||
Set<String> get readTables;
|
||||
|
||||
Map<String, GeneratedColumn>? _columnsByName;
|
||||
|
||||
@override
|
||||
Map<String, GeneratedColumn> get columnsByName {
|
||||
return _columnsByName ??= {
|
||||
for (final column in $columns) column.$name: column
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Represents an insert statement
|
||||
class InsertStatement<T extends Table, D> {
|
||||
/// The database to use then executing this statement
|
||||
@protected
|
||||
final DatabaseConnectionUser database;
|
||||
|
||||
/// The table we're inserting into
|
||||
@protected
|
||||
final TableInfo<T, D> table;
|
||||
|
||||
/// Constructs an insert statement from the database and the table. Used
|
||||
/// internally by drift.
|
||||
InsertStatement(this.database, this.table);
|
||||
|
||||
/// Inserts a row into the table and returns it.
|
||||
///
|
||||
/// Depending on the [InsertMode] or the [DoUpdate] `onConflict` clause, the
|
||||
/// insert statement may not actually insert a row into the database. Since
|
||||
/// this function was declared to return a non-nullable row, it throws an
|
||||
/// exception in that case. Use [insertReturningOrNull] when performing an
|
||||
/// insert with an insert mode like [InsertMode.insertOrIgnore] or when using
|
||||
/// a [DoUpdate] with a `where` clause clause.
|
||||
Future<D> insertReturning(Insertable<D> entity,
|
||||
{InsertMode? mode, UpsertClause<T, D>? onConflict}) async {
|
||||
final row =
|
||||
await insertReturningOrNull(entity, mode: mode, onConflict: onConflict);
|
||||
|
||||
if (row == null) {
|
||||
throw StateError('The insert statement did not insert any rows that '
|
||||
'could be returned. Please use insertReturningOrNull() when using a '
|
||||
'`DoUpdate` clause with `where`.');
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/// Inserts a row into the table and returns it.
|
||||
///
|
||||
/// When no row was inserted and no exception was thrown, for instance because
|
||||
/// [InsertMode.insertOrIgnore] was used or because the upsert clause had a
|
||||
/// `where` clause that didn't match, `null` is returned instead.
|
||||
Future<D?> insertReturningOrNull(Insertable<D> entity,
|
||||
{InsertMode? mode, UpsertClause<T, D>? onConflict}) async {
|
||||
final ctx = createContext(entity, mode ?? InsertMode.insert,
|
||||
onConflict: onConflict, returning: true);
|
||||
|
||||
return database.withCurrentExecutor((e) async {
|
||||
final result = await e.runSelect(ctx.sql, ctx.boundVariables);
|
||||
if (result.isNotEmpty) {
|
||||
database.notifyUpdates(
|
||||
{TableUpdate.onTable(table, kind: UpdateKind.insert)});
|
||||
return table.map(result.single);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Attempts to [insert] [entity] into the database. If the insert would
|
||||
/// violate a primary key or uniqueness constraint, updates the columns that
|
||||
/// are present on [entity].
|
||||
///
|
||||
/// Note that this is subtly different from [InsertMode.replace]! When using
|
||||
/// [InsertMode.replace], the old row will be deleted and replaced with the
|
||||
/// new row. With [insertOnConflictUpdate], columns from the old row that are
|
||||
/// not present on [entity] are unchanged, and no row will be deleted.
|
||||
///
|
||||
/// Be aware that [insertOnConflictUpdate] uses an upsert clause, which is not
|
||||
/// available on older sqlite implementations.
|
||||
/// Note: By default, only the primary key is used for detect uniqueness
|
||||
/// violations. If you have further uniqueness constraints, please use the
|
||||
/// general [insert] method with a [DoUpdate] including those columns in its
|
||||
/// [DoUpdate.target].
|
||||
Future<int> insertOnConflictUpdate(Insertable<D> entity) {
|
||||
return insert(entity, onConflict: DoUpdate((_) => entity));
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of different insert behaviors. See the documentation on the
|
||||
/// individual fields for details.
|
||||
enum InsertMode implements Component {
|
||||
/// A regular `INSERT INTO` statement. When a row with the same primary or
|
||||
/// unique key already exists, the insert statement will fail and an exception
|
||||
/// will be thrown. If the exception is caught, previous statements made in
|
||||
/// the same transaction will NOT be reverted.
|
||||
insert,
|
||||
|
||||
/// Identical to [InsertMode.insertOrReplace], included for the sake of
|
||||
/// completeness.
|
||||
replace,
|
||||
|
||||
/// Like [insert], but if a row with the same primary or unique key already
|
||||
/// exists, it will be deleted and re-created with the row being inserted.
|
||||
insertOrReplace,
|
||||
|
||||
/// Similar to [InsertMode.insertOrAbort], but it will revert the surrounding
|
||||
/// transaction if a constraint is violated, even if the thrown exception is
|
||||
/// caught.
|
||||
insertOrRollback,
|
||||
|
||||
/// Identical to [insert], included for the sake of completeness.
|
||||
insertOrAbort,
|
||||
|
||||
/// Like [insert], but if multiple values are inserted with the same insert
|
||||
/// statement and one of them fails, the others will still be completed.
|
||||
insertOrFail,
|
||||
|
||||
/// Like [insert], but failures will be ignored.
|
||||
insertOrIgnore;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext ctx) {
|
||||
if (ctx.dialect == SqlDialect.postgres &&
|
||||
this != InsertMode.insert &&
|
||||
this != InsertMode.insertOrIgnore) {
|
||||
throw ArgumentError('$this not supported on postgres');
|
||||
}
|
||||
|
||||
ctx.buffer.write(_insertKeywords[
|
||||
ctx.dialect == SqlDialect.postgres ? InsertMode.insert : this]);
|
||||
}
|
||||
}
|
||||
|
||||
const _insertKeywords = <InsertMode, String>{
|
||||
InsertMode.insert: 'INSERT',
|
||||
InsertMode.replace: 'REPLACE',
|
||||
InsertMode.insertOrReplace: 'INSERT OR REPLACE',
|
||||
InsertMode.insertOrRollback: 'INSERT OR ROLLBACK',
|
||||
InsertMode.insertOrAbort: 'INSERT OR ABORT',
|
||||
InsertMode.insertOrFail: 'INSERT OR FAIL',
|
||||
InsertMode.insertOrIgnore: 'INSERT OR IGNORE',
|
||||
};
|
||||
@@ -1,203 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Statement that operates with data that already exists (select, delete,
|
||||
/// update).
|
||||
abstract class Query<T extends HasResultSet, D> extends Component {
|
||||
/// The database this statement should be sent to.
|
||||
@protected
|
||||
DatabaseConnectionUser database;
|
||||
|
||||
/// The (main) table or view that this query operates on.
|
||||
ResultSetImplementation<T, D> table;
|
||||
|
||||
/// Used internally by drift. Users should use the appropriate methods on
|
||||
/// [DatabaseConnectionUser] instead.
|
||||
Query(this.database, this.table);
|
||||
|
||||
/// The `WHERE` clause for this statement
|
||||
@protected
|
||||
Where? whereExpr;
|
||||
|
||||
/// The `ORDER BY` clause for this statement
|
||||
@protected
|
||||
OrderBy? orderByExpr;
|
||||
|
||||
/// The `LIMIT` clause for this statement.
|
||||
@protected
|
||||
Limit? limitExpr;
|
||||
|
||||
/// Whether a `RETURNING *` clause should be added to this statement.
|
||||
@protected
|
||||
bool writeReturningClause = false;
|
||||
|
||||
GroupBy? _groupBy;
|
||||
|
||||
/// Subclasses must override this and write the part of the statement that
|
||||
/// comes before the where and limit expression..
|
||||
@visibleForOverriding
|
||||
void writeStartPart(GenerationContext ctx);
|
||||
|
||||
void _writeInto(GenerationContext context,
|
||||
{bool withOrderByAndLimit = true}) {
|
||||
// whether we need to insert a space before writing the next component
|
||||
var needsWhitespace = false;
|
||||
|
||||
void writeWithSpace(
|
||||
Component? component,
|
||||
) {
|
||||
if (component == null) return;
|
||||
|
||||
if (needsWhitespace) context.writeWhitespace();
|
||||
component.writeInto(context);
|
||||
needsWhitespace = true;
|
||||
}
|
||||
|
||||
writeStartPart(context);
|
||||
needsWhitespace = true;
|
||||
|
||||
writeWithSpace(whereExpr);
|
||||
writeWithSpace(_groupBy);
|
||||
if (withOrderByAndLimit) {
|
||||
writeWithSpace(orderByExpr);
|
||||
writeWithSpace(limitExpr);
|
||||
}
|
||||
|
||||
if (writeReturningClause) {
|
||||
if (needsWhitespace) context.writeWhitespace();
|
||||
|
||||
context.buffer.write('RETURNING *');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
_writeInto(context);
|
||||
}
|
||||
|
||||
/// Constructs the query that can then be sent to the database executor.
|
||||
///
|
||||
/// This is used internally by drift to run the query. Users should use the
|
||||
/// other methods explained in the [documentation](https://drift.simonbinder.eu/docs/getting-started/writing_queries/).
|
||||
GenerationContext constructQuery() {
|
||||
final ctx = GenerationContext.fromDb(database);
|
||||
writeInto(ctx);
|
||||
ctx.buffer.write(';');
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixin for a [Query] that operates on a single primary table only.
|
||||
mixin SingleTableQueryMixin<T extends HasResultSet, D> on Query<T, D> {
|
||||
/// Makes this statement only include rows that match the [filter].
|
||||
///
|
||||
/// For instance, if you have a table users with an id column, you could
|
||||
/// select a user with a specific id by using
|
||||
/// ```dart
|
||||
/// (select(users)..where((u) => u.id.equals(42))).watchSingle()
|
||||
/// ```
|
||||
///
|
||||
/// Please note that this [where] call is different to [Iterable.where] and
|
||||
/// [Stream.where] in the sense that [filter] will NOT be called for each
|
||||
/// row. Instead, it will only be called once (with the underlying table as
|
||||
/// parameter). The result [Expression] will be written as a SQL string and
|
||||
/// sent to the underlying database engine. The filtering does not happen in
|
||||
/// Dart.
|
||||
/// If a where condition has already been set before, the resulting filter
|
||||
/// will be the conjunction of both calls.
|
||||
///
|
||||
/// For more information, see:
|
||||
/// - The docs on [expressions](https://drift.simonbinder.eu/docs/getting-started/expressions/),
|
||||
/// which explains how to express most SQL expressions in Dart.
|
||||
/// If you want to remove duplicate rows from a query, use the `distinct`
|
||||
/// parameter on [DatabaseConnectionUser.select].
|
||||
void where(Expression<bool> Function(T tbl) filter) {
|
||||
final predicate = filter(table.asDslTable);
|
||||
|
||||
if (whereExpr == null) {
|
||||
whereExpr = Where(predicate);
|
||||
} else {
|
||||
whereExpr = Where(whereExpr!.predicate & predicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension for statements on a table.
|
||||
///
|
||||
/// This adds the [whereSamePrimaryKey] method as an extension. The query could
|
||||
/// run on a view, for which [whereSamePrimaryKey] is not defined.
|
||||
extension QueryTableExtensions<T extends Table, D>
|
||||
on SingleTableQueryMixin<T, D> {
|
||||
TableInfo<T, D> get _sourceTable => table as TableInfo<T, D>;
|
||||
|
||||
/// Applies a [where] statement so that the row with the same primary key as
|
||||
/// [d] will be matched.
|
||||
///
|
||||
/// Note that, as far as primary key equality is concerned, `NULL` values are
|
||||
/// considered distinct from all values (including other `NULL`s).
|
||||
/// This matches sqlite3's behavior of not counting duplicate `NULL`s as a
|
||||
/// uniqueness constraint violation for primary keys, but makes it impossible
|
||||
/// to find other rows with [whereSamePrimaryKey] if nullable primary keys are
|
||||
/// used.
|
||||
void whereSamePrimaryKey(Insertable<D> d) {
|
||||
final source = _sourceTable;
|
||||
assert(
|
||||
source.$primaryKey.isNotEmpty,
|
||||
'When using Query.whereSamePrimaryKey, which is also called from '
|
||||
'DeleteStatement.delete and UpdateStatement.replace, the affected table'
|
||||
'must have a primary key. You can either specify a primary implicitly '
|
||||
'by making an integer() column autoIncrement(), or by explictly '
|
||||
'overriding the primaryKey getter in your table class. You\'ll also '
|
||||
'have to re-run the code generation step.\n'
|
||||
'Alternatively, if you\'re using DeleteStatement.delete or '
|
||||
'UpdateStatement.replace, consider using DeleteStatement.go or '
|
||||
'UpdateStatement.write respectively. In that case, you need to use a '
|
||||
'custom where statement.');
|
||||
|
||||
final primaryKeyColumns = Map.fromEntries(source.$primaryKey.map((column) {
|
||||
return MapEntry(column.$name, column);
|
||||
}));
|
||||
|
||||
final updatedFields = d.toColumns(false);
|
||||
// Construct a map of [GeneratedColumn] to [Expression] where each column is
|
||||
// a primary key and the associated value was extracted from d.
|
||||
final primaryKeyValues = Map.fromEntries(updatedFields.entries
|
||||
.where((entry) => primaryKeyColumns.containsKey(entry.key)))
|
||||
.map((columnName, value) {
|
||||
return MapEntry(primaryKeyColumns[columnName]!, value);
|
||||
});
|
||||
|
||||
assert(
|
||||
primaryKeyValues.values
|
||||
.every((value) => value is! Variable || value.value != null),
|
||||
'Tried to find a row with a matching primary key that has a null value, '
|
||||
'which is not supported. In sqlite3, `NULL` values in a primary key are '
|
||||
'considered distinct from all other values (including other `NULL`s), so '
|
||||
"drift can't find a matching row for this query. \n"
|
||||
'For details, see https://github.com/simolus3/drift/issues/1956#issuecomment-1200502026',
|
||||
);
|
||||
|
||||
Expression<bool>? predicate;
|
||||
for (final entry in primaryKeyValues.entries) {
|
||||
final comparison =
|
||||
_Comparison(entry.key, _ComparisonOperator.equal, entry.value);
|
||||
|
||||
if (predicate == null) {
|
||||
predicate = comparison;
|
||||
} else {
|
||||
predicate = predicate & comparison;
|
||||
}
|
||||
}
|
||||
|
||||
whereExpr = Where(predicate!);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixin to provide the high-level [limit] methods for users.
|
||||
mixin LimitContainerMixin<T extends HasResultSet, D> on Query<T, D> {
|
||||
/// Limits the amount of rows returned by capping them at [limit]. If [offset]
|
||||
/// is provided as well, the first [offset] rows will be skipped and not
|
||||
/// included in the result.
|
||||
void limit(int limit, {int? offset}) {
|
||||
limitExpr = Limit(limit, offset);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
part of '../../query_builder.dart';
|
||||
|
||||
/// A select statement that is constructed with a raw sql prepared statement
|
||||
/// instead of the high-level drift api.
|
||||
class CustomSelectStatement with Selectable<QueryRow> {
|
||||
/// Tables this select statement reads from. When turning this select query
|
||||
/// into an auto-updating stream, that stream will emit new items whenever
|
||||
/// any of these tables changes.
|
||||
final Set<ResultSetImplementation> tables;
|
||||
|
||||
/// The sql query string for this statement.
|
||||
final String query;
|
||||
|
||||
/// The variables for the prepared statement, in the order they appear in
|
||||
/// [query]. Variables are denoted using a question mark in the query.
|
||||
final List<Variable> variables;
|
||||
final DatabaseConnectionUser _db;
|
||||
|
||||
/// Constructs a new custom select statement for the query, the variables,
|
||||
/// the affected tables and the database.
|
||||
CustomSelectStatement(this.query, this.variables, this.tables, this._db);
|
||||
|
||||
/// Constructs a fetcher for this query. The fetcher is responsible for
|
||||
/// updating a stream at the right moment.
|
||||
QueryStreamFetcher<List<Map<String, Object?>>> _constructFetcher() {
|
||||
final args = _mapArgs();
|
||||
|
||||
return QueryStreamFetcher(
|
||||
readsFrom: TableUpdateQuery.onAllTables(tables),
|
||||
fetchData: () => _executeRaw(args),
|
||||
key: StreamKey(query, args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<QueryRow>> get() {
|
||||
return _executeRaw(_mapArgs()).then(_mapDbResponse);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<QueryRow>> watch() {
|
||||
return _db.createStream(_constructFetcher()).map(_mapDbResponse);
|
||||
}
|
||||
|
||||
List<dynamic> _mapArgs() {
|
||||
final ctx = GenerationContext.fromDb(_db);
|
||||
return variables.map((v) => v.mapToSimpleValue(ctx)).toList();
|
||||
}
|
||||
|
||||
Future<List<Map<String, Object?>>> _executeRaw(List<Object?> mappedArgs) {
|
||||
return _db.withCurrentExecutor((e) => e.runSelect(query, mappedArgs));
|
||||
}
|
||||
|
||||
List<QueryRow> _mapDbResponse(List<Map<String, Object?>> rows) {
|
||||
return rows.map((row) => QueryRow(row, _db)).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
part of '../../query_builder.dart';
|
||||
|
||||
/// Signature of a function that generates an [OrderingTerm] when provided with
|
||||
/// a table.
|
||||
typedef OrderClauseGenerator<T> = OrderingTerm Function(T tbl);
|
||||
|
||||
/// The abstract base class for all select statements in the drift api.
|
||||
///
|
||||
/// Users are not allowed to extend, implement or mix-in this class.
|
||||
@sealed
|
||||
abstract class BaseSelectStatement<Row> extends Component with Selectable<Row> {
|
||||
Iterable<(Expression, String)> get _expandedColumns;
|
||||
|
||||
/// The name for the given [expression] in the result set, or `null` if
|
||||
/// [expression] was not added as a column to this select statement.
|
||||
String? _nameForColumn(Expression expression);
|
||||
|
||||
FutureOr<Row> _mapRow(Map<String, Object?> fromDatabase);
|
||||
}
|
||||
|
||||
/// A select statement that doesn't use joins.
|
||||
///
|
||||
/// For more information, see [DatabaseConnectionUser.select].
|
||||
class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
||||
with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D>
|
||||
implements BaseSelectStatement<D> {
|
||||
/// Whether duplicate rows should be eliminated from the result (this is a
|
||||
/// `SELECT DISTINCT` statement in sql). Defaults to false.
|
||||
final bool distinct;
|
||||
|
||||
/// Used internally by drift, users will want to call
|
||||
/// [DatabaseConnectionUser.select] instead.
|
||||
SimpleSelectStatement(super.database, super.table, {this.distinct = false});
|
||||
|
||||
/// The tables this select statement reads from.
|
||||
@visibleForOverriding
|
||||
@Deprecated('Use watchedTables on the GenerationContext')
|
||||
Set<ResultSetImplementation> get watchedTables => {table};
|
||||
|
||||
@override
|
||||
Iterable<(Expression, String)> get _expandedColumns =>
|
||||
table.$columns.map((e) => (e, e.name));
|
||||
|
||||
@override
|
||||
String? _nameForColumn(Expression expression) {
|
||||
if (table.$columns.contains(expression)) {
|
||||
return (expression as Column).name;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeStartPart(GenerationContext ctx) {
|
||||
ctx.buffer
|
||||
..write(_beginOfSelect(distinct))
|
||||
..write(' * FROM ');
|
||||
ctx.writeResultSet(table);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<D>> get() {
|
||||
final ctx = constructQuery();
|
||||
return _getRaw(ctx).then(_mapResponse);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<D>> watch() {
|
||||
final query = constructQuery();
|
||||
final fetcher = QueryStreamFetcher(
|
||||
readsFrom: TableUpdateQuery.onAllTables(query.watchedTables),
|
||||
fetchData: () => _getRaw(query),
|
||||
key: StreamKey(query.sql, query.boundVariables),
|
||||
);
|
||||
|
||||
return database.createStream(fetcher).asyncMapPerSubscription(_mapResponse);
|
||||
}
|
||||
|
||||
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
||||
return database.withCurrentExecutor((e) {
|
||||
return e.runSelect(ctx.sql, ctx.boundVariables);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<D> _mapRow(Map<String, Object?> row) {
|
||||
return table.map(row);
|
||||
}
|
||||
|
||||
FutureOr<List<D>> _mapResponse(List<Map<String, Object?>> rows) {
|
||||
return rows.mapAsyncAndAwait(table.map);
|
||||
}
|
||||
|
||||
/// Creates a select statement that operates on more than one table by
|
||||
/// applying the given joins.
|
||||
///
|
||||
/// Example from the todolist example which will load the category for each
|
||||
/// item:
|
||||
/// ```
|
||||
/// final results = await select(todos).join([
|
||||
/// leftOuterJoin(categories, categories.id.equalsExp(todos.category))
|
||||
/// ]).get();
|
||||
///
|
||||
/// return results.map((row) {
|
||||
/// final entry = row.readTable(todos);
|
||||
/// final category = row.readTable(categories);
|
||||
/// return EntryWithCategory(entry, category);
|
||||
/// }).toList();
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
/// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins
|
||||
/// - [innerJoin], [leftOuterJoin] and [crossJoin], which can be used to
|
||||
/// construct a [Join].
|
||||
/// - [DatabaseConnectionUser.alias], which can be used to build statements
|
||||
/// that refer to the same table multiple times.
|
||||
JoinedSelectStatement join(List<Join> joins) {
|
||||
final statement = JoinedSelectStatement(database, table, joins, distinct);
|
||||
|
||||
if (whereExpr != null) {
|
||||
statement.where(whereExpr!.predicate);
|
||||
}
|
||||
if (orderByExpr != null) {
|
||||
statement.orderBy(orderByExpr!.terms);
|
||||
}
|
||||
if (limitExpr != null) {
|
||||
statement.limitExpr = limitExpr;
|
||||
}
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/// {@macro drift_select_addColumns}
|
||||
JoinedSelectStatement addColumns(List<Expression> expressions) {
|
||||
return join([])..addColumns(expressions);
|
||||
}
|
||||
|
||||
/// Orders the result by the given clauses. The clauses coming first in the
|
||||
/// list have a higher priority, the later clauses are only considered if the
|
||||
/// first clause considers two rows to be equal.
|
||||
///
|
||||
/// Example that first displays the users who are awesome and sorts users by
|
||||
/// their id as a secondary criterion:
|
||||
/// ```
|
||||
/// (db.select(db.users)
|
||||
/// ..orderBy([
|
||||
/// (u) =>
|
||||
/// OrderingTerm(expression: u.isAwesome, mode: OrderingMode.desc),
|
||||
/// (u) => OrderingTerm(expression: u.id)
|
||||
/// ]))
|
||||
/// .get()
|
||||
/// ```
|
||||
void orderBy(List<OrderClauseGenerator<T>> clauses) {
|
||||
orderByExpr = OrderBy(clauses.map((t) => t(table.asDslTable)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
String _beginOfSelect(bool distinct) {
|
||||
return distinct ? 'SELECT DISTINCT' : 'SELECT';
|
||||
}
|
||||
|
||||
@internal
|
||||
final class SelectWithoutTables extends BaseSelectStatement<TypedResult>
|
||||
with Selectable<TypedResult> {
|
||||
/// Map of added columns to chosen aliases.
|
||||
final Map<Expression, String> _columns;
|
||||
final DatabaseConnectionUser _db;
|
||||
|
||||
SelectWithoutTables(this._db, Iterable<Expression> columns)
|
||||
: _columns = {
|
||||
for (final (i, column) in columns.indexed) column: 'c$i',
|
||||
};
|
||||
|
||||
@override
|
||||
Iterable<(Expression<Object>, String)> get _expandedColumns =>
|
||||
_columns.entries.map((e) => (e.key, e.value));
|
||||
|
||||
@override
|
||||
TypedResult _mapRow(Map<String, Object?> fromDatabase) {
|
||||
final queryRow = QueryRow(fromDatabase, _db);
|
||||
return TypedResult(
|
||||
const {},
|
||||
queryRow,
|
||||
_LazyExpressionMap(_columns, queryRow),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String? _nameForColumn(Expression<Object> expression) {
|
||||
return _columns[expression];
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
final isRoot = context.buffer.isEmpty;
|
||||
context.buffer.write('SELECT ');
|
||||
var first = true;
|
||||
for (final MapEntry(key: expr, value: alias) in _columns.entries) {
|
||||
if (!first) {
|
||||
context.buffer.write(', ');
|
||||
}
|
||||
first = false;
|
||||
expr.writeInto(context);
|
||||
context.buffer.write(' ${context.identifier(alias)}');
|
||||
}
|
||||
|
||||
if (isRoot) context.buffer.write(';');
|
||||
}
|
||||
|
||||
GenerationContext _createContext() {
|
||||
final context = GenerationContext.fromDb(_db);
|
||||
writeInto(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
Future<List<Map<String, Object?>>> _fetchRaw(GenerationContext context) {
|
||||
return _db.withCurrentExecutor((e) {
|
||||
return e.runSelect(context.sql, context.boundVariables);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TypedResult>> get() async {
|
||||
final context = _createContext();
|
||||
final rows = await _fetchRaw(context);
|
||||
return [for (final row in rows) _mapRow(row)];
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<TypedResult>> watch() {
|
||||
final context = _createContext();
|
||||
return _db
|
||||
.createStream(QueryStreamFetcher(
|
||||
readsFrom: TableUpdateQuery.onAllTables(context.watchedTables),
|
||||
key: StreamKey(context.sql, context.boundVariables),
|
||||
fetchData: () => _fetchRaw(context),
|
||||
))
|
||||
.map((rows) => [for (final row in rows) _mapRow(row)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A result row in a [JoinedSelectStatement] that can parse the result of
|
||||
/// multiple entities.
|
||||
class TypedResult {
|
||||
/// Creates the result from the parsed table data.
|
||||
TypedResult(this._parsedData, this.rawData,
|
||||
[this._parsedExpressions = const {}]);
|
||||
|
||||
final Map<ResultSetImplementation, dynamic> _parsedData;
|
||||
final Map<Expression, dynamic> _parsedExpressions;
|
||||
|
||||
/// The raw data contained in this row.
|
||||
final QueryRow rawData;
|
||||
|
||||
/// Reads all data that belongs to the given [table] from this row.
|
||||
///
|
||||
/// If this row does not contain non-null columns of the [table], this method
|
||||
/// will throw an [ArgumentError]. Use [readTableOrNull] for nullable tables.
|
||||
D readTable<T extends HasResultSet, D>(ResultSetImplementation<T, D> table) {
|
||||
if (!_parsedData.containsKey(table)) {
|
||||
throw ArgumentError(
|
||||
'Invalid table passed to readTable: ${table.aliasedName}. This row '
|
||||
'does not contain values for that table. \n'
|
||||
'Please use readTableOrNull for outer joins.');
|
||||
}
|
||||
|
||||
return _parsedData[table] as D;
|
||||
}
|
||||
|
||||
/// Reads all data that belongs to the given [table] from this row.
|
||||
///
|
||||
/// Returns `null` if this row does not contain non-null values of the
|
||||
/// [table].
|
||||
///
|
||||
/// See also: [readTable], which throws instead of returning `null`.
|
||||
D? readTableOrNull<T extends HasResultSet, D>(
|
||||
ResultSetImplementation<T, D> table) {
|
||||
return _parsedData[table] as D?;
|
||||
}
|
||||
|
||||
/// Reads a single column from an [expr]. The expression must have been added
|
||||
/// as a column, for instance via [JoinedSelectStatement.addColumns].
|
||||
///
|
||||
/// To access the underlying columns directly, use [rawData].
|
||||
D? read<D extends Object>(Expression<D> expr) {
|
||||
if (_parsedExpressions.containsKey(expr)) {
|
||||
return _parsedExpressions[expr] as D?;
|
||||
}
|
||||
|
||||
throw ArgumentError(
|
||||
'Invalid call to read(): $expr. This result set does not have a column '
|
||||
'for that expression.');
|
||||
}
|
||||
|
||||
/// Reads a column that has a type converter applied to it from the row.
|
||||
///
|
||||
/// This calls [read] internally, which reads the column but without applying
|
||||
/// a type converter.
|
||||
D? readWithConverter<D, S extends Object>(
|
||||
GeneratedColumnWithTypeConverter<D, S> column) {
|
||||
return NullAwareTypeConverter.wrapFromSql(
|
||||
column.converter, read<S>(column));
|
||||
}
|
||||
}
|
||||
|
||||
/// This extension is used to add custom data to a [TypedResult] row outside of this library.
|
||||
@internal
|
||||
extension EditTypedResultExtension on TypedResult {
|
||||
/// Adds a new expression to this result row.
|
||||
void addData(ResultSetImplementation table, dynamic data) {
|
||||
_parsedData[table] = data;
|
||||
}
|
||||
}
|
||||
@@ -1,585 +0,0 @@
|
||||
part of '../../query_builder.dart';
|
||||
|
||||
/// A `SELECT` statement that operates on more than one table.
|
||||
// this is called JoinedSelectStatement for legacy reasons - we also use it
|
||||
// when custom expressions are used as result columns. Basically, it stores
|
||||
// queries that are more complex than SimpleSelectStatement
|
||||
class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||
extends Query<FirstT, FirstD>
|
||||
with LimitContainerMixin, Selectable<TypedResult>
|
||||
implements BaseSelectStatement<TypedResult> {
|
||||
/// Used internally by drift, users should use [SimpleSelectStatement.join]
|
||||
/// instead.
|
||||
JoinedSelectStatement(super.database, super.table, this._joins,
|
||||
[this.distinct = false,
|
||||
this._includeMainTableInResult = true,
|
||||
this._includeJoinedTablesInResult = true]);
|
||||
|
||||
/// Whether to generate a `SELECT DISTINCT` query that will remove duplicate
|
||||
/// rows from the result set.
|
||||
final bool distinct;
|
||||
final bool _includeMainTableInResult;
|
||||
final bool _includeJoinedTablesInResult;
|
||||
final List<Join> _joins;
|
||||
|
||||
/// All columns that we're selecting from.
|
||||
final List<Expression> _selectedColumns = [];
|
||||
|
||||
/// The `AS` aliases generated for each column that isn't from a table.
|
||||
///
|
||||
/// Each table column can be uniquely identified by its (potentially aliased)
|
||||
/// table and its name. So a column named `id` in a table called `users` would
|
||||
/// be written as `users.id AS "users.id"`. These columns are also included in
|
||||
/// the map when added through [addColumns], but they have a predicatable name.
|
||||
///
|
||||
/// More interestingly, other expressions used as columns will be included
|
||||
/// here. They're just named in increasing order, so something like `AS c3`.
|
||||
final Map<Expression, String> _columnAliases = {};
|
||||
|
||||
/// Compound statements that have been added to this select statements, e.g.
|
||||
/// through
|
||||
final List<(_CompoundOperator, BaseSelectStatement)> _compounds = [];
|
||||
|
||||
/// The tables this select statement reads from
|
||||
@visibleForOverriding
|
||||
@Deprecated('Use watchedTables on the generated context')
|
||||
Set<ResultSetImplementation> get watchedTables => _queriedTables().toSet();
|
||||
|
||||
@override
|
||||
Iterable<(Expression<Object>, String)> get _expandedColumns =>
|
||||
_columnsWithName(null);
|
||||
|
||||
Iterable<(Expression<Object>, String)> _columnsWithName(
|
||||
String? generatingForView) sync* {
|
||||
for (final table in _queriedTables(true)) {
|
||||
for (final column in table.$columns) {
|
||||
yield (
|
||||
column,
|
||||
_nameForTableColumn(column, generatingForView: generatingForView)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final column in _selectedColumns) {
|
||||
if (column is GeneratedColumn) {
|
||||
yield (
|
||||
column,
|
||||
_nameForTableColumn(column, generatingForView: generatingForView)
|
||||
);
|
||||
} else {
|
||||
yield (column, _columnAliases[column]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? _nameForColumn(Expression expression) {
|
||||
// Custom column added to this join?
|
||||
if (_columnAliases.containsKey(expression)) {
|
||||
return _columnAliases[expression];
|
||||
}
|
||||
|
||||
// From an added table?
|
||||
if (expression is GeneratedColumn) {
|
||||
for (final table in _queriedTables(true)) {
|
||||
if (table.$columns.contains(expression)) {
|
||||
return _nameForTableColumn(expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not added to this join
|
||||
return null;
|
||||
}
|
||||
|
||||
String _nameForTableColumn(GeneratedColumn column,
|
||||
{String? generatingForView}) {
|
||||
if (generatingForView == column.tableName) {
|
||||
return column.$name;
|
||||
} else {
|
||||
return '${column.tableName}.${column.$name}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all tables this query reads from.
|
||||
///
|
||||
/// If [onlyResults] (defaults to false) is set, only tables that are included
|
||||
/// in the result set are returned.
|
||||
Iterable<ResultSetImplementation> _queriedTables(
|
||||
[bool onlyResults = false]) sync* {
|
||||
if (!onlyResults || _includeMainTableInResult) {
|
||||
yield table;
|
||||
}
|
||||
|
||||
for (final join in _joins) {
|
||||
if (onlyResults &&
|
||||
!(join.includeInResult ?? _includeJoinedTablesInResult)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield join.table as ResultSetImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
void _addCompound(_CompoundOperator operator, BaseSelectStatement other) {
|
||||
if (other is JoinedSelectStatement) {
|
||||
if (other.limitExpr != null ||
|
||||
other.orderByExpr != null ||
|
||||
other._compounds.isNotEmpty) {
|
||||
throw ArgumentError(
|
||||
"Can't add compound query that has a limit or an order-by clause. "
|
||||
'Also, the added query must hot have its own compound parts. Add '
|
||||
'the clauses and parts to the top-level parts instead.');
|
||||
}
|
||||
}
|
||||
|
||||
var columnsHere = _expandedColumns.iterator;
|
||||
var otherColumns = other._expandedColumns.iterator;
|
||||
var columnCount = 0;
|
||||
|
||||
while (columnsHere.moveNext()) {
|
||||
if (!otherColumns.moveNext()) {
|
||||
throw ArgumentError(
|
||||
"Can't add select with fewer columns (added part has "
|
||||
'$columnCount columns, the original source has more).');
|
||||
}
|
||||
|
||||
var here = columnsHere.current;
|
||||
var otherColumn = otherColumns.current;
|
||||
|
||||
if (here.$1.driftSqlType != otherColumn.$1.driftSqlType) {
|
||||
throw ArgumentError(
|
||||
"Can't add part because the column types at index $columnCount "
|
||||
'differ.');
|
||||
}
|
||||
|
||||
columnCount++;
|
||||
}
|
||||
|
||||
if (otherColumns.moveNext()) {
|
||||
throw ArgumentError(
|
||||
"Can't add select with more columns (the original query has "
|
||||
'$columnCount columns, the added part has more).');
|
||||
}
|
||||
|
||||
_compounds.add((operator, other));
|
||||
}
|
||||
|
||||
/// Appends the [other] statement as a `UNION` clause after this query.
|
||||
///
|
||||
/// The database will run both queries and return all rows involved in either
|
||||
/// query, removing duplicates. For this to work, this and [other] must have
|
||||
/// compatible columns.
|
||||
///
|
||||
/// The [other] query must not include a `LIMIT` or a `ORDER BY` clause.
|
||||
/// Compound statements can only contain a single `LIMIT` and `ORDER BY`
|
||||
/// clause at the end, which is set on the first statement (on which
|
||||
/// [union] is called). Also, the [other] statement must not contain compound
|
||||
/// parts on its own.
|
||||
///
|
||||
/// As an example, consider a `todos` table of todo items referencing a
|
||||
/// `categories` table used to group them. With that structure, it's possible
|
||||
/// to compute the amount of todo items in each category, as well as the
|
||||
/// amount of todo items not in a category in a single query:
|
||||
///
|
||||
/// ```dart
|
||||
/// final count = subqueryExpression<int>(selectOnly(todos)
|
||||
/// ..addColumns([countAll()])
|
||||
/// ..where(todos.category.equalsExp(categories.id)));
|
||||
/// final countWithoutCategory = subqueryExpression<int>(db.selectOnly(todos)
|
||||
/// ..addColumns([countAll()])
|
||||
/// ..where(todos.category.isNull()));
|
||||
///
|
||||
/// final query = db.selectOnly(db.categories)
|
||||
/// ..addColumns([db.categories.description, count])
|
||||
/// ..groupBy([categories.id]);
|
||||
/// query.union(db.selectExpressions(
|
||||
/// [const Constant<String>(null), countWithoutCategory]));
|
||||
/// ```
|
||||
void union(BaseSelectStatement other) {
|
||||
_addCompound(_CompoundOperator.union, other);
|
||||
}
|
||||
|
||||
/// Appends the [other] statement as a `UNION ALL` clause after this query.
|
||||
///
|
||||
/// The database will run both queries and return all rows involved in either
|
||||
/// query. For this to work, this and [other] must have compatible columns.
|
||||
///
|
||||
/// The [other] query must not include a `LIMIT` or a `ORDER BY` clause.
|
||||
/// Compound statements can only contain a single `LIMIT` and `ORDER BY`
|
||||
/// clause at the end, which is set on the first statement (on which
|
||||
/// [unionAll] is called). Also, the [other] statement must not contain
|
||||
/// compound parts on its own.
|
||||
///
|
||||
/// As an example, consider a `todos` table of todo items referencing a
|
||||
/// `categories` table used to group them. With that structure, it's possible
|
||||
/// to compute the amount of todo items in each category, as well as the
|
||||
/// amount of todo items not in a category in a single query:
|
||||
///
|
||||
/// ```dart
|
||||
/// final count = subqueryExpression<int>(selectOnly(todos)
|
||||
/// ..addColumns([countAll()])
|
||||
/// ..where(todos.category.equalsExp(categories.id)));
|
||||
/// final countWithoutCategory = subqueryExpression<int>(db.selectOnly(todos)
|
||||
/// ..addColumns([countAll()])
|
||||
/// ..where(todos.category.isNull()));
|
||||
///
|
||||
/// final query = db.selectOnly(db.categories)
|
||||
/// ..addColumns([db.categories.description, count])
|
||||
/// ..groupBy([categories.id]);
|
||||
/// query.unionAll(db.selectExpressions(
|
||||
/// [const Constant<String>(null), countWithoutCategory]));
|
||||
/// ```
|
||||
void unionAll(BaseSelectStatement other) {
|
||||
_addCompound(_CompoundOperator.unionAll, other);
|
||||
}
|
||||
|
||||
/// Appends the [other] statement as a `EXCEPT` clause after this query.
|
||||
///
|
||||
/// The database will run both queries and return all rows of the first query
|
||||
/// that were not returned by [other]. For this to work, this and [other] must
|
||||
/// have compatible columns.
|
||||
///
|
||||
/// The [other] query must not include a `LIMIT` or a `ORDER BY` clause.
|
||||
/// Compound statements can only contain a single `LIMIT` and `ORDER BY`
|
||||
/// clause at the end, which is set on the first statement (on which
|
||||
/// [except] is called). Also, the [other] statement must not contain
|
||||
/// compound parts on its own.
|
||||
void except(BaseSelectStatement other) {
|
||||
_addCompound(_CompoundOperator.except, other);
|
||||
}
|
||||
|
||||
/// Appends the [other] statement as a `INTERSECT` clause after this query.
|
||||
///
|
||||
/// The database will run both queries and return all rows that were returned
|
||||
/// by both queries. For this to work, this and [other] must have compatible
|
||||
/// columns.
|
||||
///
|
||||
/// The [other] query must not include a `LIMIT` or a `ORDER BY` clause.
|
||||
/// Compound statements can only contain a single `LIMIT` and `ORDER BY`
|
||||
/// clause at the end, which is set on the first statement (on which
|
||||
/// [intersect] is called). Also, the [other] statement must not contain
|
||||
/// compound parts on its own.
|
||||
void intersect(BaseSelectStatement other) {
|
||||
_addCompound(_CompoundOperator.intersect, other);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeStartPart(GenerationContext ctx) {
|
||||
ctx.hasMultipleTables = true;
|
||||
ctx.buffer
|
||||
..write(_beginOfSelect(distinct))
|
||||
..write(' ');
|
||||
|
||||
var first = true;
|
||||
for (final (column, name) in _columnsWithName(ctx.generatingForView)) {
|
||||
if (!first) {
|
||||
ctx.buffer.write(', ');
|
||||
}
|
||||
first = false;
|
||||
|
||||
final chosenAliasEscaped = ctx.dialect.escape(name);
|
||||
column.writeInto(ctx);
|
||||
ctx.buffer
|
||||
..write(' AS ')
|
||||
..write(chosenAliasEscaped);
|
||||
}
|
||||
|
||||
ctx.buffer.write(' FROM ');
|
||||
ctx.writeResultSet(table);
|
||||
|
||||
if (_joins.isNotEmpty) {
|
||||
ctx.writeWhitespace();
|
||||
|
||||
for (var i = 0; i < _joins.length; i++) {
|
||||
if (i != 0) ctx.writeWhitespace();
|
||||
|
||||
_joins[i].writeInto(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
if (_compounds.isEmpty) {
|
||||
super.writeInto(context);
|
||||
} else {
|
||||
// The order by and limit clauses must appear after the compounds.
|
||||
super._writeInto(context, withOrderByAndLimit: false);
|
||||
|
||||
for (final (operator, statement) in _compounds) {
|
||||
context.writeWhitespace();
|
||||
context.buffer.write(operator.lexeme);
|
||||
context.writeWhitespace();
|
||||
statement.writeInto(context);
|
||||
}
|
||||
|
||||
context.writeWhitespace();
|
||||
orderByExpr?.writeInto(context);
|
||||
context.writeWhitespace();
|
||||
limitExpr?.writeInto(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the [predicate] as the where clause, which will be used to filter
|
||||
/// results.
|
||||
///
|
||||
/// The clause should only refer to columns defined in one of the tables
|
||||
/// specified during [SimpleSelectStatement.join].
|
||||
///
|
||||
/// With the example of a todos table which refers to categories, we can write
|
||||
/// something like
|
||||
/// ```dart
|
||||
/// final query = select(todos)
|
||||
/// .join([
|
||||
/// leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
|
||||
/// ])
|
||||
/// ..where(todos.name.like("%Important") & categories.name.equals("Work"));
|
||||
/// ```
|
||||
void where(Expression<bool> predicate) {
|
||||
if (whereExpr == null) {
|
||||
whereExpr = Where(predicate);
|
||||
} else {
|
||||
whereExpr = Where(whereExpr!.predicate & predicate);
|
||||
}
|
||||
}
|
||||
|
||||
/// Orders the results of this statement by the ordering [terms].
|
||||
void orderBy(List<OrderingTerm> terms) {
|
||||
orderByExpr = OrderBy(terms);
|
||||
}
|
||||
|
||||
/// {@template drift_select_addColumns}
|
||||
/// Adds a custom expression to the query.
|
||||
///
|
||||
/// The database will evaluate the [Expression] for each row found for this
|
||||
/// query. The value of the expression can be extracted from the [TypedResult]
|
||||
/// by passing it to [TypedResult.read].
|
||||
///
|
||||
/// As an example, we could calculate the length of a column on the database:
|
||||
/// ```dart
|
||||
/// final contentLength = todos.content.length;
|
||||
/// final results = await select(todos).addColumns([contentLength]).get();
|
||||
///
|
||||
/// // we can now read the result of a column added to addColumns
|
||||
/// final lengthOfFirst = results.first.read(contentLength);
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
/// - The docs on expressions: https://drift.simonbinder.eu/docs/getting-started/expressions/
|
||||
/// {@endtemplate}
|
||||
void addColumns(Iterable<Expression> expressions) {
|
||||
for (final expression in expressions) {
|
||||
// Otherwise, we generate an alias.
|
||||
_columnAliases.putIfAbsent(expression, () {
|
||||
// Only add the column if it hasn't been added yet - it's fine if the
|
||||
// same column is added multiple times through the Dart API, they will
|
||||
// read from the same SQL column internally.
|
||||
_selectedColumns.add(expression);
|
||||
|
||||
if (expression is GeneratedColumn) {
|
||||
return _nameForTableColumn(expression);
|
||||
} else {
|
||||
return 'c${_columnAliases.length}';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds more joined tables to this [JoinedSelectStatement].
|
||||
///
|
||||
/// Always returns the same instance.
|
||||
///
|
||||
/// See also:
|
||||
/// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins
|
||||
/// - [SimpleSelectStatement.join], which is used for the first join
|
||||
/// - [innerJoin], [leftOuterJoin] and [crossJoin], which can be used to
|
||||
/// construct a [Join].
|
||||
/// - [DatabaseConnectionUser.alias], which can be used to build statements
|
||||
/// that refer to the same table multiple times.
|
||||
// ignore: avoid_returning_this
|
||||
JoinedSelectStatement join(List<Join> joins) {
|
||||
_joins.addAll(joins);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Groups the result by values in [expressions].
|
||||
///
|
||||
/// An optional [having] attribute can be set to exclude certain groups.
|
||||
void groupBy(Iterable<Expression> expressions, {Expression<bool>? having}) {
|
||||
_groupBy = GroupBy._(expressions.toList(), having);
|
||||
}
|
||||
|
||||
/// Builds a query which which will emit a new result whenever any of the
|
||||
/// tables this query depends on changes.
|
||||
/// You can pass additional tables to watch to this method.
|
||||
Stream<List<TypedResult>> _watchWithAdditionalTables(
|
||||
[Iterable<ResultSetImplementation<dynamic, dynamic>> tables = const []]) {
|
||||
final ctx = constructQuery();
|
||||
final fetcher = QueryStreamFetcher(
|
||||
readsFrom:
|
||||
TableUpdateQuery.onAllTables(ctx.watchedTables.followedBy(tables)),
|
||||
fetchData: () => _getRaw(ctx),
|
||||
key: StreamKey(ctx.sql, ctx.boundVariables),
|
||||
);
|
||||
|
||||
return database
|
||||
.createStream(fetcher)
|
||||
.asyncMapPerSubscription((rows) => _mapResponse(rows));
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<TypedResult>> watch() {
|
||||
return _watchWithAdditionalTables();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TypedResult>> get() async {
|
||||
final ctx = constructQuery();
|
||||
final raw = await _getRaw(ctx);
|
||||
return _mapResponse(raw);
|
||||
}
|
||||
|
||||
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
||||
return database.withCurrentExecutor((e) async {
|
||||
try {
|
||||
return await e.runSelect(ctx.sql, ctx.boundVariables);
|
||||
} catch (e, s) {
|
||||
final foundTables = <String>{};
|
||||
for (final table in _queriedTables()) {
|
||||
if (!foundTables.add(table.aliasedName)) {
|
||||
_warnAboutDuplicate(e, s, table);
|
||||
}
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Precomputes information used to parse results in [_mapWithStructure].
|
||||
///
|
||||
/// As the column names are the same for each row, we can pre-compute some
|
||||
/// information (like the column aliases for [_LazyExpressionMap]) once and
|
||||
/// then apply them to every row.
|
||||
_ResultStructure _computeResultStructure() {
|
||||
return _ResultStructure(
|
||||
columnAliases: {
|
||||
for (final (expr, alias) in _expandedColumns) expr: alias,
|
||||
},
|
||||
queriedTables: _queriedTables(true).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TypedResult> _mapRow(Map<String, Object?> row) async {
|
||||
return await _mapWithStructure(_computeResultStructure(), row);
|
||||
}
|
||||
|
||||
/// Reads a raw database [row] into a [TypedResult] by using information that
|
||||
/// doesn't change between rows, such as the expected columns or tables.
|
||||
Future<TypedResult> _mapWithStructure(
|
||||
_ResultStructure structure, Map<String, Object?> row) async {
|
||||
final readTables = <ResultSetImplementation, dynamic>{};
|
||||
|
||||
for (final table in structure.queriedTables) {
|
||||
final prefix = '${table.aliasedName}.';
|
||||
// if all columns of this table are null, skip the table
|
||||
if (table.$columns.any((c) => row[prefix + c.$name] != null)) {
|
||||
readTables[table] =
|
||||
await table.map(row, tablePrefix: table.aliasedName);
|
||||
}
|
||||
}
|
||||
|
||||
final driftRow = QueryRow(row, database);
|
||||
return TypedResult(
|
||||
readTables,
|
||||
driftRow,
|
||||
_LazyExpressionMap(
|
||||
structure.columnAliases,
|
||||
driftRow,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TypedResult>> _mapResponse(List<Map<String, Object?>> rows) {
|
||||
final structure = _computeResultStructure();
|
||||
|
||||
return Future.wait(rows.map((row) => _mapWithStructure(structure, row)));
|
||||
}
|
||||
|
||||
Never _warnAboutDuplicate(
|
||||
dynamic cause, StackTrace trace, ResultSetImplementation table) {
|
||||
throw DriftWrappedException(
|
||||
message: 'This query contained the table ${table.entityName} more than '
|
||||
'once. Is this a typo? \n'
|
||||
'If you need a join that includes the same table more than once, you '
|
||||
'need to alias() at least one table. See https://drift.simonbinder.eu/queries/joins#aliases '
|
||||
'for an example.',
|
||||
cause: cause,
|
||||
trace: trace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A map responsible for reading typed values for a [TypedResult].
|
||||
///
|
||||
/// In a [JoinedSelectStatement], every column of every table is read and
|
||||
/// interpreted as a result, even if it's never used later. For joins with lots
|
||||
/// of tables, this can quickly become very expensive.
|
||||
///
|
||||
/// So, to stay compatible with the [Map] interface but also be more efficient,
|
||||
/// we now use this implementation to lazily do the type mapping when a column
|
||||
/// is first read. There's a builtin cache so columns accessed a lot aren't
|
||||
/// read multiple times, but using this map we can generally speed things up
|
||||
/// when joins with lots of columns are used.
|
||||
class _LazyExpressionMap extends UnmodifiableMapBase<Expression, Object?> {
|
||||
final Map<Expression, String> _columnAliases;
|
||||
final QueryRow _rawData;
|
||||
|
||||
final Map<Expression, Object?> _cachedData = {};
|
||||
|
||||
_LazyExpressionMap(this._columnAliases, this._rawData);
|
||||
|
||||
@override
|
||||
Object? operator [](Object? key) {
|
||||
if (!containsKey(key) || key is! Expression) return null;
|
||||
|
||||
return _cachedData.putIfAbsent(key, () {
|
||||
return _rawData.readNullableWithType(
|
||||
key.driftSqlType, _columnAliases[key]!);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Expression> get keys => _columnAliases.keys;
|
||||
|
||||
@override
|
||||
bool containsKey(Object? key) => _columnAliases.containsKey(key);
|
||||
}
|
||||
|
||||
class _ResultStructure {
|
||||
final Map<Expression, String> columnAliases;
|
||||
final List<ResultSetImplementation> queriedTables;
|
||||
|
||||
_ResultStructure({required this.columnAliases, required this.queriedTables});
|
||||
}
|
||||
|
||||
@internal
|
||||
extension JoinedSelectStatementAdditionalTables on JoinedSelectStatement {
|
||||
Stream<List<TypedResult>> watchWithAdditionalTables(
|
||||
[Iterable<ResultSetImplementation<dynamic, dynamic>> tables =
|
||||
const []]) =>
|
||||
_watchWithAdditionalTables(tables);
|
||||
}
|
||||
|
||||
enum _CompoundOperator {
|
||||
union('UNION'),
|
||||
unionAll('UNION ALL'),
|
||||
intersect('INTERSECT'),
|
||||
except('EXCEPT');
|
||||
|
||||
final String lexeme;
|
||||
|
||||
const _CompoundOperator(this.lexeme);
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Represents an `UPDATE` statement in sql.
|
||||
class UpdateStatement<T extends Table, D> extends Query<T, D>
|
||||
with SingleTableQueryMixin<T, D> {
|
||||
/// Used internally by drift, construct an update statement
|
||||
UpdateStatement(super.database, TableInfo<T, D> super.table);
|
||||
|
||||
late Map<String, Expression> _updatedFields;
|
||||
|
||||
@override
|
||||
void writeStartPart(GenerationContext ctx) {
|
||||
// TODO support the OR (ROLLBACK / ABORT / REPLACE / FAIL / IGNORE...) thing
|
||||
|
||||
ctx.buffer.write('UPDATE ${table.tableWithAlias} SET ');
|
||||
|
||||
var first = true;
|
||||
_updatedFields.forEach((columnName, variable) {
|
||||
if (!first) {
|
||||
ctx.buffer.write(', ');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
ctx.buffer
|
||||
..write(ctx.identifier(columnName))
|
||||
..write(' = ');
|
||||
|
||||
variable.writeInto(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> _performQuery() async {
|
||||
final ctx = constructQuery();
|
||||
final rows = await database.withCurrentExecutor((e) async {
|
||||
return await e.runUpdate(ctx.sql, ctx.boundVariables);
|
||||
});
|
||||
|
||||
if (rows > 0) {
|
||||
database.notifyUpdates(
|
||||
{TableUpdate.onTable(_sourceTable, kind: UpdateKind.update)});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// Writes all non-null fields from [entity] into the columns of all rows
|
||||
/// that match the [where] clause. Warning: That also means that, when you're
|
||||
/// not setting a where clause explicitly, this method will update all rows in
|
||||
/// the [table].
|
||||
///
|
||||
/// The fields that are null on the [entity] object will not be changed by
|
||||
/// this operation, they will be ignored.
|
||||
///
|
||||
/// When [dontExecute] is true (defaults to false), the query will __NOT__ be
|
||||
/// run, but all the validations are still in place. This is mainly used
|
||||
/// internally by drift.
|
||||
///
|
||||
/// Returns the amount of rows that have been affected by this operation.
|
||||
///
|
||||
/// See also: [replace], which does not require [where] statements and
|
||||
/// supports setting fields back to null.
|
||||
Future<int> write(Insertable<D> entity, {bool dontExecute = false}) async {
|
||||
_sourceTable.validateIntegrity(entity).throwIfInvalid(entity);
|
||||
|
||||
_updatedFields = entity.toColumns(true);
|
||||
|
||||
if (_updatedFields.isEmpty) {
|
||||
// nothing to update, we're done
|
||||
return Future.value(0);
|
||||
}
|
||||
|
||||
if (dontExecute) return -1;
|
||||
return await _performQuery();
|
||||
}
|
||||
|
||||
/// Applies the updates from [entity] to all rows matching the applied `where`
|
||||
/// clause and returns affected rows _after the update_.
|
||||
///
|
||||
/// For more details on writing entries, see [write].
|
||||
/// Note that this requires sqlite 3.35 or later.
|
||||
Future<List<D>> writeReturning(Insertable<D> entity) async {
|
||||
writeReturningClause = true;
|
||||
await write(entity, dontExecute: true);
|
||||
if (_updatedFields.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final ctx = constructQuery();
|
||||
final rows = await database.withCurrentExecutor((e) {
|
||||
return e.runSelect(ctx.sql, ctx.boundVariables);
|
||||
});
|
||||
|
||||
if (rows.isNotEmpty) {
|
||||
database.notifyUpdates(
|
||||
{TableUpdate.onTable(_sourceTable, kind: UpdateKind.update)});
|
||||
}
|
||||
|
||||
return rows.mapAsyncAndAwait(table.map);
|
||||
}
|
||||
|
||||
/// Replaces the old version of [entity] that is stored in the database with
|
||||
/// the fields of the [entity] provided here. This implicitly applies a
|
||||
/// [where] clause to rows with the same primary key as [entity], so that only
|
||||
/// the row representing outdated data will be replaced.
|
||||
///
|
||||
/// If [entity] has absent values (set to null on the [DataClass] or
|
||||
/// explicitly to absent on the [UpdateCompanion]), and a default value for
|
||||
/// the field exists, that default value will be used. Otherwise, the field
|
||||
/// will be reset to null. This behavior is different to [write], which simply
|
||||
/// ignores such fields without changing them in the database.
|
||||
///
|
||||
/// When [dontExecute] is true (defaults to false), the query will __NOT__ be
|
||||
/// run, but all the validations are still in place. This is mainly used
|
||||
/// internally by drift.
|
||||
///
|
||||
/// Returns true if a row was affected by this operation.
|
||||
///
|
||||
/// See also:
|
||||
/// - [write], which doesn't apply a [where] statement itself and ignores
|
||||
/// null values in the entity.
|
||||
/// - [InsertStatement.insert] with the `orReplace` parameter, which behaves
|
||||
/// similar to this method but creates a new row if none exists.
|
||||
Future<bool> replace(Insertable<D> entity, {bool dontExecute = false}) async {
|
||||
// We don't turn nulls to absent values here (as opposed to a regular
|
||||
// update, where only non-null fields will be written).
|
||||
final columns = entity.toColumns(false);
|
||||
_sourceTable
|
||||
.validateIntegrity(entity, isInserting: true)
|
||||
.throwIfInvalid(entity);
|
||||
assert(
|
||||
whereExpr == null,
|
||||
'When using replace on an update statement, you may not use where(...)'
|
||||
'as well. The where clause will be determined automatically');
|
||||
|
||||
whereSamePrimaryKey(entity);
|
||||
|
||||
// copying to work around type issues - Map<String, Variable> extends
|
||||
// Map<String, Expression> but crashes when adding anything that is not
|
||||
// a Variable.
|
||||
_updatedFields = columns is Map<String, Variable>
|
||||
? Map<String, Expression>.of(columns)
|
||||
: columns;
|
||||
|
||||
final primaryKeys = _sourceTable.$primaryKey.map((c) => c.$name);
|
||||
|
||||
// entityToSql doesn't include absent values, so we might have to apply the
|
||||
// default value here
|
||||
for (final column in table.$columns) {
|
||||
// if a default value exists and no value is set, apply the default
|
||||
if (column.defaultValue != null &&
|
||||
!_updatedFields.containsKey(column.$name)) {
|
||||
_updatedFields[column.$name] = column.defaultValue!;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't update the primary key
|
||||
_updatedFields.removeWhere((key, _) => primaryKeys.contains(key));
|
||||
|
||||
if (dontExecute) return false;
|
||||
final updatedRows = await _performQuery();
|
||||
return updatedRows != 0;
|
||||
}
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
import 'dart:core' as core;
|
||||
import 'dart:core';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../query_builder/query_builder.dart';
|
||||
|
||||
/// Database-specific helper methods mapping Dart values from and to SQL
|
||||
/// variables or literals.
|
||||
final class SqlTypes {
|
||||
// Stolen from DateTime._parseFormat
|
||||
static final RegExp _timeZoneInDateTime =
|
||||
RegExp(r' ?([-+])(\d\d)(?::?(\d\d))?$');
|
||||
|
||||
/// Whether these type mappings have been configured to store date time values
|
||||
/// as text.
|
||||
///
|
||||
/// When false (the default), date times values are stored as unix timestamps
|
||||
/// with second accuracy. When true, date time values are stored as an
|
||||
/// ISO-8601 string.
|
||||
///
|
||||
/// For more details on the mapping, see [the documentation].
|
||||
///
|
||||
/// [the documentation]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#supported-column-types
|
||||
final bool storeDateTimesAsText;
|
||||
|
||||
/// The [SqlDialect] to consider when mapping values from and to Dart.
|
||||
final SqlDialect dialect;
|
||||
|
||||
/// Creates an [SqlTypes] mapper from the provided options.
|
||||
@internal
|
||||
const SqlTypes(this.storeDateTimesAsText, [this.dialect = SqlDialect.sqlite]);
|
||||
|
||||
/// Maps a Dart object to a (possibly simpler) object that can be used as a
|
||||
/// parameter in raw sql queries.
|
||||
Object? mapToSqlVariable(Object? dartValue) {
|
||||
if (dartValue == null) return null;
|
||||
|
||||
// These need special handling, all other types are a direct mapping
|
||||
if (dartValue is DateTime) {
|
||||
if (storeDateTimesAsText) {
|
||||
// sqlite3 assumes UTC by default, so we store the explicit UTC offset
|
||||
// along with the value. For UTC datetimes, there's nothing to change
|
||||
if (dartValue.isUtc) {
|
||||
return dartValue.toIso8601String();
|
||||
} else {
|
||||
final offset = dartValue.timeZoneOffset;
|
||||
// Quick sanity check: We can only store the UTC offset as `hh:mm`,
|
||||
// so if the offset has seconds for some reason we should refuse to
|
||||
// store that.
|
||||
if (offset.inSeconds - 60 * offset.inMinutes != 0) {
|
||||
throw ArgumentError.value(dartValue, 'dartValue',
|
||||
'Cannot be mapped to SQL: Invalid UTC offset $offset');
|
||||
}
|
||||
|
||||
final hours = offset.inHours.abs();
|
||||
final minutes = offset.inMinutes.abs() - 60 * hours;
|
||||
|
||||
// For local date times, add the offset as ` +hh:mm` in the end. This
|
||||
// format is understood by `DateTime.parse` and date time functions in
|
||||
// sqlite.
|
||||
final prefix = offset.isNegative ? ' -' : ' +';
|
||||
final formattedOffset = '${hours.toString().padLeft(2, '0')}:'
|
||||
'${minutes.toString().padLeft(2, '0')}';
|
||||
|
||||
return '${dartValue.toIso8601String()}$prefix$formattedOffset';
|
||||
}
|
||||
} else {
|
||||
return dartValue.millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
}
|
||||
|
||||
if (dartValue is bool && dialect == SqlDialect.sqlite) {
|
||||
return dartValue ? 1 : 0;
|
||||
}
|
||||
|
||||
if (dartValue is DriftAny) {
|
||||
return dartValue.rawSqlValue;
|
||||
}
|
||||
|
||||
return dartValue;
|
||||
}
|
||||
|
||||
/// Maps the [dart] value into a SQL literal that can be embedded in SQL
|
||||
/// queries.
|
||||
String mapToSqlLiteral(Object? dart) {
|
||||
if (dart == null) return 'NULL';
|
||||
|
||||
// todo: Inline and remove types in the next major drift version
|
||||
if (dart is bool) {
|
||||
if (dialect == SqlDialect.sqlite) {
|
||||
return dart ? '1' : '0';
|
||||
} else {
|
||||
return dart ? 'true' : 'false';
|
||||
}
|
||||
} else if (dart is String) {
|
||||
// From the sqlite docs: (https://www.sqlite.org/lang_expr.html)
|
||||
// A string constant is formed by enclosing the string in single quotes
|
||||
// (').
|
||||
// A single quote within the string can be encoded by putting two single
|
||||
// quotes in a row - as in Pascal. C-style escapes using the backslash
|
||||
// character are not supported because they are not standard SQL.
|
||||
final escapedChars = dart.replaceAll('\'', '\'\'');
|
||||
return "'$escapedChars'";
|
||||
} else if (dart is num || dart is BigInt) {
|
||||
return dart.toString();
|
||||
} else if (dart is DateTime) {
|
||||
if (storeDateTimesAsText) {
|
||||
final encoded = mapToSqlVariable(dart).toString();
|
||||
return "'$encoded'";
|
||||
} else {
|
||||
return (dart.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
}
|
||||
} else if (dart is Uint8List) {
|
||||
final String hexString = hex.encode(dart);
|
||||
|
||||
if (dialect == SqlDialect.postgres) {
|
||||
// Postgres BYTEA hex format
|
||||
// https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-HEX-FORMAT
|
||||
return "'\\x$hexString'::bytea";
|
||||
} else {
|
||||
// BLOB literals are string literals containing hexadecimal data and
|
||||
// preceded by a single "x" or "X" character. Example: X'53514C697465'
|
||||
return "x'$hexString'";
|
||||
}
|
||||
} else if (dart is DriftAny) {
|
||||
return mapToSqlLiteral(dart.rawSqlValue);
|
||||
}
|
||||
|
||||
throw ArgumentError.value(dart, 'dart',
|
||||
'Must be null, bool, String, int, DateTime, Uint8List or double');
|
||||
}
|
||||
|
||||
DateTime _readDateTime(Object sqlValue) {
|
||||
if (storeDateTimesAsText) {
|
||||
final rawValue = read(DriftSqlType.string, sqlValue)!;
|
||||
DateTime result;
|
||||
|
||||
// We store date times like this:
|
||||
//
|
||||
// - if it's in UTC, we call [DateTime.toIso8601String], so there's a
|
||||
// trailing `Z`. We can just use [DateTime.parse] and get an utc
|
||||
// datetime back.
|
||||
// - for local date times, we append the time zone offset, e.g.
|
||||
// `+02:00`. [DateTime.parse] respects this UTC offset and returns
|
||||
// the correct date, but it returns it in UTC. Since we only use
|
||||
// this format for local times, we need to transform it back to
|
||||
// local.
|
||||
//
|
||||
// Additionally, complex date time expressions are wrapped in a
|
||||
// `datetime` sqlite call, which doesn't append a `Z` or a time zone
|
||||
// offset. As sqlite3 always uses UTC for these computations
|
||||
// internally, we'll return a UTC datetime as well.
|
||||
if (_timeZoneInDateTime.hasMatch(rawValue)) {
|
||||
// Case 2: Explicit time zone offset given, we do this for local
|
||||
// dates.
|
||||
result = DateTime.parse(rawValue).toLocal();
|
||||
} else if (rawValue.endsWith('Z')) {
|
||||
// Case 1: Date time in UTC, [DateTime.parse] will do the right
|
||||
// thing.
|
||||
result = DateTime.parse(rawValue);
|
||||
} else {
|
||||
// Result from complex date time transformation. Interpret as UTC,
|
||||
// which is what sqlite3 does by default.
|
||||
result = DateTime.parse('${rawValue}Z');
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
final unixSeconds = read(DriftSqlType.int, sqlValue)!;
|
||||
return DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a raw [sqlValue] to Dart given its sql [type] (typically a
|
||||
/// [DriftSqlType]).
|
||||
T? read<T extends Object>(BaseSqlType<T> type, Object? sqlValue) {
|
||||
if (sqlValue == null) return null;
|
||||
|
||||
return switch (type) {
|
||||
DriftSqlType.bool => (sqlValue != 0 && sqlValue != false),
|
||||
DriftSqlType.string => sqlValue.toString(),
|
||||
DriftSqlType.bigInt => switch (sqlValue) {
|
||||
BigInt() => sqlValue,
|
||||
int() => BigInt.from(sqlValue),
|
||||
_ => BigInt.parse(sqlValue.toString()),
|
||||
},
|
||||
DriftSqlType.int => switch (sqlValue) {
|
||||
int() => sqlValue,
|
||||
BigInt() => sqlValue.toInt(),
|
||||
double() => sqlValue.toInt(),
|
||||
_ => int.parse(sqlValue.toString()),
|
||||
},
|
||||
DriftSqlType.dateTime => _readDateTime(sqlValue),
|
||||
DriftSqlType.blob => switch (sqlValue) {
|
||||
String() => Uint8List.fromList(sqlValue.codeUnits),
|
||||
_ => sqlValue,
|
||||
},
|
||||
DriftSqlType.double => switch (sqlValue) {
|
||||
BigInt() => sqlValue.toDouble(),
|
||||
_ => (sqlValue as num).toDouble(),
|
||||
},
|
||||
DriftSqlType.any => DriftAny(sqlValue),
|
||||
CustomSqlType() => type.read(sqlValue),
|
||||
DialectAwareSqlType() => type.read(this, sqlValue),
|
||||
} as T;
|
||||
}
|
||||
}
|
||||
|
||||
/// A drift type around a SQL value with an unknown type.
|
||||
///
|
||||
/// In [STRICT tables], a column can be declared with the type `ANY`. In such
|
||||
/// column, _any_ value can be stored without sqlite3 (or drift) attempting to
|
||||
/// cast it to a specific type. Thus, the [rawSqlValue] is directly passed to
|
||||
/// or from the underlying SQL database package.
|
||||
///
|
||||
/// To write a custom value into the database with [DriftAny], you can construct
|
||||
/// it and pass it into a [Variable] or into a companion of a table having a
|
||||
/// column with an `ANY` type.
|
||||
///
|
||||
/// [STRICT tables]: https://www.sqlite.org/stricttables.html
|
||||
final class DriftAny {
|
||||
/// The direct, unmodified SQL value being wrapped by this [DriftAny]
|
||||
/// instance.
|
||||
///
|
||||
/// Please note that a [rawSqlValue] can't always be mapped to a unique Dart
|
||||
/// interpretation - see [readAs] for a discussion of which additional
|
||||
/// information is necessary to interpret this value.
|
||||
final Object rawSqlValue;
|
||||
|
||||
/// Constructs a [DriftAny] wrapper around the [rawSqlValue] that will be
|
||||
/// written into the database without any modification by drift.
|
||||
const DriftAny(this.rawSqlValue) : assert(rawSqlValue is! DriftAny);
|
||||
|
||||
/// Interprets the [rawSqlValue] as a drift [type] under the configuration
|
||||
/// given by [types].
|
||||
///
|
||||
/// A given [rawSqlValue] may have different Dart representations that would
|
||||
/// be given to you by drift. For instance, the SQL value `1` could have the
|
||||
/// following possible Dart interpretations:
|
||||
///
|
||||
/// - The [bool] constant `true`.
|
||||
/// - The [int] constant `1`
|
||||
/// - The big integer [BigInt.one].
|
||||
/// - All [DateTime] values having `1` as their UNIX timestamp in seconds
|
||||
/// (this depends on the configuration - drift can be configured to store
|
||||
/// date times [as text] too).
|
||||
///
|
||||
/// For this reason, it is not always possible to directly map these raw
|
||||
/// values to Dart without further information. Drift also needs to know the
|
||||
/// expected type and some configuration options for context. For all SQL
|
||||
/// types _except_ `ANY`, drift will do this for you behind the scenes.
|
||||
///
|
||||
/// You can obtain a [types] instance from a database or DAO by using
|
||||
/// [DatabaseConnectionUser.typeMapping].
|
||||
///
|
||||
/// [as text]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#datetime-options
|
||||
T readAs<T extends Object>(BaseSqlType<T> type, SqlTypes types) {
|
||||
return types.read<T>(type, rawSqlValue)!;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(DriftAny, rawSqlValue);
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return identical(this, other) ||
|
||||
other is DriftAny && other.rawSqlValue == rawSqlValue;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DriftAny(raw: $rawSqlValue)';
|
||||
}
|
||||
}
|
||||
|
||||
/// The superclass for SQL types, whether built-in to drift ([DriftSqlType]) or
|
||||
/// provided by the user through [CustomSqlType]s.
|
||||
@internal
|
||||
sealed class BaseSqlType<T> {
|
||||
/// Returns a suitable representation of this type in SQL.
|
||||
String sqlTypeName(GenerationContext context);
|
||||
|
||||
static T? read<T extends Object>(
|
||||
SqlTypes types, BaseSqlType<T> type, Object fromSql) {
|
||||
return types.read(type, fromSql);
|
||||
}
|
||||
|
||||
static Object? mapToSqlParameter<T extends Object>(
|
||||
GenerationContext context, BaseSqlType<T>? type, T? value) {
|
||||
if (value == null) return null;
|
||||
|
||||
return switch (type) {
|
||||
null ||
|
||||
DriftSqlType<Object>() =>
|
||||
context.typeMapping.mapToSqlVariable(value),
|
||||
CustomSqlType<T>() => type.mapToSqlParameter(value),
|
||||
DialectAwareSqlType<T>() => type.mapToSqlParameter(context, value),
|
||||
};
|
||||
}
|
||||
|
||||
static String mapToSqlLiteral<T extends Object>(
|
||||
GenerationContext context, BaseSqlType<T>? type, T? value) {
|
||||
if (value == null) return 'NULL';
|
||||
|
||||
return switch (type) {
|
||||
null ||
|
||||
DriftSqlType<Object>() =>
|
||||
context.typeMapping.mapToSqlLiteral(value),
|
||||
CustomSqlType<T>() => type.mapToSqlLiteral(value),
|
||||
DialectAwareSqlType<T>() => type.mapToSqlLiteral(context, value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@internal
|
||||
sealed class UserDefinedSqlType<T> implements BaseSqlType<T> {}
|
||||
|
||||
/// An enumation of type mappings that are builtin to drift and `drift_dev`.
|
||||
enum DriftSqlType<T extends Object> implements BaseSqlType<T> {
|
||||
/// A boolean type, represented as `0` or `1` (int) in SQL.
|
||||
bool<core.bool>(),
|
||||
|
||||
/// A textual type, represented as `TEXT` in sqlite.
|
||||
string<String>(),
|
||||
|
||||
/// A 64-bit int type that is represented a [BigInt] in Dart for better
|
||||
/// compatibility with the web. Represented as an `INTEGER` in sqlite or as
|
||||
/// a `bigint` in postgres.
|
||||
bigInt<BigInt>(),
|
||||
|
||||
/// A 64-bit int.
|
||||
///
|
||||
/// Represented as an `INTEGER` in sqlite or as a `bigint` in postgres.
|
||||
int<core.int>(),
|
||||
|
||||
/// A [DateTime] value.
|
||||
///
|
||||
/// Depending on the options choosen at build-time, this is either stored as
|
||||
/// an unix timestamp (the default) or as a ISO 8601 string.
|
||||
dateTime<DateTime>(),
|
||||
|
||||
/// A [Uint8List] value.
|
||||
///
|
||||
/// This is stored as a `BLOB` in sqlite or as a `bytea` type in postgres.
|
||||
blob<Uint8List>(),
|
||||
|
||||
/// A [double] value, stored as a `REAL` type in sqlite.
|
||||
double<core.double>(),
|
||||
|
||||
/// The drift type for columns declared as `ANY` in [STRICT tables].
|
||||
///
|
||||
/// [STRICT tables]: https://www.sqlite.org/stricttables.html
|
||||
any<DriftAny>();
|
||||
|
||||
@override
|
||||
String sqlTypeName(GenerationContext context) {
|
||||
final dialect = context.dialect;
|
||||
|
||||
// ignore: unnecessary_cast
|
||||
switch (this as DriftSqlType<Object>) {
|
||||
case DriftSqlType.bool:
|
||||
return dialect.booleanType;
|
||||
case DriftSqlType.string:
|
||||
return dialect.textType;
|
||||
case DriftSqlType.bigInt:
|
||||
case DriftSqlType.int:
|
||||
return dialect.integerType;
|
||||
case DriftSqlType.dateTime:
|
||||
if (context.typeMapping.storeDateTimesAsText) {
|
||||
return dialect.textType;
|
||||
} else {
|
||||
return dialect.integerType;
|
||||
}
|
||||
case DriftSqlType.blob:
|
||||
return dialect.blobType;
|
||||
case DriftSqlType.double:
|
||||
return dialect.realType;
|
||||
case DriftSqlType.any:
|
||||
return 'ANY';
|
||||
}
|
||||
}
|
||||
|
||||
void _addToMap(Map<Type, DriftSqlType> map) {
|
||||
_addToTypeMap<T>(map, this);
|
||||
// Unfortunately, `T?` by itself is not an expression so we have to jump
|
||||
// through hoops to add the nullable variant to the type map.
|
||||
_addToTypeMap<T?>(map, this);
|
||||
}
|
||||
|
||||
static Map<Type, DriftSqlType> _dartToDrift = () {
|
||||
final map = <Type, DriftSqlType>{};
|
||||
|
||||
for (final value in values) {
|
||||
value._addToMap(map);
|
||||
}
|
||||
|
||||
return map;
|
||||
}();
|
||||
|
||||
static void _addToTypeMap<T>(
|
||||
Map<Type, DriftSqlType> map, DriftSqlType<Object> type) {
|
||||
map[T] = type;
|
||||
}
|
||||
|
||||
/// Attempts to find a suitable SQL type for the [Dart] type passed to this
|
||||
/// method.
|
||||
///
|
||||
/// The [Dart] type must be the type of the instance _after_ applying type
|
||||
/// converters.
|
||||
static DriftSqlType<Dart> forType<Dart extends Object>() {
|
||||
final type = _dartToDrift[Dart];
|
||||
|
||||
if (type == null) {
|
||||
throw ArgumentError('Could not find a matching SQL type for $Dart');
|
||||
}
|
||||
|
||||
return type as DriftSqlType<Dart>;
|
||||
}
|
||||
|
||||
/// A variant of [forType] that also works for nullable [Dart] types.
|
||||
///
|
||||
/// Using [forType] should pretty much always be preferred over this method,
|
||||
/// this one just exists for backwards compatibility.
|
||||
static DriftSqlType forNullableType<Dart>() {
|
||||
// Lookup the type in the map first for faster lookups. Go back to a full
|
||||
// typecheck where that doesn't work (which can be the case for complex
|
||||
// type like `forNullableType<FutureOr<int?>>`).
|
||||
final type = _dartToDrift[Dart] ??
|
||||
values.whereType<BaseSqlType<Dart>>().singleOrNull;
|
||||
|
||||
if (type == null) {
|
||||
throw ArgumentError('Could not find a matching SQL type for $Dart');
|
||||
}
|
||||
|
||||
return type as DriftSqlType;
|
||||
}
|
||||
}
|
||||
|
||||
/// Interface for a custom SQL type.
|
||||
///
|
||||
/// Being designed with sqlite3 as its primary database engine, drift lacks
|
||||
/// builtin support for the rich type system found in more complex database
|
||||
/// systems like postgres. By providing the [CustomSqlType] interface, drift can
|
||||
/// be extended to support any database type by customizing the way these types
|
||||
/// are mapped from and to the database.
|
||||
///
|
||||
/// To create a custom type, implement this interface. You can now create values
|
||||
/// of this type by passing it to [Constant] or [Variable], [Expression.cast]
|
||||
/// and other methods operating on types.
|
||||
/// Custom types can also be applied to table columns, see https://drift.simonbinder.eu/docs/sql-api/types/
|
||||
/// for details.
|
||||
abstract interface class CustomSqlType<T extends Object>
|
||||
implements UserDefinedSqlType<T> {
|
||||
/// Interprets the underlying [fromSql] value from the database driver into
|
||||
/// the Dart representation [T] of this type.
|
||||
T read(Object fromSql);
|
||||
|
||||
/// Maps the [dartValue] to a value understood by the underlying database
|
||||
/// driver.
|
||||
Object mapToSqlParameter(T dartValue);
|
||||
|
||||
/// Maps the [dartValue] to a SQL snippet that can be embedded as a literal
|
||||
/// into SQL queries generated by drift.
|
||||
String mapToSqlLiteral(T dartValue);
|
||||
}
|
||||
|
||||
/// A [CustomSqlType] with access on the dialect of the database engine when
|
||||
/// used in queries.
|
||||
///
|
||||
/// This can be used to design drift types providing polyfills for types only
|
||||
/// supported on some databases, for instance by using native `DATE` support
|
||||
/// on postgres but falling back to a textual representation on sqlite3.
|
||||
abstract interface class DialectAwareSqlType<T extends Object>
|
||||
implements UserDefinedSqlType<T> {
|
||||
/// Creates a [DialectAwareSqlType] that uses the [fallback] type by default,
|
||||
/// but can apply [overrides] on some database systems.
|
||||
///
|
||||
/// For instance, this can be used to create a custom type that stores uuids
|
||||
/// as `TEXT` on databases with no builtin UUID type, but otherwise uses the
|
||||
/// native format:
|
||||
///
|
||||
/// ```dart
|
||||
/// class UuidAsTextType implements CustomSqlType<Uuid> { ... }
|
||||
///
|
||||
/// const uuidType = DialectAwareSqlType.via(
|
||||
/// fallback: UuidAsTextType(),
|
||||
/// overrides: {
|
||||
/// SqlDialect.postgres: PgTypes.uuid,
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
const factory DialectAwareSqlType.via({
|
||||
required BaseSqlType<T> fallback,
|
||||
required Map<SqlDialect, BaseSqlType<T>> overrides,
|
||||
}) = _ByDialectType<T>;
|
||||
|
||||
/// Interprets the underlying [fromSql] value from the database driver into
|
||||
/// the Dart representation [T] of this type.
|
||||
T read(SqlTypes typeSystem, Object fromSql);
|
||||
|
||||
/// Maps the [dartValue] to a value understood by the underlying database
|
||||
/// driver.
|
||||
Object mapToSqlParameter(GenerationContext context, T dartValue);
|
||||
|
||||
/// Maps the [dartValue] to a SQL snippet that can be embedded as a literal
|
||||
/// into SQL queries generated by drift.
|
||||
String mapToSqlLiteral(GenerationContext context, T dartValue);
|
||||
}
|
||||
|
||||
final class _ByDialectType<T extends Object> implements DialectAwareSqlType<T> {
|
||||
final BaseSqlType<T> fallback;
|
||||
final Map<SqlDialect, BaseSqlType<T>> overrides;
|
||||
|
||||
const _ByDialectType({required this.fallback, required this.overrides});
|
||||
|
||||
BaseSqlType<T> _selectType(SqlTypes typeSystem) {
|
||||
return overrides[typeSystem.dialect] ?? fallback;
|
||||
}
|
||||
|
||||
@override
|
||||
String mapToSqlLiteral(GenerationContext context, T dartValue) {
|
||||
return BaseSqlType.mapToSqlLiteral(
|
||||
context, _selectType(context.typeMapping), dartValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Object mapToSqlParameter(GenerationContext context, T dartValue) {
|
||||
return BaseSqlType.mapToSqlParameter(
|
||||
context, _selectType(context.typeMapping), dartValue)!;
|
||||
}
|
||||
|
||||
@override
|
||||
T read(SqlTypes typeSystem, Object fromSql) {
|
||||
return BaseSqlType.read(typeSystem, _selectType(typeSystem), fromSql)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String sqlTypeName(GenerationContext context) {
|
||||
return _selectType(context.typeMapping).sqlTypeName(context);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user