@rvoh/dream 2.24.0 → 2.25.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 (205) hide show
  1. package/dist/cjs/src/cli/index.js +29 -9
  2. package/dist/cjs/src/dream/Query.js +247 -67
  3. package/dist/cjs/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
  4. package/dist/cjs/src/errors/InvalidMigrationTableName.js +30 -0
  5. package/dist/cjs/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
  6. package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +35 -3
  7. package/dist/cjs/src/helpers/cli/generateMigration.js +33 -3
  8. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +3 -3
  9. package/dist/esm/src/cli/index.js +29 -9
  10. package/dist/esm/src/dream/Query.js +247 -67
  11. package/dist/esm/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
  12. package/dist/esm/src/errors/InvalidMigrationTableName.js +30 -0
  13. package/dist/esm/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
  14. package/dist/esm/src/errors/NoColumnsToAlterMigration.js +35 -3
  15. package/dist/esm/src/helpers/cli/generateMigration.js +33 -3
  16. package/dist/esm/src/helpers/cli/generateMigrationContent.js +3 -3
  17. package/dist/types/src/cli/index.d.ts +8 -0
  18. package/dist/types/src/dream/Query.d.ts +316 -40
  19. package/dist/types/src/errors/BatchingIncompatibleWithLimitOrOffset.d.ts +3 -0
  20. package/dist/types/src/errors/InvalidMigrationTableName.d.ts +10 -0
  21. package/dist/types/src/errors/MissingRequiredLockOptionForUpdateCallback.d.ts +3 -0
  22. package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +11 -1
  23. package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +18 -4
  24. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  25. package/docs/classes/db.KyselyQueryDriver.html +40 -40
  26. package/docs/classes/db.PostgresQueryDriver.html +41 -41
  27. package/docs/classes/db.QueryDriverBase.html +39 -39
  28. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  29. package/docs/classes/errors.ColumnOverflow.html +3 -3
  30. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  31. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  32. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  33. package/docs/classes/errors.DecryptionError.html +2 -2
  34. package/docs/classes/errors.DecryptionParseError.html +2 -2
  35. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  36. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  37. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  38. package/docs/classes/errors.InvalidClockTime.html +2 -2
  39. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  40. package/docs/classes/errors.InvalidDateTime.html +2 -2
  41. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  42. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  43. package/docs/classes/errors.NotNullViolation.html +3 -3
  44. package/docs/classes/errors.RecordNotFound.html +3 -3
  45. package/docs/classes/errors.ValidationError.html +3 -3
  46. package/docs/classes/index.CalendarDate.html +33 -33
  47. package/docs/classes/index.ClockTime.html +32 -32
  48. package/docs/classes/index.ClockTimeTz.html +35 -35
  49. package/docs/classes/index.DateTime.html +86 -86
  50. package/docs/classes/index.Decorators.html +19 -19
  51. package/docs/classes/index.Dream.html +124 -124
  52. package/docs/classes/index.DreamApp.html +10 -10
  53. package/docs/classes/index.DreamTransaction.html +2 -2
  54. package/docs/classes/index.Env.html +2 -2
  55. package/docs/classes/index.Query.html +735 -73
  56. package/docs/classes/system.CliFileWriter.html +4 -4
  57. package/docs/classes/system.DreamBin.html +2 -2
  58. package/docs/classes/system.DreamCLI.html +7 -7
  59. package/docs/classes/system.DreamImporter.html +2 -2
  60. package/docs/classes/system.DreamLogos.html +2 -2
  61. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  62. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  63. package/docs/classes/system.PathHelpers.html +3 -3
  64. package/docs/classes/utils.Encrypt.html +3 -3
  65. package/docs/classes/utils.Range.html +2 -2
  66. package/docs/functions/db.closeAllDbConnections.html +1 -1
  67. package/docs/functions/db.dreamDbConnections.html +1 -1
  68. package/docs/functions/db.untypedDb.html +1 -1
  69. package/docs/functions/db.validateColumn.html +1 -1
  70. package/docs/functions/db.validateTable.html +1 -1
  71. package/docs/functions/errors.pgErrorType.html +1 -1
  72. package/docs/functions/index.DreamSerializer.html +1 -1
  73. package/docs/functions/index.ObjectSerializer.html +1 -1
  74. package/docs/functions/index.ReplicaSafe.html +1 -1
  75. package/docs/functions/index.STI.html +1 -1
  76. package/docs/functions/index.SoftDelete.html +1 -1
  77. package/docs/functions/utils.camelize.html +1 -1
  78. package/docs/functions/utils.capitalize.html +1 -1
  79. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  80. package/docs/functions/utils.compact.html +1 -1
  81. package/docs/functions/utils.groupBy.html +1 -1
  82. package/docs/functions/utils.hyphenize.html +1 -1
  83. package/docs/functions/utils.intersection.html +1 -1
  84. package/docs/functions/utils.isEmpty.html +1 -1
  85. package/docs/functions/utils.normalizeUnicode.html +1 -1
  86. package/docs/functions/utils.pascalize.html +1 -1
  87. package/docs/functions/utils.percent.html +1 -1
  88. package/docs/functions/utils.range.html +1 -1
  89. package/docs/functions/utils.round.html +1 -1
  90. package/docs/functions/utils.sanitizeString.html +1 -1
  91. package/docs/functions/utils.snakeify.html +1 -1
  92. package/docs/functions/utils.sort.html +1 -1
  93. package/docs/functions/utils.sortBy.html +1 -1
  94. package/docs/functions/utils.sortObjectByKey.html +1 -1
  95. package/docs/functions/utils.sortObjectByValue.html +1 -1
  96. package/docs/functions/utils.uncapitalize.html +1 -1
  97. package/docs/functions/utils.uniq.html +1 -1
  98. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  99. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  100. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  101. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  102. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  103. package/docs/interfaces/types.DecoratorContext.html +2 -2
  104. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  105. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  106. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  107. package/docs/interfaces/types.DurationObject.html +2 -2
  108. package/docs/interfaces/types.EncryptOptions.html +2 -2
  109. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  110. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  111. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  112. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  113. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  114. package/docs/types/openapi.OpenapiFormats.html +1 -1
  115. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  116. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  117. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  118. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  120. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  121. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  122. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  123. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  124. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  125. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  126. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  127. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  128. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  129. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  131. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  139. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  140. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  142. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  144. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  145. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  146. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  147. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  148. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  150. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  151. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  152. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  153. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  154. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  155. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  156. package/docs/types/types.CalendarDateObject.html +1 -1
  157. package/docs/types/types.Camelized.html +1 -1
  158. package/docs/types/types.ClockTimeObject.html +1 -1
  159. package/docs/types/types.DbConnectionType.html +1 -1
  160. package/docs/types/types.DbTypes.html +1 -1
  161. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  162. package/docs/types/types.DreamAttributes.html +1 -1
  163. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  164. package/docs/types/types.DreamClassColumn.html +1 -1
  165. package/docs/types/types.DreamColumn.html +1 -1
  166. package/docs/types/types.DreamColumnNames.html +1 -1
  167. package/docs/types/types.DreamLogLevel.html +1 -1
  168. package/docs/types/types.DreamLogger.html +2 -2
  169. package/docs/types/types.DreamModelSerializerType.html +1 -1
  170. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  171. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  172. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  173. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  174. package/docs/types/types.DreamSerializable.html +1 -1
  175. package/docs/types/types.DreamSerializableArray.html +1 -1
  176. package/docs/types/types.DreamSerializerKey.html +1 -1
  177. package/docs/types/types.DreamSerializers.html +1 -1
  178. package/docs/types/types.DreamVirtualColumns.html +1 -1
  179. package/docs/types/types.DurationUnit.html +1 -1
  180. package/docs/types/types.EncryptAlgorithm.html +1 -1
  181. package/docs/types/types.HasManyStatement.html +1 -1
  182. package/docs/types/types.HasOneStatement.html +1 -1
  183. package/docs/types/types.Hyphenized.html +1 -1
  184. package/docs/types/types.Pascalized.html +1 -1
  185. package/docs/types/types.PrimaryKeyType.html +1 -1
  186. package/docs/types/types.RoundingPrecision.html +1 -1
  187. package/docs/types/types.SerializerCasing.html +1 -1
  188. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  189. package/docs/types/types.Snakeified.html +1 -1
  190. package/docs/types/types.StrictInterface.html +1 -1
  191. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  192. package/docs/types/types.UpdateableProperties.html +1 -1
  193. package/docs/types/types.ValidationType.html +1 -1
  194. package/docs/types/types.ViewModel.html +2 -2
  195. package/docs/types/types.ViewModelClass.html +1 -1
  196. package/docs/types/types.WeekdayName.html +1 -1
  197. package/docs/types/types.WhereStatementForDream.html +1 -1
  198. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  199. package/docs/variables/index.DreamConst.html +1 -1
  200. package/docs/variables/index.ops.html +1 -1
  201. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  202. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  203. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  204. package/docs/variables/system.primaryKeyTypes.html +1 -1
  205. package/package.json +1 -1
@@ -3,10 +3,12 @@ import DreamApp from '../dream-app/index.js';
3
3
  import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
4
4
  import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
5
5
  import CannotPassAdditionalFieldsToPluckEachAfterCallback from '../errors/CannotPassAdditionalFieldsToPluckEachAfterCallback.js';
6
+ import BatchingIncompatibleWithLimitOrOffset from '../errors/BatchingIncompatibleWithLimitOrOffset.js';
6
7
  import InvalidBatchSize from '../errors/InvalidBatchSize.js';
7
8
  import LeftJoinPreloadIncompatibleWithFindEach from '../errors/LeftJoinPreloadIncompatibleWithFindEach.js';
8
9
  import RowLockIncompatibleWithDistinct from '../errors/RowLockIncompatibleWithDistinct.js';
9
10
  import MissingRequiredCallbackFunctionToPluckEach from '../errors/MissingRequiredCallbackFunctionToPluckEach.js';
11
+ import MissingRequiredLockOptionForUpdateCallback from '../errors/MissingRequiredLockOptionForUpdateCallback.js';
10
12
  import NoUpdateAllOnJoins from '../errors/NoUpdateAllOnJoins.js';
11
13
  import NoUpdateOnAssociationQuery from '../errors/NoUpdateOnAssociationQuery.js';
12
14
  import CannotPaginateWithLeftJoinPreload from '../errors/pagination/CannotPaginateWithLeftJoinPreload.js';
@@ -51,10 +53,12 @@ export default class Query {
51
53
  */
52
54
  static BATCH_SIZES = {
53
55
  FIND_EACH: 1000,
54
- // deliberately small: every record in a locked destroy batch stays
55
- // row-locked for the whole batch, so an oversized batch blocks unrelated
56
- // writers. See Query#destroy's `lock` option.
56
+ // deliberately small: every record in a locked destroy or update batch
57
+ // stays row-locked for the whole batch, so an oversized batch blocks
58
+ // unrelated writers. See the `lock` options on Query#destroy and
59
+ // Query#update.
57
60
  LOCKED_DESTROY: 10,
61
+ LOCKED_UPDATE: 10,
58
62
  PLUCK_EACH: 10000,
59
63
  PLUCK_EACH_THROUGH: 1000,
60
64
  };
@@ -446,26 +450,40 @@ export default class Query {
446
450
  * If you need records processed in a particular order, load them with a
447
451
  * regular ordered query instead of `findEach`.
448
452
  *
453
+ * A `limit` or `offset` on the Query cannot compose with the batch windows
454
+ * — each batch re-applies the Query's conditions, so the limit would be
455
+ * silently replaced by `batchSize` and the offset re-applied to every
456
+ * window — so a limit- or offset-carrying Query throws at runtime.
457
+ *
449
458
  * @param cb - The callback to call for each found record
450
459
  * @param __namedParameters - Options for batch processing
451
460
  * @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
452
461
  * @returns void
462
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
453
463
  */
454
464
  async findEach(cb, { batchSize = Query.BATCH_SIZES.FIND_EACH } = {}) {
455
465
  if (this.joinLoadActivated)
456
466
  throw new LeftJoinPreloadIncompatibleWithFindEach();
467
+ // a limit or offset cannot compose with the batch windows: the window
468
+ // queries would re-apply it per batch — the limit silently replaced by
469
+ // batchSize, the offset skipping rows inside every window
470
+ if (this.limitStatement || this.offsetStatement)
471
+ throw new BatchingIncompatibleWithLimitOrOffset();
457
472
  let records;
458
473
  const query = this.order(null)
459
474
  .order(this.namespacedPrimaryKey)
460
475
  .limit(batchSize);
461
- let lastId = null;
476
+ // the cursor is compared against a sentinel rather than tested for
477
+ // truthiness, since a primary key of 0 is a legitimate cursor value that
478
+ // would otherwise reset the window to the start of the set
479
+ let lastId = undefined;
462
480
  do {
463
- if (lastId)
481
+ if (lastId === undefined)
482
+ records = await query.all();
483
+ else
464
484
  records = await query
465
485
  .where({ [this.dreamInstance['_primaryKey']]: ops.greaterThan(lastId) })
466
486
  .all();
467
- else
468
- records = await query.all();
469
487
  for (const record of records) {
470
488
  await cb(record);
471
489
  }
@@ -946,6 +964,12 @@ export default class Query {
946
964
  * // [User{id: 1}, User{id: 2}]
947
965
  * ```
948
966
  *
967
+ * A limit is incompatible with `paginate` and `leftJoinPreload`, and with
968
+ * the batched and set-write operations — `findEach`, `pluckEach`,
969
+ * `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can
970
+ * honor one: the batched and set-write operations throw
971
+ * `BatchingIncompatibleWithLimitOrOffset` on a limit-carrying Query.
972
+ *
949
973
  * @returns A cloned Query with the limit clause applied
950
974
  */
951
975
  limit(limit) {
@@ -959,6 +983,12 @@ export default class Query {
959
983
  * // [User{id: 3}, User{id: 4}]
960
984
  * ```
961
985
  *
986
+ * An offset is incompatible with `paginate` and `leftJoinPreload`, and with
987
+ * the batched and set-write operations — `findEach`, `pluckEach`,
988
+ * `destroy`, `reallyDestroy`, `undestroy`, and `update` — none of which can
989
+ * honor one: the batched and set-write operations throw
990
+ * `BatchingIncompatibleWithLimitOrOffset` on an offset-carrying Query.
991
+ *
962
992
  * @returns A cloned Query with the offset clause applied
963
993
  */
964
994
  offset(offset) {
@@ -1322,8 +1352,14 @@ export default class Query {
1322
1352
  * // 3
1323
1353
  * ```
1324
1354
  *
1355
+ * A `limit` or `offset` on the Query cannot compose with the batch windows
1356
+ * — each window replaces the limit with the batch size and the offset with
1357
+ * the running window offset, silently ignoring both — so a limit- or
1358
+ * offset-carrying Query throws at runtime.
1359
+ *
1325
1360
  * @param args - a list of column names to pluck, followed by a callback function to call for each set of found fields
1326
1361
  * @returns void
1362
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
1327
1363
  */
1328
1364
  async pluckEach(...args) {
1329
1365
  const providedCbIndex = args.findIndex(v => typeof v === 'function');
@@ -1333,6 +1369,11 @@ export default class Query {
1333
1369
  throw new MissingRequiredCallbackFunctionToPluckEach('pluckEach', args);
1334
1370
  if (providedOpts !== undefined && !providedOpts?.batchSize)
1335
1371
  throw new CannotPassAdditionalFieldsToPluckEachAfterCallback('pluckEach', args);
1372
+ // a limit or offset cannot compose with the batch windows: the window
1373
+ // queries would replace the limit with batchSize and the offset with the
1374
+ // running window offset, silently ignoring both
1375
+ if (this.limitStatement || this.offsetStatement)
1376
+ throw new BatchingIncompatibleWithLimitOrOffset();
1336
1377
  const onlyColumns = args.filter((_, index) => index < providedCbIndex);
1337
1378
  const batchSize = providedOpts?.batchSize || Query.BATCH_SIZES.PLUCK_EACH_THROUGH;
1338
1379
  let offset = 0;
@@ -1865,20 +1906,27 @@ export default class Query {
1865
1906
  * scale, ties up connections. If that is a risk in your application, set a
1866
1907
  * `lock_timeout` on the connection so a stuck batch fails instead of
1867
1908
  * hanging.
1909
+ * - **A `limit` or `offset` on the Query is incompatible with `destroy` on
1910
+ * every path.** The batches — locked and unlocked alike — re-apply the
1911
+ * Query's conditions to each window, where a limit or offset would skip
1912
+ * or truncate rows inside every window instead of bounding the run as a
1913
+ * whole, so `destroy` throws rather than silently corrupting the
1914
+ * windows.
1868
1915
  * - **If you are destroying a large set and do not need compare-and-set, do
1869
1916
  * not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
1870
1917
  * {@link Query.delete | delete} (a single statement, no hooks or cascade)
1871
1918
  * are the right tools for bulk removal.
1872
1919
  *
1873
- * @param __namedParameters - Options for destroying the instance
1874
- * @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
1875
- * @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
1876
- * @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
1877
- * @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
1878
- * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1879
- * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1920
+ * @param options - Options for destroying the instance
1921
+ * @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
1922
+ * @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
1923
+ * @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
1924
+ * @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
1925
+ * @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
1926
+ * @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
1880
1927
  * @returns The number of records that were removed
1881
1928
  * @throws InvalidBatchSize if `batchSize` is not a positive integer
1929
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
1882
1930
  */
1883
1931
  async destroy({ skipHooks, cascade, lock, batchSize, } = {}) {
1884
1932
  let counter = 0;
@@ -1917,43 +1965,77 @@ export default class Query {
1917
1965
  /**
1918
1966
  * @internal
1919
1967
  *
1920
- * The compare-and-set implementation behind `destroy({ lock: true })`.
1968
+ * The compare-and-set implementation behind `destroy({ lock: true })`,
1969
+ * driven by {@link Query.lockedBatches}, which owns the keyset cursor, the
1970
+ * pluck/claim two-step, and the transaction dispatch.
1971
+ *
1972
+ * Each claimed record is destroyed via the ordinary per-instance path, and
1973
+ * every claimed record counts. (One thing the count does not account for,
1974
+ * because it never has: a `beforeDestroy` hook that sets `_preventDeletion`
1975
+ * leaves the record in place but still counts.)
1976
+ */
1977
+ async lockedDestroy(options, batchSize) {
1978
+ if (this.joinLoadActivated)
1979
+ throw new LeftJoinPreloadIncompatibleWithFindEach();
1980
+ return await this.lockedBatches(batchSize, async (claimed, txn) => {
1981
+ for (const record of claimed) {
1982
+ const inTransaction = record.txn(txn);
1983
+ if (this.shouldReallyDestroy) {
1984
+ await inTransaction.reallyDestroy(options);
1985
+ }
1986
+ else {
1987
+ await inTransaction.destroy(options);
1988
+ }
1989
+ }
1990
+ return claimed.length;
1991
+ });
1992
+ }
1993
+ /**
1994
+ * @internal
1995
+ *
1996
+ * The shared batch driver behind `destroy({ lock: true })` and
1997
+ * `update(..., { lock: true })`: the keyset cursor, the pluck/claim
1998
+ * two-step, and the transaction dispatch live here, so the locked
1999
+ * operations cannot drift apart.
1921
2000
  *
1922
2001
  * Each batch is handled in two steps inside one transaction:
1923
2002
  *
1924
2003
  * 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
1925
2004
  * primary key order, which establishes the batch's window (its keyset
1926
- * cursor) and tells us how many records were *attempted*. Only the keys are
1927
- * read: hydrating and preloading these records would duplicate the work
1928
- * step 2 does over the same rows; then
2005
+ * cursor) and tells us how many records were *attempted*. Only the keys
2006
+ * are read: hydrating and preloading these records would duplicate the
2007
+ * work step 2 does over the same rows; then
1929
2008
  * 2. a locked re-read restricted to exactly those primary keys, which
1930
- * re-applies every condition on the Query while acquiring an exclusive row
1931
- * lock on each row it returns. Records a concurrent transaction has moved
1932
- * out of the Query since step 1 do not come back, and are the records this
1933
- * run lost the race for.
1934
- *
1935
- * The count comes from step 2's result set, which is why it reports records
1936
- * actually claimed rather than records attempted. (One thing it does not
1937
- * account for, because it never has: a `beforeDestroy` hook that sets
1938
- * `_preventDeletion` leaves the record in place but still counts.)
1939
- *
1940
- * The two-step shape is what keeps iteration correct. If the locked read both
1941
- * claimed rows and advanced the cursor, a batch whose records all lost the
1942
- * race would come back empty and end the run early, silently skipping every
1943
- * record after it. Advancing the cursor from step 1 instead means losing a
1944
- * race skips only the records that were lost.
2009
+ * re-applies every condition on the Query while acquiring an exclusive
2010
+ * row lock on each row it returns. Records a concurrent transaction has
2011
+ * moved out of the Query since step 1 do not come back, and are the
2012
+ * records this run lost the race for.
2013
+ *
2014
+ * The claimed records are handed to `perBatch` inside the batch's
2015
+ * transaction; its return value — how many of the claimed records were
2016
+ * processed — accumulates into the returned count.
2017
+ *
2018
+ * The two-step shape is what keeps iteration correct. If the locked read
2019
+ * both claimed rows and advanced the cursor, a batch whose records all lost
2020
+ * the race would come back empty and end the run early, silently skipping
2021
+ * every record after it. Advancing the cursor from step 1 instead means
2022
+ * losing a race skips only the records that were lost.
1945
2023
  *
1946
2024
  * When the Query already carries a transaction, that transaction is used for
1947
2025
  * every batch rather than opening one per batch, so the caller's transaction
1948
2026
  * boundary wins and the locks are held until the caller commits.
1949
2027
  */
1950
- async lockedDestroy(options, batchSize) {
1951
- if (this.joinLoadActivated)
1952
- throw new LeftJoinPreloadIncompatibleWithFindEach();
2028
+ async lockedBatches(batchSize, perBatch) {
1953
2029
  // the driver seam refuses this combination too, but catching it here means
1954
2030
  // failing before any transaction is opened or any row is touched
1955
2031
  if (this.distinctColumn)
1956
2032
  throw new RowLockIncompatibleWithDistinct();
2033
+ // a limit or offset cannot compose with the batch windows: the window
2034
+ // queries would re-apply it per batch — the offset skipping rows in the
2035
+ // candidate pluck and discarding already-claimed rows from the locked
2036
+ // re-read, the limit silently replaced by batchSize
2037
+ if (this.limitStatement || this.offsetStatement)
2038
+ throw new BatchingIncompatibleWithLimitOrOffset();
1957
2039
  const primaryKey = this.dreamInstance['_primaryKey'];
1958
2040
  const orderedQuery = this.order(null)
1959
2041
  .order(this.namespacedPrimaryKey)
@@ -1968,30 +2050,21 @@ export default class Query {
1968
2050
  const batchQuery = lastId === undefined
1969
2051
  ? orderedQuery
1970
2052
  : orderedQuery.where({ [primaryKey]: ops.greaterThan(lastId) });
1971
- const destroyBatch = async (txn) => {
2053
+ const processBatch = async (txn) => {
1972
2054
  const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
1973
2055
  if (!candidateIds.length)
1974
- return { attempted: 0, claimed: 0, lastId };
2056
+ return { attempted: 0, processed: 0, lastId };
1975
2057
  const claimed = await this.dbDriverInstance(batchQuery.txn(txn).where({ [primaryKey]: candidateIds })).takeAll({ lock: true });
1976
- for (const record of claimed) {
1977
- const inTransaction = record.txn(txn);
1978
- if (this.shouldReallyDestroy) {
1979
- await inTransaction.reallyDestroy(options);
1980
- }
1981
- else {
1982
- await inTransaction.destroy(options);
1983
- }
1984
- }
1985
2058
  return {
1986
2059
  attempted: candidateIds.length,
1987
- claimed: claimed.length,
2060
+ processed: await perBatch(claimed, txn),
1988
2061
  lastId: candidateIds.at(-1),
1989
2062
  };
1990
2063
  };
1991
2064
  const results = this.dreamTransaction
1992
- ? await destroyBatch(this.dreamTransaction)
1993
- : await this.dreamClass.transaction(async (txn) => await destroyBatch(txn));
1994
- counter += results.claimed;
2065
+ ? await processBatch(this.dreamTransaction)
2066
+ : await this.dreamClass.transaction(async (txn) => await processBatch(txn));
2067
+ counter += results.processed;
1995
2068
  attemptedCount = results.attempted;
1996
2069
  lastId = results.lastId;
1997
2070
  } while (attemptedCount > 0 && attemptedCount === batchSize);
@@ -2015,15 +2088,20 @@ export default class Query {
2015
2088
  * // 12
2016
2089
  * ```
2017
2090
  *
2018
- * @param __namedParameters - Options for destroying the instance
2019
- * @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
2020
- * @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
2021
- * @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
2022
- * @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
2023
- * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
2024
- * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
2091
+ * A `limit` or `offset` on the Query is incompatible with `reallyDestroy`,
2092
+ * exactly as it is with {@link Query.destroy}: a limit- or offset-carrying
2093
+ * Query throws at runtime.
2094
+ *
2095
+ * @param options - Options for destroying the instance
2096
+ * @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
2097
+ * @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
2098
+ * @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
2099
+ * @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
2100
+ * @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
2101
+ * @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
2025
2102
  * @returns The number of records that were removed
2026
2103
  * @throws InvalidBatchSize if `batchSize` is not a positive integer
2104
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
2027
2105
  */
2028
2106
  async reallyDestroy({ skipHooks, cascade, lock, batchSize, } = {}) {
2029
2107
  return await this.clone({ shouldReallyDestroy: true }).destroy({ skipHooks, cascade, lock, batchSize });
@@ -2040,12 +2118,19 @@ export default class Query {
2040
2118
  * // 12
2041
2119
  * ```
2042
2120
  *
2043
- * @param __namedParameters - Options for undestroying the instance
2044
- * @param __namedParameters.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false
2045
- * @param __namedParameters.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true
2046
- * @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade undestroying. Defaults to false
2047
- * @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade undestroying (soft delete is always bypassed). Defaults to an empty array
2121
+ * A `limit` or `offset` on the Query cannot compose with the batch windows
2122
+ * `undestroy` iterates in — each batch re-applies the Query's conditions,
2123
+ * so the limit would be silently replaced by the batch size and the offset
2124
+ * re-applied to every window — so a limit- or offset-carrying Query throws
2125
+ * at runtime.
2126
+ *
2127
+ * @param options - Options for undestroying the instance
2128
+ * @param options.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false
2129
+ * @param options.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true
2130
+ * @param options.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade undestroying. Defaults to false
2131
+ * @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade undestroying (soft delete is always bypassed). Defaults to an empty array
2048
2132
  * @returns The number of records that were removed
2133
+ * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
2049
2134
  */
2050
2135
  async undestroy({ cascade, skipHooks, } = {}) {
2051
2136
  if (!this.dreamClass['softDelete'])
@@ -2084,25 +2169,120 @@ export default class Query {
2084
2169
  async delete() {
2085
2170
  return await this.dbDriverInstance().delete();
2086
2171
  }
2087
- async update(attributes, { skipHooks } = {}) {
2172
+ async update(attributesOrCb, { skipHooks, lock, batchSize } = {}) {
2088
2173
  if (this.baseSelectQuery)
2089
2174
  throw new NoUpdateOnAssociationQuery();
2090
2175
  if (Object.keys(this.innerJoinStatements).length)
2091
2176
  throw new NoUpdateAllOnJoins();
2092
2177
  if (Object.keys(this.leftJoinStatements).length)
2093
2178
  throw new NoUpdateAllOnJoins();
2094
- if (skipHooks)
2095
- return await this.updateWithoutCallingModelHooks(attributes);
2179
+ const cb = typeof attributesOrCb === 'function' ? attributesOrCb : undefined;
2180
+ // the overloads already demand an options object with a boolean `lock`
2181
+ // whenever a callback is passed; this backstops callers outside the type
2182
+ // system, so a callback can never silently take (or skip) row locks the
2183
+ // call site did not spell out
2184
+ if (cb && typeof lock !== 'boolean')
2185
+ throw new MissingRequiredLockOptionForUpdateCallback();
2186
+ // resolve the default before validating: `??` would let an explicit 0
2187
+ // through, and `limit(0)` means "no limit", which would turn a batched
2188
+ // update into a single unbounded pass (and, with `lock`, a lock over the
2189
+ // entire matched set)
2190
+ const resolvedBatchSize = batchSize ?? (lock ? Query.BATCH_SIZES.LOCKED_UPDATE : Query.BATCH_SIZES.FIND_EACH);
2191
+ if (!Number.isInteger(resolvedBatchSize) || resolvedBatchSize < 1)
2192
+ throw new InvalidBatchSize(batchSize);
2193
+ // a limit or offset cannot compose with any update path: the batched
2194
+ // paths (findEach and lockedBatches, which also guard) would re-apply it
2195
+ // to every batch window, silently skipping or truncating rows within each
2196
+ // window rather than bounding the run, and the single-statement skipHooks
2197
+ // path's SQL UPDATE cannot express it, so it would be silently ignored
2198
+ if (this.limitStatement || this.offsetStatement)
2199
+ throw new BatchingIncompatibleWithLimitOrOffset();
2200
+ if (lock) {
2201
+ // the attributes form never consumes the records it claims, so its
2202
+ // preloads would be pure wasted queries run while every row in the
2203
+ // batch is exclusively locked; the callback form keeps them, since the
2204
+ // callback may consume the preloaded associations
2205
+ const baseQuery = cb ? this : this.clone({ preloadStatements: {}, preloadOnStatements: {} });
2206
+ return await baseQuery.lockedUpdate(cb
2207
+ ? // the callback receives a clone of the claimed record, so that only
2208
+ // the attributes it *returns* are written: the per-instance write is
2209
+ // dirty-state-based, and mutations made directly on the record
2210
+ // Dream holds would otherwise ride along with the save
2211
+ (record) => cb(record['clone']())
2212
+ : () => attributesOrCb, { skipHooks }, resolvedBatchSize);
2213
+ }
2214
+ if (!cb && skipHooks)
2215
+ return await this.updateWithoutCallingModelHooks(attributesOrCb);
2096
2216
  let counter = 0;
2097
2217
  await this.findEach(async (result) => {
2218
+ // as on the locked path, the callback receives a clone, so only the
2219
+ // attributes it returns are written
2220
+ const attributes = cb
2221
+ ? await cb(result['clone']())
2222
+ : attributesOrCb;
2223
+ // null is not part of the callback's advertised contract, but it is
2224
+ // the natural untyped-JS skip idiom; treat it as a skip rather than
2225
+ // letting it crash the per-instance write mid-run
2226
+ if (attributes == null)
2227
+ return;
2098
2228
  const subquery = this.dreamTransaction
2099
2229
  ? result.txn(this.dreamTransaction)
2100
2230
  : result;
2101
- await subquery.update(attributes);
2231
+ await subquery.update(attributes, skipHooks ? { skipHooks } : undefined);
2102
2232
  counter++;
2233
+ }, {
2234
+ batchSize: resolvedBatchSize,
2103
2235
  });
2104
2236
  return counter;
2105
2237
  }
2238
+ /**
2239
+ * @internal
2240
+ *
2241
+ * The compare-and-set implementation behind `update(attributes, { lock: true })`,
2242
+ * driven by {@link Query.lockedBatches}, which owns the keyset cursor, the
2243
+ * pluck/claim two-step, and the transaction dispatch.
2244
+ *
2245
+ * Each claimed record is handed to `cb`, which returns the attributes to
2246
+ * write for it, or a nullish value to skip it. The attributes form is the
2247
+ * constant-callback case (a callback ignoring the record and always
2248
+ * returning the same attributes), which is how the two forms share this
2249
+ * implementation. (`update` hands the user's own callback a clone of each
2250
+ * claimed record, so only the returned attributes reach the write below —
2251
+ * see the dispatch in {@link Query.update}.)
2252
+ *
2253
+ * The count is the number of claimed records the callback returned
2254
+ * attributes for: records attempted but not claimed (lost races) never
2255
+ * reach the callback, and claimed records the callback skipped are not
2256
+ * counted. For the attributes form the callback is total, so the count
2257
+ * degenerates to records claimed and submitted for writing. (A record whose
2258
+ * write turns out to be a no-op still counts: the instance path returns
2259
+ * without issuing SQL when nothing is dirty.)
2260
+ *
2261
+ * Each claimed record is written via the ordinary per-instance update, so
2262
+ * hooks (unless `skipHooks`), custom setters, and virtual/encrypted
2263
+ * attribute preprocessing behave exactly as in a plain instance update.
2264
+ *
2265
+ * No join-load guard is needed here: update's own joins guards have already
2266
+ * thrown on every join-load query (leftJoinPreload always carries left-join
2267
+ * statements).
2268
+ */
2269
+ async lockedUpdate(cb, { skipHooks }, batchSize) {
2270
+ return await this.lockedBatches(batchSize, async (claimed, txn) => {
2271
+ let written = 0;
2272
+ for (const record of claimed) {
2273
+ const attributes = await cb(record);
2274
+ // null is not part of the callback's advertised contract, but it is
2275
+ // the natural untyped-JS skip idiom; treat it as a skip rather than
2276
+ // letting it crash the per-instance write mid-batch
2277
+ if (attributes == null)
2278
+ continue;
2279
+ const inTransaction = record.txn(txn);
2280
+ await inTransaction.update(attributes, skipHooks ? { skipHooks } : undefined);
2281
+ written++;
2282
+ }
2283
+ return written;
2284
+ });
2285
+ }
2106
2286
  async updateWithoutCallingModelHooks(attributes) {
2107
2287
  return await this.dbDriverInstance().update(attributes);
2108
2288
  }
@@ -0,0 +1,25 @@
1
+ export default class BatchingIncompatibleWithLimitOrOffset extends Error {
2
+ get message() {
3
+ // batched iteration re-applies the Query's conditions to every batch
4
+ // window, so a limit or offset would be re-applied per batch — skipping or
5
+ // truncating rows inside each window — rather than bounding the run as a
6
+ // whole; and a single-statement set update has no way to express a limit
7
+ // or offset in SQL. Silently clearing them instead would touch rows the
8
+ // caller asked to scope out, so fail loudly before any row is touched.
9
+ return `
10
+ A batched or set-write operation was called on a query carrying a \`limit\`
11
+ or \`offset\`.
12
+
13
+ These operations process the entire matched set, so a limit or offset cannot
14
+ be honored: batched iteration re-applies the query's conditions to every
15
+ batch window, where the limit or offset would be re-applied per batch —
16
+ silently skipping or truncating rows within each window instead of bounding
17
+ the run as a whole — and a single-statement set update cannot express a
18
+ limit or offset in SQL at all.
19
+
20
+ fix:
21
+ remove the \`limit\`/\`offset\` from the query, and scope the affected rows
22
+ with \`where\` conditions instead
23
+ `;
24
+ }
25
+ }
@@ -0,0 +1,30 @@
1
+ export default class InvalidMigrationTableName extends Error {
2
+ migrationName;
3
+ table;
4
+ /**
5
+ * @param migrationName - the migration name the generator was invoked with
6
+ * @param table - the table name the generator resolved for it
7
+ */
8
+ constructor(migrationName, table) {
9
+ super();
10
+ this.migrationName = migrationName;
11
+ this.table = table;
12
+ }
13
+ get message() {
14
+ return `
15
+ generating the migration '${this.migrationName}' resolved the table name:
16
+ ${this.table}
17
+
18
+ which is not a valid table name. The table name is written into the generated
19
+ migration, which 'db:migrate' later executes, so it must be a plain
20
+ identifier: lowercase letters, digits and underscores only.
21
+
22
+ A standalone 'g:migration' takes the table from the text after the first
23
+ '-to-'/'-from-' in the name, snake-cased and pluralized, e.g.:
24
+ add-phone-to-users
25
+
26
+ A model generator takes it from the model name, or verbatim from
27
+ '--table-name'.
28
+ `;
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ export default class MissingRequiredLockOptionForUpdateCallback extends Error {
2
+ get message() {
3
+ return `
4
+ When passing a callback to Query#update, the options object and its \`lock\`
5
+ key are required, and \`lock\` must be an explicit boolean:
6
+
7
+ await User.query().update(user => ({ ... }), { lock: true })
8
+ await User.query().update(user => ({ ... }), { lock: false })
9
+
10
+ \`lock: true\` runs the callback on each record after re-selecting it under an
11
+ exclusive row lock (a compare-then-write). \`lock: false\` actively opts out of
12
+ locking: the callback runs on each record with no lock and no compare-and-set
13
+ guarantee. Requiring the boolean keeps that choice visible at the call site.
14
+ `;
15
+ }
16
+ }
@@ -1,17 +1,49 @@
1
1
  export default class NoColumnsToAlterMigration extends Error {
2
2
  table;
3
3
  alterDirection;
4
- constructor(table, alterDirection) {
4
+ migrationName;
5
+ marker;
6
+ /**
7
+ * @param table - the table the migration resolved to
8
+ * @param alterDirection - 'add' for a `-to-` migration, 'remove' for a `-from-` migration
9
+ * @param migrationName - the migration name the user typed, when available. Only
10
+ * the standalone `g:migration` flow knows it, so the message degrades to a
11
+ * table-only explanation when it is omitted.
12
+ * @param marker - the `-to-`/`-from-` marker that matched within `migrationName`
13
+ */
14
+ constructor(table, alterDirection, migrationName, marker) {
5
15
  super();
6
16
  this.table = table;
7
17
  this.alterDirection = alterDirection;
18
+ this.migrationName = migrationName;
19
+ this.marker = marker;
8
20
  }
9
21
  get message() {
10
- return `
11
- no valid columns to ${this.alterDirection === 'remove' ? 'drop from' : 'add to'} table '${this.table}' in this alter migration.
22
+ const verb = this.alterDirection === 'remove' ? 'drop from' : 'add to';
23
+ if (!this.migrationName || !this.marker) {
24
+ return `
25
+ no valid columns to ${verb} table '${this.table}' in this alter migration.
12
26
 
13
27
  Pass at least one column declaration with a resolvable type, e.g.:
14
28
  name:string
29
+ `;
30
+ }
31
+ return `
32
+ no valid columns to ${verb} table '${this.table}' in this alter migration.
33
+
34
+ The migration name '${this.migrationName}' contains '${this.marker}', which selected that
35
+ table and the '${this.alterDirection}' direction: everything after the marker's first
36
+ occurrence - wherever in the name that falls - is snake-cased and pluralized
37
+ into the table name. A '-to-' anywhere in the name wins, even when a '-from-'
38
+ appears earlier; '-from-' applies only when the name contains no '-to-' at all.
39
+
40
+ Either:
41
+
42
+ - pass at least one column declaration with a resolvable type, e.g.:
43
+ name:string
44
+
45
+ - or, if this was meant to be a hand-written migration, rename it so that
46
+ neither '-to-' nor '-from-' appears anywhere in the name.
15
47
  `;
16
48
  }
17
49
  }