Deleting more old code

This commit is contained in:
Simon Binder
2025-09-08 22:16:07 +02:00
parent 07abb7c700
commit 0b65770df8
27 changed files with 0 additions and 5353 deletions
@@ -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;
}
}
-545
View File
@@ -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);
}
}