@rvoh/dream 2.31.0 → 2.32.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 +9 -5
- package/dist/cjs/src/db/errors.js +2 -0
- package/dist/cjs/src/decorators/Decorators.js +45 -5
- package/dist/cjs/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.js +51 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +5 -2
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +39 -25
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableCascadeEdge.js +32 -0
- package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +7 -1
- package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +7 -0
- package/dist/cjs/src/dream/internal/destroyDream.js +25 -14
- package/dist/cjs/src/dream/internal/undestroyDream.js +55 -32
- package/dist/cjs/src/dream/internal/withConcurrentWriterRetry.js +64 -0
- package/dist/esm/src/Dream.js +9 -5
- package/dist/esm/src/db/errors.js +2 -0
- package/dist/esm/src/decorators/Decorators.js +45 -5
- package/dist/esm/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.js +51 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +5 -2
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +39 -25
- package/dist/esm/src/decorators/field/sortable/helpers/sortableCascadeEdge.js +32 -0
- package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
- package/dist/esm/src/dream/QueryDriver/Postgres.js +7 -1
- package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +7 -0
- package/dist/esm/src/dream/internal/destroyDream.js +25 -14
- package/dist/esm/src/dream/internal/undestroyDream.js +55 -32
- package/dist/esm/src/dream/internal/withConcurrentWriterRetry.js +64 -0
- package/dist/types/src/Dream.d.ts +9 -5
- package/dist/types/src/db/errors.d.ts +2 -0
- package/dist/types/src/decorators/Decorators.d.ts +45 -5
- package/dist/types/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.d.ts +39 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +6 -1
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +37 -20
- package/dist/types/src/decorators/field/sortable/helpers/sortableCascadeEdge.d.ts +20 -0
- package/dist/types/src/dream/QueryDriver/Base.d.ts +20 -0
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +2 -0
- package/dist/types/src/dream/internal/destroyDream.d.ts +3 -1
- package/dist/types/src/dream/internal/undestroyDream.d.ts +6 -4
- package/dist/types/src/dream/internal/withConcurrentWriterRetry.d.ts +47 -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 -43
- package/docs/classes/db.PostgresQueryDriver.html +56 -44
- package/docs/classes/db.QueryDriverBase.html +54 -42
- package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
- 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.SortableScopeDidNotStabilize.html +3 -3
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.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 +62 -24
- package/docs/classes/index.Dream.html +133 -129
- 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 +65 -65
- 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 +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 +2 -2
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { SOFT_DELETE_SCOPE_NAME } from '../../decorators/class/SoftDelete.js';
|
|
1
2
|
import acquireStabilizedSortableScopeLocks from '../../decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js';
|
|
2
3
|
import filterQueryToScopeValues from '../../decorators/field/sortable/helpers/filterQueryToScopeValues.js';
|
|
3
|
-
import {
|
|
4
|
+
import { consumeSortableCascadeEdge, markSortableCascadeEdge, } from '../../decorators/field/sortable/helpers/sortableCascadeEdge.js';
|
|
5
|
+
import { readSortableSnapshots, snapshotScopeValue, } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
|
|
6
|
+
import associationUpdateQuery from './associations/associationUpdateQuery.js';
|
|
4
7
|
import runHooksFor from './runHooksFor.js';
|
|
8
|
+
import withConcurrentWriterRetry from './withConcurrentWriterRetry.js';
|
|
5
9
|
/**
|
|
6
10
|
* @internal
|
|
7
11
|
*
|
|
8
|
-
*
|
|
12
|
+
* Undestroys the Dream and any `dependent: 'destroy'` associations
|
|
9
13
|
* within a transaction. If a transaction is passed, it will be used.
|
|
10
|
-
* Otherwise, a new transaction will be created automatically
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* Otherwise, a new transaction will be created automatically, and the
|
|
15
|
+
* whole undestroy is run again if that transaction is undone by a
|
|
16
|
+
* concurrent writer (see `withConcurrentWriterRetry`).
|
|
17
|
+
* If any of the nested associations fails to undestroy, then this
|
|
18
|
+
* record will also fail to undestroy. If skipHooks is true, model hooks
|
|
13
19
|
* will be bypassed.
|
|
14
20
|
*/
|
|
15
21
|
export default async function undestroyDream(dream, txn = null, options) {
|
|
@@ -18,33 +24,35 @@ export default async function undestroyDream(dream, txn = null, options) {
|
|
|
18
24
|
}
|
|
19
25
|
else {
|
|
20
26
|
const dreamClass = dream.constructor;
|
|
21
|
-
return await dreamClass
|
|
27
|
+
return await withConcurrentWriterRetry(dreamClass, async (txn) => await undestroyDreamWithTransaction(dream, txn, options));
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* @internal
|
|
26
32
|
*
|
|
27
|
-
* Given a transaction, applies the
|
|
33
|
+
* Given a transaction, applies the undestroy query,
|
|
28
34
|
* including cascading to child associations and
|
|
29
35
|
* model hooks.
|
|
30
36
|
*/
|
|
31
37
|
async function undestroyDreamWithTransaction(dream, txn, options) {
|
|
32
38
|
const { cascade, skipHooks } = options;
|
|
39
|
+
// Whether a `dependent: 'destroy'` cascade reached this record, which decides
|
|
40
|
+
// whether its restore takes the scope lock. Consumed first, before a hook or
|
|
41
|
+
// the cascade below can re-enter this function for the same instance.
|
|
42
|
+
const cascaded = consumeSortableCascadeEdge(dream) !== null;
|
|
33
43
|
if (!skipHooks) {
|
|
34
44
|
await runHooksFor('beforeUpdate', dream, true, null, txn);
|
|
35
45
|
}
|
|
36
|
-
// The cascade runs ahead of the restore, matching `destroyDream`:
|
|
37
|
-
// record's own scope lock
|
|
38
|
-
// seated after it would hold that lock across every descendant's
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// class (`undestroyOptions`), so it reaches the children while this record is
|
|
43
|
-
// still deleted.
|
|
46
|
+
// The cascade runs ahead of the restore, matching `destroyDream`: a direct
|
|
47
|
+
// undestroy takes this record's own scope lock inside `doUndestroyDream`, so
|
|
48
|
+
// a cascade seated after it would hold that lock across every descendant's
|
|
49
|
+
// undestroy. The nested select the cascade builds bypasses the soft-delete
|
|
50
|
+
// scope on this record's own class (`undestroyOptions`), so it reaches the
|
|
51
|
+
// children while this record is still deleted.
|
|
44
52
|
if (cascade) {
|
|
45
53
|
await undestroyAssociatedRecords(dream, txn, options);
|
|
46
54
|
}
|
|
47
|
-
const restoredRowCount = await doUndestroyDream(dream, txn);
|
|
55
|
+
const restoredRowCount = await doUndestroyDream(dream, txn, { lock: !cascaded });
|
|
48
56
|
// A restore of a row that is not deleted — a second `undestroy()`, a job
|
|
49
57
|
// retry, or a record that was never destroyed, which `Query#undestroy` reaches
|
|
50
58
|
// routinely since it lifts the soft-delete scope and walks every match —
|
|
@@ -68,9 +76,16 @@ async function undestroyDreamWithTransaction(dream, txn, options) {
|
|
|
68
76
|
* is computed from, and would move one past itself on every repeat — a
|
|
69
77
|
* permanent gap in the scope.
|
|
70
78
|
*
|
|
79
|
+
* @param lock - whether to take each sortable field's scope lock around the
|
|
80
|
+
* position computation. A direct undestroy does; a cascaded one does not,
|
|
81
|
+
* since a cascade would otherwise hold one lock per distinct sort scope it
|
|
82
|
+
* restores into until the root transaction commits. Without the lock the
|
|
83
|
+
* `max(position) + 1` below can race a concurrent writer of the scope onto the
|
|
84
|
+
* same position, which the scope's deferrable unique constraint refuses at
|
|
85
|
+
* COMMIT; see `withConcurrentWriterRetry`.
|
|
71
86
|
* @returns the number of rows restored: 1, or 0 when the row was not deleted
|
|
72
87
|
*/
|
|
73
|
-
async function doUndestroyDream(dream, txn) {
|
|
88
|
+
async function doUndestroyDream(dream, txn, { lock }) {
|
|
74
89
|
const updateStatement = txn.kyselyTransaction.updateTable(dream.table);
|
|
75
90
|
let query = updateStatement
|
|
76
91
|
.where(dream['_primaryKey'], '=', dream.primaryKeyValue())
|
|
@@ -79,16 +94,16 @@ async function doUndestroyDream(dream, txn) {
|
|
|
79
94
|
const dreamClass = dream.constructor;
|
|
80
95
|
const sortableFields = (dreamClass['sortableFields'] ?? []);
|
|
81
96
|
// Undestroy has no sortable hook of its own — the position is recomputed
|
|
82
|
-
// inline, below, before `afterUpdate` ever runs — so
|
|
83
|
-
// here, around that recomputation.
|
|
84
|
-
// subquery races every other writer of the scope the record is being restored
|
|
85
|
-
// into.
|
|
97
|
+
// inline, below, before `afterUpdate` ever runs — so a direct undestroy takes
|
|
98
|
+
// the scope lock here, around that recomputation.
|
|
86
99
|
//
|
|
87
100
|
// The scope this record is being restored into is the one its row physically
|
|
88
101
|
// carries, not the one the instance was loaded with: another writer may have
|
|
89
|
-
// moved it between scopes while it was soft deleted
|
|
90
|
-
//
|
|
91
|
-
const { snapshots } =
|
|
102
|
+
// moved it between scopes while it was soft deleted. The stabilized
|
|
103
|
+
// acquisition converges on that scope; the unlocked read simply reads it.
|
|
104
|
+
const { snapshots } = lock
|
|
105
|
+
? await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields)
|
|
106
|
+
: await readSortableSnapshots(dream, txn, sortableFields);
|
|
92
107
|
sortableFields.forEach(sortableFieldMetadata => {
|
|
93
108
|
const positionColumn = sortableFieldMetadata.positionField;
|
|
94
109
|
const snapshot = snapshots.get(positionColumn);
|
|
@@ -113,22 +128,30 @@ async function doUndestroyDream(dream, txn) {
|
|
|
113
128
|
/**
|
|
114
129
|
* @internal
|
|
115
130
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
131
|
+
* Undestroys all HasOne/HasMany associations on this dream that are marked as
|
|
132
|
+
* `dependent: 'destroy'`: the walk `Query#undestroy` makes, with each record
|
|
133
|
+
* marked as reached by this cascade before its own undestroy runs, so that its
|
|
134
|
+
* restore takes no scope lock.
|
|
118
135
|
*/
|
|
119
136
|
async function undestroyAssociatedRecords(dream, txn, options) {
|
|
120
137
|
const dreamClass = dream.constructor;
|
|
121
138
|
for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
|
|
122
|
-
const
|
|
123
|
-
const associatedClass =
|
|
139
|
+
const association = dreamClass['associationMetadataMap']()[associationName];
|
|
140
|
+
const associatedClass = association?.modelCB?.();
|
|
124
141
|
if (Array.isArray(associatedClass)) {
|
|
125
142
|
// TODO: decide how to handle polymorphic associations with dependent: destroy
|
|
126
143
|
// raise?
|
|
127
144
|
}
|
|
128
|
-
else {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
else if (association && associatedClass?.['softDelete']) {
|
|
146
|
+
const query = associationUpdateQuery(dream, txn, associationName, {
|
|
147
|
+
joinAndStatements: {},
|
|
148
|
+
bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
|
|
149
|
+
defaultScopesToBypass: (options.defaultScopesToBypass ?? []),
|
|
150
|
+
});
|
|
151
|
+
await query.removeDefaultScope(SOFT_DELETE_SCOPE_NAME).findEach(async (record) => {
|
|
152
|
+
markSortableCascadeEdge(record, association);
|
|
153
|
+
await record.txn(txn).undestroy(options);
|
|
154
|
+
});
|
|
132
155
|
}
|
|
133
156
|
}
|
|
134
157
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import DreamApp from '../../dream-app/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* How many times a destroy or undestroy that opened its own transaction runs
|
|
4
|
+
* before a failure caused by a concurrent writer escapes. One such failure
|
|
5
|
+
* needs another writer of the same rows or sort scope during the operation;
|
|
6
|
+
* three in a row is contention past the point of retrying.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*
|
|
12
|
+
* Runs `callback` in a transaction of the model's own, and runs it again when
|
|
13
|
+
* that transaction is undone by a concurrent writer: a deadlock, or a unique
|
|
14
|
+
* constraint refusing the COMMIT.
|
|
15
|
+
*
|
|
16
|
+
* A cascaded destroy or undestroy does its sortable position work without the
|
|
17
|
+
* scope lock a direct one takes (see `@deco.Sortable`), so a concurrent writer
|
|
18
|
+
* of the same sort scope can leave two live rows sharing a position, which the
|
|
19
|
+
* scope's deferrable unique constraint refuses at COMMIT — or, when both
|
|
20
|
+
* writers reach COMMIT together, as a deadlock between their two checks. A
|
|
21
|
+
* cascade holding row locks can deadlock against another writer of the same
|
|
22
|
+
* rows mid-statement as well. Either way the database has undone the whole
|
|
23
|
+
* transaction, nothing has committed and no commit hook has run, so running
|
|
24
|
+
* the operation again reads the rows as the other writer left them. The last
|
|
25
|
+
* attempt's error is the adapter's own.
|
|
26
|
+
*
|
|
27
|
+
* A unique violation raised by a statement inside the callback is not a
|
|
28
|
+
* collision but a write that violates a constraint outright, and is not
|
|
29
|
+
* retried; only a refusal of the COMMIT itself is, which is why the callback
|
|
30
|
+
* having resolved is part of the test.
|
|
31
|
+
*
|
|
32
|
+
* Only a transaction Dream opens is retried. Inside a caller's transaction the
|
|
33
|
+
* failure is the caller's, and the caller retries the whole transaction.
|
|
34
|
+
*
|
|
35
|
+
* Saves are deliberately not run through this. When a direct save and a
|
|
36
|
+
* cascade collide, the loser is whichever transaction waits first at COMMIT,
|
|
37
|
+
* and that is usually the save, which is short and reaches COMMIT while the
|
|
38
|
+
* cascade is still working: it fails with the adapter's deadlock error after
|
|
39
|
+
* `deadlock_timeout`. That bounded, occasional failure is the accepted price of
|
|
40
|
+
* cascades holding no scope locks, and the save path's state restore
|
|
41
|
+
* (`saveDream`) already makes calling `save` again correct. The collision needs
|
|
42
|
+
* a position write into the same sort scope inside the cascade's window that
|
|
43
|
+
* picks the same position, which a cascade destroying a scope's owner never
|
|
44
|
+
* exposes since it writes no position at all.
|
|
45
|
+
*/
|
|
46
|
+
export default async function withConcurrentWriterRetry(dreamClass, callback) {
|
|
47
|
+
const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(dreamClass.prototype.connectionName);
|
|
48
|
+
for (let attempt = 1;; attempt++) {
|
|
49
|
+
let callbackResolved = false;
|
|
50
|
+
try {
|
|
51
|
+
return await dreamClass.transaction(async (txn) => {
|
|
52
|
+
const result = await callback(txn);
|
|
53
|
+
callbackResolved = true;
|
|
54
|
+
return result;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const undoneByConcurrentWriter = queryDriverClass.isDeadlock(error) ||
|
|
59
|
+
(callbackResolved && queryDriverClass.isUniqueConstraintViolation(error));
|
|
60
|
+
if (!undoneByConcurrentWriter || attempt === MAX_CONCURRENT_WRITER_ATTEMPTS)
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1488,16 +1488,20 @@ export default class Dream {
|
|
|
1488
1488
|
* with no gaps, accounting for the scopes specified in the
|
|
1489
1489
|
* corresponding Sortable decorator.
|
|
1490
1490
|
*
|
|
1491
|
-
* Each sort scope is renumbered in its own transaction, under the
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
* destroy cannot make the renumbering land on positions it never read.
|
|
1491
|
+
* Each sort scope is renumbered in its own transaction, under the advisory
|
|
1492
|
+
* lock every direct position-mutating path takes on that scope, from a read
|
|
1493
|
+
* of the scope taken inside that lock — so a concurrent create or direct
|
|
1494
|
+
* destroy cannot make the renumbering land on positions it never read. A
|
|
1495
|
+
* cascaded destroy or undestroy takes no scope lock (see `@deco.Sortable`),
|
|
1496
|
+
* so one running at the same moment can still change a scope under `resort`;
|
|
1497
|
+
* a later `resort` repairs whatever that leaves.
|
|
1495
1498
|
*
|
|
1496
1499
|
* **Do not call `resort` from inside a transaction you own.** It opens those
|
|
1497
1500
|
* transactions itself, on connections of its own, so it can neither join
|
|
1498
1501
|
* yours nor roll back with it: its writes commit on their own. Worse, if your
|
|
1499
1502
|
* transaction already holds the scope lock for a scope being renumbered — any
|
|
1500
|
-
* position-mutating write to that scope earlier in the transaction
|
|
1503
|
+
* direct position-mutating write to that scope earlier in the transaction
|
|
1504
|
+
* takes it; a cascaded destroy or undestroy takes none —
|
|
1501
1505
|
* `resort` waits on that lock from the outside, where no deadlock detector
|
|
1502
1506
|
* can see the cycle, until the wait bound expires and it throws
|
|
1503
1507
|
* `SortableScopeLockWaitTimedOut`.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const CHECK_VIOLATION = "CHECK_VIOLATION";
|
|
2
|
+
export declare const DEADLOCK_DETECTED = "DEADLOCK_DETECTED";
|
|
2
3
|
export declare const FOREIGN_KEY_VIOLATION = "FOREIGN_KEY_VIOLATION";
|
|
3
4
|
export declare const INTEGRITY_CONSTRAINT_VIOLATION = "INTEGRITY_CONSTRAINT_VIOLATION";
|
|
4
5
|
export declare const INVALID_INPUT_SYNTAX = "INVALID_INPUT_SYNTAX";
|
|
@@ -19,6 +20,7 @@ export declare const PG_ERRORS: {
|
|
|
19
20
|
readonly '23001': "RESTRICT_VIOLATION";
|
|
20
21
|
readonly '23503': "FOREIGN_KEY_VIOLATION";
|
|
21
22
|
readonly '55P03': "LOCK_NOT_AVAILABLE";
|
|
23
|
+
readonly '40P01': "DEADLOCK_DETECTED";
|
|
22
24
|
};
|
|
23
25
|
type PgErrorType = (typeof PG_ERRORS)[keyof typeof PG_ERRORS];
|
|
24
26
|
export declare function pgErrorType(error: any): PgErrorType | null;
|
|
@@ -65,7 +65,10 @@ export default class Decorators<TD extends typeof Dream, T extends Dream = Insta
|
|
|
65
65
|
/**
|
|
66
66
|
* Marks an integer column as a sortable position: Dream keeps the positions of
|
|
67
67
|
* every record in a sort scope contiguous, starting at 1, as records are
|
|
68
|
-
* created, moved, destroyed and undestroyed.
|
|
68
|
+
* created, moved, destroyed and undestroyed. A concurrent write racing a
|
|
69
|
+
* cascaded destroy or undestroy, under **Cascaded destroy and undestroy**
|
|
70
|
+
* below, is the one thing that can leave a scope with a gap;
|
|
71
|
+
* `Model.resort('position')` closes it.
|
|
69
72
|
*
|
|
70
73
|
* ```ts
|
|
71
74
|
* class Post extends ApplicationModel {
|
|
@@ -92,8 +95,9 @@ export default class Decorators<TD extends typeof Dream, T extends Dream = Insta
|
|
|
92
95
|
* then `await post.update({ position: 1 })`.
|
|
93
96
|
*
|
|
94
97
|
* Sortable requires a query driver that supports advisory transaction locks —
|
|
95
|
-
* the `PostgresQueryDriver` does — since
|
|
96
|
-
* writers of its sort scope on one
|
|
98
|
+
* the `PostgresQueryDriver` does — since a position write serializes the
|
|
99
|
+
* writers of its sort scope on one, apart from the cascades described below.
|
|
100
|
+
* This concurrency guarantee first shipped
|
|
97
101
|
* in Dream 2.28.0 and begins only after every writer is running a lock-aware
|
|
98
102
|
* release. During the first rolling deployment, older processes take no
|
|
99
103
|
* advisory locks and can still race the upgraded processes.
|
|
@@ -101,8 +105,10 @@ export default class Decorators<TD extends typeof Dream, T extends Dream = Insta
|
|
|
101
105
|
* All participating Dream writers of one hot scope serialize. Ordinary saves
|
|
102
106
|
* and destroys with `skipHooks`, direct query writes, raw SQL, and older
|
|
103
107
|
* pre-lock Dream processes bypass Sortable maintenance and do not participate
|
|
104
|
-
* in its locking protocol
|
|
105
|
-
* maintenance
|
|
108
|
+
* in its locking protocol; a cascaded destroy or undestroy performs its
|
|
109
|
+
* maintenance but does not participate either. A direct undestroy still
|
|
110
|
+
* performs stabilized Sortable maintenance and acquires scope locks with
|
|
111
|
+
* `skipHooks: true`; locked query
|
|
106
112
|
* batches likewise acquire their scope locks during preflight, before any
|
|
107
113
|
* per-record callbacks, even when those callbacks skip hooks. A waiter that
|
|
108
114
|
* enters the protocol holds a pooled connection until the holder finishes or
|
|
@@ -111,6 +117,40 @@ export default class Decorators<TD extends typeof Dream, T extends Dream = Insta
|
|
|
111
117
|
* inside the lock window short, and avoid cross-region database latency for
|
|
112
118
|
* hot scopes.
|
|
113
119
|
*
|
|
120
|
+
* **Cascaded destroy and undestroy take no scope lock.** A record destroyed or
|
|
121
|
+
* undestroyed because its owner was — reached through a `dependent: 'destroy'`
|
|
122
|
+
* association, or through the undestroy that restores one — does its position
|
|
123
|
+
* work without the advisory lock, so a cascade holds no lock per sort scope it
|
|
124
|
+
* touches, however many scopes that is. Calling `destroy()` or `undestroy()`
|
|
125
|
+
* on a record yourself is unchanged. Where the cascade is destroying the owner
|
|
126
|
+
* of a sort scope — a `HasMany` whose foreign key is one of the field's scope
|
|
127
|
+
* columns, with no condition and not `through`, not polymorphic, whose target
|
|
128
|
+
* is not an STI child and declares no default scope other than
|
|
129
|
+
* `@deco.SoftDelete` — every row of that scope goes with it, and the cascade
|
|
130
|
+
* neither reads nor compacts it. Any other cascaded destroy compacts the
|
|
131
|
+
* scope it vacates, and every cascaded undestroy appends at the end of its
|
|
132
|
+
* scope, exactly as the direct operations do, only without the lock.
|
|
133
|
+
*
|
|
134
|
+
* Without the lock, a writer of the same sort scope during the cascade — a
|
|
135
|
+
* create, a move, a destroy, a `resort` — is not kept out, which costs one of
|
|
136
|
+
* two things. A gap: positions stay unique and correctly ordered, and
|
|
137
|
+
* `Model.resort('position')` closes it. Or two live rows sharing a position,
|
|
138
|
+
* which the deferrable unique constraint refuses when the later of the two
|
|
139
|
+
* transactions reaches COMMIT — as a unique violation, or as a deadlock when
|
|
140
|
+
* both reach it together — so nothing half-done commits and no commit hook
|
|
141
|
+
* runs. A `destroy()` or `undestroy()` whose transaction Dream opened runs the
|
|
142
|
+
* whole operation again on such a refusal, up to three times, before letting
|
|
143
|
+
* the adapter's error escape. Inside a transaction you opened, that error is
|
|
144
|
+
* your COMMIT failing: retry the transaction from the beginning, as for a
|
|
145
|
+
* deadlock. The other writer can lose the same race and fail at its own
|
|
146
|
+
* COMMIT instead, an ordinary `create` or `update` included, and Dream does
|
|
147
|
+
* not run those again: a bounded, occasional failed request in one contended
|
|
148
|
+
* scope is the deliberate price of cascades that hold no locks, where the
|
|
149
|
+
* lock count would otherwise be set by the data. A sort scope with a
|
|
150
|
+
* nullable column has no such backstop unless its constraint is declared
|
|
151
|
+
* `NULLS NOT DISTINCT`, since a plain unique constraint never sees two NULLs
|
|
152
|
+
* as equal.
|
|
153
|
+
*
|
|
114
154
|
* `SortableScopeDidNotStabilize` and `SortableScopeLockWaitTimedOut` are the
|
|
115
155
|
* expected Dream errors an application may choose to retry. Database deadlocks
|
|
116
156
|
* remain native adapter errors (for example PostgreSQL code `40P01` or MySQL
|
package/dist/types/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Dream from '../../../../Dream.js';
|
|
2
|
+
import { SortableFieldConfig } from '../Sortable.js';
|
|
3
|
+
import { SortableCascadeEdge } from './sortableCascadeEdge.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* Whether a cascade reaching this record through `edge` reaches, and so
|
|
8
|
+
* destroys, every live row of the sortable field's sort scope. Such a scope is
|
|
9
|
+
* left with no survivor, so
|
|
10
|
+
* the compaction a destroy performs would close a vacancy nothing can observe,
|
|
11
|
+
* and the cascade skips it along with the snapshot read it needs. Every clause
|
|
12
|
+
* fails closed: a shape not positively recognized here compacts as a direct
|
|
13
|
+
* destroy does, unlocked, which costs statements and never a lock.
|
|
14
|
+
*
|
|
15
|
+
* The edge is the `dependent: 'destroy'` association the cascade destroyed
|
|
16
|
+
* through, the only kind the caller passes, and the answer is about that edge
|
|
17
|
+
* alone: another dependent edge of the same owner may reach the rows this one
|
|
18
|
+
* does not, and this fails closed rather than look for it.
|
|
19
|
+
*
|
|
20
|
+
* 1. The edge is a `HasMany`. A `HasOne` reaches one row of a scope that may
|
|
21
|
+
* hold others.
|
|
22
|
+
* 2. The edge is not polymorphic, is not a `through` association, and carries
|
|
23
|
+
* no `and`, `andNot`, `andAny`, `selfAnd` or `selfAndNot`. Each of those
|
|
24
|
+
* reaches a subset of the rows sharing the foreign key, and the rest can
|
|
25
|
+
* stay live: a soft-deleted owner's row remains, so no foreign key forces
|
|
26
|
+
* them out, and they keep their positions in a scope the cascade did not
|
|
27
|
+
* empty. (A hard destroy under a database foreign key would have refused to
|
|
28
|
+
* leave them, and compacting there is the harmless direction to be wrong in.)
|
|
29
|
+
* 3. The target is not an STI child, whose rows share a sort scope with its
|
|
30
|
+
* siblings' rows.
|
|
31
|
+
* 4. The target declares no default scope other than SoftDelete, since any
|
|
32
|
+
* other hides rows from the cascade's own load. A soft-deleted row holds no
|
|
33
|
+
* position and so is in no sort scope to begin with.
|
|
34
|
+
* 5. The edge's foreign key is one of the field's scope columns: every row the
|
|
35
|
+
* cascade destroys carries the owner's key, and so does every row of any sort
|
|
36
|
+
* scope that includes that column. Further scope members only partition that
|
|
37
|
+
* same set.
|
|
38
|
+
*/
|
|
39
|
+
export default function cascadeReachesEveryRowInSortScope(dream: Dream, config: SortableFieldConfig, edge: SortableCascadeEdge): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Dream from '../../../../Dream.js';
|
|
2
2
|
import DreamTransaction from '../../../../dream/DreamTransaction.js';
|
|
3
|
+
import { SortableFieldConfig } from '../Sortable.js';
|
|
3
4
|
/**
|
|
4
5
|
* @internal
|
|
5
6
|
*
|
|
@@ -10,5 +11,9 @@ import DreamTransaction from '../../../../dream/DreamTransaction.js';
|
|
|
10
11
|
* `afterDestroy` hook — never registered as a hook among them — so every
|
|
11
12
|
* after-destroy hook observes the compacted scope regardless of where it was
|
|
12
13
|
* declared, exactly as every after-save hook observes the computed position.
|
|
14
|
+
*
|
|
15
|
+
* @param sortableFields - the fields the preparation phase read a snapshot for,
|
|
16
|
+
* which on a cascaded destroy excludes every field whose whole sort scope the
|
|
17
|
+
* cascade is destroying
|
|
13
18
|
*/
|
|
14
|
-
export default function performSortableDestroyWork(dream: Dream, txn: DreamTransaction<any
|
|
19
|
+
export default function performSortableDestroyWork(dream: Dream, txn: DreamTransaction<any>, sortableFields: SortableFieldConfig[]): Promise<void>;
|
package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts
CHANGED
|
@@ -1,33 +1,50 @@
|
|
|
1
1
|
import Dream from '../../../../Dream.js';
|
|
2
2
|
import DreamTransaction from '../../../../dream/DreamTransaction.js';
|
|
3
|
+
import { SortableFieldConfig } from '../Sortable.js';
|
|
4
|
+
import { SortableCascadeEdge } from './sortableCascadeEdge.js';
|
|
5
|
+
export interface SortableDestroyPreparation {
|
|
6
|
+
/**
|
|
7
|
+
* False when the snapshot read found no row: this destroy has nothing to
|
|
8
|
+
* vacate, and compacting from the instance's remembered position would shift
|
|
9
|
+
* a scope some other writer has already closed. The delete's own affected-row
|
|
10
|
+
* count is the stronger signal (a writer outside the sortable path can remove
|
|
11
|
+
* the row after this read), so the caller checks both.
|
|
12
|
+
*/
|
|
13
|
+
rowExists: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The sortable fields whose scope this destroy compacts, for
|
|
16
|
+
* `performSortableDestroyWork`.
|
|
17
|
+
*/
|
|
18
|
+
sortableFields: SortableFieldConfig[];
|
|
19
|
+
}
|
|
3
20
|
/**
|
|
4
21
|
* @internal
|
|
5
22
|
*
|
|
6
23
|
* The sortable preparation phase of a destroy. Called from `destroyDream` after
|
|
7
24
|
* every user `beforeDestroy` hook has run and before the delete — a phase, not
|
|
8
|
-
* a hook among them — so the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
25
|
+
* a hook among them — so the row's real position and scope values are read in
|
|
26
|
+
* one snapshot SELECT covering every field while the row still exists.
|
|
27
|
+
* Pre-delete is the only seat for that read: on a hard destroy the row is gone
|
|
28
|
+
* afterwards, and a soft destroy nulls the position columns in the same UPDATE
|
|
29
|
+
* as `deletedAt`. The per-field compactions (`performSortableDestroyWork`)
|
|
30
|
+
* consume the cached snapshots.
|
|
13
31
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the delete and the after-destroy
|
|
32
|
+
* A direct destroy takes the operation's whole advisory key set — every
|
|
33
|
+
* sortable field — in one sorted stabilized acquisition around that read, so
|
|
34
|
+
* the snapshot cannot be invalidated before the compaction; per-field
|
|
35
|
+
* acquisition would take multi-field keys in declaration order and deadlock
|
|
36
|
+
* against the paths that acquire sorted. The lock is transaction-scoped, and
|
|
37
|
+
* `destroyDream` opens a transaction spanning the delete and the after-destroy
|
|
38
|
+
* hooks, so one acquisition covers both.
|
|
20
39
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* affected-row count is the stronger signal (a writer outside the sortable
|
|
28
|
-
* path can remove the row after this read), so the caller checks both.
|
|
40
|
+
* A cascaded destroy — one reached through a `dependent: 'destroy'` association,
|
|
41
|
+
* which passes the edge it arrived by — takes no scope lock: a cascade would
|
|
42
|
+
* otherwise hold one per distinct sort scope it touches until the root
|
|
43
|
+
* transaction commits. It reads the same snapshot unlocked and compacts from
|
|
44
|
+
* it, and skips both for a field whose whole sort scope the cascade is
|
|
45
|
+
* destroying, since nothing survives there to observe a compaction.
|
|
29
46
|
*/
|
|
30
|
-
export default function prepareSortableFieldsForDestroy(dream: Dream, txn: DreamTransaction<any
|
|
47
|
+
export default function prepareSortableFieldsForDestroy(dream: Dream, txn: DreamTransaction<any>, cascadeEdge: SortableCascadeEdge | null): Promise<SortableDestroyPreparation>;
|
|
31
48
|
/**
|
|
32
49
|
* @internal
|
|
33
50
|
*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Dream from '../../../../Dream.js';
|
|
2
|
+
import { HasStatement } from '../../../../types/associations/shared.js';
|
|
3
|
+
export type SortableCascadeEdge = HasStatement<any, any, any, any, 'HasOne' | 'HasMany'>;
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* Marks a record a `dependent: 'destroy'` cascade is about to destroy or
|
|
8
|
+
* undestroy, with the association the cascade reached it through. The record's
|
|
9
|
+
* own destroy or undestroy consumes the mark and does its sortable work without
|
|
10
|
+
* the scope lock a direct operation takes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function markSortableCascadeEdge(dream: Dream, association: SortableCascadeEdge): void;
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
*
|
|
16
|
+
* The association a cascade reached this record through, or null on a direct
|
|
17
|
+
* destroy or undestroy. Consuming removes the mark, so a later direct operation
|
|
18
|
+
* on the same instance cannot inherit a cascade's optimism.
|
|
19
|
+
*/
|
|
20
|
+
export declare function consumeSortableCascadeEdge(dream: Dream): SortableCascadeEdge | null;
|
|
@@ -162,6 +162,26 @@ export default class QueryDriverBase<DreamInstance extends Dream> {
|
|
|
162
162
|
* than silently racing on position values.
|
|
163
163
|
*/
|
|
164
164
|
static supportsAdvisoryTransactionLocks: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* @internal
|
|
167
|
+
*
|
|
168
|
+
* Whether `error` is the database refusing a write for violating a unique
|
|
169
|
+
* constraint. `withConcurrentWriterRetry` asks this of an error raised at
|
|
170
|
+
* COMMIT, which is where a `DEFERRABLE INITIALLY DEFERRED` constraint — the
|
|
171
|
+
* one `DreamMigrationHelpers.addDeferrableUniqueConstraint` declares — reports
|
|
172
|
+
* a collision. A driver that does not recognize one reports false, and nothing
|
|
173
|
+
* is retried.
|
|
174
|
+
*/
|
|
175
|
+
static isUniqueConstraintViolation(error: unknown): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* @internal
|
|
178
|
+
*
|
|
179
|
+
* Whether `error` is the database aborting a transaction to break a deadlock
|
|
180
|
+
* with another. `withConcurrentWriterRetry` reruns a transaction of Dream's
|
|
181
|
+
* own that was aborted this way. A driver that does not recognize one reports
|
|
182
|
+
* false, and nothing is retried.
|
|
183
|
+
*/
|
|
184
|
+
static isDeadlock(error: unknown): boolean;
|
|
165
185
|
/**
|
|
166
186
|
* @internal
|
|
167
187
|
*
|
|
@@ -56,6 +56,8 @@ export default class PostgresQueryDriver<DreamInstance extends Dream> extends Ky
|
|
|
56
56
|
*/
|
|
57
57
|
applyRowLock(kyselyQuery: SelectQueryBuilder<any, any, any>, tableAlias: string): SelectQueryBuilder<any, any, any>;
|
|
58
58
|
static supportsAdvisoryTransactionLocks: boolean;
|
|
59
|
+
static isUniqueConstraintViolation(error: unknown): boolean;
|
|
60
|
+
static isDeadlock(error: unknown): boolean;
|
|
59
61
|
/**
|
|
60
62
|
* @internal
|
|
61
63
|
*
|
|
@@ -10,7 +10,9 @@ export interface ReallyDestroyOptions<DreamInstance extends Dream> extends Destr
|
|
|
10
10
|
*
|
|
11
11
|
* Destroys the Dream and any `dependent: 'destroy'` associations
|
|
12
12
|
* within a transaction. If a transaction is passed, it will be used.
|
|
13
|
-
* Otherwise, a new transaction will be created automatically
|
|
13
|
+
* Otherwise, a new transaction will be created automatically, and the
|
|
14
|
+
* whole destroy is run again if that transaction is undone by a concurrent
|
|
15
|
+
* writer (see `withConcurrentWriterRetry`).
|
|
14
16
|
* If any of the nested associations fails to destroy, then this
|
|
15
17
|
* record will also fail to destroy. If skipHooks is true, model hooks
|
|
16
18
|
* will be bypassed.
|
|
@@ -5,11 +5,13 @@ type UndestroyOptions<DreamInstance extends Dream> = Required<OptionalDestroyOpt
|
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Undestroys the Dream and any `dependent: 'destroy'` associations
|
|
9
9
|
* within a transaction. If a transaction is passed, it will be used.
|
|
10
|
-
* Otherwise, a new transaction will be created automatically
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Otherwise, a new transaction will be created automatically, and the
|
|
11
|
+
* whole undestroy is run again if that transaction is undone by a
|
|
12
|
+
* concurrent writer (see `withConcurrentWriterRetry`).
|
|
13
|
+
* If any of the nested associations fails to undestroy, then this
|
|
14
|
+
* record will also fail to undestroy. If skipHooks is true, model hooks
|
|
13
15
|
* will be bypassed.
|
|
14
16
|
*/
|
|
15
17
|
export default function undestroyDream<I extends Dream>(dream: I, txn: (DreamTransaction<I> | null) | undefined, options: UndestroyOptions<I>): Promise<I>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Dream from '../../Dream.js';
|
|
2
|
+
import DreamTransaction from '../DreamTransaction.js';
|
|
3
|
+
/**
|
|
4
|
+
* How many times a destroy or undestroy that opened its own transaction runs
|
|
5
|
+
* before a failure caused by a concurrent writer escapes. One such failure
|
|
6
|
+
* needs another writer of the same rows or sort scope during the operation;
|
|
7
|
+
* three in a row is contention past the point of retrying.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* Runs `callback` in a transaction of the model's own, and runs it again when
|
|
14
|
+
* that transaction is undone by a concurrent writer: a deadlock, or a unique
|
|
15
|
+
* constraint refusing the COMMIT.
|
|
16
|
+
*
|
|
17
|
+
* A cascaded destroy or undestroy does its sortable position work without the
|
|
18
|
+
* scope lock a direct one takes (see `@deco.Sortable`), so a concurrent writer
|
|
19
|
+
* of the same sort scope can leave two live rows sharing a position, which the
|
|
20
|
+
* scope's deferrable unique constraint refuses at COMMIT — or, when both
|
|
21
|
+
* writers reach COMMIT together, as a deadlock between their two checks. A
|
|
22
|
+
* cascade holding row locks can deadlock against another writer of the same
|
|
23
|
+
* rows mid-statement as well. Either way the database has undone the whole
|
|
24
|
+
* transaction, nothing has committed and no commit hook has run, so running
|
|
25
|
+
* the operation again reads the rows as the other writer left them. The last
|
|
26
|
+
* attempt's error is the adapter's own.
|
|
27
|
+
*
|
|
28
|
+
* A unique violation raised by a statement inside the callback is not a
|
|
29
|
+
* collision but a write that violates a constraint outright, and is not
|
|
30
|
+
* retried; only a refusal of the COMMIT itself is, which is why the callback
|
|
31
|
+
* having resolved is part of the test.
|
|
32
|
+
*
|
|
33
|
+
* Only a transaction Dream opens is retried. Inside a caller's transaction the
|
|
34
|
+
* failure is the caller's, and the caller retries the whole transaction.
|
|
35
|
+
*
|
|
36
|
+
* Saves are deliberately not run through this. When a direct save and a
|
|
37
|
+
* cascade collide, the loser is whichever transaction waits first at COMMIT,
|
|
38
|
+
* and that is usually the save, which is short and reaches COMMIT while the
|
|
39
|
+
* cascade is still working: it fails with the adapter's deadlock error after
|
|
40
|
+
* `deadlock_timeout`. That bounded, occasional failure is the accepted price of
|
|
41
|
+
* cascades holding no scope locks, and the save path's state restore
|
|
42
|
+
* (`saveDream`) already makes calling `save` again correct. The collision needs
|
|
43
|
+
* a position write into the same sort scope inside the cascade's window that
|
|
44
|
+
* picks the same position, which a cascade destroying a scope's owner never
|
|
45
|
+
* exposes since it writes no position at all.
|
|
46
|
+
*/
|
|
47
|
+
export default function withConcurrentWriterRetry<R>(dreamClass: typeof Dream, callback: (txn: DreamTransaction<any>) => Promise<R>): Promise<R>;
|
package/docs/assets/hierarchy.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
window.hierarchyData = "
|
|
1
|
+
window.hierarchyData = "eJyVk0FPwyAUx7/LO+McuLCVo1tMjHFqYvSw7MDaN0tGYQE60yz77gZ3wTktO0HJ+/P7Aa97cNYGD2JB+YgRSumSgMO1xjIoazyIPVBK42BkgyDgpUXXzZzaobuVHoHARpkKBGUTAq3TIKDU0nv019VqcFI9qEOjgRwrQEDw1VWMXx0XCJS10pVDA2LBimJ5IMCKIqE/dB51l+zaw/9Vf4kBvWGTqBDHxOHZ+vDh0OdbnEn0eEQqH/KEOq2x3Eyt8cFJZcKbslrGJ/oPjc5Z5wd/RXMc2Ch1sLptzNMO3Vrbzxzyj0AOb8QS3kwGeW9K22xlUCuN7yrUcW0lPd4p1FWGQu8eF3UEH3ISL4VQzimhfPzdo/HjRPu12+Lx9HH2qHwjQ1ln+p4P51zfOP1Z5jbMW60vaZXTSB/zcPgCAPBvFw=="
|