@rvoh/dream 2.24.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/cli/index.js +29 -9
- package/dist/cjs/src/dream/Query.js +247 -67
- package/dist/cjs/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
- package/dist/cjs/src/errors/InvalidMigrationTableName.js +30 -0
- package/dist/cjs/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
- package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +35 -3
- package/dist/cjs/src/helpers/cli/generateMigration.js +33 -3
- package/dist/cjs/src/helpers/cli/generateMigrationContent.js +3 -3
- package/dist/esm/src/cli/index.js +29 -9
- package/dist/esm/src/dream/Query.js +247 -67
- package/dist/esm/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
- package/dist/esm/src/errors/InvalidMigrationTableName.js +30 -0
- package/dist/esm/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
- package/dist/esm/src/errors/NoColumnsToAlterMigration.js +35 -3
- package/dist/esm/src/helpers/cli/generateMigration.js +33 -3
- package/dist/esm/src/helpers/cli/generateMigrationContent.js +3 -3
- package/dist/types/src/cli/index.d.ts +8 -0
- package/dist/types/src/dream/Query.d.ts +316 -40
- package/dist/types/src/errors/BatchingIncompatibleWithLimitOrOffset.d.ts +3 -0
- package/dist/types/src/errors/InvalidMigrationTableName.d.ts +10 -0
- package/dist/types/src/errors/MissingRequiredLockOptionForUpdateCallback.d.ts +3 -0
- package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +11 -1
- package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +18 -4
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +40 -40
- package/docs/classes/db.PostgresQueryDriver.html +41 -41
- package/docs/classes/db.QueryDriverBase.html +39 -39
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +124 -124
- package/docs/classes/index.DreamApp.html +10 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +735 -73
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +3 -3
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +5 -5
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
|
@@ -105,6 +105,31 @@ ${INDENT}
|
|
|
105
105
|
${INDENT} rename the FK column with Model@alias (snake_case alias becomes the column name):
|
|
106
106
|
${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column with index
|
|
107
107
|
${INDENT} Messaging/Template@template:belongs_to # template_id column (strips the namespace from the column name)`);
|
|
108
|
+
/**
|
|
109
|
+
* The `<migrationName>` argument description for `g:migration`. Exported so
|
|
110
|
+
* that spec/unit/helpers/cli/generateMigration.spec.ts can read the example
|
|
111
|
+
* migration names out of this text and run them through the generator: a help
|
|
112
|
+
* example that drifts from what the generator actually produces then fails the
|
|
113
|
+
* suite instead of shipping.
|
|
114
|
+
*/
|
|
115
|
+
export const migrationNameArgumentDescription = `Kebab-case name describing the change. A -to-<table_name>
|
|
116
|
+
${INDENT}anywhere in the name — not just at the end — auto-generates
|
|
117
|
+
${INDENT}an add-columns alterTable scaffold for that table;
|
|
118
|
+
${INDENT}-from-<table_name> generates a remove-columns scaffold, but
|
|
119
|
+
${INDENT}only when the name contains no -to- at all. The text after
|
|
120
|
+
${INDENT}the marker's first occurrence is snake-cased and pluralized
|
|
121
|
+
${INDENT}into the table name, so add-a-to-b-to-cs targets b_to_cs. A
|
|
122
|
+
${INDENT}hand-written migration must avoid both strings anywhere in
|
|
123
|
+
${INDENT}its name.
|
|
124
|
+
${INDENT}
|
|
125
|
+
${INDENT}Examples:
|
|
126
|
+
${INDENT} add-phone-to-users phone:encrypted # scaffolds alterTable('users', ...)
|
|
127
|
+
${INDENT} remove-status-from-posts status:string # scaffolds alterTable('posts', ...)
|
|
128
|
+
${INDENT} create-many-to-many-posts-users # WRONG: selects 'many_posts_users'
|
|
129
|
+
${INDENT} # -to- inside "many-to-many" matches: with no columns
|
|
130
|
+
${INDENT} # this errors; with columns it silently scaffolds it
|
|
131
|
+
${INDENT} create-posts-users-join-table # correct: no marker match
|
|
132
|
+
${INDENT} # scaffolds alterTable('<table-name>') to hand-edit`;
|
|
108
133
|
export default class DreamCLI {
|
|
109
134
|
/**
|
|
110
135
|
* Starts the Dream console
|
|
@@ -151,22 +176,17 @@ export default class DreamCLI {
|
|
|
151
176
|
.description(`Generates a new Kysely migration file for schema changes. Use this for altering existing tables (adding/removing columns, indexes, constraints). Prefer g:resource or g:model when creating a new model, since they generate the migration along with the model, serializer, and spec files.
|
|
152
177
|
${INDENT}
|
|
153
178
|
${INDENT}Examples:
|
|
154
|
-
${INDENT} # Add columns to an existing table (
|
|
179
|
+
${INDENT} # Add columns to an existing table (include -to-<table_name> for auto alterTable scaffolding)
|
|
155
180
|
${INDENT} pnpm psy g:migration add-timezone-to-users timezone:string
|
|
156
181
|
${INDENT} pnpm psy g:migration add-bio-to-users bio:text:optional avatar_url:string:optional
|
|
157
182
|
${INDENT}
|
|
158
|
-
${INDENT} # Remove columns (
|
|
183
|
+
${INDENT} # Remove columns (include -from-<table_name>; columns are described with
|
|
159
184
|
${INDENT} # their types, same shorthand as -to-, so the rollback (down) can recreate them)
|
|
160
185
|
${INDENT} pnpm psy g:migration remove-legacy-fields-from-posts legacy_status:string
|
|
161
186
|
${INDENT}
|
|
162
|
-
${INDENT} # General schema change (no
|
|
187
|
+
${INDENT} # General schema change (no -to-/-from- marker — scaffolds alterTable('<table-name>') to hand-edit)
|
|
163
188
|
${INDENT} pnpm psy g:migration create-unique-index-on-invitations`)
|
|
164
|
-
.argument('<migrationName>',
|
|
165
|
-
${INDENT}
|
|
166
|
-
${INDENT}Examples:
|
|
167
|
-
${INDENT} add-phone-to-users phone:encrypted # scaffolds alterTable('users', ...)
|
|
168
|
-
${INDENT} remove-status-from-posts status:string # scaffolds alterTable('posts', ...)
|
|
169
|
-
${INDENT} create-join-table-host-places # empty migration (no table suffix match)`)
|
|
189
|
+
.argument('<migrationName>', migrationNameArgumentDescription)
|
|
170
190
|
.option('--connection-name <connectionName>', 'the database connection to use for this migration. Only needed for multi-database setups; defaults to "default"')
|
|
171
191
|
.argument('[columnsWithTypes...]', columnsWithTypesDescriptionForMigration)
|
|
172
192
|
.action(async (migrationName, columnsWithTypes, options) => {
|
|
@@ -3,10 +3,12 @@ import DreamApp from '../dream-app/index.js';
|
|
|
3
3
|
import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
|
|
4
4
|
import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
5
5
|
import CannotPassAdditionalFieldsToPluckEachAfterCallback from '../errors/CannotPassAdditionalFieldsToPluckEachAfterCallback.js';
|
|
6
|
+
import BatchingIncompatibleWithLimitOrOffset from '../errors/BatchingIncompatibleWithLimitOrOffset.js';
|
|
6
7
|
import InvalidBatchSize from '../errors/InvalidBatchSize.js';
|
|
7
8
|
import LeftJoinPreloadIncompatibleWithFindEach from '../errors/LeftJoinPreloadIncompatibleWithFindEach.js';
|
|
8
9
|
import RowLockIncompatibleWithDistinct from '../errors/RowLockIncompatibleWithDistinct.js';
|
|
9
10
|
import MissingRequiredCallbackFunctionToPluckEach from '../errors/MissingRequiredCallbackFunctionToPluckEach.js';
|
|
11
|
+
import MissingRequiredLockOptionForUpdateCallback from '../errors/MissingRequiredLockOptionForUpdateCallback.js';
|
|
10
12
|
import NoUpdateAllOnJoins from '../errors/NoUpdateAllOnJoins.js';
|
|
11
13
|
import NoUpdateOnAssociationQuery from '../errors/NoUpdateOnAssociationQuery.js';
|
|
12
14
|
import CannotPaginateWithLeftJoinPreload from '../errors/pagination/CannotPaginateWithLeftJoinPreload.js';
|
|
@@ -51,10 +53,12 @@ export default class Query {
|
|
|
51
53
|
*/
|
|
52
54
|
static BATCH_SIZES = {
|
|
53
55
|
FIND_EACH: 1000,
|
|
54
|
-
// deliberately small: every record in a locked destroy batch
|
|
55
|
-
// row-locked for the whole batch, so an oversized batch blocks
|
|
56
|
-
// writers. See
|
|
56
|
+
// deliberately small: every record in a locked destroy or update batch
|
|
57
|
+
// stays row-locked for the whole batch, so an oversized batch blocks
|
|
58
|
+
// unrelated writers. See the `lock` options on Query#destroy and
|
|
59
|
+
// Query#update.
|
|
57
60
|
LOCKED_DESTROY: 10,
|
|
61
|
+
LOCKED_UPDATE: 10,
|
|
58
62
|
PLUCK_EACH: 10000,
|
|
59
63
|
PLUCK_EACH_THROUGH: 1000,
|
|
60
64
|
};
|
|
@@ -446,26 +450,40 @@ export default class Query {
|
|
|
446
450
|
* If you need records processed in a particular order, load them with a
|
|
447
451
|
* regular ordered query instead of `findEach`.
|
|
448
452
|
*
|
|
453
|
+
* A `limit` or `offset` on the Query cannot compose with the batch windows
|
|
454
|
+
* — each batch re-applies the Query's conditions, so the limit would be
|
|
455
|
+
* silently replaced by `batchSize` and the offset re-applied to every
|
|
456
|
+
* window — so a limit- or offset-carrying Query throws at runtime.
|
|
457
|
+
*
|
|
449
458
|
* @param cb - The callback to call for each found record
|
|
450
459
|
* @param __namedParameters - Options for batch processing
|
|
451
460
|
* @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
452
461
|
* @returns void
|
|
462
|
+
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
453
463
|
*/
|
|
454
464
|
async findEach(cb, { batchSize = Query.BATCH_SIZES.FIND_EACH } = {}) {
|
|
455
465
|
if (this.joinLoadActivated)
|
|
456
466
|
throw new LeftJoinPreloadIncompatibleWithFindEach();
|
|
467
|
+
// a limit or offset cannot compose with the batch windows: the window
|
|
468
|
+
// queries would re-apply it per batch — the limit silently replaced by
|
|
469
|
+
// batchSize, the offset skipping rows inside every window
|
|
470
|
+
if (this.limitStatement || this.offsetStatement)
|
|
471
|
+
throw new BatchingIncompatibleWithLimitOrOffset();
|
|
457
472
|
let records;
|
|
458
473
|
const query = this.order(null)
|
|
459
474
|
.order(this.namespacedPrimaryKey)
|
|
460
475
|
.limit(batchSize);
|
|
461
|
-
|
|
476
|
+
// the cursor is compared against a sentinel rather than tested for
|
|
477
|
+
// truthiness, since a primary key of 0 is a legitimate cursor value that
|
|
478
|
+
// would otherwise reset the window to the start of the set
|
|
479
|
+
let lastId = undefined;
|
|
462
480
|
do {
|
|
463
|
-
if (lastId)
|
|
481
|
+
if (lastId === undefined)
|
|
482
|
+
records = await query.all();
|
|
483
|
+
else
|
|
464
484
|
records = await query
|
|
465
485
|
.where({ [this.dreamInstance['_primaryKey']]: ops.greaterThan(lastId) })
|
|
466
486
|
.all();
|
|
467
|
-
else
|
|
468
|
-
records = await query.all();
|
|
469
487
|
for (const record of records) {
|
|
470
488
|
await cb(record);
|
|
471
489
|
}
|
|
@@ -946,6 +964,12 @@ export default class Query {
|
|
|
946
964
|
* // [User{id: 1}, User{id: 2}]
|
|
947
965
|
* ```
|
|
948
966
|
*
|
|
967
|
+
* A limit is incompatible with `paginate` and `leftJoinPreload`, and with
|
|
968
|
+
* the batched and set-write operations — `findEach`, `pluckEach`,
|
|
969
|
+
* `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can
|
|
970
|
+
* honor one: the batched and set-write operations throw
|
|
971
|
+
* `BatchingIncompatibleWithLimitOrOffset` on a limit-carrying Query.
|
|
972
|
+
*
|
|
949
973
|
* @returns A cloned Query with the limit clause applied
|
|
950
974
|
*/
|
|
951
975
|
limit(limit) {
|
|
@@ -959,6 +983,12 @@ export default class Query {
|
|
|
959
983
|
* // [User{id: 3}, User{id: 4}]
|
|
960
984
|
* ```
|
|
961
985
|
*
|
|
986
|
+
* An offset is incompatible with `paginate` and `leftJoinPreload`, and with
|
|
987
|
+
* the batched and set-write operations — `findEach`, `pluckEach`,
|
|
988
|
+
* `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can
|
|
989
|
+
* honor one: the batched and set-write operations throw
|
|
990
|
+
* `BatchingIncompatibleWithLimitOrOffset` on an offset-carrying Query.
|
|
991
|
+
*
|
|
962
992
|
* @returns A cloned Query with the offset clause applied
|
|
963
993
|
*/
|
|
964
994
|
offset(offset) {
|
|
@@ -1322,8 +1352,14 @@ export default class Query {
|
|
|
1322
1352
|
* // 3
|
|
1323
1353
|
* ```
|
|
1324
1354
|
*
|
|
1355
|
+
* A `limit` or `offset` on the Query cannot compose with the batch windows
|
|
1356
|
+
* — each window replaces the limit with the batch size and the offset with
|
|
1357
|
+
* the running window offset, silently ignoring both — so a limit- or
|
|
1358
|
+
* offset-carrying Query throws at runtime.
|
|
1359
|
+
*
|
|
1325
1360
|
* @param args - a list of column names to pluck, followed by a callback function to call for each set of found fields
|
|
1326
1361
|
* @returns void
|
|
1362
|
+
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
1327
1363
|
*/
|
|
1328
1364
|
async pluckEach(...args) {
|
|
1329
1365
|
const providedCbIndex = args.findIndex(v => typeof v === 'function');
|
|
@@ -1333,6 +1369,11 @@ export default class Query {
|
|
|
1333
1369
|
throw new MissingRequiredCallbackFunctionToPluckEach('pluckEach', args);
|
|
1334
1370
|
if (providedOpts !== undefined && !providedOpts?.batchSize)
|
|
1335
1371
|
throw new CannotPassAdditionalFieldsToPluckEachAfterCallback('pluckEach', args);
|
|
1372
|
+
// a limit or offset cannot compose with the batch windows: the window
|
|
1373
|
+
// queries would replace the limit with batchSize and the offset with the
|
|
1374
|
+
// running window offset, silently ignoring both
|
|
1375
|
+
if (this.limitStatement || this.offsetStatement)
|
|
1376
|
+
throw new BatchingIncompatibleWithLimitOrOffset();
|
|
1336
1377
|
const onlyColumns = args.filter((_, index) => index < providedCbIndex);
|
|
1337
1378
|
const batchSize = providedOpts?.batchSize || Query.BATCH_SIZES.PLUCK_EACH_THROUGH;
|
|
1338
1379
|
let offset = 0;
|
|
@@ -1865,20 +1906,27 @@ export default class Query {
|
|
|
1865
1906
|
* scale, ties up connections. If that is a risk in your application, set a
|
|
1866
1907
|
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1867
1908
|
* hanging.
|
|
1909
|
+
* - **A `limit` or `offset` on the Query is incompatible with `destroy` on
|
|
1910
|
+
* every path.** The batches — locked and unlocked alike — re-apply the
|
|
1911
|
+
* Query's conditions to each window, where a limit or offset would skip
|
|
1912
|
+
* or truncate rows inside every window instead of bounding the run as a
|
|
1913
|
+
* whole, so `destroy` throws rather than silently corrupting the
|
|
1914
|
+
* windows.
|
|
1868
1915
|
* - **If you are destroying a large set and do not need compare-and-set, do
|
|
1869
1916
|
* not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
|
|
1870
1917
|
* {@link Query.delete | delete} (a single statement, no hooks or cascade)
|
|
1871
1918
|
* are the right tools for bulk removal.
|
|
1872
1919
|
*
|
|
1873
|
-
* @param
|
|
1874
|
-
* @param
|
|
1875
|
-
* @param
|
|
1876
|
-
* @param
|
|
1877
|
-
* @param
|
|
1878
|
-
* @param
|
|
1879
|
-
* @param
|
|
1920
|
+
* @param options - Options for destroying the instance
|
|
1921
|
+
* @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1922
|
+
* @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1923
|
+
* @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
|
|
1924
|
+
* @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
|
|
1925
|
+
* @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1926
|
+
* @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1880
1927
|
* @returns The number of records that were removed
|
|
1881
1928
|
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1929
|
+
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
1882
1930
|
*/
|
|
1883
1931
|
async destroy({ skipHooks, cascade, lock, batchSize, } = {}) {
|
|
1884
1932
|
let counter = 0;
|
|
@@ -1917,43 +1965,77 @@ export default class Query {
|
|
|
1917
1965
|
/**
|
|
1918
1966
|
* @internal
|
|
1919
1967
|
*
|
|
1920
|
-
* The compare-and-set implementation behind `destroy({ lock: true })
|
|
1968
|
+
* The compare-and-set implementation behind `destroy({ lock: true })`,
|
|
1969
|
+
* driven by {@link Query.lockedBatches}, which owns the keyset cursor, the
|
|
1970
|
+
* pluck/claim two-step, and the transaction dispatch.
|
|
1971
|
+
*
|
|
1972
|
+
* Each claimed record is destroyed via the ordinary per-instance path, and
|
|
1973
|
+
* every claimed record counts. (One thing the count does not account for,
|
|
1974
|
+
* because it never has: a `beforeDestroy` hook that sets `_preventDeletion`
|
|
1975
|
+
* leaves the record in place but still counts.)
|
|
1976
|
+
*/
|
|
1977
|
+
async lockedDestroy(options, batchSize) {
|
|
1978
|
+
if (this.joinLoadActivated)
|
|
1979
|
+
throw new LeftJoinPreloadIncompatibleWithFindEach();
|
|
1980
|
+
return await this.lockedBatches(batchSize, async (claimed, txn) => {
|
|
1981
|
+
for (const record of claimed) {
|
|
1982
|
+
const inTransaction = record.txn(txn);
|
|
1983
|
+
if (this.shouldReallyDestroy) {
|
|
1984
|
+
await inTransaction.reallyDestroy(options);
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
await inTransaction.destroy(options);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return claimed.length;
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* @internal
|
|
1995
|
+
*
|
|
1996
|
+
* The shared batch driver behind `destroy({ lock: true })` and
|
|
1997
|
+
* `update(..., { lock: true })`: the keyset cursor, the pluck/claim
|
|
1998
|
+
* two-step, and the transaction dispatch live here, so the locked
|
|
1999
|
+
* operations cannot drift apart.
|
|
1921
2000
|
*
|
|
1922
2001
|
* Each batch is handled in two steps inside one transaction:
|
|
1923
2002
|
*
|
|
1924
2003
|
* 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
|
|
1925
2004
|
* primary key order, which establishes the batch's window (its keyset
|
|
1926
|
-
* cursor) and tells us how many records were *attempted*. Only the keys
|
|
1927
|
-
* read: hydrating and preloading these records would duplicate the
|
|
1928
|
-
* step 2 does over the same rows; then
|
|
2005
|
+
* cursor) and tells us how many records were *attempted*. Only the keys
|
|
2006
|
+
* are read: hydrating and preloading these records would duplicate the
|
|
2007
|
+
* work step 2 does over the same rows; then
|
|
1929
2008
|
* 2. a locked re-read restricted to exactly those primary keys, which
|
|
1930
|
-
* re-applies every condition on the Query while acquiring an exclusive
|
|
1931
|
-
* lock on each row it returns. Records a concurrent transaction has
|
|
1932
|
-
* out of the Query since step 1 do not come back, and are the
|
|
1933
|
-
* run lost the race for.
|
|
1934
|
-
*
|
|
1935
|
-
* The
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
* race skips only the records that were lost.
|
|
2009
|
+
* re-applies every condition on the Query while acquiring an exclusive
|
|
2010
|
+
* row lock on each row it returns. Records a concurrent transaction has
|
|
2011
|
+
* moved out of the Query since step 1 do not come back, and are the
|
|
2012
|
+
* records this run lost the race for.
|
|
2013
|
+
*
|
|
2014
|
+
* The claimed records are handed to `perBatch` inside the batch's
|
|
2015
|
+
* transaction; its return value — how many of the claimed records were
|
|
2016
|
+
* processed — accumulates into the returned count.
|
|
2017
|
+
*
|
|
2018
|
+
* The two-step shape is what keeps iteration correct. If the locked read
|
|
2019
|
+
* both claimed rows and advanced the cursor, a batch whose records all lost
|
|
2020
|
+
* the race would come back empty and end the run early, silently skipping
|
|
2021
|
+
* every record after it. Advancing the cursor from step 1 instead means
|
|
2022
|
+
* losing a race skips only the records that were lost.
|
|
1945
2023
|
*
|
|
1946
2024
|
* When the Query already carries a transaction, that transaction is used for
|
|
1947
2025
|
* every batch rather than opening one per batch, so the caller's transaction
|
|
1948
2026
|
* boundary wins and the locks are held until the caller commits.
|
|
1949
2027
|
*/
|
|
1950
|
-
async
|
|
1951
|
-
if (this.joinLoadActivated)
|
|
1952
|
-
throw new LeftJoinPreloadIncompatibleWithFindEach();
|
|
2028
|
+
async lockedBatches(batchSize, perBatch) {
|
|
1953
2029
|
// the driver seam refuses this combination too, but catching it here means
|
|
1954
2030
|
// failing before any transaction is opened or any row is touched
|
|
1955
2031
|
if (this.distinctColumn)
|
|
1956
2032
|
throw new RowLockIncompatibleWithDistinct();
|
|
2033
|
+
// a limit or offset cannot compose with the batch windows: the window
|
|
2034
|
+
// queries would re-apply it per batch — the offset skipping rows in the
|
|
2035
|
+
// candidate pluck and discarding already-claimed rows from the locked
|
|
2036
|
+
// re-read, the limit silently replaced by batchSize
|
|
2037
|
+
if (this.limitStatement || this.offsetStatement)
|
|
2038
|
+
throw new BatchingIncompatibleWithLimitOrOffset();
|
|
1957
2039
|
const primaryKey = this.dreamInstance['_primaryKey'];
|
|
1958
2040
|
const orderedQuery = this.order(null)
|
|
1959
2041
|
.order(this.namespacedPrimaryKey)
|
|
@@ -1968,30 +2050,21 @@ export default class Query {
|
|
|
1968
2050
|
const batchQuery = lastId === undefined
|
|
1969
2051
|
? orderedQuery
|
|
1970
2052
|
: orderedQuery.where({ [primaryKey]: ops.greaterThan(lastId) });
|
|
1971
|
-
const
|
|
2053
|
+
const processBatch = async (txn) => {
|
|
1972
2054
|
const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
|
|
1973
2055
|
if (!candidateIds.length)
|
|
1974
|
-
return { attempted: 0,
|
|
2056
|
+
return { attempted: 0, processed: 0, lastId };
|
|
1975
2057
|
const claimed = await this.dbDriverInstance(batchQuery.txn(txn).where({ [primaryKey]: candidateIds })).takeAll({ lock: true });
|
|
1976
|
-
for (const record of claimed) {
|
|
1977
|
-
const inTransaction = record.txn(txn);
|
|
1978
|
-
if (this.shouldReallyDestroy) {
|
|
1979
|
-
await inTransaction.reallyDestroy(options);
|
|
1980
|
-
}
|
|
1981
|
-
else {
|
|
1982
|
-
await inTransaction.destroy(options);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
2058
|
return {
|
|
1986
2059
|
attempted: candidateIds.length,
|
|
1987
|
-
|
|
2060
|
+
processed: await perBatch(claimed, txn),
|
|
1988
2061
|
lastId: candidateIds.at(-1),
|
|
1989
2062
|
};
|
|
1990
2063
|
};
|
|
1991
2064
|
const results = this.dreamTransaction
|
|
1992
|
-
? await
|
|
1993
|
-
: await this.dreamClass.transaction(async (txn) => await
|
|
1994
|
-
counter += results.
|
|
2065
|
+
? await processBatch(this.dreamTransaction)
|
|
2066
|
+
: await this.dreamClass.transaction(async (txn) => await processBatch(txn));
|
|
2067
|
+
counter += results.processed;
|
|
1995
2068
|
attemptedCount = results.attempted;
|
|
1996
2069
|
lastId = results.lastId;
|
|
1997
2070
|
} while (attemptedCount > 0 && attemptedCount === batchSize);
|
|
@@ -2015,15 +2088,20 @@ export default class Query {
|
|
|
2015
2088
|
* // 12
|
|
2016
2089
|
* ```
|
|
2017
2090
|
*
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
* @param
|
|
2023
|
-
* @param
|
|
2024
|
-
* @param
|
|
2091
|
+
* A `limit` or `offset` on the Query is incompatible with `reallyDestroy`,
|
|
2092
|
+
* exactly as it is with {@link Query.destroy}: a limit- or offset-carrying
|
|
2093
|
+
* Query throws at runtime.
|
|
2094
|
+
*
|
|
2095
|
+
* @param options - Options for destroying the instance
|
|
2096
|
+
* @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
2097
|
+
* @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
2098
|
+
* @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
|
|
2099
|
+
* @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
|
|
2100
|
+
* @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
2101
|
+
* @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
2025
2102
|
* @returns The number of records that were removed
|
|
2026
2103
|
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
2104
|
+
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
2027
2105
|
*/
|
|
2028
2106
|
async reallyDestroy({ skipHooks, cascade, lock, batchSize, } = {}) {
|
|
2029
2107
|
return await this.clone({ shouldReallyDestroy: true }).destroy({ skipHooks, cascade, lock, batchSize });
|
|
@@ -2040,12 +2118,19 @@ export default class Query {
|
|
|
2040
2118
|
* // 12
|
|
2041
2119
|
* ```
|
|
2042
2120
|
*
|
|
2043
|
-
*
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2121
|
+
* A `limit` or `offset` on the Query cannot compose with the batch windows
|
|
2122
|
+
* `undestroy` iterates in — each batch re-applies the Query's conditions,
|
|
2123
|
+
* so the limit would be silently replaced by the batch size and the offset
|
|
2124
|
+
* re-applied to every window — so a limit- or offset-carrying Query throws
|
|
2125
|
+
* at runtime.
|
|
2126
|
+
*
|
|
2127
|
+
* @param options - Options for undestroying the instance
|
|
2128
|
+
* @param options.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false
|
|
2129
|
+
* @param options.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
2130
|
+
* @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade undestroying. Defaults to false
|
|
2131
|
+
* @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade undestroying (soft delete is always bypassed). Defaults to an empty array
|
|
2048
2132
|
* @returns The number of records that were removed
|
|
2133
|
+
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
2049
2134
|
*/
|
|
2050
2135
|
async undestroy({ cascade, skipHooks, } = {}) {
|
|
2051
2136
|
if (!this.dreamClass['softDelete'])
|
|
@@ -2084,25 +2169,120 @@ export default class Query {
|
|
|
2084
2169
|
async delete() {
|
|
2085
2170
|
return await this.dbDriverInstance().delete();
|
|
2086
2171
|
}
|
|
2087
|
-
async update(
|
|
2172
|
+
async update(attributesOrCb, { skipHooks, lock, batchSize } = {}) {
|
|
2088
2173
|
if (this.baseSelectQuery)
|
|
2089
2174
|
throw new NoUpdateOnAssociationQuery();
|
|
2090
2175
|
if (Object.keys(this.innerJoinStatements).length)
|
|
2091
2176
|
throw new NoUpdateAllOnJoins();
|
|
2092
2177
|
if (Object.keys(this.leftJoinStatements).length)
|
|
2093
2178
|
throw new NoUpdateAllOnJoins();
|
|
2094
|
-
|
|
2095
|
-
|
|
2179
|
+
const cb = typeof attributesOrCb === 'function' ? attributesOrCb : undefined;
|
|
2180
|
+
// the overloads already demand an options object with a boolean `lock`
|
|
2181
|
+
// whenever a callback is passed; this backstops callers outside the type
|
|
2182
|
+
// system, so a callback can never silently take (or skip) row locks the
|
|
2183
|
+
// call site did not spell out
|
|
2184
|
+
if (cb && typeof lock !== 'boolean')
|
|
2185
|
+
throw new MissingRequiredLockOptionForUpdateCallback();
|
|
2186
|
+
// resolve the default before validating: `??` would let an explicit 0
|
|
2187
|
+
// through, and `limit(0)` means "no limit", which would turn a batched
|
|
2188
|
+
// update into a single unbounded pass (and, with `lock`, a lock over the
|
|
2189
|
+
// entire matched set)
|
|
2190
|
+
const resolvedBatchSize = batchSize ?? (lock ? Query.BATCH_SIZES.LOCKED_UPDATE : Query.BATCH_SIZES.FIND_EACH);
|
|
2191
|
+
if (!Number.isInteger(resolvedBatchSize) || resolvedBatchSize < 1)
|
|
2192
|
+
throw new InvalidBatchSize(batchSize);
|
|
2193
|
+
// a limit or offset cannot compose with any update path: the batched
|
|
2194
|
+
// paths (findEach and lockedBatches, which also guard) would re-apply it
|
|
2195
|
+
// to every batch window, silently skipping or truncating rows within each
|
|
2196
|
+
// window rather than bounding the run, and the single-statement skipHooks
|
|
2197
|
+
// path's SQL UPDATE cannot express it, so it would be silently ignored
|
|
2198
|
+
if (this.limitStatement || this.offsetStatement)
|
|
2199
|
+
throw new BatchingIncompatibleWithLimitOrOffset();
|
|
2200
|
+
if (lock) {
|
|
2201
|
+
// the attributes form never consumes the records it claims, so its
|
|
2202
|
+
// preloads would be pure wasted queries run while every row in the
|
|
2203
|
+
// batch is exclusively locked; the callback form keeps them, since the
|
|
2204
|
+
// callback may consume the preloaded associations
|
|
2205
|
+
const baseQuery = cb ? this : this.clone({ preloadStatements: {}, preloadOnStatements: {} });
|
|
2206
|
+
return await baseQuery.lockedUpdate(cb
|
|
2207
|
+
? // the callback receives a clone of the claimed record, so that only
|
|
2208
|
+
// the attributes it *returns* are written: the per-instance write is
|
|
2209
|
+
// dirty-state-based, and mutations made directly on the record
|
|
2210
|
+
// Dream holds would otherwise ride along with the save
|
|
2211
|
+
(record) => cb(record['clone']())
|
|
2212
|
+
: () => attributesOrCb, { skipHooks }, resolvedBatchSize);
|
|
2213
|
+
}
|
|
2214
|
+
if (!cb && skipHooks)
|
|
2215
|
+
return await this.updateWithoutCallingModelHooks(attributesOrCb);
|
|
2096
2216
|
let counter = 0;
|
|
2097
2217
|
await this.findEach(async (result) => {
|
|
2218
|
+
// as on the locked path, the callback receives a clone, so only the
|
|
2219
|
+
// attributes it returns are written
|
|
2220
|
+
const attributes = cb
|
|
2221
|
+
? await cb(result['clone']())
|
|
2222
|
+
: attributesOrCb;
|
|
2223
|
+
// null is not part of the callback's advertised contract, but it is
|
|
2224
|
+
// the natural untyped-JS skip idiom; treat it as a skip rather than
|
|
2225
|
+
// letting it crash the per-instance write mid-run
|
|
2226
|
+
if (attributes == null)
|
|
2227
|
+
return;
|
|
2098
2228
|
const subquery = this.dreamTransaction
|
|
2099
2229
|
? result.txn(this.dreamTransaction)
|
|
2100
2230
|
: result;
|
|
2101
|
-
await subquery.update(attributes);
|
|
2231
|
+
await subquery.update(attributes, skipHooks ? { skipHooks } : undefined);
|
|
2102
2232
|
counter++;
|
|
2233
|
+
}, {
|
|
2234
|
+
batchSize: resolvedBatchSize,
|
|
2103
2235
|
});
|
|
2104
2236
|
return counter;
|
|
2105
2237
|
}
|
|
2238
|
+
/**
|
|
2239
|
+
* @internal
|
|
2240
|
+
*
|
|
2241
|
+
* The compare-and-set implementation behind `update(attributes, { lock: true })`,
|
|
2242
|
+
* driven by {@link Query.lockedBatches}, which owns the keyset cursor, the
|
|
2243
|
+
* pluck/claim two-step, and the transaction dispatch.
|
|
2244
|
+
*
|
|
2245
|
+
* Each claimed record is handed to `cb`, which returns the attributes to
|
|
2246
|
+
* write for it, or a nullish value to skip it. The attributes form is the
|
|
2247
|
+
* constant-callback case (a callback ignoring the record and always
|
|
2248
|
+
* returning the same attributes), which is how the two forms share this
|
|
2249
|
+
* implementation. (`update` hands the user's own callback a clone of each
|
|
2250
|
+
* claimed record, so only the returned attributes reach the write below —
|
|
2251
|
+
* see the dispatch in {@link Query.update}.)
|
|
2252
|
+
*
|
|
2253
|
+
* The count is the number of claimed records the callback returned
|
|
2254
|
+
* attributes for: records attempted but not claimed (lost races) never
|
|
2255
|
+
* reach the callback, and claimed records the callback skipped are not
|
|
2256
|
+
* counted. For the attributes form the callback is total, so the count
|
|
2257
|
+
* degenerates to records claimed and submitted for writing. (A record whose
|
|
2258
|
+
* write turns out to be a no-op still counts: the instance path returns
|
|
2259
|
+
* without issuing SQL when nothing is dirty.)
|
|
2260
|
+
*
|
|
2261
|
+
* Each claimed record is written via the ordinary per-instance update, so
|
|
2262
|
+
* hooks (unless `skipHooks`), custom setters, and virtual/encrypted
|
|
2263
|
+
* attribute preprocessing behave exactly as in a plain instance update.
|
|
2264
|
+
*
|
|
2265
|
+
* No join-load guard is needed here: update's own joins guards have already
|
|
2266
|
+
* thrown on every join-load query (leftJoinPreload always carries left-join
|
|
2267
|
+
* statements).
|
|
2268
|
+
*/
|
|
2269
|
+
async lockedUpdate(cb, { skipHooks }, batchSize) {
|
|
2270
|
+
return await this.lockedBatches(batchSize, async (claimed, txn) => {
|
|
2271
|
+
let written = 0;
|
|
2272
|
+
for (const record of claimed) {
|
|
2273
|
+
const attributes = await cb(record);
|
|
2274
|
+
// null is not part of the callback's advertised contract, but it is
|
|
2275
|
+
// the natural untyped-JS skip idiom; treat it as a skip rather than
|
|
2276
|
+
// letting it crash the per-instance write mid-batch
|
|
2277
|
+
if (attributes == null)
|
|
2278
|
+
continue;
|
|
2279
|
+
const inTransaction = record.txn(txn);
|
|
2280
|
+
await inTransaction.update(attributes, skipHooks ? { skipHooks } : undefined);
|
|
2281
|
+
written++;
|
|
2282
|
+
}
|
|
2283
|
+
return written;
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2106
2286
|
async updateWithoutCallingModelHooks(attributes) {
|
|
2107
2287
|
return await this.dbDriverInstance().update(attributes);
|
|
2108
2288
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default class BatchingIncompatibleWithLimitOrOffset extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// batched iteration re-applies the Query's conditions to every batch
|
|
4
|
+
// window, so a limit or offset would be re-applied per batch — skipping or
|
|
5
|
+
// truncating rows inside each window — rather than bounding the run as a
|
|
6
|
+
// whole; and a single-statement set update has no way to express a limit
|
|
7
|
+
// or offset in SQL. Silently clearing them instead would touch rows the
|
|
8
|
+
// caller asked to scope out, so fail loudly before any row is touched.
|
|
9
|
+
return `
|
|
10
|
+
A batched or set-write operation was called on a query carrying a \`limit\`
|
|
11
|
+
or \`offset\`.
|
|
12
|
+
|
|
13
|
+
These operations process the entire matched set, so a limit or offset cannot
|
|
14
|
+
be honored: batched iteration re-applies the query's conditions to every
|
|
15
|
+
batch window, where the limit or offset would be re-applied per batch —
|
|
16
|
+
silently skipping or truncating rows within each window instead of bounding
|
|
17
|
+
the run as a whole — and a single-statement set update cannot express a
|
|
18
|
+
limit or offset in SQL at all.
|
|
19
|
+
|
|
20
|
+
fix:
|
|
21
|
+
remove the \`limit\`/\`offset\` from the query, and scope the affected rows
|
|
22
|
+
with \`where\` conditions instead
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default class InvalidMigrationTableName extends Error {
|
|
2
|
+
migrationName;
|
|
3
|
+
table;
|
|
4
|
+
/**
|
|
5
|
+
* @param migrationName - the migration name the generator was invoked with
|
|
6
|
+
* @param table - the table name the generator resolved for it
|
|
7
|
+
*/
|
|
8
|
+
constructor(migrationName, table) {
|
|
9
|
+
super();
|
|
10
|
+
this.migrationName = migrationName;
|
|
11
|
+
this.table = table;
|
|
12
|
+
}
|
|
13
|
+
get message() {
|
|
14
|
+
return `
|
|
15
|
+
generating the migration '${this.migrationName}' resolved the table name:
|
|
16
|
+
${this.table}
|
|
17
|
+
|
|
18
|
+
which is not a valid table name. The table name is written into the generated
|
|
19
|
+
migration, which 'db:migrate' later executes, so it must be a plain
|
|
20
|
+
identifier: lowercase letters, digits and underscores only.
|
|
21
|
+
|
|
22
|
+
A standalone 'g:migration' takes the table from the text after the first
|
|
23
|
+
'-to-'/'-from-' in the name, snake-cased and pluralized, e.g.:
|
|
24
|
+
add-phone-to-users
|
|
25
|
+
|
|
26
|
+
A model generator takes it from the model name, or verbatim from
|
|
27
|
+
'--table-name'.
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default class MissingRequiredLockOptionForUpdateCallback extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
return `
|
|
4
|
+
When passing a callback to Query#update, the options object and its \`lock\`
|
|
5
|
+
key are required, and \`lock\` must be an explicit boolean:
|
|
6
|
+
|
|
7
|
+
await User.query().update(user => ({ ... }), { lock: true })
|
|
8
|
+
await User.query().update(user => ({ ... }), { lock: false })
|
|
9
|
+
|
|
10
|
+
\`lock: true\` runs the callback on each record after re-selecting it under an
|
|
11
|
+
exclusive row lock (a compare-then-write). \`lock: false\` actively opts out of
|
|
12
|
+
locking: the callback runs on each record with no lock and no compare-and-set
|
|
13
|
+
guarantee. Requiring the boolean keeps that choice visible at the call site.
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
}
|