@rvoh/dream 2.18.1 → 2.20.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 (265) hide show
  1. package/dist/cjs/src/Dream.js +186 -0
  2. package/dist/cjs/src/db/DreamDbConnection.js +6 -0
  3. package/dist/cjs/src/decorators/class/SoftDelete.js +12 -0
  4. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +113 -0
  5. package/dist/cjs/src/dream/Query.js +148 -0
  6. package/dist/cjs/src/dream/QueryDriver/Base.js +84 -0
  7. package/dist/cjs/src/dream/QueryDriver/Kysely.js +420 -81
  8. package/dist/cjs/src/dream/internal/filterRowToKnownColumns.js +41 -0
  9. package/dist/cjs/src/dream/internal/saveDream.js +6 -1
  10. package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  11. package/dist/cjs/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  12. package/dist/cjs/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  13. package/dist/cjs/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  14. package/dist/cjs/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  15. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  16. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  17. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  18. package/dist/cjs/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  19. package/dist/cjs/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  20. package/dist/cjs/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  21. package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  22. package/dist/cjs/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  23. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +21 -13
  24. package/dist/cjs/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  25. package/dist/esm/src/Dream.js +186 -0
  26. package/dist/esm/src/db/DreamDbConnection.js +6 -0
  27. package/dist/esm/src/decorators/class/SoftDelete.js +12 -0
  28. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +113 -0
  29. package/dist/esm/src/dream/Query.js +148 -0
  30. package/dist/esm/src/dream/QueryDriver/Base.js +84 -0
  31. package/dist/esm/src/dream/QueryDriver/Kysely.js +420 -81
  32. package/dist/esm/src/dream/internal/filterRowToKnownColumns.js +41 -0
  33. package/dist/esm/src/dream/internal/saveDream.js +6 -1
  34. package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  35. package/dist/esm/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  36. package/dist/esm/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  37. package/dist/esm/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  38. package/dist/esm/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  39. package/dist/esm/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  40. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  41. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  42. package/dist/esm/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  43. package/dist/esm/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  44. package/dist/esm/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  45. package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  46. package/dist/esm/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  47. package/dist/esm/src/helpers/cli/generateMigrationContent.js +21 -13
  48. package/dist/esm/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  49. package/dist/types/src/Dream.d.ts +178 -4
  50. package/dist/types/src/decorators/class/SoftDelete.d.ts +12 -0
  51. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +103 -0
  52. package/dist/types/src/dream/Query.d.ts +138 -0
  53. package/dist/types/src/dream/QueryDriver/Base.d.ts +69 -0
  54. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +187 -8
  55. package/dist/types/src/dream/internal/filterRowToKnownColumns.d.ts +30 -0
  56. package/dist/types/src/errors/CannotNamespaceAssociationFilterToAnotherTable.d.ts +8 -0
  57. package/dist/types/src/errors/schema-builder/CannotIgnoreAssociationColumn.d.ts +12 -0
  58. package/dist/types/src/errors/schema-builder/CannotIgnoreEncryptedColumn.d.ts +7 -0
  59. package/dist/types/src/errors/schema-builder/CannotIgnorePrimaryKey.d.ts +6 -0
  60. package/dist/types/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.d.ts +7 -0
  61. package/dist/types/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.d.ts +7 -0
  62. package/dist/types/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.d.ts +8 -0
  63. package/dist/types/src/errors/schema-builder/CannotIgnoreStiTypeColumn.d.ts +6 -0
  64. package/dist/types/src/errors/schema-builder/ConflictingIgnoredColumns.d.ts +7 -0
  65. package/dist/types/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.d.ts +7 -0
  66. package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +20 -0
  67. package/dist/types/src/helpers/cli/ASTKyselyCodegenEnhancer.d.ts +13 -0
  68. package/dist/types/src/helpers/cli/resolveIgnoredColumns.d.ts +41 -0
  69. package/dist/types/src/types/associations/shared.d.ts +4 -1
  70. package/dist/types/src/types/dream.d.ts +17 -0
  71. package/dist/types/src/types/types/associations/shared.ts +10 -1
  72. package/dist/types/src/types/types/dream.ts +53 -0
  73. package/dist/types/src/types/types/variadic.ts +179 -140
  74. package/dist/types/src/types/variadic.d.ts +15 -10
  75. package/docs/assets/hierarchy.js +1 -1
  76. package/docs/assets/search.js +1 -1
  77. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  78. package/docs/classes/db.KyselyQueryDriver.html +96 -34
  79. package/docs/classes/db.PostgresQueryDriver.html +97 -35
  80. package/docs/classes/db.QueryDriverBase.html +77 -33
  81. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  82. package/docs/classes/errors.ColumnOverflow.html +3 -3
  83. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  84. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  85. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  86. package/docs/classes/errors.DecryptionError.html +2 -2
  87. package/docs/classes/errors.DecryptionParseError.html +2 -2
  88. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  89. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  90. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  91. package/docs/classes/errors.InvalidClockTime.html +2 -2
  92. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  93. package/docs/classes/errors.InvalidDateTime.html +2 -2
  94. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  95. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  96. package/docs/classes/errors.NotNullViolation.html +3 -3
  97. package/docs/classes/errors.RecordNotFound.html +3 -3
  98. package/docs/classes/errors.ValidationError.html +3 -3
  99. package/docs/classes/index.CalendarDate.html +33 -33
  100. package/docs/classes/index.ClockTime.html +32 -32
  101. package/docs/classes/index.ClockTimeTz.html +35 -35
  102. package/docs/classes/index.DateTime.html +86 -86
  103. package/docs/classes/index.Decorators.html +19 -19
  104. package/docs/classes/index.Dream.html +247 -119
  105. package/docs/classes/index.DreamApp.html +10 -10
  106. package/docs/classes/index.DreamTransaction.html +2 -2
  107. package/docs/classes/index.Env.html +2 -2
  108. package/docs/classes/index.Query.html +155 -57
  109. package/docs/classes/system.CliFileWriter.html +4 -4
  110. package/docs/classes/system.DreamBin.html +2 -2
  111. package/docs/classes/system.DreamCLI.html +7 -7
  112. package/docs/classes/system.DreamImporter.html +2 -2
  113. package/docs/classes/system.DreamLogos.html +2 -2
  114. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  115. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  116. package/docs/classes/system.PathHelpers.html +3 -3
  117. package/docs/classes/utils.Encrypt.html +3 -3
  118. package/docs/classes/utils.Range.html +2 -2
  119. package/docs/functions/db.closeAllDbConnections.html +1 -1
  120. package/docs/functions/db.dreamDbConnections.html +1 -1
  121. package/docs/functions/db.untypedDb.html +1 -1
  122. package/docs/functions/db.validateColumn.html +1 -1
  123. package/docs/functions/db.validateTable.html +1 -1
  124. package/docs/functions/errors.pgErrorType.html +1 -1
  125. package/docs/functions/index.DreamSerializer.html +1 -1
  126. package/docs/functions/index.ObjectSerializer.html +1 -1
  127. package/docs/functions/index.ReplicaSafe.html +1 -1
  128. package/docs/functions/index.STI.html +1 -1
  129. package/docs/functions/index.SoftDelete.html +12 -1
  130. package/docs/functions/utils.camelize.html +1 -1
  131. package/docs/functions/utils.capitalize.html +1 -1
  132. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  133. package/docs/functions/utils.compact.html +1 -1
  134. package/docs/functions/utils.groupBy.html +1 -1
  135. package/docs/functions/utils.hyphenize.html +1 -1
  136. package/docs/functions/utils.intersection.html +1 -1
  137. package/docs/functions/utils.isEmpty.html +1 -1
  138. package/docs/functions/utils.normalizeUnicode.html +1 -1
  139. package/docs/functions/utils.pascalize.html +1 -1
  140. package/docs/functions/utils.percent.html +1 -1
  141. package/docs/functions/utils.range.html +1 -1
  142. package/docs/functions/utils.round.html +1 -1
  143. package/docs/functions/utils.sanitizeString.html +1 -1
  144. package/docs/functions/utils.snakeify.html +1 -1
  145. package/docs/functions/utils.sort.html +1 -1
  146. package/docs/functions/utils.sortBy.html +1 -1
  147. package/docs/functions/utils.sortObjectByKey.html +1 -1
  148. package/docs/functions/utils.sortObjectByValue.html +1 -1
  149. package/docs/functions/utils.uncapitalize.html +1 -1
  150. package/docs/functions/utils.uniq.html +1 -1
  151. package/docs/hierarchy.html +1 -1
  152. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  153. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  154. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  155. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  156. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  157. package/docs/interfaces/types.DecoratorContext.html +2 -2
  158. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  159. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  160. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  161. package/docs/interfaces/types.DurationObject.html +2 -2
  162. package/docs/interfaces/types.EncryptOptions.html +2 -2
  163. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  164. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  165. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  166. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  167. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  168. package/docs/types/openapi.OpenapiFormats.html +1 -1
  169. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  170. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  171. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  179. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  180. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  181. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  182. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  183. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  185. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  187. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  189. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  190. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  191. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  192. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  193. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  194. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  195. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  196. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  197. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  198. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  199. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  200. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  201. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  202. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  203. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  204. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  205. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  206. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  207. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  208. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  209. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  210. package/docs/types/types.CalendarDateObject.html +1 -1
  211. package/docs/types/types.Camelized.html +1 -1
  212. package/docs/types/types.ClockTimeObject.html +1 -1
  213. package/docs/types/types.DbConnectionType.html +1 -1
  214. package/docs/types/types.DbTypes.html +1 -1
  215. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  216. package/docs/types/types.DreamAttributes.html +1 -1
  217. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  218. package/docs/types/types.DreamClassColumn.html +1 -1
  219. package/docs/types/types.DreamColumn.html +1 -1
  220. package/docs/types/types.DreamColumnNames.html +1 -1
  221. package/docs/types/types.DreamLogLevel.html +1 -1
  222. package/docs/types/types.DreamLogger.html +2 -2
  223. package/docs/types/types.DreamModelSerializerType.html +1 -1
  224. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  225. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  226. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  227. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  228. package/docs/types/types.DreamSerializable.html +1 -1
  229. package/docs/types/types.DreamSerializableArray.html +1 -1
  230. package/docs/types/types.DreamSerializerKey.html +1 -1
  231. package/docs/types/types.DreamSerializers.html +1 -1
  232. package/docs/types/types.DreamVirtualColumns.html +1 -1
  233. package/docs/types/types.DurationUnit.html +1 -1
  234. package/docs/types/types.EncryptAlgorithm.html +1 -1
  235. package/docs/types/types.HasManyStatement.html +1 -1
  236. package/docs/types/types.HasOneStatement.html +1 -1
  237. package/docs/types/types.Hyphenized.html +1 -1
  238. package/docs/types/types.Pascalized.html +1 -1
  239. package/docs/types/types.PrimaryKeyType.html +1 -1
  240. package/docs/types/types.RoundingPrecision.html +1 -1
  241. package/docs/types/types.SerializerCasing.html +1 -1
  242. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  243. package/docs/types/types.Snakeified.html +1 -1
  244. package/docs/types/types.StrictInterface.html +1 -1
  245. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  246. package/docs/types/types.UpdateableProperties.html +1 -1
  247. package/docs/types/types.ValidationType.html +1 -1
  248. package/docs/types/types.ViewModel.html +2 -2
  249. package/docs/types/types.ViewModelClass.html +1 -1
  250. package/docs/types/types.WeekdayName.html +1 -1
  251. package/docs/types/types.WhereStatementForDream.html +1 -1
  252. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  253. package/docs/variables/index.DreamConst.html +1 -1
  254. package/docs/variables/index.ops.html +1 -1
  255. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  256. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  257. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  258. package/docs/variables/system.primaryKeyTypes.html +1 -1
  259. package/package.json +3 -3
  260. package/dist/cjs/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  261. package/dist/cjs/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  262. package/dist/esm/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  263. package/dist/esm/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  264. package/dist/types/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.d.ts +0 -13
  265. package/dist/types/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.d.ts +0 -12
@@ -26,8 +26,8 @@ import MissingRequiredAssociationAndClause from '../../errors/associations/Missi
26
26
  import MissingRequiredPassthroughForAssociationAndClause from '../../errors/associations/MissingRequiredPassthroughForAssociationAndClause.js';
27
27
  import MissingThroughAssociation from '../../errors/associations/MissingThroughAssociation.js';
28
28
  import MissingThroughAssociationSource from '../../errors/associations/MissingThroughAssociationSource.js';
29
- import ThroughAssociationConditionsIncompatibleWithThroughAssociationSource from '../../errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js';
30
29
  import CannotNegateSimilarityClause from '../../errors/CannotNegateSimilarityClause.js';
30
+ import CannotNamespaceAssociationFilterToAnotherTable from '../../errors/CannotNamespaceAssociationFilterToAnotherTable.js';
31
31
  import CannotPassUndefinedAsAValueToAWhereClause from '../../errors/CannotPassUndefinedAsAValueToAWhereClause.js';
32
32
  import CheckConstraintViolation from '../../errors/db/CheckConstraintViolation.js';
33
33
  import ColumnOverflow from '../../errors/db/ColumnOverflow.js';
@@ -55,7 +55,6 @@ import CurriedOpsStatement from '../../ops/curried-ops-statement.js';
55
55
  import OpsStatement from '../../ops/ops-statement.js';
56
56
  import { DreamConst, primaryKeyTypes } from '../constants.js';
57
57
  import DreamTransaction from '../DreamTransaction.js';
58
- import throughAssociationHasOptionsBesidesThroughAndSource from '../internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js';
59
58
  import associationStringToNameAndAlias from '../internal/associationStringToNameAndAlias.js';
60
59
  import executeDatabaseQuery from '../internal/executeDatabaseQuery.js';
61
60
  import extractAssignableAssociationAttributes from '../internal/extractAssignableAssociationAttributes.js';
@@ -213,6 +212,10 @@ export default class KyselyQueryDriver extends QueryDriverBase {
213
212
  await DreamCLI.logger.logProgress('[dream] sync failed, reverting file contents...', async () => {
214
213
  await CliFileWriter.revert();
215
214
  });
215
+ // rethrow after reverting so callers (e.g. the `psy sync` and
216
+ // `db:migrate` CLI commands) exit nonzero instead of reporting success
217
+ // with stale generated types
218
+ throw error;
216
219
  }
217
220
  }
218
221
  static get syncDialect() {
@@ -441,6 +444,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
441
444
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
442
445
  return data.max;
443
446
  }
447
+ /**
448
+ * Retrieves the max value of the specified column within each group,
449
+ * keyed by the value of the provided group column.
450
+ *
451
+ * ```ts
452
+ * await CompositionAsset.query().maxBy('name', 'score')
453
+ * // Map(2) { 'primary' => 9, 'secondary' => 4 }
454
+ * ```
455
+ *
456
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
457
+ * @param aggregatedColumn - the column to take the max of within each group
458
+ * @returns A Map from each present group value to the max of the aggregated column in that group
459
+ */
460
+ async maxBy(groupColumn, aggregatedColumn) {
461
+ // eslint-disable-next-line @typescript-eslint/unbound-method
462
+ const { max } = this.dbFor('select').fn;
463
+ return this.groupedAggregate(groupColumn, max(aggregatedColumn));
464
+ }
444
465
  /**
445
466
  * Retrieves the min value of the specified column
446
467
  * for this Query
@@ -464,6 +485,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
464
485
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
465
486
  return data.min;
466
487
  }
488
+ /**
489
+ * Retrieves the min value of the specified column within each group,
490
+ * keyed by the value of the provided group column.
491
+ *
492
+ * ```ts
493
+ * await CompositionAsset.query().minBy('name', 'score')
494
+ * // Map(2) { 'primary' => 1, 'secondary' => 4 }
495
+ * ```
496
+ *
497
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
498
+ * @param aggregatedColumn - the column to take the min of within each group
499
+ * @returns A Map from each present group value to the min of the aggregated column in that group
500
+ */
501
+ async minBy(groupColumn, aggregatedColumn) {
502
+ // eslint-disable-next-line @typescript-eslint/unbound-method
503
+ const { min } = this.dbFor('select').fn;
504
+ return this.groupedAggregate(groupColumn, min(aggregatedColumn));
505
+ }
467
506
  /**
468
507
  * Retrieves the sum value of the specified column
469
508
  * for this Query
@@ -487,6 +526,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
487
526
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
488
527
  return data.sum === null ? null : parseFloat(data.sum);
489
528
  }
529
+ /**
530
+ * Retrieves the sum of the specified column within each group,
531
+ * keyed by the value of the provided group column.
532
+ *
533
+ * ```ts
534
+ * await CompositionAsset.query().sumBy('name', 'score')
535
+ * // Map(2) { 'primary' => 10, 'secondary' => 4 }
536
+ * ```
537
+ *
538
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
539
+ * @param aggregatedColumn - the column to sum within each group
540
+ * @returns A Map from each present group value to the sum of the aggregated column in that group
541
+ */
542
+ async sumBy(groupColumn, aggregatedColumn) {
543
+ // eslint-disable-next-line @typescript-eslint/unbound-method
544
+ const { sum } = this.dbFor('select').fn;
545
+ return this.groupedAggregate(groupColumn, sum(aggregatedColumn), rawSum => rawSum === null ? null : parseFloat(rawSum));
546
+ }
490
547
  /**
491
548
  * Retrieves the average value of the specified column
492
549
  * for this Query
@@ -510,6 +567,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
510
567
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
511
568
  return data.avg;
512
569
  }
570
+ /**
571
+ * Retrieves the average of the specified column within each group,
572
+ * keyed by the value of the provided group column.
573
+ *
574
+ * ```ts
575
+ * await CompositionAsset.query().avgBy('name', 'score')
576
+ * // Map(2) { 'primary' => 5, 'secondary' => 4 }
577
+ * ```
578
+ *
579
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
580
+ * @param aggregatedColumn - the column to average within each group
581
+ * @returns A Map from each present group value to the average of the aggregated column in that group
582
+ */
583
+ async avgBy(groupColumn, aggregatedColumn) {
584
+ // eslint-disable-next-line @typescript-eslint/unbound-method
585
+ const { avg } = this.dbFor('select').fn;
586
+ return this.groupedAggregate(groupColumn, avg(aggregatedColumn));
587
+ }
513
588
  /**
514
589
  * Retrieves the number of records in the database
515
590
  *
@@ -535,6 +610,59 @@ export default class KyselyQueryDriver extends QueryDriverBase {
535
610
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
536
611
  return parseInt(data.tablecount.toString());
537
612
  }
613
+ /**
614
+ * Retrieves the number of records in each group, keyed by the
615
+ * value of the provided group column.
616
+ *
617
+ * ```ts
618
+ * await User.query().countBy('name')
619
+ * // Map(2) { 'fred' => 2, 'zed' => 1 }
620
+ * ```
621
+ *
622
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
623
+ * @returns A Map from each present group value to the number of records in that group
624
+ */
625
+ async countBy(groupColumn) {
626
+ // eslint-disable-next-line @typescript-eslint/unbound-method
627
+ const { count } = this.dbFor('select').fn;
628
+ const primaryKeyRef = this.namespaceColumn(this.query.dreamInstance['_primaryKey']);
629
+ return this.groupedAggregate(groupColumn, count(primaryKeyRef), rawCount => parseInt(rawCount.toString()));
630
+ }
631
+ /**
632
+ * @internal
633
+ *
634
+ * Shared plumbing for grouped aggregates (`countBy` and the value-aggregate
635
+ * siblings `minBy` / `maxBy` / `sumBy` / `avgBy`). Selects the group column
636
+ * alongside a single aggregate expression, groups by the group column, executes,
637
+ * and folds the resulting rows into a Map keyed by the group value.
638
+ *
639
+ * The group column and the aggregate are selected under stable, underscore-free
640
+ * aliases so they can be read back off each row without being affected by
641
+ * Kysely's CamelCasePlugin (which would otherwise mangle a namespaced alias),
642
+ * mirroring the short-alias strategy used by `pluck`.
643
+ *
644
+ * @param groupColumn - the column to GROUP BY (base or joined-association-namespaced)
645
+ * @param aggregateExpression - the Kysely aggregate expression to select per group (e.g. `count(pk)`)
646
+ * @param coerceValue - maps each raw aggregate value to the value stored in the returned Map
647
+ * @returns A Map from each present group value to its coerced aggregate value
648
+ */
649
+ async groupedAggregate(groupColumn, aggregateExpression, coerceValue) {
650
+ const groupColumnRef = this.namespaceColumn(groupColumn);
651
+ let kyselyQuery = new this.constructor(this.query).buildSelect({
652
+ bypassSelectAll: true,
653
+ bypassOrder: true,
654
+ });
655
+ kyselyQuery = kyselyQuery
656
+ .select(`${groupColumnRef} as groupvalue`)
657
+ .select(aggregateExpression.as('aggregatevalue'))
658
+ .groupBy(groupColumnRef);
659
+ const rows = await executeDatabaseQuery(kyselyQuery, 'execute');
660
+ const result = new Map();
661
+ for (const row of rows) {
662
+ result.set(row.groupvalue, coerceValue ? coerceValue(row.aggregatevalue) : row.aggregatevalue);
663
+ }
664
+ return result;
665
+ }
538
666
  /**
539
667
  * @internal
540
668
  *
@@ -606,6 +734,27 @@ export default class KyselyQueryDriver extends QueryDriverBase {
606
734
  * is provided as a second argument, it will use that transaction
607
735
  * to encapsulate the persisting of the dream, as well as any
608
736
  * subsequent model hooks that are fired.
737
+ *
738
+ * `RETURNING *` (rather than enumerating the compiled column list) keeps
739
+ * writes working under schema/image skew: during a rolling deploy, a
740
+ * container built before a drop-column migration would otherwise name the
741
+ * dropped column in `RETURNING` and fail with `42703 column does not
742
+ * exist` on every write, even writes that never touch that column. The
743
+ * `SET`/`VALUES` half still names only dirty attributes, so a write that
744
+ * actually sets a dropped column still fails loudly. The returned row is
745
+ * filtered to the compiled column list before hydration (see
746
+ * internal/saveDream.ts), so a column the image doesn't know about never
747
+ * reaches `setAttributes`.
748
+ *
749
+ * KNOWN CONSTRAINT: star-selects are only safe because nothing in this
750
+ * stack uses named prepared statements — node-postgres prepares a
751
+ * statement only when given an explicit `name`, and Kysely never names
752
+ * them, so every query is re-planned. With named prepared statements, a
753
+ * concurrent `ADD COLUMN` changes a cached plan's result shape and
754
+ * Postgres raises `cached plan must not change result type` (the reason
755
+ * Rails added `enumerate_columns_in_select_statements`). If a future
756
+ * driver or pooling layer enables named prepared statements, revisit
757
+ * every `RETURNING *` / `select *` in this driver.
609
758
  */
610
759
  static async saveDream(dream, txn = null) {
611
760
  const connectionName = dream.connectionName || 'default';
@@ -617,13 +766,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
617
766
  .updateTable(dream.table)
618
767
  .set(sqlifiedAttributes)
619
768
  .where(namespaceColumn(dream['_primaryKey'], dream.table), '=', dream.primaryKeyValue());
620
- return await executeDatabaseQuery(query.returning([...dream.columns()]), 'executeTakeFirstOrThrow');
769
+ return await executeDatabaseQuery(query.returningAll(), 'executeTakeFirstOrThrow');
621
770
  }
622
771
  else {
623
772
  const query = db
624
773
  .insertInto(dream.table)
625
774
  .values(sqlifiedAttributes)
626
- .returning([...dream.columns()]);
775
+ .returningAll();
627
776
  return await executeDatabaseQuery(query, 'executeTakeFirstOrThrow');
628
777
  }
629
778
  }
@@ -999,6 +1148,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
999
1148
  }, {}), {
1000
1149
  negate,
1001
1150
  disallowSimilarityOperator,
1151
+ expectedAlias: rootTableOrAssociationAlias,
1002
1152
  });
1003
1153
  }
1004
1154
  buildCommon(kyselyQuery) {
@@ -1041,20 +1191,25 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1041
1191
  kyselyQuery = kyselyQuery.where((eb) => eb.and([
1042
1192
  ...this.aliasWhereStatements(query['whereStatements'], query['baseSqlAlias']).map(whereStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereStatement, {
1043
1193
  disallowSimilarityOperator: false,
1194
+ expectedAlias: query['baseSqlAlias'],
1044
1195
  })),
1045
1196
  ...this.aliasWhereStatements(query['whereNotStatements'], query['baseSqlAlias']).map(whereNotStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereNotStatement, {
1046
1197
  negate: true,
1198
+ expectedAlias: query['baseSqlAlias'],
1047
1199
  })),
1048
- ...query['whereAnyStatements'].map(whereAnyStatements => eb.or(this.aliasWhereStatements(whereAnyStatements, query['baseSqlAlias']).map(whereAnyStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereAnyStatement)))),
1200
+ ...query['whereAnyStatements'].map(whereAnyStatements => eb.or(this.aliasWhereStatements(whereAnyStatements, query['baseSqlAlias']).map(whereAnyStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereAnyStatement, {
1201
+ expectedAlias: query['baseSqlAlias'],
1202
+ })))),
1049
1203
  ]));
1050
1204
  }
1051
1205
  return kyselyQuery;
1052
1206
  }
1053
- whereStatementToExpressionWrapper(dreamClass, eb, whereStatement, { negate = false, disallowSimilarityOperator = true, } = {}) {
1207
+ whereStatementToExpressionWrapper(dreamClass, eb, whereStatement, { negate = false, disallowSimilarityOperator = true, expectedAlias, } = {}) {
1054
1208
  const clauses = compact(Object.keys(whereStatement)
1055
1209
  .filter(key => whereStatement[key] !== DreamConst.required)
1056
1210
  .map(attr => {
1057
1211
  const val = whereStatement[attr];
1212
+ this.validateAssociationFilterAlias(attr, val, expectedAlias);
1058
1213
  if (val?.isOpsStatement &&
1059
1214
  val.shouldBypassWhereStatement) {
1060
1215
  if (disallowSimilarityOperator)
@@ -1063,6 +1218,10 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1063
1218
  // and should be ommited from the where clause directly
1064
1219
  return;
1065
1220
  }
1221
+ if (this.isAssociationInstanceArray(dreamClass, attr, val))
1222
+ return this.associationInstanceArrayToExpressionWrapper(dreamClass, eb, attr, val, {
1223
+ negate,
1224
+ });
1066
1225
  const { a, b, c, a2, b2, c2 } = this.dreamWhereStatementToExpressionBuilderParts(dreamClass, attr, val);
1067
1226
  // postgres is unable to handle WHERE IN statements with blank arrays, such as in
1068
1227
  // "WHERE id IN ()", meaning that:
@@ -1106,7 +1265,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1106
1265
  //
1107
1266
  }
1108
1267
  else if (b === '=' && negate) {
1109
- return eb.and([eb(a, '=', c), eb(a, 'is not', null)]);
1268
+ const conditions = [eb(a, '=', c), eb(a, 'is not', null)];
1269
+ // when a second condition is present (e.g. the type field of a polymorphic
1270
+ // association), it must be included so that the caller negates the full
1271
+ // conjunction rather than the first condition alone
1272
+ if (b2)
1273
+ conditions.push(eb(a2, b2, c2), eb(a2, 'is not', null));
1274
+ return eb.and(conditions);
1110
1275
  //
1111
1276
  }
1112
1277
  else if (b === '!=' && c !== null) {
@@ -1147,6 +1312,95 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1147
1312
  return eb.and([eb(a, 'not in', compactedC), isNotNullStatement]);
1148
1313
  return isNotNullStatement;
1149
1314
  }
1315
+ /**
1316
+ * @internal
1317
+ *
1318
+ * A Dream instance (or array of Dream instances) as a where-clause value is
1319
+ * resolved as an association of the dreamClass whose statement is being
1320
+ * compiled, so a key namespaced to a different table (e.g.
1321
+ * `where({ 'c.user': user })` after `innerJoin('composition as c')`) would
1322
+ * silently resolve the association against the wrong class, emitting invalid
1323
+ * SQL or SQL that filters the wrong table. Such keys are already rejected at
1324
+ * the type level; this guard rejects them at runtime. Un-namespaced keys and
1325
+ * keys namespaced to the alias the statement applies to are unaffected, as
1326
+ * are callers that do not declare an expected alias.
1327
+ */
1328
+ validateAssociationFilterAlias(attr, val, expectedAlias) {
1329
+ if (expectedAlias === undefined)
1330
+ return;
1331
+ const isolatedColumn = maybeNamespacedColumnNameToColumnName(attr);
1332
+ if (isolatedColumn === attr)
1333
+ return;
1334
+ const namespace = attr.slice(0, attr.length - isolatedColumn.length - 1);
1335
+ if (namespace === expectedAlias)
1336
+ return;
1337
+ if (val instanceof Dream ||
1338
+ (Array.isArray(val) && val.length > 0 && val.every(element => element instanceof Dream)))
1339
+ throw new CannotNamespaceAssociationFilterToAnotherTable(this.dreamClass, attr, expectedAlias);
1340
+ }
1341
+ /**
1342
+ * @internal
1343
+ *
1344
+ * An array under an association name can only be an array of Dream instances
1345
+ * (association names are not columns). The every-element check simply avoids
1346
+ * changing the handling of invalid runtime input (e.g. an array of ids under
1347
+ * an association name), which the type system already rejects.
1348
+ */
1349
+ isAssociationInstanceArray(dreamClass, attr, val) {
1350
+ return (Array.isArray(val) &&
1351
+ dreamClass.associationNames.includes(maybeNamespacedColumnNameToColumnName(attr)) &&
1352
+ val.every(element => element instanceof Dream));
1353
+ }
1354
+ /**
1355
+ * @internal
1356
+ *
1357
+ * Expands an array of Dream instances under a BelongsTo association name into
1358
+ * foreign key filtering:
1359
+ * - non-polymorphic: `foreignKey IN (...)`
1360
+ * - polymorphic: instances are grouped by their reference type (STI children
1361
+ * collapse into their base class), each group becoming
1362
+ * `(foreignKey IN (...) AND foreignKeyTypeField = 'TheType')`, with multiple
1363
+ * groups ORed together
1364
+ * - empty array: matches nothing (FALSE); when the caller negates, NOT(FALSE)
1365
+ * matches everything, mirroring empty `in`/`not in` array semantics
1366
+ *
1367
+ * When `negate` is set, IS NOT NULL conditions are included so that the
1368
+ * caller's negation also matches records in which the foreign key (or type
1369
+ * field) is null, consistent with negation of single Dream instances and of
1370
+ * `in` arrays.
1371
+ */
1372
+ associationInstanceArrayToExpressionWrapper(dreamClass, eb, attr, instances, { negate }) {
1373
+ if (instances.length === 0)
1374
+ return sql `FALSE`;
1375
+ const isolatedColumn = maybeNamespacedColumnNameToColumnName(attr);
1376
+ // preserve any table alias prefix from the attribute (e.g. `posts.user` -> `posts.`)
1377
+ // on the columns we compare against
1378
+ const namespacePrefix = attr.slice(0, attr.length - isolatedColumn.length);
1379
+ const association = dreamClass['associationMetadataMap']()[isolatedColumn];
1380
+ const foreignKeyColumn = `${namespacePrefix}${association.foreignKey()}`;
1381
+ const primaryKeyValuesFor = (instances) => instances.map(instance => {
1382
+ const primaryKeyValue = association.primaryKeyValue(instance);
1383
+ if (primaryKeyValue === undefined)
1384
+ throw new CannotPassUndefinedAsAValueToAWhereClause(this.dreamClass, foreignKeyColumn);
1385
+ return primaryKeyValue;
1386
+ });
1387
+ if (!association.polymorphic) {
1388
+ const inClause = eb(foreignKeyColumn, 'in', primaryKeyValuesFor(instances));
1389
+ return negate ? eb.and([inClause, eb(foreignKeyColumn, 'is not', null)]) : inClause;
1390
+ }
1391
+ const typeColumn = `${namespacePrefix}${association.foreignKeyTypeField()}`;
1392
+ const instancesByType = groupBy(instances, instance => instance.referenceTypeString);
1393
+ const typeClauses = Object.keys(instancesByType).map(referenceTypeString => {
1394
+ const conditions = [
1395
+ eb(foreignKeyColumn, 'in', primaryKeyValuesFor(instancesByType[referenceTypeString])),
1396
+ eb(typeColumn, '=', referenceTypeString),
1397
+ ];
1398
+ if (negate)
1399
+ conditions.push(eb(foreignKeyColumn, 'is not', null), eb(typeColumn, 'is not', null));
1400
+ return eb.and(conditions);
1401
+ });
1402
+ return eb.or(typeClauses);
1403
+ }
1150
1404
  dreamWhereStatementToExpressionBuilderParts(dreamClass, attr, val) {
1151
1405
  let a;
1152
1406
  let b;
@@ -1260,7 +1514,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1260
1514
  explicitAlias: alias,
1261
1515
  joinAndStatement,
1262
1516
  joinType,
1263
- previousThroughAssociation: undefined,
1517
+ previousThroughAssociations: [],
1264
1518
  dreamClassThroughAssociationWantsToHydrate: undefined,
1265
1519
  });
1266
1520
  query = results.query;
@@ -1563,20 +1817,35 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1563
1817
  * public b
1564
1818
  * ```
1565
1819
  *
1820
+ * Options declared on a through association (`and`/`andAny`/`andNot`/`selfAnd`/
1821
+ * `selfAndNot`/`order`/`distinct`) are applied at the join of the table where the
1822
+ * through association's target model materializes. When a source is itself a
1823
+ * through association, that join is only reached after bridging further through
1824
+ * associations, so each through association is pushed onto the
1825
+ * `previousThroughAssociations` stack (along with the alias `selfAnd`/`selfAndNot`
1826
+ * clauses reference) before the terminal `applyOneJoin` call, and the stack is
1827
+ * threaded through every recursion until `addAssociationJoinStatementToQuery`
1828
+ * reaches a concrete (non-through) association, where every stacked entry is
1829
+ * applied to that join.
1830
+ *
1566
1831
  * Then `MyModel.leftJoinPreload('myB')` is processed as follows:
1567
1832
  * - `applyOneJoin` is called with the `myB` association
1568
1833
  * - `joinsBridgeThroughAssociations` is called with the `myB` association
1569
1834
  * - `joinsBridgeThroughAssociations` is called with the `myA` association
1570
1835
  * - `addAssociationJoinStatementToQuery` is called with the `otherModel` association
1571
- * - `applyOneJoin` is called with the `a` association from OtherModel
1572
- * - `joinsBridgeThroughAssociations` is called with the `a` association from OtherModel
1573
- * // throw ThroughAssociationConditionsIncompatibleWithThroughAssociationSource if
1574
- * // myA in MyModel defines conditions, distinct, or order
1836
+ * - `applyOneJoin` is called with the `a` association from OtherModel, with `myA` pushed
1837
+ * onto the previousThroughAssociations stack
1838
+ * - `joinsBridgeThroughAssociations` is called with the `a` association from OtherModel,
1839
+ * inheriting the stack
1575
1840
  * - `addAssociationJoinStatementToQuery` is called with the `aToOtherModelJoinModel` association
1576
- * - `applyOneJoin` is called with the `a` association from AToOtherModelJoinModel with conditions (if present) on `a` defined on OtherModel
1577
- * - `addAssociationJoinStatementToQuery` is called with the `myA` association
1578
- * - `applyOneJoin` is called with the `b` association from A with conditions (if present) from `myB` defined on MyModel
1579
- * - `addAssociationJoinStatementToQuery` is called with the `myB` association
1841
+ * - `applyOneJoin` is called with the `a` association from AToOtherModelJoinModel, with the
1842
+ * `a` association from OtherModel pushed onto the stack
1843
+ * - `addAssociationJoinStatementToQuery` is called with the `a` association from
1844
+ * AToOtherModelJoinModel, applying the options (if present) of every stacked through
1845
+ * association (`myA` defined on MyModel, `a` defined on OtherModel) to the `through_as` join
1846
+ * - `applyOneJoin` is called with the `b` association from A, with `myB` pushed onto the stack
1847
+ * - `addAssociationJoinStatementToQuery` is called with the `b` association from A, applying
1848
+ * the options (if present) of `myB` defined on MyModel to the `through_bs` join
1580
1849
  */
1581
1850
  joinsBridgeThroughAssociations({ query, dreamClassTheAssociationIsDefinedOn, throughAssociation,
1582
1851
  /**
@@ -1598,7 +1867,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1598
1867
  /**
1599
1868
  * previousTableAlias is always set
1600
1869
  */
1601
- previousTableAlias, joinAndStatement, joinType, }) {
1870
+ previousTableAlias, joinAndStatement,
1871
+ /**
1872
+ * Through associations from further out in the association chain whose
1873
+ * options are waiting to be applied at the terminal concrete join of this
1874
+ * chain (see the doc comment above this method).
1875
+ */
1876
+ previousThroughAssociations, joinType, }) {
1602
1877
  /**
1603
1878
  * `through` associations always point to other associations on the same model
1604
1879
  * they are defined on. So when we want to find an association referenced by a
@@ -1632,6 +1907,14 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1632
1907
  explicitAlias: undefined,
1633
1908
  previousTableAlias,
1634
1909
  joinAndStatement: {},
1910
+ /**
1911
+ * The through associations pending in previousThroughAssociations are
1912
+ * applied at the terminal join of _this_ association's chain (their
1913
+ * target model is this through association's target model), not at the
1914
+ * terminal join of the nested through association being bridged here,
1915
+ * so the nested chain starts with an empty stack.
1916
+ */
1917
+ previousThroughAssociations: [],
1635
1918
  joinType,
1636
1919
  });
1637
1920
  //
@@ -1658,7 +1941,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1658
1941
  * The joinAndStatement is reserved for the final association, not intermediary join tables
1659
1942
  */
1660
1943
  joinAndStatement: {},
1661
- previousThroughAssociation: undefined,
1944
+ previousThroughAssociations: [],
1662
1945
  joinType,
1663
1946
  dreamClassThroughAssociationWantsToHydrate: undefined,
1664
1947
  });
@@ -1698,15 +1981,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1698
1981
  // recursively and therefore may have added other through associations and their sources prior to reaching this point)
1699
1982
  explicitAlias,
1700
1983
  previousTableAlias: recursiveResult.association.as,
1701
- selfTableAlias: (throughAssociation.selfAnd ?? throughAssociation.selfAndNot)
1702
- ? previousTableAlias
1703
- : recursiveResult.association.as,
1984
+ selfTableAlias: recursiveResult.association.as,
1704
1985
  // since joinsBridgeThroughAssociations passes {} to joinAndStatement recursively, we know this is only set on the
1705
1986
  // first call to joinsBridgeThroughAssociations, which corresponds to the outermoset through association and therefore
1706
1987
  // the last source association to be added to the statement (because joinsBridgeThroughAssociations was called
1707
1988
  // recursively and therefore may have added other through associations and their sources prior to reaching this point)
1708
1989
  joinAndStatement,
1709
- previousThroughAssociation: throughAssociation,
1990
+ previousThroughAssociations: [
1991
+ ...previousThroughAssociations,
1992
+ /**
1993
+ * This through association's options are applied at the terminal
1994
+ * concrete join of this chain. `selfAnd`/`selfAndNot` clauses on a
1995
+ * through association reference the model the association is defined
1996
+ * on, whose table is aliased with this method's previousTableAlias.
1997
+ */
1998
+ { association: throughAssociation, selfTableAlias: previousTableAlias },
1999
+ ],
1710
2000
  joinType,
1711
2001
  dreamClassThroughAssociationWantsToHydrate,
1712
2002
  });
@@ -1731,14 +2021,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1731
2021
  /**
1732
2022
  * previousTableAlias is always set
1733
2023
  */
1734
- previousTableAlias, selfTableAlias = previousTableAlias, joinAndStatement = {}, previousThroughAssociation, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
2024
+ previousTableAlias, selfTableAlias = previousTableAlias, joinAndStatement = {}, previousThroughAssociations, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
1735
2025
  if (association.type !== 'BelongsTo' && association.through) {
1736
- if (throughAssociationHasOptionsBesidesThroughAndSource(previousThroughAssociation)) {
1737
- throw new ThroughAssociationConditionsIncompatibleWithThroughAssociationSource({
1738
- dreamClass,
1739
- association,
1740
- });
1741
- }
1742
2026
  return this.joinsBridgeThroughAssociations({
1743
2027
  query,
1744
2028
  dreamClassTheAssociationIsDefinedOn: dreamClass,
@@ -1746,6 +2030,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1746
2030
  explicitAlias,
1747
2031
  previousTableAlias,
1748
2032
  joinAndStatement,
2033
+ previousThroughAssociations,
1749
2034
  joinType,
1750
2035
  });
1751
2036
  }
@@ -1757,7 +2042,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1757
2042
  previousTableAlias,
1758
2043
  selfTableAlias,
1759
2044
  joinAndStatement,
1760
- previousThroughAssociation,
2045
+ previousThroughAssociations,
1761
2046
  joinType,
1762
2047
  dreamClassThroughAssociationWantsToHydrate,
1763
2048
  });
@@ -1782,24 +2067,32 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1782
2067
  /**
1783
2068
  * previousTableAlias is always set
1784
2069
  */
1785
- previousTableAlias, selfTableAlias, joinAndStatement, previousThroughAssociation, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
2070
+ previousTableAlias, selfTableAlias, joinAndStatement, previousThroughAssociations, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
1786
2071
  const currentTableAlias = explicitAlias ?? association.as;
1787
2072
  const _associatedDreamClass = association.modelCB();
1788
2073
  const associatedDreamClass = Array.isArray(_associatedDreamClass)
1789
2074
  ? _associatedDreamClass[0]
1790
2075
  : _associatedDreamClass;
1791
- if (previousThroughAssociation?.type === 'HasMany') {
1792
- if (query?.distinctOn && previousThroughAssociation.distinct) {
2076
+ /**
2077
+ * Stacked order/distinct clauses are applied in join order: the options of
2078
+ * the through association whose bridging joins were added to the query
2079
+ * first are applied first, and the terminal association's own
2080
+ * order/distinct (applied after the join, below) come last.
2081
+ */
2082
+ for (const { association: throughAssociation } of previousThroughAssociations) {
2083
+ if (throughAssociation.type !== 'HasMany')
2084
+ continue;
2085
+ if (query?.distinctOn && throughAssociation.distinct) {
1793
2086
  query = query.distinctOn(this.distinctColumnNameForAssociation({
1794
- association: previousThroughAssociation,
2087
+ association: throughAssociation,
1795
2088
  tableNameOrAlias: currentTableAlias,
1796
- foreignKey: previousThroughAssociation.primaryKey(),
2089
+ foreignKey: throughAssociation.primaryKey(),
1797
2090
  }));
1798
2091
  }
1799
- if (previousThroughAssociation?.order) {
2092
+ if (throughAssociation.order) {
1800
2093
  query = this.applyOrderStatementForAssociation({
1801
2094
  query,
1802
- association: previousThroughAssociation,
2095
+ association: throughAssociation,
1803
2096
  tableNameOrAlias: currentTableAlias,
1804
2097
  });
1805
2098
  }
@@ -1818,21 +2111,18 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1818
2111
  join = join.onRef(this.namespaceColumn(association.foreignKey(), previousTableAlias), '=', this.namespaceColumn(association.primaryKey(undefined, {
1819
2112
  associatedClassOverride: dreamClassThroughAssociationWantsToHydrate,
1820
2113
  }), currentTableAlias));
1821
- if (previousThroughAssociation) {
1822
- if (dreamClassThroughAssociationWantsToHydrate) {
1823
- join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement({
1824
- [association.foreignKeyTypeField()]: dreamClassThroughAssociationWantsToHydrate.sanitizedName,
1825
- }, previousThroughAssociation.through)));
1826
- }
1827
- join = this.applyAssociationAndStatementsToJoinStatement({
1828
- dreamClass,
1829
- join,
1830
- association: previousThroughAssociation,
1831
- currentTableAlias,
1832
- selfTableAlias,
1833
- joinAndStatement,
1834
- });
2114
+ if (dreamClassThroughAssociationWantsToHydrate) {
2115
+ join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement({
2116
+ [association.foreignKeyTypeField()]: dreamClassThroughAssociationWantsToHydrate.sanitizedName,
2117
+ }, previousTableAlias)));
1835
2118
  }
2119
+ join = this.applyPreviousThroughAssociationAndStatementsToJoinStatement({
2120
+ dreamClass,
2121
+ join,
2122
+ previousThroughAssociations,
2123
+ currentTableAlias,
2124
+ joinAndStatement,
2125
+ });
1836
2126
  join = this.conditionallyApplyDefaultScopesDependentOnAssociation({
1837
2127
  dreamClass,
1838
2128
  join,
@@ -1856,16 +2146,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1856
2146
  : dreamClass.referenceTypeString,
1857
2147
  }, currentTableAlias)));
1858
2148
  }
1859
- if (previousThroughAssociation) {
1860
- join = this.applyAssociationAndStatementsToJoinStatement({
1861
- dreamClass,
1862
- join,
1863
- association: previousThroughAssociation,
1864
- currentTableAlias,
1865
- selfTableAlias,
1866
- joinAndStatement,
1867
- });
1868
- }
2149
+ join = this.applyPreviousThroughAssociationAndStatementsToJoinStatement({
2150
+ dreamClass,
2151
+ join,
2152
+ previousThroughAssociations,
2153
+ currentTableAlias,
2154
+ joinAndStatement,
2155
+ });
1869
2156
  join = this.applyAssociationAndStatementsToJoinStatement({
1870
2157
  dreamClass,
1871
2158
  join,
@@ -1929,27 +2216,52 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1929
2216
  return this.namespaceColumn(foreignKey, tableNameOrAlias);
1930
2217
  return this.namespaceColumn(association.distinct, tableNameOrAlias);
1931
2218
  }
2219
+ /**
2220
+ * Applies the and-family clauses (`and`/`andAny`/`andNot`/`selfAnd`/`selfAndNot`)
2221
+ * of every pending through association to the terminal concrete join of a
2222
+ * through association chain.
2223
+ *
2224
+ * The first entry in the stack is the association the developer named in the
2225
+ * join/preload/associationQuery statement, so it is the association the
2226
+ * developer-supplied joinAndStatement corresponds to (used to satisfy
2227
+ * `DreamConst.required` clauses); `DreamConst.required` on any other stacked
2228
+ * through association cannot be satisfied and will throw
2229
+ * `MissingRequiredAssociationAndClause`.
2230
+ */
2231
+ applyPreviousThroughAssociationAndStatementsToJoinStatement({ dreamClass, join, previousThroughAssociations, currentTableAlias, joinAndStatement, }) {
2232
+ previousThroughAssociations.forEach(({ association, selfTableAlias }, index) => {
2233
+ join = this.applyAssociationAndStatementsToJoinStatement({
2234
+ dreamClass,
2235
+ join,
2236
+ association,
2237
+ currentTableAlias,
2238
+ selfTableAlias,
2239
+ joinAndStatement: index === 0 ? joinAndStatement : {},
2240
+ });
2241
+ });
2242
+ return join;
2243
+ }
1932
2244
  applyAssociationAndStatementsToJoinStatement({ dreamClass, join, currentTableAlias, selfTableAlias, association, joinAndStatement, }) {
1933
2245
  if (association.and) {
1934
2246
  this.throwUnlessAllRequiredWhereClausesProvided(association, currentTableAlias, joinAndStatement);
1935
- join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.and, currentTableAlias), { disallowSimilarityOperator: false }));
2247
+ join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.and, currentTableAlias), { disallowSimilarityOperator: false, expectedAlias: currentTableAlias }));
1936
2248
  }
1937
2249
  if (association.andNot) {
1938
- join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.andNot, currentTableAlias), { negate: true }));
2250
+ join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.andNot, currentTableAlias), { negate: true, expectedAlias: currentTableAlias }));
1939
2251
  }
1940
2252
  if (association.andAny) {
1941
- join = join.on((eb) => eb.or(association.andAny.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, currentTableAlias), { disallowSimilarityOperator: false }))));
2253
+ join = join.on((eb) => eb.or(association.andAny.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, currentTableAlias), { disallowSimilarityOperator: false, expectedAlias: currentTableAlias }))));
1942
2254
  }
1943
2255
  if (association.selfAnd) {
1944
2256
  join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.rawifiedSelfOnClause({
1945
- associationAlias: association.as,
2257
+ associationAlias: currentTableAlias,
1946
2258
  selfAlias: selfTableAlias,
1947
2259
  selfAndClause: association.selfAnd,
1948
2260
  })));
1949
2261
  }
1950
2262
  if (association.selfAndNot) {
1951
2263
  join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.rawifiedSelfOnClause({
1952
- associationAlias: association.as,
2264
+ associationAlias: currentTableAlias,
1953
2265
  selfAlias: selfTableAlias,
1954
2266
  selfAndClause: association.selfAndNot,
1955
2267
  }), { negate: true }));
@@ -1997,9 +2309,9 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1997
2309
  // branches below, such default scopes are silently dropped on association loads, leaking
1998
2310
  // rows the app intended to hide.
1999
2311
  join = join.on((eb) => eb.and([
2000
- ...scopesQuery['whereStatements'].map(whereStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereStatement, tableNameOrAlias), { disallowSimilarityOperator: false })),
2001
- ...scopesQuery['whereNotStatements'].map(whereNotStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereNotStatement, tableNameOrAlias), { negate: true })),
2002
- ...scopesQuery['whereAnyStatements'].map(whereAnyStatements => eb.or(whereAnyStatements.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, tableNameOrAlias))))),
2312
+ ...scopesQuery['whereStatements'].map(whereStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereStatement, tableNameOrAlias), { disallowSimilarityOperator: false, expectedAlias: tableNameOrAlias })),
2313
+ ...scopesQuery['whereNotStatements'].map(whereNotStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereNotStatement, tableNameOrAlias), { negate: true, expectedAlias: tableNameOrAlias })),
2314
+ ...scopesQuery['whereAnyStatements'].map(whereAnyStatements => eb.or(whereAnyStatements.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, tableNameOrAlias), { expectedAlias: tableNameOrAlias })))),
2003
2315
  ]));
2004
2316
  }
2005
2317
  return join;
@@ -2110,15 +2422,29 @@ export default class KyselyQueryDriver extends QueryDriverBase {
2110
2422
  const preloadedPolymorphicBelongsTos = await this.preloadPolymorphicBelongsTo(association, dreamClassToHydrate, dreams);
2111
2423
  return preloadedPolymorphicBelongsTos;
2112
2424
  }
2113
- const dreamClassToHydrateColumns = [...dreamClassToHydrate.columns()];
2114
- const columnsToPluck = dreamClassToHydrateColumns.map(column => this.namespaceColumn(column.toString(), alias));
2115
- columnsToPluck.push(this.namespaceColumn(dreamClass.primaryKey, dreamClass.table));
2425
+ const dreamClassToHydrateColumns = dreamClassToHydrate.columns();
2426
+ // The base-model primary key rides along under a short alias so each
2427
+ // associated row can be matched back to the dream it belongs to. The
2428
+ // alias is lowercase without underscores so Kysely's CamelCasePlugin
2429
+ // passes it through untouched (the same strategy as `pluck0` /
2430
+ // `groupvalue`), and is extended until it cannot collide with a real
2431
+ // column of the associated table. That collision loop can only consult
2432
+ // the compiled column set, so under add-column skew a live column
2433
+ // literally named like the alias would produce two identically-named
2434
+ // result fields. The aliased base PK is deliberately selected last,
2435
+ // and node-postgres resolves duplicate field names last-wins, so the
2436
+ // base PK still wins even in that pathological case — and the
2437
+ // hydration filter strips the colliding key from the instance's
2438
+ // attributes.
2439
+ let basePrimaryKeyAlias = 'preloadbasepk';
2440
+ while (dreamClassToHydrateColumns.has(basePrimaryKeyAlias))
2441
+ basePrimaryKeyAlias += '0';
2116
2442
  const baseClass = dreamClass['stiBaseClassOrOwnClass']['getAssociationMetadata'](associationName)
2117
2443
  ? dreamClass['stiBaseClassOrOwnClass']
2118
2444
  : dreamClass;
2119
2445
  const associationDataScope = this.dreamClassQueryWithScopeBypasses(baseClass, {
2120
2446
  // In order to stay DRY, preloading leverages the association logic built into
2121
- // `joins` (by using `pluck`, which calls `joins`). However, baseClass may have
2447
+ // `joins`. However, baseClass may have
2122
2448
  // default scopes that would preclude finding that instance. We remove all
2123
2449
  // default scopes on baseClass, but not subsequent associations, so that the
2124
2450
  // single query will be able to find each row corresponding to a Dream in `dreams`,
@@ -2127,16 +2453,29 @@ export default class KyselyQueryDriver extends QueryDriverBase {
2127
2453
  }).where({
2128
2454
  [dreamClass.primaryKey]: dreams.map(obj => obj.primaryKeyValue()),
2129
2455
  });
2130
- const hydrationData = await associationDataScope['_connection'](this.connectionOverride)
2131
- .innerJoin(associationName, (onStatement || {}))
2132
- .pluck(...columnsToPluck);
2133
- const preloadedDreamsAndWhatTheyPointTo = hydrationData.map(pluckedData => {
2134
- const attributes = {};
2135
- dreamClassToHydrateColumns.forEach((columnName, index) => (attributes[protectAgainstPollutingAssignment(columnName)] = pluckedData[index]));
2136
- const hydratedDream = this.dbResultToDreamInstance(attributes, dreamClassToHydrate);
2456
+ const joinedQuery = associationDataScope['_connection'](this.connectionOverride).innerJoin(associationName, (onStatement || {}));
2457
+ // Select the association's row wholesale (`"alias".*`) rather than
2458
+ // enumerating the compiled column list. Enumerating made every preload
2459
+ // assert the compiled schema against the live database: under
2460
+ // schema/image skew (rolling deploy or rollback around a drop-column
2461
+ // migration), naming a dropped column fails the whole preload with
2462
+ // `42703 column does not exist`. The raw row is passed straight to
2463
+ // hydration, where sqlResultToDreamInstance — the single filtering
2464
+ // point — intersects it with the compiled column list (also stripping
2465
+ // the base-PK alias key), so a column the image doesn't know about
2466
+ // (the add-column direction of skew) never reaches the instance. See
2467
+ // the prepared-statements note on `saveDream` before changing this
2468
+ // back to an enumerated select.
2469
+ const kyselyQuery = new this.constructor(joinedQuery)
2470
+ .buildSelect({ bypassSelectAll: true })
2471
+ .selectAll(alias)
2472
+ .select(`${this.namespaceColumn(dreamClass.primaryKey, dreamClass.table)} as ${basePrimaryKeyAlias}`);
2473
+ const rows = await executeDatabaseQuery(kyselyQuery, 'execute');
2474
+ const preloadedDreamsAndWhatTheyPointTo = rows.map((row) => {
2475
+ const hydratedDream = this.dbResultToDreamInstance(row, dreamClassToHydrate);
2137
2476
  return {
2138
2477
  dream: hydratedDream,
2139
- pointsToPrimaryKey: pluckedData.at(-1),
2478
+ pointsToPrimaryKey: row[basePrimaryKeyAlias],
2140
2479
  };
2141
2480
  });
2142
2481
  this.hydrateAssociation(dreams, association, preloadedDreamsAndWhatTheyPointTo);