@rvoh/dream 2.21.1 → 2.23.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 (248) hide show
  1. package/dist/cjs/src/Dream.js +71 -23
  2. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +3 -0
  3. package/dist/cjs/src/dream/Query.js +242 -12
  4. package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
  5. package/dist/cjs/src/dream/QueryDriver/Kysely.js +46 -4
  6. package/dist/cjs/src/dream/QueryDriver/Postgres.js +21 -0
  7. package/dist/cjs/src/dream/internal/buildSerializerPreloadPaths.js +93 -20
  8. package/dist/cjs/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js +18 -10
  9. package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +76 -4
  10. package/dist/cjs/src/dream/internal/destroyDream.js +6 -7
  11. package/dist/cjs/src/dream/internal/mergeSerializerAssociationEdges.js +75 -0
  12. package/dist/cjs/src/dream/internal/printSerializerHierarchyLevel.js +50 -8
  13. package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +79 -14
  14. package/dist/cjs/src/errors/InvalidBatchSize.js +19 -0
  15. package/dist/cjs/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
  16. package/dist/cjs/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
  17. package/dist/cjs/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
  18. package/dist/cjs/src/errors/serializers/NoSerializersResolvedForKey.js +30 -0
  19. package/dist/cjs/src/ops/index.js +19 -0
  20. package/dist/cjs/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +22 -0
  21. package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +13 -0
  22. package/dist/esm/src/Dream.js +71 -23
  23. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +3 -0
  24. package/dist/esm/src/dream/Query.js +242 -12
  25. package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
  26. package/dist/esm/src/dream/QueryDriver/Kysely.js +46 -4
  27. package/dist/esm/src/dream/QueryDriver/Postgres.js +21 -0
  28. package/dist/esm/src/dream/internal/buildSerializerPreloadPaths.js +93 -20
  29. package/dist/esm/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js +18 -10
  30. package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +76 -4
  31. package/dist/esm/src/dream/internal/destroyDream.js +6 -7
  32. package/dist/esm/src/dream/internal/mergeSerializerAssociationEdges.js +75 -0
  33. package/dist/esm/src/dream/internal/printSerializerHierarchyLevel.js +50 -8
  34. package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +79 -14
  35. package/dist/esm/src/errors/InvalidBatchSize.js +19 -0
  36. package/dist/esm/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
  37. package/dist/esm/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
  38. package/dist/esm/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
  39. package/dist/esm/src/errors/serializers/NoSerializersResolvedForKey.js +30 -0
  40. package/dist/esm/src/ops/index.js +19 -0
  41. package/dist/esm/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +22 -0
  42. package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +13 -0
  43. package/dist/types/src/Dream.d.ts +12 -0
  44. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -0
  45. package/dist/types/src/dream/Query.d.ts +150 -3
  46. package/dist/types/src/dream/QueryDriver/Base.d.ts +21 -0
  47. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
  48. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +20 -0
  49. package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +22 -3
  50. package/dist/types/src/dream/internal/mergeSerializerAssociationEdges.d.ts +69 -0
  51. package/dist/types/src/dream/internal/printSerializerHierarchyLevel.d.ts +23 -5
  52. package/dist/types/src/dream/internal/resolveSerializerAssociationEdges.d.ts +27 -1
  53. package/dist/types/src/errors/InvalidBatchSize.d.ts +5 -0
  54. package/dist/types/src/errors/RowLockIncompatibleWithDistinct.d.ts +3 -0
  55. package/dist/types/src/errors/RowLockIncompatibleWithJoinLoad.d.ts +3 -0
  56. package/dist/types/src/errors/ops/ArrayEqualityRequiresArrayColumn.d.ts +8 -0
  57. package/dist/types/src/errors/serializers/NoSerializersResolvedForKey.d.ts +18 -0
  58. package/dist/types/src/ops/index.d.ts +19 -0
  59. package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +16 -0
  60. package/dist/types/src/types/associations/shared.d.ts +2 -2
  61. package/dist/types/src/types/dream.d.ts +8 -0
  62. package/dist/types/src/types/types/associations/shared.ts +84 -55
  63. package/dist/types/src/types/types/dream.ts +30 -0
  64. package/docs/assets/hierarchy.js +1 -1
  65. package/docs/assets/search.js +1 -1
  66. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  67. package/docs/classes/db.KyselyQueryDriver.html +55 -41
  68. package/docs/classes/db.PostgresQueryDriver.html +55 -42
  69. package/docs/classes/db.QueryDriverBase.html +54 -40
  70. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  71. package/docs/classes/errors.ColumnOverflow.html +3 -3
  72. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  73. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  74. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  75. package/docs/classes/errors.DecryptionError.html +2 -2
  76. package/docs/classes/errors.DecryptionParseError.html +2 -2
  77. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  78. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  79. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  80. package/docs/classes/errors.InvalidClockTime.html +2 -2
  81. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  82. package/docs/classes/errors.InvalidDateTime.html +2 -2
  83. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  84. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  85. package/docs/classes/errors.NotNullViolation.html +3 -3
  86. package/docs/classes/errors.RecordNotFound.html +3 -3
  87. package/docs/classes/errors.ValidationError.html +3 -3
  88. package/docs/classes/index.CalendarDate.html +33 -33
  89. package/docs/classes/index.ClockTime.html +32 -32
  90. package/docs/classes/index.ClockTimeTz.html +35 -35
  91. package/docs/classes/index.DateTime.html +86 -86
  92. package/docs/classes/index.Decorators.html +19 -19
  93. package/docs/classes/index.Dream.html +127 -125
  94. package/docs/classes/index.DreamApp.html +10 -10
  95. package/docs/classes/index.DreamTransaction.html +2 -2
  96. package/docs/classes/index.Env.html +2 -2
  97. package/docs/classes/index.Query.html +161 -71
  98. package/docs/classes/system.CliFileWriter.html +4 -4
  99. package/docs/classes/system.DreamBin.html +2 -2
  100. package/docs/classes/system.DreamCLI.html +7 -7
  101. package/docs/classes/system.DreamImporter.html +2 -2
  102. package/docs/classes/system.DreamLogos.html +2 -2
  103. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  104. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  105. package/docs/classes/system.PathHelpers.html +3 -3
  106. package/docs/classes/utils.Encrypt.html +3 -3
  107. package/docs/classes/utils.Range.html +2 -2
  108. package/docs/functions/db.closeAllDbConnections.html +1 -1
  109. package/docs/functions/db.dreamDbConnections.html +1 -1
  110. package/docs/functions/db.untypedDb.html +1 -1
  111. package/docs/functions/db.validateColumn.html +1 -1
  112. package/docs/functions/db.validateTable.html +1 -1
  113. package/docs/functions/errors.pgErrorType.html +1 -1
  114. package/docs/functions/index.DreamSerializer.html +1 -1
  115. package/docs/functions/index.ObjectSerializer.html +1 -1
  116. package/docs/functions/index.ReplicaSafe.html +1 -1
  117. package/docs/functions/index.STI.html +1 -1
  118. package/docs/functions/index.SoftDelete.html +1 -1
  119. package/docs/functions/utils.camelize.html +1 -1
  120. package/docs/functions/utils.capitalize.html +1 -1
  121. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  122. package/docs/functions/utils.compact.html +1 -1
  123. package/docs/functions/utils.groupBy.html +1 -1
  124. package/docs/functions/utils.hyphenize.html +1 -1
  125. package/docs/functions/utils.intersection.html +1 -1
  126. package/docs/functions/utils.isEmpty.html +1 -1
  127. package/docs/functions/utils.normalizeUnicode.html +1 -1
  128. package/docs/functions/utils.pascalize.html +1 -1
  129. package/docs/functions/utils.percent.html +1 -1
  130. package/docs/functions/utils.range.html +1 -1
  131. package/docs/functions/utils.round.html +1 -1
  132. package/docs/functions/utils.sanitizeString.html +1 -1
  133. package/docs/functions/utils.snakeify.html +1 -1
  134. package/docs/functions/utils.sort.html +1 -1
  135. package/docs/functions/utils.sortBy.html +1 -1
  136. package/docs/functions/utils.sortObjectByKey.html +1 -1
  137. package/docs/functions/utils.sortObjectByValue.html +1 -1
  138. package/docs/functions/utils.uncapitalize.html +1 -1
  139. package/docs/functions/utils.uniq.html +1 -1
  140. package/docs/hierarchy.html +1 -1
  141. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  142. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  143. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  144. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  145. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  146. package/docs/interfaces/types.DecoratorContext.html +2 -2
  147. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  148. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  149. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  150. package/docs/interfaces/types.DurationObject.html +2 -2
  151. package/docs/interfaces/types.EncryptOptions.html +2 -2
  152. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  153. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  154. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  155. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  156. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  157. package/docs/types/openapi.OpenapiFormats.html +1 -1
  158. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  159. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  160. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  161. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  163. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  164. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  165. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  166. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  167. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  168. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  169. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  170. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  171. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  172. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  174. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  181. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  183. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  185. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  187. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  188. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  189. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  190. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  191. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  192. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  193. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  194. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  195. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  196. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  197. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  198. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  199. package/docs/types/types.CalendarDateObject.html +1 -1
  200. package/docs/types/types.Camelized.html +1 -1
  201. package/docs/types/types.ClockTimeObject.html +1 -1
  202. package/docs/types/types.DbConnectionType.html +1 -1
  203. package/docs/types/types.DbTypes.html +1 -1
  204. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  205. package/docs/types/types.DreamAttributes.html +1 -1
  206. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  207. package/docs/types/types.DreamClassColumn.html +1 -1
  208. package/docs/types/types.DreamColumn.html +1 -1
  209. package/docs/types/types.DreamColumnNames.html +1 -1
  210. package/docs/types/types.DreamLogLevel.html +1 -1
  211. package/docs/types/types.DreamLogger.html +2 -2
  212. package/docs/types/types.DreamModelSerializerType.html +1 -1
  213. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  214. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  215. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  216. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  217. package/docs/types/types.DreamSerializable.html +1 -1
  218. package/docs/types/types.DreamSerializableArray.html +1 -1
  219. package/docs/types/types.DreamSerializerKey.html +1 -1
  220. package/docs/types/types.DreamSerializers.html +1 -1
  221. package/docs/types/types.DreamVirtualColumns.html +1 -1
  222. package/docs/types/types.DurationUnit.html +1 -1
  223. package/docs/types/types.EncryptAlgorithm.html +1 -1
  224. package/docs/types/types.HasManyStatement.html +1 -1
  225. package/docs/types/types.HasOneStatement.html +1 -1
  226. package/docs/types/types.Hyphenized.html +1 -1
  227. package/docs/types/types.Pascalized.html +1 -1
  228. package/docs/types/types.PrimaryKeyType.html +1 -1
  229. package/docs/types/types.RoundingPrecision.html +1 -1
  230. package/docs/types/types.SerializerCasing.html +1 -1
  231. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  232. package/docs/types/types.Snakeified.html +1 -1
  233. package/docs/types/types.StrictInterface.html +1 -1
  234. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  235. package/docs/types/types.UpdateableProperties.html +1 -1
  236. package/docs/types/types.ValidationType.html +1 -1
  237. package/docs/types/types.ViewModel.html +2 -2
  238. package/docs/types/types.ViewModelClass.html +1 -1
  239. package/docs/types/types.WeekdayName.html +1 -1
  240. package/docs/types/types.WhereStatementForDream.html +1 -1
  241. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  242. package/docs/variables/index.DreamConst.html +1 -1
  243. package/docs/variables/index.ops.html +38 -23
  244. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  245. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  246. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  247. package/docs/variables/system.primaryKeyTypes.html +1 -1
  248. package/package.json +1 -1
@@ -1,19 +1,78 @@
1
- import MissingSerializersDefinition from '../../errors/serializers/MissingSerializersDefinition.js';
2
1
  import compact from '../../helpers/compact.js';
3
2
  import { inferSerializersFromDreamClassOrViewModelClass } from '../../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
3
+ /**
4
+ * Keyed on the two arguments. Weak on both, so an entry is collectable once either the Dream class
5
+ * or the serializer is. The cached arrays are handed out by reference and must be treated as
6
+ * read-only by callers.
7
+ */
8
+ let resolvedEdgesCache = new WeakMap();
9
+ /**
10
+ * @internal
11
+ *
12
+ * Drops every memoized edge set. Only needed when a class object that stays in the process has its
13
+ * `serializers` getter replaced, which the memo cannot detect — specs that install or remove one
14
+ * around an example. Replacing the model classes themselves needs no call, since the memo keys on
15
+ * the class and serializer objects and entries resolved against the previous ones become
16
+ * unreachable.
17
+ */
18
+ export function clearResolvedSerializerAssociationEdgesCache() {
19
+ resolvedEdgesCache = new WeakMap();
20
+ }
21
+ /**
22
+ * @internal
23
+ *
24
+ * Resolves the rendersOne/rendersMany/delegatedAttribute attributes of `serializer` to the
25
+ * associations of `dreamClass` they preload, along with the classes and serializers each one
26
+ * targets.
27
+ *
28
+ * Memoized on its two arguments, because every call rebuilds a throwaway serializer builder and
29
+ * re-infers the serializers of every association target, and the traversal in
30
+ * buildSerializerPreloadPaths revisits the same (class, serializer) pair many times.
31
+ *
32
+ * The two arguments are not the function's only inputs, though: resolving a target's serializers
33
+ * also reads that target's `serializers` getter, the STI children registered on it, and the
34
+ * DreamApp serializer registry. Loading a fresh set of classes replaces the keys along with that
35
+ * state, so the memo follows it. Mutating a `serializers` getter in place does not, and needs
36
+ * `clearResolvedSerializerAssociationEdgesCache` — see there.
37
+ */
4
38
  export default function resolveSerializerAssociationEdges(dreamClass, serializer) {
39
+ let cachedForDreamClass = resolvedEdgesCache.get(dreamClass);
40
+ if (cachedForDreamClass === undefined) {
41
+ cachedForDreamClass = new WeakMap();
42
+ resolvedEdgesCache.set(dreamClass, cachedForDreamClass);
43
+ }
44
+ const cached = cachedForDreamClass.get(serializer);
45
+ if (cached !== undefined)
46
+ return cached;
47
+ const edges = buildSerializerAssociationEdges(dreamClass, serializer);
48
+ cachedForDreamClass.set(serializer, edges);
49
+ return edges;
50
+ }
51
+ function buildSerializerAssociationEdges(dreamClass, serializer) {
5
52
  const serializerBuilder = serializer(undefined, undefined);
6
53
  const serializerAssociations = serializerBuilder['attributes'].filter(attribute => ['rendersOne', 'rendersMany', 'delegatedAttribute'].includes(attribute.type));
7
54
  return compact(serializerAssociations.map(serializerAssociation => {
8
55
  const serializerAssociationName = serializerAssociation.targetName ??
9
56
  serializerAssociation.name;
57
+ // `dreamClass` is the class the walk is rooted at, which for an STI base is the base itself
58
+ // even while `serializer` is one child's serializer. That pairing is safe because an STI child
59
+ // cannot declare associations of its own — the `STI` class decorator throws
60
+ // StiChildCannotDefineNewAssociations — so every association a child's serializer can render
61
+ // is declared on the base and resolves here.
62
+ //
63
+ // The one hole: that guard reads decorator metadata and
64
+ // `associationDeclarationNamesFromMetadata` returns [] when the metadata is absent, which is
65
+ // also what happens when the `STI` decorator runs without a `context`. Under a TS config that
66
+ // does not emit decorator metadata the guard silently passes, a child-declared association
67
+ // survives, and it is dropped *here*, silently, by the return below — the symptom is then a
68
+ // missing preload and a NonLoadedAssociation at render, not a decorator error naming the real
69
+ // problem.
10
70
  const association = dreamClass['getAssociationMetadata'](serializerAssociationName);
11
71
  if (!association)
12
72
  return null;
13
73
  if (serializerAssociation.type === 'delegatedAttribute') {
14
74
  return {
15
75
  associationAs: association.as,
16
- sourceDreamClass: dreamClass,
17
76
  type: serializerAssociation.type,
18
77
  serializerAssociationName,
19
78
  targets: [],
@@ -26,17 +85,24 @@ export default function resolveSerializerAssociationEdges(dreamClass, serializer
26
85
  ? maybeAssociatedClasses
27
86
  : [maybeAssociatedClasses];
28
87
  const targets = associatedClasses.flatMap(associatedClass => {
29
- let serializers = [];
30
- try {
31
- serializers = serializerAssociation.options.serializer
32
- ? compact([serializerAssociation.options.serializer])
33
- : compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey));
34
- }
35
- catch (error) {
36
- if (!(error instanceof MissingSerializersDefinition))
37
- throw error;
38
- serializers = [];
39
- }
88
+ // Deliberately unguarded: every serializer that cannot be resolved while building preload
89
+ // paths throws, and there is no tolerance here by design.
90
+ //
91
+ // Why: an unresolvable serializer is a configuration error, and the first query that walks
92
+ // the serializer graph is where it should be found — no rows, no rendering required.
93
+ // Swallowing it means the only way to discover a broken `serializerKey` (or a missing
94
+ // `serializers` getter) is a spec that creates a row of the affected class *and* renders it.
95
+ //
96
+ // Why the obvious tolerance is also wrong, not merely undesirable:
97
+ // `inferSerializersFromDreamClassOrViewModelClass` below expands STI and throws on the
98
+ // *first* child that fails, so catching here would drop the serializers of *every* child of
99
+ // this target. An edge with no targets is treated as terminal by buildSerializerPreloadPaths,
100
+ // so the association is preloaded but never descended into, and a row of a *well-formed*
101
+ // sibling fails at render with NonLoadedAssociation — an error naming a class that has
102
+ // nothing wrong with it.
103
+ const serializers = serializerAssociation.options.serializer
104
+ ? compact([serializerAssociation.options.serializer])
105
+ : compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey));
40
106
  return serializers.map(associatedSerializer => ({
41
107
  dreamClass: associatedClass,
42
108
  serializer: associatedSerializer,
@@ -44,7 +110,6 @@ export default function resolveSerializerAssociationEdges(dreamClass, serializer
44
110
  });
45
111
  return {
46
112
  associationAs: association.as,
47
- sourceDreamClass: dreamClass,
48
113
  type: serializerAssociation.type,
49
114
  serializerAssociationName,
50
115
  targets,
@@ -0,0 +1,19 @@
1
+ export default class InvalidBatchSize extends Error {
2
+ batchSize;
3
+ constructor(batchSize) {
4
+ super();
5
+ this.batchSize = batchSize;
6
+ }
7
+ get message() {
8
+ return `
9
+ Invalid batchSize provided.
10
+
11
+ batchSize given:
12
+ ${String(this.batchSize)}
13
+
14
+ batchSize must be a positive integer. A batchSize of 0 is especially dangerous,
15
+ since a limit of 0 is treated as "no limit", which would collapse a batched
16
+ operation into a single unbounded pass over the entire matched set.
17
+ `;
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ export default class RowLockIncompatibleWithDistinct extends Error {
2
+ get message() {
3
+ // `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database
4
+ // ("FOR UPDATE is not allowed with DISTINCT clause"), so fail here, with a
5
+ // message that names the two incompatible parts, rather than surfacing a
6
+ // raw driver error from inside a per-batch transaction.
7
+ return `
8
+ An exclusive row lock was requested on a query that also calls \`distinct\`.
9
+
10
+ A locked read cannot be combined with DISTINCT. Drop the \`distinct\` call, or
11
+ drop the lock.
12
+ `;
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ export default class RowLockIncompatibleWithJoinLoad extends Error {
2
+ get message() {
3
+ // A join load builds its own select statement (one row per joined
4
+ // association row, later collapsed in memory), which cannot carry the
5
+ // Query's row lock. Rather than silently performing an unlocked read and
6
+ // dropping the compare-and-set guarantee the caller asked for, refuse.
7
+ return `
8
+ An exclusive row lock was requested on a query that also uses leftJoinPreload.
9
+
10
+ A join-loaded query cannot be locked. Use \`preload\` instead of
11
+ \`leftJoinPreload\`, or drop the lock.
12
+ `;
13
+ }
14
+ }
@@ -0,0 +1,23 @@
1
+ export default class ArrayEqualityRequiresArrayColumn extends Error {
2
+ dreamClass;
3
+ column;
4
+ operator;
5
+ constructor(dreamClass, column, operator) {
6
+ super();
7
+ this.dreamClass = dreamClass;
8
+ this.column = column;
9
+ this.operator = operator;
10
+ }
11
+ get message() {
12
+ return `
13
+ Attempting to compare an array for equality against a column that is not an
14
+ array in the database:
15
+
16
+ ${this.dreamClass.sanitizedName}#${this.column} (operator: ${this.operator})
17
+
18
+ Whole-array equality (\`=\`, \`!=\`, \`<>\`) is only meaningful for array columns.
19
+ If you meant "is this column one of these values", use \`ops.in([...])\`
20
+ (or a bare array) instead.
21
+ `;
22
+ }
23
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Raised when resolving a serializer key against a class yields an empty set of serializers.
3
+ *
4
+ * This is an internal-invariant failure, not a misconfiguration: an unresolvable serializer key
5
+ * raises `MissingSerializersDefinitionForKey` (and a class with no `serializers` getter at all
6
+ * raises `MissingSerializersDefinition`) while it is being resolved, and STI expansion always
7
+ * yields at least one class, so there is no supported application shape that reaches this. It is
8
+ * typed rather than a bare `Error` so that it can be asserted on, and so that the two callers that
9
+ * guard the invariant raise the same thing.
10
+ */
11
+ export default class NoSerializersResolvedForKey extends Error {
12
+ classDef;
13
+ serializerKey;
14
+ constructor(classDef, serializerKey) {
15
+ super();
16
+ this.classDef = classDef;
17
+ this.serializerKey = serializerKey;
18
+ }
19
+ get message() {
20
+ const className = this.classDef === null || this.classDef === undefined
21
+ ? String(this.classDef)
22
+ : (this.classDef.globalName ?? this.classDef.name);
23
+ return `
24
+ No serializers resolved for \`${className}\` under serializer key \`${this.serializerKey}\`
25
+
26
+ This should be impossible — a serializer key that cannot be resolved throws while it is being
27
+ resolved, before reaching here — and indicates a bug in Dream rather than a problem with your
28
+ application. Please report it.`;
29
+ }
30
+ }
@@ -117,11 +117,27 @@ const ops = {
117
117
  /**
118
118
  * Creates an `OpsStatement` using the `=` operator for strict equality.
119
119
  *
120
+ * **Array columns:** passing an array means literal array equality — the
121
+ * whole array is bound as a single parameter, so the column must match
122
+ * element-for-element, in order. This is the opt-in counterpart to a bare
123
+ * array in a where clause (`Post.where({ tags: ['a', 'b'] })`), which means
124
+ * `IN`, and to `ops.any`, which tests for containment of a single element.
125
+ * Element types are constrained by the column, so an enum array column
126
+ * rejects a string that is not a member of its enum.
127
+ *
128
+ * Note that PostgreSQL defines no equality operator for `json` (as opposed
129
+ * to `jsonb`), so `ops.equal` against a `json[]` column raises at the
130
+ * database.
131
+ *
120
132
  * @param equal - The value the column must equal.
121
133
  * @returns An `OpsStatement` using the `=` operator.
122
134
  *
123
135
  * @example
124
136
  * User.where({ role: ops.equal('admin') })
137
+ *
138
+ * @example
139
+ * // matches only rows whose tags column is exactly ['a', 'b'], in that order
140
+ * Post.where({ tags: ops.equal(['a', 'b']) })
125
141
  */
126
142
  equal: (equal) => new OpsStatement('=', equal),
127
143
  /**
@@ -260,6 +276,9 @@ const ops = {
260
276
  /**
261
277
  * Creates an `OpsStatement` using the `!=` operator for inequality.
262
278
  *
279
+ * **Array columns:** as with `ops.equal`, passing an array compares against
280
+ * the whole array rather than against its elements.
281
+ *
263
282
  * @param equal - The value the column must not equal.
264
283
  * @returns An `OpsStatement` using the `!=` operator.
265
284
  *
@@ -3,6 +3,7 @@ import MissingSerializersDefinition from '../../errors/serializers/MissingSerial
3
3
  import MissingSerializersDefinitionForKey from '../../errors/serializers/MissingSerializersDefinitionForKey.js';
4
4
  import NoGlobalSerializerForSpecifiedKey from '../../errors/serializers/NoGlobalSerializerForSpecifiedKey.js';
5
5
  import NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey from '../../errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js';
6
+ import NoSerializersResolvedForKey from '../../errors/serializers/NoSerializersResolvedForKey.js';
6
7
  import compact from '../../helpers/compact.js';
7
8
  import expandStiClasses from '../../helpers/sti/expandStiClasses.js';
8
9
  import uniq from '../../helpers/uniq.js';
@@ -35,3 +36,24 @@ export function inferSerializersFromDreamClassOrViewModelClass(classDef, seriali
35
36
  const serializers = classes.map(classDef => inferSerializerFromDreamOrViewModel(classDef.prototype, serializerKey));
36
37
  return uniq(compact(serializers));
37
38
  }
39
+ /**
40
+ * As `inferSerializersFromDreamClassOrViewModelClass`, but guarantees a non-empty result.
41
+ *
42
+ * The guard is unreachable today, and deliberately not phrased as "no serializer found for key": a
43
+ * serializer key that a class does not register throws `MissingSerializersDefinitionForKey` (and a
44
+ * class with no `serializers` getter at all throws `MissingSerializersDefinition`) from inside the
45
+ * inference above, and `expandStiClasses` always yields at least one class. It exists only so that
46
+ * a future change to any of those cannot turn an unresolvable serializer key into a silently empty
47
+ * serializer set — which, for the callers below, means a silently empty preload set and a
48
+ * `NonLoadedAssociation` at render rather than an error naming the real problem.
49
+ *
50
+ * Used by everything that walks a serializer graph rooted on a class rather than an instance —
51
+ * preload-path building and serialization display — so that they raise one typed error between
52
+ * them.
53
+ */
54
+ export function inferSerializersFromDreamClassOrViewModelClassOrFail(classDef, serializerKey) {
55
+ const serializers = inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey);
56
+ if (serializers.length === 0)
57
+ throw new NoSerializersResolvedForKey(classDef, serializerKey);
58
+ return serializers;
59
+ }
@@ -15,5 +15,18 @@ export function serializerForAssociatedClass(object, associationName, options) {
15
15
  const associatedClass = association.modelCB();
16
16
  if (Array.isArray(associatedClass))
17
17
  throw new Error('rendersOne flatten is incompatible with a polymorphic belongs-to association');
18
+ // Taking the first is only a narrowing when `associatedClass` is an STI base, where
19
+ // inferSerializersFromDreamClassOrViewModelClass returns one serializer per child sorted by
20
+ // sanitizedName. It is correct here because there is no associated row: this function is reached
21
+ // only from SerializerRenderer's `flatten: true` branch for a *null* associated object, so no
22
+ // child of the base is the right one — the row that would have decided is absent. (rendersMany
23
+ // never reaches here and does not accept `flatten`.)
24
+ //
25
+ // It is observable, though, so it is not "nothing happens": the picked serializer is invoked and
26
+ // its output spread into the parent's payload, so the parent JSON carries the
27
+ // alphabetically-first child's flattened keys as nulls, and omits keys only a later-sorting child
28
+ // would flatten. Unioning every child's serializer here — which is what buildSerializerPreloadPaths
29
+ // does for preload paths — is meaningless: exactly one set of keys can be flattened into the
30
+ // payload.
18
31
  return inferSerializersFromDreamClassOrViewModelClass(associatedClass, options.serializerKey)[0] ?? null;
19
32
  }
@@ -429,6 +429,15 @@ export default class Dream {
429
429
  private static setGlobalName;
430
430
  private static serializationMap;
431
431
  private static displaySerialization;
432
+ /**
433
+ * @internal
434
+ *
435
+ * Maps the association tree reachable from `serializers` — every serializer that can render this
436
+ * class at this point in the walk, which is more than one when this class is an STI base. The
437
+ * serializers are merged into one edge set (see mergeSerializerAssociationEdges) rather than
438
+ * walked one at a time, so an association several of them render is visited once and the nested
439
+ * trees of a shared target merge instead of clobbering each other.
440
+ */
432
441
  private static recursiveSerializationMap;
433
442
  /**
434
443
  * Returns the column names for the given model
@@ -1011,6 +1020,9 @@ export default class Dream {
1011
1020
  * // User{email: 'goodbye@world'}
1012
1021
  * ```
1013
1022
  *
1023
+ * Records are always visited in ascending primary key order; any `order`
1024
+ * applied to the query is discarded. See {@link Query.findEach} for why.
1025
+ *
1014
1026
  * @param cb - The callback to call for each found record
1015
1027
  * @param opts - Optional parameters for batch processing
1016
1028
  * @param opts.batchSize - the batch size you wish to collect records in. If not provided, it will default to 1000
@@ -384,6 +384,9 @@ export default class DreamClassTransactionBuilder<DreamClass extends typeof Drea
384
384
  * // User{email: 'goodbye@world'}
385
385
  * ```
386
386
  *
387
+ * Records are always visited in ascending primary key order; any `order`
388
+ * applied to the query is discarded. See {@link Query.findEach} for why.
389
+ *
387
390
  * @param cb - The callback to call for each found record
388
391
  * @param options.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
389
392
  * @returns void
@@ -7,6 +7,16 @@ import { CursorPaginatedDreamQueryOptions, CursorPaginatedDreamQueryResult, Defa
7
7
  import { JoinedAssociation, JoinedAssociationsTypeFromAssociations, QueryTypeOptions, VariadicJoinsArgs, VariadicLeftJoinLoadArgs, VariadicLoadArgs } from '../types/variadic.js';
8
8
  import DreamTransaction from './DreamTransaction.js';
9
9
  import QueryDriverBase from './QueryDriver/Base.js';
10
+ /**
11
+ * @internal
12
+ *
13
+ * Drops every memoized preload path. Only needed when a class object that stays in the process has
14
+ * its `serializers` getter replaced, which the cache cannot detect — specs that install or remove
15
+ * one around an example. Replacing the model classes themselves needs no call, since the cache
16
+ * keys on the class object. See `clearResolvedSerializerAssociationEdgesCache`, which has the same
17
+ * contract and is cleared alongside it.
18
+ */
19
+ export declare function clearSerializerPreloadPathsCache(): void;
10
20
  export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Readonly<QueryTypeOptions> = DefaultQueryTypeOptions<DreamInstance>> {
11
21
  /**
12
22
  * @internal
@@ -16,6 +26,7 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
16
26
  */
17
27
  static readonly BATCH_SIZES: {
18
28
  FIND_EACH: number;
29
+ LOCKED_DESTROY: number;
19
30
  PLUCK_EACH: number;
20
31
  PLUCK_EACH_THROUGH: number;
21
32
  };
@@ -282,13 +293,30 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
282
293
  * records matching the Query have been fetched.
283
294
  *
284
295
  * ```ts
285
- * await User.order('id').findEach(user => {
296
+ * await User.where({ deletedAt: null }).findEach(user => {
286
297
  * DreamApp.log(user)
287
298
  * })
288
299
  * // User{id: 1}
289
300
  * // User{id: 2}
290
301
  * ```
291
302
  *
303
+ * IMPORTANT: `findEach` always iterates in ascending primary key order.
304
+ * Any `order` applied to the Query is discarded; passing one has no effect
305
+ * on the order records are visited in.
306
+ *
307
+ * This is not a limitation that can be lifted. `findEach` guarantees that
308
+ * every matching record is visited exactly once, and it delivers that by
309
+ * paginating on a keyset: each batch selects the next `batchSize` records
310
+ * with a primary key greater than the last record of the previous batch.
311
+ * That guarantee only holds over a stable, unique key. Ordering by a
312
+ * mutable, non-unique column would mean a record whose sort value changed
313
+ * partway through the iteration could be skipped entirely or visited twice.
314
+ * ({@link Query.cursorPaginate} can honor an arbitrary order precisely
315
+ * because a paginated UI tolerates that anomaly; a full traversal cannot.)
316
+ *
317
+ * If you need records processed in a particular order, load them with a
318
+ * regular ordered query instead of `findEach`.
319
+ *
292
320
  * @param cb - The callback to call for each found record
293
321
  * @param __namedParameters - Options for batch processing
294
322
  * @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
@@ -1209,17 +1237,131 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1209
1237
  * // 12
1210
1238
  * ```
1211
1239
  *
1240
+ * ## Guarded (compare-and-set) destroy
1241
+ *
1242
+ * By default, `destroy` reads the matching records and then destroys each of
1243
+ * them, so a concurrent writer can change a record in between — the record is
1244
+ * destroyed even though it no longer matches the Query that selected it.
1245
+ * Passing `lock: true` closes that window, making `destroy` a compare-and-set
1246
+ * the way `update(..., { skipHooks: true })` already is:
1247
+ *
1248
+ * ```ts
1249
+ * const destroyed = await Order.where({ status: 'pending' }).destroy({ lock: true })
1250
+ * // 0 — someone else moved every one of them out of 'pending' first
1251
+ * ```
1252
+ *
1253
+ * With `lock: true`, records are processed in batches, and each batch runs in
1254
+ * its own transaction which first re-selects that batch's records with an
1255
+ * exclusive row lock and then destroys them before committing. Because the
1256
+ * database re-checks the Query's conditions after acquiring each row lock,
1257
+ * a record a concurrent transaction has already moved out of the Query drops
1258
+ * out of the locked read and is never destroyed. The returned count is the
1259
+ * number of records actually claimed, so a caller can detect that it lost a
1260
+ * race by comparing the count against what it expected.
1261
+ *
1262
+ * Things to know before relying on it:
1263
+ *
1264
+ * - **The guarantee is per batch, not set-wide.** A run spanning more than one
1265
+ * batch can win a race in batch 1 and lose one in batch 2; nothing holds the
1266
+ * whole matched set still for the duration of the run. Sharing one
1267
+ * transaction across every batch — by threading a transaction onto the Query
1268
+ * itself, `Order.query().txn(txn).where({ ... }).destroy({ lock: true })` —
1269
+ * buys two things: the destroys become all-or-nothing (a rollback undoes the
1270
+ * whole run), and the row locks taken by every batch are held until you
1271
+ * commit. It does **not** extend the compare-and-set to the part of the set
1272
+ * the run has not reached yet: under READ COMMITTED each statement takes a
1273
+ * fresh snapshot, so a record moved out of the Query between batch 1 and
1274
+ * batch 2 still drops out of batch 2's locked read. A genuinely set-wide
1275
+ * guarantee needs REPEATABLE READ / SERIALIZABLE, or a single locking
1276
+ * statement over the whole set.
1277
+ * - **Only a transaction carried by the Query is shared.** Merely wrapping the
1278
+ * call in `Model.transaction(...)` does not thread that transaction onto the
1279
+ * Query — `Dream.transaction` establishes no ambient transaction, it only
1280
+ * hands a `txn` to your callback — so each batch would open its own
1281
+ * transaction on a different pooled connection. Worse, if the surrounding
1282
+ * transaction has already written to any of the matched rows, each batch's
1283
+ * locked read blocks on locks the surrounding transaction holds while that
1284
+ * transaction awaits the destroy: a cycle across two connections, which
1285
+ * Postgres's deadlock detector cannot see, so it hangs rather than aborting.
1286
+ * Always pass the transaction with `.txn(txn)`.
1287
+ * - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result
1288
+ * behavior described above is READ COMMITTED (Postgres's default). Under
1289
+ * REPEATABLE READ or SERIALIZABLE, the same race raises a serialization
1290
+ * failure instead of silently returning a lower count. Both are safe; only
1291
+ * one of them is quiet.
1292
+ * - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the
1293
+ * default is deliberately small. Every row in a batch stays locked for the
1294
+ * whole batch, including the time spent running each record's hooks and
1295
+ * `dependent: 'destroy'` cascade, so an oversized batch blocks unrelated
1296
+ * writers touching those rows and surfaces as timeouts somewhere else in the
1297
+ * application, which is very hard to trace back to this destroy. Too small a
1298
+ * batch, by contrast, only slows down this call, which you can see and fix.
1299
+ * Lower it further for models with deep `dependent: 'destroy'` cascades or
1300
+ * expensive destroy hooks.
1301
+ * - **A batch waits, without bound, for whoever holds the row.** The locked
1302
+ * read is a plain `FOR UPDATE`: if another transaction already holds a lock
1303
+ * on one of the batch's rows, this call blocks until that transaction ends,
1304
+ * holding its own transaction and its pooled connection open the whole
1305
+ * time. Dream sets no `lock_timeout` or `statement_timeout`, so a
1306
+ * long-running writer on the other side stalls the run indefinitely and, at
1307
+ * scale, ties up connections. If that is a risk in your application, set a
1308
+ * `lock_timeout` on the connection so a stuck batch fails instead of
1309
+ * hanging.
1310
+ * - **If you are destroying a large set and do not need compare-and-set, do
1311
+ * not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
1312
+ * {@link Query.delete | delete} (a single statement, no hooks or cascade)
1313
+ * are the right tools for bulk removal.
1314
+ *
1212
1315
  * @param __namedParameters - Options for destroying the instance
1213
1316
  * @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
1214
1317
  * @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
1318
+ * @param __namedParameters.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. Defaults to false
1319
+ * @param __namedParameters.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise
1215
1320
  * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1216
1321
  * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1217
1322
  * @returns The number of records that were removed
1323
+ * @throws InvalidBatchSize if `batchSize` is not a positive integer
1218
1324
  */
1219
- destroy({ skipHooks, cascade, }?: {
1325
+ destroy({ skipHooks, cascade, lock, batchSize, }?: {
1220
1326
  skipHooks?: boolean | undefined;
1221
1327
  cascade?: boolean | undefined;
1328
+ lock?: boolean | undefined;
1329
+ batchSize?: number | undefined;
1222
1330
  }): Promise<number>;
1331
+ /**
1332
+ * @internal
1333
+ *
1334
+ * The compare-and-set implementation behind `destroy({ lock: true })`.
1335
+ *
1336
+ * Each batch is handled in two steps inside one transaction:
1337
+ *
1338
+ * 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
1339
+ * primary key order, which establishes the batch's window (its keyset
1340
+ * cursor) and tells us how many records were *attempted*. Only the keys are
1341
+ * read: hydrating and preloading these records would duplicate the work
1342
+ * step 2 does over the same rows; then
1343
+ * 2. a locked re-read restricted to exactly those primary keys, which
1344
+ * re-applies every condition on the Query while acquiring an exclusive row
1345
+ * lock on each row it returns. Records a concurrent transaction has moved
1346
+ * out of the Query since step 1 do not come back, and are the records this
1347
+ * run lost the race for.
1348
+ *
1349
+ * The count comes from step 2's result set, which is why it reports records
1350
+ * actually claimed rather than records attempted. (One thing it does not
1351
+ * account for, because it never has: a `beforeDestroy` hook that sets
1352
+ * `_preventDeletion` leaves the record in place but still counts.)
1353
+ *
1354
+ * The two-step shape is what keeps iteration correct. If the locked read both
1355
+ * claimed rows and advanced the cursor, a batch whose records all lost the
1356
+ * race would come back empty and end the run early, silently skipping every
1357
+ * record after it. Advancing the cursor from step 1 instead means losing a
1358
+ * race skips only the records that were lost.
1359
+ *
1360
+ * When the Query already carries a transaction, that transaction is used for
1361
+ * every batch rather than opening one per batch, so the caller's transaction
1362
+ * boundary wins and the locks are held until the caller commits.
1363
+ */
1364
+ private lockedDestroy;
1223
1365
  /**
1224
1366
  * Destroys all records matching the Query,
1225
1367
  * ignoring the SoftDelete decorator and
@@ -1241,13 +1383,18 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1241
1383
  * @param __namedParameters - Options for destroying the instance
1242
1384
  * @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
1243
1385
  * @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
1386
+ * @param __namedParameters.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. See {@link Query.destroy} for the full semantics. Defaults to false
1387
+ * @param __namedParameters.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise
1244
1388
  * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1245
1389
  * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1246
1390
  * @returns The number of records that were removed
1391
+ * @throws InvalidBatchSize if `batchSize` is not a positive integer
1247
1392
  */
1248
- reallyDestroy({ skipHooks, cascade, }?: {
1393
+ reallyDestroy({ skipHooks, cascade, lock, batchSize, }?: {
1249
1394
  skipHooks?: boolean;
1250
1395
  cascade?: boolean;
1396
+ lock?: boolean;
1397
+ batchSize?: number;
1251
1398
  }): Promise<number>;
1252
1399
  /**
1253
1400
  * Undestroys a SoftDelete model, unsetting
@@ -190,7 +190,28 @@ export default class QueryDriverBase<DreamInstance extends Dream> {
190
190
  */
191
191
  takeAll(options?: {
192
192
  columns?: DreamColumnNames<DreamInstance>[];
193
+ lock?: boolean;
193
194
  }): Promise<DreamInstance[]>;
195
+ /**
196
+ * @internal
197
+ *
198
+ * Adapter seam for exclusive row locking. Given the select statement about to
199
+ * be executed, return a statement that additionally acquires an exclusive
200
+ * lock on each row it returns, held until the enclosing transaction commits
201
+ * or rolls back (Postgres: `SELECT ... FOR UPDATE OF <table>`).
202
+ *
203
+ * This is only ever reached when a caller opts into a locked read (today,
204
+ * `Query#destroy({ lock: true })`), and only from inside a transaction. The
205
+ * base implementation throws so that an adapter which has not implemented row
206
+ * locking fails loud rather than silently performing an unlocked read and
207
+ * quietly dropping the compare-and-set guarantee the caller asked for.
208
+ *
209
+ * @param kyselyQuery - the select statement to lock
210
+ * @param tableAlias - the alias of the Query's base table; only rows of that
211
+ * table are locked, so a joined query does not lock unrelated rows and does
212
+ * not fail on the nullable side of an outer join
213
+ */
214
+ applyRowLock(kyselyQuery: SelectQueryBuilder<any, any, any>, tableAlias: string): SelectQueryBuilder<any, any, any>;
194
215
  /**
195
216
  * Retrieves the max value of the specified column
196
217
  * for this Query
@@ -142,6 +142,7 @@ export default class KyselyQueryDriver<DreamInstance extends Dream> extends Quer
142
142
  */
143
143
  takeAll(options?: {
144
144
  columns?: DreamColumnNames<DreamInstance>[];
145
+ lock?: boolean;
145
146
  }): Promise<DreamInstance[]>;
146
147
  /**
147
148
  * Retrieves the max value of the specified column
@@ -1,3 +1,4 @@
1
+ import { type SelectQueryBuilder } from 'kysely';
1
2
  import Dream from '../../Dream.js';
2
3
  import { SchemaBuilderAssociationData, SchemaBuilderColumnData } from '../../helpers/cli/ASTBuilder.js';
3
4
  import KyselyQueryDriver from './Kysely.js';
@@ -34,6 +35,25 @@ export default class PostgresQueryDriver<DreamInstance extends Dream> extends Ky
34
35
  [key: string]: SchemaBuilderColumnData;
35
36
  }>;
36
37
  static duplicateDatabase(connectionName: string): Promise<void>;
38
+ /**
39
+ * @internal
40
+ *
41
+ * Postgres implementation of the row-locking seam: appends
42
+ * `FOR UPDATE OF "<tableAlias>"` to the select, so every row the statement
43
+ * returns is exclusively locked until the enclosing transaction ends.
44
+ *
45
+ * Scoping the lock to the Query's own base table (rather than a bare
46
+ * `FOR UPDATE`) means a query that joins other tables neither locks rows the
47
+ * caller never asked about nor raises Postgres's "FOR UPDATE cannot be
48
+ * applied to the nullable side of an outer join".
49
+ *
50
+ * This is a waiting lock: neither `NOWAIT` nor `SKIP LOCKED` is applied, so a
51
+ * statement that collides with another transaction's lock blocks until that
52
+ * transaction ends. Callers that cannot afford an unbounded wait should set a
53
+ * `lock_timeout` on the connection; see the guarded-destroy notes on
54
+ * `Query#destroy`.
55
+ */
56
+ applyRowLock(kyselyQuery: SelectQueryBuilder<any, any, any>, tableAlias: string): SelectQueryBuilder<any, any, any>;
37
57
  static supportsParallelTestDatabases: boolean;
38
58
  /**
39
59
  * @internal