Add docstings, limit & complete modular support

This commit is contained in:
Moshe Dicker 2024-04-03 14:00:43 -04:00
parent 242ec4d686
commit 0af30cfb11
5 changed files with 285 additions and 118 deletions

View File

@ -0,0 +1,53 @@
/// Internal library used by generated managers.
///
/// This library is not part of drift's public API and should not be imported
/// manually.
library drift.internal.manager;
import 'package:drift/drift.dart';
import 'package:drift/src/runtime/manager/manager.dart';
/// Utility for creating a composer which contains the joins needed to
/// execute a query on a table that is referenced by a foreign key.
B composeWithJoins<RT extends Table, DB extends GeneratedDatabase,
CT extends Table, QC extends Composer<DB, RT>, B extends HasJoinBuilders>({
required ComposerState<DB, CT> $state,
required GeneratedColumn Function(CT) getCurrentColumn,
required RT referencedTable,
required GeneratedColumn Function(RT) getReferencedColumn,
required B Function(QC) builder,
required QC Function(DB db, RT table) getReferencedComposer,
}) {
// The name of the alias will be created using the following logic:
// "currentTableName__currentColumnName__referencedColumnName__referencedTableName"
// This is to ensure that the alias is unique
final currentColumn = getCurrentColumn($state.table);
final tempReferencedColumn = getReferencedColumn(referencedTable);
final aliasName =
'${currentColumn.tableName}__${currentColumn.name}__${tempReferencedColumn.tableName}__${tempReferencedColumn.name}';
final aliasedReferencedTable =
$state.db.alias(referencedTable as TableInfo, aliasName);
final aliasedReferencedColumn =
getReferencedColumn(aliasedReferencedTable as RT);
// Create a join builder for the referenced table
final joinBuilder = JoinBuilder(
currentTable: $state.table,
currentColumn: currentColumn,
referencedTable: aliasedReferencedTable,
referencedColumn: aliasedReferencedColumn,
);
// Get the query composer for the referenced table, passing in the aliased
// table and all the join builders
final referencedComposer =
getReferencedComposer($state.db, aliasedReferencedTable);
// Run the user provided builder with the referencedQueryComposer
// This may return a filter or ordering, but we only enforce that it's
// a HasJoinBuilders
final result = builder(referencedComposer);
result.addJoinBuilders({joinBuilder});
return result;
}

View File

@ -34,50 +34,7 @@ class ComposerState<DB extends GeneratedDatabase, T extends Table> {
@internal
sealed class Composer<DB extends GeneratedDatabase, CT extends Table> {
/// The state of the composer
final ComposerState<DB, CT> state;
final ComposerState<DB, CT> $state;
Composer(DB db, CT table) : state = ComposerState._(db, table);
/// Method for create a join between two tables
B referenced<RT extends Table, QC extends Composer<DB, RT>,
B extends HasJoinBuilders>({
required GeneratedColumn Function(CT) getCurrentColumn,
required RT referencedTable,
required GeneratedColumn Function(RT) getReferencedColumn,
required B Function(QC) builder,
required QC Function(DB db, RT table) getReferencedComposer,
}) {
// The name of the alias will be created using the following logic:
// "currentTableName__currentColumnName__referencedColumnName__referencedTableName"
// This is to ensure that the alias is unique
final currentColumn = getCurrentColumn(state.table);
final tempReferencedColumn = getReferencedColumn(referencedTable);
final aliasName =
'${currentColumn.tableName}__${currentColumn.name}__${tempReferencedColumn.tableName}__${tempReferencedColumn.name}';
final aliasedReferencedTable =
state.db.alias(referencedTable as TableInfo, aliasName);
final aliasedReferencedColumn =
getReferencedColumn(aliasedReferencedTable as RT);
// Create a join builder for the referenced table
final joinBuilder = JoinBuilder(
currentTable: state.table,
currentColumn: currentColumn,
referencedTable: aliasedReferencedTable,
referencedColumn: aliasedReferencedColumn,
);
// Get the query composer for the referenced table, passing in the aliased
// table and all the join builders
final referencedComposer =
getReferencedComposer(state.db, aliasedReferencedTable);
// Run the user provided builder with the referencedQueryComposer
// This may return a filter or ordering, but we only enforce that it's
// a HasJoinBuilders
final result = builder(referencedComposer);
result.addJoinBuilders({joinBuilder});
return result;
}
Composer(DB db, CT table) : $state = ComposerState._(db, table);
}

View File

@ -45,6 +45,7 @@ class ColumnFilters<T extends Object> {
ColumnFilters<int> get count => ColumnFilters(column.count());
}
/// Built in filters for bool columns
extension BoolFilters on ColumnFilters<bool> {
/// Create a filter to check if the column is bigger than a value
ComposableFilter isTrue(bool value) =>

View File

@ -214,15 +214,16 @@ class TableManagerState<
};
}
/// Build an update statement based on the manager state
UpdateStatement<T, DT> buildUpdateStatement() {
final UpdateStatement<T, DT> deleteStatement;
final UpdateStatement<T, DT> updateStatement;
if (joinBuilders.isEmpty) {
deleteStatement = db.update(_tableAsTableInfo);
updateStatement = db.update(_tableAsTableInfo);
if (filter != null) {
deleteStatement.where((_) => filter!);
updateStatement.where((_) => filter!);
}
} else {
deleteStatement = db.update(_tableAsTableInfo);
updateStatement = db.update(_tableAsTableInfo);
for (var col in _tableAsTableInfo.primaryKey) {
final subquery = _buildSelectStatement(
targetColumns: [col],
@ -230,12 +231,13 @@ class TableManagerState<
applyFilters: true,
applyOrdering: false,
applyLimit: false) as _JoinedResult<T, DT>;
deleteStatement.where((tbl) => col.isInQuery(subquery.statement));
updateStatement.where((tbl) => col.isInQuery(subquery.statement));
}
}
return deleteStatement;
return updateStatement;
}
/// Count the number of rows that would be returned by the built statement
Future<int> count() {
final count = countAll();
final result = _buildSelectStatement(
@ -247,7 +249,7 @@ class TableManagerState<
return result.statement.map((row) => row.read(count)!).getSingle();
}
// Build a delete statement based on the manager state
/// Build a delete statement based on the manager state
DeleteStatement buildDeleteStatement() {
final DeleteStatement deleteStatement;
if (joinBuilders.isEmpty) {
@ -285,36 +287,63 @@ abstract class BaseTableManager<
CI extends Function,
CU extends Function> {
/// The state for this manager
final TableManagerState<DB, T, DT, FS, OS, C, CI, CU> state;
final TableManagerState<DB, T, DT, FS, OS, C, CI, CU> $state;
/// Create a new [BaseTableManager] instance
const BaseTableManager(this.state);
Future<int> delete() => state.buildDeleteStatement().go();
const BaseTableManager(this.$state);
/// Deletes all rows matched by built statement
///
/// Returns the amount of rows that were deleted by this statement directly
/// (not including additional rows that might be affected through triggers or
/// foreign key constraints).
Future<int> delete() => $state.buildDeleteStatement().go();
/// Add ordering to the statement
C orderBy(ComposableOrdering Function(OS o) o) {
final orderings = o(state.orderingComposer);
return state._getChildManagerBuilder(state.copyWith(
final orderings = o($state.orderingComposer);
return $state._getChildManagerBuilder($state.copyWith(
orderingBuilders:
state.orderingBuilders.union(orderings.orderingBuilders),
joinBuilders: state.joinBuilders.union(orderings.joinBuilders)));
$state.orderingBuilders.union(orderings.orderingBuilders),
joinBuilders: $state.joinBuilders.union(orderings.joinBuilders)));
}
/// Add a filter to the statement
C filter(ComposableFilter Function(FS f) f) {
final filter = f(state.filteringComposer);
return state._getChildManagerBuilder(state.copyWith(
filter: state.filter == null
final filter = f($state.filteringComposer);
return $state._getChildManagerBuilder($state.copyWith(
filter: $state.filter == null
? filter.expression
: filter.expression & state.filter!,
joinBuilders: state.joinBuilders.union(filter.joinBuilders)));
: filter.expression & $state.filter!,
joinBuilders: $state.joinBuilders.union(filter.joinBuilders)));
}
Future<int> count() => state.count();
/// Add a limit to the statement
C limit(int limit, {int? offset}) {
return $state
._getChildManagerBuilder($state.copyWith(limit: limit, offset: offset));
}
/// Return the count of rows matched by the built statement
Future<int> count() => $state.count();
/// Writes all non-null fields from the entity into the columns of all rows
/// that match the [filter] clause. Warning: That also means that, when you're
/// not setting a where clause explicitly, this method will update all rows in
/// the [$state.table].
///
/// The fields that are null on the entity object will not be changed by
/// this operation, they will be ignored.
///
/// Returns the amount of rows that have been affected by this operation.
///
/// See also: [RootTableManager.replace], which does not require [filter] statements and
/// supports setting fields back to null.
Future<int> write(Insertable<DT> Function(CU o) f) =>
state.buildUpdateStatement().write(f(state._getUpdateCompanionBuilder));
$state.buildUpdateStatement().write(f($state._getUpdateCompanionBuilder));
}
/// A table manager that can be used to select rows from a table
abstract class ProcessedTableManager<
DB extends GeneratedDatabase,
T extends Table,
@ -329,24 +358,61 @@ abstract class ProcessedTableManager<
MultiSelectable<D>,
SingleSelectable<D>,
SingleOrNullSelectable<D> {
const ProcessedTableManager(super.state);
/// Create a new [ProcessedTableManager] instance
const ProcessedTableManager(super.$state);
/// Executes this statement, like [get], but only returns one
/// value. If the query returns no or too many rows, the returned future will
/// complete with an error.
///
/// Be aware that this operation won't put a limit clause on this statement,
/// if that's needed you would have to do use [limit]:
/// You should only use this method if you know the query won't have more than
/// one row, for instance because you used `limit(1)` or you know the filters
/// you've applied will only match one row.
///
/// See also: [getSingleOrNull], which returns `null` instead of
/// throwing if the query completes with no rows.
@override
Future<D> getSingle() => state.buildSelectStatement().getSingle();
Future<D> getSingle() => $state.buildSelectStatement().getSingle();
/// Creates an auto-updating stream of this statement, similar to
/// [watch]. However, it is assumed that the query will only emit
/// one result, so instead of returning a `Stream<List<D>>`, this returns a
/// `Stream<D>`. If, at any point, the query emits no or more than one rows,
/// an error will be added to the stream instead.
@override
Stream<D> watchSingle() => state.buildSelectStatement().watchSingle();
Stream<D> watchSingle() => $state.buildSelectStatement().watchSingle();
/// Executes the statement and returns the first all rows as a list.
@override
Future<List<D>> get() => state.buildSelectStatement().get();
Future<List<D>> get() => $state.buildSelectStatement().get();
/// Creates an auto-updating stream of the result that emits new items
/// whenever any table used in this statement changes.
@override
Stream<List<D>> watch() => state.buildSelectStatement().watch();
Stream<List<D>> watch() => $state.buildSelectStatement().watch();
/// Executes this statement, like [get], but only returns one
/// value. If the result too many values, this method will throw. If no
/// row is returned, `null` will be returned instead.
///
/// See also: [getSingle], which can be used if the query will
/// always evaluate to exactly one row.
@override
Future<D?> getSingleOrNull() =>
state.buildSelectStatement().getSingleOrNull();
$state.buildSelectStatement().getSingleOrNull();
/// Creates an auto-updating stream of this statement, similar to
/// [watch]. However, it is assumed that the query will only
/// emit one result, so instead of returning a `Stream<List<D>>`, this
/// returns a `Stream<D?>`. If the query emits more than one row at
/// some point, an error will be emitted to the stream instead.
/// If the query emits zero rows at some point, `null` will be added
/// to the stream instead.
@override
Stream<D?> watchSingleOrNull() =>
state.buildSelectStatement().watchSingleOrNull();
Future<int> delete() => state.buildDeleteStatement().go();
$state.buildSelectStatement().watchSingleOrNull();
}
/// A table manager with top level function for creating, reading, updating, and deleting items
@ -359,34 +425,102 @@ abstract class RootTableManager<
C extends ProcessedTableManager<DB, T, D, FS, OS, C, CI, CU>,
CI extends Function,
CU extends Function> extends BaseTableManager<DB, T, D, FS, OS, C, CI, CU> {
const RootTableManager(super.state);
/// Create a new [RootTableManager] instance
const RootTableManager(super.$state);
C all() => state._getChildManagerBuilder(state);
/// Select all rows from the table
C all() => $state._getChildManagerBuilder($state);
/// Creates a new row in the table using the given function
///
/// By default, an exception will be thrown if another row with the same
/// primary key already exists. This behavior can be overridden with [mode],
/// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
///
/// To apply a partial or custom update in case of a conflict, you can also
/// use an [upsert clause](https://sqlite.org/lang_UPSERT.html) by using
/// [onConflict]. See [InsertStatement.insert] for more information.
///
/// By default, the [onConflict] clause will only consider the table's primary
/// key. If you have additional columns with uniqueness constraints, you have
/// to manually add them to the clause's [DoUpdate.target].
///
/// Returns the `rowid` of the inserted row. For tables with an auto-increment
/// column, the `rowid` is the generated value of that column. The returned
/// value can be inaccurate when [onConflict] is set and the insert behaved
/// like an update.
///
/// If the table doesn't have a `rowid`, you can't rely on the return value.
/// Still, the future will always complete with an error if the insert fails.
Future<int> create(Insertable<D> Function(CI o) f,
{InsertMode? mode, UpsertClause<T, D>? onConflict}) {
return state.db.into(state._tableAsTableInfo).insert(
f(state._getInsertCompanionBuilder),
return $state.db.into($state._tableAsTableInfo).insert(
f($state._getInsertCompanionBuilder),
mode: mode,
onConflict: onConflict);
}
/// 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 [createReturningOrNull] when performing an
/// insert with an insert mode like [InsertMode.insertOrIgnore] or when using
/// a [DoUpdate] with a `where` clause clause.
Future<D> createReturning(Insertable<D> Function(CI o) f,
{InsertMode? mode, UpsertClause<T, D>? onConflict}) {
return state.db.into(state._tableAsTableInfo).insertReturning(
f(state._getInsertCompanionBuilder),
return $state.db.into($state._tableAsTableInfo).insertReturning(
f($state._getInsertCompanionBuilder),
mode: mode,
onConflict: onConflict);
}
/// 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?> createReturningOrNull(Insertable<D> Function(CI o) f,
{InsertMode? mode, UpsertClause<T, D>? onConflict}) {
return $state.db.into($state._tableAsTableInfo).insertReturningOrNull(
f($state._getInsertCompanionBuilder),
mode: mode,
onConflict: onConflict);
}
/// Create multiple rows in the table using the given function
///
/// All fields in a row that don't have a default value or auto-increment
/// must be set and non-null. Otherwise, an [InvalidDataException] will be
/// thrown.
/// By default, an exception will be thrown if another row with the same
/// primary key already exists. This behavior can be overridden with [mode],
/// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
/// Using [bulkCreate] will not disable primary keys or any column constraint
/// checks.
/// [onConflict] can be used to create an upsert clause for engines that
/// support it. For details and examples, see [InsertStatement.insert].
Future<void> bulkCreate(Iterable<Insertable<D>> Function(CI o) f,
{InsertMode? mode, UpsertClause<T, D>? onConflict}) {
return state.db.batch((b) => b.insertAll(
state._tableAsTableInfo, f(state._getInsertCompanionBuilder),
return $state.db.batch((b) => b.insertAll(
$state._tableAsTableInfo, f($state._getInsertCompanionBuilder),
mode: mode, onConflict: onConflict));
}
Future<bool> replace(Insertable<D> entry) {
return state.db.update(state._tableAsTableInfo).replace(entry);
/// Replaces the old version of [entity] that is stored in the database with
/// the fields of the [entity] provided here. This implicitly applies a
/// [filter] 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.
///
/// Returns true if a row was affected by this operation.
Future<bool> replace(Insertable<D> entity) {
return $state.db.update($state._tableAsTableInfo).replace(entity);
}
}

View File

@ -1,5 +1,6 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:drift_dev/src/writer/modules.dart';
import 'package:drift_dev/src/writer/writer.dart';
abstract class _FilterWriter {
@ -36,7 +37,7 @@ class _RegularFilterWriter extends _FilterWriter {
..writeDriftRef("ColumnFilters")
..write("<$type> get $filterName =>")
..writeDriftRef("ColumnFilters")
..write("(state.table.$fieldGetter);");
..write("(\$state.table.$fieldGetter);");
}
}
@ -63,15 +64,15 @@ class _FilterWithConverterWriter extends _FilterWriter {
..writeDriftRef("ColumnWithTypeConverterFilters")
..write("<$converterType,$type> get $filterName =>")
..writeDriftRef("ColumnWithTypeConverterFilters")
..writeln("(state.table.$fieldGetter);");
..writeln("(\$state.table.$fieldGetter);");
}
}
class _ReferencedFilter extends _FilterWriter {
/// The full function used to get the referenced table
///
/// E.G `state.db.resultSet<$CategoryTable>('categories')`
/// or `state.db.categories`
/// E.G `\$state.db.resultSet<$CategoryTable>('categories')`
/// or `\$state.db.categories`
final String referencedTableField;
/// The getter for the column on the referenced table
@ -100,14 +101,18 @@ class _ReferencedFilter extends _FilterWriter {
..write(" $filterName(")
..writeDriftRef("ComposableFilter")
..writeln(" Function( $referencedFilterComposer f) f) {")
..write('''
return referenced(
referencedTable: $referencedTableField,
getCurrentColumn: (f) => f.$fieldGetter,
getReferencedColumn: (f) => f.$referencedColumnGetter,
getReferencedComposer: (db, table) => $referencedFilterComposer(db, table),
builder: f);
}''');
..write("return ")
..writeUriRef(
Uri.parse('package:drift/internal/manager.dart'), 'composeWithJoins')
..writeln('(')
..writeln("\$state: \$state,")
..writeln("referencedTable: $referencedTableField,")
..writeln("getCurrentColumn: (f) => f.$fieldGetter,")
..writeln("getReferencedColumn: (f) => f.$referencedColumnGetter,")
..writeln(
"getReferencedComposer: (db, table) => $referencedFilterComposer(db, table),")
..writeln("builder: f);")
..writeln("}");
}
}
@ -145,15 +150,15 @@ class _RegularOrderingWriter extends _OrderingWriter {
..writeDriftRef("ColumnOrderings")
..write(" get $orderingName =>")
..writeDriftRef("ColumnOrderings")
..write("(state.table.$fieldGetter);");
..write("(\$state.table.$fieldGetter);");
}
}
class _ReferencedOrderingWriter extends _OrderingWriter {
/// The full function used to get the referenced table
///
/// E.G `state.db.resultSet<$CategoryTable>('categories')`
/// or `state.db.categories`
/// E.G `\$state.db.resultSet<$CategoryTable>('categories')`
/// or `\$state.db.categories`
final String referencedTableField;
/// The getter for the column on the referenced table
@ -180,14 +185,18 @@ class _ReferencedOrderingWriter extends _OrderingWriter {
..write(" $orderingName(")
..writeDriftRef("ComposableOrdering")
..writeln(" Function( $referencedOrderingComposer o) o) {")
..writeln('''
return referenced(
referencedTable: $referencedTableField,
getCurrentColumn: (f) => f.$fieldGetter,
getReferencedColumn: (f) => f.$referencedColumnGetter,
getReferencedComposer: (db, table) => $referencedOrderingComposer(db, table),
builder: o);
}''');
..write("return ")
..writeUriRef(
Uri.parse('package:drift/internal/manager.dart'), 'composeWithJoins')
..writeln('(')
..writeln("\$state: \$state,")
..writeln("referencedTable: $referencedTableField,")
..writeln("getCurrentColumn: (f) => f.$fieldGetter,")
..writeln("getReferencedColumn: (f) => f.$referencedColumnGetter,")
..writeln(
"getReferencedComposer: (db, table) => $referencedOrderingComposer(db, table),")
..writeln("builder: o);")
..writeln("}");
}
}
@ -315,7 +324,7 @@ class _TableWriter {
..writeDriftRef('ProcessedTableManager')
..writeln(
'<$databaseGenericName,$tableClassName,$rowClassName,$filterComposer,$orderingComposer,$processedTableManager,$insertCompanionBuilderTypeDefName,$updateCompanionBuilderTypeDefName> {')
..writeln('const $processedTableManager(super.state);')
..writeln('const $processedTableManager(super.\$state);')
..writeln('}');
}
@ -478,8 +487,8 @@ class _TableWriter {
_TableWriter(referencedTable, scope, dbScope, databaseGenericName);
final referencedColumnNames = _ColumnWriter(referencedCol.nameInDart);
final String referencedTableField = scope.generationOptions.isModular
? "state.db.resultSet<${referencedTableNames.tableClassName}>('${referencedTable.schemaName}')"
: "state.db.${referencedTable.dbGetterName}";
? "\$state.db.resultSet<${referencedTableNames.tableClassName}>('${referencedTable.schemaName}')"
: "\$state.db.${referencedTable.dbGetterName}";
c.filters.add(_ReferencedFilter(c.fieldGetter,
fieldGetter: c.fieldGetter,
@ -506,8 +515,8 @@ class _TableWriter {
_TableWriter(ot, scope, dbScope, databaseGenericName);
final referencedColumnNames = _ColumnWriter(oc.nameInDart);
final String referencedTableField = scope.generationOptions.isModular
? "state.db.resultSet<${referencedTableNames.tableClassName}>('${ot.schemaName}')"
: "state.db.${ot.dbGetterName}";
? "\$state.db.resultSet<${referencedTableNames.tableClassName}>('${ot.schemaName}')"
: "\$state.db.${ot.dbGetterName}";
final filterName = oc.referenceName ??
"${referencedTableNames.table.dbGetterName}Refs";
@ -582,17 +591,30 @@ class ManagerWriter {
/// Write the manager to a provider [TextEmitter]
void write() {
final leaf = _scope.leaf();
final tableNames = <_TableWriter>[];
for (var table in _addedTables) {
tableNames.add(_TableWriter(table, _scope, _dbScope, databaseGenericName)
..addFiltersAndOrderings(_addedTables));
// When generating with modular generation, we need to add the imports
// for the internal `resultSet` helper
if (_scope.generationOptions.isModular) {
leaf.refUri(ModularAccessorWriter.modularSupport, '');
}
// Write the manager class for each table
final tableWriters = <_TableWriter>[];
for (var table in _addedTables) {
tableWriters.add(
_TableWriter(table, _scope, _dbScope, databaseGenericName)
..addFiltersAndOrderings(_addedTables));
}
// Write each tables manager to the leaf and append the getter to the main manager
final tableManagerGetters = StringBuffer();
for (var table in tableNames) {
for (var table in tableWriters) {
table.writeManager(leaf);
tableManagerGetters.writeln(
"${table.rootTableManager} get ${table.table.dbGetterName} => ${table.rootTableManager}(_db, _db.${table.table.dbGetterName});");
}
// Write the main manager class
leaf
..writeln('class $databaseManagerName{')
..writeln('final $_dbClassName _db;')