@rvoh/dream 2.19.0 → 2.21.1

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 (249) hide show
  1. package/dist/cjs/src/Dream.js +83 -0
  2. package/dist/cjs/src/cli/index.js +7 -4
  3. package/dist/cjs/src/dream/Query.js +11 -0
  4. package/dist/cjs/src/dream/QueryDriver/Kysely.js +62 -14
  5. package/dist/cjs/src/dream/internal/filterRowToKnownColumns.js +41 -0
  6. package/dist/cjs/src/dream/internal/saveDream.js +6 -1
  7. package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  8. package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +17 -0
  9. package/dist/cjs/src/errors/UnparseableMigrationColumn.js +16 -0
  10. package/dist/cjs/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  11. package/dist/cjs/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  12. package/dist/cjs/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  13. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  14. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  15. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  16. package/dist/cjs/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  17. package/dist/cjs/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  18. package/dist/cjs/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  19. package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  20. package/dist/cjs/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  21. package/dist/cjs/src/helpers/cli/generateMigration.js +20 -3
  22. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +74 -9
  23. package/dist/cjs/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  24. package/dist/esm/src/Dream.js +83 -0
  25. package/dist/esm/src/cli/index.js +7 -4
  26. package/dist/esm/src/dream/Query.js +11 -0
  27. package/dist/esm/src/dream/QueryDriver/Kysely.js +62 -14
  28. package/dist/esm/src/dream/internal/filterRowToKnownColumns.js +41 -0
  29. package/dist/esm/src/dream/internal/saveDream.js +6 -1
  30. package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  31. package/dist/esm/src/errors/NoColumnsToAlterMigration.js +17 -0
  32. package/dist/esm/src/errors/UnparseableMigrationColumn.js +16 -0
  33. package/dist/esm/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  34. package/dist/esm/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  35. package/dist/esm/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  36. package/dist/esm/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  37. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  38. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  39. package/dist/esm/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  40. package/dist/esm/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  41. package/dist/esm/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  42. package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  43. package/dist/esm/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  44. package/dist/esm/src/helpers/cli/generateMigration.js +20 -3
  45. package/dist/esm/src/helpers/cli/generateMigrationContent.js +74 -9
  46. package/dist/esm/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  47. package/dist/types/src/Dream.d.ts +81 -0
  48. package/dist/types/src/cli/index.d.ts +1 -1
  49. package/dist/types/src/dream/Query.d.ts +11 -0
  50. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +21 -0
  51. package/dist/types/src/dream/internal/filterRowToKnownColumns.d.ts +30 -0
  52. package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +6 -0
  53. package/dist/types/src/errors/UnparseableMigrationColumn.d.ts +5 -0
  54. package/dist/types/src/errors/schema-builder/CannotIgnoreAssociationColumn.d.ts +12 -0
  55. package/dist/types/src/errors/schema-builder/CannotIgnoreEncryptedColumn.d.ts +7 -0
  56. package/dist/types/src/errors/schema-builder/CannotIgnorePrimaryKey.d.ts +6 -0
  57. package/dist/types/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.d.ts +7 -0
  58. package/dist/types/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.d.ts +7 -0
  59. package/dist/types/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.d.ts +8 -0
  60. package/dist/types/src/errors/schema-builder/CannotIgnoreStiTypeColumn.d.ts +6 -0
  61. package/dist/types/src/errors/schema-builder/ConflictingIgnoredColumns.d.ts +7 -0
  62. package/dist/types/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.d.ts +7 -0
  63. package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +20 -0
  64. package/dist/types/src/helpers/cli/ASTKyselyCodegenEnhancer.d.ts +13 -0
  65. package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +11 -1
  66. package/dist/types/src/helpers/cli/resolveIgnoredColumns.d.ts +41 -0
  67. package/docs/assets/search.js +1 -1
  68. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  69. package/docs/classes/db.KyselyQueryDriver.html +57 -39
  70. package/docs/classes/db.PostgresQueryDriver.html +58 -40
  71. package/docs/classes/db.QueryDriverBase.html +38 -38
  72. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  73. package/docs/classes/errors.ColumnOverflow.html +3 -3
  74. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  75. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  76. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  77. package/docs/classes/errors.DecryptionError.html +2 -2
  78. package/docs/classes/errors.DecryptionParseError.html +2 -2
  79. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  80. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  81. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  82. package/docs/classes/errors.InvalidClockTime.html +2 -2
  83. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  84. package/docs/classes/errors.InvalidDateTime.html +2 -2
  85. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  86. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  87. package/docs/classes/errors.NotNullViolation.html +3 -3
  88. package/docs/classes/errors.RecordNotFound.html +3 -3
  89. package/docs/classes/errors.ValidationError.html +3 -3
  90. package/docs/classes/index.CalendarDate.html +33 -33
  91. package/docs/classes/index.ClockTime.html +32 -32
  92. package/docs/classes/index.ClockTimeTz.html +35 -35
  93. package/docs/classes/index.DateTime.html +86 -86
  94. package/docs/classes/index.Decorators.html +19 -19
  95. package/docs/classes/index.Dream.html +188 -123
  96. package/docs/classes/index.DreamApp.html +10 -10
  97. package/docs/classes/index.DreamTransaction.html +2 -2
  98. package/docs/classes/index.Env.html +2 -2
  99. package/docs/classes/index.Query.html +73 -62
  100. package/docs/classes/system.CliFileWriter.html +4 -4
  101. package/docs/classes/system.DreamBin.html +2 -2
  102. package/docs/classes/system.DreamCLI.html +7 -7
  103. package/docs/classes/system.DreamImporter.html +2 -2
  104. package/docs/classes/system.DreamLogos.html +2 -2
  105. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  106. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  107. package/docs/classes/system.PathHelpers.html +3 -3
  108. package/docs/classes/utils.Encrypt.html +3 -3
  109. package/docs/classes/utils.Range.html +2 -2
  110. package/docs/functions/db.closeAllDbConnections.html +1 -1
  111. package/docs/functions/db.dreamDbConnections.html +1 -1
  112. package/docs/functions/db.untypedDb.html +1 -1
  113. package/docs/functions/db.validateColumn.html +1 -1
  114. package/docs/functions/db.validateTable.html +1 -1
  115. package/docs/functions/errors.pgErrorType.html +1 -1
  116. package/docs/functions/index.DreamSerializer.html +1 -1
  117. package/docs/functions/index.ObjectSerializer.html +1 -1
  118. package/docs/functions/index.ReplicaSafe.html +1 -1
  119. package/docs/functions/index.STI.html +1 -1
  120. package/docs/functions/index.SoftDelete.html +1 -1
  121. package/docs/functions/utils.camelize.html +1 -1
  122. package/docs/functions/utils.capitalize.html +1 -1
  123. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  124. package/docs/functions/utils.compact.html +1 -1
  125. package/docs/functions/utils.groupBy.html +1 -1
  126. package/docs/functions/utils.hyphenize.html +1 -1
  127. package/docs/functions/utils.intersection.html +1 -1
  128. package/docs/functions/utils.isEmpty.html +1 -1
  129. package/docs/functions/utils.normalizeUnicode.html +1 -1
  130. package/docs/functions/utils.pascalize.html +1 -1
  131. package/docs/functions/utils.percent.html +1 -1
  132. package/docs/functions/utils.range.html +1 -1
  133. package/docs/functions/utils.round.html +1 -1
  134. package/docs/functions/utils.sanitizeString.html +1 -1
  135. package/docs/functions/utils.snakeify.html +1 -1
  136. package/docs/functions/utils.sort.html +1 -1
  137. package/docs/functions/utils.sortBy.html +1 -1
  138. package/docs/functions/utils.sortObjectByKey.html +1 -1
  139. package/docs/functions/utils.sortObjectByValue.html +1 -1
  140. package/docs/functions/utils.uncapitalize.html +1 -1
  141. package/docs/functions/utils.uniq.html +1 -1
  142. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  143. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  144. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  145. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  146. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  147. package/docs/interfaces/types.DecoratorContext.html +2 -2
  148. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  149. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  150. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  151. package/docs/interfaces/types.DurationObject.html +2 -2
  152. package/docs/interfaces/types.EncryptOptions.html +2 -2
  153. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  154. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  155. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  156. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  157. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  158. package/docs/types/openapi.OpenapiFormats.html +1 -1
  159. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  160. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  161. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  163. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  164. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  165. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  166. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  167. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  168. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  169. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  170. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  171. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  172. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  173. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  175. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  181. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  183. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  185. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  187. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  188. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  189. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  190. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  191. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  192. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  193. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  194. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  195. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  196. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  197. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  198. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  199. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  200. package/docs/types/types.CalendarDateObject.html +1 -1
  201. package/docs/types/types.Camelized.html +1 -1
  202. package/docs/types/types.ClockTimeObject.html +1 -1
  203. package/docs/types/types.DbConnectionType.html +1 -1
  204. package/docs/types/types.DbTypes.html +1 -1
  205. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  206. package/docs/types/types.DreamAttributes.html +1 -1
  207. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  208. package/docs/types/types.DreamClassColumn.html +1 -1
  209. package/docs/types/types.DreamColumn.html +1 -1
  210. package/docs/types/types.DreamColumnNames.html +1 -1
  211. package/docs/types/types.DreamLogLevel.html +1 -1
  212. package/docs/types/types.DreamLogger.html +2 -2
  213. package/docs/types/types.DreamModelSerializerType.html +1 -1
  214. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  215. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  216. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  217. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  218. package/docs/types/types.DreamSerializable.html +1 -1
  219. package/docs/types/types.DreamSerializableArray.html +1 -1
  220. package/docs/types/types.DreamSerializerKey.html +1 -1
  221. package/docs/types/types.DreamSerializers.html +1 -1
  222. package/docs/types/types.DreamVirtualColumns.html +1 -1
  223. package/docs/types/types.DurationUnit.html +1 -1
  224. package/docs/types/types.EncryptAlgorithm.html +1 -1
  225. package/docs/types/types.HasManyStatement.html +1 -1
  226. package/docs/types/types.HasOneStatement.html +1 -1
  227. package/docs/types/types.Hyphenized.html +1 -1
  228. package/docs/types/types.Pascalized.html +1 -1
  229. package/docs/types/types.PrimaryKeyType.html +1 -1
  230. package/docs/types/types.RoundingPrecision.html +1 -1
  231. package/docs/types/types.SerializerCasing.html +1 -1
  232. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  233. package/docs/types/types.Snakeified.html +1 -1
  234. package/docs/types/types.StrictInterface.html +1 -1
  235. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  236. package/docs/types/types.UpdateableProperties.html +1 -1
  237. package/docs/types/types.ValidationType.html +1 -1
  238. package/docs/types/types.ViewModel.html +2 -2
  239. package/docs/types/types.ViewModelClass.html +1 -1
  240. package/docs/types/types.WeekdayName.html +1 -1
  241. package/docs/types/types.WhereStatementForDream.html +1 -1
  242. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  243. package/docs/variables/index.DreamConst.html +1 -1
  244. package/docs/variables/index.ops.html +1 -1
  245. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  246. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  247. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  248. package/docs/variables/system.primaryKeyTypes.html +1 -1
  249. package/package.json +1 -1
@@ -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
  }
@@ -0,0 +1,198 @@
1
+ import scopeArray from '../../decorators/field/sortable/helpers/scopeArray.js';
2
+ import CannotIgnoreAssociationColumn from '../../errors/schema-builder/CannotIgnoreAssociationColumn.js';
3
+ import CannotIgnoreEncryptedColumn from '../../errors/schema-builder/CannotIgnoreEncryptedColumn.js';
4
+ import CannotIgnorePrimaryKey from '../../errors/schema-builder/CannotIgnorePrimaryKey.js';
5
+ import CannotIgnoreSoftDeleteColumn from '../../errors/schema-builder/CannotIgnoreSoftDeleteColumn.js';
6
+ import CannotIgnoreSortablePositionColumn from '../../errors/schema-builder/CannotIgnoreSortablePositionColumn.js';
7
+ import CannotIgnoreSortableScopeColumn from '../../errors/schema-builder/CannotIgnoreSortableScopeColumn.js';
8
+ import CannotIgnoreStiTypeColumn from '../../errors/schema-builder/CannotIgnoreStiTypeColumn.js';
9
+ import ConflictingIgnoredColumns from '../../errors/schema-builder/ConflictingIgnoredColumns.js';
10
+ import IgnoredColumnMustBeCamelCase from '../../errors/schema-builder/IgnoredColumnMustBeCamelCase.js';
11
+ import camelize from '../camelize.js';
12
+ import uniq from '../uniq.js';
13
+ /**
14
+ * @internal
15
+ *
16
+ * Resolves the set of ignored columns for a table from the `ignoredColumns`
17
+ * declarations of every model backed by that table, validating the
18
+ * declarations along the way. Called while `sync` builds the generated types
19
+ * files (the declarations have no runtime behavior; see the `ignoredColumns`
20
+ * getter on Dream), so each of these guards fails the sync command loudly
21
+ * rather than surfacing as broken behavior at runtime:
22
+ *
23
+ * - ignored columns must be declared in camelCase, since generated column
24
+ * names are camelized, so any other shape can never match a generated
25
+ * column and would be silently inert
26
+ * - a model may never ignore its primary key
27
+ * - an STI model may never ignore the STI "type" column
28
+ * - a model may never ignore an @Sortable position field or a plain-column
29
+ * @Sortable scope, the backing column of an @Encrypted property, or (on a
30
+ * SoftDelete model) its deletedAtField — the framework reads and writes
31
+ * those columns by name
32
+ * - models sharing a table must agree on their ignored columns, since there
33
+ * is only one generated schema per table (STI children inherit the base
34
+ * model's getter, so agreement is automatic unless a child overrides it)
35
+ * - no association anywhere in the app may name an ignored column as the
36
+ * foreign key (or polymorphic type field) it reads and writes on this
37
+ * table — a BelongsTo on a model backed by this table names a foreign key
38
+ * on this table, and so does a HasMany/HasOne on any other model that
39
+ * points at a model backed by this table
40
+ * - no association anywhere in the app may name an ignored column as its
41
+ * primaryKeyOverride — the column the association's foreign key points
42
+ * at, which lives on the associated model's table for a BelongsTo and on
43
+ * the declaring model's own table for a HasMany/HasOne
44
+ *
45
+ * @param models - every model backed by the table
46
+ * @param tableName - the table whose ignored columns are being resolved
47
+ * @param allModels - every model in the app (on the table's connection);
48
+ * required because associations declared on other models can name foreign
49
+ * keys on this table
50
+ * @returns the set of column names to omit from the table's generated types
51
+ */
52
+ export default function resolveIgnoredColumns(models, tableName, allModels) {
53
+ models.forEach(modelClass => {
54
+ const ignoredColumns = modelClass.prototype.ignoredColumns;
55
+ ignoredColumns.forEach(columnName => {
56
+ if (camelize(columnName) !== columnName)
57
+ throw new IgnoredColumnMustBeCamelCase(modelClass, columnName);
58
+ });
59
+ if (ignoredColumns.includes(modelClass.primaryKey))
60
+ throw new CannotIgnorePrimaryKey(modelClass);
61
+ if (ignoredColumns.includes('type') && (modelClass['isSTIBase'] || modelClass['isSTIChild']))
62
+ throw new CannotIgnoreStiTypeColumn(modelClass);
63
+ modelClass['sortableFields'].forEach(sortableFieldConfig => {
64
+ if (ignoredColumns.includes(sortableFieldConfig.positionField))
65
+ throw new CannotIgnoreSortablePositionColumn(modelClass, sortableFieldConfig.positionField);
66
+ // a Sortable scope entry names either a column or a BelongsTo
67
+ // association on the model (getColumnForSortableScope resolves columns
68
+ // first, then falls back to association metadata), so an ignored scope
69
+ // entry only breaks @Sortable when no association answers to the name
70
+ scopeArray(sortableFieldConfig.scope).forEach(scopeEntry => {
71
+ if (ignoredColumns.includes(scopeEntry) &&
72
+ modelClass['associationMetadataMap']()[scopeEntry] === undefined)
73
+ throw new CannotIgnoreSortableScopeColumn(modelClass, scopeEntry, sortableFieldConfig.positionField);
74
+ });
75
+ });
76
+ // the Encrypted decorator records each backing column it manages in
77
+ // explicitUnsafeParamColumns (which nothing else populates)
78
+ modelClass['explicitUnsafeParamColumns'].forEach(columnName => {
79
+ if (ignoredColumns.includes(columnName))
80
+ throw new CannotIgnoreEncryptedColumn(modelClass, columnName);
81
+ });
82
+ if (modelClass['softDelete']) {
83
+ const deletedAtField = modelClass.prototype['_deletedAtField'];
84
+ if (ignoredColumns.includes(deletedAtField))
85
+ throw new CannotIgnoreSoftDeleteColumn(modelClass, deletedAtField);
86
+ }
87
+ });
88
+ const distinctDeclarations = uniq(models.map(modelClass => JSON.stringify(uniq([...modelClass.prototype.ignoredColumns]).sort())));
89
+ if (distinctDeclarations.length > 1)
90
+ throw new ConflictingIgnoredColumns(tableName, models);
91
+ const ignoredColumns = new Set(models.flatMap(modelClass => [...modelClass.prototype.ignoredColumns]));
92
+ if (ignoredColumns.size)
93
+ guardAssociationColumns(ignoredColumns, tableName, allModels);
94
+ return ignoredColumns;
95
+ }
96
+ /**
97
+ * @internal
98
+ *
99
+ * fails the sync when an ignored column of the table is the foreign key (or
100
+ * polymorphic type field) that any association in the app reads and writes
101
+ * on this table, or the primaryKeyOverride column that any association's
102
+ * foreign key points at on this table. A BelongsTo association names a
103
+ * foreign key on the declaring model's own table and a primaryKeyOverride
104
+ * on the associated model's table; a HasMany/HasOne association names a
105
+ * foreign key on the associated model's table and a primaryKeyOverride on
106
+ * the declaring model's own table — so the load-bearing association may be
107
+ * declared on any model in the app, not just the models backed by this
108
+ * table.
109
+ */
110
+ function guardAssociationColumns(ignoredColumns, tableName, allModels) {
111
+ for (const modelClass of allModels) {
112
+ for (const associationName of modelClass.associationNames) {
113
+ const associationMetaData = modelClass['associationMetadataMap']()[associationName];
114
+ if (associationMetaData === undefined)
115
+ continue;
116
+ // a through association names no foreign key of its own; the source
117
+ // association it travels through is guarded directly
118
+ if (associationMetaData.through)
119
+ continue;
120
+ // unlike foreignKey(), primaryKeyOverride is a plain string on the
121
+ // association statement, so it can be guarded without consulting the
122
+ // generated schema — before the foreignKey-specific short-circuits below
123
+ const primaryKeyOverride = associationMetaData.primaryKeyOverride;
124
+ if (primaryKeyOverride &&
125
+ ignoredColumns.has(primaryKeyOverride) &&
126
+ primaryKeyOverrideTableForAssociationMatches(associationMetaData, modelClass, tableName))
127
+ throw new CannotIgnoreAssociationColumn(tableName, primaryKeyOverride, modelClass, associationMetaData, 'primary key override');
128
+ if (!foreignKeyTableForAssociationMatches(associationMetaData, modelClass, tableName))
129
+ continue;
130
+ // NOTE
131
+ // this try-catch mirrors getAssociationData in ASTConnectionBuilder:
132
+ // computing foreignKey() introspects columns via the generated schema
133
+ // file, so it may throw — on the first sync pass because the schema
134
+ // file has not been regenerated yet, and on the second pass because
135
+ // the regenerated schema omits the ignored column. So in orderings
136
+ // where foreignKey() throws on the first pass, it throws on the second
137
+ // pass too, and these guards never see the association's foreign key;
138
+ // the InvalidComputedForeignKey / ExplicitForeignKeyRequired errors
139
+ // swallowed here resurface at runtime as the backstop.
140
+ let foreignKey = null;
141
+ let foreignKeyTypeColumn = null;
142
+ try {
143
+ foreignKey = associationMetaData.foreignKey();
144
+ foreignKeyTypeColumn = associationMetaData.polymorphic
145
+ ? associationMetaData.foreignKeyTypeField()
146
+ : null;
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ if (ignoredColumns.has(foreignKey))
152
+ throw new CannotIgnoreAssociationColumn(tableName, foreignKey, modelClass, associationMetaData, 'foreign key');
153
+ if (foreignKeyTypeColumn && ignoredColumns.has(foreignKeyTypeColumn))
154
+ throw new CannotIgnoreAssociationColumn(tableName, foreignKeyTypeColumn, modelClass, associationMetaData, 'polymorphic type field');
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * @internal
160
+ *
161
+ * returns whether the given association's foreign key physically lives on
162
+ * the given table: on the declaring model's own table for a BelongsTo, and
163
+ * on the associated model's table for a HasMany/HasOne
164
+ */
165
+ function foreignKeyTableForAssociationMatches(associationMetaData, modelClass, tableName) {
166
+ if (associationMetaData.type === 'BelongsTo')
167
+ return modelClass.table === tableName;
168
+ const dreamClassOrClasses = associationMetaData.modelCB();
169
+ // a missing associated class raises FailedToIdentifyAssociation while the
170
+ // builder gathers association data; it is not this guard's concern
171
+ if (!dreamClassOrClasses)
172
+ return false;
173
+ return Array.isArray(dreamClassOrClasses)
174
+ ? dreamClassOrClasses.some(dreamClass => dreamClass.table === tableName)
175
+ : dreamClassOrClasses.table === tableName;
176
+ }
177
+ /**
178
+ * @internal
179
+ *
180
+ * returns whether the given association's primaryKeyOverride column
181
+ * physically lives on the given table — the opposite side from the foreign
182
+ * key (see associationPrimaryKeyAccessors in
183
+ * decorators/field/association/shared.ts): on the associated model's table
184
+ * for a BelongsTo, and on the declaring model's own table for a
185
+ * HasMany/HasOne
186
+ */
187
+ function primaryKeyOverrideTableForAssociationMatches(associationMetaData, modelClass, tableName) {
188
+ if (associationMetaData.type !== 'BelongsTo')
189
+ return modelClass.table === tableName;
190
+ const dreamClassOrClasses = associationMetaData.modelCB();
191
+ // a missing associated class raises FailedToIdentifyAssociation while the
192
+ // builder gathers association data; it is not this guard's concern
193
+ if (!dreamClassOrClasses)
194
+ return false;
195
+ return Array.isArray(dreamClassOrClasses)
196
+ ? dreamClassOrClasses.some(dreamClass => dreamClass.table === tableName)
197
+ : dreamClassOrClasses.table === tableName;
198
+ }
@@ -1120,6 +1120,12 @@ export default class Dream {
1120
1120
  * 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinPreload('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.preload('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
1121
1121
  * 4. the individual query becomes more complex the more associations are included
1122
1122
  * 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once
1123
+ * 6. leftJoinPreload must enumerate every compiled column of every joined
1124
+ * model, so unlike base-model reads, `preload`/`load`, and saves, it does
1125
+ * not tolerate schema/image skew from an unplanned column drop during a
1126
+ * rolling deploy; see {@link Query.leftJoinPreload} and the
1127
+ * `ignoredColumns` getter for the two-deploy process that makes a
1128
+ * planned drop safe.
1123
1129
  *
1124
1130
  * ```ts
1125
1131
  * const user = await User.leftJoinPreload('posts', 'comments', { visibilty: 'public' }, 'replies').first()
@@ -1949,6 +1955,81 @@ export default class Dream {
1949
1955
  * @returns The table name for this model
1950
1956
  */
1951
1957
  get table(): AssociationTableNames<any, any>;
1958
+ /**
1959
+ * Columns that Dream should behave as though they do not exist.
1960
+ *
1961
+ * Declaring a column ignored removes it from the generated types the next
1962
+ * time `sync` runs: it is omitted from both the db types file (the Kysely
1963
+ * `DB` interface) and the dream schema file, so it disappears from
1964
+ * `columns()` and from every place that flows from `columns()` — select
1965
+ * lists built for `preload`/`load` and `leftJoinPreload`, save hydration,
1966
+ * attribute definition, and param safety. References to the column in
1967
+ * application code become type errors, which is the point: they must be
1968
+ * removed before the column can be dropped.
1969
+ *
1970
+ * This enables safely dropping a column under rolling deploys — the same
1971
+ * problem Rails solves with `ignored_columns`. The safety requirement is
1972
+ * that no image that can run against the post-drop schema names the
1973
+ * column in any SQL it generates. To satisfy it: remove all application
1974
+ * code that uses the column, declare it here, and run `sync`; then let
1975
+ * the drop migration run only once no image lacking the declaration can
1976
+ * run against the database — whether the migration ships in a later
1977
+ * deploy of its own, or together with the declaration in a pipeline that
1978
+ * runs migrations only after the new images have rolled out. Once the
1979
+ * column is dropped, remove this declaration and resync.
1980
+ *
1981
+ * Precondition: as soon as an image built with this declaration runs,
1982
+ * the column is never placed in INSERT column lists, so before that
1983
+ * image can run, the column must be nullable or carry a database
1984
+ * default — a live `NOT NULL` column without a default fails every
1985
+ * create against the table.
1986
+ *
1987
+ * Dropping the column while an image that names it can still run leaves
1988
+ * a window during which those containers (including a rolled-back image)
1989
+ * fail with `42703 column does not exist` — `leftJoinPreload` in
1990
+ * particular has no runtime tolerance for this, since it must enumerate
1991
+ * aliased columns.
1992
+ *
1993
+ * This is a mechanism for the drop window, not for permanently hiding
1994
+ * wide columns: the ignored column is still transferred from the
1995
+ * database on every `RETURNING *` / `select *` until it is actually
1996
+ * dropped.
1997
+ *
1998
+ * The declaration is read only while `sync` generates the types files; it
1999
+ * has no runtime behavior of its own. A declared-but-not-synced model is
2000
+ * therefore not yet protected — CI should verify that `sync` produces no
2001
+ * diff. `sync` will fail loudly if a declared name is not camelCase
2002
+ * (generated column names are camelized, so any other shape could never
2003
+ * match and would be silently inert), if models sharing a table declare
2004
+ * different ignored columns, or if a model attempts to ignore a column
2005
+ * the framework itself reads and writes by name: its primary key, an STI
2006
+ * model's `type` column, any association's foreign key, polymorphic type
2007
+ * field, or `primaryKeyOverride` column, an `@Sortable` position field or
2008
+ * plain-column `@Sortable` scope, an `@Encrypted` backing column, or a
2009
+ * SoftDelete model's `deletedAt` column.
2010
+ *
2011
+ * Because an ignored column vanishes from `columns()`, runtime access via
2012
+ * type escape hatches behaves exactly like any unknown attribute: reads
2013
+ * return `undefined`, and writes assign a plain instance property that is
2014
+ * never persisted.
2015
+ *
2016
+ * ```ts
2017
+ * class User extends ApplicationModel {
2018
+ * public override get ignoredColumns() {
2019
+ * return ['legacyEmail'] as const
2020
+ * }
2021
+ * }
2022
+ * ```
2023
+ *
2024
+ * NOTE: this getter is intentionally typed as `readonly string[]` rather
2025
+ * than as a union of known column names: during the deploy that declares
2026
+ * a column ignored, the regenerated types no longer contain the column,
2027
+ * so a column-name-derived type would reject the very declaration that
2028
+ * removed it.
2029
+ *
2030
+ * @returns The list of column names this model should ignore
2031
+ */
2032
+ get ignoredColumns(): readonly string[];
1952
2033
  /**
1953
2034
  * @internal
1954
2035
  *
@@ -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
  /**
@@ -308,6 +308,17 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
308
308
  * 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinPreload('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.preload('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
309
309
  * 4. the individual query becomes more complex the more associations are included
310
310
  * 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once
311
+ * 6. unlike base-model reads, `preload`/`load`, and saves — which tolerate
312
+ * schema/image skew (e.g. a rolling deploy dropping a column while
313
+ * containers compiled against the previous schema are still draining) —
314
+ * leftJoinPreload must enumerate every compiled column of every joined
315
+ * model under per-alias names (per-alias `*` is not expressible in a
316
+ * single flat row), so an **unplanned** column drop breaks
317
+ * leftJoinPreload queries for the duration of the rollout window. A
318
+ * **planned** drop is safe when performed via the two-deploy process
319
+ * documented on the `ignoredColumns` getter of Dream: declaring the
320
+ * column ignored removes it from the generated schema, so leftJoinPreload
321
+ * stops naming it a full deploy before the column is actually dropped.
311
322
  *
312
323
  *
313
324
  * ```ts
@@ -332,6 +332,27 @@ export default class KyselyQueryDriver<DreamInstance extends Dream> extends Quer
332
332
  * is provided as a second argument, it will use that transaction
333
333
  * to encapsulate the persisting of the dream, as well as any
334
334
  * subsequent model hooks that are fired.
335
+ *
336
+ * `RETURNING *` (rather than enumerating the compiled column list) keeps
337
+ * writes working under schema/image skew: during a rolling deploy, a
338
+ * container built before a drop-column migration would otherwise name the
339
+ * dropped column in `RETURNING` and fail with `42703 column does not
340
+ * exist` on every write, even writes that never touch that column. The
341
+ * `SET`/`VALUES` half still names only dirty attributes, so a write that
342
+ * actually sets a dropped column still fails loudly. The returned row is
343
+ * filtered to the compiled column list before hydration (see
344
+ * internal/saveDream.ts), so a column the image doesn't know about never
345
+ * reaches `setAttributes`.
346
+ *
347
+ * KNOWN CONSTRAINT: star-selects are only safe because nothing in this
348
+ * stack uses named prepared statements — node-postgres prepares a
349
+ * statement only when given an explicit `name`, and Kysely never names
350
+ * them, so every query is re-planned. With named prepared statements, a
351
+ * concurrent `ADD COLUMN` changes a cached plan's result shape and
352
+ * Postgres raises `cached plan must not change result type` (the reason
353
+ * Rails added `enumerate_columns_in_select_statements`). If a future
354
+ * driver or pooling layer enables named prepared statements, revisit
355
+ * every `RETURNING *` / `select *` in this driver.
335
356
  */
336
357
  static saveDream(dream: Dream, txn?: DreamTransaction<Dream> | null): Promise<any>;
337
358
  dbConnectionType(sqlCommandType: SqlCommandType): DbConnectionType;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * Returns an object containing only the keys of `row` that are columns
5
+ * the compiled schema knows about (per the provided column set).
6
+ *
7
+ * Under schema/image skew (e.g. a rolling deploy in which a migration adds
8
+ * a column while containers built against the previous schema are still
9
+ * draining, or application code is rolled back after an add-column
10
+ * migration), a `RETURNING *` / `select *` row can include columns this
11
+ * build has never heard of. Passing such keys to `setAttributes` would
12
+ * assign them as plain properties — invoking a same-named user-defined
13
+ * setter, or throwing on a getter-only property — so they must be dropped
14
+ * before hydration.
15
+ *
16
+ * Keys are intersected rather than enumerated from the column set so that
17
+ * a column missing from the row (the dropped-column direction of skew)
18
+ * simply does not appear, rather than appearing with an `undefined` value.
19
+ *
20
+ * Identity fast path: when every key of `row` is a known column (the
21
+ * no-skew steady state), `row` itself is returned unchanged, without
22
+ * allocating a copy; a filtered copy is built only when at least one
23
+ * unknown key is present.
24
+ *
25
+ * @param row - a raw database row
26
+ * @param columns - the compiled column set for the Dream class being hydrated
27
+ * @returns `row` itself when every key is a known column; otherwise a new
28
+ * object containing only the known-column entries of `row`
29
+ */
30
+ export default function filterRowToKnownColumns(row: Record<string, any>, columns: Set<string>): Record<string, any>;
@@ -0,0 +1,6 @@
1
+ export default class NoColumnsToAlterMigration extends Error {
2
+ table: string;
3
+ alterDirection: 'add' | 'remove';
4
+ constructor(table: string, alterDirection: 'add' | 'remove');
5
+ get message(): string;
6
+ }
@@ -0,0 +1,5 @@
1
+ export default class UnparseableMigrationColumn extends Error {
2
+ declaration: string;
3
+ constructor(declaration: string);
4
+ get message(): string;
5
+ }
@@ -0,0 +1,12 @@
1
+ import Dream from '../../Dream.js';
2
+ import { AssociationStatement } from '../../types/associations/shared.js';
3
+ export default class CannotIgnoreAssociationColumn extends Error {
4
+ private tableName;
5
+ private columnName;
6
+ private modelClass;
7
+ private association;
8
+ private columnRole;
9
+ constructor(tableName: string, columnName: string, modelClass: typeof Dream, association: AssociationStatement, columnRole: 'foreign key' | 'polymorphic type field' | 'primary key override');
10
+ get message(): string;
11
+ private get consequence();
12
+ }
@@ -0,0 +1,7 @@
1
+ import Dream from '../../Dream.js';
2
+ export default class CannotIgnoreEncryptedColumn extends Error {
3
+ private modelClass;
4
+ private columnName;
5
+ constructor(modelClass: typeof Dream, columnName: string);
6
+ get message(): string;
7
+ }
@@ -0,0 +1,6 @@
1
+ import Dream from '../../Dream.js';
2
+ export default class CannotIgnorePrimaryKey extends Error {
3
+ private modelClass;
4
+ constructor(modelClass: typeof Dream);
5
+ get message(): string;
6
+ }
@@ -0,0 +1,7 @@
1
+ import Dream from '../../Dream.js';
2
+ export default class CannotIgnoreSoftDeleteColumn extends Error {
3
+ private modelClass;
4
+ private columnName;
5
+ constructor(modelClass: typeof Dream, columnName: string);
6
+ get message(): string;
7
+ }
@@ -0,0 +1,7 @@
1
+ import Dream from '../../Dream.js';
2
+ export default class CannotIgnoreSortablePositionColumn extends Error {
3
+ private modelClass;
4
+ private columnName;
5
+ constructor(modelClass: typeof Dream, columnName: string);
6
+ get message(): string;
7
+ }