@rvoh/dream 2.20.0 → 2.22.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/Dream.js +3 -0
- package/dist/cjs/src/cli/index.js +7 -4
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/cjs/src/dream/Query.js +203 -4
- package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/cjs/src/dream/internal/destroyDream.js +6 -7
- package/dist/cjs/src/errors/InvalidBatchSize.js +19 -0
- package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +17 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/cjs/src/errors/UnparseableMigrationColumn.js +16 -0
- package/dist/cjs/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
- package/dist/cjs/src/helpers/cli/generateMigration.js +20 -3
- package/dist/cjs/src/helpers/cli/generateMigrationContent.js +74 -9
- package/dist/cjs/src/ops/index.js +19 -0
- package/dist/esm/src/Dream.js +3 -0
- package/dist/esm/src/cli/index.js +7 -4
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/esm/src/dream/Query.js +203 -4
- package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/esm/src/dream/internal/destroyDream.js +6 -7
- package/dist/esm/src/errors/InvalidBatchSize.js +19 -0
- package/dist/esm/src/errors/NoColumnsToAlterMigration.js +17 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/esm/src/errors/UnparseableMigrationColumn.js +16 -0
- package/dist/esm/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
- package/dist/esm/src/helpers/cli/generateMigration.js +20 -3
- package/dist/esm/src/helpers/cli/generateMigrationContent.js +74 -9
- package/dist/esm/src/ops/index.js +19 -0
- package/dist/types/src/Dream.d.ts +3 -0
- package/dist/types/src/cli/index.d.ts +1 -1
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -0
- package/dist/types/src/dream/Query.d.ts +140 -3
- package/dist/types/src/dream/QueryDriver/Base.d.ts +21 -0
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +20 -0
- package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +22 -3
- package/dist/types/src/errors/InvalidBatchSize.d.ts +5 -0
- package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +6 -0
- package/dist/types/src/errors/RowLockIncompatibleWithDistinct.d.ts +3 -0
- package/dist/types/src/errors/RowLockIncompatibleWithJoinLoad.d.ts +3 -0
- package/dist/types/src/errors/UnparseableMigrationColumn.d.ts +5 -0
- package/dist/types/src/errors/ops/ArrayEqualityRequiresArrayColumn.d.ts +8 -0
- package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +11 -1
- package/dist/types/src/ops/index.d.ts +19 -0
- package/dist/types/src/types/associations/shared.d.ts +2 -2
- package/dist/types/src/types/dream.d.ts +8 -0
- package/dist/types/src/types/types/associations/shared.ts +84 -55
- package/dist/types/src/types/types/dream.ts +30 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +55 -41
- package/docs/classes/db.PostgresQueryDriver.html +55 -42
- package/docs/classes/db.QueryDriverBase.html +54 -40
- 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 +127 -125
- 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 +161 -71
- 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/hierarchy.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 +38 -23
- 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
package/dist/cjs/src/Dream.js
CHANGED
|
@@ -1330,6 +1330,9 @@ export default class Dream {
|
|
|
1330
1330
|
* // User{email: 'goodbye@world'}
|
|
1331
1331
|
* ```
|
|
1332
1332
|
*
|
|
1333
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
1334
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
1335
|
+
*
|
|
1333
1336
|
* @param cb - The callback to call for each found record
|
|
1334
1337
|
* @param opts - Optional parameters for batch processing
|
|
1335
1338
|
* @param opts.batchSize - the batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -45,6 +45,8 @@ ${INDENT} - timetz
|
|
|
45
45
|
${INDENT} - timetz[]
|
|
46
46
|
${INDENT} - integer
|
|
47
47
|
${INDENT} - integer[]
|
|
48
|
+
${INDENT} - boolean
|
|
49
|
+
${INDENT} - boolean[]
|
|
48
50
|
${INDENT}
|
|
49
51
|
${INDENT} - decimal:
|
|
50
52
|
${INDENT} - decimal[]:
|
|
@@ -153,16 +155,17 @@ ${INDENT} # Add columns to an existing table (suffix with -to-<table_name> for
|
|
|
153
155
|
${INDENT} pnpm psy g:migration add-timezone-to-users timezone:string
|
|
154
156
|
${INDENT} pnpm psy g:migration add-bio-to-users bio:text:optional avatar_url:string:optional
|
|
155
157
|
${INDENT}
|
|
156
|
-
${INDENT} # Remove columns (suffix with -from-<table_name
|
|
157
|
-
${INDENT}
|
|
158
|
+
${INDENT} # Remove columns (suffix with -from-<table_name>; columns are described with
|
|
159
|
+
${INDENT} # their types, same shorthand as -to-, so the rollback (down) can recreate them)
|
|
160
|
+
${INDENT} pnpm psy g:migration remove-legacy-fields-from-posts legacy_status:string
|
|
158
161
|
${INDENT}
|
|
159
162
|
${INDENT} # General schema change (no table suffix — generates empty up/down methods)
|
|
160
163
|
${INDENT} pnpm psy g:migration create-unique-index-on-invitations`)
|
|
161
164
|
.argument('<migrationName>', `Kebab-case name describing the change. End with -to-<table_name> or -from-<table_name> to auto-generate an alterTable scaffold for that table.
|
|
162
165
|
${INDENT}
|
|
163
166
|
${INDENT}Examples:
|
|
164
|
-
${INDENT} add-phone-to-users
|
|
165
|
-
${INDENT} remove-status-from-posts
|
|
167
|
+
${INDENT} add-phone-to-users phone:encrypted # scaffolds alterTable('users', ...)
|
|
168
|
+
${INDENT} remove-status-from-posts status:string # scaffolds alterTable('posts', ...)
|
|
166
169
|
${INDENT} create-join-table-host-places # empty migration (no table suffix match)`)
|
|
167
170
|
.option('--connection-name <connectionName>', 'the database connection to use for this migration. Only needed for multi-database setups; defaults to "default"')
|
|
168
171
|
.argument('[columnsWithTypes...]', columnsWithTypesDescriptionForMigration)
|
|
@@ -421,6 +421,9 @@ export default class DreamClassTransactionBuilder {
|
|
|
421
421
|
* // User{email: 'goodbye@world'}
|
|
422
422
|
* ```
|
|
423
423
|
*
|
|
424
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
425
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
426
|
+
*
|
|
424
427
|
* @param cb - The callback to call for each found record
|
|
425
428
|
* @param options.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
426
429
|
* @returns void
|
|
@@ -3,7 +3,9 @@ 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 InvalidBatchSize from '../errors/InvalidBatchSize.js';
|
|
6
7
|
import LeftJoinPreloadIncompatibleWithFindEach from '../errors/LeftJoinPreloadIncompatibleWithFindEach.js';
|
|
8
|
+
import RowLockIncompatibleWithDistinct from '../errors/RowLockIncompatibleWithDistinct.js';
|
|
7
9
|
import MissingRequiredCallbackFunctionToPluckEach from '../errors/MissingRequiredCallbackFunctionToPluckEach.js';
|
|
8
10
|
import NoUpdateAllOnJoins from '../errors/NoUpdateAllOnJoins.js';
|
|
9
11
|
import NoUpdateOnAssociationQuery from '../errors/NoUpdateOnAssociationQuery.js';
|
|
@@ -31,6 +33,10 @@ export default class Query {
|
|
|
31
33
|
*/
|
|
32
34
|
static BATCH_SIZES = {
|
|
33
35
|
FIND_EACH: 1000,
|
|
36
|
+
// deliberately small: every record in a locked destroy batch stays
|
|
37
|
+
// row-locked for the whole batch, so an oversized batch blocks unrelated
|
|
38
|
+
// writers. See Query#destroy's `lock` option.
|
|
39
|
+
LOCKED_DESTROY: 10,
|
|
34
40
|
PLUCK_EACH: 10000,
|
|
35
41
|
PLUCK_EACH_THROUGH: 1000,
|
|
36
42
|
};
|
|
@@ -398,13 +404,30 @@ export default class Query {
|
|
|
398
404
|
* records matching the Query have been fetched.
|
|
399
405
|
*
|
|
400
406
|
* ```ts
|
|
401
|
-
* await User.
|
|
407
|
+
* await User.where({ deletedAt: null }).findEach(user => {
|
|
402
408
|
* DreamApp.log(user)
|
|
403
409
|
* })
|
|
404
410
|
* // User{id: 1}
|
|
405
411
|
* // User{id: 2}
|
|
406
412
|
* ```
|
|
407
413
|
*
|
|
414
|
+
* IMPORTANT: `findEach` always iterates in ascending primary key order.
|
|
415
|
+
* Any `order` applied to the Query is discarded; passing one has no effect
|
|
416
|
+
* on the order records are visited in.
|
|
417
|
+
*
|
|
418
|
+
* This is not a limitation that can be lifted. `findEach` guarantees that
|
|
419
|
+
* every matching record is visited exactly once, and it delivers that by
|
|
420
|
+
* paginating on a keyset: each batch selects the next `batchSize` records
|
|
421
|
+
* with a primary key greater than the last record of the previous batch.
|
|
422
|
+
* That guarantee only holds over a stable, unique key. Ordering by a
|
|
423
|
+
* mutable, non-unique column would mean a record whose sort value changed
|
|
424
|
+
* partway through the iteration could be skipped entirely or visited twice.
|
|
425
|
+
* ({@link Query.cursorPaginate} can honor an arbitrary order precisely
|
|
426
|
+
* because a paginated UI tolerates that anomaly; a full traversal cannot.)
|
|
427
|
+
*
|
|
428
|
+
* If you need records processed in a particular order, load them with a
|
|
429
|
+
* regular ordered query instead of `findEach`.
|
|
430
|
+
*
|
|
408
431
|
* @param cb - The callback to call for each found record
|
|
409
432
|
* @param __namedParameters - Options for batch processing
|
|
410
433
|
* @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -1741,21 +1764,109 @@ export default class Query {
|
|
|
1741
1764
|
* // 12
|
|
1742
1765
|
* ```
|
|
1743
1766
|
*
|
|
1767
|
+
* ## Guarded (compare-and-set) destroy
|
|
1768
|
+
*
|
|
1769
|
+
* By default, `destroy` reads the matching records and then destroys each of
|
|
1770
|
+
* them, so a concurrent writer can change a record in between — the record is
|
|
1771
|
+
* destroyed even though it no longer matches the Query that selected it.
|
|
1772
|
+
* Passing `lock: true` closes that window, making `destroy` a compare-and-set
|
|
1773
|
+
* the way `update(..., { skipHooks: true })` already is:
|
|
1774
|
+
*
|
|
1775
|
+
* ```ts
|
|
1776
|
+
* const destroyed = await Order.where({ status: 'pending' }).destroy({ lock: true })
|
|
1777
|
+
* // 0 — someone else moved every one of them out of 'pending' first
|
|
1778
|
+
* ```
|
|
1779
|
+
*
|
|
1780
|
+
* With `lock: true`, records are processed in batches, and each batch runs in
|
|
1781
|
+
* its own transaction which first re-selects that batch's records with an
|
|
1782
|
+
* exclusive row lock and then destroys them before committing. Because the
|
|
1783
|
+
* database re-checks the Query's conditions after acquiring each row lock,
|
|
1784
|
+
* a record a concurrent transaction has already moved out of the Query drops
|
|
1785
|
+
* out of the locked read and is never destroyed. The returned count is the
|
|
1786
|
+
* number of records actually claimed, so a caller can detect that it lost a
|
|
1787
|
+
* race by comparing the count against what it expected.
|
|
1788
|
+
*
|
|
1789
|
+
* Things to know before relying on it:
|
|
1790
|
+
*
|
|
1791
|
+
* - **The guarantee is per batch, not set-wide.** A run spanning more than one
|
|
1792
|
+
* batch can win a race in batch 1 and lose one in batch 2; nothing holds the
|
|
1793
|
+
* whole matched set still for the duration of the run. Sharing one
|
|
1794
|
+
* transaction across every batch — by threading a transaction onto the Query
|
|
1795
|
+
* itself, `Order.query().txn(txn).where({ ... }).destroy({ lock: true })` —
|
|
1796
|
+
* buys two things: the destroys become all-or-nothing (a rollback undoes the
|
|
1797
|
+
* whole run), and the row locks taken by every batch are held until you
|
|
1798
|
+
* commit. It does **not** extend the compare-and-set to the part of the set
|
|
1799
|
+
* the run has not reached yet: under READ COMMITTED each statement takes a
|
|
1800
|
+
* fresh snapshot, so a record moved out of the Query between batch 1 and
|
|
1801
|
+
* batch 2 still drops out of batch 2's locked read. A genuinely set-wide
|
|
1802
|
+
* guarantee needs REPEATABLE READ / SERIALIZABLE, or a single locking
|
|
1803
|
+
* statement over the whole set.
|
|
1804
|
+
* - **Only a transaction carried by the Query is shared.** Merely wrapping the
|
|
1805
|
+
* call in `Model.transaction(...)` does not thread that transaction onto the
|
|
1806
|
+
* Query — `Dream.transaction` establishes no ambient transaction, it only
|
|
1807
|
+
* hands a `txn` to your callback — so each batch would open its own
|
|
1808
|
+
* transaction on a different pooled connection. Worse, if the surrounding
|
|
1809
|
+
* transaction has already written to any of the matched rows, each batch's
|
|
1810
|
+
* locked read blocks on locks the surrounding transaction holds while that
|
|
1811
|
+
* transaction awaits the destroy: a cycle across two connections, which
|
|
1812
|
+
* Postgres's deadlock detector cannot see, so it hangs rather than aborting.
|
|
1813
|
+
* Always pass the transaction with `.txn(txn)`.
|
|
1814
|
+
* - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result
|
|
1815
|
+
* behavior described above is READ COMMITTED (Postgres's default). Under
|
|
1816
|
+
* REPEATABLE READ or SERIALIZABLE, the same race raises a serialization
|
|
1817
|
+
* failure instead of silently returning a lower count. Both are safe; only
|
|
1818
|
+
* one of them is quiet.
|
|
1819
|
+
* - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the
|
|
1820
|
+
* default is deliberately small. Every row in a batch stays locked for the
|
|
1821
|
+
* whole batch, including the time spent running each record's hooks and
|
|
1822
|
+
* `dependent: 'destroy'` cascade, so an oversized batch blocks unrelated
|
|
1823
|
+
* writers touching those rows and surfaces as timeouts somewhere else in the
|
|
1824
|
+
* application, which is very hard to trace back to this destroy. Too small a
|
|
1825
|
+
* batch, by contrast, only slows down this call, which you can see and fix.
|
|
1826
|
+
* Lower it further for models with deep `dependent: 'destroy'` cascades or
|
|
1827
|
+
* expensive destroy hooks.
|
|
1828
|
+
* - **A batch waits, without bound, for whoever holds the row.** The locked
|
|
1829
|
+
* read is a plain `FOR UPDATE`: if another transaction already holds a lock
|
|
1830
|
+
* on one of the batch's rows, this call blocks until that transaction ends,
|
|
1831
|
+
* holding its own transaction and its pooled connection open the whole
|
|
1832
|
+
* time. Dream sets no `lock_timeout` or `statement_timeout`, so a
|
|
1833
|
+
* long-running writer on the other side stalls the run indefinitely and, at
|
|
1834
|
+
* scale, ties up connections. If that is a risk in your application, set a
|
|
1835
|
+
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1836
|
+
* hanging.
|
|
1837
|
+
* - **If you are destroying a large set and do not need compare-and-set, do
|
|
1838
|
+
* not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
|
|
1839
|
+
* {@link Query.delete | delete} (a single statement, no hooks or cascade)
|
|
1840
|
+
* are the right tools for bulk removal.
|
|
1841
|
+
*
|
|
1744
1842
|
* @param __namedParameters - Options for destroying the instance
|
|
1745
1843
|
* @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1746
1844
|
* @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1845
|
+
* @param __namedParameters.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
|
|
1846
|
+
* @param __namedParameters.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
|
|
1747
1847
|
* @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1748
1848
|
* @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1749
1849
|
* @returns The number of records that were removed
|
|
1850
|
+
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1750
1851
|
*/
|
|
1751
|
-
async destroy({ skipHooks, cascade, } = {}) {
|
|
1852
|
+
async destroy({ skipHooks, cascade, lock, batchSize, } = {}) {
|
|
1752
1853
|
let counter = 0;
|
|
1854
|
+
// resolve the default before validating: `??` would let an explicit 0
|
|
1855
|
+
// through, and `limit(0)` means "no limit", which would turn a batched
|
|
1856
|
+
// destroy into a single unbounded pass (and, with `lock`, a lock over the
|
|
1857
|
+
// entire matched set)
|
|
1858
|
+
const resolvedBatchSize = batchSize ?? (lock ? Query.BATCH_SIZES.LOCKED_DESTROY : Query.BATCH_SIZES.FIND_EACH);
|
|
1859
|
+
if (!Number.isInteger(resolvedBatchSize) || resolvedBatchSize < 1)
|
|
1860
|
+
throw new InvalidBatchSize(batchSize);
|
|
1753
1861
|
const options = {
|
|
1754
1862
|
bypassAllDefaultScopes: this.bypassAllDefaultScopes,
|
|
1755
1863
|
defaultScopesToBypass: this.defaultScopesToBypass,
|
|
1756
1864
|
skipHooks,
|
|
1757
1865
|
cascade,
|
|
1758
1866
|
};
|
|
1867
|
+
if (lock) {
|
|
1868
|
+
return await this.lockedDestroy(options, resolvedBatchSize);
|
|
1869
|
+
}
|
|
1759
1870
|
await this.findEach(async (result) => {
|
|
1760
1871
|
const subquery = this.dreamTransaction
|
|
1761
1872
|
? result.txn(this.dreamTransaction)
|
|
@@ -1767,9 +1878,94 @@ export default class Query {
|
|
|
1767
1878
|
await subquery.destroy(options);
|
|
1768
1879
|
}
|
|
1769
1880
|
counter++;
|
|
1881
|
+
}, {
|
|
1882
|
+
batchSize: resolvedBatchSize,
|
|
1770
1883
|
});
|
|
1771
1884
|
return counter;
|
|
1772
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* @internal
|
|
1888
|
+
*
|
|
1889
|
+
* The compare-and-set implementation behind `destroy({ lock: true })`.
|
|
1890
|
+
*
|
|
1891
|
+
* Each batch is handled in two steps inside one transaction:
|
|
1892
|
+
*
|
|
1893
|
+
* 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
|
|
1894
|
+
* primary key order, which establishes the batch's window (its keyset
|
|
1895
|
+
* cursor) and tells us how many records were *attempted*. Only the keys are
|
|
1896
|
+
* read: hydrating and preloading these records would duplicate the work
|
|
1897
|
+
* step 2 does over the same rows; then
|
|
1898
|
+
* 2. a locked re-read restricted to exactly those primary keys, which
|
|
1899
|
+
* re-applies every condition on the Query while acquiring an exclusive row
|
|
1900
|
+
* lock on each row it returns. Records a concurrent transaction has moved
|
|
1901
|
+
* out of the Query since step 1 do not come back, and are the records this
|
|
1902
|
+
* run lost the race for.
|
|
1903
|
+
*
|
|
1904
|
+
* The count comes from step 2's result set, which is why it reports records
|
|
1905
|
+
* actually claimed rather than records attempted. (One thing it does not
|
|
1906
|
+
* account for, because it never has: a `beforeDestroy` hook that sets
|
|
1907
|
+
* `_preventDeletion` leaves the record in place but still counts.)
|
|
1908
|
+
*
|
|
1909
|
+
* The two-step shape is what keeps iteration correct. If the locked read both
|
|
1910
|
+
* claimed rows and advanced the cursor, a batch whose records all lost the
|
|
1911
|
+
* race would come back empty and end the run early, silently skipping every
|
|
1912
|
+
* record after it. Advancing the cursor from step 1 instead means losing a
|
|
1913
|
+
* race skips only the records that were lost.
|
|
1914
|
+
*
|
|
1915
|
+
* When the Query already carries a transaction, that transaction is used for
|
|
1916
|
+
* every batch rather than opening one per batch, so the caller's transaction
|
|
1917
|
+
* boundary wins and the locks are held until the caller commits.
|
|
1918
|
+
*/
|
|
1919
|
+
async lockedDestroy(options, batchSize) {
|
|
1920
|
+
if (this.joinLoadActivated)
|
|
1921
|
+
throw new LeftJoinPreloadIncompatibleWithFindEach();
|
|
1922
|
+
// the driver seam refuses this combination too, but catching it here means
|
|
1923
|
+
// failing before any transaction is opened or any row is touched
|
|
1924
|
+
if (this.distinctColumn)
|
|
1925
|
+
throw new RowLockIncompatibleWithDistinct();
|
|
1926
|
+
const primaryKey = this.dreamInstance['_primaryKey'];
|
|
1927
|
+
const orderedQuery = this.order(null)
|
|
1928
|
+
.order(this.namespacedPrimaryKey)
|
|
1929
|
+
.limit(batchSize);
|
|
1930
|
+
let counter = 0;
|
|
1931
|
+
// the cursor is compared against a sentinel rather than tested for
|
|
1932
|
+
// truthiness, since a primary key of 0 is a legitimate cursor value that
|
|
1933
|
+
// would otherwise reset the window to the start of the set
|
|
1934
|
+
let lastId = undefined;
|
|
1935
|
+
let attemptedCount = 0;
|
|
1936
|
+
do {
|
|
1937
|
+
const batchQuery = lastId === undefined
|
|
1938
|
+
? orderedQuery
|
|
1939
|
+
: orderedQuery.where({ [primaryKey]: ops.greaterThan(lastId) });
|
|
1940
|
+
const destroyBatch = async (txn) => {
|
|
1941
|
+
const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
|
|
1942
|
+
if (!candidateIds.length)
|
|
1943
|
+
return { attempted: 0, claimed: 0, lastId };
|
|
1944
|
+
const claimed = await this.dbDriverInstance(batchQuery.txn(txn).where({ [primaryKey]: candidateIds })).takeAll({ lock: true });
|
|
1945
|
+
for (const record of claimed) {
|
|
1946
|
+
const inTransaction = record.txn(txn);
|
|
1947
|
+
if (this.shouldReallyDestroy) {
|
|
1948
|
+
await inTransaction.reallyDestroy(options);
|
|
1949
|
+
}
|
|
1950
|
+
else {
|
|
1951
|
+
await inTransaction.destroy(options);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
attempted: candidateIds.length,
|
|
1956
|
+
claimed: claimed.length,
|
|
1957
|
+
lastId: candidateIds.at(-1),
|
|
1958
|
+
};
|
|
1959
|
+
};
|
|
1960
|
+
const results = this.dreamTransaction
|
|
1961
|
+
? await destroyBatch(this.dreamTransaction)
|
|
1962
|
+
: await this.dreamClass.transaction(async (txn) => await destroyBatch(txn));
|
|
1963
|
+
counter += results.claimed;
|
|
1964
|
+
attemptedCount = results.attempted;
|
|
1965
|
+
lastId = results.lastId;
|
|
1966
|
+
} while (attemptedCount > 0 && attemptedCount === batchSize);
|
|
1967
|
+
return counter;
|
|
1968
|
+
}
|
|
1773
1969
|
/**
|
|
1774
1970
|
* Destroys all records matching the Query,
|
|
1775
1971
|
* ignoring the SoftDelete decorator and
|
|
@@ -1791,12 +1987,15 @@ export default class Query {
|
|
|
1791
1987
|
* @param __namedParameters - Options for destroying the instance
|
|
1792
1988
|
* @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1793
1989
|
* @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1990
|
+
* @param __namedParameters.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
|
|
1991
|
+
* @param __namedParameters.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
|
|
1794
1992
|
* @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1795
1993
|
* @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1796
1994
|
* @returns The number of records that were removed
|
|
1995
|
+
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1797
1996
|
*/
|
|
1798
|
-
async reallyDestroy({ skipHooks, cascade, } = {}) {
|
|
1799
|
-
return await this.clone({ shouldReallyDestroy: true }).destroy({ skipHooks, cascade });
|
|
1997
|
+
async reallyDestroy({ skipHooks, cascade, lock, batchSize, } = {}) {
|
|
1998
|
+
return await this.clone({ shouldReallyDestroy: true }).destroy({ skipHooks, cascade, lock, batchSize });
|
|
1800
1999
|
}
|
|
1801
2000
|
/**
|
|
1802
2001
|
* Undestroys a SoftDelete model, unsetting
|
|
@@ -256,6 +256,32 @@ export default class QueryDriverBase {
|
|
|
256
256
|
options = {}) {
|
|
257
257
|
throw new Error('implement takeAll in child class');
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* @internal
|
|
261
|
+
*
|
|
262
|
+
* Adapter seam for exclusive row locking. Given the select statement about to
|
|
263
|
+
* be executed, return a statement that additionally acquires an exclusive
|
|
264
|
+
* lock on each row it returns, held until the enclosing transaction commits
|
|
265
|
+
* or rolls back (Postgres: `SELECT ... FOR UPDATE OF <table>`).
|
|
266
|
+
*
|
|
267
|
+
* This is only ever reached when a caller opts into a locked read (today,
|
|
268
|
+
* `Query#destroy({ lock: true })`), and only from inside a transaction. The
|
|
269
|
+
* base implementation throws so that an adapter which has not implemented row
|
|
270
|
+
* locking fails loud rather than silently performing an unlocked read and
|
|
271
|
+
* quietly dropping the compare-and-set guarantee the caller asked for.
|
|
272
|
+
*
|
|
273
|
+
* @param kyselyQuery - the select statement to lock
|
|
274
|
+
* @param tableAlias - the alias of the Query's base table; only rows of that
|
|
275
|
+
* table are locked, so a joined query does not lock unrelated rows and does
|
|
276
|
+
* not fail on the nullable side of an outer join
|
|
277
|
+
*/
|
|
278
|
+
applyRowLock(
|
|
279
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
280
|
+
kyselyQuery,
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
282
|
+
tableAlias) {
|
|
283
|
+
throw new Error('implement applyRowLock in child class');
|
|
284
|
+
}
|
|
259
285
|
/**
|
|
260
286
|
* Retrieves the max value of the specified column
|
|
261
287
|
* for this Query
|
|
@@ -29,6 +29,9 @@ import MissingThroughAssociationSource from '../../errors/associations/MissingTh
|
|
|
29
29
|
import CannotNegateSimilarityClause from '../../errors/CannotNegateSimilarityClause.js';
|
|
30
30
|
import CannotNamespaceAssociationFilterToAnotherTable from '../../errors/CannotNamespaceAssociationFilterToAnotherTable.js';
|
|
31
31
|
import CannotPassUndefinedAsAValueToAWhereClause from '../../errors/CannotPassUndefinedAsAValueToAWhereClause.js';
|
|
32
|
+
import ArrayEqualityRequiresArrayColumn from '../../errors/ops/ArrayEqualityRequiresArrayColumn.js';
|
|
33
|
+
import RowLockIncompatibleWithDistinct from '../../errors/RowLockIncompatibleWithDistinct.js';
|
|
34
|
+
import RowLockIncompatibleWithJoinLoad from '../../errors/RowLockIncompatibleWithJoinLoad.js';
|
|
32
35
|
import CheckConstraintViolation from '../../errors/db/CheckConstraintViolation.js';
|
|
33
36
|
import ColumnOverflow from '../../errors/db/ColumnOverflow.js';
|
|
34
37
|
import DataTypeColumnTypeMismatch from '../../errors/db/DataTypeColumnTypeMismatch.js';
|
|
@@ -39,6 +42,7 @@ import ASTSchemaBuilder from '../../helpers/cli/ASTSchemaBuilder.js';
|
|
|
39
42
|
import generateMigration from '../../helpers/cli/generateMigration.js';
|
|
40
43
|
import compact from '../../helpers/compact.js';
|
|
41
44
|
import { normalizeDataForDb } from '../../helpers/db/normalizeDataForDb.js';
|
|
45
|
+
import isDatabaseArrayColumn from '../../helpers/db/types/isDatabaseArrayColumn.js';
|
|
42
46
|
import EnvInternal from '../../helpers/EnvInternal.js';
|
|
43
47
|
import groupBy from '../../helpers/groupBy.js';
|
|
44
48
|
import isEmpty from '../../helpers/isEmpty.js';
|
|
@@ -412,9 +416,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
412
416
|
* @returns an array of dreams
|
|
413
417
|
*/
|
|
414
418
|
async takeAll(options = {}) {
|
|
415
|
-
if (this.query['joinLoadActivated'])
|
|
419
|
+
if (this.query['joinLoadActivated']) {
|
|
420
|
+
// executeJoinLoad builds its own select and has nowhere to put a row
|
|
421
|
+
// lock, so a locked read here has to fail rather than quietly return
|
|
422
|
+
// unlocked rows and drop the guarantee the caller asked for
|
|
423
|
+
if (options.lock)
|
|
424
|
+
throw new RowLockIncompatibleWithJoinLoad();
|
|
416
425
|
return await this.executeJoinLoad(options);
|
|
417
|
-
|
|
426
|
+
}
|
|
427
|
+
// `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database, so
|
|
428
|
+
// fail loudly here instead of emitting SQL that cannot run
|
|
429
|
+
if (options.lock && this.query['distinctColumn'])
|
|
430
|
+
throw new RowLockIncompatibleWithDistinct();
|
|
431
|
+
let kyselyQuery = this.buildSelect(options);
|
|
432
|
+
// row locking is adapter-specific, so it goes through the query driver seam
|
|
433
|
+
if (options.lock)
|
|
434
|
+
kyselyQuery = this.applyRowLock(kyselyQuery, this.query['baseSqlAlias']);
|
|
418
435
|
const results = await executeDatabaseQuery(kyselyQuery, 'execute');
|
|
419
436
|
const theAll = results.map(r => this.dbResultToDreamInstance(r, this.dreamClass));
|
|
420
437
|
await this.applyPreload(this.query['preloadStatements'], this.query['preloadOnStatements'], theAll);
|
|
@@ -1484,8 +1501,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
1484
1501
|
b = '=';
|
|
1485
1502
|
c = val;
|
|
1486
1503
|
}
|
|
1487
|
-
c = this.normalizedWhereValue(dreamClass, attr, c);
|
|
1488
|
-
c2 = this.normalizedWhereValue(dreamClass, attr, c2);
|
|
1504
|
+
c = maybeBindArrayAsSingleValue(dreamClass, a, b, this.normalizedWhereValue(dreamClass, attr, c));
|
|
1505
|
+
c2 = maybeBindArrayAsSingleValue(dreamClass, a2, b2, this.normalizedWhereValue(dreamClass, attr, c2));
|
|
1489
1506
|
if (a && c === undefined)
|
|
1490
1507
|
throw new CannotPassUndefinedAsAValueToAWhereClause(this.dreamClass, a);
|
|
1491
1508
|
if (a2 && c2 === undefined)
|
|
@@ -2525,6 +2542,31 @@ export function resolvePostgresSsl(connectionConf) {
|
|
|
2525
2542
|
return { rejectUnauthorized: false };
|
|
2526
2543
|
return false;
|
|
2527
2544
|
}
|
|
2545
|
+
const EQUALITY_OPERATORS_TAKING_A_WHOLE_ARRAY_VALUE = ['=', '!=', '<>'];
|
|
2546
|
+
/**
|
|
2547
|
+
* Kysely renders a raw Javascript array passed as the right-hand side of a
|
|
2548
|
+
* comparison as a value list (`($1, $2)`), which is what `in` / `not in` need,
|
|
2549
|
+
* but which Postgres rejects for `=` against an array column
|
|
2550
|
+
* (`operator does not exist: text[] = record`). For the equality operators,
|
|
2551
|
+
* bind the array as a single parameter so that `ops.equal([...])` against an
|
|
2552
|
+
* array column means literal array equality (`"col" = $1`).
|
|
2553
|
+
*
|
|
2554
|
+
* The rewrite is gated on the column actually being a database array column.
|
|
2555
|
+
* An array reaching an equality operator against a *scalar* column is a
|
|
2556
|
+
* mistake, and binding it as a single value would turn that mistake into a
|
|
2557
|
+
* clause that matches nothing (`"name" = '{}'`) instead of one that raises, so
|
|
2558
|
+
* it throws instead.
|
|
2559
|
+
*/
|
|
2560
|
+
function maybeBindArrayAsSingleValue(dreamClass, column, operator, val) {
|
|
2561
|
+
if (!Array.isArray(val))
|
|
2562
|
+
return val;
|
|
2563
|
+
if (!EQUALITY_OPERATORS_TAKING_A_WHOLE_ARRAY_VALUE.includes(operator))
|
|
2564
|
+
return val;
|
|
2565
|
+
const isolatedColumn = maybeNamespacedColumnNameToColumnName(column ?? '');
|
|
2566
|
+
if (!isDatabaseArrayColumn(dreamClass, isolatedColumn))
|
|
2567
|
+
throw new ArrayEqualityRequiresArrayColumn(dreamClass, isolatedColumn, operator);
|
|
2568
|
+
return sql.val(val);
|
|
2569
|
+
}
|
|
2528
2570
|
function shouldSoftDelete(dream, reallyDestroy) {
|
|
2529
2571
|
const dreamClass = dream.constructor;
|
|
2530
2572
|
return dreamClass['softDelete'] && !reallyDestroy;
|
|
@@ -198,6 +198,27 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
|
|
|
198
198
|
await client.end();
|
|
199
199
|
DreamCLI.logger.logEndProgress();
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
*
|
|
204
|
+
* Postgres implementation of the row-locking seam: appends
|
|
205
|
+
* `FOR UPDATE OF "<tableAlias>"` to the select, so every row the statement
|
|
206
|
+
* returns is exclusively locked until the enclosing transaction ends.
|
|
207
|
+
*
|
|
208
|
+
* Scoping the lock to the Query's own base table (rather than a bare
|
|
209
|
+
* `FOR UPDATE`) means a query that joins other tables neither locks rows the
|
|
210
|
+
* caller never asked about nor raises Postgres's "FOR UPDATE cannot be
|
|
211
|
+
* applied to the nullable side of an outer join".
|
|
212
|
+
*
|
|
213
|
+
* This is a waiting lock: neither `NOWAIT` nor `SKIP LOCKED` is applied, so a
|
|
214
|
+
* statement that collides with another transaction's lock blocks until that
|
|
215
|
+
* transaction ends. Callers that cannot afford an unbounded wait should set a
|
|
216
|
+
* `lock_timeout` on the connection; see the guarded-destroy notes on
|
|
217
|
+
* `Query#destroy`.
|
|
218
|
+
*/
|
|
219
|
+
applyRowLock(kyselyQuery, tableAlias) {
|
|
220
|
+
return kyselyQuery.forUpdate(tableAlias);
|
|
221
|
+
}
|
|
201
222
|
static supportsParallelTestDatabases = true;
|
|
202
223
|
/**
|
|
203
224
|
* @internal
|
|
@@ -1,20 +1,69 @@
|
|
|
1
|
+
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Marks a Dream instance as one whose `dependent: 'destroy'` associations were
|
|
6
|
+
* hydrated by this cascade's own call to `loadDependentDestroyTree`, using the
|
|
7
|
+
* same destroy options the cascade is running under.
|
|
8
|
+
*
|
|
9
|
+
* Associations a caller happened to preload themselves (e.g.
|
|
10
|
+
* `Post.preload('comments')`) are deliberately NOT trusted: they may have been
|
|
11
|
+
* loaded under different default scopes than the destroy requires — most
|
|
12
|
+
* importantly, `reallyDestroy` strips the soft-delete scope, so a
|
|
13
|
+
* caller-preloaded association could be missing soft-deleted records that the
|
|
14
|
+
* cascade must destroy.
|
|
15
|
+
*/
|
|
16
|
+
const CASCADE_LOADED = Symbol.for('dream:dependentDestroyTreeLoaded');
|
|
1
17
|
/**
|
|
2
18
|
* @internal
|
|
3
19
|
*
|
|
4
20
|
* Destroys all HasOne/HasMany associations on this
|
|
5
21
|
* dream that are marked as `dependent: 'destroy'`.
|
|
6
22
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
23
|
+
* The whole dependent-destroy tree is loaded ONCE, at the root of the destroy
|
|
24
|
+
* operation, by `loadDependentDestroyTree`. Descendants reached by recursing
|
|
25
|
+
* through this function consume those already-hydrated associations rather
|
|
26
|
+
* than re-querying their own subtrees.
|
|
27
|
+
*
|
|
28
|
+
* A load still happens here when an association is not hydrated. That covers
|
|
29
|
+
* two cases:
|
|
30
|
+
*
|
|
31
|
+
* 1. the root of the operation, whose associations have not been loaded yet
|
|
32
|
+
* 2. a node past `RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS` levels of
|
|
33
|
+
* self-reference, which the root's preload could not reach
|
|
34
|
+
*
|
|
35
|
+
* The second case is why the check is per-association-hydration rather than a
|
|
36
|
+
* "descendants are already loaded" flag: such a flag would truncate cascades
|
|
37
|
+
* deeper than the preload horizon.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: reusing a hydrated subtree is only sound because the same `options`
|
|
40
|
+
* are forwarded unchanged all the way down the cascade (see the recursive
|
|
41
|
+
* `destroy`/`reallyDestroy` calls below), so every node's associations were
|
|
42
|
+
* loaded under the scopes this node would have used. Nothing in the types
|
|
43
|
+
* enforces that; if `options` ever start varying by depth, this reuse has to
|
|
44
|
+
* be revisited.
|
|
10
45
|
*/
|
|
11
46
|
export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
12
47
|
const dreamClass = dream.constructor;
|
|
13
48
|
const { reallyDestroy } = options;
|
|
49
|
+
// `loadDependentDestroyTree` hydrates and returns a CLONE rather than
|
|
50
|
+
// mutating the receiver, so the result must be assigned back — otherwise
|
|
51
|
+
// each association in the loop below would trigger its own reload.
|
|
52
|
+
let dreamWithAssociations = dream;
|
|
53
|
+
let alreadyLoadedTree = false;
|
|
14
54
|
for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
|
|
15
|
-
|
|
55
|
+
if (!alreadyLoadedTree && !cascadeLoaded(dreamWithAssociations, associationName)) {
|
|
56
|
+
dreamWithAssociations = await loadDependentDestroyTree(dreamWithAssociations, txn, {
|
|
57
|
+
reallyDestroy,
|
|
58
|
+
bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
|
|
59
|
+
defaultScopesToBypass: options.defaultScopesToBypass ?? [],
|
|
60
|
+
});
|
|
61
|
+
alreadyLoadedTree = true;
|
|
62
|
+
}
|
|
63
|
+
const loaded = dreamWithAssociations[associationName];
|
|
16
64
|
const records = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
17
65
|
for (const record of records) {
|
|
66
|
+
markCascadeLoaded(record);
|
|
18
67
|
if (reallyDestroy) {
|
|
19
68
|
await record.txn(txn).reallyDestroy(options);
|
|
20
69
|
}
|
|
@@ -24,3 +73,26 @@ export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
|
24
73
|
}
|
|
25
74
|
}
|
|
26
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
*
|
|
79
|
+
* True when this dream was hydrated by this cascade's own preload AND the
|
|
80
|
+
* given association is hydrated on it.
|
|
81
|
+
*/
|
|
82
|
+
function cascadeLoaded(dream, associationName) {
|
|
83
|
+
if (!dream[CASCADE_LOADED])
|
|
84
|
+
return false;
|
|
85
|
+
return dream.loaded(associationName);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
*
|
|
90
|
+
* A plain assignment rather than `Object.defineProperty`: the key is a symbol,
|
|
91
|
+
* so it is already excluded from `Object.keys`, `for...in` and
|
|
92
|
+
* `JSON.stringify` without an explicit descriptor, and this runs once per node
|
|
93
|
+
* of a potentially wide cascade tree.
|
|
94
|
+
*/
|
|
95
|
+
function markCascadeLoaded(dream) {
|
|
96
|
+
;
|
|
97
|
+
dream[CASCADE_LOADED] = true;
|
|
98
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import DreamApp from '../../dream-app/index.js';
|
|
2
2
|
import destroyAssociatedRecords from './destroyAssociatedRecords.js';
|
|
3
|
-
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
4
3
|
import runHooksFor from './runHooksFor.js';
|
|
5
4
|
/**
|
|
6
5
|
* @internal
|
|
@@ -31,12 +30,12 @@ export default async function destroyDream(dream, txn = null, options) {
|
|
|
31
30
|
async function destroyDreamWithTransaction(dream, txn, options) {
|
|
32
31
|
const { cascade, reallyDestroy, skipHooks } = options;
|
|
33
32
|
if (cascade) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await destroyAssociatedRecords(
|
|
33
|
+
// NOTE: the dependent-destroy tree is loaded lazily, inside
|
|
34
|
+
// destroyAssociatedRecords, so that a cascaded descendant whose
|
|
35
|
+
// associations the root already preloaded does not re-query its own
|
|
36
|
+
// subtree. `dream` itself is intentionally never replaced by the loaded
|
|
37
|
+
// clone: the hooks and the delete below operate on the original.
|
|
38
|
+
await destroyAssociatedRecords(dream, txn, options);
|
|
40
39
|
}
|
|
41
40
|
if (!skipHooks) {
|
|
42
41
|
await runHooksFor('beforeDestroy', dream, true, null, txn);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default class InvalidBatchSize extends Error {
|
|
2
|
+
batchSize;
|
|
3
|
+
constructor(batchSize) {
|
|
4
|
+
super();
|
|
5
|
+
this.batchSize = batchSize;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
Invalid batchSize provided.
|
|
10
|
+
|
|
11
|
+
batchSize given:
|
|
12
|
+
${String(this.batchSize)}
|
|
13
|
+
|
|
14
|
+
batchSize must be a positive integer. A batchSize of 0 is especially dangerous,
|
|
15
|
+
since a limit of 0 is treated as "no limit", which would collapse a batched
|
|
16
|
+
operation into a single unbounded pass over the entire matched set.
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default class NoColumnsToAlterMigration extends Error {
|
|
2
|
+
table;
|
|
3
|
+
alterDirection;
|
|
4
|
+
constructor(table, alterDirection) {
|
|
5
|
+
super();
|
|
6
|
+
this.table = table;
|
|
7
|
+
this.alterDirection = alterDirection;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
return `
|
|
11
|
+
no valid columns to ${this.alterDirection === 'remove' ? 'drop from' : 'add to'} table '${this.table}' in this alter migration.
|
|
12
|
+
|
|
13
|
+
Pass at least one column declaration with a resolvable type, e.g.:
|
|
14
|
+
name:string
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
}
|