import { DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder } from 'kysely'; import Dream from '../Dream.js'; import { ColumnNamesAccountingForJoinedAssociations, InternalWhereStatement, LimitStatement, OffsetStatement, OrderQueryStatement, PassthroughOnClause, WhereStatement, WhereStatementForJoinedAssociation } from '../types/associations/shared.js'; import { DbConnectionType } from '../types/db.js'; import { AllDefaultScopeNames, DefaultScopeName, DreamColumnNames, DreamConstructorType, DreamSerializerKey, DreamTableSchema, OrderDir, PassthroughColumnNames, PluckEachArgs, PrimaryKeyForFind, RelaxedJoinAndStatement, RelaxedJoinStatement, RelaxedPreloadOnStatement, RelaxedPreloadStatement, TableColumnNames, TableOrAssociationName, UpdateableProperties, ValidatedPluckEachColumnNames } from '../types/dream.js'; import { CursorPaginatedDreamQueryOptions, CursorPaginatedDreamQueryResult, DefaultQueryTypeOptions, ExtendQueryType, LoadForModifierFn, NamespacedOrBaseModelColumnTypes, PaginatedDreamQueryOptions, PaginatedDreamQueryResult, QueryToKyselyDBType, QueryToKyselyTableNamesType } from '../types/query.js'; import { JoinedAssociation, JoinedAssociationsTypeFromAssociations, QueryTypeOptions, VariadicJoinsArgs, VariadicLeftJoinLoadArgs, VariadicLoadArgs } from '../types/variadic.js'; import DreamTransaction from './DreamTransaction.js'; import QueryDriverBase from './QueryDriver/Base.js'; /** * @internal * * Drops every memoized preload path. Only needed when a class object that stays in the process has * its `serializers` getter replaced, which the cache cannot detect — specs that install or remove * one around an example. Replacing the model classes themselves needs no call, since the cache * keys on the class object. See `clearResolvedSerializerAssociationEdgesCache`, which has the same * contract and is cleared alongside it. */ export declare function clearSerializerPreloadPathsCache(): void; export default class Query = DefaultQueryTypeOptions> { /** * @internal * * stores the default batch sizes for various * provided batching methods */ static readonly BATCH_SIZES: { FIND_EACH: number; LOCKED_DESTROY: number; LOCKED_UPDATE: number; PLUCK_EACH: number; PLUCK_EACH_THROUGH: number; }; /** * @internal * purely for typing */ queryTypeOpts: QueryTypeOpts; /** * @internal * * stores the dream transaction applied to the * current Query instance */ dreamTransaction: DreamTransaction | null; /** * @internal * * stores the passthrough on statements applied to the * current Query instance */ private readonly passthroughOnStatement; /** * @internal * * stores the where statements applied to the * current Query instance */ private readonly whereStatements; /** * @internal * * stores the where not statements applied to the * current Query instance */ private readonly whereNotStatements; /** * @internal * * stores the limit statements applied to the * current Query instance */ private readonly limitStatement; /** * @internal * * stores the offset statements applied to the * current Query instance */ private readonly offsetStatement; /** * @internal * * stores the or statements applied to the * current Query instance */ private readonly whereAnyStatements; /** * @internal * * stores the order statements applied to the * current Query instance */ private readonly orderStatements; /** * @internal * * whether or not to turn joins into load statements */ private readonly joinLoadActivated; /** * @internal * * stores the preload statements applied to the * current Query instance */ private readonly preloadStatements; /** * @internal * * stores the preload on statements applied to the * current Query instance */ private readonly preloadOnStatements; /** * @internal * * stores the joins statements applied to the * current Query instance */ private readonly innerJoinStatements; /** * @internal * * stores the joins on statements applied to the * current Query instance */ private readonly innerJoinAndStatements; /** * @internal * * stores the joins statements applied to the * current Query instance */ private readonly leftJoinStatements; /** * @internal * * stores the joins on statements applied to the * current Query instance */ private readonly leftJoinAndStatements; /** * @internal * * Whether or not to bypass all user-removable default scopes for this Query */ private readonly bypassAllDefaultScopes; /** * @internal * * Whether or not to bypass all user-removable default scopes for this Query, but not associations */ private readonly bypassAllDefaultScopesExceptOnAssociations; /** * @internal * * Specific default scopes to bypass */ private readonly defaultScopesToBypass; /** * @internal * * Specific default scopes to bypass, but not associations */ private readonly defaultScopesToBypassExceptOnAssociations; /** * @internal * * Whether or not to bypass SoftDelete and really destroy a record * when calling destroy. */ private readonly shouldReallyDestroy; /** * @internal * * The distinct column to apply to the Query */ private readonly distinctColumn; /** * @internal * * Whether the Query is ordered by its own order statements alone. The * driver otherwise sorts by a similarity condition's rank, and by the * declared `order` of the associations the Query is built on or joins, * ahead of those statements. The keyset walks and cursor pagination set * this, since their cursors compare only the Query's order statements. */ private readonly bypassImplicitOrder; /** * @internal * * The base sql alias to use for the base model * of this Query */ private baseSqlAlias; private get tableName(); private get namespacedPrimaryKey(); /** * @internal * * Used for unscoping Query instances. In most cases, this will be null, * but when calling `removeAllDefaultScopes`, a removeAllDefaultScopes Query is stored as * baseSelectQuery. */ private baseSelectQuery; private originalOpts; protected readonly dreamClass: DreamConstructorType; readonly dreamInstance: DreamInstance; protected connectionOverride: DbConnectionType | undefined; /** * @internal * * stores the Dream models joined in this Query instance */ protected readonly innerJoinDreamClasses: readonly (typeof Dream)[]; constructor(dreamInstance: DreamInstance, opts?: QueryOpts>); /** * Returns true. Useful for distinguishing Query instances * from other objects. * * @returns true */ get isDreamQuery(): boolean; /** * @internal * * Returns a cloned version of the Query * * ```ts * const clonedQuery = User.query().clone() * ``` * * @param opts - Statements to override when cloning the Query * @returns A cloned Query with the provided overrides clause applied */ clone>(this: Q, opts?: QueryOpts): Q; /** * Finds a record matching the Query with the * specified primary key. Returns null if not found. * * ```ts * await User.query().find(123) * // User{id: 123} * * await User.query().find(null) * // null * * await User.query().find(undefined) * // null * ``` * * @param primaryKey - The primary key of the record to look up. * @returns Either the found record, or else null */ find(primaryKey: PrimaryKeyForFind): Promise; /** * Finds a record matching the Query with the * specified primary key. If not found, an exception * is raised. * * ```ts * await User.query().findOrFail(123) * // User{id: 123} * ``` * * @param primaryKey - The primary key of the record to look up * @returns The found record */ findOrFail(primaryKey: PrimaryKeyForFind): Promise; /** * Finds a record matching the Query and the * specified where statement. If not found, null * is returned. * * ```ts * await User.query().findBy({ email: 'how@yadoin' }) * // User{email: 'how@yadoin'} * ``` * * @param whereStatement - The where statement used to locate the record * @returns Either the first record found matching the attributes, or else null */ findBy(whereStatement: WhereStatement): Promise; /** * Finds a record matching the Query and the * specified where statement. If not found, an exception * is raised. * * ```ts * await User.query().findOrFailBy({ email: 'how@yadoin' }) * // User{email: 'how@yadoin'} * ``` * * @param whereStatement - The where statement used to locate the record * @returns The first record found matching the attributes */ findOrFailBy(whereStatement: WhereStatement): Promise; /** * Finds all records matching the Query in batches, * and then calls the provided callback for each found record. * Once all records have been passed for a given batch, the next set of * records will be fetched and passed to your callback, until all * records matching the Query have been fetched. * * ```ts * await User.where({ deletedAt: null }).findEach(user => { * DreamApp.log(user) * }) * // User{id: 1} * // User{id: 2} * ``` * * IMPORTANT: `findEach` always iterates in ascending primary key order. * Any `order` applied to the Query is discarded; passing one has no effect * on the order records are visited in. The same goes for the `order` an * association declares, when the Query is an `associationQuery` or joins * that association, and for the relevance ranking of a similarity condition * (`ops.similarity`, `ops.wordSimilarity`, `ops.strictWordSimilarity`), * which still decides which records are visited, but not in what order. * `findEach` visits the records {@link Query.all} returns: a Query that * joins a HasOne or HasMany association (directly, through an association * it joins, or as the association an `associationQuery` goes through) * returns a record once for each row the join matches, and `findEach` * visits it once for each of those rows, the rows of one record in * ascending order of the joined rows' primary keys. Call `distinct()` to * visit each record once. * * This is not a limitation that can be lifted. `findEach` guarantees that * every matching row is visited exactly once, and it delivers that by * paginating on a keyset: each batch selects the next `batchSize` rows * whose primary keys (the record's, then the joined rows') sort after the * last row of the previous batch. That guarantee only holds over a stable, * unique key. Ordering by a mutable, non-unique column would mean a record * whose sort value changed partway through the iteration could be skipped * entirely or visited twice. ({@link Query.cursorPaginate} can honor an * arbitrary order precisely because a paginated UI tolerates that anomaly; * a full traversal cannot.) * * A Query that keeps one record of each group (one that calls `distinct`, * or an `associationQuery` of an association that declares `distinct`, or * a Query that joins one) cannot be paginated that way: its own `order`, or * the association's, chooses which record of each group it returns, and a * batch that narrowed it to the rows after the previous batch would let it * choose another. So `findEach` reads the keys of all of its rows before * visiting the first, and visits those whose rows still match when their * batch is read. It visits the rows the Query returned when the walk began, * less any destroyed or changed so that they no longer match the Query's * conditions, and not a row the Query returns only after the walk began, * such as the next record of a group whose record the callback destroyed. * * If you need records processed in a particular order, load them with a * regular ordered query instead of `findEach`. * * A `limit` or `offset` on the Query cannot compose with the batches — * each batch re-applies the Query's conditions, so the limit would be * silently replaced by `batchSize` and the offset re-applied to every * batch — so a limit- or offset-carrying Query throws at runtime. * * @param cb - The callback to call for each found record * @param __namedParameters - Options for batch processing * @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000 * @returns void * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset` */ findEach(this: Query, cb: (instance: DreamInstance) => void | Promise, { batchSize }?: { batchSize?: number; }): Promise; /** * @internal * * `findEach`, visiting each record once rather than once for each row a * HasOne or HasMany join returns it in, for the operations that act on each * record it visits (`destroy`, `reallyDestroy`, `undestroy` and `update` * without `lock: true`). */ private findEachRecordOnce; /** * @internal * * Calls `cb` with the record of each row this Query returns, telling its * rows apart by `rowKeyColumns` (see `walkRowBatches`). */ private findEachRow; /** * @internal * * The columns that tell apart the rows this Query returns: its primary key, * then the primary keys of the rows HasOne and HasMany joins add (see * `rowKeyColumnsOfJoinedRows`). */ private rowKeyColumns; /** * @internal * * A HasOne or HasMany join returns a record once for each row it matches, * and the primary keys of the joined rows, with the Query's own, tell those * rows apart. These are those primary keys, namespaced, other than the * Query's own. A Query that calls `distinct` returns one row for each value * of its distinct column rather than one for each row the join matches, so * it has none. */ private rowKeyColumnsOfJoinedRows; /** * @internal * * Walks the rows this Query returns in batches, for `findEach` and * `pluckEach`, and calls `visit` with what `readBatch` reads for each row, * in ascending order of `rowKeyColumns`, the columns that tell its rows * apart. `readBatch` reads a Query that returns a batch's rows, and returns * each row's values of `rowKeyColumns` with what to visit for it, if * anything. A row whose keys appear again in the same batch is visited once. * * A select that keeps one row of each group (DISTINCT ON) is walked from the * keys of all of its rows, read before the first batch (see * `walkRowBatchesFromKeys`); any other, by a cursor (see * `walkRowBatchesByCursor`). */ private walkRowBatches; /** * @internal * * Walks the rows this Query returns in batches of the next `batchSize` rows * after the last row of the batch before, compared on `rowKeyColumns` by a * where clause, which every driver applies (see `walkRowBatches`). */ private walkRowBatchesByCursor; /** * @internal * * Walks a select that keeps one row of each group (DISTINCT ON), whose rows' * values of `rowKeyColumns` are `distinctOnRowKeys`, read before the first * batch (see `walkRowBatches`). Such a select cannot be walked by a cursor: * narrowing it to the rows after the cursor before it chooses each group's * row would let it choose a row it does not return. So each batch reads the * rows of the next of those keys that still match, by their keys alone and * unordered, since each group's row is chosen already, and puts them back * in the order of those keys. The walk visits the rows the select returned * when it began, less any destroyed or changed so that they no longer match; * a row the select returns only after the walk began is not visited. */ private walkRowBatchesFromKeys; /** * Load each specified association using a single SQL query. * See {@link Query.preload} for preloading in separate queries. * * Note: since leftJoinPreload loads via single query, it has * some downsides and that may be avoided using {@link Query.preload}: * 1. `limit` and `offset` will be automatically removed * 2. `through` associations will bring additional namespaces into the query that can conflict with through associations from other associations, creating an invalid query * 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinPreload('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.preload('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once. * 4. the individual query becomes more complex the more associations are included * 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once * 6. unlike base-model reads, `preload`/`load`, and saves — which tolerate * schema/image skew (e.g. a rolling deploy dropping a column while * containers compiled against the previous schema are still draining) — * leftJoinPreload must enumerate every compiled column of every joined * model under per-alias names (per-alias `*` is not expressible in a * single flat row), so an **unplanned** column drop breaks * leftJoinPreload queries for the duration of the rollout window. A * **planned** drop is safe when performed via the two-deploy process * documented on the `ignoredColumns` getter of Dream: declaring the * column ignored removes it from the generated schema, so leftJoinPreload * stops naming it a full deploy before the column is actually dropped. * * * ```ts * const posts = await user.associationQuery('posts').leftJoinPreload('comments', { and: { visibility: 'public' } }, 'replies').all() * console.log(posts[0].comments[0].replies[0]) * // [Reply{id: 1}, Reply{id: 2}] * ``` * * @param args - A chain of association names and and/andNot/andAny clauses * @returns A cloned Query with the joinLoad statement applied */ leftJoinPreload, DB extends DreamInstance['DB'], Schema extends DreamInstance['schema'], TableName extends DreamInstance['table'], const Arr extends readonly unknown[], const LastArg extends VariadicLeftJoinLoadArgs, Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowLeftJoinPreload: false; }> ? true : false, const JoinedAssociationsCandidate = JoinedAssociationsTypeFromAssociations, const JoinedAssociations extends readonly JoinedAssociation[] = JoinedAssociationsCandidate extends readonly JoinedAssociation[] ? JoinedAssociationsCandidate : never, RetQuery = Query>>>(this: Q, ...args: Incompatible extends true ? 'leftJoinPreload is incompatible with preload, limit, offset, and paginate'[] : [...Arr, LastArg]): RetQuery; /** * Load each specified association using a separate SQL query. * See {@link Query.leftJoinPreload} for preloading in a single query. * * ```ts * const user = await User.query().preload('posts', 'comments', { and: { visibility: 'public' } }, 'replies').first() * console.log(user.posts[0].comments[0].replies[0]) * // [Reply{id: 1}, Reply{id: 2}] * ``` * * @param args - A chain of association names and and/andNot/andAny clauses * @returns A cloned Query with the preload statement applied */ preload, DB extends DreamInstance['DB'], Schema extends DreamInstance['schema'], TableName extends DreamInstance['table'], const Arr extends readonly unknown[], Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowPreload: false; }> ? true : false, RetQuery = Query>>>(this: Q, ...args: Incompatible extends true ? 'preload is incompatible with leftJoinPreload'[] : [...Arr, VariadicLoadArgs]): RetQuery; /** * Recursively preloads all Dream associations referenced by `rendersOne` and `rendersMany` * in a DreamSerializer. This traverses the entire content tree of serializers to automatically * load all necessary associations, eliminating N+1 query problems and removing the need to * manually remember which associations to preload for serialization. * * This method decouples data loading code from data rendering code by having the serializer * (rendering code) inform the query (loading code) about which associations are needed. * As serializers evolve over time - adding new `rendersOne` and `rendersMany` calls or * modifying existing ones - the loading code automatically adapts without requiring * corresponding modifications to preload statements. * * This method analyzes the serializer (specified by `serializerKey` or 'default') and * automatically preloads all associations that will be needed during serialization. * * ```ts * // Instead of manually specifying all associations: * await User.preload('posts', 'comments', 'replies').all() * * // Automatically preload everything needed for serialization: * await User.preloadFor('summary').all() * * // Add where conditions to specific associations during preloading: * await User.preloadFor('detailed', (associationName, dreamClass) => { * if (dreamClass.typeof(Post) && associationName === 'comments') { * return { and: { published: true } } * } * }) * .all() * * // Skip preloading specific associations to handle them manually: * await User.preloadFor('summary', (associationName, dreamClass) => { * if (dreamClass.typeof(User) && associationName === 'posts') { * return 'omit' // Handle posts preloading separately with custom logic * } * }) * .preload('posts', { and: { featured: true } }) // Custom preloading * .all() * ``` * * @param serializerKey - The serializer key to use for determining which associations to preload. * @param modifierFn - Optional callback function to modify or omit specific associations during preloading. Called for each association with the Dream class and association name. Return an object with `and`, `andAny`, or `andNot` properties to add where conditions, return 'omit' to skip preloading that association (useful when you want to handle it manually), or return undefined to use default preloading * @returns A Query with all serialization associations preloaded */ preloadFor, SerializerKey extends DreamSerializerKey, RetQuery = Query>>>(this: Q, serializerKey: SerializerKey, modifierFn?: LoadForModifierFn): RetQuery; /** * Returns a new Query instance, with the provided * joins statement attached * * ```ts * await User.query().innerJoin('posts').first() * ``` * * @param args - A chain of association names and and/andNot/andAny clauses * @returns A cloned Query with the joins clause applied */ innerJoin, const JoinedAssociationsCandidate = JoinedAssociationsTypeFromAssociations, const JoinedAssociations extends readonly JoinedAssociation[] = JoinedAssociationsCandidate extends readonly JoinedAssociation[] ? JoinedAssociationsCandidate : never, RetQuery = Query>>>(...args: [...Arr, LastArg]): RetQuery; /** * @internal * * @param args - A chain of association names and and/andNot/andAny clauses * @returns A cloned Query with the joins clause applied */ leftJoin, const JoinedAssociationsCandidate = JoinedAssociationsTypeFromAssociations, const JoinedAssociations extends readonly JoinedAssociation[] = JoinedAssociationsCandidate extends readonly JoinedAssociation[] ? JoinedAssociationsCandidate : never, RetQuery = Query>>>(...args: [...Arr, LastArg]): RetQuery; /** * @internal * */ private fleshOutJoinStatements; /** * @internal * * Adds Dream class to temporary innerJoinDreamClasses array based on association name * */ private addAssociatedDreamClassToInnerJoinDreamClasses; private addThroughAssociatedDreamClassToInnerJoinDreamClasses; /** * @internal * * Changes the base sql alias * */ private setBaseSQLAlias; /** * @internal * * Association queries start from the table corresponding to an instance * of a Dream and join the association. However, the Dream may have * default scopes that would preclude finding that instance, so the * Query that forms the base of an association query must be unscoped, * but that unscoping should not carry through to other associations * (thus the use of `removeAllDefaultScopesExceptOnAssociations` instead of * `removeAllDefaultScopes`). * * The association that this query is loading leverages `joins`, and the * joining code explicitly handles applying / omitting default scopes. * We set `bypassAllDefaultScopesExceptOnAssociations: true` on this Query * to let that code do its work. This keeps the implementation DRY and simpler * (without `bypassAllDefaultScopesExceptOnAssociations`, the default scopes would * be applied twice, and when the association attempts to remove a default * association would be foiled because it would be applied outside of the context * where the association is modifying the query). * */ private setAssociationQueryBase; /** * @deprecated Use {@link removeDefaultScope} instead. This method will become private in v3. * * Prevents user-removable default scopes from applying when the Query is * executed, including when associations are loaded. An STI child's reserved * type discriminator remains enforced; query the STI base model to span its * registered children. * * @returns A new Query which bypasses user-removable default scopes */ removeAllDefaultScopes(): Query; /** * Prevents default scopes from applying when * the Query is executed, but not when applying to associations * * @returns A new Query which will prevent default scopes from applying, but not when applying to asociations */ protected removeAllDefaultScopesExceptOnAssociations(): Query; /** * Prevents a specific default scope from applying when * the Query is executed * * @returns A new Query which will prevent a specific default scope from applying */ removeDefaultScope(scopeName: AllDefaultScopeNames): Query; /** * Prevents a specific default scope from applying when * the Query is executed, but not when applying to asociations * * @returns A new Query which will prevent a specific default scope from applying, but not when applying to asociations */ protected removeDefaultScopeExceptOnAssociations(scopeName: DefaultScopeName): Query; /** * Sends data through for use as passthrough data * for the associations that require it. * * ```ts * class Post { * @deco.HasMany('LocalizedText') * public localizedTexts: LocalizedText[] * * @deco.HasOne('LocalizedText', { * and: { locale: DreamConst.passthrough }, * }) * public currentLocalizedText: LocalizedText * } * * await User.query().passthrough({ locale: 'es-ES' }) * .preload('posts', 'currentLocalizedText') * .first() * ``` * * @param passthroughOnStatement - where statement used for associations that require passthrough data * @returns A cloned Query with the passthrough data */ passthrough(passthroughOnStatement: PassthroughOnClause>): this; /** * Applies a where statement to the Query instance * * ```ts * await User.where({ email: 'how@yadoin' }).first() * // User{email: 'how@yadoin'} * ``` * * @param whereStatement - Where statement to apply to the Query * @returns A cloned Query with the where clause applied */ where(whereStatement: WhereStatementForJoinedAssociation | null): Query; /** * Accepts a list of where statements, each of * which is combined via `OR` * * ```ts * await User.query().whereAny([{ email: 'how@yadoin' }, { name: 'fred' }]).first() * // [User{email: 'how@yadoin'}, User{name: 'fred'}, User{name: 'fred'}] * ``` * * @param whereStatements - a list of where statements to `OR` together * @returns A cloned Query with the whereAny clause applied */ whereAny(whereStatements: WhereStatementForJoinedAssociation[] | null): Query; /** * Applies a whereNot statement to the Query instance * * ```ts * await User.query().whereNot({ email: 'how@yadoin' }).first() * // User{email: 'hello@world'} * ``` * * @param whereStatement - A where statement to negate and apply to the Query * @returns A cloned Query with the whereNot clause applied */ whereNot(whereStatement: WhereStatementForJoinedAssociation | null): Query; /** * @internal * * Applies a where clause */ private _where; /** * Returns a new Kysely SelectQueryBuilder instance to be used * in a sub Query * * ```ts * const records = await User.where({ * id: Post.query().nestedSelect('userId'), * }).all() * // [User{id: 1}, ...] * ``` * * @param selection - the column to use for your nested Query * @returns A Kysely SelectQueryBuilder instance */ nestedSelect, PluckThroughFieldType>(this: Query, selection: SimpleFieldType | PluckThroughFieldType): SelectQueryBuilder; /** * Returns a new Query instance, attaching the provided * order statement * * ```ts * await User.query().order('id').all() * // [User{id: 1}, User{id: 2}, ...] * ``` * * ```ts * await User.query().order({ name: 'asc', id: 'desc' }).all() * // [User{name: 'a', id: 99}, User{name: 'a', id: 97}, User{ name: 'b', id: 98 } ...] * ``` * * @param arg - Either a string or an object specifying order. If a string, the order is implicitly ascending. If the orderStatement is an object, statements will be provided in the order of the keys set in the object * @returns A cloned Query with the order clause applied */ order, DB extends DreamInstance['DB'], ColumnName extends ColumnNamesAccountingForJoinedAssociations>(arg: ColumnName | Partial> | null): this; /** * Returns a new Query instance, specifying a limit * * ```ts * await User.order('id').limit(2).all() * // [User{id: 1}, User{id: 2}] * ``` * * A limit is incompatible with `paginate` and `leftJoinPreload`, and with * the batched and set-write operations — `findEach`, `pluckEach`, * `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can * honor one: the batched and set-write operations throw * `BatchingIncompatibleWithLimitOrOffset` on a limit-carrying Query. * * @returns A cloned Query with the limit clause applied */ limit, Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowLimit: false; }> ? true : false, RetQuery = Query>>>(this: Q, limit: Incompatible extends true ? 'limit is incompatible with paginate and leftJoinPreload'[] : number | null): RetQuery; /** * Returns a new Query instance, specifying an offset * * ```ts * await User.order('id').offset(2).limit(2).all() * // [User{id: 3}, User{id: 4}] * ``` * * An offset is incompatible with `paginate` and `leftJoinPreload`, and with * the batched and set-write operations — `findEach`, `pluckEach`, * `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can * honor one: the batched and set-write operations throw * `BatchingIncompatibleWithLimitOrOffset` on an offset-carrying Query. * * @returns A cloned Query with the offset clause applied */ offset, Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowOffset: false; }> ? true : false, RetQuery = Query>>>(this: Q, offset: Incompatible extends true ? 'offset is incompatible with paginate and leftJoinPreload'[] : number | null): RetQuery; /** * Returns the sql that would be executed by this Query * * ```ts * User.where({ email: 'how@yadoin' }).sql() * // { * // query: { * // kind: 'SelectQueryNode', * // from: { kind: 'FromNode', froms: [Array] }, * // selections: [ [Object] ], * // distinctOn: undefined, * // joins: undefined, * // groupBy: undefined, * // orderBy: undefined, * // where: { kind: 'WhereNode', where: [Object] }, * // frontModifiers: undefined, * // endModifiers: undefined, * // limit: undefined, * // offset: undefined, * // with: undefined, * // having: undefined, * // explain: undefined, * // setOperations: undefined * // }, * // sql: 'select "users".* from "users" where ("users"."email" = $1 and "users"."deleted_at" is null)', * // parameters: [ 'how@yadoin' ] * //} * ``` * * @returns An object representing the underlying sql statement * */ sql(): import("kysely").CompiledQuery; /** * Converts the given dream class into a Kysely query, enabling * you to build custom queries using the Kysely API * * ```ts * await User.query().toKysely('select').where('email', '=', 'how@yadoin').execute() * ``` * * @param type - the type of Kysely query builder instance you would like to obtain * @returns A Kysely query. Depending on the type passed, it will return either a SelectQueryBuilder, DeleteQueryBuilder, or an UpdateQueryBuilder */ toKysely, TableNames = QueryToKyselyTableNamesType, ToKyselyReturnType = QueryType extends 'select' ? SelectQueryBuilder : QueryType extends 'delete' ? DeleteQueryBuilder : QueryType extends 'update' ? UpdateQueryBuilder : never>(type: QueryType): ToKyselyReturnType; /** * @internal * * Prevents mutation paths that discard an association query's ownership constraint. */ private assertMutationPreservesAssociationQuery; /** * Applies transaction to the Query instance * * ```ts * await ApplicationModel.transaction(async txn => { * await User.query().txn(txn).create({ email: 'how@yadoin' }) * }) * ``` * * @param dreamTransaction - A DreamTransaction instance (usually collected by calling `ApplicationModel.transaction`) * @returns A cloned Query with the transaction applied * */ txn(dreamTransaction: DreamTransaction | null): this; /** * Retrieves the number of records matching the Query * * ```ts * await User.query().count() * // 42 * * await User.where({ email: ops.ilike('%gmail.com') }).count() * // 15 * ``` * * @returns The number of records matching the Query */ count(): Promise; /** * Retrieves the number of records in each group, keyed by the * value of the provided group column (a SQL `GROUP BY`). * * ```ts * await User.query().countBy('name') * // Map(2) { 'fred' => 2, 'zed' => 1 } * * await User.where({ email: ops.ilike('%gmail.com') }).countBy('name') * // Map(1) { 'fred' => 3 } * ``` * * Any `where` clauses, base scopes (soft-delete / STI), and joined-association * `and` clauses on the Query carry through automatically. A joined-association * column may be grouped on by passing its namespaced name (e.g. * `'compositionAssets.name'`). * * Only groups with at least one matching row appear in the Map (inherent to * `GROUP BY`); seed absent groups yourself with `map.get(key) ?? 0`. When the * group column is nullable, records with a `null` value are grouped under a real * `null` key. * * ```ts * await Composition.query().innerJoin('compositionAssets').countBy('compositionAssets.name') * // Map(2) { 'primary' => 3, null => 1 } * ``` * * @param groupColumn - the column to group by (base or joined-association-namespaced) * @returns A Map from each present group value to the number of records in that group */ countBy, DB extends DreamInstance['DB'], GroupColumnName extends ColumnNamesAccountingForJoinedAssociations, GroupKey extends NamespacedOrBaseModelColumnTypes<[GroupColumnName], Q, DreamInstance>[0]>(groupColumn: GroupColumnName): Promise>; /** * Returns new Query with distinct clause applied. * If no column is specified, applies distinct to the primary key. * Pass true to apply distinct to primary key, false to disable distinct. * * ```ts * await User.query().distinct('name').pluck('name') * // Returns unique names * * await User.query().distinct(true).pluck('id') * // Returns unique primary keys * * await User.query().distinct(false).pluck('name') * // Disables distinct, returns all names including duplicates * ``` * * @param column - The column name to apply distinct to, `true` to enable distinct on primary key, or `false` to disable a previously applied distinct call * @returns A cloned Query with the distinct clause applied */ distinct(column?: TableColumnNames | boolean): this; /** * @internal * * Returns a namespaced column name * * @returns A string */ private namespaceColumn; /** * Retrieves the max value of the specified column * for this Query * * ```ts * await User.query().max('id') * // 99 * ``` * * @param columnName - a column name on the model * @returns the max value of the specified column for this Query * */ max, DB extends DreamInstance['DB'], ColumnName extends ColumnNamesAccountingForJoinedAssociations, ReturnType extends NamespacedOrBaseModelColumnTypes<[ColumnName], Q, DreamInstance>[0]>(columnName: ColumnName): Promise; /** * Retrieves the max value of the specified column within each group, keyed by * the value of the provided group column (a SQL `GROUP BY`). * * ```ts * await CompositionAsset.query().maxBy('name', 'score') * // Map(2) { 'primary' => 9, 'secondary' => 4 } * ``` * * Any `where` clauses, base scopes (soft-delete / STI), and joined-association * `and` clauses on the Query carry through automatically. A joined-association * column may be grouped on or aggregated by passing its namespaced name (e.g. * `'compositionAssets.name'`). * * Only groups with at least one matching row appear in the Map (inherent to * `GROUP BY`). When the group column is nullable, records with a `null` value * are grouped under a real `null` key; a group whose aggregated values are all * `null` yields a `null` value. * * @param groupColumn - the column to group by (base or joined-association-namespaced) * @param aggregatedColumn - the column to take the max of within each group * @returns A Map from each present group value to the max of the aggregated column in that group */ maxBy, DB extends DreamInstance['DB'], GroupColumnName extends ColumnNamesAccountingForJoinedAssociations, AggregatedColumnName extends ColumnNamesAccountingForJoinedAssociations, GroupKey extends NamespacedOrBaseModelColumnTypes<[GroupColumnName], Q, DreamInstance>[0], AggregatedType extends NamespacedOrBaseModelColumnTypes<[AggregatedColumnName], Q, DreamInstance>[0]>(groupColumn: GroupColumnName, aggregatedColumn: AggregatedColumnName): Promise>; /** * Retrieves the min value of the specified column * for this Query * * ```ts * await User.query().min('id') * // 1 * ``` * * @param columnName - a column name on the model * @returns the min value of the specified column for this Query */ min, DB extends DreamInstance['DB'], ColumnName extends ColumnNamesAccountingForJoinedAssociations, ReturnType extends NamespacedOrBaseModelColumnTypes<[ColumnName], Q, DreamInstance>[0]>(columnName: ColumnName): Promise; /** * Retrieves the min value of the specified column within each group, keyed by * the value of the provided group column (a SQL `GROUP BY`). * * ```ts * await CompositionAsset.query().minBy('name', 'score') * // Map(2) { 'primary' => 1, 'secondary' => 4 } * ``` * * Any `where` clauses, base scopes (soft-delete / STI), and joined-association * `and` clauses on the Query carry through automatically. A joined-association * column may be grouped on or aggregated by passing its namespaced name (e.g. * `'compositionAssets.name'`). * * Only groups with at least one matching row appear in the Map (inherent to * `GROUP BY`). When the group column is nullable, records with a `null` value * are grouped under a real `null` key; a group whose aggregated values are all * `null` yields a `null` value. * * @param groupColumn - the column to group by (base or joined-association-namespaced) * @param aggregatedColumn - the column to take the min of within each group * @returns A Map from each present group value to the min of the aggregated column in that group */ minBy, DB extends DreamInstance['DB'], GroupColumnName extends ColumnNamesAccountingForJoinedAssociations, AggregatedColumnName extends ColumnNamesAccountingForJoinedAssociations, GroupKey extends NamespacedOrBaseModelColumnTypes<[GroupColumnName], Q, DreamInstance>[0], AggregatedType extends NamespacedOrBaseModelColumnTypes<[AggregatedColumnName], Q, DreamInstance>[0]>(groupColumn: GroupColumnName, aggregatedColumn: AggregatedColumnName): Promise>; /** * Retrieves the sum value of the specified column * for this Query * * ```ts * await Game.query().sum('score') * // 1 * ``` * * @param columnName - a column name on the model * @returns the sum of the values of the specified column for this Query */ sum, DB extends DreamInstance['DB'], ColumnName extends ColumnNamesAccountingForJoinedAssociations, ReturnType extends NamespacedOrBaseModelColumnTypes<[ColumnName], Q, DreamInstance>[0]>(columnName: ColumnName): Promise; /** * Retrieves the sum of the specified column within each group, keyed by * the value of the provided group column (a SQL `GROUP BY`). * * ```ts * await CompositionAsset.query().sumBy('name', 'score') * // Map(2) { 'primary' => 10, 'secondary' => 4 } * ``` * * Any `where` clauses, base scopes (soft-delete / STI), and joined-association * `and` clauses on the Query carry through automatically. A joined-association * column may be grouped on or aggregated by passing its namespaced name (e.g. * `'compositionAssets.name'`). * * Only groups with at least one matching row appear in the Map (inherent to * `GROUP BY`). When the group column is nullable, records with a `null` value * are grouped under a real `null` key; a group whose aggregated values are all * `null` yields a `null` value. * * @param groupColumn - the column to group by (base or joined-association-namespaced) * @param aggregatedColumn - the column to sum within each group * @returns A Map from each present group value to the sum of the aggregated column in that group */ sumBy, DB extends DreamInstance['DB'], GroupColumnName extends ColumnNamesAccountingForJoinedAssociations, AggregatedColumnName extends ColumnNamesAccountingForJoinedAssociations, GroupKey extends NamespacedOrBaseModelColumnTypes<[GroupColumnName], Q, DreamInstance>[0], AggregatedType extends NamespacedOrBaseModelColumnTypes<[AggregatedColumnName], Q, DreamInstance>[0]>(groupColumn: GroupColumnName, aggregatedColumn: AggregatedColumnName): Promise>; /** * Retrieves the average value of the specified column * for this Query * * ```ts * await Game.query().avg('score') * // 1 * ``` * * @param columnName - a column name on the model * @returns the average of the values of the specified column for this Query */ avg, DB extends DreamInstance['DB'], ColumnName extends ColumnNamesAccountingForJoinedAssociations, ReturnType extends NamespacedOrBaseModelColumnTypes<[ColumnName], Q, DreamInstance>[0]>(columnName: ColumnName): Promise; /** * Retrieves the average of the specified column within each group, keyed by * the value of the provided group column (a SQL `GROUP BY`). * * ```ts * await CompositionAsset.query().avgBy('name', 'score') * // Map(2) { 'primary' => 5, 'secondary' => 4 } * ``` * * Any `where` clauses, base scopes (soft-delete / STI), and joined-association * `and` clauses on the Query carry through automatically. A joined-association * column may be grouped on or aggregated by passing its namespaced name (e.g. * `'compositionAssets.name'`). * * Only groups with at least one matching row appear in the Map (inherent to * `GROUP BY`). When the group column is nullable, records with a `null` value * are grouped under a real `null` key; a group whose aggregated values are all * `null` yields a `null` value. * * @param groupColumn - the column to group by (base or joined-association-namespaced) * @param aggregatedColumn - the column to average within each group * @returns A Map from each present group value to the average of the aggregated column in that group */ avgBy, DB extends DreamInstance['DB'], GroupColumnName extends ColumnNamesAccountingForJoinedAssociations, AggregatedColumnName extends ColumnNamesAccountingForJoinedAssociations, GroupKey extends NamespacedOrBaseModelColumnTypes<[GroupColumnName], Q, DreamInstance>[0], AggregatedType extends NamespacedOrBaseModelColumnTypes<[AggregatedColumnName], Q, DreamInstance>[0]>(groupColumn: GroupColumnName, aggregatedColumn: AggregatedColumnName): Promise>; /** * Plucks the provided fields from the given dream class table * * ```ts * await User.order('id').pluck('id') * // [1, 2, 3] * ``` * * If more than one column is requested, a multi-dimensional * array is returned: * * ```ts * await User.order('id').pluck('id', 'email') * // [[1, 'a@a.com'], [2, 'b@b.com']] * ``` * * @param columnNames - The column or array of columns to pluck * @returns An array of pluck results */ pluck, DB extends DreamInstance['DB'], const ColumnNames extends ColumnNamesAccountingForJoinedAssociations[], ReturnValue extends ColumnNames['length'] extends 1 ? NamespacedOrBaseModelColumnTypes[0][] : NamespacedOrBaseModelColumnTypes[]>(this: Q, ...columnNames: ColumnNames): Promise; /** * Plucks the specified column names from the given dream class table * in batches, passing each found columns into the * provided callback function * * ```ts * await User.order('id').pluckEach('id', (id) => { * console.log(id) * }) * // 1 * // 2 * // 3 * ``` * * `pluckEach` reads the rows {@link Query.findEach} visits, in the same * batches and the same order: ascending primary key, then, for a Query * that joins a HasOne or HasMany association, the joined rows' primary * keys, so each row the join returns is plucked once. Like `findEach`, it * discards the Query's `order`, an association's declared `order` and the * relevance ranking of a similarity condition, and it reads a Query that * keeps one record of each group (one that calls `distinct`, or over an * association that declares `distinct`) from the keys of all of its rows, * read before the first batch. * * A `limit` or `offset` on the Query cannot compose with the batches — * each batch re-applies the Query's conditions, so the limit would be * silently replaced by the batch size and the offset re-applied to every * batch — so a limit- or offset-carrying Query throws at runtime. * * @param args - a list of column names to pluck, followed by a callback function to call for each set of found fields * @returns void * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset` */ pluckEach, const ColumnNames extends unknown[]>(this: Q, ...args: PluckEachArgs>, NamespacedOrBaseModelColumnTypes>, Q, DreamInstance>>): Promise; pluckEach, const ColumnNames extends ColumnNamesAccountingForJoinedAssociations[]>(this: Q, ...args: PluckEachArgs>): Promise; /** * Retrieves an array containing all records matching the Query. * Be careful using this, since it will attempt to pull every * record into memory at once. When querying might return a large * number of records, consider using `.findEach`, which will pull * the records in batches. * * ```ts * await User.query().all() * // [User{id: 1}, User{id: 2}, ...] (ordered by id) * * // With specific columns (always includes primary key) * await User.query().all({ columns: ['name'] }) * // Users will have both 'id' and 'name' properties * * // With custom ordering * await User.order('email').all() * // [User{email: 'a@a.com'}, User{email: 'b@b.com'}, ...] * ``` * * @param options - Query options * @param options.columns - Array of column names to select. The primary key is always included automatically. * @returns an array of dreams */ all(options?: { columns?: DreamColumnNames[]; }): Promise; static dbDriverClass(connectionName: string): typeof QueryDriverBase; dbDriverInstance(query?: Query): QueryDriverBase; get connectionName(): any; /** * @internal * * Resolves the effective page size for a pagination request: applies the * configured default when no page size is requested, and clamps the result to * the configured maximum (`paginationMaxPageSize`, default 200) so that a * forwarded, attacker-controlled `pageSize` cannot force a full-table load. */ private clampedPaginationPageSize; /** * Paginates the results of your query, accepting a pageSize and page argument, * which it uses to segment your query into pages, leveraging limit and offset * to deliver your query to you in pages. * * ```ts * const paginated = await User.order('email').paginate({ pageSize: 100, page: 2 }) * paginated.results * // [ { User{id: 101}, User{id: 102}, ...}] * * paginated.recordCount * // 350 * * paginated.pageCount * // 4 * * paginated.currentPage * // 2 * ``` * * Under a similarity condition (`ops.similarity`, `ops.wordSimilarity`, * `ops.strictWordSimilarity`), pages come best match first: records are * ordered by relevance ahead of the Query's `order` (or, without one, the * descending primary key). For best-match-first pages, use `paginate` * rather than {@link Query.cursorPaginate}, which does not order by * relevance. * * @param opts - Pagination options * @param opts.page - the page number that you want to fetch results for * @param opts.pageSize - the number of results per page (optional) * @returns results.recordCount - A number representing the total number of records matching your query * @returns results.pageCount - The number of pages needed to encapsulate all the matching records * @returns results.currentPage - The current page (same as what is provided in the paginate args) * @returns results.results - An array of records matching the current record */ paginate, Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowPaginate: false; }> ? true : false>(this: Q, opts: Incompatible extends true ? 'paginate is incompatible with limit, offset, and leftJoinPreload'[] : PaginatedDreamQueryOptions): Promise>; /** * @deprecated Use {@link cursorPaginate} instead. * * Paginates the results of your query using cursor-based pagination, * accepting a pageSize and cursor argument. This method * provides better performance for large datasets by using cursor-based * pagination instead of offset-based pagination. * * Pages follow the Query's `order`, preceded by the `order` declared on any * association the Query is built on or joins (so an `associationQuery` of * an ordered association pages in that association's order), with * ascending primary key as the final tiebreaker; with neither, pages run in * ascending primary key order. If those orders already include the primary * key (e.g.: `id: 'asc'`), the implicit primary key ordering is omitted. * * A Query that joins a HasOne or HasMany association (directly, through an * association it joins, or as the association an `associationQuery` goes * through) returns a record once for each row the join matches, as * {@link Query.all} does, and the pages return each of those rows once, * sorting the rows of one record by the joined rows' primary keys after the * primary key. The cursor of such a Query is an opaque string that names * its last row by primary keys, the record's and the joined rows' (readable * by whoever holds it; it is not encrypted), and the next page starts after * that row, or, if the row is gone, after the record's first row. Any other * Query's cursor is its last record's primary key. A record's primary key * is always accepted as a cursor: the next page starts after that record's * first row. A cursor carries no other value its row sorts by: those are * read from the database when the next page is. * * A Query that calls `distinct` on its primary key (as `distinct()` does, * e.g.: `User.innerJoin('posts').distinct()`) returns each record once, as * {@link Query.all} does, and so do its pages, whose cursor is the last * record's primary key. A Query that calls `distinct` on another column has * to be ordered by that column first (e.g.: * `Post.distinct('category').order({ category: 'asc', createdAt: 'desc' })`), * or the database rejects its pages. Its pages return the records * {@link Query.all} returns, the one its order keeps for each value of that * column (the newest post of each category), and its cursor is also the * last record's primary key. Over an association that declares `distinct` * and an `order` led by its distinct column (an `associationQuery` of it, or * a Query that joins it), the pages return the rows {@link Query.all} * returns: the row the association's `order` keeps for each value of its * distinct column. * * A similarity condition (`ops.similarity`, `ops.wordSimilarity`, * `ops.strictWordSimilarity`) still decides which records match, but its * relevance ranking does not order the pages: a cursor can only resume from * values it compares, and the rank is not one of them. For best-match-first * pages, use {@link Query.paginate}. * * ```ts * // First page (using undefined to start from beginning) * const firstPage = await User.order('email').scrollPaginate({ pageSize: 100, cursor: undefined }) * firstPage.results * // [ { User{id: 1}, User{id: 2}, ...}] * * firstPage.cursor * // "100" (or null if no more pages) * * // Next page using cursor from previous result * const nextPage = await User.order('email').scrollPaginate({ * pageSize: 100, * cursor: firstPage.cursor * }) * ``` * * @param opts - scroll pagination options * @param opts.pageSize - the number of results per page (optional) * @param opts.cursor - identifier of where to start the next page; undefined to start from the beginning; null when no more pages * @returns results.cursor - identifier for the next page, or null if no more pages * @returns results.results - An array of records for the current page */ scrollPaginate, Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowPaginate: false; }> ? true : false>(this: Q, opts: Incompatible extends true ? 'scrollPaginate is incompatible with limit, offset, and leftJoinPreload'[] : CursorPaginatedDreamQueryOptions): Promise>; /** * Paginates the results of your query using cursor-based pagination, * accepting a pageSize and cursor argument. This method * provides better performance for large datasets by using cursor-based * pagination instead of offset-based pagination. * * Pages follow the Query's `order`, preceded by the `order` declared on any * association the Query is built on or joins (so an `associationQuery` of * an ordered association pages in that association's order), with * descending primary key as the final tiebreaker; with neither, pages run * in descending primary key order. If those orders already include the * primary key (e.g.: `id: 'asc'`), the implicit primary key ordering is * omitted. * * A Query that joins a HasOne or HasMany association (directly, through an * association it joins, or as the association an `associationQuery` goes * through) returns a record once for each row the join matches, as * {@link Query.all} does, and the pages return each of those rows once, * sorting the rows of one record by the joined rows' primary keys after the * primary key. The cursor of such a Query is an opaque string that names * its last row by primary keys, the record's and the joined rows' (readable * by whoever holds it; it is not encrypted), and the next page starts after * that row, or, if the row is gone, after the record's first row. Any other * Query's cursor is its last record's primary key. A record's primary key * is always accepted as a cursor: the next page starts after that record's * first row. A cursor carries no other value its row sorts by: those are * read from the database when the next page is. * * A Query that calls `distinct` on its primary key (as `distinct()` does, * e.g.: `User.innerJoin('posts').distinct()`) returns each record once, as * {@link Query.all} does, and so do its pages, whose cursor is the last * record's primary key. A Query that calls `distinct` on another column has * to be ordered by that column first (e.g.: * `Post.distinct('category').order({ category: 'asc', createdAt: 'desc' })`), * or the database rejects its pages. Its pages return the records * {@link Query.all} returns, the one its order keeps for each value of that * column (the newest post of each category), and its cursor is also the * last record's primary key. Over an association that declares `distinct` * and an `order` led by its distinct column (an `associationQuery` of it, or * a Query that joins it), the pages return the rows {@link Query.all} * returns: the row the association's `order` keeps for each value of its * distinct column. * * A similarity condition (`ops.similarity`, `ops.wordSimilarity`, * `ops.strictWordSimilarity`) still decides which records match, but its * relevance ranking does not order the pages: a cursor can only resume from * values it compares, and the rank is not one of them. For best-match-first * pages, use {@link Query.paginate}. * * ```ts * // First page (using undefined to start from beginning) * const firstPage = await User.order('email').cursorPaginate({ pageSize: 100, cursor: undefined }) * firstPage.results * // [ { User{id: 777}, User{id: 776}, ...}] * * firstPage.cursor * // "100" (or null if no more pages) * * // Next page using cursor from previous result * const nextPage = await User.order('email').cursorPaginate({ * pageSize: 100, * cursor: firstPage.cursor * }) * ``` * * @param opts - cursor pagination options * @param opts.pageSize - the number of results per page (optional) * @param opts.cursor - identifier of where to start the next page; undefined to start from the beginning; null when no more pages * @returns results.cursor - identifier for the next page, or null if no more pages * @returns results.results - An array of records for the current page */ cursorPaginate, Incompatible extends Q['queryTypeOpts'] extends Readonly<{ allowPaginate: false; }> ? true : false>(this: Q, opts: Incompatible extends true ? 'scrollPaginate is incompatible with limit, offset, and leftJoinPreload'[] : CursorPaginatedDreamQueryOptions): Promise>; /** * @internal * * The values the row a cursor names sorts by now, in the order of * `orderStatements`, or undefined when no such row is found. * * Unless the primary key is the only order statement, the values are read * from the database, a soft-deleted record included, so that a page can * follow a record removed since the cursor was issued. A cursor that names its row by keys this Query's rows carry is * resumed from that row. Any other cursor (a primary key, as every cursor * issued before 2.36.0 is, or a cursor from a Query that joins other * associations), and a cursor whose row is gone, is resumed from the first * row of the record whose primary key it is or carries. */ private cursorBoundaryValues; /** * Forces use of a database connection (e.g. 'primary') during the query. * * NOTE: all queries within a transaction always use the 'primary' replica, so * explicitly setting connection within a transaction has no effect. * * @param connection - The connection you wish to access ('primary' or 'replica') * @returns A Query with the requested connection */ connection(connection: DbConnectionType): this; private _connection; /** * Returns true if a record exists for the given Query, false otherwise. * * ```ts * await User.query().exists() * // false * * await User.create({ email: 'how@yadoin' }) * * await User.query().exists() * // true * * await User.where({ email: 'nonexistent@example.com' }).exists() * // false * ``` * * @returns boolean - true if any records match the query, false otherwise */ exists(): Promise; /** * Returns the first record in the database * matching the Query. If the Query is not * ordered, it will automatically order * by primary key. * * ```ts * await User.query().first() * // User{id: 1} (first by primary key) * * await User.order('email').first() * // User with lowest email alphabetically * ``` * * @returns First record in the database, or null if no record exists */ first(): Promise; /** * Returns the first record in the database * matching the Query. If the Query is not * ordered, it will automatically order * by primary key. If no record is found, * an exception is raised. * * ```ts * await User.query().firstOrFail() * // User{id: 1} * ``` * * @returns First record in the database * @throws RecordNotFound if no record exists */ firstOrFail(): Promise; /** * Returns the last record in the database * matching the Query. If the Query is not * ordered, it will automatically order * by primary key. * * ```ts * await User.query().last() * // User{id: 99} (last by primary key) * * await User.order('email').last() * // User with highest email alphabetically * ``` * * @returns Last record in the database, or null if no record exists */ last(): Promise; /** * Returns the last record in the database * matching the Query. If the Query is not * ordered, it will automatically order * by primary key. If no record is found, * it will raise an exception. * * ```ts * await User.where(...).lastOrFail() * // User{id: 99} * ``` * * @returns Last record in the database * @throws RecordNotFound if no record exists */ lastOrFail(): Promise; /** * Destroys all records matching the Query, * calling model hooks and cascading destroy * to associations with `dependent: 'destroy'`, * and returns the number of records that * were destroyed. * * To delete in a single database query, * ignoring model hooks and association * dependent-destroy declarations, use * {@link Query.delete | delete} instead. * * ```ts * await User.where({ email: ops.ilike('%burpcollaborator%') }).destroy() * // 12 * ``` * * ## Guarded (compare-and-set) destroy * * By default, `destroy` reads the matching records and then destroys each of * them, so a concurrent writer can change a record in between — the record is * destroyed even though it no longer matches the Query that selected it. * Passing `lock: true` closes that window, making `destroy` a compare-and-set * the way `update(..., { skipHooks: true })` already is: * * ```ts * const destroyed = await Order.where({ status: 'pending' }).destroy({ lock: true }) * // 0 — someone else moved every one of them out of 'pending' first * ``` * * With `lock: true`, records are processed in batches, and each batch runs in * its own transaction which first re-selects that batch's records with an * exclusive row lock and then destroys them before committing. Because the * database re-checks the Query's conditions after acquiring each row lock, * a record a concurrent transaction has already moved out of the Query drops * out of the locked read and is never destroyed. The returned count is the * number of records actually claimed, so a caller can detect that it lost a * race by comparing the count against what it expected. * * Things to know before relying on it: * * - **The guarantee is per batch, not set-wide.** A run spanning more than one * batch can win a race in batch 1 and lose one in batch 2; nothing holds the * whole matched set still for the duration of the run. Sharing one * transaction across every batch — by threading a transaction onto the Query * itself, `Order.query().txn(txn).where({ ... }).destroy({ lock: true })` — * buys two things: the destroys become all-or-nothing (a rollback undoes the * whole run), and the row locks taken by every batch are held until you * commit. It does **not** extend the compare-and-set to the part of the set * the run has not reached yet: under READ COMMITTED each statement takes a * fresh snapshot, so a record moved out of the Query between batch 1 and * batch 2 still drops out of batch 2's locked read. A genuinely set-wide * guarantee needs REPEATABLE READ / SERIALIZABLE, or a single locking * statement over the whole set. * - **Only a transaction carried by the Query is shared.** Merely wrapping the * call in `Model.transaction(...)` does not thread that transaction onto the * Query — `Dream.transaction` establishes no ambient transaction, it only * hands a `txn` to your callback — so each batch would open its own * transaction on a different pooled connection. Worse, if the surrounding * transaction has already written to any of the matched rows, each batch's * locked read blocks on locks the surrounding transaction holds while that * transaction awaits the destroy: a cycle across two connections, which * Postgres's deadlock detector cannot see, so it hangs rather than aborting. * Always pass the transaction with `.txn(txn)`. * - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result * behavior described above is READ COMMITTED (Postgres's default). Under * REPEATABLE READ or SERIALIZABLE, the same race raises a serialization * failure instead of silently returning a lower count. Both are safe; only * one of them is quiet. * - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the * default is deliberately small. Every row in a batch stays locked for the * whole batch, including the time spent running each record's hooks and * `dependent: 'destroy'` cascade, so an oversized batch blocks unrelated * writers touching those rows and surfaces as timeouts somewhere else in the * application, which is very hard to trace back to this destroy. Too small a * batch, by contrast, only slows down this call, which you can see and fix. * Lower it further for models with deep `dependent: 'destroy'` cascades or * expensive destroy hooks. * - **A batch waits, without bound, for whoever holds the row.** The locked * read is a plain `FOR UPDATE`: if another transaction already holds a lock * on one of the batch's rows, this call blocks until that transaction ends, * holding its own transaction and its pooled connection open the whole * time. Dream sets no `lock_timeout` or `statement_timeout` on a row-lock * wait — the one it does set, around a sortable model's scope-lock * acquisition, is restored before the rows are claimed — so a long-running * writer on the other side stalls the run indefinitely and, at scale, ties * up connections. If that is a risk in your application, set a * `lock_timeout` on the connection so a stuck batch fails instead of * hanging. * - **A `limit` or `offset` on the Query is incompatible with `destroy` on * every path.** The batches — locked and unlocked alike — re-apply the * Query's conditions to each window, where a limit or offset would skip * or truncate rows inside every window instead of bounding the run as a * whole, so `destroy` throws rather than silently corrupting the * windows. * - **If you are destroying a large set and do not need compare-and-set, do * not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or * {@link Query.delete | delete} (a single statement, no hooks or cascade) * are the right tools for bulk removal. * * To bypass a default scope, chain `removeDefaultScope` onto the Query * before calling this method; it is Query state, not an option, and it * carries through to the cascade. * * @param options - Options for destroying the instance * @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false * @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true * @param options.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. Defaults to false * @param options.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise * @returns The number of records that were removed * @throws InvalidBatchSize if `batchSize` is not a positive integer * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset` */ destroy({ skipHooks, cascade, lock, batchSize, }?: { skipHooks?: boolean | undefined; cascade?: boolean | undefined; lock?: boolean | undefined; batchSize?: number | undefined; }): Promise; /** * @internal * * The compare-and-set implementation behind `destroy({ lock: true })`, * driven by {@link Query.lockedBatches}, which owns the keyset cursor, the * pluck/claim two-step, and the transaction dispatch. * * Each claimed record is destroyed via the ordinary per-instance path, and * every claimed record counts. (One thing the count does not account for, * because it never has: a `beforeDestroy` hook that sets `_preventDeletion` * leaves the record in place but still counts.) */ private lockedDestroy; /** * @internal * * The shared batch driver behind `destroy({ lock: true })` and * `update(..., { lock: true })`: the keyset cursor, the pluck/claim * two-step, and the transaction dispatch live here, so the locked * operations cannot drift apart. * * Each batch is handled in two steps inside one transaction: * * 1. an unlocked pluck of the next `batchSize` primary keys, in ascending * primary key order, which establishes the batch's window (its keyset * cursor) and tells us how many records were *attempted*. Only the keys * are read: hydrating and preloading these records would duplicate the * work step 2 does over the same rows; then * 2. a locked re-read restricted to exactly those primary keys, which * re-applies every condition on the Query while acquiring an exclusive * row lock on each row it returns. Records a concurrent transaction has * moved out of the Query since step 1 do not come back, and are the * records this run lost the race for. * * The claimed records are handed to `perBatch` inside the batch's * transaction; its return value — how many of the claimed records were * processed — accumulates into the returned count. * * The two-step shape is what keeps iteration correct. If the locked read * both claimed rows and advanced the cursor, a batch whose records all lost * the race would come back empty and end the run early, silently skipping * every record after it. Advancing the cursor from step 1 instead means * losing a race skips only the records that were lost. * * When the Query already carries a transaction, that transaction is used for * every batch rather than opening one per batch, so the caller's transaction * boundary wins and the locks are held until the caller commits. * * On a sortable model, the advisory scope keys this batch needs are acquired * before the claim (see `acquireStabilizedSortableBatchLocks`), which removes * the row-lock/advisory-lock inversion for the rows the batch itself claims. * Two things stay outside that guarantee and are fallbacks rather than * guarantees, with Postgres's `40P01` as the residual failure in both: * * - the ordering is **per batch**. A multi-batch run inside a caller-owned * transaction keeps batch N-1's row locks while batch N preflights. The * same shape accumulates *advisory* locks: they are released only when the * transaction ends, so every batch's scope keys are still held when the * next one preflights. That accumulation is bounded — the preflight counts * the keys the transaction already holds along with the ones the batch * would add, and raises rather than letting a long run over a * high-cardinality sort scope exhaust `max_locks_per_transaction` for the * whole cluster — so a run long enough raises instead of completing. That * refusal is a sizing failure to fix, not a condition to catch: letting * Dream open a transaction per batch releases the keys as it goes and gives * each batch the whole budget, and a smaller `batchSize` takes fewer. * - the preflight covers **the claimed rows' own scopes only**. A * `dependent: 'destroy'` cascade under `destroy({ lock: true })` destroys * child records of other tables, and a sortable child takes its own table's * advisory key inside `perBatch` — after this transaction is already * holding the root rows' row locks. Walking * `dependentDestroyAssociationNames()` to fold those keys into the sorted * pass is deliberately not done: the cascade's shape is only knowable per * record, and the deadlock it leaves is detected and retryable. The keys * the preflight *did* take are held across the whole batch, cascade * included — a per-record destroy holds its scope lock across one delete * and one compaction, while here the hold spans batch size × cascade depth. * A smaller `batchSize` shortens it (the default is 10). */ private lockedBatches; /** * Destroys all records matching the Query, * permanently removing each matched record from * the database instead of soft deleting it. * * The SoftDelete decorator is bypassed when * deleting each matched record, not when * matching: the Query selects under its default * scopes, `dream:SoftDelete` among them, so on a * SoftDelete model it matches only records that * have not already been soft deleted. Reaching * soft deleted records takes * `removeDefaultScope('dream:SoftDelete')` on * the Query. * * Calls model hooks and applies cascade destroy * to associations with `dependent: 'destroy'`, * returning the number of records that * were destroyed. * * To destroy without bypassing the SoftDelete * decorator, use {@link Query.destroy} instead. * * ```ts * await User.where({ email: ops.ilike('%burpcollaborator%') }).reallyDestroy() * // 12 * ``` * * ```ts * await User.removeDefaultScope('dream:SoftDelete') * .where({ email: ops.ilike('%burpcollaborator%') }) * .reallyDestroy() * // 12 * ``` * * A `limit` or `offset` on the Query is incompatible with `reallyDestroy`, * exactly as it is with {@link Query.destroy}: a limit- or offset-carrying * Query throws at runtime. * * To bypass a default scope, chain `removeDefaultScope` onto the Query * before calling this method; it is Query state, not an option, and it * carries through to the cascade. * * @param options - Options for destroying the instance * @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false * @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true * @param options.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. See {@link Query.destroy} for the full semantics. Defaults to false * @param options.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise * @returns The number of records that were removed * @throws InvalidBatchSize if `batchSize` is not a positive integer * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset` */ reallyDestroy({ skipHooks, cascade, lock, batchSize, }?: { skipHooks?: boolean; cascade?: boolean; lock?: boolean; batchSize?: number; }): Promise; /** * Undestroys a SoftDelete model, unsetting * the `deletedAt` field in the database. * * If the model is not a SoftDelete model, * this will raise an exception. * * ```ts * await User.where({ email: ops.ilike('%burpcollaborator%') }).undestroy() * // 12 * ``` * * A `limit` or `offset` on the Query cannot compose with the batch windows * `undestroy` iterates in — each batch re-applies the Query's conditions, * so the limit would be silently replaced by the batch size and the offset * re-applied to every window — so a limit- or offset-carrying Query throws * at runtime. * * To bypass a default scope, chain `removeDefaultScope` onto the Query * before calling this method; it is Query state, not an option, and it * carries through to the cascade. The SoftDelete scope is always bypassed. * * @param options - Options for undestroying the instance * @param options.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false * @param options.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true * @returns The number of records that were removed * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset` */ undestroy({ cascade, skipHooks, }?: { cascade?: boolean; skipHooks?: boolean; }): Promise; /** * Deletes all records matching query using a single * database query, but does not call underlying callbacks. * Ignores association dependent destroy declarations, * though cascading may still happen at the database level. * * To apply model hooks and association dependent destroy, * use {@link Query.destroy} instead. * * ```ts * await User.where({ email: ops.ilike('%burpcollaborator%') }).delete() * // 12 * ``` * * @returns The number of records that were removed */ delete(): Promise; /** * Updates all records matching the Query * * ```ts * await User.where({ email: ops.ilike('%burpcollaborator%') }).update({ email: null }) * // 12 * ``` * * Instead of an attributes object, the first argument may be a callback * that derives each record's attributes from the record itself. The * callback (sync or async) receives each matching record and returns the * attributes to write for it, or `undefined` to skip it — a skipped record * gets no write, runs no hooks, and is not counted (a `null` return, from * untyped callers, is treated the same). Only the returned attributes are * written: the callback receives a clone of the record, so mutations made * directly on the received record are discarded rather than riding along * with the write. With a callback, the options object and its `lock` key * are mandatory, and `lock` is a full boolean, so every call site says out * loud whether the callback runs under row locks: * * ```ts * await User.query().update( * user => (user.email.endsWith('@example.com') ? { name: 'example user' } : undefined), * { lock: false } * ) * ``` * * Under `lock: false`, the callback runs once per matching record — * batched reads (`batchSize` defaults to 1000 here), no locks, no claim * step, no compare-and-set guarantee — the right shape for a derived * backfill where racing writers are not a concern. (On a `@ReplicaSafe` * model, these unlocked batch reads may come from a lagging replica, * feeding stale column values into the callback; use `lock: true` to * derive from the freshest committed state — its transactional reads pin * the primary.) Under `lock: true`, the callback runs once per claimed * record, under that record's row lock, inside the batch's transaction, * receiving a clone of the freshly locked read; Dream applies the returned * attributes to the record it holds and writes before the batch commits, * so there is no transaction handle for the caller to forget. (The record * the callback receives is not bound to the batch's transaction — see the * callback bullet below before touching the database from inside a * callback.) That makes the callback form the tool * for serializing writers on state a `WHERE` clause cannot express — for * example an `@Encrypted` column, whose ciphertext changes on every write * and whose plaintext has no column to compare against in the database: * * ```ts * const rotated = await User.where({ id: userId }).update( * user => (user.secret === expectedSecret ? { secret: newSecret } : undefined), * { lock: true } * ) * // 1 — or 0, if a competing writer got there first (the callback saw a * // different secret and skipped the record) * ``` * * With a callback, the returned count is the number of records the * callback returned attributes for: skipped records are not counted, and * under `lock: true` records that lost the race never reach the callback * at all. (As with the attributes form, a write that turns out to be a * no-op still counts — the count means "the callback chose to write", not * "SQL was emitted".) * * The `lock` option turns the update into a guarded (compare-and-set) * update: each record is written only if it still matches the Query at the * moment an exclusive row lock on it is acquired. Use it when the Query's * conditions are the thing being changed and a concurrent writer racing this * call must not have its write clobbered: * * ```ts * const updated = await Order.where({ status: 'pending' }) * .update({ status: 'processing' }, { lock: true }) * // 0 — someone else moved every one of them out of 'pending' first * ``` * * With `lock: true`, records are processed in batches, and each batch runs * in its own transaction which first re-selects that batch's records with an * exclusive row lock and then updates them before committing. Because the * database re-checks the Query's conditions after acquiring each row lock, a * record a concurrent transaction has already moved out of the Query drops * out of the locked read and is never updated. The returned count is the * number of records claimed and submitted for writing, so a caller can * detect that it lost a race by comparing the count against what it * expected. (A claimed record whose write turns out to be a no-op — the * attributes already match, or a hook cleared every dirty attribute — still * counts; the count is not a guarantee that SQL was emitted per counted * record.) * * Things to know before relying on it: * * - **The guarantee is per batch, not set-wide.** A run spanning more than * one batch can win a race in batch 1 and lose one in batch 2; nothing * holds the whole matched set still for the duration of the run. Sharing * one transaction across every batch — by threading a transaction onto the * Query itself, `Order.query().txn(txn).where({ ... }).update({ ... }, * { lock: true })` — buys two things: the updates become all-or-nothing (a * rollback undoes the whole run), and the row locks taken by every batch * are held until you commit. It does **not** extend the compare-and-set to * the part of the set the run has not reached yet: under READ COMMITTED * each statement takes a fresh snapshot, so a record moved out of the * Query between batch 1 and batch 2 still drops out of batch 2's locked * read. A genuinely set-wide guarantee needs REPEATABLE READ / * SERIALIZABLE, or a single locking statement over the whole set. * - **Only a transaction carried by the Query is shared.** Merely wrapping * the call in `Model.transaction(...)` does not thread that transaction * onto the Query — `Dream.transaction` establishes no ambient transaction, * it only hands a `txn` to your callback — so each batch would open its * own transaction on a different pooled connection. Worse, if the * surrounding transaction has already written to any of the matched rows, * each batch's locked read blocks on locks the surrounding transaction * holds while that transaction awaits the update: a cycle across two * connections, which Postgres's deadlock detector cannot see, so it hangs * rather than aborting. Always pass the transaction with `.txn(txn)`. * - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result * behavior described above is READ COMMITTED (Postgres's default). Under * REPEATABLE READ or SERIALIZABLE, the same race raises a serialization * failure instead of silently returning a lower count. Both are safe; only * one of them is quiet. * - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the * default is deliberately small. Every row in a batch stays locked for the * whole batch, including the time spent running each record's hooks — and, * on the callback form, the time spent running the Query's `preload` * queries, which are fetched inside the lock window (the attributes form * strips preloads, which it never consumes) — so an oversized batch blocks * unrelated writers touching those rows and surfaces as timeouts somewhere * else in the application, which is very hard to trace back to this * update. Too small a batch, by contrast, only slows down this call, which * you can see and fix. Lower it further for models with expensive update * hooks. * - **The callback must not write to the database.** The record handed to * the callback is not bound to the batch's transaction: a write through it * (e.g. `await record.update(...)`) goes out on a different pooled * connection and blocks on the batch's own row locks while the batch * transaction waits on the callback — a cycle across two connections that * the database's deadlock detector cannot see, so with no `lock_timeout` * the run hangs indefinitely and pins both connections. Reads through the * record (`reload`, `associationQuery`, ...) likewise run on a separate * connection and snapshot. Derive the attributes and return them; Dream * performs the write inside the batch's transaction. * - **Without `lock: true`, a record is not re-checked before it is * written.** The unlocked per-record paths — an attributes object without * `lock: true` or `skipHooks: true`, and a callback under `lock: false` — * read a batch, then write each record by its primary key alone, with no * compare-and-set. What a concurrent delete between that read and a * record's write does depends on what the delete leaves behind. A soft * delete (`destroy()` on a `@SoftDelete` model, which generated models are * by default) leaves the row in place, so the write lands on the * soft-deleted record: its update hooks run, it is counted, and its * `deletedAt` stays set. A hard delete (`reallyDestroy()`, a Query's * `delete()`, or `destroy()` on a model without `@SoftDelete`) removes the * row, so that record's write throws `CannotSaveMissingDream` — after its * `beforeUpdate` and `beforeSave` hooks have run — and the run rejects * without returning a count. Every record written before it, in its own * batch as well as earlier ones, stays committed, unless the Query carries * `.txn(txn)`, which puts those writes in the caller's transaction. (A * record whose write turns out to be a no-op emits no SQL, so it does not * throw even if its row is gone, and it is counted.) The single-statement * `skipHooks` attributes form is one `UPDATE ... WHERE` that carries the * Query's default scopes, so it never writes a soft-deleted record and * cannot fail part-way through. Use `lock: true` when deleters may race * the update: its locked re-read re-applies the Query's conditions and * default scopes, so a soft- or hard-deleted record drops out of the run, * neither written nor counted. (A Query that removes the * `dream:SoftDelete` default scope reads and writes soft-deleted records * on every path.) * - **A `limit` or `offset` on the Query is incompatible with `update` on * every path.** The batched paths re-apply the Query's conditions to each * batch window, where a limit or offset would skip or truncate rows * inside every window instead of bounding the run as a whole, and the * single-statement `skipHooks` attributes form cannot express either in * its SQL UPDATE. Every form throws rather than silently corrupting the * windows or ignoring the caller's scope. * - **A batch waits, without bound, for whoever holds the row.** The locked * read is a plain `FOR UPDATE`: if another transaction already holds a * lock on one of the batch's rows, this call blocks until that transaction * ends, holding its own transaction and its pooled connection open the * whole time. Dream sets no `lock_timeout` or `statement_timeout` on a * row-lock wait — the one it does set, around a sortable model's scope-lock * acquisition, is restored before the rows are claimed — so a long-running * writer on the other side stalls the run indefinitely and, at scale, ties * up connections. If that is a risk in your application, set a * `lock_timeout` on the connection so a stuck batch fails instead of * hanging. * - **`skipHooks` composes with `lock`, but stays on the per-instance * path.** With an attributes object and no `lock`, `skipHooks: true` * issues a single `UPDATE ... WHERE` statement that writes raw columns — * with one refusal: it will not write a non-null value into an * `@Encrypted` backing column, since nothing on that path encrypts it, and * throws instead; `null`, which clears the column, is permitted. * With `lock: true`, `skipHooks: true` skips the model hooks on each * claimed record but still writes through the instance, so custom setters * — including the setters encrypted attributes encrypt through — run as * they would for a plain instance update. (Virtual attributes whose * processing lives in a hook are still skipped, exactly as in an instance * update with `skipHooks`.) A callback never takes the single-statement * path: `skipHooks` beside a callback only skips the hooks on each * per-record write, under `lock: true` and `lock: false` alike. * - **If you are updating a large set and do not need compare-and-set, do * not pass `lock`.** Plain `update(attributes)` (hooks, no locking) or * `update(attributes, { skipHooks: true })` (a single statement, no hooks) * are the right tools for bulk updates. * * @param attributesOrCb - The attributes used to update the records, or a callback receiving each record and returning the attributes to write for it (or `undefined` to skip that record). Passing a callback makes `options` and its `lock` key mandatory * @param options - Options for updating the instance * @param options.skipHooks - If true, skips applying model hooks. Defaults to false * @param options.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being updated, making the update a compare-and-set. Defaults to false for the attributes form; with a callback there is no default — an explicit boolean is required. One typing asymmetry to know: with an attributes object, an explicit `lock: false` keeps the raw single-statement (`DreamTableSchema`) typing that `skipHooks` uses, so virtual/encrypted attributes type-check only when the options object is omitted entirely or `lock` is `true` * @param options.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 for the unlocked callback form * @returns The number of records that were updated * @throws InvalidBatchSize if `batchSize` is not a positive integer * @throws MissingRequiredLockOptionForUpdateCallback if a callback is passed without an options object carrying a boolean `lock` * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset` * @throws CannotSetEncryptedColumnInQueryUpdate if the single-statement `skipHooks` form's attributes name an `@Encrypted` backing column with anything other than `null` * @throws CannotSaveMissingDream if, on an unlocked per-record path, a record's row is hard-deleted between its batch's read and a write that is not a no-op */ update(cb: (record: DreamInstance) => UpdateableProperties | undefined | Promise | undefined>, options: { lock: boolean; batchSize?: number; skipHooks?: boolean; }): Promise; update(attributes: UpdateableProperties, options: { lock: true; batchSize?: number; skipHooks?: boolean; }): Promise; update(attributes: DreamTableSchema, options?: { skipHooks?: boolean; lock?: false; }): Promise; update(attributes: UpdateableProperties): Promise; /** * @internal * * The compare-and-set implementation behind `update(attributes, { lock: true })`, * driven by {@link Query.lockedBatches}, which owns the keyset cursor, the * pluck/claim two-step, and the transaction dispatch. * * Each claimed record is handed to `cb`, which returns the attributes to * write for it, or a nullish value to skip it. The attributes form is the * constant-callback case (a callback ignoring the record and always * returning the same attributes), which is how the two forms share this * implementation. (`update` hands the user's own callback a clone of each * claimed record, so only the returned attributes reach the write below — * see the dispatch in {@link Query.update}.) * * The count is the number of claimed records the callback returned * attributes for: records attempted but not claimed (lost races) never * reach the callback, and claimed records the callback skipped are not * counted. For the attributes form the callback is total, so the count * degenerates to records claimed and submitted for writing. (A record whose * write turns out to be a no-op still counts: the instance path returns * without issuing SQL when nothing is dirty.) * * Each claimed record is written via the ordinary per-instance update, so * hooks (unless `skipHooks`), custom setters, and virtual/encrypted * attribute preprocessing behave exactly as in a plain instance update. * * No join-load guard is needed here: update's own joins guards have already * thrown on every join-load query (leftJoinPreload always carries left-join * statements). */ private lockedUpdate; private updateWithoutCallingModelHooks; private invertOrder; } export interface QueryOpts = DreamColumnNames, Schema extends DreamInstance['schema'] = DreamInstance['schema'], DB extends DreamInstance['DB'] = DreamInstance['DB'], PassthroughColumns extends PassthroughColumnNames = PassthroughColumnNames> { baseSqlAlias?: TableOrAssociationName | undefined; baseSelectQuery?: Query | null | undefined; passthroughOnStatement?: PassthroughOnClause | null | undefined; where?: readonly InternalWhereStatement[] | null | undefined; whereNot?: readonly InternalWhereStatement[] | null | undefined; limit?: LimitStatement | null | undefined; offset?: OffsetStatement | null | undefined; or?: InternalWhereStatement[][] | null | undefined; order?: OrderQueryStatement[] | null | undefined; loadFromJoins?: boolean | undefined; preloadStatements?: RelaxedPreloadStatement | undefined; preloadOnStatements?: RelaxedPreloadOnStatement | undefined; distinctColumn?: ColumnType | null | undefined; innerJoinDreamClasses?: readonly (typeof Dream)[] | undefined; innerJoinStatements?: RelaxedJoinStatement | undefined; innerJoinAndStatements?: RelaxedJoinAndStatement | undefined; leftJoinStatements?: RelaxedJoinStatement | undefined; leftJoinAndStatements?: RelaxedJoinAndStatement | undefined; bypassAllDefaultScopes?: boolean | undefined; bypassAllDefaultScopesExceptOnAssociations?: boolean | undefined; defaultScopesToBypass?: AllDefaultScopeNames[] | undefined; defaultScopesToBypassExceptOnAssociations?: DefaultScopeName[] | undefined; transaction?: DreamTransaction | null | undefined; connection?: DbConnectionType | undefined; shouldReallyDestroy?: boolean | undefined; bypassImplicitOrder?: boolean | undefined; }