@rvoh/dream 2.26.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 -17
- 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/DreamClassTransactionBuilder.js +0 -18
- package/dist/cjs/src/dream/Query.js +64 -32
- package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +19 -12
- 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 -17
- 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/DreamClassTransactionBuilder.js +0 -18
- package/dist/esm/src/dream/Query.js +64 -32
- package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
- package/dist/esm/src/dream/QueryDriver/Kysely.js +19 -12
- 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 +35 -2
- 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/DreamClassTransactionBuilder.d.ts +3 -2
- package/dist/types/src/dream/Query.d.ts +44 -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/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/dist/types/src/types/associations/shared.d.ts +1 -1
- package/dist/types/src/types/dream.d.ts +3 -0
- package/dist/types/src/types/query.d.ts +1 -1
- package/dist/types/src/types/types/associations/shared.ts +8 -6
- package/dist/types/src/types/types/dream.ts +6 -0
- package/dist/types/src/types/types/query.ts +1 -1
- 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 +159 -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 +122 -99
- 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
|
}
|
|
@@ -747,24 +747,6 @@ export default class DreamClassTransactionBuilder {
|
|
|
747
747
|
async pluck(...columnNames) {
|
|
748
748
|
return (await this.queryInstance().pluck(...columnNames));
|
|
749
749
|
}
|
|
750
|
-
/**
|
|
751
|
-
* Plucks the specified fields from the given dream class table
|
|
752
|
-
* in batches, passing each found columns into the provided callback function.
|
|
753
|
-
*
|
|
754
|
-
* ```ts
|
|
755
|
-
* await ApplicationModel.transaction(async txn => {
|
|
756
|
-
* await User.txn(txn).order('id').pluckEach('id', (id) => {
|
|
757
|
-
* console.log(id)
|
|
758
|
-
* })
|
|
759
|
-
* // 1
|
|
760
|
-
* // 2
|
|
761
|
-
* // 3
|
|
762
|
-
* })
|
|
763
|
-
* ```
|
|
764
|
-
*
|
|
765
|
-
* @param fields - A list of fields to pluck, followed by a callback function to call for each set of found fields
|
|
766
|
-
* @returns void
|
|
767
|
-
*/
|
|
768
750
|
async pluckEach(...args) {
|
|
769
751
|
await this.queryInstance().pluckEach(...args);
|
|
770
752
|
}
|
|
@@ -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';
|
|
@@ -1338,29 +1340,6 @@ export default class Query {
|
|
|
1338
1340
|
const vals = await this.dbDriverInstance().pluck(...columnNames);
|
|
1339
1341
|
return (columnNames.length > 1 ? vals : vals.flat());
|
|
1340
1342
|
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Plucks the specified column names from the given dream class table
|
|
1343
|
-
* in batches, passing each found columns into the
|
|
1344
|
-
* provided callback function
|
|
1345
|
-
*
|
|
1346
|
-
* ```ts
|
|
1347
|
-
* await User.order('id').pluckEach('id', (id) => {
|
|
1348
|
-
* console.log(id)
|
|
1349
|
-
* })
|
|
1350
|
-
* // 1
|
|
1351
|
-
* // 2
|
|
1352
|
-
* // 3
|
|
1353
|
-
* ```
|
|
1354
|
-
*
|
|
1355
|
-
* A `limit` or `offset` on the Query cannot compose with the batch windows
|
|
1356
|
-
* — each window replaces the limit with the batch size and the offset with
|
|
1357
|
-
* the running window offset, silently ignoring both — so a limit- or
|
|
1358
|
-
* offset-carrying Query throws at runtime.
|
|
1359
|
-
*
|
|
1360
|
-
* @param args - a list of column names to pluck, followed by a callback function to call for each set of found fields
|
|
1361
|
-
* @returns void
|
|
1362
|
-
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
1363
|
-
*/
|
|
1364
1343
|
async pluckEach(...args) {
|
|
1365
1344
|
const providedCbIndex = args.findIndex(v => typeof v === 'function');
|
|
1366
1345
|
const providedCb = args[providedCbIndex];
|
|
@@ -1901,9 +1880,11 @@ export default class Query {
|
|
|
1901
1880
|
* read is a plain `FOR UPDATE`: if another transaction already holds a lock
|
|
1902
1881
|
* on one of the batch's rows, this call blocks until that transaction ends,
|
|
1903
1882
|
* holding its own transaction and its pooled connection open the whole
|
|
1904
|
-
* time. Dream sets no `lock_timeout` or `statement_timeout
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
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
|
|
1907
1888
|
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1908
1889
|
* hanging.
|
|
1909
1890
|
* - **A `limit` or `offset` on the Query is incompatible with `destroy` on
|
|
@@ -2024,8 +2005,38 @@ export default class Query {
|
|
|
2024
2005
|
* When the Query already carries a transaction, that transaction is used for
|
|
2025
2006
|
* every batch rather than opening one per batch, so the caller's transaction
|
|
2026
2007
|
* boundary wins and the locks are held until the caller commits.
|
|
2027
|
-
|
|
2028
|
-
|
|
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) {
|
|
2029
2040
|
// the driver seam refuses this combination too, but catching it here means
|
|
2030
2041
|
// failing before any transaction is opened or any row is touched
|
|
2031
2042
|
if (this.distinctColumn)
|
|
@@ -2054,10 +2065,27 @@ export default class Query {
|
|
|
2054
2065
|
const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
|
|
2055
2066
|
if (!candidateIds.length)
|
|
2056
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);
|
|
2057
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);
|
|
2058
2086
|
return {
|
|
2059
2087
|
attempted: candidateIds.length,
|
|
2060
|
-
processed
|
|
2088
|
+
processed,
|
|
2061
2089
|
lastId: candidateIds.at(-1),
|
|
2062
2090
|
};
|
|
2063
2091
|
};
|
|
@@ -2209,7 +2237,11 @@ export default class Query {
|
|
|
2209
2237
|
// dirty-state-based, and mutations made directly on the record
|
|
2210
2238
|
// Dream holds would otherwise ride along with the save
|
|
2211
2239
|
(record) => cb(record['clone']())
|
|
2212
|
-
: () => 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);
|
|
2213
2245
|
}
|
|
2214
2246
|
if (!cb && skipHooks)
|
|
2215
2247
|
return await this.updateWithoutCallingModelHooks(attributesOrCb);
|
|
@@ -2266,7 +2298,7 @@ export default class Query {
|
|
|
2266
2298
|
* thrown on every join-load query (leftJoinPreload always carries left-join
|
|
2267
2299
|
* statements).
|
|
2268
2300
|
*/
|
|
2269
|
-
async lockedUpdate(cb, { skipHooks }, batchSize) {
|
|
2301
|
+
async lockedUpdate(cb, { skipHooks }, batchSize, sortableIncomingAttributes) {
|
|
2270
2302
|
return await this.lockedBatches(batchSize, async (claimed, txn) => {
|
|
2271
2303
|
let written = 0;
|
|
2272
2304
|
for (const record of claimed) {
|
|
@@ -2281,7 +2313,7 @@ export default class Query {
|
|
|
2281
2313
|
written++;
|
|
2282
2314
|
}
|
|
2283
2315
|
return written;
|
|
2284
|
-
});
|
|
2316
|
+
}, sortableIncomingAttributes);
|
|
2285
2317
|
}
|
|
2286
2318
|
async updateWithoutCallingModelHooks(attributes) {
|
|
2287
2319
|
return await this.dbDriverInstance().update(attributes);
|