@rvoh/dream 2.20.0 → 2.22.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 (241) hide show
  1. package/dist/cjs/src/Dream.js +3 -0
  2. package/dist/cjs/src/cli/index.js +7 -4
  3. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +3 -0
  4. package/dist/cjs/src/dream/Query.js +203 -4
  5. package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
  6. package/dist/cjs/src/dream/QueryDriver/Kysely.js +46 -4
  7. package/dist/cjs/src/dream/QueryDriver/Postgres.js +21 -0
  8. package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +76 -4
  9. package/dist/cjs/src/dream/internal/destroyDream.js +6 -7
  10. package/dist/cjs/src/errors/InvalidBatchSize.js +19 -0
  11. package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +17 -0
  12. package/dist/cjs/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
  13. package/dist/cjs/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
  14. package/dist/cjs/src/errors/UnparseableMigrationColumn.js +16 -0
  15. package/dist/cjs/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
  16. package/dist/cjs/src/helpers/cli/generateMigration.js +20 -3
  17. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +74 -9
  18. package/dist/cjs/src/ops/index.js +19 -0
  19. package/dist/esm/src/Dream.js +3 -0
  20. package/dist/esm/src/cli/index.js +7 -4
  21. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +3 -0
  22. package/dist/esm/src/dream/Query.js +203 -4
  23. package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
  24. package/dist/esm/src/dream/QueryDriver/Kysely.js +46 -4
  25. package/dist/esm/src/dream/QueryDriver/Postgres.js +21 -0
  26. package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +76 -4
  27. package/dist/esm/src/dream/internal/destroyDream.js +6 -7
  28. package/dist/esm/src/errors/InvalidBatchSize.js +19 -0
  29. package/dist/esm/src/errors/NoColumnsToAlterMigration.js +17 -0
  30. package/dist/esm/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
  31. package/dist/esm/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
  32. package/dist/esm/src/errors/UnparseableMigrationColumn.js +16 -0
  33. package/dist/esm/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
  34. package/dist/esm/src/helpers/cli/generateMigration.js +20 -3
  35. package/dist/esm/src/helpers/cli/generateMigrationContent.js +74 -9
  36. package/dist/esm/src/ops/index.js +19 -0
  37. package/dist/types/src/Dream.d.ts +3 -0
  38. package/dist/types/src/cli/index.d.ts +1 -1
  39. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -0
  40. package/dist/types/src/dream/Query.d.ts +140 -3
  41. package/dist/types/src/dream/QueryDriver/Base.d.ts +21 -0
  42. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
  43. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +20 -0
  44. package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +22 -3
  45. package/dist/types/src/errors/InvalidBatchSize.d.ts +5 -0
  46. package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +6 -0
  47. package/dist/types/src/errors/RowLockIncompatibleWithDistinct.d.ts +3 -0
  48. package/dist/types/src/errors/RowLockIncompatibleWithJoinLoad.d.ts +3 -0
  49. package/dist/types/src/errors/UnparseableMigrationColumn.d.ts +5 -0
  50. package/dist/types/src/errors/ops/ArrayEqualityRequiresArrayColumn.d.ts +8 -0
  51. package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +11 -1
  52. package/dist/types/src/ops/index.d.ts +19 -0
  53. package/dist/types/src/types/associations/shared.d.ts +2 -2
  54. package/dist/types/src/types/dream.d.ts +8 -0
  55. package/dist/types/src/types/types/associations/shared.ts +84 -55
  56. package/dist/types/src/types/types/dream.ts +30 -0
  57. package/docs/assets/hierarchy.js +1 -1
  58. package/docs/assets/search.js +1 -1
  59. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  60. package/docs/classes/db.KyselyQueryDriver.html +55 -41
  61. package/docs/classes/db.PostgresQueryDriver.html +55 -42
  62. package/docs/classes/db.QueryDriverBase.html +54 -40
  63. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  64. package/docs/classes/errors.ColumnOverflow.html +3 -3
  65. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  66. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  67. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  68. package/docs/classes/errors.DecryptionError.html +2 -2
  69. package/docs/classes/errors.DecryptionParseError.html +2 -2
  70. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  71. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  72. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  73. package/docs/classes/errors.InvalidClockTime.html +2 -2
  74. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  75. package/docs/classes/errors.InvalidDateTime.html +2 -2
  76. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  77. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  78. package/docs/classes/errors.NotNullViolation.html +3 -3
  79. package/docs/classes/errors.RecordNotFound.html +3 -3
  80. package/docs/classes/errors.ValidationError.html +3 -3
  81. package/docs/classes/index.CalendarDate.html +33 -33
  82. package/docs/classes/index.ClockTime.html +32 -32
  83. package/docs/classes/index.ClockTimeTz.html +35 -35
  84. package/docs/classes/index.DateTime.html +86 -86
  85. package/docs/classes/index.Decorators.html +19 -19
  86. package/docs/classes/index.Dream.html +127 -125
  87. package/docs/classes/index.DreamApp.html +10 -10
  88. package/docs/classes/index.DreamTransaction.html +2 -2
  89. package/docs/classes/index.Env.html +2 -2
  90. package/docs/classes/index.Query.html +161 -71
  91. package/docs/classes/system.CliFileWriter.html +4 -4
  92. package/docs/classes/system.DreamBin.html +2 -2
  93. package/docs/classes/system.DreamCLI.html +7 -7
  94. package/docs/classes/system.DreamImporter.html +2 -2
  95. package/docs/classes/system.DreamLogos.html +2 -2
  96. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  97. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  98. package/docs/classes/system.PathHelpers.html +3 -3
  99. package/docs/classes/utils.Encrypt.html +3 -3
  100. package/docs/classes/utils.Range.html +2 -2
  101. package/docs/functions/db.closeAllDbConnections.html +1 -1
  102. package/docs/functions/db.dreamDbConnections.html +1 -1
  103. package/docs/functions/db.untypedDb.html +1 -1
  104. package/docs/functions/db.validateColumn.html +1 -1
  105. package/docs/functions/db.validateTable.html +1 -1
  106. package/docs/functions/errors.pgErrorType.html +1 -1
  107. package/docs/functions/index.DreamSerializer.html +1 -1
  108. package/docs/functions/index.ObjectSerializer.html +1 -1
  109. package/docs/functions/index.ReplicaSafe.html +1 -1
  110. package/docs/functions/index.STI.html +1 -1
  111. package/docs/functions/index.SoftDelete.html +1 -1
  112. package/docs/functions/utils.camelize.html +1 -1
  113. package/docs/functions/utils.capitalize.html +1 -1
  114. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  115. package/docs/functions/utils.compact.html +1 -1
  116. package/docs/functions/utils.groupBy.html +1 -1
  117. package/docs/functions/utils.hyphenize.html +1 -1
  118. package/docs/functions/utils.intersection.html +1 -1
  119. package/docs/functions/utils.isEmpty.html +1 -1
  120. package/docs/functions/utils.normalizeUnicode.html +1 -1
  121. package/docs/functions/utils.pascalize.html +1 -1
  122. package/docs/functions/utils.percent.html +1 -1
  123. package/docs/functions/utils.range.html +1 -1
  124. package/docs/functions/utils.round.html +1 -1
  125. package/docs/functions/utils.sanitizeString.html +1 -1
  126. package/docs/functions/utils.snakeify.html +1 -1
  127. package/docs/functions/utils.sort.html +1 -1
  128. package/docs/functions/utils.sortBy.html +1 -1
  129. package/docs/functions/utils.sortObjectByKey.html +1 -1
  130. package/docs/functions/utils.sortObjectByValue.html +1 -1
  131. package/docs/functions/utils.uncapitalize.html +1 -1
  132. package/docs/functions/utils.uniq.html +1 -1
  133. package/docs/hierarchy.html +1 -1
  134. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  135. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  136. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  137. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  138. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  139. package/docs/interfaces/types.DecoratorContext.html +2 -2
  140. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  141. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  142. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  143. package/docs/interfaces/types.DurationObject.html +2 -2
  144. package/docs/interfaces/types.EncryptOptions.html +2 -2
  145. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  146. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  147. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  148. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  149. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  150. package/docs/types/openapi.OpenapiFormats.html +1 -1
  151. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  152. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  153. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  154. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  155. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  156. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  157. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  158. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  159. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  160. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  161. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  162. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  163. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  164. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  165. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  166. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  167. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  168. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  169. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  170. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  171. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  180. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  181. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  182. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  183. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  184. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  185. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  186. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  187. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  188. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  189. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  190. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  191. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  192. package/docs/types/types.CalendarDateObject.html +1 -1
  193. package/docs/types/types.Camelized.html +1 -1
  194. package/docs/types/types.ClockTimeObject.html +1 -1
  195. package/docs/types/types.DbConnectionType.html +1 -1
  196. package/docs/types/types.DbTypes.html +1 -1
  197. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  198. package/docs/types/types.DreamAttributes.html +1 -1
  199. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  200. package/docs/types/types.DreamClassColumn.html +1 -1
  201. package/docs/types/types.DreamColumn.html +1 -1
  202. package/docs/types/types.DreamColumnNames.html +1 -1
  203. package/docs/types/types.DreamLogLevel.html +1 -1
  204. package/docs/types/types.DreamLogger.html +2 -2
  205. package/docs/types/types.DreamModelSerializerType.html +1 -1
  206. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  207. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  208. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  209. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  210. package/docs/types/types.DreamSerializable.html +1 -1
  211. package/docs/types/types.DreamSerializableArray.html +1 -1
  212. package/docs/types/types.DreamSerializerKey.html +1 -1
  213. package/docs/types/types.DreamSerializers.html +1 -1
  214. package/docs/types/types.DreamVirtualColumns.html +1 -1
  215. package/docs/types/types.DurationUnit.html +1 -1
  216. package/docs/types/types.EncryptAlgorithm.html +1 -1
  217. package/docs/types/types.HasManyStatement.html +1 -1
  218. package/docs/types/types.HasOneStatement.html +1 -1
  219. package/docs/types/types.Hyphenized.html +1 -1
  220. package/docs/types/types.Pascalized.html +1 -1
  221. package/docs/types/types.PrimaryKeyType.html +1 -1
  222. package/docs/types/types.RoundingPrecision.html +1 -1
  223. package/docs/types/types.SerializerCasing.html +1 -1
  224. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  225. package/docs/types/types.Snakeified.html +1 -1
  226. package/docs/types/types.StrictInterface.html +1 -1
  227. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  228. package/docs/types/types.UpdateableProperties.html +1 -1
  229. package/docs/types/types.ValidationType.html +1 -1
  230. package/docs/types/types.ViewModel.html +2 -2
  231. package/docs/types/types.ViewModelClass.html +1 -1
  232. package/docs/types/types.WeekdayName.html +1 -1
  233. package/docs/types/types.WhereStatementForDream.html +1 -1
  234. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  235. package/docs/variables/index.DreamConst.html +1 -1
  236. package/docs/variables/index.ops.html +38 -23
  237. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  238. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  239. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  240. package/docs/variables/system.primaryKeyTypes.html +1 -1
  241. package/package.json +1 -1
@@ -256,6 +256,32 @@ export default class QueryDriverBase {
256
256
  options = {}) {
257
257
  throw new Error('implement takeAll in child class');
258
258
  }
259
+ /**
260
+ * @internal
261
+ *
262
+ * Adapter seam for exclusive row locking. Given the select statement about to
263
+ * be executed, return a statement that additionally acquires an exclusive
264
+ * lock on each row it returns, held until the enclosing transaction commits
265
+ * or rolls back (Postgres: `SELECT ... FOR UPDATE OF <table>`).
266
+ *
267
+ * This is only ever reached when a caller opts into a locked read (today,
268
+ * `Query#destroy({ lock: true })`), and only from inside a transaction. The
269
+ * base implementation throws so that an adapter which has not implemented row
270
+ * locking fails loud rather than silently performing an unlocked read and
271
+ * quietly dropping the compare-and-set guarantee the caller asked for.
272
+ *
273
+ * @param kyselyQuery - the select statement to lock
274
+ * @param tableAlias - the alias of the Query's base table; only rows of that
275
+ * table are locked, so a joined query does not lock unrelated rows and does
276
+ * not fail on the nullable side of an outer join
277
+ */
278
+ applyRowLock(
279
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
280
+ kyselyQuery,
281
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
282
+ tableAlias) {
283
+ throw new Error('implement applyRowLock in child class');
284
+ }
259
285
  /**
260
286
  * Retrieves the max value of the specified column
261
287
  * for this Query
@@ -29,6 +29,9 @@ import MissingThroughAssociationSource from '../../errors/associations/MissingTh
29
29
  import CannotNegateSimilarityClause from '../../errors/CannotNegateSimilarityClause.js';
30
30
  import CannotNamespaceAssociationFilterToAnotherTable from '../../errors/CannotNamespaceAssociationFilterToAnotherTable.js';
31
31
  import CannotPassUndefinedAsAValueToAWhereClause from '../../errors/CannotPassUndefinedAsAValueToAWhereClause.js';
32
+ import ArrayEqualityRequiresArrayColumn from '../../errors/ops/ArrayEqualityRequiresArrayColumn.js';
33
+ import RowLockIncompatibleWithDistinct from '../../errors/RowLockIncompatibleWithDistinct.js';
34
+ import RowLockIncompatibleWithJoinLoad from '../../errors/RowLockIncompatibleWithJoinLoad.js';
32
35
  import CheckConstraintViolation from '../../errors/db/CheckConstraintViolation.js';
33
36
  import ColumnOverflow from '../../errors/db/ColumnOverflow.js';
34
37
  import DataTypeColumnTypeMismatch from '../../errors/db/DataTypeColumnTypeMismatch.js';
@@ -39,6 +42,7 @@ import ASTSchemaBuilder from '../../helpers/cli/ASTSchemaBuilder.js';
39
42
  import generateMigration from '../../helpers/cli/generateMigration.js';
40
43
  import compact from '../../helpers/compact.js';
41
44
  import { normalizeDataForDb } from '../../helpers/db/normalizeDataForDb.js';
45
+ import isDatabaseArrayColumn from '../../helpers/db/types/isDatabaseArrayColumn.js';
42
46
  import EnvInternal from '../../helpers/EnvInternal.js';
43
47
  import groupBy from '../../helpers/groupBy.js';
44
48
  import isEmpty from '../../helpers/isEmpty.js';
@@ -412,9 +416,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
412
416
  * @returns an array of dreams
413
417
  */
414
418
  async takeAll(options = {}) {
415
- if (this.query['joinLoadActivated'])
419
+ if (this.query['joinLoadActivated']) {
420
+ // executeJoinLoad builds its own select and has nowhere to put a row
421
+ // lock, so a locked read here has to fail rather than quietly return
422
+ // unlocked rows and drop the guarantee the caller asked for
423
+ if (options.lock)
424
+ throw new RowLockIncompatibleWithJoinLoad();
416
425
  return await this.executeJoinLoad(options);
417
- const kyselyQuery = this.buildSelect(options);
426
+ }
427
+ // `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database, so
428
+ // fail loudly here instead of emitting SQL that cannot run
429
+ if (options.lock && this.query['distinctColumn'])
430
+ throw new RowLockIncompatibleWithDistinct();
431
+ let kyselyQuery = this.buildSelect(options);
432
+ // row locking is adapter-specific, so it goes through the query driver seam
433
+ if (options.lock)
434
+ kyselyQuery = this.applyRowLock(kyselyQuery, this.query['baseSqlAlias']);
418
435
  const results = await executeDatabaseQuery(kyselyQuery, 'execute');
419
436
  const theAll = results.map(r => this.dbResultToDreamInstance(r, this.dreamClass));
420
437
  await this.applyPreload(this.query['preloadStatements'], this.query['preloadOnStatements'], theAll);
@@ -1484,8 +1501,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1484
1501
  b = '=';
1485
1502
  c = val;
1486
1503
  }
1487
- c = this.normalizedWhereValue(dreamClass, attr, c);
1488
- c2 = this.normalizedWhereValue(dreamClass, attr, c2);
1504
+ c = maybeBindArrayAsSingleValue(dreamClass, a, b, this.normalizedWhereValue(dreamClass, attr, c));
1505
+ c2 = maybeBindArrayAsSingleValue(dreamClass, a2, b2, this.normalizedWhereValue(dreamClass, attr, c2));
1489
1506
  if (a && c === undefined)
1490
1507
  throw new CannotPassUndefinedAsAValueToAWhereClause(this.dreamClass, a);
1491
1508
  if (a2 && c2 === undefined)
@@ -2525,6 +2542,31 @@ export function resolvePostgresSsl(connectionConf) {
2525
2542
  return { rejectUnauthorized: false };
2526
2543
  return false;
2527
2544
  }
2545
+ const EQUALITY_OPERATORS_TAKING_A_WHOLE_ARRAY_VALUE = ['=', '!=', '<>'];
2546
+ /**
2547
+ * Kysely renders a raw Javascript array passed as the right-hand side of a
2548
+ * comparison as a value list (`($1, $2)`), which is what `in` / `not in` need,
2549
+ * but which Postgres rejects for `=` against an array column
2550
+ * (`operator does not exist: text[] = record`). For the equality operators,
2551
+ * bind the array as a single parameter so that `ops.equal([...])` against an
2552
+ * array column means literal array equality (`"col" = $1`).
2553
+ *
2554
+ * The rewrite is gated on the column actually being a database array column.
2555
+ * An array reaching an equality operator against a *scalar* column is a
2556
+ * mistake, and binding it as a single value would turn that mistake into a
2557
+ * clause that matches nothing (`"name" = '{}'`) instead of one that raises, so
2558
+ * it throws instead.
2559
+ */
2560
+ function maybeBindArrayAsSingleValue(dreamClass, column, operator, val) {
2561
+ if (!Array.isArray(val))
2562
+ return val;
2563
+ if (!EQUALITY_OPERATORS_TAKING_A_WHOLE_ARRAY_VALUE.includes(operator))
2564
+ return val;
2565
+ const isolatedColumn = maybeNamespacedColumnNameToColumnName(column ?? '');
2566
+ if (!isDatabaseArrayColumn(dreamClass, isolatedColumn))
2567
+ throw new ArrayEqualityRequiresArrayColumn(dreamClass, isolatedColumn, operator);
2568
+ return sql.val(val);
2569
+ }
2528
2570
  function shouldSoftDelete(dream, reallyDestroy) {
2529
2571
  const dreamClass = dream.constructor;
2530
2572
  return dreamClass['softDelete'] && !reallyDestroy;
@@ -198,6 +198,27 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
198
198
  await client.end();
199
199
  DreamCLI.logger.logEndProgress();
200
200
  }
201
+ /**
202
+ * @internal
203
+ *
204
+ * Postgres implementation of the row-locking seam: appends
205
+ * `FOR UPDATE OF "<tableAlias>"` to the select, so every row the statement
206
+ * returns is exclusively locked until the enclosing transaction ends.
207
+ *
208
+ * Scoping the lock to the Query's own base table (rather than a bare
209
+ * `FOR UPDATE`) means a query that joins other tables neither locks rows the
210
+ * caller never asked about nor raises Postgres's "FOR UPDATE cannot be
211
+ * applied to the nullable side of an outer join".
212
+ *
213
+ * This is a waiting lock: neither `NOWAIT` nor `SKIP LOCKED` is applied, so a
214
+ * statement that collides with another transaction's lock blocks until that
215
+ * transaction ends. Callers that cannot afford an unbounded wait should set a
216
+ * `lock_timeout` on the connection; see the guarded-destroy notes on
217
+ * `Query#destroy`.
218
+ */
219
+ applyRowLock(kyselyQuery, tableAlias) {
220
+ return kyselyQuery.forUpdate(tableAlias);
221
+ }
201
222
  static supportsParallelTestDatabases = true;
202
223
  /**
203
224
  * @internal
@@ -1,20 +1,69 @@
1
+ import loadDependentDestroyTree from './loadDependentDestroyTree.js';
2
+ /**
3
+ * @internal
4
+ *
5
+ * Marks a Dream instance as one whose `dependent: 'destroy'` associations were
6
+ * hydrated by this cascade's own call to `loadDependentDestroyTree`, using the
7
+ * same destroy options the cascade is running under.
8
+ *
9
+ * Associations a caller happened to preload themselves (e.g.
10
+ * `Post.preload('comments')`) are deliberately NOT trusted: they may have been
11
+ * loaded under different default scopes than the destroy requires — most
12
+ * importantly, `reallyDestroy` strips the soft-delete scope, so a
13
+ * caller-preloaded association could be missing soft-deleted records that the
14
+ * cascade must destroy.
15
+ */
16
+ const CASCADE_LOADED = Symbol.for('dream:dependentDestroyTreeLoaded');
1
17
  /**
2
18
  * @internal
3
19
  *
4
20
  * Destroys all HasOne/HasMany associations on this
5
21
  * dream that are marked as `dependent: 'destroy'`.
6
22
  *
7
- * Expects associations to be preloaded onto the dream instance
8
- * via `loadDependentDestroyTree`. Iterates loaded associations
9
- * directly instead of querying the database.
23
+ * The whole dependent-destroy tree is loaded ONCE, at the root of the destroy
24
+ * operation, by `loadDependentDestroyTree`. Descendants reached by recursing
25
+ * through this function consume those already-hydrated associations rather
26
+ * than re-querying their own subtrees.
27
+ *
28
+ * A load still happens here when an association is not hydrated. That covers
29
+ * two cases:
30
+ *
31
+ * 1. the root of the operation, whose associations have not been loaded yet
32
+ * 2. a node past `RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS` levels of
33
+ * self-reference, which the root's preload could not reach
34
+ *
35
+ * The second case is why the check is per-association-hydration rather than a
36
+ * "descendants are already loaded" flag: such a flag would truncate cascades
37
+ * deeper than the preload horizon.
38
+ *
39
+ * NOTE: reusing a hydrated subtree is only sound because the same `options`
40
+ * are forwarded unchanged all the way down the cascade (see the recursive
41
+ * `destroy`/`reallyDestroy` calls below), so every node's associations were
42
+ * loaded under the scopes this node would have used. Nothing in the types
43
+ * enforces that; if `options` ever start varying by depth, this reuse has to
44
+ * be revisited.
10
45
  */
11
46
  export default async function destroyAssociatedRecords(dream, txn, options) {
12
47
  const dreamClass = dream.constructor;
13
48
  const { reallyDestroy } = options;
49
+ // `loadDependentDestroyTree` hydrates and returns a CLONE rather than
50
+ // mutating the receiver, so the result must be assigned back — otherwise
51
+ // each association in the loop below would trigger its own reload.
52
+ let dreamWithAssociations = dream;
53
+ let alreadyLoadedTree = false;
14
54
  for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
15
- const loaded = dream[associationName];
55
+ if (!alreadyLoadedTree && !cascadeLoaded(dreamWithAssociations, associationName)) {
56
+ dreamWithAssociations = await loadDependentDestroyTree(dreamWithAssociations, txn, {
57
+ reallyDestroy,
58
+ bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
59
+ defaultScopesToBypass: options.defaultScopesToBypass ?? [],
60
+ });
61
+ alreadyLoadedTree = true;
62
+ }
63
+ const loaded = dreamWithAssociations[associationName];
16
64
  const records = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
17
65
  for (const record of records) {
66
+ markCascadeLoaded(record);
18
67
  if (reallyDestroy) {
19
68
  await record.txn(txn).reallyDestroy(options);
20
69
  }
@@ -24,3 +73,26 @@ export default async function destroyAssociatedRecords(dream, txn, options) {
24
73
  }
25
74
  }
26
75
  }
76
+ /**
77
+ * @internal
78
+ *
79
+ * True when this dream was hydrated by this cascade's own preload AND the
80
+ * given association is hydrated on it.
81
+ */
82
+ function cascadeLoaded(dream, associationName) {
83
+ if (!dream[CASCADE_LOADED])
84
+ return false;
85
+ return dream.loaded(associationName);
86
+ }
87
+ /**
88
+ * @internal
89
+ *
90
+ * A plain assignment rather than `Object.defineProperty`: the key is a symbol,
91
+ * so it is already excluded from `Object.keys`, `for...in` and
92
+ * `JSON.stringify` without an explicit descriptor, and this runs once per node
93
+ * of a potentially wide cascade tree.
94
+ */
95
+ function markCascadeLoaded(dream) {
96
+ ;
97
+ dream[CASCADE_LOADED] = true;
98
+ }
@@ -1,6 +1,5 @@
1
1
  import DreamApp from '../../dream-app/index.js';
2
2
  import destroyAssociatedRecords from './destroyAssociatedRecords.js';
3
- import loadDependentDestroyTree from './loadDependentDestroyTree.js';
4
3
  import runHooksFor from './runHooksFor.js';
5
4
  /**
6
5
  * @internal
@@ -31,12 +30,12 @@ export default async function destroyDream(dream, txn = null, options) {
31
30
  async function destroyDreamWithTransaction(dream, txn, options) {
32
31
  const { cascade, reallyDestroy, skipHooks } = options;
33
32
  if (cascade) {
34
- const dreamWithAssociations = await loadDependentDestroyTree(dream, txn, {
35
- reallyDestroy,
36
- bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
37
- defaultScopesToBypass: options.defaultScopesToBypass ?? [],
38
- });
39
- await destroyAssociatedRecords(dreamWithAssociations, txn, options);
33
+ // NOTE: the dependent-destroy tree is loaded lazily, inside
34
+ // destroyAssociatedRecords, so that a cascaded descendant whose
35
+ // associations the root already preloaded does not re-query its own
36
+ // subtree. `dream` itself is intentionally never replaced by the loaded
37
+ // clone: the hooks and the delete below operate on the original.
38
+ await destroyAssociatedRecords(dream, txn, options);
40
39
  }
41
40
  if (!skipHooks) {
42
41
  await runHooksFor('beforeDestroy', dream, true, null, txn);
@@ -0,0 +1,19 @@
1
+ export default class InvalidBatchSize extends Error {
2
+ batchSize;
3
+ constructor(batchSize) {
4
+ super();
5
+ this.batchSize = batchSize;
6
+ }
7
+ get message() {
8
+ return `
9
+ Invalid batchSize provided.
10
+
11
+ batchSize given:
12
+ ${String(this.batchSize)}
13
+
14
+ batchSize must be a positive integer. A batchSize of 0 is especially dangerous,
15
+ since a limit of 0 is treated as "no limit", which would collapse a batched
16
+ operation into a single unbounded pass over the entire matched set.
17
+ `;
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ export default class NoColumnsToAlterMigration extends Error {
2
+ table;
3
+ alterDirection;
4
+ constructor(table, alterDirection) {
5
+ super();
6
+ this.table = table;
7
+ this.alterDirection = alterDirection;
8
+ }
9
+ get message() {
10
+ return `
11
+ no valid columns to ${this.alterDirection === 'remove' ? 'drop from' : 'add to'} table '${this.table}' in this alter migration.
12
+
13
+ Pass at least one column declaration with a resolvable type, e.g.:
14
+ name:string
15
+ `;
16
+ }
17
+ }
@@ -0,0 +1,14 @@
1
+ export default class RowLockIncompatibleWithDistinct extends Error {
2
+ get message() {
3
+ // `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database
4
+ // ("FOR UPDATE is not allowed with DISTINCT clause"), so fail here, with a
5
+ // message that names the two incompatible parts, rather than surfacing a
6
+ // raw driver error from inside a per-batch transaction.
7
+ return `
8
+ An exclusive row lock was requested on a query that also calls \`distinct\`.
9
+
10
+ A locked read cannot be combined with DISTINCT. Drop the \`distinct\` call, or
11
+ drop the lock.
12
+ `;
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ export default class RowLockIncompatibleWithJoinLoad extends Error {
2
+ get message() {
3
+ // A join load builds its own select statement (one row per joined
4
+ // association row, later collapsed in memory), which cannot carry the
5
+ // Query's row lock. Rather than silently performing an unlocked read and
6
+ // dropping the compare-and-set guarantee the caller asked for, refuse.
7
+ return `
8
+ An exclusive row lock was requested on a query that also uses leftJoinPreload.
9
+
10
+ A join-loaded query cannot be locked. Use \`preload\` instead of
11
+ \`leftJoinPreload\`, or drop the lock.
12
+ `;
13
+ }
14
+ }
@@ -0,0 +1,16 @@
1
+ export default class UnparseableMigrationColumn extends Error {
2
+ declaration;
3
+ constructor(declaration) {
4
+ super();
5
+ this.declaration = declaration;
6
+ }
7
+ get message() {
8
+ return `
9
+ could not determine a column type for the following migration column declaration:
10
+ ${this.declaration}
11
+
12
+ Make sure the column includes a type, e.g.:
13
+ ${this.declaration.split(':')[0]}:string
14
+ `;
15
+ }
16
+ }
@@ -0,0 +1,23 @@
1
+ export default class ArrayEqualityRequiresArrayColumn extends Error {
2
+ dreamClass;
3
+ column;
4
+ operator;
5
+ constructor(dreamClass, column, operator) {
6
+ super();
7
+ this.dreamClass = dreamClass;
8
+ this.column = column;
9
+ this.operator = operator;
10
+ }
11
+ get message() {
12
+ return `
13
+ Attempting to compare an array for equality against a column that is not an
14
+ array in the database:
15
+
16
+ ${this.dreamClass.sanitizedName}#${this.column} (operator: ${this.operator})
17
+
18
+ Whole-array equality (\`=\`, \`!=\`, \`<>\`) is only meaningful for array columns.
19
+ If you meant "is this column one of these values", use \`ops.in([...])\`
20
+ (or a bare array) instead.
21
+ `;
22
+ }
23
+ }
@@ -1,6 +1,6 @@
1
1
  import * as path from 'node:path';
2
2
  import pluralize from 'pluralize-esm';
3
- import generateMigrationContent from '../cli/generateMigrationContent.js';
3
+ import generateMigrationContent, { MIGRATION_TABLE_NAME_PLACEHOLDER, } from '../cli/generateMigrationContent.js';
4
4
  import primaryKeyType from '../db/primaryKeyType.js';
5
5
  import hyphenize from '../hyphenize.js';
6
6
  import migrationVersion from '../migrationVersion.js';
@@ -9,6 +9,18 @@ import dreamPath from '../path/dreamPath.js';
9
9
  import snakeify from '../snakeify.js';
10
10
  import generateStiMigrationContent from './generateStiMigrationContent.js';
11
11
  import writeGeneratedFile from './writeGeneratedFile.js';
12
+ /**
13
+ * Equivalent to `migrationName.match(new RegExp(`${marker}(.+)$`))?.[1]`, but
14
+ * without a regex: CodeQL flags `.+` anchored to `$` as a polynomial-ReDoS
15
+ * shape, and a plain `indexOf`/`slice` does the same leftmost-match-to-end
16
+ * job with no backtracking risk.
17
+ */
18
+ function tableNameFromSuffix(migrationName, marker) {
19
+ const markerIndex = migrationName.indexOf(marker);
20
+ if (markerIndex === -1)
21
+ return undefined;
22
+ return migrationName.slice(markerIndex + marker.length) || undefined;
23
+ }
12
24
  export default async function generateMigration({ migrationName, columnsWithTypes, connectionName, fullyQualifiedModelName, fullyQualifiedParentName, tableName: explicitTableName, modelClassName, softDelete = false, }) {
13
25
  const migrationsBasePath = connectionName === 'default'
14
26
  ? path.join(dreamPath('db'), 'migrations')
@@ -33,12 +45,17 @@ export default async function generateMigration({ migrationName, columnsWithType
33
45
  });
34
46
  }
35
47
  else {
36
- const tableName = migrationName.match(/-to-(.+)$/)?.[1];
48
+ const toTableName = tableNameFromSuffix(migrationName, '-to-');
49
+ const fromTableName = toTableName
50
+ ? undefined
51
+ : tableNameFromSuffix(migrationName, '-from-');
52
+ const tableName = toTableName || fromTableName;
37
53
  content = generateMigrationContent({
38
- table: tableName ? pluralize(snakeify(tableName)) : '<table-name>',
54
+ table: tableName ? pluralize(snakeify(tableName)) : MIGRATION_TABLE_NAME_PLACEHOLDER,
39
55
  columnsWithTypes,
40
56
  primaryKeyType: primaryKeyType(connectionName),
41
57
  createOrAlter: 'alter',
58
+ alterDirection: fromTableName ? 'remove' : 'add',
42
59
  });
43
60
  }
44
61
  await writeGeneratedFile({
@@ -2,11 +2,19 @@ import pluralize from 'pluralize-esm';
2
2
  import lookupModelByGlobalName from '../../dream-app/helpers/lookupModelByGlobalName.js';
3
3
  import Query from '../../dream/Query.js';
4
4
  import InvalidDecimalFieldPassedToGenerator from '../../errors/InvalidDecimalFieldPassedToGenerator.js';
5
+ import NoColumnsToAlterMigration from '../../errors/NoColumnsToAlterMigration.js';
6
+ import UnparseableMigrationColumn from '../../errors/UnparseableMigrationColumn.js';
5
7
  import camelize from '../camelize.js';
6
8
  import compact from '../compact.js';
7
9
  import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
8
10
  import snakeify from '../snakeify.js';
9
11
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
12
+ // Sentinel table name used by generateMigration.ts when a standalone
13
+ // migration name matches neither a `-to-<table>` nor `-from-<table>` suffix.
14
+ // That path intentionally produces a stub the user is expected to hand-edit
15
+ // (e.g. an index-only migration with no column operations at all), so it's
16
+ // exempt from the "no valid columns" check below.
17
+ export const MIGRATION_TABLE_NAME_PLACEHOLDER = '<table-name>';
10
18
  const STI_TYPE_COLUMN_NAME = 'type';
11
19
  // deleted_at is deliberately NOT in this list: the SoftDelete default scope's
12
20
  // `WHERE deleted_at IS NULL` is unselective on healthy tables, Dream internals
@@ -15,8 +23,9 @@ const STI_TYPE_COLUMN_NAME = 'type';
15
23
  // default. See spec/unit/cli/generateMigrationContent.spec.ts
16
24
  // ("deleted_at is deliberately NOT indexed") and the CHANGELOG.
17
25
  const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME];
18
- export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, softDelete = false, } = {}) {
26
+ export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', alterDirection = 'add', stiChildClassName, softDelete = false, } = {}) {
19
27
  const altering = createOrAlter === 'alter';
28
+ const removingInUp = altering && alterDirection === 'remove';
20
29
  let requireCitextExtension = false;
21
30
  const checkConstraints = [];
22
31
  // When creating a new table, we automatically emit `created_at`,
@@ -65,7 +74,17 @@ export default function generateMigrationContent({ connectionName = 'default', t
65
74
  // when creating a migration for an STI child, we don't want to include notNull;
66
75
  // instead, we'll add a check constraint that uses the STI child class name
67
76
  const sqlAttributeType = getAttributeType(attributeType, descriptors);
68
- if (attributeType === undefined || ['hasone', 'hasmany'].includes(processedAttrType))
77
+ if (attributeType === undefined) {
78
+ // In alter mode (both `-to-`/add and `-from-`/remove), a column whose
79
+ // type can't be resolved (e.g. a mistyped declaration with no `:type`
80
+ // segment) must not silently vanish from the generated migration —
81
+ // that's especially dangerous for `-from-` migrations, whose whole
82
+ // premise is describing removed columns so `down` can restore them.
83
+ if (altering)
84
+ throw new UnparseableMigrationColumn(attributeDeclaration);
85
+ return acc;
86
+ }
87
+ if (['hasone', 'hasmany'].includes(processedAttrType))
69
88
  return acc;
70
89
  if (attributeType === 'citext')
71
90
  requireCitextExtension = true;
@@ -169,6 +188,25 @@ export async function down(db: Kysely<any>): Promise<void> {
169
188
  }\
170
189
  `;
171
190
  }
191
+ // An alter migration (either `-to-`/add or `-from-`/remove) with no valid
192
+ // columns to add/drop would otherwise emit a bare `.alterTable(...).execute()`
193
+ // with no column operation at all, which Postgres rejects at migration-run
194
+ // time rather than generation time. Fail loudly here instead.
195
+ //
196
+ // This guard is scoped to the standalone `g:migration` `-to-`/`-from-` flow
197
+ // only (`stiChildClassName` is unset there). `g:sti-child` legitimately
198
+ // calls this function in alter mode with zero `columnsWithTypes` — a
199
+ // zero-attribute STI child is a documented use case (see `g:sti-child`'s
200
+ // help example in src/cli/index.ts), and `generateStiMigrationContent`
201
+ // handles the type-column/check-constraint machinery separately from the
202
+ // `columnDefs` this check inspects, so an empty `columnDefs` there is
203
+ // expected, not a sign of a mistyped/unparseable column list.
204
+ if (altering &&
205
+ columnDefs.length === 0 &&
206
+ table !== MIGRATION_TABLE_NAME_PLACEHOLDER &&
207
+ !stiChildClassName) {
208
+ throw new NoColumnsToAlterMigration(table, alterDirection);
209
+ }
172
210
  const citextExtension = requireCitextExtension
173
211
  ? ` await DreamMigrationHelpers.createExtension(db, 'citext')\n\n`
174
212
  : '';
@@ -179,7 +217,22 @@ export async function down(db: Kysely<any>): Promise<void> {
179
217
  const newlineIndent = '\n ';
180
218
  const newlineDoubleIndent = '\n ';
181
219
  const doubleNewlineIndent = '\n\n ';
182
- const columnDefLines = columnDefs.length ? newlineDoubleIndent + columnDefs.join(newlineDoubleIndent) : '';
220
+ // For a `-from-` migration (`alterDirection: 'remove'`), `down` re-adds the
221
+ // column(s) that `up` removed. The generator has no way to know the
222
+ // column's original default, so a non-optional (`.notNull()`) column with
223
+ // no default emitted here will fail at migration-run time against a table
224
+ // that already has rows. We can't fix that without a lot more heavy
225
+ // lifting (and the risk that comes with it), so flag it with a comment
226
+ // instead of silently generating a `down` that's likely to break.
227
+ const NO_KNOWN_DEFAULT_COMMENT = "// NOTE: the generator doesn't know this column's original default; this addColumn will fail against a table with existing rows unless you add a default by hand.";
228
+ const finalColumnDefs = removingInUp
229
+ ? columnDefs.map(def => def.includes('.notNull()') && !def.includes('.defaultTo(')
230
+ ? `${NO_KNOWN_DEFAULT_COMMENT}\n ${def}`
231
+ : def)
232
+ : columnDefs;
233
+ const columnDefLines = finalColumnDefs.length
234
+ ? newlineDoubleIndent + finalColumnDefs.join(newlineDoubleIndent)
235
+ : '';
183
236
  const columnDropLines = columnDrops.length
184
237
  ? newlineDoubleIndent + columnDrops.join(newlineDoubleIndent) + newlineDoubleIndent
185
238
  : '';
@@ -190,21 +243,33 @@ export async function down(db: Kysely<any>): Promise<void> {
190
243
  newlineDoubleIndent +
191
244
  ".addColumn('updated_at', 'timestamp', col => col.notNull())" +
192
245
  (emitDeletedAtColumn ? newlineDoubleIndent + ".addColumn('deleted_at', 'timestamp')" : '');
246
+ // For `alterDirection: 'remove'` (a `-from-` migration), the enum *type*
247
+ // itself must never be created or dropped here, even when the column
248
+ // declaration includes inline enum values — enums are frequently reused
249
+ // across columns/tables, so `up` must drop only the column (no `dropType`)
250
+ // and `down` must re-add only the column (no `createType`), identical to
251
+ // referencing an existing enum by name with no values.
252
+ const enumCreateStatements = removingInUp ? '' : generateEnumStatements(columnsWithTypes);
253
+ const enumDropStatements = removingInUp ? '' : generateEnumDropStatements(columnsWithTypes);
254
+ const addColumnsBody = `${citextExtension}${enumCreateStatements} await db.schema
255
+ .${altering ? 'alterTable' : 'createTable'}('${table}')${altering ? '' : newlineDoubleIndent + generateIdStr({ primaryKeyType })}${columnDefLines}${timestampColumnLines}
256
+ .execute()${indexDefs.length ? `\n${newlineIndent}` : ''}${indexDefs.join(doubleNewlineIndent)}${checkConstraints.join('')}`;
257
+ const removeColumnsBody = ` ${altering
258
+ ? `await db.schema${newlineDoubleIndent}.alterTable('${table}')${columnDropLines}.execute()`
259
+ : `await db.schema.dropTable('${table}').execute()`}${enumDropStatements}`;
260
+ const upBody = removingInUp ? removeColumnsBody : addColumnsBody;
261
+ const downBody = removingInUp ? addColumnsBody : removeColumnsBody;
193
262
  return `\
194
263
  ${dreamDbImports.length ? `import { ${dreamDbImports.join(', ')} } from '@rvoh/dream/db'\n` : ''}import { ${kyselyImports.join(', ')} } from 'kysely'
195
264
 
196
265
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
266
  export async function up(db: Kysely<any>): Promise<void> {
198
- ${citextExtension}${generateEnumStatements(columnsWithTypes)} await db.schema
199
- .${altering ? 'alterTable' : 'createTable'}('${table}')${altering ? '' : newlineDoubleIndent + generateIdStr({ primaryKeyType })}${columnDefLines}${timestampColumnLines}
200
- .execute()${indexDefs.length ? `\n${newlineIndent}` : ''}${indexDefs.join(doubleNewlineIndent)}${checkConstraints.join('')}
267
+ ${upBody}
201
268
  }
202
269
 
203
270
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
271
  export async function down(db: Kysely<any>): Promise<void> {
205
- ${altering
206
- ? `await db.schema${newlineDoubleIndent}.alterTable('${table}')${columnDropLines}.execute()`
207
- : `await db.schema.dropTable('${table}').execute()`}${generateEnumDropStatements(columnsWithTypes)}
272
+ ${downBody}
208
273
  }\
209
274
  `;
210
275
  }
@@ -117,11 +117,27 @@ const ops = {
117
117
  /**
118
118
  * Creates an `OpsStatement` using the `=` operator for strict equality.
119
119
  *
120
+ * **Array columns:** passing an array means literal array equality — the
121
+ * whole array is bound as a single parameter, so the column must match
122
+ * element-for-element, in order. This is the opt-in counterpart to a bare
123
+ * array in a where clause (`Post.where({ tags: ['a', 'b'] })`), which means
124
+ * `IN`, and to `ops.any`, which tests for containment of a single element.
125
+ * Element types are constrained by the column, so an enum array column
126
+ * rejects a string that is not a member of its enum.
127
+ *
128
+ * Note that PostgreSQL defines no equality operator for `json` (as opposed
129
+ * to `jsonb`), so `ops.equal` against a `json[]` column raises at the
130
+ * database.
131
+ *
120
132
  * @param equal - The value the column must equal.
121
133
  * @returns An `OpsStatement` using the `=` operator.
122
134
  *
123
135
  * @example
124
136
  * User.where({ role: ops.equal('admin') })
137
+ *
138
+ * @example
139
+ * // matches only rows whose tags column is exactly ['a', 'b'], in that order
140
+ * Post.where({ tags: ops.equal(['a', 'b']) })
125
141
  */
126
142
  equal: (equal) => new OpsStatement('=', equal),
127
143
  /**
@@ -260,6 +276,9 @@ const ops = {
260
276
  /**
261
277
  * Creates an `OpsStatement` using the `!=` operator for inequality.
262
278
  *
279
+ * **Array columns:** as with `ops.equal`, passing an array compares against
280
+ * the whole array rather than against its elements.
281
+ *
263
282
  * @param equal - The value the column must not equal.
264
283
  * @returns An `OpsStatement` using the `!=` operator.
265
284
  *
@@ -1011,6 +1011,9 @@ export default class Dream {
1011
1011
  * // User{email: 'goodbye@world'}
1012
1012
  * ```
1013
1013
  *
1014
+ * Records are always visited in ascending primary key order; any `order`
1015
+ * applied to the query is discarded. See {@link Query.findEach} for why.
1016
+ *
1014
1017
  * @param cb - The callback to call for each found record
1015
1018
  * @param opts - Optional parameters for batch processing
1016
1019
  * @param opts.batchSize - the batch size you wish to collect records in. If not provided, it will default to 1000
@@ -14,7 +14,7 @@ export type SpawnOptions = Omit<NodeSpawnOptions, 'shell'> & {
14
14
  args?: string[];
15
15
  };
16
16
  export declare const CLI_INDENT = " ";
17
- export declare const baseColumnsWithTypesDescription = "space separated snake-case properties like this:\n title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal\n \n all properties default to not nullable; null can be allowed by appending ':optional':\n subtitle:string:optional\n \n supported types:\n - uuid:\n - uuid[]:\n a column optimized for storing UUIDs\n \n - citext:\n - citext[]:\n case insensitive text (indexes and queries are automatically case insensitive)\n \n - encrypted:\n encrypted text (used in conjunction with the @deco.Encrypted decorator)\n \n - string:\n - string[]:\n varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional\n \n - text\n - text[]\n - date\n - date[]\n - datetime\n - datetime[]\n - time\n - time[]\n - timetz\n - timetz[]\n - integer\n - integer[]\n \n - decimal:\n - decimal[]:\n precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: volume:decimal[]:3,2\n \n - enum:\n - enum[]:\n include the enum name to automatically create the enum:\n type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional\n \n omit the enum values to leverage an existing enum (omits the enum type creation):\n type:enum:room_types or type:enum:room_types:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom";
17
+ export declare const baseColumnsWithTypesDescription = "space separated snake-case properties like this:\n title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal\n \n all properties default to not nullable; null can be allowed by appending ':optional':\n subtitle:string:optional\n \n supported types:\n - uuid:\n - uuid[]:\n a column optimized for storing UUIDs\n \n - citext:\n - citext[]:\n case insensitive text (indexes and queries are automatically case insensitive)\n \n - encrypted:\n encrypted text (used in conjunction with the @deco.Encrypted decorator)\n \n - string:\n - string[]:\n varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional\n \n - text\n - text[]\n - date\n - date[]\n - datetime\n - datetime[]\n - time\n - time[]\n - timetz\n - timetz[]\n - integer\n - integer[]\n - boolean\n - boolean[]\n \n - decimal:\n - decimal[]:\n precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: volume:decimal[]:3,2\n \n - enum:\n - enum[]:\n include the enum name to automatically create the enum:\n type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional\n \n omit the enum values to leverage an existing enum (omits the enum type creation):\n type:enum:room_types or type:enum:room_types:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom";
18
18
  export declare const columnsWithTypesDescriptionForStiChild: string;
19
19
  export default class DreamCLI {
20
20
  /**