@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,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);
@@ -193,6 +193,69 @@ export default class QueryDriverBase {
193
193
  connectionName) {
194
194
  throw new Error('override openTestDatabaseLockSession in child class');
195
195
  }
196
+ /**
197
+ * @internal
198
+ *
199
+ * Whether this driver implements the transaction-scoped advisory lock seam
200
+ * ({@link acquireAdvisoryTransactionLocks}). Adapters that can hold a
201
+ * key-addressed lock for the life of a transaction override this to `true`.
202
+ * It defaults to `false` so that a driver without the primitive fails loud
203
+ * for features that depend on it (today, the `Sortable` decorator) rather
204
+ * than silently racing on position values.
205
+ */
206
+ static supportsAdvisoryTransactionLocks = false;
207
+ /**
208
+ * @internal
209
+ *
210
+ * Acquire exclusive, transaction-scoped locks on arbitrary keys, waiting
211
+ * until any other transaction holding one of them finishes. The locks are
212
+ * released when the enclosing transaction commits or rolls back — never by an
213
+ * explicit unlock — so callers only ever have to acquire (Postgres:
214
+ * `pg_advisory_xact_lock(bigint)`).
215
+ *
216
+ * Unlike {@link applyRowLock}, this does not depend on rows existing: it is
217
+ * the only way to serialize writers that are about to insert into the same
218
+ * logical group. The `Sortable` decorator uses it to make each sort scope's
219
+ * position computation exclusive, keyed on the physical database, table,
220
+ * position field and scope values.
221
+ *
222
+ * The seam takes the whole key set rather than one key at a time so that a
223
+ * driver able to express it can take them in a single round trip — the
224
+ * batched, row-locking query APIs can need one key per candidate record per
225
+ * sortable field. **Keys must be acquired in the order given**: callers sort
226
+ * them, and that shared order is what keeps two operations over the same
227
+ * scopes from deadlocking. A driver that has no bulk form implements this by
228
+ * looping over the keys in order.
229
+ *
230
+ * **The wait must be bounded.** A caller that finds a key held waits for the
231
+ * holder's transaction, and an implementation that waits forever turns an
232
+ * ordinary mistake — a sortable operation issued without the enclosing
233
+ * transaction, so that it waits on a key that transaction itself holds — into
234
+ * a permanently wedged connection that no deadlock detector can see, because
235
+ * only one side is waiting. So an implementation applies the
236
+ * `sortableScopeLockTimeout` DreamApp option (milliseconds) to the
237
+ * acquisition and raises `SortableScopeLockWaitTimedOut` when it elapses. A
238
+ * configured `0` means no bound, and is applied just as literally: the
239
+ * acquisition waits without bound even where the connection carries a
240
+ * wait bound of its own. The bound belongs to
241
+ * the acquisition alone: whatever lock-wait behavior the connection was
242
+ * configured with has to be in force again by the time this returns, so that
243
+ * the row locks {@link applyRowLock} takes are unaffected.
244
+ *
245
+ * Only ever called when {@link supportsAdvisoryTransactionLocks} is `true`;
246
+ * the base default throws.
247
+ *
248
+ * @param txn - the transaction the locks are scoped to
249
+ * @param keys - the 64-bit signed lock keys, in acquisition order
250
+ */
251
+ // eslint-disable-next-line @typescript-eslint/require-await
252
+ static async acquireAdvisoryTransactionLocks(
253
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
254
+ txn,
255
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
256
+ keys) {
257
+ throw new Error('override acquireAdvisoryTransactionLocks in child class');
258
+ }
196
259
  // eslint-disable-next-line @typescript-eslint/require-await
197
260
  static async getColumnData(
198
261
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -507,9 +570,19 @@ export default class QueryDriverBase {
507
570
  * @param dream - the dream instance you wish to destroy
508
571
  * @param txn - a transaction to encapsulate, consistently provided by underlying dream mechanisms
509
572
  * @param reallyDestroy - whether or not to reallyDestroy. If false, soft delete will be attempted when relevant
573
+ * @returns the number of rows this call removed (or soft deleted). Zero means
574
+ * the row was already gone, or that a `preventDeletion` veto stopped the
575
+ * write — callers distinguish the two by the veto flag, and use the count to
576
+ * decide whether there is anything for the after-destroy hooks to act on.
510
577
  */
511
- // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
512
- static async destroyDream(dream, txn, reallyDestroy) {
578
+ // eslint-disable-next-line @typescript-eslint/require-await
579
+ static async destroyDream(
580
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
581
+ dream,
582
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
583
+ txn,
584
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
585
+ reallyDestroy) {
513
586
  throw new Error('implement destroyDream in child class');
514
587
  }
515
588
  static get syncDialect() {