@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.
Files changed (331) hide show
  1. package/dist/cjs/src/Dream.js +32 -17
  2. package/dist/cjs/src/db/errors.js +4 -0
  3. package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
  4. package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  5. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  6. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  7. package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  8. package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  9. package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  10. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  11. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  12. package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  13. package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  14. package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  15. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  16. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  17. package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  18. package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  19. package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  20. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  21. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  22. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  23. package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  24. package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  25. package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  26. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  27. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  28. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +0 -18
  29. package/dist/cjs/src/dream/Query.js +64 -32
  30. package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
  31. package/dist/cjs/src/dream/QueryDriver/Kysely.js +19 -12
  32. package/dist/cjs/src/dream/QueryDriver/Postgres.js +74 -0
  33. package/dist/cjs/src/dream/internal/destroyDream.js +60 -6
  34. package/dist/cjs/src/dream/internal/saveDream.js +109 -7
  35. package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  36. package/dist/cjs/src/dream/internal/softDeleteDream.js +13 -1
  37. package/dist/cjs/src/dream/internal/undestroyDream.js +56 -8
  38. package/dist/cjs/src/dream-app/index.js +21 -0
  39. package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  40. package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  41. package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
  42. package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  43. package/dist/cjs/src/package-exports/errors.js +3 -0
  44. package/dist/esm/src/Dream.js +32 -17
  45. package/dist/esm/src/db/errors.js +4 -0
  46. package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
  47. package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  48. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  49. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  50. package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  51. package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  52. package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  53. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  54. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  55. package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  56. package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  57. package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  58. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  59. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  60. package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  61. package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  62. package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  63. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  64. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  65. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  66. package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  67. package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  68. package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  69. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  70. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  71. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +0 -18
  72. package/dist/esm/src/dream/Query.js +64 -32
  73. package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
  74. package/dist/esm/src/dream/QueryDriver/Kysely.js +19 -12
  75. package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
  76. package/dist/esm/src/dream/internal/destroyDream.js +60 -6
  77. package/dist/esm/src/dream/internal/saveDream.js +109 -7
  78. package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  79. package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
  80. package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
  81. package/dist/esm/src/dream-app/index.js +21 -0
  82. package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  83. package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  84. package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
  85. package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  86. package/dist/esm/src/package-exports/errors.js +3 -0
  87. package/dist/types/src/Dream.d.ts +35 -2
  88. package/dist/types/src/db/errors.d.ts +2 -0
  89. package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
  90. package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
  91. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
  92. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
  93. package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
  94. package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
  95. package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
  96. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
  97. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
  98. package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
  99. package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
  100. package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
  101. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
  102. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
  103. package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
  104. package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
  105. package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
  106. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
  107. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
  108. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
  109. package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
  110. package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
  111. package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
  112. package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
  113. package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
  114. package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
  115. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -2
  116. package/dist/types/src/dream/Query.d.ts +44 -9
  117. package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
  118. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
  119. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
  120. package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
  121. package/dist/types/src/dream-app/index.d.ts +18 -2
  122. package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
  123. package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
  124. package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
  125. package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
  126. package/dist/types/src/package-exports/errors.d.ts +3 -0
  127. package/dist/types/src/types/associations/shared.d.ts +1 -1
  128. package/dist/types/src/types/dream.d.ts +3 -0
  129. package/dist/types/src/types/query.d.ts +1 -1
  130. package/dist/types/src/types/types/associations/shared.ts +8 -6
  131. package/dist/types/src/types/types/dream.ts +6 -0
  132. package/dist/types/src/types/types/query.ts +1 -1
  133. package/docs/assets/hierarchy.js +1 -1
  134. package/docs/assets/navigation.js +1 -1
  135. package/docs/assets/search.js +1 -1
  136. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  137. package/docs/classes/db.KyselyQueryDriver.html +88 -41
  138. package/docs/classes/db.PostgresQueryDriver.html +82 -42
  139. package/docs/classes/db.QueryDriverBase.html +86 -39
  140. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  141. package/docs/classes/errors.ColumnOverflow.html +3 -3
  142. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  143. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  144. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  145. package/docs/classes/errors.DecryptionError.html +2 -2
  146. package/docs/classes/errors.DecryptionParseError.html +2 -2
  147. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  148. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  149. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  150. package/docs/classes/errors.InvalidClockTime.html +2 -2
  151. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  152. package/docs/classes/errors.InvalidDateTime.html +2 -2
  153. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  154. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  155. package/docs/classes/errors.NotNullViolation.html +3 -3
  156. package/docs/classes/errors.RecordNotFound.html +3 -3
  157. package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +61 -0
  158. package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
  159. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
  160. package/docs/classes/errors.ValidationError.html +3 -3
  161. package/docs/classes/index.CalendarDate.html +33 -33
  162. package/docs/classes/index.ClockTime.html +32 -32
  163. package/docs/classes/index.ClockTimeTz.html +35 -35
  164. package/docs/classes/index.DateTime.html +86 -86
  165. package/docs/classes/index.Decorators.html +19 -19
  166. package/docs/classes/index.Dream.html +159 -124
  167. package/docs/classes/index.DreamApp.html +11 -10
  168. package/docs/classes/index.DreamTransaction.html +2 -2
  169. package/docs/classes/index.Env.html +2 -2
  170. package/docs/classes/index.Query.html +122 -99
  171. package/docs/classes/system.CliFileWriter.html +4 -4
  172. package/docs/classes/system.DreamBin.html +2 -2
  173. package/docs/classes/system.DreamCLI.html +7 -7
  174. package/docs/classes/system.DreamImporter.html +2 -2
  175. package/docs/classes/system.DreamLogos.html +2 -2
  176. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  177. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  178. package/docs/classes/system.PathHelpers.html +3 -3
  179. package/docs/classes/utils.Encrypt.html +3 -3
  180. package/docs/classes/utils.Range.html +2 -2
  181. package/docs/functions/db.closeAllDbConnections.html +1 -1
  182. package/docs/functions/db.dreamDbConnections.html +1 -1
  183. package/docs/functions/db.untypedDb.html +1 -1
  184. package/docs/functions/db.validateColumn.html +1 -1
  185. package/docs/functions/db.validateTable.html +1 -1
  186. package/docs/functions/errors.pgErrorType.html +1 -1
  187. package/docs/functions/index.DreamSerializer.html +1 -1
  188. package/docs/functions/index.ObjectSerializer.html +1 -1
  189. package/docs/functions/index.ReplicaSafe.html +1 -1
  190. package/docs/functions/index.STI.html +1 -1
  191. package/docs/functions/index.SoftDelete.html +1 -1
  192. package/docs/functions/utils.camelize.html +1 -1
  193. package/docs/functions/utils.capitalize.html +1 -1
  194. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  195. package/docs/functions/utils.compact.html +1 -1
  196. package/docs/functions/utils.groupBy.html +1 -1
  197. package/docs/functions/utils.hyphenize.html +1 -1
  198. package/docs/functions/utils.intersection.html +1 -1
  199. package/docs/functions/utils.isEmpty.html +1 -1
  200. package/docs/functions/utils.normalizeUnicode.html +1 -1
  201. package/docs/functions/utils.pascalize.html +1 -1
  202. package/docs/functions/utils.percent.html +1 -1
  203. package/docs/functions/utils.range.html +1 -1
  204. package/docs/functions/utils.round.html +1 -1
  205. package/docs/functions/utils.sanitizeString.html +1 -1
  206. package/docs/functions/utils.snakeify.html +1 -1
  207. package/docs/functions/utils.sort.html +2 -2
  208. package/docs/functions/utils.sortBy.html +1 -1
  209. package/docs/functions/utils.sortObjectByKey.html +1 -1
  210. package/docs/functions/utils.sortObjectByValue.html +1 -1
  211. package/docs/functions/utils.uncapitalize.html +1 -1
  212. package/docs/functions/utils.uniq.html +1 -1
  213. package/docs/hierarchy.html +1 -1
  214. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  215. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  216. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  217. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  218. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  219. package/docs/interfaces/types.DecoratorContext.html +2 -2
  220. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  221. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  222. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  223. package/docs/interfaces/types.DurationObject.html +2 -2
  224. package/docs/interfaces/types.EncryptOptions.html +2 -2
  225. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  226. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  227. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  228. package/docs/modules/errors.html +1 -1
  229. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  230. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  231. package/docs/types/openapi.OpenapiFormats.html +1 -1
  232. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  233. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  234. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  235. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  236. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  237. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  238. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  239. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  240. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  241. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  242. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  243. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  244. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  245. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  246. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  247. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  248. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  249. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  250. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  251. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  252. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  253. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  254. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  255. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  256. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  257. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  258. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  259. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  260. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  261. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  262. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  263. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  264. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  265. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  266. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  267. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  268. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  269. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  270. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  271. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  272. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  273. package/docs/types/types.CalendarDateObject.html +1 -1
  274. package/docs/types/types.Camelized.html +1 -1
  275. package/docs/types/types.ClockTimeObject.html +1 -1
  276. package/docs/types/types.DbConnectionType.html +1 -1
  277. package/docs/types/types.DbTypes.html +1 -1
  278. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  279. package/docs/types/types.DreamAttributes.html +1 -1
  280. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  281. package/docs/types/types.DreamClassColumn.html +1 -1
  282. package/docs/types/types.DreamColumn.html +1 -1
  283. package/docs/types/types.DreamColumnNames.html +1 -1
  284. package/docs/types/types.DreamLogLevel.html +1 -1
  285. package/docs/types/types.DreamLogger.html +2 -2
  286. package/docs/types/types.DreamModelSerializerType.html +1 -1
  287. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  288. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  289. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  290. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  291. package/docs/types/types.DreamSerializable.html +1 -1
  292. package/docs/types/types.DreamSerializableArray.html +1 -1
  293. package/docs/types/types.DreamSerializerKey.html +1 -1
  294. package/docs/types/types.DreamSerializers.html +1 -1
  295. package/docs/types/types.DreamVirtualColumns.html +1 -1
  296. package/docs/types/types.DurationUnit.html +1 -1
  297. package/docs/types/types.EncryptAlgorithm.html +1 -1
  298. package/docs/types/types.HasManyStatement.html +1 -1
  299. package/docs/types/types.HasOneStatement.html +1 -1
  300. package/docs/types/types.Hyphenized.html +1 -1
  301. package/docs/types/types.Pascalized.html +1 -1
  302. package/docs/types/types.PrimaryKeyType.html +1 -1
  303. package/docs/types/types.RoundingPrecision.html +1 -1
  304. package/docs/types/types.SerializerCasing.html +1 -1
  305. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  306. package/docs/types/types.Snakeified.html +1 -1
  307. package/docs/types/types.StrictInterface.html +1 -1
  308. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  309. package/docs/types/types.UpdateableProperties.html +1 -1
  310. package/docs/types/types.ValidationType.html +1 -1
  311. package/docs/types/types.ViewModel.html +2 -2
  312. package/docs/types/types.ViewModelClass.html +1 -1
  313. package/docs/types/types.WeekdayName.html +1 -1
  314. package/docs/types/types.WhereStatementForDream.html +1 -1
  315. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  316. package/docs/variables/index.DreamConst.html +1 -1
  317. package/docs/variables/index.ops.html +1 -1
  318. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  319. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  320. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  321. package/docs/variables/system.primaryKeyTypes.html +1 -1
  322. package/package.json +1 -1
  323. package/dist/cjs/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  324. package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  325. package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  326. package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  327. package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  328. package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  329. package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
  330. package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
  331. 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 applySortableScopeToQuery from './applySortableScopeToQuery.js';
2
- export default async function validPosition({ changingScope, dream, position, positionField, query, scope, wasNewRecord, previousPosition, txn, }) {
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 maxPosition = (await applySortableScopeToQuery(query.txn(txn), dream, scope).max(positionField)) +
5
- (increasingNumberOfItemsToSort ? 1 : 0);
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
- export default async function afterSortableDestroy({ positionField, dream, query, scope, }) {
4
- await decrementPositionForScopedRecordsGreaterThanPosition(dream[positionField], {
5
- dream,
6
- positionField,
7
- scope,
8
- query,
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`, so a
1905
- * long-running writer on the other side stalls the run indefinitely and, at
1906
- * scale, ties up connections. If that is a risk in your application, set a
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
- async lockedBatches(batchSize, perBatch) {
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: await perBatch(claimed, txn),
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);