@rvoh/dream 2.27.0 → 2.28.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 +32 -0
- package/dist/cjs/src/db/errors.js +4 -0
- package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
- package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
- package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
- package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
- package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
- package/dist/cjs/src/dream/Query.js +64 -9
- package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +11 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +74 -0
- package/dist/cjs/src/dream/internal/destroyDream.js +60 -6
- package/dist/cjs/src/dream/internal/saveDream.js +109 -7
- package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
- package/dist/cjs/src/dream/internal/softDeleteDream.js +13 -1
- package/dist/cjs/src/dream/internal/undestroyDream.js +56 -8
- package/dist/cjs/src/dream-app/index.js +21 -0
- package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
- package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
- package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
- package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
- package/dist/cjs/src/package-exports/errors.js +3 -0
- package/dist/esm/src/Dream.js +32 -0
- package/dist/esm/src/db/errors.js +4 -0
- package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
- package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
- package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
- package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
- package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
- package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
- package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
- package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
- package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
- package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
- package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
- package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
- package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
- package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
- package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
- package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
- package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
- package/dist/esm/src/dream/Query.js +64 -9
- package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
- package/dist/esm/src/dream/QueryDriver/Kysely.js +11 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
- package/dist/esm/src/dream/internal/destroyDream.js +60 -6
- package/dist/esm/src/dream/internal/saveDream.js +109 -7
- package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
- package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
- package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
- package/dist/esm/src/dream-app/index.js +21 -0
- package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
- package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
- package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
- package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
- package/dist/esm/src/package-exports/errors.js +3 -0
- package/dist/types/src/Dream.d.ts +32 -0
- package/dist/types/src/db/errors.d.ts +2 -0
- package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
- package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
- package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
- package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
- package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
- package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
- package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
- package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
- package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
- package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
- package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
- package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
- package/dist/types/src/dream/Query.d.ts +41 -7
- package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
- package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
- package/dist/types/src/dream-app/index.d.ts +18 -2
- package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
- package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
- package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
- package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
- package/dist/types/src/package-exports/errors.d.ts +3 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +88 -41
- package/docs/classes/db.PostgresQueryDriver.html +82 -42
- package/docs/classes/db.QueryDriverBase.html +86 -39
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +61 -0
- package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
- 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 +151 -124
- package/docs/classes/index.DreamApp.html +11 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +94 -84
- 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 +2 -2
- 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/modules/errors.html +1 -1
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/dist/cjs/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
- package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
- package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
- package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
- package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
- package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
- package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
- package/dist/types/src/decorators/field/sortable/hooks/beforeSortableSave.d.ts +0 -12
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import sortableScopeLockKey from './sortableScopeLockKey.js';
|
|
2
|
+
import { sortableScopeValues } from './sortableScopeColumns.js';
|
|
3
|
+
import { snapshotScopeValue } from './sortableSnapshot.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* The single key for the scope the record currently occupies — the scope its
|
|
8
|
+
* position is about to be computed within. Used by the position write itself,
|
|
9
|
+
* where the record already carries its final scope values.
|
|
10
|
+
*/
|
|
11
|
+
export function sortableScopeLockKeyForCurrentScope(dream, positionField, scope) {
|
|
12
|
+
return sortableScopeLockKey(dream, positionField, sortableScopeValues(dream, scope, column => dream[column]));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*
|
|
17
|
+
* Every key a create's position work needs — one per sortable field, derived
|
|
18
|
+
* from the scope values the row now carries — sorted and deduplicated, so that
|
|
19
|
+
* a multi-sortable-field create acquires in the same global order as every
|
|
20
|
+
* other position-mutating operation. Declaration order cannot serve: the keys
|
|
21
|
+
* hash the runtime scope values, so which field's key sorts first varies from
|
|
22
|
+
* record to record.
|
|
23
|
+
*/
|
|
24
|
+
export function sortableCurrentScopeLockKeys(dream, configs) {
|
|
25
|
+
return sortedUniqueKeys(configs.map(({ positionField, scope }) => sortableScopeLockKeyForCurrentScope(dream, positionField, scope)));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @internal
|
|
29
|
+
*
|
|
30
|
+
* Every key a per-record position-mutating operation needs, sorted and
|
|
31
|
+
* deduplicated. A save that moves a record between sort scopes compacts the
|
|
32
|
+
* scope it leaves and makes room in the scope it enters, so it needs both keys;
|
|
33
|
+
* a save within one scope needs one, and so do a destroy and an undestroy, which
|
|
34
|
+
* write no scope values at all.
|
|
35
|
+
*
|
|
36
|
+
* The *new*-scope key comes from the values being written. The *old*-scope key
|
|
37
|
+
* comes from the database snapshot of the record's physical scope columns when
|
|
38
|
+
* one is available, and only falls back to the instance's pending changes when
|
|
39
|
+
* it is not — which is the case exactly once per save, on the first
|
|
40
|
+
* acquisition, because the snapshot cannot be read until something is locked.
|
|
41
|
+
* The caller closes that window by re-reading under the lock and calling this
|
|
42
|
+
* again with the snapshot; see `acquireStabilizedSortableScopeLocks`.
|
|
43
|
+
*
|
|
44
|
+
* A pending change is only an *old* scope when it has a previous persisted
|
|
45
|
+
* value. A record created earlier in this process reports every column as
|
|
46
|
+
* changed with `was === undefined` (`Dream#changes` compares against the
|
|
47
|
+
* pre-save state), which is not a scope the row was ever in: reading it as one
|
|
48
|
+
* would take a second, bogus lock — and, since every such record produces the
|
|
49
|
+
* same all-null scope tuple, it would be the *same* bogus key for every record
|
|
50
|
+
* in the table, serializing writers that touch unrelated scopes.
|
|
51
|
+
*
|
|
52
|
+
* Either way this must be called *before* the driver write overwrites the
|
|
53
|
+
* record's scope columns.
|
|
54
|
+
*/
|
|
55
|
+
export function sortableSaveLockKeys(dream, configs, snapshots = new Map()) {
|
|
56
|
+
const keys = configs.flatMap(({ positionField, scope }) => {
|
|
57
|
+
const newScopeKey = sortableScopeLockKey(dream, positionField, sortableScopeValues(dream, scope, column => dream[column]));
|
|
58
|
+
if (!dream.isPersisted)
|
|
59
|
+
return [newScopeKey];
|
|
60
|
+
const changes = dream.changes();
|
|
61
|
+
const snapshot = snapshots.get(positionField);
|
|
62
|
+
const oldScopeKey = sortableScopeLockKey(dream, positionField, sortableScopeValues(dream, scope, column => snapshotScopeValue(snapshot, column, () => {
|
|
63
|
+
const previousValue = changes[column]?.was;
|
|
64
|
+
return previousValue === undefined ? dream[column] : previousValue;
|
|
65
|
+
})));
|
|
66
|
+
return [newScopeKey, oldScopeKey];
|
|
67
|
+
});
|
|
68
|
+
return sortedUniqueKeys(keys);
|
|
69
|
+
}
|
|
70
|
+
function sortedUniqueKeys(keys) {
|
|
71
|
+
return [...new Set(keys)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
72
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pascalize from '../../../../helpers/pascalize.js';
|
|
2
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
3
|
+
import { takeCachedSortableRow } from './sortableRowCache.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* Reads the record's real position and sort scope column values straight from
|
|
8
|
+
* the row, inside the given transaction.
|
|
9
|
+
*
|
|
10
|
+
* The read deliberately bypasses Dream's default scopes and goes to the table
|
|
11
|
+
* by primary key: soft-deleted rows have to be readable here (undestroy reads
|
|
12
|
+
* one), and an STI child's row is the same physical row whatever scope the
|
|
13
|
+
* class carries.
|
|
14
|
+
*
|
|
15
|
+
* When the row is not there the snapshots are empty and `rowExists` is false;
|
|
16
|
+
* a caller that goes on to compute a shift falls back to the instance, which is
|
|
17
|
+
* the pre-existing behavior.
|
|
18
|
+
*
|
|
19
|
+
* @param dream - the record to snapshot
|
|
20
|
+
* @param txn - the transaction the read runs in; the read must be protected by
|
|
21
|
+
* whatever scope lock the caller holds in that transaction
|
|
22
|
+
* @param configs - the sortable fields whose position and scope columns to read
|
|
23
|
+
*/
|
|
24
|
+
export async function readSortableSnapshots(dream, txn, configs) {
|
|
25
|
+
const snapshots = new Map();
|
|
26
|
+
if (!configs.length)
|
|
27
|
+
return { rowExists: true, snapshots };
|
|
28
|
+
const scopeColumnsByPositionField = new Map();
|
|
29
|
+
const columns = new Set();
|
|
30
|
+
for (const { positionField, scope } of configs) {
|
|
31
|
+
columns.add(positionField);
|
|
32
|
+
const scopeColumns = sortableScopeColumns(dream, scope);
|
|
33
|
+
scopeColumns.forEach(column => columns.add(column));
|
|
34
|
+
scopeColumnsByPositionField.set(positionField, scopeColumns);
|
|
35
|
+
}
|
|
36
|
+
// A locked batch's preflight already read this row, in this transaction and
|
|
37
|
+
// under these locks, to derive the batch's key set — asking again would be
|
|
38
|
+
// the same question of the same row. The cached row is handed over once and
|
|
39
|
+
// is dropped by every position write in the transaction, so a record an
|
|
40
|
+
// earlier record's compaction may have moved reads its row again.
|
|
41
|
+
const cachedRow = takeCachedSortableRow(txn, dream.table, dream.primaryKeyValue());
|
|
42
|
+
const row = cachedRow && [...columns].every(column => column in cachedRow)
|
|
43
|
+
? cachedRow
|
|
44
|
+
: (await txn.kyselyTransaction
|
|
45
|
+
.selectFrom(dream.table)
|
|
46
|
+
.where(dream['_primaryKey'], '=', dream.primaryKeyValue())
|
|
47
|
+
.select([...columns])
|
|
48
|
+
.executeTakeFirst());
|
|
49
|
+
if (row === undefined)
|
|
50
|
+
return { rowExists: false, snapshots };
|
|
51
|
+
for (const [positionField, scopeColumns] of scopeColumnsByPositionField) {
|
|
52
|
+
snapshots.set(positionField, {
|
|
53
|
+
position: row[positionField] ?? null,
|
|
54
|
+
scopeValues: new Map(scopeColumns.map(column => [column, row[column] ?? null])),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return { rowExists: true, snapshots };
|
|
58
|
+
}
|
|
59
|
+
function sortableSnapshotName(positionField) {
|
|
60
|
+
return `_sortableSnapshotFor${pascalize(positionField)}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @internal
|
|
64
|
+
*
|
|
65
|
+
* Stashes snapshots on the instance so the phase that computes the shift can
|
|
66
|
+
* reach the read the earlier phase made. The read and the shift are separate
|
|
67
|
+
* calls on every path — `prepareSortableFieldsForDestroy` to
|
|
68
|
+
* `performSortableDestroyWork`, the `saveDream` lock acquisition to
|
|
69
|
+
* `performSortablePositionWork` — and the instance is what crosses between
|
|
70
|
+
* them.
|
|
71
|
+
*/
|
|
72
|
+
export function cacheSortableSnapshots(dream, snapshots) {
|
|
73
|
+
for (const [positionField, snapshot] of snapshots) {
|
|
74
|
+
;
|
|
75
|
+
dream[sortableSnapshotName(positionField)] = snapshot;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export function sortableSnapshotFor(dream, positionField) {
|
|
82
|
+
return dream[sortableSnapshotName(positionField)];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export function clearSortableSnapshot(dream, positionField) {
|
|
88
|
+
;
|
|
89
|
+
dream[sortableSnapshotName(positionField)] = undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* @internal
|
|
93
|
+
*
|
|
94
|
+
* The value to filter a sort scope column by: the row's real value when a
|
|
95
|
+
* snapshot is available, and the caller's own fallback when it is not.
|
|
96
|
+
*/
|
|
97
|
+
export function snapshotScopeValue(snapshot, column, fallback) {
|
|
98
|
+
if (snapshot?.scopeValues.has(column))
|
|
99
|
+
return snapshot.scopeValues.get(column);
|
|
100
|
+
return fallback();
|
|
101
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import SortableScopeDidNotStabilize from '../../../../errors/SortableScopeDidNotStabilize.js';
|
|
2
|
+
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
3
|
+
import heldSortableScopeLockKeys from './heldSortableScopeLockKeys.js';
|
|
4
|
+
/**
|
|
5
|
+
* How many acquisition passes a position-mutating operation will make while
|
|
6
|
+
* following a record that has been moved to another sort scope, before giving
|
|
7
|
+
* up. One pass is the ordinary case (lock what the caller says, read, agree); a
|
|
8
|
+
* second covers a competitor that moved the row between the two; a third exists
|
|
9
|
+
* so that a single unlucky retry is not fatal. Beyond that the row is being
|
|
10
|
+
* moved faster than it can be read, and raising is better than compacting a
|
|
11
|
+
* scope it no longer occupies.
|
|
12
|
+
*
|
|
13
|
+
* The bound counts acquisitions, and every one of them counts: the convergence
|
|
14
|
+
* check runs after the last acquisition and the read taken under it, so an
|
|
15
|
+
* operation that settles on its final pass settles rather than raising.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_SCOPE_STABILIZATION_ROUNDS = 3;
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
*
|
|
21
|
+
* The stabilization protocol every sortable lock acquisition follows, in one
|
|
22
|
+
* place: acquire the keys the current knowledge implies, read again under those
|
|
23
|
+
* locks, and take whatever additional key the read reveals — until a pass
|
|
24
|
+
* reveals nothing new, or the bound above is exhausted.
|
|
25
|
+
*
|
|
26
|
+
* The bootstrapping problem it solves: a sort scope's lock key is derived from
|
|
27
|
+
* the record's *physical* scope columns, and those cannot be read safely until
|
|
28
|
+
* that scope is locked. So the first pass locks what the caller can know without
|
|
29
|
+
* reading — an instance's own values, or an unprotected candidate read — and
|
|
30
|
+
* every later pass locks what the read taken under those locks reveals.
|
|
31
|
+
*
|
|
32
|
+
* Nothing here is ever unwound: the locks are transaction-scoped advisory locks,
|
|
33
|
+
* which are additive and are released only when the transaction ends. A pass
|
|
34
|
+
* that discovers a new key simply also takes it, holding at worst one lock too
|
|
35
|
+
* many for the rest of the transaction — which costs contention and never
|
|
36
|
+
* correctness. That is what lets this run inside a caller-owned transaction
|
|
37
|
+
* unchanged; no path here restarts or rolls back a transaction it did not open.
|
|
38
|
+
*
|
|
39
|
+
* Keys the transaction already holds — taken by an earlier phase of the same
|
|
40
|
+
* operation, or by the preflight of the locked batch this record is being
|
|
41
|
+
* walked in — are not taken again; they are held to the end of the transaction
|
|
42
|
+
* regardless. Convergence is judged on that same set, so a pass whose read
|
|
43
|
+
* reveals only keys already held is a pass that agrees.
|
|
44
|
+
*
|
|
45
|
+
* Keys are taken in sorted order within a pass, so that two operations touching
|
|
46
|
+
* the same set of scopes cannot deadlock against each other. Across passes the
|
|
47
|
+
* order is discovery-driven and therefore unsortable in principle, which is the
|
|
48
|
+
* named fallback: two operations moving records in opposite directions between
|
|
49
|
+
* the same two scopes can raise Postgres's `40P01` deadlock error, a visible
|
|
50
|
+
* retryable failure.
|
|
51
|
+
*
|
|
52
|
+
* @param dream - a record of the model being locked; supplies the connection
|
|
53
|
+
* and, when this fails, the class name in the error
|
|
54
|
+
* @param txn - the transaction the locks are scoped to
|
|
55
|
+
* @param keysForRound - reads whatever this pass can see — the caller's own
|
|
56
|
+
* snapshot or candidate read — and returns every lock key it implies. Called
|
|
57
|
+
* once per pass with the number of acquisitions made so far, so a caller whose
|
|
58
|
+
* first pass needs no read can skip it on round 0
|
|
59
|
+
*/
|
|
60
|
+
export default async function stabilizeSortableScopeLocks(dream, txn, keysForRound) {
|
|
61
|
+
const heldKeys = heldSortableScopeLockKeys(txn);
|
|
62
|
+
for (let acquisitions = 0;; acquisitions++) {
|
|
63
|
+
const keys = await keysForRound(acquisitions);
|
|
64
|
+
const keysToAcquire = [...keys]
|
|
65
|
+
.filter(key => !heldKeys.has(key))
|
|
66
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
67
|
+
// the key set this pass's read implies is the key set already held: nothing
|
|
68
|
+
// can move out from under the operation until the transaction ends, and the
|
|
69
|
+
// read taken beside it was taken under those locks. This check follows every
|
|
70
|
+
// acquisition, the last one included, so no pass's work is discarded unread.
|
|
71
|
+
//
|
|
72
|
+
// Round 0 is never allowed to end the loop, however many of its keys the
|
|
73
|
+
// transaction already holds: it ran before this operation read anything
|
|
74
|
+
// under them, and the read is what the caller came for. Where the earlier
|
|
75
|
+
// shape ended the loop unread — a locked batch's per-record work, whose
|
|
76
|
+
// preflight already took every key the record implies — the pass after it
|
|
77
|
+
// now reads under those very locks and agrees, at the cost of no statement
|
|
78
|
+
// at all
|
|
79
|
+
if (acquisitions > 0 && !keysToAcquire.length)
|
|
80
|
+
return;
|
|
81
|
+
if (acquisitions === MAX_SCOPE_STABILIZATION_ROUNDS)
|
|
82
|
+
throw new SortableScopeDidNotStabilize(dream['sanitizedConstructorName'], MAX_SCOPE_STABILIZATION_ROUNDS);
|
|
83
|
+
// records what it takes in `heldKeys`, and takes nothing already there
|
|
84
|
+
await acquireSortableScopeLocks(dream, txn, keysToAcquire);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import filterQueryToInstanceScope from './filterQueryToInstanceScope.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* The position the record will be written at: the caller's, clamped to the
|
|
6
|
+
* scope the record now occupies.
|
|
7
|
+
*
|
|
8
|
+
* The scope is read off the instance rather than off the snapshot because the
|
|
9
|
+
* scope in question is the one being *entered*, which is the instance's — the
|
|
10
|
+
* snapshot holds the scope the row was in before the driver write. On every
|
|
11
|
+
* path into here the driver write has already landed the instance's scope
|
|
12
|
+
* values on the row.
|
|
13
|
+
*/
|
|
14
|
+
export default async function validPosition({ changingScope, dream, position, positionField, positionUnderSentinel, query, scope, wasNewRecord, previousPosition, txn, }) {
|
|
3
15
|
const increasingNumberOfItemsToSort = !previousPosition || wasNewRecord || changingScope;
|
|
4
|
-
const
|
|
5
|
-
|
|
16
|
+
const scopeMax = await filterQueryToInstanceScope(query.txn(txn), dream, scope).max(positionField);
|
|
17
|
+
// `scopeMax` includes this record's own row, but mid-save that row holds the
|
|
18
|
+
// sentinel `0` instead of its real position. When the record already had a
|
|
19
|
+
// position in this scope, the caller passes it as `positionUnderSentinel`,
|
|
20
|
+
// and it counts toward the scope's true max — so take the larger of the two
|
|
21
|
+
// before clamping.
|
|
22
|
+
const maxPosition = Math.max(scopeMax ?? 0, positionUnderSentinel ?? 0) + (increasingNumberOfItemsToSort ? 1 : 0);
|
|
6
23
|
return Math.max(1, Math.min(maxPosition, position === null || position === undefined || position < 1 ? maxPosition : position));
|
|
7
24
|
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import clearCachedSortableValues from '../helpers/clearCachedSortableValues.js';
|
|
2
2
|
import decrementPositionForScopedRecordsGreaterThanPosition from '../helpers/decrementScopedRecordsGreaterThanPosition.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { invalidateSortableRowCache } from '../helpers/sortableRowCache.js';
|
|
4
|
+
import { clearSortableSnapshot, sortableSnapshotFor } from '../helpers/sortableSnapshot.js';
|
|
5
|
+
export default async function afterSortableDestroy({ positionField, dream, query, scope, txn, }) {
|
|
6
|
+
// The row's real position and scope, read by the destroy's preparation phase
|
|
7
|
+
// (`prepareSortableFieldsForDestroy`) while the row still existed. The
|
|
8
|
+
// instance's own position is whatever it held when
|
|
9
|
+
// it was loaded, which is too low as soon as another create or update moved
|
|
10
|
+
// the record down — and shifting from a position lower than the vacated one
|
|
11
|
+
// moves rows onto occupied positions rather than closing the gap.
|
|
12
|
+
const snapshot = sortableSnapshotFor(dream, positionField);
|
|
13
|
+
const position = snapshot ? snapshot.position : dream[positionField];
|
|
14
|
+
if (position !== null && position !== undefined) {
|
|
15
|
+
await decrementPositionForScopedRecordsGreaterThanPosition(position, {
|
|
16
|
+
dream,
|
|
17
|
+
positionField,
|
|
18
|
+
scope,
|
|
19
|
+
query,
|
|
20
|
+
snapshot,
|
|
21
|
+
});
|
|
22
|
+
// every surviving row above the vacancy just moved, which includes rows a
|
|
23
|
+
// locked batch's preflight read for the records it has yet to walk
|
|
24
|
+
invalidateSortableRowCache(txn);
|
|
25
|
+
}
|
|
10
26
|
clearCachedSortableValues(dream, positionField);
|
|
27
|
+
clearSortableSnapshot(dream, positionField);
|
|
11
28
|
}
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import clearCachedSortableValues from '../helpers/clearCachedSortableValues.js';
|
|
2
2
|
import setPosition from '../helpers/setPosition.js';
|
|
3
3
|
import sortableCacheValuesName from '../helpers/sortableCacheValuesName.js';
|
|
4
|
+
import { clearSortableSnapshot, sortableSnapshotFor } from '../helpers/sortableSnapshot.js';
|
|
4
5
|
export default async function afterUpdateSortable({ positionField, dream, query, txn, scope, }) {
|
|
5
6
|
const cachedValuesName = sortableCacheValuesName(positionField);
|
|
6
7
|
const sortableCache = dream[cachedValuesName];
|
|
7
8
|
if (!sortableCache)
|
|
8
9
|
return;
|
|
10
|
+
// The range this update compacts starts at the position the record really
|
|
11
|
+
// held, which the `saveDream` sortable phase read from the row under the
|
|
12
|
+
// scope lock. The instance's own memory of it is stale whenever another
|
|
13
|
+
// writer moved the record after it was loaded, and shifting from a stale
|
|
14
|
+
// position lands rows on occupied ones.
|
|
15
|
+
const snapshot = sortableSnapshotFor(dream, positionField);
|
|
9
16
|
await setPosition({
|
|
10
17
|
...sortableCache,
|
|
11
18
|
dream,
|
|
12
19
|
positionField,
|
|
13
20
|
position: sortableCache.changingScope ? undefined : sortableCache.position,
|
|
21
|
+
previousPosition: snapshot ? (snapshot.position ?? undefined) : sortableCache.previousPosition,
|
|
22
|
+
snapshot,
|
|
14
23
|
query,
|
|
15
24
|
scope,
|
|
16
25
|
txn,
|
|
17
26
|
});
|
|
18
27
|
clearCachedSortableValues(dream, positionField);
|
|
28
|
+
clearSortableSnapshot(dream, positionField);
|
|
19
29
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SOFT_DELETE_SCOPE_NAME } from '../decorators/class/SoftDelete.js';
|
|
2
|
+
import acquireStabilizedSortableBatchLocks from '../decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js';
|
|
3
|
+
import { invalidateSortableRowCache } from '../decorators/field/sortable/helpers/sortableRowCache.js';
|
|
2
4
|
import DreamApp from '../dream-app/index.js';
|
|
3
5
|
import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
|
|
4
6
|
import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
@@ -1878,9 +1880,11 @@ export default class Query {
|
|
|
1878
1880
|
* read is a plain `FOR UPDATE`: if another transaction already holds a lock
|
|
1879
1881
|
* on one of the batch's rows, this call blocks until that transaction ends,
|
|
1880
1882
|
* holding its own transaction and its pooled connection open the whole
|
|
1881
|
-
* time. Dream sets no `lock_timeout` or `statement_timeout
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1883
|
+
* time. Dream sets no `lock_timeout` or `statement_timeout` on a row-lock
|
|
1884
|
+
* wait — the one it does set, around a sortable model's scope-lock
|
|
1885
|
+
* acquisition, is restored before the rows are claimed — so a long-running
|
|
1886
|
+
* writer on the other side stalls the run indefinitely and, at scale, ties
|
|
1887
|
+
* up connections. If that is a risk in your application, set a
|
|
1884
1888
|
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1885
1889
|
* hanging.
|
|
1886
1890
|
* - **A `limit` or `offset` on the Query is incompatible with `destroy` on
|
|
@@ -2001,8 +2005,38 @@ export default class Query {
|
|
|
2001
2005
|
* When the Query already carries a transaction, that transaction is used for
|
|
2002
2006
|
* every batch rather than opening one per batch, so the caller's transaction
|
|
2003
2007
|
* boundary wins and the locks are held until the caller commits.
|
|
2004
|
-
|
|
2005
|
-
|
|
2008
|
+
*
|
|
2009
|
+
* On a sortable model, the advisory scope keys this batch needs are acquired
|
|
2010
|
+
* before the claim (see `acquireStabilizedSortableBatchLocks`), which removes
|
|
2011
|
+
* the row-lock/advisory-lock inversion for the rows the batch itself claims.
|
|
2012
|
+
* Two things stay outside that guarantee and are fallbacks rather than
|
|
2013
|
+
* guarantees, with Postgres's `40P01` as the residual failure in both:
|
|
2014
|
+
*
|
|
2015
|
+
* - the ordering is **per batch**. A multi-batch run inside a caller-owned
|
|
2016
|
+
* transaction keeps batch N-1's row locks while batch N preflights. The
|
|
2017
|
+
* same shape accumulates *advisory* locks: they are released only when the
|
|
2018
|
+
* transaction ends, so every batch's scope keys are still held when the
|
|
2019
|
+
* next one preflights. That accumulation is bounded — the preflight counts
|
|
2020
|
+
* the keys the transaction already holds along with the ones the batch
|
|
2021
|
+
* would add, and refuses with `SortableBatchRequiresTooManyScopeLocks`
|
|
2022
|
+
* rather than letting a long run over a high-cardinality sort scope exhaust
|
|
2023
|
+
* `max_locks_per_transaction` for the whole cluster — so a run long enough
|
|
2024
|
+
* raises instead of completing. Letting Dream open a transaction per batch
|
|
2025
|
+
* releases the keys as it goes and gives each batch the whole budget.
|
|
2026
|
+
* - the preflight covers **the claimed rows' own scopes only**. A
|
|
2027
|
+
* `dependent: 'destroy'` cascade under `destroy({ lock: true })` destroys
|
|
2028
|
+
* child records of other tables, and a sortable child takes its own table's
|
|
2029
|
+
* advisory key inside `perBatch` — after this transaction is already
|
|
2030
|
+
* holding the root rows' row locks. Walking
|
|
2031
|
+
* `dependentDestroyAssociationNames()` to fold those keys into the sorted
|
|
2032
|
+
* pass is deliberately not done: the cascade's shape is only knowable per
|
|
2033
|
+
* record, and the deadlock it leaves is detected and retryable. The keys
|
|
2034
|
+
* the preflight *did* take are held across the whole batch, cascade
|
|
2035
|
+
* included — a per-record destroy holds its scope lock across one delete
|
|
2036
|
+
* and one compaction, while here the hold spans batch size × cascade depth.
|
|
2037
|
+
* A smaller `batchSize` shortens it (the default is 10).
|
|
2038
|
+
*/
|
|
2039
|
+
async lockedBatches(batchSize, perBatch, sortableIncomingAttributes) {
|
|
2006
2040
|
// the driver seam refuses this combination too, but catching it here means
|
|
2007
2041
|
// failing before any transaction is opened or any row is touched
|
|
2008
2042
|
if (this.distinctColumn)
|
|
@@ -2031,10 +2065,27 @@ export default class Query {
|
|
|
2031
2065
|
const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
|
|
2032
2066
|
if (!candidateIds.length)
|
|
2033
2067
|
return { attempted: 0, processed: 0, lastId };
|
|
2068
|
+
// On a sortable model, every advisory scope lock this batch will need is
|
|
2069
|
+
// taken here, sorted, before a single row is claimed. Claiming first and
|
|
2070
|
+
// locking per record inside `perBatch` is the lock-order inversion that
|
|
2071
|
+
// deadlocks against an ordinary concurrent writer of the same scope,
|
|
2072
|
+
// which holds the advisory lock while waiting for the row.
|
|
2073
|
+
//
|
|
2074
|
+
// The ordering this establishes is per batch. A multi-batch run inside a
|
|
2075
|
+
// caller-owned transaction keeps batch N-1's row locks while batch N
|
|
2076
|
+
// preflights, and is a documented fallback rather than a guarantee.
|
|
2077
|
+
await acquireStabilizedSortableBatchLocks(this.dreamInstance, txn, candidateIds, sortableIncomingAttributes);
|
|
2034
2078
|
const claimed = await this.dbDriverInstance(batchQuery.txn(txn).where({ [primaryKey]: candidateIds })).takeAll({ lock: true });
|
|
2079
|
+
const processed = await perBatch(claimed, txn);
|
|
2080
|
+
// The preflight stashed the rows it read so this batch's per-record
|
|
2081
|
+
// sortable work could consume them instead of re-reading each one. They
|
|
2082
|
+
// describe this batch's candidates as they were before it ran, so they
|
|
2083
|
+
// are worth nothing to whatever the enclosing transaction does next —
|
|
2084
|
+
// and a caller-owned transaction outlives the batch.
|
|
2085
|
+
invalidateSortableRowCache(txn);
|
|
2035
2086
|
return {
|
|
2036
2087
|
attempted: candidateIds.length,
|
|
2037
|
-
processed
|
|
2088
|
+
processed,
|
|
2038
2089
|
lastId: candidateIds.at(-1),
|
|
2039
2090
|
};
|
|
2040
2091
|
};
|
|
@@ -2186,7 +2237,11 @@ export default class Query {
|
|
|
2186
2237
|
// dirty-state-based, and mutations made directly on the record
|
|
2187
2238
|
// Dream holds would otherwise ride along with the save
|
|
2188
2239
|
(record) => cb(record['clone']())
|
|
2189
|
-
: () => attributesOrCb, { skipHooks }, resolvedBatchSize
|
|
2240
|
+
: () => attributesOrCb, { skipHooks }, resolvedBatchSize,
|
|
2241
|
+
// only the attributes form knows what it is going to write before the
|
|
2242
|
+
// claim; the callback form computes it inside the batch, after the row
|
|
2243
|
+
// locks are taken, and is a documented preflight fallback
|
|
2244
|
+
cb ? undefined : attributesOrCb);
|
|
2190
2245
|
}
|
|
2191
2246
|
if (!cb && skipHooks)
|
|
2192
2247
|
return await this.updateWithoutCallingModelHooks(attributesOrCb);
|
|
@@ -2243,7 +2298,7 @@ export default class Query {
|
|
|
2243
2298
|
* thrown on every join-load query (leftJoinPreload always carries left-join
|
|
2244
2299
|
* statements).
|
|
2245
2300
|
*/
|
|
2246
|
-
async lockedUpdate(cb, { skipHooks }, batchSize) {
|
|
2301
|
+
async lockedUpdate(cb, { skipHooks }, batchSize, sortableIncomingAttributes) {
|
|
2247
2302
|
return await this.lockedBatches(batchSize, async (claimed, txn) => {
|
|
2248
2303
|
let written = 0;
|
|
2249
2304
|
for (const record of claimed) {
|
|
@@ -2258,7 +2313,7 @@ export default class Query {
|
|
|
2258
2313
|
written++;
|
|
2259
2314
|
}
|
|
2260
2315
|
return written;
|
|
2261
|
-
});
|
|
2316
|
+
}, sortableIncomingAttributes);
|
|
2262
2317
|
}
|
|
2263
2318
|
async updateWithoutCallingModelHooks(attributes) {
|
|
2264
2319
|
return await this.dbDriverInstance().update(attributes);
|
|
@@ -193,6 +193,69 @@ export default class QueryDriverBase {
|
|
|
193
193
|
connectionName) {
|
|
194
194
|
throw new Error('override openTestDatabaseLockSession in child class');
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* @internal
|
|
198
|
+
*
|
|
199
|
+
* Whether this driver implements the transaction-scoped advisory lock seam
|
|
200
|
+
* ({@link acquireAdvisoryTransactionLocks}). Adapters that can hold a
|
|
201
|
+
* key-addressed lock for the life of a transaction override this to `true`.
|
|
202
|
+
* It defaults to `false` so that a driver without the primitive fails loud
|
|
203
|
+
* for features that depend on it (today, the `Sortable` decorator) rather
|
|
204
|
+
* than silently racing on position values.
|
|
205
|
+
*/
|
|
206
|
+
static supportsAdvisoryTransactionLocks = false;
|
|
207
|
+
/**
|
|
208
|
+
* @internal
|
|
209
|
+
*
|
|
210
|
+
* Acquire exclusive, transaction-scoped locks on arbitrary keys, waiting
|
|
211
|
+
* until any other transaction holding one of them finishes. The locks are
|
|
212
|
+
* released when the enclosing transaction commits or rolls back — never by an
|
|
213
|
+
* explicit unlock — so callers only ever have to acquire (Postgres:
|
|
214
|
+
* `pg_advisory_xact_lock(bigint)`).
|
|
215
|
+
*
|
|
216
|
+
* Unlike {@link applyRowLock}, this does not depend on rows existing: it is
|
|
217
|
+
* the only way to serialize writers that are about to insert into the same
|
|
218
|
+
* logical group. The `Sortable` decorator uses it to make each sort scope's
|
|
219
|
+
* position computation exclusive, keyed on the physical database, table,
|
|
220
|
+
* position field and scope values.
|
|
221
|
+
*
|
|
222
|
+
* The seam takes the whole key set rather than one key at a time so that a
|
|
223
|
+
* driver able to express it can take them in a single round trip — the
|
|
224
|
+
* batched, row-locking query APIs can need one key per candidate record per
|
|
225
|
+
* sortable field. **Keys must be acquired in the order given**: callers sort
|
|
226
|
+
* them, and that shared order is what keeps two operations over the same
|
|
227
|
+
* scopes from deadlocking. A driver that has no bulk form implements this by
|
|
228
|
+
* looping over the keys in order.
|
|
229
|
+
*
|
|
230
|
+
* **The wait must be bounded.** A caller that finds a key held waits for the
|
|
231
|
+
* holder's transaction, and an implementation that waits forever turns an
|
|
232
|
+
* ordinary mistake — a sortable operation issued without the enclosing
|
|
233
|
+
* transaction, so that it waits on a key that transaction itself holds — into
|
|
234
|
+
* a permanently wedged connection that no deadlock detector can see, because
|
|
235
|
+
* only one side is waiting. So an implementation applies the
|
|
236
|
+
* `sortableScopeLockTimeout` DreamApp option (milliseconds) to the
|
|
237
|
+
* acquisition and raises `SortableScopeLockWaitTimedOut` when it elapses. A
|
|
238
|
+
* configured `0` means no bound, and is applied just as literally: the
|
|
239
|
+
* acquisition waits without bound even where the connection carries a
|
|
240
|
+
* wait bound of its own. The bound belongs to
|
|
241
|
+
* the acquisition alone: whatever lock-wait behavior the connection was
|
|
242
|
+
* configured with has to be in force again by the time this returns, so that
|
|
243
|
+
* the row locks {@link applyRowLock} takes are unaffected.
|
|
244
|
+
*
|
|
245
|
+
* Only ever called when {@link supportsAdvisoryTransactionLocks} is `true`;
|
|
246
|
+
* the base default throws.
|
|
247
|
+
*
|
|
248
|
+
* @param txn - the transaction the locks are scoped to
|
|
249
|
+
* @param keys - the 64-bit signed lock keys, in acquisition order
|
|
250
|
+
*/
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
252
|
+
static async acquireAdvisoryTransactionLocks(
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
254
|
+
txn,
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
256
|
+
keys) {
|
|
257
|
+
throw new Error('override acquireAdvisoryTransactionLocks in child class');
|
|
258
|
+
}
|
|
196
259
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
197
260
|
static async getColumnData(
|
|
198
261
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -507,9 +570,19 @@ export default class QueryDriverBase {
|
|
|
507
570
|
* @param dream - the dream instance you wish to destroy
|
|
508
571
|
* @param txn - a transaction to encapsulate, consistently provided by underlying dream mechanisms
|
|
509
572
|
* @param reallyDestroy - whether or not to reallyDestroy. If false, soft delete will be attempted when relevant
|
|
573
|
+
* @returns the number of rows this call removed (or soft deleted). Zero means
|
|
574
|
+
* the row was already gone, or that a `preventDeletion` veto stopped the
|
|
575
|
+
* write — callers distinguish the two by the veto flag, and use the count to
|
|
576
|
+
* decide whether there is anything for the after-destroy hooks to act on.
|
|
510
577
|
*/
|
|
511
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
512
|
-
static async destroyDream(
|
|
578
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
579
|
+
static async destroyDream(
|
|
580
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
581
|
+
dream,
|
|
582
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
583
|
+
txn,
|
|
584
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
585
|
+
reallyDestroy) {
|
|
513
586
|
throw new Error('implement destroyDream in child class');
|
|
514
587
|
}
|
|
515
588
|
static get syncDialect() {
|
|
@@ -265,21 +265,28 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
267
|
* destroys a dream, possibly implementing soft delete if reallyDestroy is false
|
|
268
|
-
* and the record being deleted implements soft delete.
|
|
268
|
+
* and the record being deleted implements soft delete. Does nothing at all when a
|
|
269
|
+
* beforeDestroy hook called `preventDeletion`.
|
|
269
270
|
*
|
|
270
271
|
* @param dream - the dream instance you wish to destroy
|
|
271
272
|
* @param txn - a transaction to encapsulate, consistently provided by underlying dream mechanisms
|
|
272
273
|
* @param reallyDestroy - whether or not to reallyDestroy. If false, soft delete will be attempted when relevant
|
|
274
|
+
* @returns the number of rows removed — zero when a `preventDeletion` veto
|
|
275
|
+
* stopped the write, and zero when the row was already gone, which on the
|
|
276
|
+
* soft delete path includes a row that is already soft deleted
|
|
273
277
|
*/
|
|
274
278
|
static async destroyDream(dream, txn, reallyDestroy) {
|
|
279
|
+
if (dream['_preventDeletion'])
|
|
280
|
+
return 0;
|
|
275
281
|
if (shouldSoftDelete(dream, reallyDestroy)) {
|
|
276
|
-
await softDeleteDream(dream, txn);
|
|
282
|
+
return await softDeleteDream(dream, txn);
|
|
277
283
|
}
|
|
278
|
-
else
|
|
279
|
-
await txn.kyselyTransaction
|
|
284
|
+
else {
|
|
285
|
+
const results = await txn.kyselyTransaction
|
|
280
286
|
.deleteFrom(dream.table)
|
|
281
287
|
.where(dream['_primaryKey'], '=', dream.primaryKeyValue())
|
|
282
288
|
.execute();
|
|
289
|
+
return Number(results[0]?.numDeletedRows ?? 0);
|
|
283
290
|
}
|
|
284
291
|
}
|
|
285
292
|
/**
|