@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
|
@@ -1,35 +1,192 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import Query from '../../../../dream/Query.js';
|
|
3
|
+
import SortableRequiresAdvisoryTransactionLocks from '../../../../errors/SortableRequiresAdvisoryTransactionLocks.js';
|
|
4
|
+
import { STI_SCOPE_NAME } from '../../../class/STI.js';
|
|
5
|
+
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
6
|
+
import canonicalScopeValue from './canonicalScopeValue.js';
|
|
7
|
+
import filterQueryToScopeValues from './filterQueryToScopeValues.js';
|
|
8
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
9
|
+
import sortableScopeLockKey from './sortableScopeLockKey.js';
|
|
10
|
+
/**
|
|
11
|
+
* The alias and column names the renumbering UPDATE joins its `row_number()`
|
|
12
|
+
* subquery on. They are namespaced so that no real column of the table being
|
|
13
|
+
* resorted can collide with them.
|
|
14
|
+
*/
|
|
15
|
+
const RENUMBER_ALIAS = 'dream_resort_renumber';
|
|
16
|
+
const RENUMBER_PRIMARY_KEY = 'dream_resort_primary_key';
|
|
17
|
+
const RENUMBER_POSITION = 'dream_resort_position';
|
|
18
|
+
/**
|
|
19
|
+
* The aliases the one-row summary of a sort scope's positions comes back under.
|
|
20
|
+
* They are namespaced for the same reason the renumbering aliases are, and are
|
|
21
|
+
* lowercase without underscores so that Kysely's CamelCasePlugin passes them
|
|
22
|
+
* through untouched and each value can be read straight off the result row (the
|
|
23
|
+
* same alias strategy `pluck` and the grouped aggregates use).
|
|
24
|
+
*/
|
|
25
|
+
const SUMMARY_ROW_COUNT = 'dreamresortrowcount';
|
|
26
|
+
const SUMMARY_DISTINCT_POSITIONS = 'dreamresortdistinctpositions';
|
|
27
|
+
const SUMMARY_MIN_POSITION = 'dreamresortminposition';
|
|
28
|
+
const SUMMARY_MAX_POSITION = 'dreamresortmaxposition';
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
*
|
|
32
|
+
* Renumbers every sort scope of a model so that each one runs 1..n with no gaps.
|
|
33
|
+
*
|
|
34
|
+
* Four things make this safe to run against a live table:
|
|
35
|
+
*
|
|
36
|
+
* - **A driver that cannot take the scope lock fails before the table is
|
|
37
|
+
* read**, rather than succeeding for whichever scopes happen to be contiguous
|
|
38
|
+
* and throwing on the first one that is not.
|
|
39
|
+
* - **The STI scope is stripped**, exactly as every sortable hook strips it. An
|
|
40
|
+
* STI hierarchy shares one position space, so renumbering one child's rows in
|
|
41
|
+
* isolation would hand them positions its siblings already hold.
|
|
42
|
+
* - **Groups are identified structurally**, by the scope columns' `[column,
|
|
43
|
+
* value]` tuple rather than by joining the values into one string. Joining
|
|
44
|
+
* merges scopes whose values differ only in where a delimiter falls, and
|
|
45
|
+
* merges a null member with an empty-string one; a merged group is two
|
|
46
|
+
* physical sort scopes renumbered as though they were one, and no single lock
|
|
47
|
+
* key covers it.
|
|
48
|
+
* - **Every discovered scope is renumbered under its own scope lock, from a
|
|
49
|
+
* read taken inside that lock.** The unlocked pass below only discovers
|
|
50
|
+
* *which* sort scopes exist; it decides nothing about their contents, because
|
|
51
|
+
* a concurrent create, destroy or raw update between the two reads is
|
|
52
|
+
* precisely the case this exists to repair. Whether a scope needs renumbering
|
|
53
|
+
* at all is decided under the lock.
|
|
54
|
+
*
|
|
55
|
+
* Each scope's transaction is this function's own: `resort` cannot join a
|
|
56
|
+
* transaction the caller owns, and must not be called from inside one. It would
|
|
57
|
+
* wait on a connection of its own for a scope lock the caller's transaction may
|
|
58
|
+
* already hold — a wait no deadlock detector can see, which ends in
|
|
59
|
+
* `SortableScopeLockWaitTimedOut` rather than in progress.
|
|
60
|
+
*/
|
|
4
61
|
export default async function resortAllRecords(dreamClass, positionField, scope) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
for (const
|
|
13
|
-
const dreams = dreamArr;
|
|
14
|
-
if (isSortedCorrectly(dreams, positionField))
|
|
15
|
-
continue;
|
|
62
|
+
const queryDriverClass = Query.dbDriverClass(dreamClass.prototype.connectionName || 'default');
|
|
63
|
+
if (!queryDriverClass.supportsAdvisoryTransactionLocks)
|
|
64
|
+
throw new SortableRequiresAdvisoryTransactionLocks(queryDriverClass.name);
|
|
65
|
+
// every scope value, lock key and group identity below is derived from class
|
|
66
|
+
// level metadata — the table, the connection, the scope members' columns and
|
|
67
|
+
// their database types — so one bare instance stands in for every row
|
|
68
|
+
const derivationRecord = dreamClass.new();
|
|
69
|
+
for (const scopeValues of await discoverSortScopes(dreamClass, derivationRecord, scope)) {
|
|
16
70
|
await dreamClass.transaction(async (txn) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.set({
|
|
24
|
-
[positionField]: counter++,
|
|
25
|
-
})
|
|
26
|
-
.execute();
|
|
27
|
-
}
|
|
71
|
+
await acquireSortableScopeLocks(derivationRecord, txn, [
|
|
72
|
+
sortableScopeLockKey(derivationRecord, positionField, scopeValues),
|
|
73
|
+
]);
|
|
74
|
+
if (await scopeIsAlreadyOrdered({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn }))
|
|
75
|
+
return;
|
|
76
|
+
await renumberScope({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn });
|
|
28
77
|
});
|
|
29
78
|
}
|
|
30
79
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Every physical sort scope the table currently holds, as the `[column, value]`
|
|
82
|
+
* tuples a lock key is derived from.
|
|
83
|
+
*
|
|
84
|
+
* One distinct read over the scope columns alone, which both bounds the cost —
|
|
85
|
+
* a single round trip returning one row per sort scope, however many rows the
|
|
86
|
+
* table holds — and makes the answer stable: a walk that windows the table can
|
|
87
|
+
* be shifted past rows by a concurrent delete, and a sort scope whose only rows
|
|
88
|
+
* fall in that gap would never be renumbered at all. Each discovered scope's
|
|
89
|
+
* rows are read again under its own lock before anything is written.
|
|
90
|
+
*
|
|
91
|
+
* A sortable field with no scope has exactly one sort scope, the whole table,
|
|
92
|
+
* so its answer needs no read.
|
|
93
|
+
*/
|
|
94
|
+
async function discoverSortScopes(dreamClass, derivationRecord, scope) {
|
|
95
|
+
const columns = sortableScopeColumns(derivationRecord, scope);
|
|
96
|
+
if (columns.length === 0)
|
|
97
|
+
return [[]];
|
|
98
|
+
const rows = (await stiUnscopedQuery(dreamClass)
|
|
99
|
+
.toKysely('select')
|
|
100
|
+
.clearSelect()
|
|
101
|
+
.distinct()
|
|
102
|
+
.select(columns)
|
|
103
|
+
.execute());
|
|
104
|
+
// the database's `distinct` is by raw value; two raw values the column's type
|
|
105
|
+
// treats as one (a citext scope column matching case-insensitively) are one
|
|
106
|
+
// sort scope under one lock key, and canonicalization is what says so
|
|
107
|
+
const sortScopes = new Map();
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
const scopeValues = columns.map((column) => [column, row[column]]);
|
|
110
|
+
const identity = scopeIdentity(derivationRecord, scopeValues);
|
|
111
|
+
if (!sortScopes.has(identity))
|
|
112
|
+
sortScopes.set(identity, scopeValues);
|
|
113
|
+
}
|
|
114
|
+
return [...sortScopes.values()];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether the sort scope already runs 1..n, decided inside the scope's lock by
|
|
118
|
+
* one aggregate row rather than by shipping every position in the scope to the
|
|
119
|
+
* client: a sort scope can hold as many rows as the table does, and this runs
|
|
120
|
+
* with the scope's lock held.
|
|
121
|
+
*
|
|
122
|
+
* A scope of `n` rows is already correct exactly when its positions are `n`
|
|
123
|
+
* distinct non-null values running from 1 to n. `count(distinct position)`
|
|
124
|
+
* drops both duplicates and nulls, so it agrees with `count(*)` only when every
|
|
125
|
+
* row holds a position of its own, and `min`/`max` pin that set to 1..n. An
|
|
126
|
+
* empty scope — one whose last row was destroyed between its discovery and this
|
|
127
|
+
* lock — has nothing to renumber.
|
|
128
|
+
*/
|
|
129
|
+
async function scopeIsAlreadyOrdered({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn, }) {
|
|
130
|
+
const summary = (await scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn })
|
|
131
|
+
.clearSelect()
|
|
132
|
+
.select([
|
|
133
|
+
sql `count(*)`.as(SUMMARY_ROW_COUNT),
|
|
134
|
+
sql `count(distinct ${sql.ref(positionField)})`.as(SUMMARY_DISTINCT_POSITIONS),
|
|
135
|
+
sql `min(${sql.ref(positionField)})`.as(SUMMARY_MIN_POSITION),
|
|
136
|
+
sql `max(${sql.ref(positionField)})`.as(SUMMARY_MAX_POSITION),
|
|
137
|
+
])
|
|
138
|
+
.executeTakeFirstOrThrow());
|
|
139
|
+
// counts come back from some drivers as strings, since a count is a bigint
|
|
140
|
+
const rowCount = Number(summary[SUMMARY_ROW_COUNT]);
|
|
141
|
+
if (rowCount === 0)
|
|
142
|
+
return true;
|
|
143
|
+
return (Number(summary[SUMMARY_DISTINCT_POSITIONS]) === rowCount &&
|
|
144
|
+
Number(summary[SUMMARY_MIN_POSITION]) === 1 &&
|
|
145
|
+
Number(summary[SUMMARY_MAX_POSITION]) === rowCount);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Renumbers the whole sort scope in one statement, from a `row_number()` taken
|
|
149
|
+
* over the scope's rows ordered by position, nulls first, ties broken by
|
|
150
|
+
* primary key. A row already holding the position it is owed is left alone, so
|
|
151
|
+
* a scope with a single gap in it writes a single row rather than all of them.
|
|
152
|
+
*/
|
|
153
|
+
async function renumberScope({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn, }) {
|
|
154
|
+
const table = derivationRecord.table;
|
|
155
|
+
const primaryKey = derivationRecord['_primaryKey'];
|
|
156
|
+
const renumbered = scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn })
|
|
157
|
+
.clearSelect()
|
|
158
|
+
.select([
|
|
159
|
+
sql.ref(primaryKey).as(RENUMBER_PRIMARY_KEY),
|
|
160
|
+
sql `row_number() over (order by ${sql.ref(positionField)} asc nulls first, ${sql.ref(primaryKey)} asc)`.as(RENUMBER_POSITION),
|
|
161
|
+
]);
|
|
162
|
+
await stiUnscopedQuery(dreamClass)
|
|
163
|
+
.txn(txn)
|
|
164
|
+
.toKysely('update')
|
|
165
|
+
.from(renumbered.as(RENUMBER_ALIAS))
|
|
166
|
+
.set({ [positionField]: sql.ref(`${RENUMBER_ALIAS}.${RENUMBER_POSITION}`) })
|
|
167
|
+
.whereRef(`${table}.${primaryKey}`, '=', `${RENUMBER_ALIAS}.${RENUMBER_PRIMARY_KEY}`)
|
|
168
|
+
.where(sql.ref(`${table}.${positionField}`), 'is distinct from', sql.ref(`${RENUMBER_ALIAS}.${RENUMBER_POSITION}`))
|
|
169
|
+
.execute();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A kysely select over the rows of one physical sort scope, carrying the
|
|
173
|
+
* model's default scopes (a soft-deleted row is not part of any sort scope) but
|
|
174
|
+
* not the STI scope, and matching a null scope member with `is null`.
|
|
175
|
+
*/
|
|
176
|
+
function scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn, }) {
|
|
177
|
+
const values = new Map(scopeValues);
|
|
178
|
+
return filterQueryToScopeValues(derivationRecord, stiUnscopedQuery(dreamClass).txn(txn).toKysely('select'), column => values.get(column) ?? null, scope);
|
|
179
|
+
}
|
|
180
|
+
function stiUnscopedQuery(dreamClass) {
|
|
181
|
+
return dreamClass.query().removeDefaultScope(STI_SCOPE_NAME);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A collision-free identity for a physical sort scope. Values are canonicalized
|
|
185
|
+
* against the column's database type by the same function the advisory lock key
|
|
186
|
+
* uses (null distinct from every string, a pending value agreeing with its
|
|
187
|
+
* hydrated counterpart) and encoded structurally, so two distinct tuples can
|
|
188
|
+
* never share an identity and a scope can never disagree with its own lock.
|
|
189
|
+
*/
|
|
190
|
+
function scopeIdentity(derivationRecord, scopeValues) {
|
|
191
|
+
return JSON.stringify(scopeValues.map(([column, value]) => [column, canonicalScopeValue(derivationRecord, column, value)]));
|
|
35
192
|
}
|
|
@@ -1,62 +1,116 @@
|
|
|
1
|
-
import DreamApp from '../../../../dream-app/index.js';
|
|
2
|
-
import PostgresQueryDriver from '../../../../dream/QueryDriver/Postgres.js';
|
|
3
1
|
import range from '../../../../helpers/range.js';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import ops from '../../../../ops/index.js';
|
|
3
|
+
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
4
|
+
import canonicalScopeValue from './canonicalScopeValue.js';
|
|
5
|
+
import filterQueryToScopeValues from './filterQueryToScopeValues.js';
|
|
6
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
7
|
+
import { sortableScopeLockKeyForCurrentScope } from './sortableScopeLockKeys.js';
|
|
8
|
+
import { invalidateSortableRowCache } from './sortableRowCache.js';
|
|
9
|
+
import { snapshotScopeValue } from './sortableSnapshot.js';
|
|
6
10
|
import validPosition from './validPosition.js';
|
|
7
11
|
export default async function setPosition(obj) {
|
|
8
12
|
const { dream, txn } = obj;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await dream.txn(txn).reload();
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
await dream.constructor.transaction(async (txn) => {
|
|
15
|
-
await applyUpdates({ ...obj, txn });
|
|
16
|
-
await dream.txn(txn).reload();
|
|
17
|
-
});
|
|
18
|
-
}
|
|
13
|
+
await applyUpdates(obj);
|
|
14
|
+
await dream.txn(txn).reload();
|
|
19
15
|
}
|
|
20
16
|
async function applyUpdates(obj) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
const { dream, positionField, scope, txn } = obj;
|
|
18
|
+
// Serialize every writer of this sort scope for the rest of the transaction,
|
|
19
|
+
// before the first position read. The operation-level phase has already
|
|
20
|
+
// acquired this key in its one sorted pass over every sortable field's keys
|
|
21
|
+
// (performSortablePositionWork on create; saveDream, before the driver
|
|
22
|
+
// write, on update), so this asks for a key the transaction holds and issues
|
|
23
|
+
// no statement at all.
|
|
24
|
+
await acquireSortableScopeLocks(dream, txn, [
|
|
25
|
+
sortableScopeLockKeyForCurrentScope(dream, positionField, scope),
|
|
26
|
+
]);
|
|
27
|
+
// A pending scope change that lands on the scope the row already physically
|
|
28
|
+
// occupies moves the record between no scopes at all, and the rest of the
|
|
29
|
+
// write has to see it that way: the scope being vacated *is* the destination,
|
|
30
|
+
// so one source computes a new position from a max the other is about to
|
|
31
|
+
// shift out from under it. The record still counts toward the destination's
|
|
32
|
+
// extent — its row is in the scope, holding the position sentinel in place of
|
|
33
|
+
// the position the snapshot read — so the position write carries that
|
|
34
|
+
// position through to the clamp, and a record that was already last in the
|
|
35
|
+
// scope stays last rather than swapping with the record below it.
|
|
36
|
+
const write = alreadyInDestinationScope(obj)
|
|
37
|
+
? { ...obj, changingScope: false, positionUnderSentinel: obj.previousPosition }
|
|
38
|
+
: obj;
|
|
39
|
+
const position = await validPosition(write);
|
|
40
|
+
await updateConflictingRecords({ ...write, position });
|
|
41
|
+
await updatePositionForRecord({ ...write, position });
|
|
42
|
+
// the shift above moved rows this transaction may be holding preflight reads
|
|
43
|
+
// of — every position a locked batch's later records were read with is now a
|
|
44
|
+
// position they no longer have
|
|
45
|
+
invalidateSortableRowCache(txn);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a save that means to change sort scope is really a move within one
|
|
49
|
+
* scope: the snapshot read under the lock says the row already sits in the
|
|
50
|
+
* scope the instance is being written into, so nothing joins the destination
|
|
51
|
+
* and nothing is left behind.
|
|
52
|
+
*
|
|
53
|
+
* Values are compared through `canonicalScopeValue`, the same normalization the
|
|
54
|
+
* advisory lock key is derived from, so two shapes of one physical value — the
|
|
55
|
+
* pending `'1.00'` and the hydrated `1` — are one scope here exactly as they
|
|
56
|
+
* are when the key is taken.
|
|
57
|
+
*
|
|
58
|
+
* A row whose snapshot position is null holds no position in that scope, so it
|
|
59
|
+
* is joining the destination's position space and remains a scope change.
|
|
60
|
+
*/
|
|
61
|
+
function alreadyInDestinationScope({ changingScope, dream, previousPosition, scope, snapshot, }) {
|
|
62
|
+
if (!changingScope || snapshot === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
if (previousPosition === undefined || previousPosition === null)
|
|
65
|
+
return false;
|
|
66
|
+
const columns = sortableScopeColumns(dream, scope);
|
|
67
|
+
if (!columns.length)
|
|
68
|
+
return false;
|
|
69
|
+
return columns.every(column => snapshot.scopeValues.has(column) &&
|
|
70
|
+
canonicalScopeValue(dream, column, snapshot.scopeValues.get(column)) ===
|
|
71
|
+
canonicalScopeValue(dream, column, dream[column]));
|
|
24
72
|
}
|
|
25
73
|
async function updateConflictingRecords(obj) {
|
|
26
|
-
const { wasNewRecord, position, previousPosition, dream, positionField, query, scope, txn, changingScope } = obj;
|
|
74
|
+
const { wasNewRecord, position, previousPosition, dream, positionField, query, scope, txn, changingScope, snapshot, } = obj;
|
|
75
|
+
// A scope move spans two unrelated position spaces: `previousPosition` is a
|
|
76
|
+
// position in the scope being left, `position` one in the scope being
|
|
77
|
+
// entered. Only the scope being left is compacted here, and it compacts all
|
|
78
|
+
// the way up — bounding it by a position from the other space closes nothing
|
|
79
|
+
// at all whenever the destination is the smaller of the two, leaving a
|
|
80
|
+
// permanent hole. The destination needs no room made: a scope-changing save
|
|
81
|
+
// always lands at the end of it, because `afterSortableUpdate` passes no
|
|
82
|
+
// position and `validPosition` then returns the destination's max + 1.
|
|
83
|
+
//
|
|
84
|
+
// A record with no position to vacate — a soft-deleted row, whose position
|
|
85
|
+
// column is NULL — leaves the scope it is moving out of untouched.
|
|
86
|
+
if (changingScope && (previousPosition === undefined || previousPosition === null))
|
|
87
|
+
return;
|
|
27
88
|
const increasing = changingScope ||
|
|
28
89
|
position === undefined ||
|
|
29
90
|
(previousPosition !== undefined && previousPosition !== null && previousPosition < position);
|
|
91
|
+
const conflictingPositions = changingScope
|
|
92
|
+
? ops.greaterThanOrEqualTo(previousPosition)
|
|
93
|
+
: increasing
|
|
94
|
+
? range(previousPosition, position)
|
|
95
|
+
: range(position, previousPosition);
|
|
30
96
|
let kyselyQuery = query
|
|
31
97
|
.txn(txn)
|
|
32
98
|
.whereNot({ [dream['_primaryKey']]: dream.primaryKeyValue() })
|
|
33
99
|
.where({
|
|
34
|
-
[positionField]:
|
|
100
|
+
[positionField]: conflictingPositions,
|
|
35
101
|
})
|
|
36
102
|
.toKysely('update')
|
|
37
103
|
.set((eb) => ({
|
|
38
104
|
[positionField]: eb(positionField, increasing ? '-' : '+', 1),
|
|
39
105
|
}));
|
|
40
|
-
|
|
106
|
+
// the range being compacted belongs to the scope the record is *leaving*, so
|
|
107
|
+
// the filter has to name that scope's real column values — the snapshot read
|
|
108
|
+
// under the lock before the write, not what the instance remembers
|
|
109
|
+
kyselyQuery = filterQueryToScopeValues(dream, kyselyQuery, column => snapshotScopeValue(snapshot, column, () => !wasNewRecord && dream.savedChangeToAttribute(column)
|
|
41
110
|
? (dream.changes()[column]?.was ?? dream[column])
|
|
42
|
-
: dream[column], scope);
|
|
111
|
+
: dream[column]), scope);
|
|
43
112
|
await kyselyQuery.execute();
|
|
44
113
|
}
|
|
45
|
-
export function applySortableScopesToQuery(dream, kyselyQuery, whereValueCB, scope) {
|
|
46
|
-
for (const singleScope of scopeArray(scope)) {
|
|
47
|
-
const column = getColumnForSortableScope(dream, singleScope);
|
|
48
|
-
if (column) {
|
|
49
|
-
const columnValue = whereValueCB(column);
|
|
50
|
-
if (columnValue === null) {
|
|
51
|
-
kyselyQuery = kyselyQuery.where(column, 'is', null);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
kyselyQuery = kyselyQuery.where(column, '=', columnValue);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return kyselyQuery;
|
|
59
|
-
}
|
|
60
114
|
async function updatePositionForRecord(obj) {
|
|
61
115
|
const { txn, dream, positionField, position } = obj;
|
|
62
116
|
await txn.kyselyTransaction
|
|
@@ -67,13 +121,3 @@ async function updatePositionForRecord(obj) {
|
|
|
67
121
|
})
|
|
68
122
|
.execute();
|
|
69
123
|
}
|
|
70
|
-
export function getPostgresQueryDriver(connectionName) {
|
|
71
|
-
const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(connectionName);
|
|
72
|
-
if (!(queryDriverClass === PostgresQueryDriver)) {
|
|
73
|
-
throw new Error(`
|
|
74
|
-
${queryDriverClass.name} is not an instance of PostgresQueryDriver.
|
|
75
|
-
You must be using the PostgresQueryDriver when leveraging the Sortable decorator
|
|
76
|
-
`);
|
|
77
|
-
}
|
|
78
|
-
return queryDriverClass;
|
|
79
|
-
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The cached rows of every live transaction, keyed by the `DreamTransaction`
|
|
2
|
+
// object itself. Module-level state is safe here because the key is an object
|
|
3
|
+
// identity: every transaction — including a nested one — is its own
|
|
4
|
+
// `DreamTransaction` instance, so no lookup can ever see rows stashed by
|
|
5
|
+
// another transaction, whatever request or async flow it belongs to. The keys
|
|
6
|
+
// are held weakly, so an entry becomes collectible with its transaction and
|
|
7
|
+
// nothing accumulates for the life of the process. (The lock ledger in
|
|
8
|
+
// `heldSortableScopeLockKeys.ts` relies on the same properties.)
|
|
9
|
+
const rowsByTransaction = new WeakMap();
|
|
10
|
+
// a JSON tuple rather than a joined string, so that no table name and no
|
|
11
|
+
// primary key value can combine into another pair's key
|
|
12
|
+
function cacheKey(table, primaryKeyValue) {
|
|
13
|
+
return JSON.stringify([table, String(primaryKeyValue)]);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*
|
|
18
|
+
* Stashes rows a locked batch's preflight read under the batch's scope locks,
|
|
19
|
+
* so that the per-record position work that follows does not read them a second
|
|
20
|
+
* time. The preflight reads every candidate's position and sort scope columns
|
|
21
|
+
* to derive the key set; each record's own snapshot read then asks the same
|
|
22
|
+
* question of the same row, in the same transaction, under the same locks.
|
|
23
|
+
*
|
|
24
|
+
* Called once per preflight, with the rows of the pass that converged: an
|
|
25
|
+
* earlier pass read before some of the locks were held, and a preflight that
|
|
26
|
+
* raised holds no lock the rest of the transaction can rely on. The rows are
|
|
27
|
+
* consumed one at a time and the whole entry is dropped by the first position
|
|
28
|
+
* write of the batch, since a compaction moves rows this holds stale positions
|
|
29
|
+
* for — so what it saves is the snapshot reads of the records walked before
|
|
30
|
+
* that first write.
|
|
31
|
+
*
|
|
32
|
+
* @param txn - the batch's transaction
|
|
33
|
+
* @param table - the table the rows came from
|
|
34
|
+
* @param primaryKey - the column the rows are keyed by
|
|
35
|
+
* @param rows - the rows read, carrying at least the position and scope columns
|
|
36
|
+
*/
|
|
37
|
+
export function cacheSortableRowsReadUnderLock(txn, table, primaryKey, rows) {
|
|
38
|
+
rowsByTransaction.set(txn, new Map(rows.map(row => [cacheKey(table, row[primaryKey]), row])));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*
|
|
43
|
+
* The cached row for one record, removed from the cache as it is handed over.
|
|
44
|
+
*
|
|
45
|
+
* Consuming is destructive because the consumer is a position-mutating
|
|
46
|
+
* operation: it is about to compact or renumber the scope the row sits in, and
|
|
47
|
+
* a second read of the same cached row would see the position the row had
|
|
48
|
+
* before that. {@link invalidateSortableRowCache} covers the same hazard for
|
|
49
|
+
* the *other* records a compaction moves.
|
|
50
|
+
*/
|
|
51
|
+
export function takeCachedSortableRow(txn, table, primaryKeyValue) {
|
|
52
|
+
const rows = rowsByTransaction.get(txn);
|
|
53
|
+
if (!rows)
|
|
54
|
+
return undefined;
|
|
55
|
+
const key = cacheKey(table, primaryKeyValue);
|
|
56
|
+
const row = rows.get(key);
|
|
57
|
+
rows.delete(key);
|
|
58
|
+
return row;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @internal
|
|
62
|
+
*
|
|
63
|
+
* Discards everything cached for this transaction. Called for two reasons: a
|
|
64
|
+
* position write just moved rows the cache cannot identify — a compaction
|
|
65
|
+
* shifts every row in one sort scope above a position, and the cache holds no
|
|
66
|
+
* index by scope — and the batch the rows were read for has finished, after
|
|
67
|
+
* which they describe nothing the rest of a caller-owned transaction should
|
|
68
|
+
* trust. The records still to be walked read their own rows again, which is
|
|
69
|
+
* what they did before the cache existed.
|
|
70
|
+
*/
|
|
71
|
+
export function invalidateSortableRowCache(txn) {
|
|
72
|
+
rowsByTransaction.delete(txn);
|
|
73
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import getColumnForSortableScope from './getColumnForSortableScope.js';
|
|
2
|
+
import scopeArray from './scopeArray.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* The scope members of a sortable field paired with the database columns they
|
|
7
|
+
* resolve to. A member names either the column itself or the BelongsTo
|
|
8
|
+
* association whose foreign key backs it, and a caller matching incoming
|
|
9
|
+
* attributes needs both spellings; a member that resolves to no column is
|
|
10
|
+
* dropped.
|
|
11
|
+
*
|
|
12
|
+
* Every sortable path resolves its scope columns through this one function, so
|
|
13
|
+
* that a lock key, a snapshot read, a group identity and a scope filter can
|
|
14
|
+
* never disagree about which columns a sort scope is made of.
|
|
15
|
+
*/
|
|
16
|
+
export function sortableScopeColumnPairs(dream, scope) {
|
|
17
|
+
return scopeArray(scope).reduce((acc, singleScope) => {
|
|
18
|
+
const column = getColumnForSortableScope(dream, singleScope);
|
|
19
|
+
if (column)
|
|
20
|
+
acc.push([singleScope, column]);
|
|
21
|
+
return acc;
|
|
22
|
+
}, []);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*
|
|
27
|
+
* The database columns a sortable field's scope resolves to.
|
|
28
|
+
*/
|
|
29
|
+
export default function sortableScopeColumns(dream, scope) {
|
|
30
|
+
return sortableScopeColumnPairs(dream, scope).map(([, column]) => column);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @internal
|
|
34
|
+
*
|
|
35
|
+
* The sort scope as `[column, value]` pairs — the shape a lock key is derived
|
|
36
|
+
* from and a group is identified by. The value for each column comes from the
|
|
37
|
+
* caller, which decides whether it is reading the in-memory instance or a
|
|
38
|
+
* snapshot of the row.
|
|
39
|
+
*/
|
|
40
|
+
export function sortableScopeValues(dream, scope, valueForColumn) {
|
|
41
|
+
return sortableScopeColumns(dream, scope).map(column => [column, valueForColumn(column)]);
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|