@rvoh/dream 2.27.0 → 2.28.1
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 +63 -1
- package/dist/cjs/src/db/errors.js +4 -0
- package/dist/cjs/src/decorators/field/Encrypted.js +12 -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 +73 -11
- 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/encrypt/assertNoEncryptedColumnWrites.js +62 -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/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
- package/dist/cjs/src/package-exports/errors.js +3 -0
- package/dist/esm/src/Dream.js +63 -1
- package/dist/esm/src/db/errors.js +4 -0
- package/dist/esm/src/decorators/field/Encrypted.js +12 -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 +73 -11
- 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/encrypt/assertNoEncryptedColumnWrites.js +62 -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/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
- package/dist/esm/src/package-exports/errors.js +3 -0
- package/dist/types/src/Dream.d.ts +56 -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 +47 -9
- 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/encrypt/assertNoEncryptedColumnWrites.d.ts +27 -0
- 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/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.d.ts +8 -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 +124 -98
- 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,99 @@
|
|
|
1
|
+
import DreamApp from '../../../../dream-app/index.js';
|
|
2
|
+
import canonicalScopeValue from './canonicalScopeValue.js';
|
|
3
|
+
const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
|
|
4
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
5
|
+
const UINT64_MASK = 0xffffffffffffffffn;
|
|
6
|
+
const INT64_SIGN_BIT = 0x8000000000000000n;
|
|
7
|
+
const UINT64_MODULUS = 0x10000000000000000n;
|
|
8
|
+
const UTF8 = new TextEncoder();
|
|
9
|
+
/**
|
|
10
|
+
* The hash of the part of the key that cannot vary for one sortable field —
|
|
11
|
+
* database, table, position field — so that a path deriving a key per row, per
|
|
12
|
+
* field, per stabilization round only ever serializes and hashes the scope
|
|
13
|
+
* values. Keyed by the serialization of those three, so a `DreamApp` whose
|
|
14
|
+
* database name changes derives a new key rather than reusing a stale one.
|
|
15
|
+
*/
|
|
16
|
+
const invariantPrefixHashes = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* @internal
|
|
19
|
+
*
|
|
20
|
+
* Derives the advisory lock key that makes one sort scope's position
|
|
21
|
+
* computation exclusive. Every position-mutating path derives it the same way,
|
|
22
|
+
* since two paths that disagree about the key do not exclude each other.
|
|
23
|
+
*
|
|
24
|
+
* The key covers:
|
|
25
|
+
*
|
|
26
|
+
* - the **physical database name**, because Postgres advisory locks are scoped
|
|
27
|
+
* to the cluster rather than to a database. Without it, two unrelated
|
|
28
|
+
* databases in one cluster — including this repo's own parallel test
|
|
29
|
+
* databases — would contend on the same key, and that contention shows up
|
|
30
|
+
* only as writers of unrelated scopes queueing behind each other.
|
|
31
|
+
* - the table and the position field, since a model can declare several
|
|
32
|
+
* sortable fields, each with its own position space.
|
|
33
|
+
* - the scope column values, which are what a "sort scope" actually is.
|
|
34
|
+
*
|
|
35
|
+
* Values are encoded structurally rather than joined into a string, so a null
|
|
36
|
+
* member is distinct from an empty string and no combination of delimiters in
|
|
37
|
+
* one member can be mistaken for a different tuple. They are also canonicalized
|
|
38
|
+
* against the column's database type (`canonicalScopeValue`), so a writer
|
|
39
|
+
* holding a pending JavaScript value and a writer holding the value Postgres
|
|
40
|
+
* hydrated derive one key rather than two.
|
|
41
|
+
*
|
|
42
|
+
* **Scope values are assumed non-adversarial.** The hash is FNV-1a, which is
|
|
43
|
+
* unkeyed: anyone who can choose a scope column's value — where that column
|
|
44
|
+
* holds user-supplied text rather than a foreign key — can compute a value
|
|
45
|
+
* whose key equals another scope's, and share that scope's lock with it. A
|
|
46
|
+
* collision costs contention and never correctness, since a shared lock
|
|
47
|
+
* over-serializes rather than under-locking, and the bounded wait
|
|
48
|
+
* (`sortableScopeLockTimeout`) caps what a deliberate one can achieve at
|
|
49
|
+
* stalls rather than at hangs. A keyed digest would make deliberate collisions
|
|
50
|
+
* expensive, and is not used: changing the derivation would give old and new
|
|
51
|
+
* processes different keys for one scope during a rolling deploy, which is the
|
|
52
|
+
* race this lock exists to eliminate.
|
|
53
|
+
*
|
|
54
|
+
* @param dream - the record whose scope is being locked
|
|
55
|
+
* @param positionField - the sortable position column
|
|
56
|
+
* @param scopeValues - `[column, value]` pairs for the scope being locked
|
|
57
|
+
* @returns a 64-bit signed key
|
|
58
|
+
*/
|
|
59
|
+
export default function sortableScopeLockKey(dream, positionField, scopeValues) {
|
|
60
|
+
const databaseName = DreamApp.getOrFail().dbName(dream.connectionName || 'default', 'primary');
|
|
61
|
+
const normalizedScopeValues = [...scopeValues]
|
|
62
|
+
.sort(([columnA], [columnB]) => (columnA < columnB ? -1 : columnA > columnB ? 1 : 0))
|
|
63
|
+
.map(([column, value]) => [column, canonicalScopeValue(dream, column, value)]);
|
|
64
|
+
// the hashed text is exactly
|
|
65
|
+
// `JSON.stringify([databaseName, table, positionField, normalizedScopeValues])`,
|
|
66
|
+
// hashed in two halves so the invariant half is hashed once per sortable
|
|
67
|
+
// field rather than once per row. FNV-1a is a streaming hash, so resuming
|
|
68
|
+
// from the cached state gives the same key byte for byte
|
|
69
|
+
return toSignedKey(fnv1a64(JSON.stringify(normalizedScopeValues) + ']', invariantPrefixHash(databaseName, dream.table, positionField)));
|
|
70
|
+
}
|
|
71
|
+
function invariantPrefixHash(databaseName, table, positionField) {
|
|
72
|
+
// the serialization of the first three members, with the array's closing
|
|
73
|
+
// bracket replaced by the separator the scope values follow. It is also the
|
|
74
|
+
// memo key: a JSON array of the three members is unambiguous, where any
|
|
75
|
+
// joined form would need a delimiter no member could contain
|
|
76
|
+
const prefix = JSON.stringify([databaseName, table, positionField]).slice(0, -1) + ',';
|
|
77
|
+
const cached = invariantPrefixHashes.get(prefix);
|
|
78
|
+
if (cached !== undefined)
|
|
79
|
+
return cached;
|
|
80
|
+
const hash = fnv1a64(prefix, FNV_OFFSET_BASIS);
|
|
81
|
+
invariantPrefixHashes.set(prefix, hash);
|
|
82
|
+
return hash;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 64-bit FNV-1a, resumed from a previously hashed prefix.
|
|
86
|
+
*/
|
|
87
|
+
function fnv1a64(value, hash) {
|
|
88
|
+
for (const byte of UTF8.encode(value)) {
|
|
89
|
+
hash = ((hash ^ BigInt(byte)) * FNV_PRIME) & UINT64_MASK;
|
|
90
|
+
}
|
|
91
|
+
return hash;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Folds the unsigned hash into the signed range Postgres's
|
|
95
|
+
* `pg_advisory_xact_lock(bigint)` accepts.
|
|
96
|
+
*/
|
|
97
|
+
function toSignedKey(hash) {
|
|
98
|
+
return hash >= INT64_SIGN_BIT ? hash - UINT64_MODULUS : hash;
|
|
99
|
+
}
|
|
@@ -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,5 +1,8 @@
|
|
|
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';
|
|
5
|
+
import assertNoEncryptedColumnWrites from '../encrypt/assertNoEncryptedColumnWrites.js';
|
|
3
6
|
import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
|
|
4
7
|
import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
5
8
|
import CannotPassAdditionalFieldsToPluckEachAfterCallback from '../errors/CannotPassAdditionalFieldsToPluckEachAfterCallback.js';
|
|
@@ -1878,9 +1881,11 @@ export default class Query {
|
|
|
1878
1881
|
* read is a plain `FOR UPDATE`: if another transaction already holds a lock
|
|
1879
1882
|
* on one of the batch's rows, this call blocks until that transaction ends,
|
|
1880
1883
|
* holding its own transaction and its pooled connection open the whole
|
|
1881
|
-
* time. Dream sets no `lock_timeout` or `statement_timeout
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
+
* time. Dream sets no `lock_timeout` or `statement_timeout` on a row-lock
|
|
1885
|
+
* wait — the one it does set, around a sortable model's scope-lock
|
|
1886
|
+
* acquisition, is restored before the rows are claimed — so a long-running
|
|
1887
|
+
* writer on the other side stalls the run indefinitely and, at scale, ties
|
|
1888
|
+
* up connections. If that is a risk in your application, set a
|
|
1884
1889
|
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1885
1890
|
* hanging.
|
|
1886
1891
|
* - **A `limit` or `offset` on the Query is incompatible with `destroy` on
|
|
@@ -2001,8 +2006,38 @@ export default class Query {
|
|
|
2001
2006
|
* When the Query already carries a transaction, that transaction is used for
|
|
2002
2007
|
* every batch rather than opening one per batch, so the caller's transaction
|
|
2003
2008
|
* boundary wins and the locks are held until the caller commits.
|
|
2004
|
-
|
|
2005
|
-
|
|
2009
|
+
*
|
|
2010
|
+
* On a sortable model, the advisory scope keys this batch needs are acquired
|
|
2011
|
+
* before the claim (see `acquireStabilizedSortableBatchLocks`), which removes
|
|
2012
|
+
* the row-lock/advisory-lock inversion for the rows the batch itself claims.
|
|
2013
|
+
* Two things stay outside that guarantee and are fallbacks rather than
|
|
2014
|
+
* guarantees, with Postgres's `40P01` as the residual failure in both:
|
|
2015
|
+
*
|
|
2016
|
+
* - the ordering is **per batch**. A multi-batch run inside a caller-owned
|
|
2017
|
+
* transaction keeps batch N-1's row locks while batch N preflights. The
|
|
2018
|
+
* same shape accumulates *advisory* locks: they are released only when the
|
|
2019
|
+
* transaction ends, so every batch's scope keys are still held when the
|
|
2020
|
+
* next one preflights. That accumulation is bounded — the preflight counts
|
|
2021
|
+
* the keys the transaction already holds along with the ones the batch
|
|
2022
|
+
* would add, and refuses with `SortableBatchRequiresTooManyScopeLocks`
|
|
2023
|
+
* rather than letting a long run over a high-cardinality sort scope exhaust
|
|
2024
|
+
* `max_locks_per_transaction` for the whole cluster — so a run long enough
|
|
2025
|
+
* raises instead of completing. Letting Dream open a transaction per batch
|
|
2026
|
+
* releases the keys as it goes and gives each batch the whole budget.
|
|
2027
|
+
* - the preflight covers **the claimed rows' own scopes only**. A
|
|
2028
|
+
* `dependent: 'destroy'` cascade under `destroy({ lock: true })` destroys
|
|
2029
|
+
* child records of other tables, and a sortable child takes its own table's
|
|
2030
|
+
* advisory key inside `perBatch` — after this transaction is already
|
|
2031
|
+
* holding the root rows' row locks. Walking
|
|
2032
|
+
* `dependentDestroyAssociationNames()` to fold those keys into the sorted
|
|
2033
|
+
* pass is deliberately not done: the cascade's shape is only knowable per
|
|
2034
|
+
* record, and the deadlock it leaves is detected and retryable. The keys
|
|
2035
|
+
* the preflight *did* take are held across the whole batch, cascade
|
|
2036
|
+
* included — a per-record destroy holds its scope lock across one delete
|
|
2037
|
+
* and one compaction, while here the hold spans batch size × cascade depth.
|
|
2038
|
+
* A smaller `batchSize` shortens it (the default is 10).
|
|
2039
|
+
*/
|
|
2040
|
+
async lockedBatches(batchSize, perBatch, sortableIncomingAttributes) {
|
|
2006
2041
|
// the driver seam refuses this combination too, but catching it here means
|
|
2007
2042
|
// failing before any transaction is opened or any row is touched
|
|
2008
2043
|
if (this.distinctColumn)
|
|
@@ -2031,10 +2066,27 @@ export default class Query {
|
|
|
2031
2066
|
const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
|
|
2032
2067
|
if (!candidateIds.length)
|
|
2033
2068
|
return { attempted: 0, processed: 0, lastId };
|
|
2069
|
+
// On a sortable model, every advisory scope lock this batch will need is
|
|
2070
|
+
// taken here, sorted, before a single row is claimed. Claiming first and
|
|
2071
|
+
// locking per record inside `perBatch` is the lock-order inversion that
|
|
2072
|
+
// deadlocks against an ordinary concurrent writer of the same scope,
|
|
2073
|
+
// which holds the advisory lock while waiting for the row.
|
|
2074
|
+
//
|
|
2075
|
+
// The ordering this establishes is per batch. A multi-batch run inside a
|
|
2076
|
+
// caller-owned transaction keeps batch N-1's row locks while batch N
|
|
2077
|
+
// preflights, and is a documented fallback rather than a guarantee.
|
|
2078
|
+
await acquireStabilizedSortableBatchLocks(this.dreamInstance, txn, candidateIds, sortableIncomingAttributes);
|
|
2034
2079
|
const claimed = await this.dbDriverInstance(batchQuery.txn(txn).where({ [primaryKey]: candidateIds })).takeAll({ lock: true });
|
|
2080
|
+
const processed = await perBatch(claimed, txn);
|
|
2081
|
+
// The preflight stashed the rows it read so this batch's per-record
|
|
2082
|
+
// sortable work could consume them instead of re-reading each one. They
|
|
2083
|
+
// describe this batch's candidates as they were before it ran, so they
|
|
2084
|
+
// are worth nothing to whatever the enclosing transaction does next —
|
|
2085
|
+
// and a caller-owned transaction outlives the batch.
|
|
2086
|
+
invalidateSortableRowCache(txn);
|
|
2035
2087
|
return {
|
|
2036
2088
|
attempted: candidateIds.length,
|
|
2037
|
-
processed
|
|
2089
|
+
processed,
|
|
2038
2090
|
lastId: candidateIds.at(-1),
|
|
2039
2091
|
};
|
|
2040
2092
|
};
|
|
@@ -2137,7 +2189,7 @@ export default class Query {
|
|
|
2137
2189
|
* use {@link Query.destroy} instead.
|
|
2138
2190
|
*
|
|
2139
2191
|
* ```ts
|
|
2140
|
-
* await User.where({ email: ops.ilike('%burpcollaborator%').delete()
|
|
2192
|
+
* await User.where({ email: ops.ilike('%burpcollaborator%') }).delete()
|
|
2141
2193
|
* // 12
|
|
2142
2194
|
* ```
|
|
2143
2195
|
*
|
|
@@ -2186,7 +2238,11 @@ export default class Query {
|
|
|
2186
2238
|
// dirty-state-based, and mutations made directly on the record
|
|
2187
2239
|
// Dream holds would otherwise ride along with the save
|
|
2188
2240
|
(record) => cb(record['clone']())
|
|
2189
|
-
: () => attributesOrCb, { skipHooks }, resolvedBatchSize
|
|
2241
|
+
: () => attributesOrCb, { skipHooks }, resolvedBatchSize,
|
|
2242
|
+
// only the attributes form knows what it is going to write before the
|
|
2243
|
+
// claim; the callback form computes it inside the batch, after the row
|
|
2244
|
+
// locks are taken, and is a documented preflight fallback
|
|
2245
|
+
cb ? undefined : attributesOrCb);
|
|
2190
2246
|
}
|
|
2191
2247
|
if (!cb && skipHooks)
|
|
2192
2248
|
return await this.updateWithoutCallingModelHooks(attributesOrCb);
|
|
@@ -2243,7 +2299,7 @@ export default class Query {
|
|
|
2243
2299
|
* thrown on every join-load query (leftJoinPreload always carries left-join
|
|
2244
2300
|
* statements).
|
|
2245
2301
|
*/
|
|
2246
|
-
async lockedUpdate(cb, { skipHooks }, batchSize) {
|
|
2302
|
+
async lockedUpdate(cb, { skipHooks }, batchSize, sortableIncomingAttributes) {
|
|
2247
2303
|
return await this.lockedBatches(batchSize, async (claimed, txn) => {
|
|
2248
2304
|
let written = 0;
|
|
2249
2305
|
for (const record of claimed) {
|
|
@@ -2258,10 +2314,16 @@ export default class Query {
|
|
|
2258
2314
|
written++;
|
|
2259
2315
|
}
|
|
2260
2316
|
return written;
|
|
2261
|
-
});
|
|
2317
|
+
}, sortableIncomingAttributes);
|
|
2262
2318
|
}
|
|
2263
2319
|
async updateWithoutCallingModelHooks(attributes) {
|
|
2264
|
-
|
|
2320
|
+
// the guard and the driver must read the same values, so the caller's object
|
|
2321
|
+
// is materialized once here and both read the snapshot
|
|
2322
|
+
const snapshot = { ...attributes };
|
|
2323
|
+
// this path compiles to a single UPDATE statement and never instantiates a
|
|
2324
|
+
// model, so the setter that encrypts an @Encrypted property never runs
|
|
2325
|
+
assertNoEncryptedColumnWrites(this.dreamClass, snapshot);
|
|
2326
|
+
return await this.dbDriverInstance().update(snapshot);
|
|
2265
2327
|
}
|
|
2266
2328
|
invertOrder() {
|
|
2267
2329
|
let query = this.clone({ order: null });
|