@rvoh/dream 2.24.1 → 2.26.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 (244) hide show
  1. package/dist/cjs/src/Dream.js +10 -1
  2. package/dist/cjs/src/cli/index.js +31 -5
  3. package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +127 -6
  4. package/dist/cjs/src/dream/Query.js +247 -67
  5. package/dist/cjs/src/dream/internal/printSerializerHierarchyLevel.js +4 -3
  6. package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +14 -1
  7. package/dist/cjs/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
  8. package/dist/cjs/src/errors/DropEnumValueRetypeFailed.js +208 -0
  9. package/dist/cjs/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
  10. package/dist/cjs/src/errors/serializers/MissingSerializersDefinition.js +16 -2
  11. package/dist/cjs/src/errors/serializers/MissingSerializersDefinitionForKey.js +5 -2
  12. package/dist/cjs/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.js +5 -3
  13. package/dist/cjs/src/errors/serializers/NoSerializersResolvedForKey.js +5 -2
  14. package/dist/cjs/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js +5 -3
  15. package/dist/cjs/src/errors/serializers/serializerResolutionContextMessage.js +46 -0
  16. package/dist/cjs/src/helpers/cli/generateFactoryContent.js +7 -1
  17. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +80 -13
  18. package/dist/cjs/src/helpers/cli/syncWithFailureContext.js +41 -0
  19. package/dist/cjs/src/serializer/SerializerRenderer.js +32 -4
  20. package/dist/cjs/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +28 -10
  21. package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +6 -1
  22. package/dist/cjs/src/serializer/helpers/serializerGlobalName.js +16 -0
  23. package/dist/esm/src/Dream.js +10 -1
  24. package/dist/esm/src/cli/index.js +31 -5
  25. package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +127 -6
  26. package/dist/esm/src/dream/Query.js +247 -67
  27. package/dist/esm/src/dream/internal/printSerializerHierarchyLevel.js +4 -3
  28. package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +14 -1
  29. package/dist/esm/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
  30. package/dist/esm/src/errors/DropEnumValueRetypeFailed.js +208 -0
  31. package/dist/esm/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
  32. package/dist/esm/src/errors/serializers/MissingSerializersDefinition.js +16 -2
  33. package/dist/esm/src/errors/serializers/MissingSerializersDefinitionForKey.js +5 -2
  34. package/dist/esm/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.js +5 -3
  35. package/dist/esm/src/errors/serializers/NoSerializersResolvedForKey.js +5 -2
  36. package/dist/esm/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js +5 -3
  37. package/dist/esm/src/errors/serializers/serializerResolutionContextMessage.js +46 -0
  38. package/dist/esm/src/helpers/cli/generateFactoryContent.js +7 -1
  39. package/dist/esm/src/helpers/cli/generateMigrationContent.js +80 -13
  40. package/dist/esm/src/helpers/cli/syncWithFailureContext.js +41 -0
  41. package/dist/esm/src/serializer/SerializerRenderer.js +32 -4
  42. package/dist/esm/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +28 -10
  43. package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +6 -1
  44. package/dist/esm/src/serializer/helpers/serializerGlobalName.js +16 -0
  45. package/dist/types/src/Dream.d.ts +8 -2
  46. package/dist/types/src/cli/index.d.ts +1 -1
  47. package/dist/types/src/dream/Query.d.ts +316 -40
  48. package/dist/types/src/errors/BatchingIncompatibleWithLimitOrOffset.d.ts +3 -0
  49. package/dist/types/src/errors/DropEnumValueRetypeFailed.d.ts +89 -0
  50. package/dist/types/src/errors/MissingRequiredLockOptionForUpdateCallback.d.ts +3 -0
  51. package/dist/types/src/errors/serializers/MissingSerializersDefinition.d.ts +3 -1
  52. package/dist/types/src/errors/serializers/MissingSerializersDefinitionForKey.d.ts +3 -1
  53. package/dist/types/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.d.ts +3 -1
  54. package/dist/types/src/errors/serializers/NoSerializersResolvedForKey.d.ts +3 -1
  55. package/dist/types/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.d.ts +3 -1
  56. package/dist/types/src/errors/serializers/serializerResolutionContextMessage.d.ts +20 -0
  57. package/dist/types/src/helpers/cli/syncWithFailureContext.d.ts +58 -0
  58. package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +11 -4
  59. package/dist/types/src/serializer/helpers/serializerGlobalName.d.ts +15 -0
  60. package/dist/types/src/types/serializer.d.ts +38 -0
  61. package/dist/types/src/types/types/serializer.ts +40 -0
  62. package/docs/assets/search.js +1 -1
  63. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  64. package/docs/classes/db.KyselyQueryDriver.html +40 -40
  65. package/docs/classes/db.PostgresQueryDriver.html +41 -41
  66. package/docs/classes/db.QueryDriverBase.html +39 -39
  67. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  68. package/docs/classes/errors.ColumnOverflow.html +3 -3
  69. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  70. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  71. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  72. package/docs/classes/errors.DecryptionError.html +2 -2
  73. package/docs/classes/errors.DecryptionParseError.html +2 -2
  74. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  75. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  76. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  77. package/docs/classes/errors.InvalidClockTime.html +2 -2
  78. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  79. package/docs/classes/errors.InvalidDateTime.html +2 -2
  80. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  81. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  82. package/docs/classes/errors.NotNullViolation.html +3 -3
  83. package/docs/classes/errors.RecordNotFound.html +3 -3
  84. package/docs/classes/errors.ValidationError.html +3 -3
  85. package/docs/classes/index.CalendarDate.html +33 -33
  86. package/docs/classes/index.ClockTime.html +32 -32
  87. package/docs/classes/index.ClockTimeTz.html +35 -35
  88. package/docs/classes/index.DateTime.html +86 -86
  89. package/docs/classes/index.Decorators.html +19 -19
  90. package/docs/classes/index.Dream.html +123 -137
  91. package/docs/classes/index.DreamApp.html +10 -10
  92. package/docs/classes/index.DreamTransaction.html +2 -2
  93. package/docs/classes/index.Env.html +2 -2
  94. package/docs/classes/index.Query.html +735 -73
  95. package/docs/classes/system.CliFileWriter.html +4 -4
  96. package/docs/classes/system.DreamBin.html +2 -2
  97. package/docs/classes/system.DreamCLI.html +7 -7
  98. package/docs/classes/system.DreamImporter.html +2 -2
  99. package/docs/classes/system.DreamLogos.html +2 -2
  100. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  101. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  102. package/docs/classes/system.PathHelpers.html +3 -3
  103. package/docs/classes/utils.Encrypt.html +3 -3
  104. package/docs/classes/utils.Range.html +2 -2
  105. package/docs/functions/db.closeAllDbConnections.html +1 -1
  106. package/docs/functions/db.dreamDbConnections.html +1 -1
  107. package/docs/functions/db.untypedDb.html +1 -1
  108. package/docs/functions/db.validateColumn.html +1 -1
  109. package/docs/functions/db.validateTable.html +1 -1
  110. package/docs/functions/errors.pgErrorType.html +1 -1
  111. package/docs/functions/index.DreamSerializer.html +1 -1
  112. package/docs/functions/index.ObjectSerializer.html +1 -1
  113. package/docs/functions/index.ReplicaSafe.html +1 -1
  114. package/docs/functions/index.STI.html +1 -1
  115. package/docs/functions/index.SoftDelete.html +1 -1
  116. package/docs/functions/utils.camelize.html +1 -1
  117. package/docs/functions/utils.capitalize.html +1 -1
  118. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  119. package/docs/functions/utils.compact.html +1 -1
  120. package/docs/functions/utils.groupBy.html +1 -1
  121. package/docs/functions/utils.hyphenize.html +1 -1
  122. package/docs/functions/utils.intersection.html +1 -1
  123. package/docs/functions/utils.isEmpty.html +1 -1
  124. package/docs/functions/utils.normalizeUnicode.html +1 -1
  125. package/docs/functions/utils.pascalize.html +1 -1
  126. package/docs/functions/utils.percent.html +1 -1
  127. package/docs/functions/utils.range.html +1 -1
  128. package/docs/functions/utils.round.html +1 -1
  129. package/docs/functions/utils.sanitizeString.html +1 -1
  130. package/docs/functions/utils.snakeify.html +1 -1
  131. package/docs/functions/utils.sort.html +1 -1
  132. package/docs/functions/utils.sortBy.html +1 -1
  133. package/docs/functions/utils.sortObjectByKey.html +1 -1
  134. package/docs/functions/utils.sortObjectByValue.html +1 -1
  135. package/docs/functions/utils.uncapitalize.html +1 -1
  136. package/docs/functions/utils.uniq.html +1 -1
  137. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  138. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  139. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  140. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  141. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  142. package/docs/interfaces/types.DecoratorContext.html +2 -2
  143. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  144. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  145. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  146. package/docs/interfaces/types.DurationObject.html +2 -2
  147. package/docs/interfaces/types.EncryptOptions.html +2 -2
  148. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  149. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  150. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  151. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  152. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  153. package/docs/types/openapi.OpenapiFormats.html +1 -1
  154. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  155. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  156. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  157. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  158. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  159. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  160. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  161. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  163. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  164. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  165. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  166. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  167. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  168. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  169. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  170. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  171. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  181. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  183. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  184. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  185. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  186. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  187. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  189. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  190. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  191. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  192. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  193. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  194. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  195. package/docs/types/types.CalendarDateObject.html +1 -1
  196. package/docs/types/types.Camelized.html +1 -1
  197. package/docs/types/types.ClockTimeObject.html +1 -1
  198. package/docs/types/types.DbConnectionType.html +1 -1
  199. package/docs/types/types.DbTypes.html +1 -1
  200. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  201. package/docs/types/types.DreamAttributes.html +1 -1
  202. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  203. package/docs/types/types.DreamClassColumn.html +1 -1
  204. package/docs/types/types.DreamColumn.html +1 -1
  205. package/docs/types/types.DreamColumnNames.html +1 -1
  206. package/docs/types/types.DreamLogLevel.html +1 -1
  207. package/docs/types/types.DreamLogger.html +2 -2
  208. package/docs/types/types.DreamModelSerializerType.html +1 -1
  209. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  210. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  211. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  212. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  213. package/docs/types/types.DreamSerializable.html +1 -1
  214. package/docs/types/types.DreamSerializableArray.html +1 -1
  215. package/docs/types/types.DreamSerializerKey.html +1 -1
  216. package/docs/types/types.DreamSerializers.html +1 -1
  217. package/docs/types/types.DreamVirtualColumns.html +1 -1
  218. package/docs/types/types.DurationUnit.html +1 -1
  219. package/docs/types/types.EncryptAlgorithm.html +1 -1
  220. package/docs/types/types.HasManyStatement.html +1 -1
  221. package/docs/types/types.HasOneStatement.html +1 -1
  222. package/docs/types/types.Hyphenized.html +1 -1
  223. package/docs/types/types.Pascalized.html +1 -1
  224. package/docs/types/types.PrimaryKeyType.html +1 -1
  225. package/docs/types/types.RoundingPrecision.html +1 -1
  226. package/docs/types/types.SerializerCasing.html +1 -1
  227. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  228. package/docs/types/types.Snakeified.html +1 -1
  229. package/docs/types/types.StrictInterface.html +1 -1
  230. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  231. package/docs/types/types.UpdateableProperties.html +1 -1
  232. package/docs/types/types.ValidationType.html +1 -1
  233. package/docs/types/types.ViewModel.html +2 -2
  234. package/docs/types/types.ViewModelClass.html +1 -1
  235. package/docs/types/types.WeekdayName.html +1 -1
  236. package/docs/types/types.WhereStatementForDream.html +1 -1
  237. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  238. package/docs/variables/index.DreamConst.html +1 -1
  239. package/docs/variables/index.ops.html +1 -1
  240. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  241. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  242. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  243. package/docs/variables/system.primaryKeyTypes.html +1 -1
  244. package/package.json +1 -1
@@ -9,33 +9,51 @@ import expandStiClasses from '../../helpers/sti/expandStiClasses.js';
9
9
  import uniq from '../../helpers/uniq.js';
10
10
  import isDreamSerializer from './isDreamSerializer.js';
11
11
  export const DEFAULT_SERIALIZER_KEY = 'default';
12
- export default function inferSerializerFromDreamOrViewModel(obj, serializerKey = DEFAULT_SERIALIZER_KEY) {
12
+ /**
13
+ * `resolutionContext` says how resolution reached `obj` — the `rendersOne`/`rendersMany` edge that
14
+ * led here, and the STI base it was expanded from. It is threaded down purely so that the errors
15
+ * below can name the route instead of only the class, and it is optional so that a caller with no
16
+ * route to describe (and every existing caller outside this repo) is unaffected. See
17
+ * `SerializerResolutionContext`.
18
+ */
19
+ export default function inferSerializerFromDreamOrViewModel(obj, serializerKey = DEFAULT_SERIALIZER_KEY, resolutionContext) {
13
20
  if (!obj)
14
21
  return null;
15
22
  const serializers = obj.serializers;
16
23
  if (!serializers)
17
- throw new MissingSerializersDefinition(obj);
24
+ throw new MissingSerializersDefinition(obj, serializerKey, resolutionContext);
18
25
  const serializerOrGlobalName = serializers[serializerKey];
19
26
  if (!serializerOrGlobalName)
20
- throw new MissingSerializersDefinitionForKey(obj, serializerKey);
27
+ throw new MissingSerializersDefinitionForKey(obj, serializerKey, resolutionContext);
21
28
  if (isDreamSerializer(serializerOrGlobalName))
22
29
  return serializerOrGlobalName;
23
30
  const globalName = serializerOrGlobalName;
24
31
  const dreamApp = DreamApp.getOrFail();
25
32
  const serializer = dreamApp.serializers[globalName];
26
33
  if (!serializer)
27
- throw new NoGlobalSerializerForSpecifiedKey(obj, serializerKey, globalName);
34
+ throw new NoGlobalSerializerForSpecifiedKey(obj, serializerKey, globalName, resolutionContext);
28
35
  if (!isDreamSerializer(serializer))
29
- throw new NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey(obj, serializerKey, globalName, serializer);
36
+ throw new NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey(obj, serializerKey, globalName, serializer, resolutionContext);
30
37
  return serializer;
31
38
  }
32
- export function inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey = undefined) {
39
+ export function inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey = undefined, resolutionContext) {
33
40
  if (!classDef)
34
41
  return [];
35
42
  const classes = expandStiClasses(classDef);
36
- const serializers = classes.map(classDef => inferSerializerFromDreamOrViewModel(classDef.prototype, serializerKey));
43
+ // This is the one place that knows *whether* the class an error ends up naming came from STI
44
+ // expansion: below this line the class the caller passed is gone, replaced by one child per
45
+ // iteration, and every downstream error names a child. Identity rather than `isSTIBase` because
46
+ // `classDef` may be a ViewModelClass, which `expandStiClasses` hands straight back.
47
+ const expandedFromStiBase = classes.length !== 1 || classes[0] !== classDef;
48
+ const childResolutionContext = expandedFromStiBase
49
+ ? { ...resolutionContext, stiBase: stiBaseName(classDef) }
50
+ : resolutionContext;
51
+ const serializers = classes.map(classDef => inferSerializerFromDreamOrViewModel(classDef.prototype, serializerKey, childResolutionContext));
37
52
  return uniq(compact(serializers));
38
53
  }
54
+ function stiBaseName(classDef) {
55
+ return classDef.sanitizedName ?? classDef.name;
56
+ }
39
57
  /**
40
58
  * As `inferSerializersFromDreamClassOrViewModelClass`, but guarantees a non-empty result.
41
59
  *
@@ -51,9 +69,9 @@ export function inferSerializersFromDreamClassOrViewModelClass(classDef, seriali
51
69
  * preload-path building and serialization display — so that they raise one typed error between
52
70
  * them.
53
71
  */
54
- export function inferSerializersFromDreamClassOrViewModelClassOrFail(classDef, serializerKey) {
55
- const serializers = inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey);
72
+ export function inferSerializersFromDreamClassOrViewModelClassOrFail(classDef, serializerKey, resolutionContext) {
73
+ const serializers = inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey, resolutionContext);
56
74
  if (serializers.length === 0)
57
- throw new NoSerializersResolvedForKey(classDef, serializerKey);
75
+ throw new NoSerializersResolvedForKey(classDef, serializerKey, resolutionContext);
58
76
  return serializers;
59
77
  }
@@ -28,5 +28,10 @@ export function serializerForAssociatedClass(object, associationName, options) {
28
28
  // would flatten. Unioning every child's serializer here — which is what buildSerializerPreloadPaths
29
29
  // does for preload paths — is meaningless: exactly one set of keys can be flattened into the
30
30
  // payload.
31
- return inferSerializersFromDreamClassOrViewModelClass(associatedClass, options.serializerKey)[0] ?? null;
31
+ return (inferSerializersFromDreamClassOrViewModelClass(associatedClass, options.serializerKey, {
32
+ // Reached only from SerializerRenderer's rendersOne branch, so the edge type is fixed. No
33
+ // declaring serializer: the renderer holds the rendered attribute, not the serializer that
34
+ // declared it.
35
+ edge: { type: 'rendersOne', associationName },
36
+ })[0] ?? null);
32
37
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * The single reader of a serializer's `globalName`.
5
+ *
6
+ * Serializers are plain functions; `globalName` is stamped onto them by `importSerializers` when the
7
+ * app loads them, so the property is not on any serializer type and every reader would otherwise
8
+ * have to invent its own cast — three different spellings of it existed before this helper.
9
+ *
10
+ * `undefined` is a legitimate answer: a serializer built inline (a spec, or a `{ serializer }`
11
+ * option) is never stamped. Callers decide what that means for them — the resolution-context message
12
+ * simply drops the declarer.
13
+ */
14
+ export default function serializerGlobalName(serializer) {
15
+ return serializer.globalName;
16
+ }
@@ -8,7 +8,7 @@ import LoadBuilder from './dream/LoadBuilder.js';
8
8
  import Query from './dream/Query.js';
9
9
  import { OnStatementForSpecificColumns, PassthroughOnClause, WhereStatement } from './types/associations/shared.js';
10
10
  import { AssociationTableNames, DbConnectionType } from './types/db.js';
11
- import { AllDefaultScopeNames, AssociationNameToDream, AttributeKeys, CreateOrFindByExtraOpts, DefaultOrNamedScopeName, DreamAssociationNames, DreamAssociationNamesWithoutRequiredOnClauses, DreamAssociationNamesWithPassthroughOnClauses, DreamAssociationNamesWithRequiredOnClauses, DreamAttributes, DreamBelongsToAssociationNames, DreamColumnNames, DreamHasManyAssociationNames, DreamHasOneAssociationNames, DreamModelAssociationNames, DreamParamSafeColumnNames, DreamPrimaryKeyType, DreamSerializerKey, FindablePropertiesForClass, GlobalModelNames, GlobalSerializerName, JoinAndStatements, OrderDir, PassthroughColumnNames, PassthroughOnClauseKeys, PluckEachArgs, PrimaryKeyForFind, RequiredOnClauseKeys, TableColumnNames, UpdateableAssociationProperties, UpdateableProperties, UpdateablePropertiesForClass, UpdateOrCreateByExtraOpts } from './types/dream.js';
11
+ import { AllDefaultScopeNames, AssociationNameToDream, AttributeKeys, CreateOrFindByExtraOpts, DefaultOrNamedScopeName, DreamAssociationNames, DreamAssociationNamesWithoutRequiredOnClauses, DreamAssociationNamesWithPassthroughOnClauses, DreamAssociationNamesWithRequiredOnClauses, DreamAttributes, DreamBelongsToAssociationNames, DreamColumnNames, DreamHasManyAssociationNames, DreamHasOneAssociationNames, DreamModelAssociationNames, DreamPrimaryKeyType, DreamSerializerKey, FindablePropertiesForClass, GlobalModelNames, GlobalSerializerName, JoinAndStatements, OrderDir, PassthroughColumnNames, PassthroughOnClauseKeys, PluckEachArgs, PrimaryKeyForFind, RequiredOnClauseKeys, TableColumnNames, UpdateableAssociationProperties, UpdateableProperties, UpdateablePropertiesForClass, UpdateOrCreateByExtraOpts } from './types/dream.js';
12
12
  import { BaseModelColumnTypes, CursorPaginatedDreamQueryOptions, CursorPaginatedDreamQueryResult, DefaultQueryTypeOptions, FindEachOpts, LoadForModifierFn, PaginatedDreamQueryOptions, PaginatedDreamQueryResult, QueryWithJoinedAssociationsType, QueryWithJoinedAssociationsTypeAndNoPreload } from './types/query.js';
13
13
  import { DreamModelSerializerType, SimpleObjectSerializerType } from './types/serializer.js';
14
14
  import { ValidationType } from './types/validation.js';
@@ -458,9 +458,15 @@ export default class Dream {
458
458
  * * foreign key fields for belongs to associations (these should usually be verified before being set)
459
459
  * * type fields corresponding to polymorphic associations
460
460
  *
461
+ * @internal
462
+ *
463
+ * Not part of the public API. Psychic reaches it through the bracketed
464
+ * back-door (`dreamClass['paramSafeColumnsOrFallback']()`), the same escape
465
+ * hatch used for other Dream internals such as `virtualAttributes`.
466
+ *
461
467
  * @returns A subset of columns for the given dream class
462
468
  */
463
- static paramSafeColumnsOrFallback<T extends typeof Dream, I extends InstanceType<T>, ParamSafeColumnsOverride extends InstanceType<T>['paramSafeColumns' & keyof InstanceType<T>] extends never ? undefined : InstanceType<T>['paramSafeColumns' & keyof InstanceType<T>] & string[], ReturnVal extends ParamSafeColumnsOverride extends string[] ? Extract<DreamParamSafeColumnNames<I>, ParamSafeColumnsOverride[number] & DreamParamSafeColumnNames<I>>[] : DreamParamSafeColumnNames<I>[]>(this: T): ReturnVal;
469
+ private static paramSafeColumnsOrFallback;
464
470
  private static defaultParamSafeColumns;
465
471
  /**
466
472
  * @internal
@@ -23,7 +23,7 @@ export declare const columnsWithTypesDescriptionForStiChild: string;
23
23
  * example that drifts from what the generator actually produces then fails the
24
24
  * suite instead of shipping.
25
25
  */
26
- export declare const migrationNameArgumentDescription = "Kebab-case name describing the change. A -to-<table_name>\n anywhere in the name \u2014 not just at the end \u2014 auto-generates\n an add-columns alterTable scaffold for that table;\n -from-<table_name> generates a remove-columns scaffold, but\n only when the name contains no -to- at all. The text after\n the marker's first occurrence is snake-cased and pluralized\n into the table name, so add-a-to-b-to-cs targets b_to_cs. A\n hand-written migration must avoid both strings anywhere in\n its name.\n \n Examples:\n add-phone-to-users phone:encrypted # scaffolds alterTable('users', ...)\n remove-status-from-posts status:string # scaffolds alterTable('posts', ...)\n create-many-to-many-posts-users # WRONG: selects 'many_posts_users'\n # -to- inside \"many-to-many\" matches: with no columns\n # this errors; with columns it silently scaffolds it\n create-posts-users-join-table # correct: no marker match\n # scaffolds alterTable('<table-name>') to hand-edit";
26
+ export declare const migrationNameArgumentDescription = "Kebab-case name describing the change. A -to-<table_name>\n anywhere in the name \u2014 not just at the end \u2014 auto-generates\n an add-columns alterTable scaffold for that table;\n -from-<table_name> generates a remove-columns scaffold, but\n only when the name contains no -to- at all. The text after\n the marker's first occurrence is snake-cased and pluralized\n into the table name, so add-a-to-b-to-cs targets b_to_cs. A\n hand-written migration must avoid both strings anywhere in\n its name.\n \n Examples:\n add-phone-to-users phone:encrypted # scaffolds alterTable('users', ...)\n remove-status-from-posts status:string # scaffolds alterTable('posts', ...)\n create-many-to-many-posts-users # WRONG: selects 'many_posts_users'\n # -to- inside \"many-to-many\" matches: with no columns\n # this errors; with columns it silently scaffolds it\n create-posts-users-join-table # correct: no marker match\n # with no columns, generates empty up/down methods to\n # write by hand; with columns, scaffolds\n # alterTable('<table-name>') to hand-edit";
27
27
  export default class DreamCLI {
28
28
  /**
29
29
  * Starts the Dream console
@@ -27,6 +27,7 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
27
27
  static readonly BATCH_SIZES: {
28
28
  FIND_EACH: number;
29
29
  LOCKED_DESTROY: number;
30
+ LOCKED_UPDATE: number;
30
31
  PLUCK_EACH: number;
31
32
  PLUCK_EACH_THROUGH: number;
32
33
  };
@@ -317,10 +318,16 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
317
318
  * If you need records processed in a particular order, load them with a
318
319
  * regular ordered query instead of `findEach`.
319
320
  *
321
+ * A `limit` or `offset` on the Query cannot compose with the batch windows
322
+ * — each batch re-applies the Query's conditions, so the limit would be
323
+ * silently replaced by `batchSize` and the offset re-applied to every
324
+ * window — so a limit- or offset-carrying Query throws at runtime.
325
+ *
320
326
  * @param cb - The callback to call for each found record
321
327
  * @param __namedParameters - Options for batch processing
322
328
  * @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
323
329
  * @returns void
330
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
324
331
  */
325
332
  findEach(this: Query<DreamInstance, QueryTypeOpts>, cb: (instance: DreamInstance) => void | Promise<void>, { batchSize }?: {
326
333
  batchSize?: number;
@@ -639,6 +646,12 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
639
646
  * // [User{id: 1}, User{id: 2}]
640
647
  * ```
641
648
  *
649
+ * A limit is incompatible with `paginate` and `leftJoinPreload`, and with
650
+ * the batched and set-write operations — `findEach`, `pluckEach`,
651
+ * `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can
652
+ * honor one: the batched and set-write operations throw
653
+ * `BatchingIncompatibleWithLimitOrOffset` on a limit-carrying Query.
654
+ *
642
655
  * @returns A cloned Query with the limit clause applied
643
656
  */
644
657
  limit<Q extends Query<DreamInstance, any>, Incompatible extends Q['queryTypeOpts'] extends Readonly<{
@@ -655,6 +668,12 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
655
668
  * // [User{id: 3}, User{id: 4}]
656
669
  * ```
657
670
  *
671
+ * An offset is incompatible with `paginate` and `leftJoinPreload`, and with
672
+ * the batched and set-write operations — `findEach`, `pluckEach`,
673
+ * `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can
674
+ * honor one: the batched and set-write operations throw
675
+ * `BatchingIncompatibleWithLimitOrOffset` on an offset-carrying Query.
676
+ *
658
677
  * @returns A cloned Query with the offset clause applied
659
678
  */
660
679
  offset<Q extends Query<DreamInstance, any>, Incompatible extends Q['queryTypeOpts'] extends Readonly<{
@@ -978,8 +997,14 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
978
997
  * // 3
979
998
  * ```
980
999
  *
1000
+ * A `limit` or `offset` on the Query cannot compose with the batch windows
1001
+ * — each window replaces the limit with the batch size and the offset with
1002
+ * the running window offset, silently ignoring both — so a limit- or
1003
+ * offset-carrying Query throws at runtime.
1004
+ *
981
1005
  * @param args - a list of column names to pluck, followed by a callback function to call for each set of found fields
982
1006
  * @returns void
1007
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
983
1008
  */
984
1009
  pluckEach<Q extends Query<DreamInstance, QueryTypeOpts>, DB extends DreamInstance['DB'], const ColumnNames extends ColumnNamesAccountingForJoinedAssociations<Q['queryTypeOpts']['joinedAssociations'], DB, QueryTypeOpts['rootTableName'], QueryTypeOpts['rootTableAlias']>[], CbArgTypes extends NamespacedOrBaseModelColumnTypes<ColumnNames, Q, DreamInstance>>(this: Q, ...args: PluckEachArgs<ColumnNames, CbArgTypes>): Promise<void>;
985
1010
  /**
@@ -1307,20 +1332,27 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1307
1332
  * scale, ties up connections. If that is a risk in your application, set a
1308
1333
  * `lock_timeout` on the connection so a stuck batch fails instead of
1309
1334
  * hanging.
1335
+ * - **A `limit` or `offset` on the Query is incompatible with `destroy` on
1336
+ * every path.** The batches — locked and unlocked alike — re-apply the
1337
+ * Query's conditions to each window, where a limit or offset would skip
1338
+ * or truncate rows inside every window instead of bounding the run as a
1339
+ * whole, so `destroy` throws rather than silently corrupting the
1340
+ * windows.
1310
1341
  * - **If you are destroying a large set and do not need compare-and-set, do
1311
1342
  * not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
1312
1343
  * {@link Query.delete | delete} (a single statement, no hooks or cascade)
1313
1344
  * are the right tools for bulk removal.
1314
1345
  *
1315
- * @param __namedParameters - Options for destroying the instance
1316
- * @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
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
1320
- * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1321
- * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1346
+ * @param options - Options for destroying the instance
1347
+ * @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
1348
+ * @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
1349
+ * @param options.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
1350
+ * @param options.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
1351
+ * @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1352
+ * @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1322
1353
  * @returns The number of records that were removed
1323
1354
  * @throws InvalidBatchSize if `batchSize` is not a positive integer
1355
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
1324
1356
  */
1325
1357
  destroy({ skipHooks, cascade, lock, batchSize, }?: {
1326
1358
  skipHooks?: boolean | undefined;
@@ -1331,37 +1363,52 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1331
1363
  /**
1332
1364
  * @internal
1333
1365
  *
1334
- * The compare-and-set implementation behind `destroy({ lock: true })`.
1366
+ * The compare-and-set implementation behind `destroy({ lock: true })`,
1367
+ * driven by {@link Query.lockedBatches}, which owns the keyset cursor, the
1368
+ * pluck/claim two-step, and the transaction dispatch.
1369
+ *
1370
+ * Each claimed record is destroyed via the ordinary per-instance path, and
1371
+ * every claimed record counts. (One thing the count does not account for,
1372
+ * because it never has: a `beforeDestroy` hook that sets `_preventDeletion`
1373
+ * leaves the record in place but still counts.)
1374
+ */
1375
+ private lockedDestroy;
1376
+ /**
1377
+ * @internal
1378
+ *
1379
+ * The shared batch driver behind `destroy({ lock: true })` and
1380
+ * `update(..., { lock: true })`: the keyset cursor, the pluck/claim
1381
+ * two-step, and the transaction dispatch live here, so the locked
1382
+ * operations cannot drift apart.
1335
1383
  *
1336
1384
  * Each batch is handled in two steps inside one transaction:
1337
1385
  *
1338
1386
  * 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
1339
1387
  * 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
1388
+ * cursor) and tells us how many records were *attempted*. Only the keys
1389
+ * are read: hydrating and preloading these records would duplicate the
1390
+ * work step 2 does over the same rows; then
1343
1391
  * 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.
1392
+ * re-applies every condition on the Query while acquiring an exclusive
1393
+ * row lock on each row it returns. Records a concurrent transaction has
1394
+ * moved out of the Query since step 1 do not come back, and are the
1395
+ * records this run lost the race for.
1396
+ *
1397
+ * The claimed records are handed to `perBatch` inside the batch's
1398
+ * transaction; its return value — how many of the claimed records were
1399
+ * processed — accumulates into the returned count.
1400
+ *
1401
+ * The two-step shape is what keeps iteration correct. If the locked read
1402
+ * both claimed rows and advanced the cursor, a batch whose records all lost
1403
+ * the race would come back empty and end the run early, silently skipping
1404
+ * every record after it. Advancing the cursor from step 1 instead means
1405
+ * losing a race skips only the records that were lost.
1359
1406
  *
1360
1407
  * When the Query already carries a transaction, that transaction is used for
1361
1408
  * every batch rather than opening one per batch, so the caller's transaction
1362
1409
  * boundary wins and the locks are held until the caller commits.
1363
1410
  */
1364
- private lockedDestroy;
1411
+ private lockedBatches;
1365
1412
  /**
1366
1413
  * Destroys all records matching the Query,
1367
1414
  * ignoring the SoftDelete decorator and
@@ -1380,15 +1427,20 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1380
1427
  * // 12
1381
1428
  * ```
1382
1429
  *
1383
- * @param __namedParameters - Options for destroying the instance
1384
- * @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
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
1388
- * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1389
- * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1430
+ * A `limit` or `offset` on the Query is incompatible with `reallyDestroy`,
1431
+ * exactly as it is with {@link Query.destroy}: a limit- or offset-carrying
1432
+ * Query throws at runtime.
1433
+ *
1434
+ * @param options - Options for destroying the instance
1435
+ * @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
1436
+ * @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
1437
+ * @param options.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
1438
+ * @param options.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
1439
+ * @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1440
+ * @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1390
1441
  * @returns The number of records that were removed
1391
1442
  * @throws InvalidBatchSize if `batchSize` is not a positive integer
1443
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
1392
1444
  */
1393
1445
  reallyDestroy({ skipHooks, cascade, lock, batchSize, }?: {
1394
1446
  skipHooks?: boolean;
@@ -1408,12 +1460,19 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1408
1460
  * // 12
1409
1461
  * ```
1410
1462
  *
1411
- * @param __namedParameters - Options for undestroying the instance
1412
- * @param __namedParameters.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false
1413
- * @param __namedParameters.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true
1414
- * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade undestroying. Defaults to false
1415
- * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade undestroying (soft delete is always bypassed). Defaults to an empty array
1463
+ * A `limit` or `offset` on the Query cannot compose with the batch windows
1464
+ * `undestroy` iterates in — each batch re-applies the Query's conditions,
1465
+ * so the limit would be silently replaced by the batch size and the offset
1466
+ * re-applied to every window — so a limit- or offset-carrying Query throws
1467
+ * at runtime.
1468
+ *
1469
+ * @param options - Options for undestroying the instance
1470
+ * @param options.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false
1471
+ * @param options.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true
1472
+ * @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade undestroying. Defaults to false
1473
+ * @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade undestroying (soft delete is always bypassed). Defaults to an empty array
1416
1474
  * @returns The number of records that were removed
1475
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
1417
1476
  */
1418
1477
  undestroy({ cascade, skipHooks, }?: {
1419
1478
  cascade?: boolean;
@@ -1443,15 +1502,232 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
1443
1502
  * await User.where({ email: ops.ilike('%burpcollaborator%') }).update({ email: null })
1444
1503
  * // 12
1445
1504
  * ```
1446
- * @param attributes - The attributes used to update the records
1505
+ *
1506
+ * Instead of an attributes object, the first argument may be a callback
1507
+ * that derives each record's attributes from the record itself. The
1508
+ * callback (sync or async) receives each matching record and returns the
1509
+ * attributes to write for it, or `undefined` to skip it — a skipped record
1510
+ * gets no write, runs no hooks, and is not counted (a `null` return, from
1511
+ * untyped callers, is treated the same). Only the returned attributes are
1512
+ * written: the callback receives a clone of the record, so mutations made
1513
+ * directly on the received record are discarded rather than riding along
1514
+ * with the write. With a callback, the options object and its `lock` key
1515
+ * are mandatory, and `lock` is a full boolean, so every call site says out
1516
+ * loud whether the callback runs under row locks:
1517
+ *
1518
+ * ```ts
1519
+ * await User.query().update(
1520
+ * user => (user.email.endsWith('@example.com') ? { name: 'example user' } : undefined),
1521
+ * { lock: false }
1522
+ * )
1523
+ * ```
1524
+ *
1525
+ * Under `lock: false`, the callback runs once per matching record —
1526
+ * batched reads (`batchSize` defaults to 1000 here), no locks, no claim
1527
+ * step, no compare-and-set guarantee — the right shape for a derived
1528
+ * backfill where racing writers are not a concern. (On a `@ReplicaSafe`
1529
+ * model, these unlocked batch reads may come from a lagging replica,
1530
+ * feeding stale column values into the callback; use `lock: true` to
1531
+ * derive from the freshest committed state — its transactional reads pin
1532
+ * the primary.) Under `lock: true`, the callback runs once per claimed
1533
+ * record, under that record's row lock, inside the batch's transaction,
1534
+ * receiving a clone of the freshly locked read; Dream applies the returned
1535
+ * attributes to the record it holds and writes before the batch commits,
1536
+ * so there is no transaction handle for the caller to forget. (The record
1537
+ * the callback receives is not bound to the batch's transaction — see the
1538
+ * callback bullet below before touching the database from inside a
1539
+ * callback.) That makes the callback form the tool
1540
+ * for serializing writers on state a `WHERE` clause cannot express — for
1541
+ * example an `@Encrypted` column, whose ciphertext changes on every write
1542
+ * and whose plaintext has no column to compare against in the database:
1543
+ *
1544
+ * ```ts
1545
+ * const rotated = await User.where({ id: userId }).update(
1546
+ * user => (user.secret === expectedSecret ? { secret: newSecret } : undefined),
1547
+ * { lock: true }
1548
+ * )
1549
+ * // 1 — or 0, if a competing writer got there first (the callback saw a
1550
+ * // different secret and skipped the record)
1551
+ * ```
1552
+ *
1553
+ * With a callback, the returned count is the number of records the
1554
+ * callback returned attributes for: skipped records are not counted, and
1555
+ * under `lock: true` records that lost the race never reach the callback
1556
+ * at all. (As with the attributes form, a write that turns out to be a
1557
+ * no-op still counts — the count means "the callback chose to write", not
1558
+ * "SQL was emitted".)
1559
+ *
1560
+ * The `lock` option turns the update into a guarded (compare-and-set)
1561
+ * update: each record is written only if it still matches the Query at the
1562
+ * moment an exclusive row lock on it is acquired. Use it when the Query's
1563
+ * conditions are the thing being changed and a concurrent writer racing this
1564
+ * call must not have its write clobbered:
1565
+ *
1566
+ * ```ts
1567
+ * const updated = await Order.where({ status: 'pending' })
1568
+ * .update({ status: 'processing' }, { lock: true })
1569
+ * // 0 — someone else moved every one of them out of 'pending' first
1570
+ * ```
1571
+ *
1572
+ * With `lock: true`, records are processed in batches, and each batch runs
1573
+ * in its own transaction which first re-selects that batch's records with an
1574
+ * exclusive row lock and then updates them before committing. Because the
1575
+ * database re-checks the Query's conditions after acquiring each row lock, a
1576
+ * record a concurrent transaction has already moved out of the Query drops
1577
+ * out of the locked read and is never updated. The returned count is the
1578
+ * number of records claimed and submitted for writing, so a caller can
1579
+ * detect that it lost a race by comparing the count against what it
1580
+ * expected. (A claimed record whose write turns out to be a no-op — the
1581
+ * attributes already match, or a hook cleared every dirty attribute — still
1582
+ * counts; the count is not a guarantee that SQL was emitted per counted
1583
+ * record.)
1584
+ *
1585
+ * Things to know before relying on it:
1586
+ *
1587
+ * - **The guarantee is per batch, not set-wide.** A run spanning more than
1588
+ * one batch can win a race in batch 1 and lose one in batch 2; nothing
1589
+ * holds the whole matched set still for the duration of the run. Sharing
1590
+ * one transaction across every batch — by threading a transaction onto the
1591
+ * Query itself, `Order.query().txn(txn).where({ ... }).update({ ... },
1592
+ * { lock: true })` — buys two things: the updates become all-or-nothing (a
1593
+ * rollback undoes the whole run), and the row locks taken by every batch
1594
+ * are held until you commit. It does **not** extend the compare-and-set to
1595
+ * the part of the set the run has not reached yet: under READ COMMITTED
1596
+ * each statement takes a fresh snapshot, so a record moved out of the
1597
+ * Query between batch 1 and batch 2 still drops out of batch 2's locked
1598
+ * read. A genuinely set-wide guarantee needs REPEATABLE READ /
1599
+ * SERIALIZABLE, or a single locking statement over the whole set.
1600
+ * - **Only a transaction carried by the Query is shared.** Merely wrapping
1601
+ * the call in `Model.transaction(...)` does not thread that transaction
1602
+ * onto the Query — `Dream.transaction` establishes no ambient transaction,
1603
+ * it only hands a `txn` to your callback — so each batch would open its
1604
+ * own transaction on a different pooled connection. Worse, if the
1605
+ * surrounding transaction has already written to any of the matched rows,
1606
+ * each batch's locked read blocks on locks the surrounding transaction
1607
+ * holds while that transaction awaits the update: a cycle across two
1608
+ * connections, which Postgres's deadlock detector cannot see, so it hangs
1609
+ * rather than aborting. Always pass the transaction with `.txn(txn)`.
1610
+ * - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result
1611
+ * behavior described above is READ COMMITTED (Postgres's default). Under
1612
+ * REPEATABLE READ or SERIALIZABLE, the same race raises a serialization
1613
+ * failure instead of silently returning a lower count. Both are safe; only
1614
+ * one of them is quiet.
1615
+ * - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the
1616
+ * default is deliberately small. Every row in a batch stays locked for the
1617
+ * whole batch, including the time spent running each record's hooks — and,
1618
+ * on the callback form, the time spent running the Query's `preload`
1619
+ * queries, which are fetched inside the lock window (the attributes form
1620
+ * strips preloads, which it never consumes) — so an oversized batch blocks
1621
+ * unrelated writers touching those rows and surfaces as timeouts somewhere
1622
+ * else in the application, which is very hard to trace back to this
1623
+ * update. Too small a batch, by contrast, only slows down this call, which
1624
+ * you can see and fix. Lower it further for models with expensive update
1625
+ * hooks.
1626
+ * - **The callback must not write to the database.** The record handed to
1627
+ * the callback is not bound to the batch's transaction: a write through it
1628
+ * (e.g. `await record.update(...)`) goes out on a different pooled
1629
+ * connection and blocks on the batch's own row locks while the batch
1630
+ * transaction waits on the callback — a cycle across two connections that
1631
+ * the database's deadlock detector cannot see, so with no `lock_timeout`
1632
+ * the run hangs indefinitely and pins both connections. Reads through the
1633
+ * record (`reload`, `associationQuery`, ...) likewise run on a separate
1634
+ * connection and snapshot. Derive the attributes and return them; Dream
1635
+ * performs the write inside the batch's transaction.
1636
+ * - **`lock: false` can throw mid-run when racing a deleter.** On the
1637
+ * unlocked paths, a matched record that a concurrent transaction destroys
1638
+ * between a batch's read and that record's write aborts the run with an
1639
+ * error: earlier batches stay committed, and no count is returned. Use
1640
+ * `lock: true` when deleters may race the update — claimed rows are held
1641
+ * under their locks until written.
1642
+ * - **A `limit` or `offset` on the Query is incompatible with `update` on
1643
+ * every path.** The batched paths re-apply the Query's conditions to each
1644
+ * batch window, where a limit or offset would skip or truncate rows
1645
+ * inside every window instead of bounding the run as a whole, and the
1646
+ * single-statement `skipHooks` attributes form cannot express either in
1647
+ * its SQL UPDATE. Every form throws rather than silently corrupting the
1648
+ * windows or ignoring the caller's scope.
1649
+ * - **A batch waits, without bound, for whoever holds the row.** The locked
1650
+ * read is a plain `FOR UPDATE`: if another transaction already holds a
1651
+ * lock on one of the batch's rows, this call blocks until that transaction
1652
+ * ends, holding its own transaction and its pooled connection open the
1653
+ * whole time. Dream sets no `lock_timeout` or `statement_timeout`, so a
1654
+ * long-running writer on the other side stalls the run indefinitely and,
1655
+ * at scale, ties up connections. If that is a risk in your application,
1656
+ * set a `lock_timeout` on the connection so a stuck batch fails instead of
1657
+ * hanging.
1658
+ * - **`skipHooks` composes with `lock`, but stays on the per-instance
1659
+ * path.** With an attributes object and no `lock`, `skipHooks: true`
1660
+ * issues a single `UPDATE ... WHERE` statement that writes raw columns.
1661
+ * With `lock: true`, `skipHooks: true` skips the model hooks on each
1662
+ * claimed record but still writes through the instance, so custom setters
1663
+ * — including the setters encrypted attributes encrypt through — run as
1664
+ * they would for a plain instance update. (Virtual attributes whose
1665
+ * processing lives in a hook are still skipped, exactly as in an instance
1666
+ * update with `skipHooks`.) A callback never takes the single-statement
1667
+ * path: `skipHooks` beside a callback only skips the hooks on each
1668
+ * per-record write, under `lock: true` and `lock: false` alike.
1669
+ * - **If you are updating a large set and do not need compare-and-set, do
1670
+ * not pass `lock`.** Plain `update(attributes)` (hooks, no locking) or
1671
+ * `update(attributes, { skipHooks: true })` (a single statement, no hooks)
1672
+ * are the right tools for bulk updates.
1673
+ *
1674
+ * @param attributesOrCb - The attributes used to update the records, or a callback receiving each record and returning the attributes to write for it (or `undefined` to skip that record). Passing a callback makes `options` and its `lock` key mandatory
1447
1675
  * @param options - Options for updating the instance
1448
1676
  * @param options.skipHooks - If true, skips applying model hooks. Defaults to false
1677
+ * @param options.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being updated, making the update a compare-and-set. Defaults to false for the attributes form; with a callback there is no default — an explicit boolean is required. One typing asymmetry to know: with an attributes object, an explicit `lock: false` keeps the raw single-statement (`DreamTableSchema`) typing that `skipHooks` uses, so virtual/encrypted attributes type-check only when the options object is omitted entirely or `lock` is `true`
1678
+ * @param options.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 for the unlocked callback form
1449
1679
  * @returns The number of records that were updated
1680
+ * @throws InvalidBatchSize if `batchSize` is not a positive integer
1681
+ * @throws MissingRequiredLockOptionForUpdateCallback if a callback is passed without an options object carrying a boolean `lock`
1682
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
1450
1683
  */
1684
+ update(cb: (record: DreamInstance) => UpdateableProperties<DreamInstance> | undefined | Promise<UpdateableProperties<DreamInstance> | undefined>, options: {
1685
+ lock: boolean;
1686
+ batchSize?: number;
1687
+ skipHooks?: boolean;
1688
+ }): Promise<number>;
1689
+ update(attributes: UpdateableProperties<DreamInstance>, options: {
1690
+ lock: true;
1691
+ batchSize?: number;
1692
+ skipHooks?: boolean;
1693
+ }): Promise<number>;
1451
1694
  update(attributes: DreamTableSchema<DreamInstance>, options?: {
1452
1695
  skipHooks?: boolean;
1696
+ lock?: false;
1453
1697
  }): Promise<number>;
1454
1698
  update(attributes: UpdateableProperties<DreamInstance>): Promise<number>;
1699
+ /**
1700
+ * @internal
1701
+ *
1702
+ * The compare-and-set implementation behind `update(attributes, { lock: true })`,
1703
+ * driven by {@link Query.lockedBatches}, which owns the keyset cursor, the
1704
+ * pluck/claim two-step, and the transaction dispatch.
1705
+ *
1706
+ * Each claimed record is handed to `cb`, which returns the attributes to
1707
+ * write for it, or a nullish value to skip it. The attributes form is the
1708
+ * constant-callback case (a callback ignoring the record and always
1709
+ * returning the same attributes), which is how the two forms share this
1710
+ * implementation. (`update` hands the user's own callback a clone of each
1711
+ * claimed record, so only the returned attributes reach the write below —
1712
+ * see the dispatch in {@link Query.update}.)
1713
+ *
1714
+ * The count is the number of claimed records the callback returned
1715
+ * attributes for: records attempted but not claimed (lost races) never
1716
+ * reach the callback, and claimed records the callback skipped are not
1717
+ * counted. For the attributes form the callback is total, so the count
1718
+ * degenerates to records claimed and submitted for writing. (A record whose
1719
+ * write turns out to be a no-op still counts: the instance path returns
1720
+ * without issuing SQL when nothing is dirty.)
1721
+ *
1722
+ * Each claimed record is written via the ordinary per-instance update, so
1723
+ * hooks (unless `skipHooks`), custom setters, and virtual/encrypted
1724
+ * attribute preprocessing behave exactly as in a plain instance update.
1725
+ *
1726
+ * No join-load guard is needed here: update's own joins guards have already
1727
+ * thrown on every join-load query (leftJoinPreload always carries left-join
1728
+ * statements).
1729
+ */
1730
+ private lockedUpdate;
1455
1731
  private updateWithoutCallingModelHooks;
1456
1732
  private invertOrder;
1457
1733
  }
@@ -0,0 +1,3 @@
1
+ export default class BatchingIncompatibleWithLimitOrOffset extends Error {
2
+ get message(): string;
3
+ }