ts-prorm-orm 1.2.3 → 2.0.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 (129) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/README.md +32 -24
  3. package/dist/compliance/index.d.ts +2 -0
  4. package/dist/compliance/index.js +13 -1
  5. package/dist/connection-manager.js +13 -1
  6. package/dist/core/expressions.d.ts +58 -0
  7. package/dist/core/expressions.js +83 -0
  8. package/dist/core/hooks-manager.d.ts +179 -0
  9. package/dist/core/hooks-manager.js +291 -0
  10. package/dist/core/types.d.ts +235 -0
  11. package/dist/core/types.js +9 -0
  12. package/dist/decorators/index.d.ts +2 -0
  13. package/dist/decorators/index.js +11 -1
  14. package/dist/diagrams/chen-diagram.d.ts +15 -12
  15. package/dist/diagrams/chen-diagram.js +76 -85
  16. package/dist/diagrams/class-diagram.d.ts +9 -8
  17. package/dist/diagrams/class-diagram.js +60 -82
  18. package/dist/diagrams/core/index.d.ts +13 -0
  19. package/dist/diagrams/core/index.js +33 -0
  20. package/dist/diagrams/core/layout.d.ts +104 -0
  21. package/dist/diagrams/core/layout.js +223 -0
  22. package/dist/diagrams/{svg-dom.d.ts → core/svg.d.ts} +8 -20
  23. package/dist/diagrams/{svg-dom.js → core/svg.js} +21 -29
  24. package/dist/diagrams/core/theme.d.ts +84 -0
  25. package/dist/diagrams/core/theme.js +62 -0
  26. package/dist/diagrams/dependency-diagram.d.ts +15 -14
  27. package/dist/diagrams/dependency-diagram.js +71 -105
  28. package/dist/diagrams/er-diagram.d.ts +13 -26
  29. package/dist/diagrams/er-diagram.js +180 -259
  30. package/dist/diagrams/flow-diagram.d.ts +16 -11
  31. package/dist/diagrams/flow-diagram.js +96 -91
  32. package/dist/diagrams/gantt-diagram.d.ts +10 -5
  33. package/dist/diagrams/gantt-diagram.js +77 -103
  34. package/dist/diagrams/index-diagram.d.ts +10 -10
  35. package/dist/diagrams/index-diagram.js +106 -116
  36. package/dist/diagrams/index.d.ts +6 -9
  37. package/dist/diagrams/index.js +9 -24
  38. package/dist/diagrams/migration-diagram.d.ts +8 -4
  39. package/dist/diagrams/migration-diagram.js +189 -161
  40. package/dist/diagrams/model-diagram.d.ts +34 -34
  41. package/dist/diagrams/model-diagram.js +106 -485
  42. package/dist/diagrams/package-diagram.d.ts +9 -6
  43. package/dist/diagrams/package-diagram.js +64 -101
  44. package/dist/diagrams/relational-diagram.d.ts +12 -10
  45. package/dist/diagrams/relational-diagram.js +87 -114
  46. package/dist/diagrams/schemadoc-diagram.d.ts +8 -8
  47. package/dist/diagrams/schemadoc-diagram.js +71 -93
  48. package/dist/diagrams/sequence-diagram.d.ts +10 -10
  49. package/dist/diagrams/sequence-diagram.js +64 -89
  50. package/dist/diagrams/state-diagram.d.ts +10 -9
  51. package/dist/diagrams/state-diagram.js +69 -92
  52. package/dist/diagrams/tree-diagram.d.ts +11 -5
  53. package/dist/diagrams/tree-diagram.js +41 -65
  54. package/dist/dialects/clickhouse/index.js +15 -0
  55. package/dist/dialects/cockroachdb/index.d.ts +1 -1
  56. package/dist/dialects/cockroachdb/index.js +32 -63
  57. package/dist/dialects/db2/index.js +23 -8
  58. package/dist/dialects/dialect.d.ts +29 -5
  59. package/dist/dialects/duckdb/index.js +9 -2
  60. package/dist/dialects/hana/index.js +22 -5
  61. package/dist/dialects/lock-clause-helper.d.ts +167 -0
  62. package/dist/dialects/lock-clause-helper.js +316 -0
  63. package/dist/dialects/mariadb/index.d.ts +7 -4
  64. package/dist/dialects/mariadb/index.js +36 -60
  65. package/dist/dialects/mssql/index.js +34 -3
  66. package/dist/dialects/mysql/index.d.ts +1 -1
  67. package/dist/dialects/mysql/index.js +17 -45
  68. package/dist/dialects/oracle/index.d.ts +14 -3
  69. package/dist/dialects/oracle/index.js +66 -36
  70. package/dist/dialects/order-expression-helper.d.ts +72 -0
  71. package/dist/dialects/order-expression-helper.js +154 -0
  72. package/dist/dialects/partial-index.d.ts +52 -0
  73. package/dist/dialects/partial-index.js +251 -0
  74. package/dist/dialects/postgres/index.d.ts +1 -1
  75. package/dist/dialects/postgres/index.js +43 -68
  76. package/dist/dialects/redshift/index.js +9 -2
  77. package/dist/dialects/snowflake/index.js +17 -0
  78. package/dist/dialects/spanner/index.js +7 -2
  79. package/dist/dialects/sqlite/driver.d.ts +55 -0
  80. package/dist/dialects/sqlite/driver.js +102 -0
  81. package/dist/dialects/sqlite/index.d.ts +1 -1
  82. package/dist/dialects/sqlite/index.js +49 -27
  83. package/dist/graph/base-graph-dialect.d.ts +24 -3
  84. package/dist/graph/base-graph-dialect.js +43 -2
  85. package/dist/graph/dgraph/index.js +6 -0
  86. package/dist/graph/gremlin/index.d.ts +6 -0
  87. package/dist/graph/gremlin/index.js +12 -1
  88. package/dist/graph/index.d.ts +1 -1
  89. package/dist/graph/neo4j/index.d.ts +2 -0
  90. package/dist/graph/neo4j/index.js +6 -1
  91. package/dist/graph/types.d.ts +12 -1
  92. package/dist/index.d.ts +12 -3
  93. package/dist/index.js +53 -7
  94. package/dist/logging/index.d.ts +10 -0
  95. package/dist/logging/index.js +23 -0
  96. package/dist/logging/query-logging.d.ts +82 -0
  97. package/dist/logging/query-logging.js +102 -0
  98. package/dist/models/decorators.d.ts +39 -3
  99. package/dist/models/decorators.js +74 -0
  100. package/dist/models/eager-load.d.ts +99 -0
  101. package/dist/models/eager-load.js +348 -5
  102. package/dist/models/indexes.d.ts +3 -2
  103. package/dist/models/indexes.js +7 -1
  104. package/dist/models/model.js +69 -43
  105. package/dist/prorm.d.ts +175 -409
  106. package/dist/prorm.js +1132 -553
  107. package/dist/query-builders/order-limit-builder.js +12 -10
  108. package/dist/query-builders/sql-compiler.d.ts +10 -3
  109. package/dist/query-builders/sql-compiler.js +14 -43
  110. package/dist/query-interface.d.ts +2 -1
  111. package/dist/sql-constants.d.ts +44 -0
  112. package/dist/sql-constants.js +138 -1
  113. package/dist/types/index.d.ts +53 -17
  114. package/dist/types/index.js +10 -15
  115. package/dist/types/query-types.d.ts +26 -0
  116. package/dist/types/query-types.js +30 -0
  117. package/package.json +32 -5
  118. package/dist/decorators/belongs-to-many.js +0 -115
  119. package/dist/decorators/belongs-to.js +0 -115
  120. package/dist/decorators/has-many.js +0 -127
  121. package/dist/decorators/has-one.js +0 -116
  122. package/dist/diagrams/palette.d.ts +0 -138
  123. package/dist/diagrams/palette.js +0 -194
  124. package/dist/hooks/hooks-manager.d.ts +0 -189
  125. package/dist/hooks/hooks-manager.js +0 -350
  126. package/dist/hooks/index.d.ts +0 -8
  127. package/dist/hooks/index.js +0 -37
  128. /package/dist/{logging.d.ts → logging/logger.d.ts} +0 -0
  129. /package/dist/{logging.js → logging/logger.js} +0 -0
package/dist/prorm.js CHANGED
@@ -78,372 +78,21 @@ const query_interface_1 = require("./query-interface");
78
78
  // Import UUIDV4 from data-types
79
79
  const data_types_1 = require("./models/data-types");
80
80
  const sql_constants_1 = require("./sql-constants");
81
- /**
82
- * Hook manager for Prorm instance
83
- */
84
- class HooksManager {
85
- constructor() {
86
- this.beforeDefineHooks = [];
87
- this.afterDefineHooks = [];
88
- this.beforeSyncHooks = [];
89
- this.afterSyncHooks = [];
90
- this.beforeConnectHooks = [];
91
- this.afterConnectHooks = [];
92
- this.beforeDisconnectHooks = [];
93
- this.afterDisconnectHooks = [];
94
- this.beforeDestroyHooks = [];
95
- this.afterDestroyHooks = [];
96
- this.beforeUpsertHooks = [];
97
- this.afterUpsertHooks = [];
98
- this.beforeReloadHooks = [];
99
- }
100
- /**
101
- * Register a beforeDefine hook
102
- */
103
- beforeDefine(callback) {
104
- this.beforeDefineHooks.push(callback);
105
- }
106
- /**
107
- * Register an afterDefine hook
108
- */
109
- afterDefine(callback) {
110
- this.afterDefineHooks.push(callback);
111
- }
112
- /**
113
- * Register a beforeSync hook
114
- */
115
- beforeSync(callback) {
116
- this.beforeSyncHooks.push(callback);
117
- }
118
- /**
119
- * Register an afterSync hook
120
- */
121
- afterSync(callback) {
122
- this.afterSyncHooks.push(callback);
123
- }
124
- /**
125
- * Register a beforeConnect hook
126
- */
127
- beforeConnect(callback) {
128
- this.beforeConnectHooks.push(callback);
129
- }
130
- /**
131
- * Register an afterConnect hook
132
- */
133
- afterConnect(callback) {
134
- this.afterConnectHooks.push(callback);
135
- }
136
- /**
137
- * Register a beforeDisconnect hook
138
- */
139
- beforeDisconnect(callback) {
140
- this.beforeDisconnectHooks.push(callback);
141
- }
142
- /**
143
- * Register an afterDisconnect hook
144
- */
145
- afterDisconnect(callback) {
146
- this.afterDisconnectHooks.push(callback);
147
- }
148
- /**
149
- * Register a beforeDestroy hook
150
- */
151
- beforeDestroy(callback) {
152
- this.beforeDestroyHooks.push(callback);
153
- }
154
- /**
155
- * Register an afterDestroy hook
156
- */
157
- afterDestroy(callback) {
158
- this.afterDestroyHooks.push(callback);
159
- }
160
- /**
161
- * Register a beforeUpsert hook
162
- */
163
- beforeUpsert(callback) {
164
- this.beforeUpsertHooks.push(callback);
165
- }
166
- /**
167
- * Register an afterUpsert hook
168
- */
169
- afterUpsert(callback) {
170
- this.afterUpsertHooks.push(callback);
171
- }
172
- /**
173
- * Register a beforeReload hook
174
- */
175
- beforeReload(callback) {
176
- this.beforeReloadHooks.push(callback);
177
- }
178
- /**
179
- * Execute all beforeDefine hooks
180
- */
181
- async runBeforeDefine(modelName, attributes, options) {
182
- for (const hook of this.beforeDefineHooks) {
183
- await hook({ modelName, attributes, options });
184
- }
185
- }
186
- /**
187
- * Execute all afterDefine hooks
188
- */
189
- async runAfterDefine(model) {
190
- for (const hook of this.afterDefineHooks) {
191
- await hook({ model });
192
- }
193
- }
194
- /**
195
- * Execute all beforeSync hooks
196
- */
197
- async runBeforeSync(options) {
198
- for (const hook of this.beforeSyncHooks) {
199
- await hook({ options });
200
- }
201
- }
202
- /**
203
- * Execute all afterSync hooks
204
- */
205
- async runAfterSync(options) {
206
- for (const hook of this.afterSyncHooks) {
207
- await hook({ options });
208
- }
209
- }
210
- /**
211
- * Execute all beforeConnect hooks
212
- */
213
- async runBeforeConnect() {
214
- for (const hook of this.beforeConnectHooks) {
215
- await hook();
216
- }
217
- }
218
- /**
219
- * Execute all afterConnect hooks
220
- * @param connection - The connection object to pass to hooks
221
- */
222
- async runAfterConnect(connection) {
223
- for (const hook of this.afterConnectHooks) {
224
- await hook(connection);
225
- }
226
- }
227
- /**
228
- * Execute all beforeDisconnect hooks
229
- * @param connection - The connection object to pass to hooks
230
- */
231
- async runBeforeDisconnect(connection) {
232
- for (const hook of this.beforeDisconnectHooks) {
233
- await hook(connection);
234
- }
235
- }
236
- /**
237
- * Execute all afterDisconnect hooks
238
- * @param connection - The connection object to pass to hooks
239
- */
240
- async runAfterDisconnect(connection) {
241
- for (const hook of this.afterDisconnectHooks) {
242
- await hook(connection);
243
- }
244
- }
245
- /**
246
- * Execute all beforeDestroy hooks
247
- * @param options - The destroy options
248
- */
249
- async runBeforeDestroy(options) {
250
- for (const hook of this.beforeDestroyHooks) {
251
- await hook({ options });
252
- }
253
- }
254
- /**
255
- * Execute all afterDestroy hooks
256
- * @param options - The destroy options
257
- */
258
- async runAfterDestroy(options) {
259
- for (const hook of this.afterDestroyHooks) {
260
- await hook({ options });
261
- }
262
- }
263
- /**
264
- * Execute all beforeUpsert hooks
265
- * @param options - The upsert options
266
- */
267
- async runBeforeUpsert(options) {
268
- for (const hook of this.beforeUpsertHooks) {
269
- await hook({ options });
270
- }
271
- }
272
- /**
273
- * Execute all afterUpsert hooks
274
- * @param options - The upsert options
275
- */
276
- async runAfterUpsert(options) {
277
- for (const hook of this.afterUpsertHooks) {
278
- await hook({ options });
279
- }
280
- }
281
- /**
282
- * Execute all beforeReload hooks
283
- * @param options - The reload options
284
- */
285
- async runBeforeReload(options) {
286
- for (const hook of this.beforeReloadHooks) {
287
- await hook({ options });
288
- }
289
- }
290
- /**
291
- * Add a hook by name
292
- * @param hookName - The name of the hook (e.g., 'beforeDefine', 'afterSync')
293
- * @param callback - The hook callback function
294
- */
295
- addHook(hookName, callback) {
296
- const methodName = hookName;
297
- if (typeof this[methodName] === 'function') {
298
- this[methodName](callback);
299
- }
300
- }
301
- /**
302
- * Clear all hooks
303
- */
304
- clearHooks() {
305
- this.beforeDefineHooks = [];
306
- this.afterDefineHooks = [];
307
- this.beforeSyncHooks = [];
308
- this.afterSyncHooks = [];
309
- this.beforeConnectHooks = [];
310
- this.afterConnectHooks = [];
311
- this.beforeDisconnectHooks = [];
312
- this.afterDisconnectHooks = [];
313
- this.beforeDestroyHooks = [];
314
- this.afterDestroyHooks = [];
315
- this.beforeUpsertHooks = [];
316
- this.afterUpsertHooks = [];
317
- this.beforeReloadHooks = [];
318
- }
319
- }
320
- exports.HooksManager = HooksManager;
321
- /**
322
- * Model-level hooks manager
323
- */
324
- class ModelHooks {
325
- constructor() {
326
- this.beforeSyncHooks = [];
327
- this.afterSyncHooks = [];
328
- }
329
- /**
330
- * Register a beforeSync hook on the model
331
- */
332
- beforeSync(callback) {
333
- this.beforeSyncHooks.push(callback);
334
- }
335
- /**
336
- * Register an afterSync hook on the model
337
- */
338
- afterSync(callback) {
339
- this.afterSyncHooks.push(callback);
340
- }
341
- /**
342
- * Run beforeSync hooks
343
- */
344
- async runBeforeSync(options) {
345
- for (const hook of this.beforeSyncHooks) {
346
- await hook({ options });
347
- }
348
- }
349
- /**
350
- * Run afterSync hooks
351
- */
352
- async runAfterSync(options) {
353
- for (const hook of this.afterSyncHooks) {
354
- await hook({ options });
355
- }
356
- }
357
- }
358
- /**
359
- * Represents a raw SQL literal value
360
- * Used for inserting raw SQL expressions into queries
361
- *
362
- * @example
363
- * // Use in default values
364
- * createdAt: { type: DATE, defaultValue: prorm.literal('NOW()') }
365
- *
366
- * @example
367
- * // Use in updates
368
- * User.update({ count: prorm.literal('count + 1') }, {...})
369
- *
370
- * @example
371
- * // Use in select with calculations
372
- * User.findAll({ attributes: [[prorm.literal('1 + 1'), 'two']] })
373
- */
374
- class Literal {
375
- constructor(sql) {
376
- this.val = sql;
377
- }
378
- /**
379
- * Get the raw SQL string
380
- */
381
- toString() {
382
- return this.val;
383
- }
384
- }
385
- exports.Literal = Literal;
386
- /**
387
- * Class representing a column reference for use with Prorm functions
388
- * Used to create properly quoted column identifiers
389
- *
390
- * @example
391
- * // Single column: col('name') -> "name"
392
- * // Table and column: col('User', 'name') -> "User"."name"
393
- * // Dot notation: col('table.column') -> "table"."column"
394
- */
395
- class Col {
396
- /**
397
- * Create a column reference
398
- * @param tableOrColumn - Table name (if second param provided) or column name
399
- * @param column - Column name (if first param is table name)
400
- */
401
- constructor(tableOrColumn, column) {
402
- if (column !== undefined) {
403
- // col('User', 'name') -> "User"."name"
404
- this.col = `"${tableOrColumn}"."${column}"`;
405
- }
406
- else if (tableOrColumn.includes('.')) {
407
- // col('table.column') -> "table"."column"
408
- const [table, col] = tableOrColumn.split('.');
409
- this.col = `"${table}"."${col}"`;
410
- }
411
- else {
412
- // col('name') -> "name"
413
- this.col = `"${tableOrColumn}"`;
414
- }
415
- }
416
- /**
417
- * Get the SQL representation of the column
418
- */
419
- toSql() {
420
- return this.col;
421
- }
422
- /**
423
- * Get the column as a string
424
- */
425
- toString() {
426
- return this.col;
427
- }
428
- }
429
- exports.Col = Col;
81
+ // Instance-API types, hook registries and SQL expression wrappers now live in
82
+ // ./core. They are imported for local use and re-exported, so the public API
83
+ // exposed from this module is unchanged.
84
+ const hooks_manager_1 = require("./core/hooks-manager");
85
+ Object.defineProperty(exports, "HooksManager", { enumerable: true, get: function () { return hooks_manager_1.HooksManager; } });
86
+ const expressions_1 = require("./core/expressions");
87
+ Object.defineProperty(exports, "Literal", { enumerable: true, get: function () { return expressions_1.Literal; } });
88
+ Object.defineProperty(exports, "Col", { enumerable: true, get: function () { return expressions_1.Col; } });
430
89
  /**
431
90
  * Query types for raw SQL queries
432
91
  */
433
- var QueryTypes;
434
- (function (QueryTypes) {
435
- QueryTypes["SELECT"] = "SELECT";
436
- QueryTypes["INSERT"] = "INSERT";
437
- QueryTypes["UPDATE"] = "UPDATE";
438
- QueryTypes["DELETE"] = "DELETE";
439
- QueryTypes["BULKDELETE"] = "BULKDELETE";
440
- QueryTypes["BULKINSERT"] = "BULKINSERT";
441
- QueryTypes["UPSERT"] = "UPSERT";
442
- QueryTypes["VERSION"] = "VERSION";
443
- QueryTypes["SHOWTABLES"] = "SHOWTABLES";
444
- QueryTypes["DESCRIBE"] = "DESCRIBE";
445
- QueryTypes["RAW"] = "RAW";
446
- })(QueryTypes || (exports.QueryTypes = QueryTypes = {}));
92
+ // QueryTypes is defined once in ./types/query-types. Imported for local use
93
+ // and re-exported, since the package root has always exported it from here.
94
+ const query_types_1 = require("./types/query-types");
95
+ Object.defineProperty(exports, "QueryTypes", { enumerable: true, get: function () { return query_types_1.QueryTypes; } });
447
96
  /**
448
97
  * Resolve the primary-key attribute name for a model, based on its `rawAttributes`
449
98
  * (looking for the field marked `primaryKey: true`). Falls back to `'id'` when the
@@ -493,6 +142,48 @@ function extractAssociationConditions(where) {
493
142
  }
494
143
  return { rest, conditions };
495
144
  }
145
+ /**
146
+ * Model options inherited from an `@AbstractModel` base.
147
+ *
148
+ * A concrete model with its own `@Table` uses that; one without falls back to
149
+ * the nearest annotated ancestor, so timestamps/paranoid/scopes declared once
150
+ * on a base apply to every subclass.
151
+ */
152
+ function inheritedModelMetadata(modelClass) {
153
+ let current = Object.getPrototypeOf(modelClass);
154
+ while (current && current.name) {
155
+ const meta = (0, decorators_1.getModelMetadata)(current.name);
156
+ if (meta)
157
+ return meta;
158
+ current = Object.getPrototypeOf(current);
159
+ }
160
+ return undefined;
161
+ }
162
+ /**
163
+ * The column names a model's table actually has.
164
+ *
165
+ * `rawAttributes` holds only what the caller declared. `createTableFromModel`
166
+ * additionally synthesizes an `id` primary key when none was declared, plus the
167
+ * timestamp columns - so a model defined as `{ name }` yields a table with
168
+ * `id, name, createdAt, updatedAt`. Validating an attribute name against
169
+ * `rawAttributes` alone therefore rejects columns that genuinely exist.
170
+ */
171
+ function modelColumnNames(model) {
172
+ const names = new Set(Object.keys(model?.rawAttributes || {}));
173
+ const options = (model?.options || {});
174
+ // Implicit primary key: added only when the model declared none.
175
+ const hasDeclaredPk = Object.values(model?.rawAttributes || {}).some((a) => a?.primaryKey);
176
+ if (!hasDeclaredPk && !names.has('id'))
177
+ names.add('id');
178
+ if (options?.timestamps !== false) {
179
+ names.add(options?.createdAt || 'createdAt');
180
+ names.add(options?.updatedAt || 'updatedAt');
181
+ }
182
+ if (options?.paranoid) {
183
+ names.add(options?.deletedAt || 'deletedAt');
184
+ }
185
+ return names;
186
+ }
496
187
  /**
497
188
  * Derive a model's table name from its name and options.
498
189
  *
@@ -554,6 +245,127 @@ function resolveForeignKeyColumnType(refModel, refField) {
554
245
  // Otherwise mirror the referenced column's own type (UUID, STRING(n), BIGINT, ...).
555
246
  return (type ?? 'INTEGER');
556
247
  }
248
+ /**
249
+ * Resolve the physical column name a model attribute maps to.
250
+ *
251
+ * `field` renames a column explicitly and `underscored` snake_cases the rest,
252
+ * but a constraint has to name the column the CREATE TABLE statement actually
253
+ * emitted. This DDL path keys its column map by attribute name, so when the
254
+ * renamed name is not among the emitted columns the attribute name is used
255
+ * instead: naming a column that does not exist would turn a silently-missing
256
+ * constraint into a hard CREATE TABLE failure, which is not an improvement.
257
+ */
258
+ function resolvePhysicalColumnName(attrName, attributes, options, emittedColumns) {
259
+ const attr = attributes[attrName];
260
+ const renamed = attr?.field ?? (options?.underscored ? (0, utils_2.snakeCase)(attrName) : undefined);
261
+ if (!renamed || renamed === attrName)
262
+ return attrName;
263
+ if (emittedColumns[renamed])
264
+ return renamed;
265
+ return emittedColumns[attrName] ? attrName : renamed;
266
+ }
267
+ /**
268
+ * Collect every composite-unique declaration on a model into one canonical list.
269
+ *
270
+ * Three documented, typed ways to declare a composite unique constraint all
271
+ * used to be read by nobody, so they emitted no SQL at all: the developer read
272
+ * back a `CREATE TABLE` with no UNIQUE in it only if they went looking, the
273
+ * database never enforced anything, and the duplicate rows turned up later.
274
+ *
275
+ * - attribute-level `uniqueKey: 'name'` - every attribute sharing that value
276
+ * joins one constraint of that name, in declaration order (as documented on
277
+ * `AttributeOptions.uniqueKey`);
278
+ * - model-level `uniqueKeys: UniqueKeyOptions[]`, whose optional `unique: true`
279
+ * selects a unique index over a table constraint;
280
+ * - the legacy `uniqueKeysLegacy: Record<name, fields[]>` map, still declared
281
+ * on `ModelOptions` and still supported here so that models written against
282
+ * it keep working.
283
+ *
284
+ * Declaring the same constraint through more than one form is deliberately not
285
+ * an error - it is the natural thing to do when migrating from one form to
286
+ * another - so entries are collapsed both by name and by column set, and the
287
+ * first declaration of each wins.
288
+ *
289
+ * @throws {Error} when a declaration names a column the table does not have -
290
+ * the whole point of this function is that a unique key which cannot be
291
+ * enforced must not pass silently.
292
+ */
293
+ function collectUniqueKeys(tableName, attributes, options, emittedColumns) {
294
+ const opts = options;
295
+ const collected = [];
296
+ const seenNames = new Set();
297
+ const seenColumnSets = new Set();
298
+ const columnOf = (attrName) => resolvePhysicalColumnName(attrName, attributes, options, emittedColumns);
299
+ const add = (name, columns, asIndex, source) => {
300
+ if (columns.length === 0)
301
+ return;
302
+ for (const column of columns) {
303
+ if (!emittedColumns[column]) {
304
+ throw new Error(`Unique key "${name}" on table "${tableName}" (${source}) references ` +
305
+ `column "${column}", which the table does not have. ` +
306
+ `Known columns: ${Object.keys(emittedColumns).join(', ')}.`);
307
+ }
308
+ }
309
+ // Column order is part of a composite key's identity for index purposes but
310
+ // not for uniqueness, so the de-duplication signature is order-independent:
311
+ // (a, b) and (b, a) enforce exactly the same rule and one of them is enough.
312
+ const signature = [...columns].sort().join(' ');
313
+ if (seenNames.has(name) || seenColumnSets.has(signature))
314
+ return;
315
+ seenNames.add(name);
316
+ seenColumnSets.add(signature);
317
+ collected.push({ name, fields: columns, asIndex });
318
+ };
319
+ // 1. Attribute-level grouping. Insertion order of `attributes` is the
320
+ // declaration order the user wrote, which is the order the columns take.
321
+ const groups = new Map();
322
+ for (const [attrName, attr] of Object.entries(attributes)) {
323
+ const key = attr?.uniqueKey;
324
+ if (typeof key !== 'string' || key.length === 0)
325
+ continue;
326
+ const group = groups.get(key);
327
+ if (group)
328
+ group.push(columnOf(attrName));
329
+ else
330
+ groups.set(key, [columnOf(attrName)]);
331
+ }
332
+ for (const [name, columns] of groups) {
333
+ add(name, columns, false, 'attribute-level uniqueKey');
334
+ }
335
+ // 2. Model-level `uniqueKeys: UniqueKeyOptions[]`. Tolerate the legacy record
336
+ // shape here too - `ModelOptions.uniqueKeys` is typed as an array, but the
337
+ // dialects have always accepted `Record<name, fields[]>` under the same
338
+ // option name, and an untyped caller passing one should not be dropped.
339
+ const uniqueKeys = opts?.uniqueKeys;
340
+ if (Array.isArray(uniqueKeys)) {
341
+ for (const uk of uniqueKeys) {
342
+ if (!uk || !Array.isArray(uk.fields))
343
+ continue;
344
+ const columns = uk.fields.map(columnOf);
345
+ const name = uk.name || `${tableName}_${columns.join('_')}_key`;
346
+ add(name, columns, uk.unique === true, 'model-level uniqueKeys');
347
+ }
348
+ }
349
+ else if (uniqueKeys && typeof uniqueKeys === 'object') {
350
+ for (const [name, fields] of Object.entries(uniqueKeys)) {
351
+ if (!Array.isArray(fields))
352
+ continue;
353
+ const columns = fields.map(columnOf);
354
+ add(name || `${tableName}_${columns.join('_')}_key`, columns, false, 'model-level uniqueKeys');
355
+ }
356
+ }
357
+ // 3. Legacy `uniqueKeysLegacy` map.
358
+ const legacy = opts?.uniqueKeysLegacy;
359
+ if (legacy && typeof legacy === 'object') {
360
+ for (const [name, fields] of Object.entries(legacy)) {
361
+ if (!Array.isArray(fields))
362
+ continue;
363
+ const columns = fields.map(columnOf);
364
+ add(name || `${tableName}_${columns.join('_')}_key`, columns, false, 'uniqueKeysLegacy');
365
+ }
366
+ }
367
+ return collected;
368
+ }
557
369
  // Tracks (via Node's AsyncLocalStorage, which correctly follows a logical
558
370
  // call chain across `await`s regardless of how concurrent operations
559
371
  // interleave) whether the currently-executing code is running inside an
@@ -648,6 +460,17 @@ class Prorm extends events_1.EventEmitter {
648
460
  this._fdwManager = null;
649
461
  this._userManager = null;
650
462
  this._models = new Map();
463
+ /**
464
+ * The registry this instance was created by, if any.
465
+ *
466
+ * `ConnectionManager.addConnection()` sets this on the instance it builds,
467
+ * which is what makes `FindOptions.using` work: a query can name a sibling
468
+ * connection and be routed to that connection's copy of the model. Nothing
469
+ * else assigned it before, so `using` threw "Available: none" on every call.
470
+ * Typed structurally (rather than as `ConnectionManager`) to keep prorm.ts
471
+ * free of an import cycle with connection-manager.ts.
472
+ */
473
+ this.connectionManager = null;
651
474
  this.currentTransaction = null;
652
475
  // Serializes root-level (non-nested) transactions for dialects backed by a
653
476
  // single shared connection (currently just SQLite/better-sqlite3) - see
@@ -680,7 +503,7 @@ class Prorm extends events_1.EventEmitter {
680
503
  // i.e. missing the FOREIGN KEY/REFERENCES clause associations imply.
681
504
  this.pendingTableCreates = [];
682
505
  this._isConnected = false;
683
- this.hooksManager = new HooksManager();
506
+ this.hooksManager = new hooks_manager_1.HooksManager();
684
507
  this.modelHooks = new Map();
685
508
  /**
686
509
  * The connection pool instance
@@ -721,9 +544,11 @@ class Prorm extends events_1.EventEmitter {
721
544
  logSQL: options.logSQL || false,
722
545
  slowQueryThreshold: options.slowQueryThreshold || 1000,
723
546
  logSlowQueries: options.logSlowQueries !== false,
724
- prormLogging: typeof options.logging === 'function'
725
- ? options.logging
726
- : undefined,
547
+ // Deliberately NOT wired to `options.logging`. User-facing query logging
548
+ // is owned by `_logQuery`, which honours per-query overrides; handing the
549
+ // same callback to this logger too made it fire a second time and ignore
550
+ // `{ logging: false }` on an individual call. This logger's remaining job
551
+ // is slow-query detection and the debug/info/warn/error levels.
727
552
  colors: options.colors !== false,
728
553
  });
729
554
  // Set up query logging event handlers
@@ -780,7 +605,7 @@ class Prorm extends events_1.EventEmitter {
780
605
  * });
781
606
  */
782
607
  literal(sql) {
783
- return new Literal((0, operators_1.literal)(sql).$literal);
608
+ return new expressions_1.Literal((0, operators_1.literal)(sql).$literal);
784
609
  }
785
610
  /**
786
611
  * Set up the connection pool with event forwarding
@@ -1194,7 +1019,7 @@ class Prorm extends events_1.EventEmitter {
1194
1019
  sql: 'SELECT 1',
1195
1020
  duration,
1196
1021
  timestamp: new Date(),
1197
- type: QueryTypes.SELECT,
1022
+ type: query_types_1.QueryTypes.SELECT,
1198
1023
  });
1199
1024
  }
1200
1025
  catch (error) {
@@ -1293,40 +1118,31 @@ class Prorm extends events_1.EventEmitter {
1293
1118
  }
1294
1119
  /**
1295
1120
  * Disable foreign key checks for the current session
1121
+ *
1122
+ * On PostgreSQL this issues `SET session_replication_role = 'replica'`, which
1123
+ * needs superuser or a role granted `SET ON PARAMETER
1124
+ * session_replication_role` (PostgreSQL 15+).
1125
+ *
1126
+ * @throws {UnsupportedForeignKeyChecksError} on dialects with no
1127
+ * session-level switch (SQL Server, Oracle and Db2 are per-table; Redshift,
1128
+ * Snowflake and ClickHouse never enforce foreign keys).
1296
1129
  * @returns Promise that resolves when foreign key checks are disabled
1297
1130
  */
1298
1131
  async disableForeignKeyChecks() {
1299
- const dialect = this.getDialect();
1300
- let sql;
1301
- if (dialect === 'sqlite' || dialect === 'turso') {
1302
- sql = sql_constants_1.SQLITE.PRAGMA.foreign_keys.off;
1303
- }
1304
- else if (dialect === 'postgres') {
1305
- // PostgreSQL uses deferrable constraints, not FK checks
1306
- sql = 'SET FOREIGN_KEY_CHECKS = 0'; // Fallback
1307
- }
1308
- else {
1309
- sql = sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.disable;
1310
- }
1311
- await this.query(sql, { raw: true });
1132
+ await this.query(this.getDisableForeignKeyChecksSQL(), { raw: true });
1312
1133
  }
1313
1134
  /**
1314
1135
  * Enable foreign key checks for the current session
1136
+ *
1137
+ * The inverse of {@link disableForeignKeyChecks}, with the same dialect
1138
+ * support and the same PostgreSQL privilege requirement.
1139
+ *
1140
+ * @throws {UnsupportedForeignKeyChecksError} on dialects with no
1141
+ * session-level switch
1315
1142
  * @returns Promise that resolves when foreign key checks are enabled
1316
1143
  */
1317
1144
  async enableForeignKeyChecks() {
1318
- const dialect = this.getDialect();
1319
- let sql;
1320
- if (dialect === 'sqlite' || dialect === 'turso') {
1321
- sql = sql_constants_1.SQLITE.PRAGMA.foreign_keys.on;
1322
- }
1323
- else if (dialect === 'postgres') {
1324
- sql = 'SET FOREIGN_KEY_CHECKS = 1'; // Fallback
1325
- }
1326
- else {
1327
- sql = sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.enable;
1328
- }
1329
- await this.query(sql, { raw: true });
1145
+ await this.query(this.getEnableForeignKeyChecksSQL(), { raw: true });
1330
1146
  }
1331
1147
  /**
1332
1148
  * Set the transaction isolation level
@@ -1349,31 +1165,30 @@ class Prorm extends events_1.EventEmitter {
1349
1165
  }
1350
1166
  /**
1351
1167
  * Get SQL for disabling foreign key checks for the current dialect
1168
+ *
1169
+ * Routing lives in `sql-constants.getForeignKeyChecksSQL` so that the three
1170
+ * mutually incompatible statements (SQLite's PRAGMA, MySQL's session
1171
+ * variable, PostgreSQL's `session_replication_role`) are chosen from one
1172
+ * table. This used to fall through to MySQL syntax for every dialect it did
1173
+ * not name - PostgreSQL included - so `disableForeignKeyChecks()` threw a
1174
+ * syntax error at the server on most of the supported databases.
1175
+ *
1176
+ * @throws {UnsupportedForeignKeyChecksError} when the dialect has no
1177
+ * session-level equivalent
1352
1178
  * @returns SQL string for disabling foreign key checks
1353
1179
  */
1354
1180
  getDisableForeignKeyChecksSQL() {
1355
- const dialect = this.getDialect();
1356
- if (dialect === 'sqlite' || dialect === 'turso') {
1357
- return sql_constants_1.SQLITE.PRAGMA.foreign_keys.off;
1358
- }
1359
- else if (dialect === 'postgres') {
1360
- return 'SET FOREIGN_KEY_CHECKS = 0';
1361
- }
1362
- return sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.disable;
1181
+ return (0, sql_constants_1.getForeignKeyChecksSQL)(this.getDialect(), 'disable');
1363
1182
  }
1364
1183
  /**
1365
1184
  * Get SQL for enabling foreign key checks for the current dialect
1185
+ *
1186
+ * @throws {UnsupportedForeignKeyChecksError} when the dialect has no
1187
+ * session-level equivalent
1366
1188
  * @returns SQL string for enabling foreign key checks
1367
1189
  */
1368
1190
  getEnableForeignKeyChecksSQL() {
1369
- const dialect = this.getDialect();
1370
- if (dialect === 'sqlite' || dialect === 'turso') {
1371
- return sql_constants_1.SQLITE.PRAGMA.foreign_keys.on;
1372
- }
1373
- else if (dialect === 'postgres') {
1374
- return 'SET FOREIGN_KEY_CHECKS = 1';
1375
- }
1376
- return sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.enable;
1191
+ return (0, sql_constants_1.getForeignKeyChecksSQL)(this.getDialect(), 'enable');
1377
1192
  }
1378
1193
  /**
1379
1194
  * Get SQL for setting transaction isolation level for the current dialect
@@ -1392,6 +1207,19 @@ class Prorm extends events_1.EventEmitter {
1392
1207
  }
1393
1208
  /**
1394
1209
  * Disable unique key checks for the current session (MySQL/MariaDB only)
1210
+ *
1211
+ * The no-op on other dialects is deliberate, and is not the same situation as
1212
+ * the foreign key switch above. `SET UNIQUE_CHECKS` is a MySQL/MariaDB
1213
+ * *optimisation* hint: it defers uniqueness verification on InnoDB secondary
1214
+ * indexes during bulk loads. No other supported database has a session
1215
+ * setting that suspends unique constraint checking - PostgreSQL's
1216
+ * `session_replication_role = 'replica'` suppresses triggers and foreign
1217
+ * keys but still enforces unique indexes - so there is nothing to translate
1218
+ * it to. Skipping the statement leaves the database in exactly the state the
1219
+ * caller already had (uniqueness enforced), which is safe; throwing would
1220
+ * break the common `disableUniqueKeyChecks(); bulkCreate(); enable...()`
1221
+ * bulk-load pattern on every non-MySQL dialect for no benefit.
1222
+ *
1395
1223
  * @returns Promise that resolves when unique key checks are disabled
1396
1224
  */
1397
1225
  async disableUniqueKeyChecks() {
@@ -1402,6 +1230,9 @@ class Prorm extends events_1.EventEmitter {
1402
1230
  }
1403
1231
  /**
1404
1232
  * Enable unique key checks for the current session (MySQL/MariaDB only)
1233
+ *
1234
+ * A no-op elsewhere, for the reason given on {@link disableUniqueKeyChecks}.
1235
+ *
1405
1236
  * @returns Promise that resolves when unique key checks are enabled
1406
1237
  */
1407
1238
  async enableUniqueKeyChecks() {
@@ -1746,6 +1577,70 @@ class Prorm extends events_1.EventEmitter {
1746
1577
  }
1747
1578
  return val;
1748
1579
  }
1580
+ if (typeKey === 'JSON' || typeKey === 'JSONB') {
1581
+ // Dialects disagree about the wire format of a JSON column: SQLite
1582
+ // stores it as TEXT and hands back the raw string, while the
1583
+ // PostgreSQL and MySQL drivers parse it and hand back an object. That
1584
+ // made the same model code dialect-specific - `row.attrs.a` worked on
1585
+ // Postgres and threw on SQLite. Parse the string form here so every
1586
+ // dialect yields the same JS value.
1587
+ //
1588
+ // Parse defensively, exactly as `castValuesFromDatabase` in
1589
+ // models/model.ts does: a value that is already an object is left
1590
+ // alone, and a string that isn't valid JSON is returned untouched
1591
+ // rather than throwing or collapsing to undefined (a column that
1592
+ // predates the JSON type, or hand-written data, should still read
1593
+ // back as whatever it actually holds).
1594
+ if (typeof val === 'string') {
1595
+ try {
1596
+ return JSON.parse(val);
1597
+ }
1598
+ catch {
1599
+ // Not valid JSON. Rows written before the encoding fix above stored
1600
+ // strings raw, so this is the path that still reads them back
1601
+ // correctly; hand-written data and pre-JSON-type columns land here
1602
+ // too. Returning the raw string beats throwing or yielding
1603
+ // undefined.
1604
+ return val;
1605
+ }
1606
+ }
1607
+ return val;
1608
+ }
1609
+ return val;
1610
+ };
1611
+ // Helper: prepare a JS value for the wire format its column expects.
1612
+ //
1613
+ // Only JSON/JSONB needs this today, and it is the write-side mirror of
1614
+ // the JSON branch in `_coerceValue`. The base dialect's
1615
+ // `escapeReplacement()` does stringify a plain object, but it expands an
1616
+ // *array* into the `(a, b, c)` IN-list form, so `{ tags: ['a', 'b'] }` on
1617
+ // a JSON column produced invalid SQL instead of a JSON literal.
1618
+ // Serializing here settles the format before it reaches the dialect and
1619
+ // keeps write and read symmetric. Strings are passed through so a caller
1620
+ // that already did their own `JSON.stringify` isn't double-encoded.
1621
+ const _serializeValue = (key, val) => {
1622
+ const attr = attributes[key];
1623
+ if (attr === undefined || val === null || val === undefined)
1624
+ return val;
1625
+ const typeObj = (attr.type || attr);
1626
+ const typeKey = typeObj?.key || (typeof typeObj === 'string' ? typeObj : undefined);
1627
+ if (typeKey === 'JSON' || typeKey === 'JSONB') {
1628
+ // A Literal/Col is raw SQL the caller wants inlined, not data - the
1629
+ // insert/update builders detect it by class, so stringifying it here
1630
+ // would turn `literal("'{}'::jsonb")` into a quoted string.
1631
+ if (val instanceof expressions_1.Literal || val instanceof expressions_1.Col)
1632
+ return val;
1633
+ // Encode EVERY value, strings included. Returning a string unencoded
1634
+ // here is what made the round trip lossy: the value went to the column
1635
+ // raw, and the read side then JSON.parse'd it unconditionally, so any
1636
+ // string that happened to be valid JSON came back as something else -
1637
+ // '123' as the number 123, 'true' as a boolean, '{"a":1}' as an object,
1638
+ // and '12345678901234567890' silently rounded to 12345678901234567000.
1639
+ // It also produced invalid JSON on PostgreSQL, which rejects a bare
1640
+ // `hello` for a json column. Encoding strings makes write and read
1641
+ // symmetric, so a value round-trips as the type it was stored as.
1642
+ return JSON.stringify(val);
1643
+ }
1749
1644
  return val;
1750
1645
  };
1751
1646
  // Helper: wrap a plain data object with full instance methods (get/set/update/save/destroy/toJSON)
@@ -1960,15 +1855,26 @@ class Prorm extends events_1.EventEmitter {
1960
1855
  if (typeof modelHooksRef.beforeUpdate === 'function') {
1961
1856
  await modelHooksRef.beforeUpdate(rowInstance, {});
1962
1857
  }
1858
+ // beforeSave covers both halves of persistence, so a single hook can
1859
+ // normalize a record whether it is being created or updated.
1860
+ if (typeof modelHooksRef.beforeSave === 'function') {
1861
+ await modelHooksRef.beforeSave(rowInstance, {});
1862
+ }
1963
1863
  const pk = dataValues.id;
1964
1864
  if (pk !== undefined) {
1965
- await modelRef.update(vals, { where: { id: pk } });
1865
+ // This path has already fired the update hooks for this record;
1866
+ // without the marker the static update() fires them a second time,
1867
+ // with a different argument shape.
1868
+ await modelRef.update(vals, { where: { id: pk }, _hooksAlreadyRun: true });
1966
1869
  }
1967
1870
  Object.assign(dataValues, vals);
1968
1871
  Object.assign(rowInstance, vals);
1969
1872
  if (typeof modelHooksRef.afterUpdate === 'function') {
1970
1873
  await modelHooksRef.afterUpdate(rowInstance, {});
1971
1874
  }
1875
+ if (typeof modelHooksRef.afterSave === 'function') {
1876
+ await modelHooksRef.afterSave(rowInstance, {});
1877
+ }
1972
1878
  return rowInstance;
1973
1879
  },
1974
1880
  save: async () => {
@@ -2024,7 +1930,24 @@ class Prorm extends events_1.EventEmitter {
2024
1930
  saveVals[k] = v;
2025
1931
  }
2026
1932
  }
2027
- await modelRef.update(saveVals, { where: { id: pk } });
1933
+ // Fire the persistence hooks once, here, rather than letting the
1934
+ // delegated static update() fire a different-shaped pair.
1935
+ if (typeof modelHooksRef.beforeUpdate === 'function') {
1936
+ await modelHooksRef.beforeUpdate(rowInstance, {});
1937
+ }
1938
+ if (typeof modelHooksRef.beforeSave === 'function') {
1939
+ await modelHooksRef.beforeSave(rowInstance, {});
1940
+ }
1941
+ await modelRef.update(saveVals, {
1942
+ where: { id: pk },
1943
+ _hooksAlreadyRun: true,
1944
+ });
1945
+ if (typeof modelHooksRef.afterUpdate === 'function') {
1946
+ await modelHooksRef.afterUpdate(rowInstance, {});
1947
+ }
1948
+ if (typeof modelHooksRef.afterSave === 'function') {
1949
+ await modelHooksRef.afterSave(rowInstance, {});
1950
+ }
2028
1951
  }
2029
1952
  else {
2030
1953
  // New record - do an INSERT via create
@@ -2161,7 +2084,11 @@ class Prorm extends events_1.EventEmitter {
2161
2084
  // Validation methods
2162
2085
  validate: async function (options) {
2163
2086
  const modelOptions = modelRef.options || {};
2164
- const hooksManager = modelRef._hooksManager;
2087
+ // `validate({ hooks: true })` used to read `_hooksManager`, a property
2088
+ // nothing ever assigned - so the flag was accepted and the hooks never
2089
+ // ran. Use the model's declared hooks, the same source every other
2090
+ // lifecycle hook comes from.
2091
+ const validateHooks = modelOptions?.hooks || {};
2165
2092
  // Parse options
2166
2093
  let fieldsToValidate;
2167
2094
  let runHooks = false;
@@ -2177,8 +2104,8 @@ class Prorm extends events_1.EventEmitter {
2177
2104
  return { valid: true, errors: [] };
2178
2105
  }
2179
2106
  // Run beforeValidate hook if requested
2180
- if (runHooks && hooksManager) {
2181
- await hooksManager.runHook('beforeValidate', rowInstance, { model: modelRef });
2107
+ if (runHooks && typeof validateHooks.beforeValidate === 'function') {
2108
+ await validateHooks.beforeValidate(rowInstance, { model: modelRef });
2182
2109
  }
2183
2110
  const validations = {};
2184
2111
  // Collect validations from attributes
@@ -2216,8 +2143,10 @@ class Prorm extends events_1.EventEmitter {
2216
2143
  // Use the validator
2217
2144
  const result = (0, validator_1.validate)(dataToValidate, validations);
2218
2145
  // Run afterValidate hook if requested
2219
- if (runHooks && hooksManager && result.valid) {
2220
- await hooksManager.runHook('afterValidate', rowInstance, { model: modelRef });
2146
+ if (runHooks &&
2147
+ result.valid &&
2148
+ typeof validateHooks.afterValidate === 'function') {
2149
+ await validateHooks.afterValidate(rowInstance, { model: modelRef });
2221
2150
  }
2222
2151
  return {
2223
2152
  valid: result.valid,
@@ -2369,11 +2298,23 @@ class Prorm extends events_1.EventEmitter {
2369
2298
  return assoc.target.findOne({ where: { id: fkVal }, ...(opts || {}) });
2370
2299
  };
2371
2300
  rowInstance[`set${capitalize(assocAlias)}`] = async (instance) => {
2372
- const targetId = instance?.id ?? instance;
2301
+ // Passing null clears the association, matching `unset`.
2302
+ const targetId = instance === null ? null : (instance?.id ?? instance);
2373
2303
  dataValues[fk] = targetId;
2374
2304
  rowInstance[fk] = targetId;
2375
2305
  await modelRef.update({ [fk]: targetId }, { where: { id: dataValues.id } });
2376
2306
  };
2307
+ /**
2308
+ * Clear the association: null the foreign key and persist.
2309
+ *
2310
+ * `setX(null)` does the same thing, but reads as an assignment; this
2311
+ * says what it does at the call site.
2312
+ */
2313
+ rowInstance[`unset${capitalize(assocAlias)}`] = async () => {
2314
+ dataValues[fk] = null;
2315
+ rowInstance[fk] = null;
2316
+ await modelRef.update({ [fk]: null }, { where: { id: dataValues.id } });
2317
+ };
2377
2318
  // create/has/count were missing here while the equivalent mixins in
2378
2319
  // models/associations.ts defined them, so which helpers existed on an
2379
2320
  // instance depended on how the model had been defined.
@@ -2878,10 +2819,25 @@ class Prorm extends events_1.EventEmitter {
2878
2819
  * Find all records
2879
2820
  */
2880
2821
  async findAll(findOptions = {}) {
2822
+ // `using: 'name'` runs this query against a sibling connection
2823
+ // registered on the same ConnectionManager, rather than this one.
2824
+ // Resolve it before anything else touches `self.dialect` - the whole
2825
+ // point is that the query never runs here.
2826
+ if (findOptions.using) {
2827
+ return self._findAllUsingConnection(modelName, findOptions);
2828
+ }
2881
2829
  if (!self.dialect) {
2882
2830
  throw new Error('Database not connected');
2883
2831
  }
2884
2832
  const dialect = self.dialect;
2833
+ // beforeFind runs before scopes are merged so a hook sees - and can
2834
+ // adjust - the options the caller actually passed.
2835
+ const findHooks = this.options?.hooks || {};
2836
+ if (typeof findHooks.beforeFind === 'function') {
2837
+ const replaced = await findHooks.beforeFind(findOptions);
2838
+ if (replaced)
2839
+ findOptions = replaced;
2840
+ }
2885
2841
  // Apply scopes (defaultScope + named scopes)
2886
2842
  findOptions = this._mergeScopes(findOptions);
2887
2843
  // Get model options for paranoid configuration
@@ -2900,6 +2856,19 @@ class Prorm extends events_1.EventEmitter {
2900
2856
  [deletedAt]: { $isNull: true },
2901
2857
  };
2902
2858
  }
2859
+ // Resolve `{ all: true }` / `{ all: 'alias' }` into one concrete include
2860
+ // per association *before* anything reads the include list. This used to
2861
+ // happen inside `loadIncludes()`, at the very end of the query - which
2862
+ // is after the two passes below have already run over the raw list, so
2863
+ // `{ all: true, required: true }` reached `resolveRequiredIncludeFilter`
2864
+ // as an entry with no `model` and was skipped: the associations loaded
2865
+ // but the parents were never narrowed.
2866
+ if (findOptions.include && findOptions.include.length > 0) {
2867
+ findOptions = {
2868
+ ...findOptions,
2869
+ include: self._expandIncludes(model, modelName, findOptions.include),
2870
+ };
2871
+ }
2903
2872
  // Apply INNER-JOIN semantics for filtering includes.
2904
2873
  //
2905
2874
  // `include: [{ model: Post, required: true }]` and an include carrying
@@ -2929,24 +2898,11 @@ class Prorm extends events_1.EventEmitter {
2929
2898
  }
2930
2899
  findOptions = { ...findOptions, include: includeList };
2931
2900
  }
2932
- if (findOptions.include && findOptions.include.length > 0) {
2933
- for (const rawInclude of findOptions.include) {
2934
- const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(self.eagerLoadContext(), model, modelName, rawInclude);
2935
- if (!filter)
2936
- continue;
2937
- if (filter.values.length === 0) {
2938
- // No parent can satisfy a required include with no matches.
2939
- return [];
2940
- }
2941
- const existing = where?.[filter.parentAttr];
2942
- where = {
2943
- ...where,
2944
- [filter.parentAttr]: existing
2945
- ? { $and: [existing, { $in: filter.values }] }
2946
- : { $in: filter.values },
2947
- };
2948
- }
2949
- }
2901
+ const filteredWhere = await self._applyRequiredIncludeFilters(model, modelName, findOptions.include, where);
2902
+ // null means some required include matches nothing, so nothing matches.
2903
+ if (filteredWhere === null)
2904
+ return [];
2905
+ where = filteredWhere;
2950
2906
  // Translate attributes.exclude to explicit list (SQLite doesn't support "* EXCEPT(...)")
2951
2907
  let resolvedAttributes = findOptions.attributes;
2952
2908
  if (resolvedAttributes &&
@@ -2976,6 +2932,26 @@ class Prorm extends events_1.EventEmitter {
2976
2932
  subQuery: findOptions.subquery,
2977
2933
  benchmark: findOptions.benchmark,
2978
2934
  cte: findOptions.cte,
2935
+ // Row-level locking. `lock` was declared on FindOptions, documented,
2936
+ // and compiled by the dialects (PostgreSQL emits FOR UPDATE / FOR
2937
+ // SHARE / FOR KEY SHARE, MySQL LOCK IN SHARE MODE), but never made
2938
+ // it out of here - so `findAll({ lock: 'UPDATE', transaction: t })`
2939
+ // silently took no lock at all and code written to serialize
2940
+ // concurrent updates didn't.
2941
+ //
2942
+ // Dialects with no row-level locking (SQLite above all) drop the
2943
+ // clause rather than throwing. That is deliberate: SQLite already
2944
+ // serializes writers at the database level, so the transaction the
2945
+ // lock was protecting is serialized there anyway, and rejecting the
2946
+ // option would make otherwise-portable model code dialect-specific.
2947
+ lock: findOptions.lock,
2948
+ // UNION / UNION ALL / EXCEPT / INTERSECT branches appended after the
2949
+ // main query. Each dialect pushes the branches' bound values onto
2950
+ // the same `values` array in the order they are emitted, so the
2951
+ // single replaceReplacements() call below binds them correctly
2952
+ // alongside the main query's.
2953
+ unionType: findOptions.unionType,
2954
+ union: findOptions.union,
2979
2955
  });
2980
2956
  // Handle benchmark option
2981
2957
  const startTime = Date.now();
@@ -2984,44 +2960,15 @@ class Prorm extends events_1.EventEmitter {
2984
2960
  // Calculate query duration
2985
2961
  const duration = Date.now() - startTime;
2986
2962
  // Determine logging function
2987
- const shouldLog = findOptions.logging !== false && self.loggingOptions !== false;
2988
- const logFn = typeof findOptions.logging === 'function'
2989
- ? findOptions.logging
2990
- : typeof self.loggingOptions === 'function'
2991
- ? self.loggingOptions
2992
- : null;
2993
- // Log benchmark/timing if enabled
2994
- const benchmarkEnabled = findOptions.benchmark === true;
2995
- if (shouldLog || benchmarkEnabled) {
2996
- if (logFn) {
2997
- logFn(query.sql, duration);
2998
- }
2999
- else if (findOptions.logging === true ||
3000
- self.loggingOptions === true ||
3001
- benchmarkEnabled) {
3002
- console.log(`Executed (${duration}ms): ${query.sql}`);
3003
- }
3004
- // Log to internal logger for slow query detection
3005
- self.logger.logQuery(query.sql, duration);
3006
- }
3007
- // Emit query event for external listeners
3008
- self.emit('query', {
2963
+ // Query logging, slow-query detection and the query/slowQuery events all
2964
+ // go through one path so every operation behaves the same way.
2965
+ self._logQuery({
3009
2966
  sql: query.sql,
3010
2967
  duration,
3011
- timestamp: new Date(),
2968
+ parameters: query.values,
3012
2969
  model: modelName,
3013
- });
3014
- // Emit slow query event if query exceeds threshold
3015
- const slowQueryThreshold = self.config.slowQueryThreshold || 1000;
3016
- if (duration > slowQueryThreshold) {
3017
- self.emit('slowQuery', {
3018
- sql: query.sql,
3019
- duration,
3020
- threshold: slowQueryThreshold,
3021
- timestamp: new Date(),
3022
- model: modelName,
3023
- });
3024
- }
2970
+ type: 'SELECT',
2971
+ }, findOptions.logging);
3025
2972
  // Wrap each row with instance methods.
3026
2973
  //
3027
2974
  // `raw: true` asks for plain rows and was accepted but ignored - every
@@ -3034,6 +2981,12 @@ class Prorm extends events_1.EventEmitter {
3034
2981
  const wrappedRows = (findOptions.raw === true
3035
2982
  ? result.rows.map((row) => ({ ...row }))
3036
2983
  : result.rows.map((row) => _wrapInstance(row, modelRef, modelHooksRef)));
2984
+ // Related-row counts, without loading the rows. One grouped query per
2985
+ // association rather than one per parent.
2986
+ const withCount = findOptions.withCount;
2987
+ if (withCount?.length) {
2988
+ await (0, eager_load_1.loadCounts)(self.eagerLoadContext(), wrappedRows, model, modelName, withCount);
2989
+ }
3037
2990
  // Eager-load associations. The implementation is shared with the
3038
2991
  // class-based Model API (see models/eager-load.ts) - it used to be
3039
2992
  // duplicated here and in models/model.ts, and the two copies had
@@ -3041,6 +2994,11 @@ class Prorm extends events_1.EventEmitter {
3041
2994
  if (findOptions.include && findOptions.include.length > 0) {
3042
2995
  await (0, eager_load_1.loadIncludes)(self.eagerLoadContext(), wrappedRows, model, modelName, findOptions.include);
3043
2996
  }
2997
+ if (typeof findHooks.afterFind === 'function') {
2998
+ const replaced = await findHooks.afterFind(wrappedRows, findOptions);
2999
+ if (replaced)
3000
+ return replaced;
3001
+ }
3044
3002
  return wrappedRows;
3045
3003
  },
3046
3004
  /**
@@ -3051,7 +3009,8 @@ class Prorm extends events_1.EventEmitter {
3051
3009
  // Count with an aggregate rather than by re-running the full query
3052
3010
  // without a limit and measuring the array. `limit`/`offset` are dropped
3053
3011
  // (the count describes the whole matching set), everything else that
3054
- // narrows the set - including filtering includes - is preserved.
3012
+ // narrows the set - filtering includes and `union` branches included -
3013
+ // is preserved, so `count` and `rows` always describe the same set.
3055
3014
  const { limit: _limit, offset: _offset, ...countable } = this._mergeScopes(findOptions);
3056
3015
  const count = await self._countRows(model, modelName, countable);
3057
3016
  return { rows, count };
@@ -3313,6 +3272,11 @@ class Prorm extends events_1.EventEmitter {
3313
3272
  if (typeof modelHooks.beforeCreate === 'function') {
3314
3273
  await modelHooks.beforeCreate(hookInstance, createOptions);
3315
3274
  }
3275
+ // beforeSave fires for both create and update, so a single hook can
3276
+ // normalize a record however it is being persisted.
3277
+ if (typeof modelHooks.beforeSave === 'function') {
3278
+ await modelHooks.beforeSave(hookInstance, createOptions);
3279
+ }
3316
3280
  // Build dbValues from hookInstance (after hooks may have modified it), filtering virtual fields and respecting 'fields' option
3317
3281
  const allowedFields = createOptions.fields;
3318
3282
  const dbValues = {};
@@ -3325,7 +3289,7 @@ class Prorm extends events_1.EventEmitter {
3325
3289
  if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
3326
3290
  continue;
3327
3291
  }
3328
- dbValues[key] = value;
3292
+ dbValues[key] = _serializeValue(key, value);
3329
3293
  }
3330
3294
  const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {
3331
3295
  returning: createOptions.returning,
@@ -3420,6 +3384,9 @@ class Prorm extends events_1.EventEmitter {
3420
3384
  if (typeof modelHooks.afterCreate === 'function') {
3421
3385
  await modelHooks.afterCreate(createdInstance, createOptions);
3422
3386
  }
3387
+ if (typeof modelHooks.afterSave === 'function') {
3388
+ await modelHooks.afterSave(createdInstance, createOptions);
3389
+ }
3423
3390
  return createdInstance;
3424
3391
  },
3425
3392
  /**
@@ -3432,11 +3399,27 @@ class Prorm extends events_1.EventEmitter {
3432
3399
  throw new Error('Database not connected');
3433
3400
  }
3434
3401
  const bulkModelHooks = this.options?.hooks || {};
3402
+ // `instance.update()` fires the update hooks itself before delegating
3403
+ // here; the marker stops them running twice per call.
3404
+ const hooksAlreadyRun = updateOptions?._hooksAlreadyRun === true;
3405
+ // beforeUpdate fires for the statement as a whole, alongside the bulk
3406
+ // hook; it previously only ran for `instance.update()`, so a model-level
3407
+ // update bypassed it entirely.
3408
+ if (!hooksAlreadyRun && typeof bulkModelHooks.beforeUpdate === 'function') {
3409
+ await bulkModelHooks.beforeUpdate(values, updateOptions);
3410
+ }
3435
3411
  if (typeof bulkModelHooks.beforeBulkUpdate === 'function') {
3436
3412
  await bulkModelHooks.beforeBulkUpdate({ ...updateOptions, attributes: values });
3437
3413
  }
3438
3414
  const dialect = self.dialect;
3439
- const { sql: updateSql, values: updateVals } = dialect.buildUpdateQuery(this.tableName, values, updateOptions.where, { limit: updateOptions.limit, returning: updateOptions.returning });
3415
+ // Serialize JSON/JSONB columns on the way to the dialect (see
3416
+ // `_serializeValue`). `values` itself is left untouched so the
3417
+ // after-hooks below still see what the caller passed.
3418
+ const dbValues = {};
3419
+ for (const [key, value] of Object.entries(values)) {
3420
+ dbValues[key] = _serializeValue(key, value);
3421
+ }
3422
+ const { sql: updateSql, values: updateVals } = dialect.buildUpdateQuery(this.tableName, dbValues, updateOptions.where, { limit: updateOptions.limit, returning: updateOptions.returning });
3440
3423
  const sql = dialect.replaceReplacements(updateSql, updateVals);
3441
3424
  // Handle logging
3442
3425
  const startTime = Date.now();
@@ -3466,6 +3449,9 @@ class Prorm extends events_1.EventEmitter {
3466
3449
  model: modelName,
3467
3450
  type: 'UPDATE',
3468
3451
  });
3452
+ if (!hooksAlreadyRun && typeof bulkModelHooks.afterUpdate === 'function') {
3453
+ await bulkModelHooks.afterUpdate(values, updateOptions);
3454
+ }
3469
3455
  if (typeof bulkModelHooks.afterBulkUpdate === 'function') {
3470
3456
  await bulkModelHooks.afterBulkUpdate({ ...updateOptions, attributes: values });
3471
3457
  }
@@ -3482,6 +3468,11 @@ class Prorm extends events_1.EventEmitter {
3482
3468
  if (!destroyOptions.where) {
3483
3469
  throw new Error('Missing where option in destroy');
3484
3470
  }
3471
+ // As with update, this previously only fired for `instance.destroy()`.
3472
+ const destroyHooks = this.options?.hooks || {};
3473
+ if (typeof destroyHooks.beforeDestroy === 'function') {
3474
+ await destroyHooks.beforeDestroy(destroyOptions);
3475
+ }
3485
3476
  const dialect = self.dialect;
3486
3477
  const modelOptions = this.options;
3487
3478
  const bulkDestroyHooks = modelOptions?.hooks || {};
@@ -3555,6 +3546,9 @@ class Prorm extends events_1.EventEmitter {
3555
3546
  model: modelName,
3556
3547
  type: isParanoid && !forceDelete ? 'UPDATE' : 'DELETE',
3557
3548
  });
3549
+ if (typeof destroyHooks.afterDestroy === 'function') {
3550
+ await destroyHooks.afterDestroy(destroyOptions);
3551
+ }
3558
3552
  if (typeof bulkDestroyHooks.afterBulkDestroy === 'function') {
3559
3553
  await bulkDestroyHooks.afterBulkDestroy(destroyOptions);
3560
3554
  }
@@ -3615,6 +3609,11 @@ class Prorm extends events_1.EventEmitter {
3615
3609
  const dialect = self.dialect;
3616
3610
  for (const record of records) {
3617
3611
  const dbValues = {};
3612
+ // The values as the caller supplied them, used to build the
3613
+ // returned record: `dbValues` holds the wire format (a JSON column
3614
+ // is serialized on the way to the dialect), which is not what a
3615
+ // caller should get back from bulkCreate.
3616
+ const jsValues = {};
3618
3617
  for (const [key, value] of Object.entries(record)) {
3619
3618
  const attr = attributes[key];
3620
3619
  if (attr && attr.type) {
@@ -3622,14 +3621,23 @@ class Prorm extends events_1.EventEmitter {
3622
3621
  if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
3623
3622
  continue;
3624
3623
  }
3625
- dbValues[key] = value;
3624
+ dbValues[key] = _serializeValue(key, value);
3625
+ jsValues[key] = value;
3626
3626
  }
3627
3627
  const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {});
3628
3628
  const finalSql = dialect.replaceReplacements(sql, insertValues);
3629
+ const bcStarted = Date.now();
3629
3630
  const insertResult = await dialect.query(finalSql);
3631
+ self._logQuery({
3632
+ sql: finalSql,
3633
+ duration: Date.now() - bcStarted,
3634
+ parameters: insertValues,
3635
+ model: modelName,
3636
+ type: 'INSERT',
3637
+ }, bulkCreateOptions?.logging);
3630
3638
  const lastId = insertResult.lastInsertRowid;
3631
3639
  results.push({
3632
- ...dbValues,
3640
+ ...jsValues,
3633
3641
  ...(lastId !== undefined && !record.id ? { id: lastId } : {}),
3634
3642
  isNewRecord: false,
3635
3643
  });
@@ -3653,6 +3661,12 @@ class Prorm extends events_1.EventEmitter {
3653
3661
  * and `distinct`, `col` and `group` were silently discarded.
3654
3662
  */
3655
3663
  async count(countOptions = {}) {
3664
+ const countHooks = this.options?.hooks || {};
3665
+ if (typeof countHooks.beforeCount === 'function') {
3666
+ const replaced = await countHooks.beforeCount(countOptions);
3667
+ if (replaced)
3668
+ countOptions = replaced;
3669
+ }
3656
3670
  // Apply defaultScope / named scopes exactly as findAll does, so
3657
3671
  // `User.scope('admin').count()` counts the scoped set.
3658
3672
  return self._countRows(model, modelName, this._mergeScopes(countOptions));
@@ -3926,7 +3940,148 @@ class Prorm extends events_1.EventEmitter {
3926
3940
  async avg(attribute, aggOptions = {}) {
3927
3941
  return self._aggregate(model, modelName, 'AVG', attribute, this._mergeScopes(aggOptions));
3928
3942
  },
3943
+ /**
3944
+ * Like `findOne`, but throws `EmptyResultError` when nothing matches.
3945
+ *
3946
+ * Saves the `if (!row) throw ...` that otherwise follows every lookup
3947
+ * whose absence is a genuine error rather than a case to handle.
3948
+ */
3949
+ async findOneOrFail(findOptions = {}) {
3950
+ const row = await this.findOne(findOptions);
3951
+ if (!row) {
3952
+ throw new errors_1.EmptyResultError(`No ${modelName} matched the given conditions`, {
3953
+ model: modelName,
3954
+ });
3955
+ }
3956
+ return row;
3957
+ },
3958
+ /** Like `findByPk`, but throws `EmptyResultError` when the row is absent. */
3959
+ async findByPkOrFail(pk, findOptions = {}) {
3960
+ const row = await this.findByPk(pk, findOptions);
3961
+ if (!row) {
3962
+ throw new errors_1.EmptyResultError(`${modelName} ${String(pk)} not found`, {
3963
+ model: modelName,
3964
+ primaryKey: pk,
3965
+ });
3966
+ }
3967
+ return row;
3968
+ },
3969
+ /** The last row in `order`, or null. Inverts the ordering and takes one. */
3970
+ async findLast(findOptions = {}) {
3971
+ const pkAttr = resolveModelPrimaryKeyAttr(model);
3972
+ const order = findOptions.order ?? [[pkAttr, 'ASC']];
3973
+ const inverted = order.map((entry) => {
3974
+ if (!Array.isArray(entry))
3975
+ return [entry, 'DESC'];
3976
+ const [col, dir] = entry;
3977
+ return [col, String(dir ?? 'ASC').toUpperCase() === 'DESC' ? 'ASC' : 'DESC'];
3978
+ });
3979
+ const rows = await this.findAll({ ...findOptions, order: inverted, limit: 1 });
3980
+ return rows[0] ?? null;
3981
+ },
3982
+ /**
3983
+ * Whether any row matches, without fetching one.
3984
+ *
3985
+ * `SELECT COUNT(*) ... LIMIT 1` rather than loading a row and testing it
3986
+ * for null.
3987
+ */
3988
+ async exists(where = {}) {
3989
+ return (await self._countRows(model, modelName, this._mergeScopes({ where }))) > 0;
3990
+ },
3991
+ /**
3992
+ * Walk the whole matching set in batches, without holding it in memory.
3993
+ *
3994
+ * Pages by primary key rather than OFFSET, so the cost per batch stays
3995
+ * flat instead of degrading as the offset grows, and rows written during
3996
+ * the walk cannot cause a page to be skipped.
3997
+ *
3998
+ * ```typescript
3999
+ * await User.findInBatches({ where: { active: true } }, async (batch) => {
4000
+ * await sendDigest(batch);
4001
+ * }, { batchSize: 500 });
4002
+ * ```
4003
+ */
4004
+ async findInBatches(findOptions, handler, options = {}) {
4005
+ const size = options.batchSize ?? 100;
4006
+ const pkAttr = resolveModelPrimaryKeyAttr(model);
4007
+ let cursor = undefined;
4008
+ let seen = 0;
4009
+ for (;;) {
4010
+ const where = cursor === undefined
4011
+ ? findOptions.where
4012
+ : {
4013
+ ...findOptions.where,
4014
+ [pkAttr]: { $gt: cursor },
4015
+ };
4016
+ const batch = await this.findAll({
4017
+ ...findOptions,
4018
+ where,
4019
+ order: [[pkAttr, 'ASC']],
4020
+ limit: size,
4021
+ });
4022
+ if (batch.length === 0)
4023
+ break;
4024
+ await handler(batch);
4025
+ seen += batch.length;
4026
+ cursor = batch[batch.length - 1][pkAttr];
4027
+ if (batch.length < size)
4028
+ break;
4029
+ }
4030
+ return seen;
4031
+ },
4032
+ /** Call `handler` once per matching row, in batches. See `findInBatches`. */
4033
+ async findEach(findOptions, handler, options = {}) {
4034
+ return this.findInBatches(findOptions, async (batch) => {
4035
+ for (const row of batch)
4036
+ await handler(row);
4037
+ }, options);
4038
+ },
4039
+ /**
4040
+ * Add to one or more numeric columns in the database.
4041
+ *
4042
+ * `SET n = n + 1` in a single statement, so concurrent callers each apply
4043
+ * their delta. Reading a value, adding to it in JS and writing it back
4044
+ * loses one of two simultaneous increments.
4045
+ *
4046
+ * ```typescript
4047
+ * await Post.increment('views', { where: { id: 1 } });
4048
+ * await Post.increment(['views', 'score'], { by: 2, where: { id: 1 } });
4049
+ * await Post.increment({ views: 1, score: -1 }, { where: { id: 1 } });
4050
+ * ```
4051
+ *
4052
+ * @returns the number of rows affected
4053
+ */
4054
+ async increment(fields, options = {}) {
4055
+ return self._applyDelta(model, modelName, fields, options, 1);
4056
+ },
4057
+ /** Subtract from one or more numeric columns. See `increment`. */
4058
+ async decrement(fields, options = {}) {
4059
+ return self._applyDelta(model, modelName, fields, options, -1);
4060
+ },
4061
+ /**
4062
+ * Update many rows to *different* values in one statement.
4063
+ *
4064
+ * Looping `update()` costs one round trip per row. This compiles a single
4065
+ * UPDATE with a CASE per column, which every supported dialect accepts:
4066
+ *
4067
+ * ```sql
4068
+ * UPDATE users SET name = CASE id WHEN 1 THEN 'a' WHEN 2 THEN 'b' END
4069
+ * WHERE id IN (1, 2)
4070
+ * ```
4071
+ *
4072
+ * Each row must carry the primary key. Columns absent from a given row
4073
+ * keep their current value.
4074
+ *
4075
+ * @returns the number of rows affected
4076
+ */
4077
+ async bulkUpdate(rows, options = {}) {
4078
+ return self._bulkUpdate(model, modelName, rows, options);
4079
+ },
3929
4080
  async upsert(values, upsertOptions) {
4081
+ const upsertHooks = this.options?.hooks || {};
4082
+ if (typeof upsertHooks.beforeUpsert === 'function') {
4083
+ await upsertHooks.beforeUpsert(values, upsertOptions);
4084
+ }
3930
4085
  if (!self.dialect) {
3931
4086
  throw new Error('Database not connected');
3932
4087
  }
@@ -3949,7 +4104,9 @@ class Prorm extends events_1.EventEmitter {
3949
4104
  if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
3950
4105
  continue;
3951
4106
  }
3952
- dbValues[key] = value;
4107
+ // Serialized on the way to the dialect (JSON/JSONB) - see
4108
+ // `_serializeValue`.
4109
+ dbValues[key] = _serializeValue(key, value);
3953
4110
  }
3954
4111
  const columns = Object.keys(dbValues);
3955
4112
  // Default the conflict target: prefer an explicit primary key value, but
@@ -4035,6 +4192,9 @@ class Prorm extends events_1.EventEmitter {
4035
4192
  if (!instance) {
4036
4193
  instance = _wrapInstance({ ...dbValues }, model, options.hooks || {});
4037
4194
  }
4195
+ if (typeof upsertHooks.afterUpsert === 'function') {
4196
+ await upsertHooks.afterUpsert([instance, created], upsertOptions);
4197
+ }
4038
4198
  return [instance, created];
4039
4199
  },
4040
4200
  // ============ Model-level Hooks ============
@@ -4050,7 +4210,7 @@ class Prorm extends events_1.EventEmitter {
4050
4210
  beforeSync(callback) {
4051
4211
  let modelHook = self.modelHooks.get(modelName);
4052
4212
  if (!modelHook) {
4053
- modelHook = new ModelHooks();
4213
+ modelHook = new hooks_manager_1.ModelHooks();
4054
4214
  self.modelHooks.set(modelName, modelHook);
4055
4215
  }
4056
4216
  modelHook.beforeSync(callback);
@@ -4067,7 +4227,7 @@ class Prorm extends events_1.EventEmitter {
4067
4227
  afterSync(callback) {
4068
4228
  let modelHook = self.modelHooks.get(modelName);
4069
4229
  if (!modelHook) {
4070
- modelHook = new ModelHooks();
4230
+ modelHook = new hooks_manager_1.ModelHooks();
4071
4231
  self.modelHooks.set(modelName, modelHook);
4072
4232
  }
4073
4233
  modelHook.afterSync(callback);
@@ -4182,9 +4342,16 @@ class Prorm extends events_1.EventEmitter {
4182
4342
  */
4183
4343
  addModel(modelClass) {
4184
4344
  const modelName = modelClass.name;
4185
- // Get model metadata from decorators
4186
- const modelMeta = (0, decorators_1.getModelMetadata)(modelName);
4187
- const attrMeta = (0, decorators_1.getAttributeMetadata)(modelName);
4345
+ if ((0, decorators_1.isAbstractModel)(modelClass)) {
4346
+ throw new Error(`${modelName} is declared @AbstractModel and has no table of its own. ` +
4347
+ `Register the concrete models that extend it instead.`);
4348
+ }
4349
+ // Get model metadata from decorators. Attributes are collected up the
4350
+ // prototype chain so columns declared on an @AbstractModel base are
4351
+ // inherited; metadata is keyed by constructor name, so without this a
4352
+ // subclass would only see the columns declared directly on it.
4353
+ const modelMeta = (0, decorators_1.getModelMetadata)(modelName) ?? inheritedModelMetadata(modelClass);
4354
+ const attrMeta = (0, decorators_1.getInheritedAttributeMetadata)(modelClass);
4188
4355
  // A plain class with no @Table/@Attribute decorators is still valid — register it
4189
4356
  // with no attributes rather than failing, matching a bare `class Foo extends Model {}`.
4190
4357
  // Convert attribute metadata to Prorm format
@@ -4523,9 +4690,29 @@ class Prorm extends events_1.EventEmitter {
4523
4690
  : undefined,
4524
4691
  check: c.check,
4525
4692
  }));
4693
+ // Composite unique keys, from all three declaration forms. They are turned
4694
+ // into ordinary table-level UNIQUE constraints - and unique indexes, for
4695
+ // the entries that asked for one - rather than being forwarded as the
4696
+ // dialects' `uniqueKeys` option, because that option is honored
4697
+ // inconsistently: ClickHouse warns and drops it, and roughly half the
4698
+ // dialects have no handling for it at all, while every dialect renders
4699
+ // `constraints`. (The SQLite dialect built its list and then never used it,
4700
+ // which is fixed there too, for callers who reach `createTable` directly.)
4701
+ // Forwarding both would emit the constraint twice on Postgres/MySQL/MariaDB,
4702
+ // which do implement it.
4703
+ const uniqueKeys = collectUniqueKeys(tableName, attributes, options, columns);
4526
4704
  const allTableConstraints = [
4527
4705
  ...(tableConstraints || []),
4528
4706
  ...synthesizedFkConstraints,
4707
+ ...uniqueKeys
4708
+ .filter((uk) => !uk.asIndex)
4709
+ .map((uk) => ({ name: uk.name, type: 'UNIQUE', fields: uk.fields })),
4710
+ ];
4711
+ const allTableIndexes = [
4712
+ ...(tableIndexes || []),
4713
+ ...uniqueKeys
4714
+ .filter((uk) => uk.asIndex)
4715
+ .map((uk) => ({ name: uk.name, unique: true, fields: uk.fields })),
4529
4716
  ];
4530
4717
  await this.dialect.createTable(tableName, columns, {
4531
4718
  ifNotExists: true,
@@ -4535,8 +4722,7 @@ class Prorm extends events_1.EventEmitter {
4535
4722
  comment: options.comment,
4536
4723
  rowFormat: options.rowFormat,
4537
4724
  initialAutoIncrement: options.initialAutoIncrement,
4538
- uniqueKeys: options.uniqueKeys,
4539
- indexes: tableIndexes,
4725
+ indexes: allTableIndexes.length ? allTableIndexes : undefined,
4540
4726
  constraints: allTableConstraints,
4541
4727
  });
4542
4728
  }
@@ -4621,30 +4807,30 @@ class Prorm extends events_1.EventEmitter {
4621
4807
  }
4622
4808
  const trimmedSql = sql.trim().toUpperCase();
4623
4809
  if (trimmedSql.startsWith('SELECT')) {
4624
- return QueryTypes.SELECT;
4810
+ return query_types_1.QueryTypes.SELECT;
4625
4811
  }
4626
4812
  if (trimmedSql.startsWith('INSERT')) {
4627
4813
  // Check for UPSERT patterns: ON CONFLICT, ON DUPLICATE KEY, INSERT OR REPLACE
4628
4814
  if (trimmedSql.includes('ON CONFLICT') ||
4629
4815
  trimmedSql.includes('ON DUPLICATE KEY') ||
4630
4816
  trimmedSql.includes('INSERT OR REPLACE')) {
4631
- return QueryTypes.UPSERT;
4817
+ return query_types_1.QueryTypes.UPSERT;
4632
4818
  }
4633
- return QueryTypes.INSERT;
4819
+ return query_types_1.QueryTypes.INSERT;
4634
4820
  }
4635
4821
  if (trimmedSql.startsWith('UPDATE')) {
4636
- return QueryTypes.UPDATE;
4822
+ return query_types_1.QueryTypes.UPDATE;
4637
4823
  }
4638
4824
  if (trimmedSql.startsWith('DELETE')) {
4639
- return QueryTypes.DELETE;
4825
+ return query_types_1.QueryTypes.DELETE;
4640
4826
  }
4641
4827
  if (trimmedSql.startsWith('UPSERT')) {
4642
- return QueryTypes.UPSERT;
4828
+ return query_types_1.QueryTypes.UPSERT;
4643
4829
  }
4644
4830
  if (trimmedSql.startsWith('PRAGMA') || trimmedSql.startsWith('EXPLAIN')) {
4645
- return QueryTypes.RAW;
4831
+ return query_types_1.QueryTypes.RAW;
4646
4832
  }
4647
- return QueryTypes.RAW;
4833
+ return query_types_1.QueryTypes.RAW;
4648
4834
  }
4649
4835
  /**
4650
4836
  * Execute a raw query
@@ -4755,10 +4941,10 @@ class Prorm extends events_1.EventEmitter {
4755
4941
  }
4756
4942
  // Process results based on query type
4757
4943
  switch (queryType) {
4758
- case QueryTypes.SELECT:
4759
- case QueryTypes.SHOWTABLES:
4760
- case QueryTypes.DESCRIBE:
4761
- case QueryTypes.VERSION: {
4944
+ case query_types_1.QueryTypes.SELECT:
4945
+ case query_types_1.QueryTypes.SHOWTABLES:
4946
+ case query_types_1.QueryTypes.DESCRIBE:
4947
+ case query_types_1.QueryTypes.VERSION: {
4762
4948
  // Map raw results to model instances if requested
4763
4949
  if (options?.mapToModel && options?.model) {
4764
4950
  const ModelStatic = options.model;
@@ -4768,22 +4954,22 @@ class Prorm extends events_1.EventEmitter {
4768
4954
  // Return array of results
4769
4955
  return result.rows;
4770
4956
  }
4771
- case QueryTypes.INSERT:
4957
+ case query_types_1.QueryTypes.INSERT:
4772
4958
  // For INSERT, return [results, created]
4773
4959
  // Check if a row was inserted by examining the last insert rowid
4774
4960
  const insertId = result.rowid || result.insertId || 0;
4775
4961
  const created = insertId > 0;
4776
4962
  return [result.rows, created];
4777
- case QueryTypes.UPDATE:
4778
- case QueryTypes.DELETE:
4779
- case QueryTypes.BULKDELETE:
4780
- case QueryTypes.UPSERT:
4963
+ case query_types_1.QueryTypes.UPDATE:
4964
+ case query_types_1.QueryTypes.DELETE:
4965
+ case query_types_1.QueryTypes.BULKDELETE:
4966
+ case query_types_1.QueryTypes.UPSERT:
4781
4967
  // Return affected count
4782
4968
  return result.rowCount;
4783
- case QueryTypes.BULKINSERT:
4969
+ case query_types_1.QueryTypes.BULKINSERT:
4784
4970
  // Return number of inserted rows
4785
4971
  return result.rowCount;
4786
- case QueryTypes.RAW:
4972
+ case query_types_1.QueryTypes.RAW:
4787
4973
  default:
4788
4974
  // For RAW queries, return rows
4789
4975
  return result.rows;
@@ -4843,10 +5029,10 @@ class Prorm extends events_1.EventEmitter {
4843
5029
  // Determine query type
4844
5030
  const queryType = this.determineQueryType(processedSql, options?.type);
4845
5031
  const isSelect = [
4846
- QueryTypes.SELECT,
4847
- QueryTypes.SHOWTABLES,
4848
- QueryTypes.DESCRIBE,
4849
- QueryTypes.VERSION,
5032
+ query_types_1.QueryTypes.SELECT,
5033
+ query_types_1.QueryTypes.SHOWTABLES,
5034
+ query_types_1.QueryTypes.DESCRIBE,
5035
+ query_types_1.QueryTypes.VERSION,
4850
5036
  ].includes(queryType);
4851
5037
  // Execute the query
4852
5038
  const result = await this.dialect.query(processedSql, { raw: options?.raw });
@@ -6321,6 +6507,103 @@ $$ LANGUAGE plpgsql;`;
6321
6507
  }
6322
6508
  return Array.from(names);
6323
6509
  }
6510
+ /**
6511
+ * Run a `findAll` against the connection named by `FindOptions.using`.
6512
+ *
6513
+ * `using` lets one query hop to a sibling database registered on the same
6514
+ * `ConnectionManager` (`User.findAll({ using: 'analytics' })`). The target
6515
+ * connection has its own models, so this re-dispatches to *that*
6516
+ * connection's copy of the model by name and drops `using` from the options
6517
+ * so the delegate runs the query normally instead of bouncing forever.
6518
+ *
6519
+ * Both failure modes report what is actually available, since a typo in a
6520
+ * connection name and a model that was never defined on the target
6521
+ * connection are otherwise indistinguishable from "the query returned
6522
+ * nothing".
6523
+ */
6524
+ async _findAllUsingConnection(modelName, findOptions) {
6525
+ const name = findOptions.using;
6526
+ const registry = this.connectionManager;
6527
+ const connection = registry?.getConnection(name);
6528
+ if (!connection) {
6529
+ const available = registry?.getConnectionNames().join(', ') || 'none';
6530
+ throw new Error(`Connection '${name}' not found. Available: ${available}`);
6531
+ }
6532
+ const target = connection.models[modelName];
6533
+ if (!target) {
6534
+ const defined = Object.keys(connection.models).join(', ') || 'none';
6535
+ throw new Error(`Model '${modelName}' is not defined on connection '${name}'. Defined there: ${defined}`);
6536
+ }
6537
+ const { using: _using, ...queryOptions } = findOptions;
6538
+ return target.findAll(queryOptions);
6539
+ }
6540
+ /**
6541
+ * Resolve an `include` list into concrete, per-association entries.
6542
+ *
6543
+ * Every pass that reads an include - the cross-table `$assoc.col$` rewrite,
6544
+ * the parent-filtering (`required`) pass, and the eager loader itself - is
6545
+ * written against an entry that carries a `model`. `{ all: true }` carries
6546
+ * none, so each of those passes has to see the *expanded* list or it silently
6547
+ * does nothing for `all`: that is how `include: [{ all: true, required: true }]`
6548
+ * came to load the associations without narrowing the parents, while the
6549
+ * equivalent explicit include filtered correctly.
6550
+ *
6551
+ * Expansion is idempotent - a list with no `all` entry comes back as the same
6552
+ * entries - so running it here as well as inside `loadIncludes()` is safe.
6553
+ */
6554
+ _expandIncludes(model, modelName, include) {
6555
+ if (!Array.isArray(include) || include.length === 0)
6556
+ return [];
6557
+ return (0, eager_load_1.expandAllIncludes)(this.eagerLoadContext(), model, modelName, include);
6558
+ }
6559
+ /**
6560
+ * Narrow a WHERE clause by the query's filtering (`required`) includes.
6561
+ *
6562
+ * Shared by `findAll`, `_countRows` and `_aggregate` so that a filtering
6563
+ * include restricts the rows, the count and an aggregate to the same set.
6564
+ * Returns `null` when some include can match no parent at all - the whole
6565
+ * query is then empty, and each caller spells "empty" its own way (no rows,
6566
+ * a count of 0, a null aggregate).
6567
+ *
6568
+ * Several includes can constrain the *same* parent column, and since
6569
+ * `{ all: true, required: true }` expands to one include per association that
6570
+ * is now the common case rather than a corner. Their key sets are intersected
6571
+ * here. They must not be emitted as two conditions on one key: a
6572
+ * `{ id: { $and: [{ $in: [...] }, { $in: [...] }] } }` shape is not something
6573
+ * the where-builders recognize, and it collapsed to a single `"id" = ?` -
6574
+ * which is why two `required` includes used to return the wrong rows.
6575
+ */
6576
+ async _applyRequiredIncludeFilters(model, modelName, include, where) {
6577
+ const byParentAttr = new Map();
6578
+ for (const rawInclude of this._expandIncludes(model, modelName, include)) {
6579
+ const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
6580
+ if (!filter)
6581
+ continue;
6582
+ // No parent can satisfy a required include with no matches.
6583
+ if (filter.values.length === 0)
6584
+ return null;
6585
+ const previous = byParentAttr.get(filter.parentAttr);
6586
+ if (!previous) {
6587
+ byParentAttr.set(filter.parentAttr, filter.values);
6588
+ continue;
6589
+ }
6590
+ const keep = new Set(filter.values);
6591
+ const intersection = previous.filter((v) => keep.has(v));
6592
+ if (intersection.length === 0)
6593
+ return null;
6594
+ byParentAttr.set(filter.parentAttr, intersection);
6595
+ }
6596
+ let narrowed = where;
6597
+ for (const [parentAttr, values] of byParentAttr) {
6598
+ // A condition the caller wrote on the same column still applies on top.
6599
+ const existing = narrowed?.[parentAttr];
6600
+ narrowed = {
6601
+ ...narrowed,
6602
+ [parentAttr]: existing ? { $and: [existing, { $in: values }] } : { $in: values },
6603
+ };
6604
+ }
6605
+ return narrowed;
6606
+ }
6324
6607
  /**
6325
6608
  * Issue a real `SELECT COUNT(...)` for a model.
6326
6609
  *
@@ -6329,6 +6612,9 @@ $$ LANGUAGE plpgsql;`;
6329
6612
  * - `col` picks the counted expression (`COUNT("col")`)
6330
6613
  * - `distinct: true` counts distinct values of `col`, or of the primary key
6331
6614
  * - `group` returns the number of groups
6615
+ *
6616
+ * A query carrying `union` branches is counted over the combined query rather
6617
+ * than this model's table alone - see the derived-table note below.
6332
6618
  */
6333
6619
  async _countRows(model, modelName, options = {}) {
6334
6620
  if (!this.dialect) {
@@ -6344,22 +6630,10 @@ $$ LANGUAGE plpgsql;`;
6344
6630
  where = { ...where, [deletedAt]: { $isNull: true } };
6345
6631
  }
6346
6632
  // Filtering includes restrict the counted set exactly as they restrict findAll.
6347
- if (options.include && options.include.length > 0) {
6348
- for (const rawInclude of options.include) {
6349
- const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
6350
- if (!filter)
6351
- continue;
6352
- if (filter.values.length === 0)
6353
- return 0;
6354
- const existing = where?.[filter.parentAttr];
6355
- where = {
6356
- ...where,
6357
- [filter.parentAttr]: existing
6358
- ? { $and: [existing, { $in: filter.values }] }
6359
- : { $in: filter.values },
6360
- };
6361
- }
6362
- }
6633
+ const countableWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
6634
+ if (countableWhere === null)
6635
+ return 0;
6636
+ where = countableWhere;
6363
6637
  const pkAttr = resolveModelPrimaryKeyAttr(model);
6364
6638
  const countCol = opts.col;
6365
6639
  const distinct = opts.distinct === true;
@@ -6380,18 +6654,89 @@ $$ LANGUAGE plpgsql;`;
6380
6654
  const groupFields = opts.group
6381
6655
  ? (Array.isArray(opts.group) ? opts.group : [opts.group]).map((g) => typeof g === 'string' ? g : String(g))
6382
6656
  : undefined;
6383
- let sql = `SELECT ${countExpr} AS ${dialect.escapeId('count')} FROM ${qualified}`;
6657
+ // `union` branches widen the set the query returns, so counting this
6658
+ // model's table alone leaves `findAndCountAll` contradicting itself: `rows`
6659
+ // spans every branch while `count` describes only the first. When the query
6660
+ // carries branches, count over the combined query instead:
6661
+ //
6662
+ // SELECT COUNT(*) AS "count" FROM (
6663
+ // SELECT * FROM "active" WHERE ... UNION ALL SELECT * FROM "archived" WHERE ...
6664
+ // ) AS "prorm_count"
6665
+ //
6666
+ // The set operator does its work *inside* the derived table, so `UNION` and
6667
+ // `UNION ALL` count differently with no special-casing here: the distinct
6668
+ // form collapses duplicate rows before they reach COUNT, the ALL form does
6669
+ // not, and EXCEPT/INTERSECT likewise count exactly what they produce.
6670
+ //
6671
+ // Left out on purpose: `order` and the outer `limit`/`offset`. Neither can
6672
+ // change a count, and several dialects reject ORDER BY inside a derived
6673
+ // table. Every other narrowing option is kept, each branch's own `where`
6674
+ // and `limit` included. The projection is resolved the way findAll resolves
6675
+ // it, because a distinct UNION dedupes on the selected columns.
6676
+ //
6677
+ // Not every dialect compiles `union` - the ones that don't drop it from
6678
+ // findAll too, so their rows come from this table alone and the plain count
6679
+ // is already correct for them. Rather than keep a list of which do, compile
6680
+ // the query both with and without the branches and compare: identical SQL
6681
+ // means the dialect ignored them.
6682
+ const unionBranches = opts.union ?? [];
6683
+ let unionSource = null;
6684
+ if (unionBranches.length > 0) {
6685
+ let projection = opts.attributes;
6686
+ if (projection && !Array.isArray(projection) && projection.exclude) {
6687
+ const excluded = new Set(projection.exclude);
6688
+ projection = Array.from(modelColumnNames(model)).filter((k) => !excluded.has(k));
6689
+ }
6690
+ const shared = {
6691
+ tableName,
6692
+ schema: model.schema,
6693
+ attributes: projection,
6694
+ where,
6695
+ distinct: opts.distinct,
6696
+ distinctOn: opts.distinctOn,
6697
+ // Associations are resolved with their own queries, never joined in.
6698
+ include: undefined,
6699
+ };
6700
+ const withoutBranches = dialect.buildSelectQuery({ ...shared });
6701
+ const withBranches = dialect.buildSelectQuery({
6702
+ ...shared,
6703
+ unionType: opts.unionType,
6704
+ union: unionBranches,
6705
+ });
6706
+ if (withBranches.sql !== withoutBranches.sql) {
6707
+ unionSource = { sql: withBranches.sql, values: withBranches.values };
6708
+ }
6709
+ }
6710
+ let sql;
6384
6711
  let values = [];
6385
- if (where && Object.keys(where).length > 0) {
6386
- const built = dialect.buildWhereClause(where);
6387
- sql += ` WHERE ${built.sql}`;
6388
- values = built.values;
6712
+ if (unionSource) {
6713
+ // `where` is already compiled into the derived table; no outer WHERE.
6714
+ sql =
6715
+ `SELECT ${countExpr} AS ${dialect.escapeId('count')} ` +
6716
+ `FROM (${unionSource.sql}) AS ${dialect.escapeId('prorm_count')}`;
6717
+ values = unionSource.values;
6718
+ }
6719
+ else {
6720
+ sql = `SELECT ${countExpr} AS ${dialect.escapeId('count')} FROM ${qualified}`;
6721
+ if (where && Object.keys(where).length > 0) {
6722
+ const built = dialect.buildWhereClause(where);
6723
+ sql += ` WHERE ${built.sql}`;
6724
+ values = built.values;
6725
+ }
6389
6726
  }
6390
6727
  if (groupFields?.length) {
6391
6728
  sql += ` GROUP BY ${groupFields.map((g) => dialect.escapeId(g)).join(', ')}`;
6392
6729
  }
6393
6730
  const finalSql = dialect.replaceReplacements(sql, values);
6731
+ const started = Date.now();
6394
6732
  const result = await dialect.query(finalSql);
6733
+ this._logQuery({
6734
+ sql: finalSql,
6735
+ duration: Date.now() - started,
6736
+ parameters: values,
6737
+ model: modelName,
6738
+ type: 'SELECT',
6739
+ }, options.logging);
6395
6740
  const rows = result.rows || [];
6396
6741
  // With GROUP BY the aggregate returns one row per group; the count is the
6397
6742
  // number of groups, matching how callers use `count({ group })`.
@@ -6417,10 +6762,10 @@ $$ LANGUAGE plpgsql;`;
6417
6762
  throw new Error('Database not connected');
6418
6763
  }
6419
6764
  const dialect = this.dialect;
6420
- if (!model.rawAttributes?.[attribute]) {
6421
- const known = Object.keys(model.rawAttributes || {}).join(', ');
6765
+ const columns = modelColumnNames(model);
6766
+ if (!columns.has(attribute)) {
6422
6767
  throw new Error(`${modelName} has no attribute '${attribute}' to ${aggregate.toLowerCase()}.` +
6423
- (known ? ` Known attributes: ${known}.` : ''));
6768
+ ` Known attributes: ${Array.from(columns).join(', ')}.`);
6424
6769
  }
6425
6770
  const modelOptions = model.options;
6426
6771
  const deletedAt = modelOptions?.deletedAt || 'deletedAt';
@@ -6431,22 +6776,10 @@ $$ LANGUAGE plpgsql;`;
6431
6776
  where = { ...where, [deletedAt]: { $isNull: true } };
6432
6777
  }
6433
6778
  // Filtering includes restrict the aggregated set, as they do for findAll.
6434
- if (options.include && options.include.length > 0) {
6435
- for (const rawInclude of options.include) {
6436
- const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
6437
- if (!filter)
6438
- continue;
6439
- if (filter.values.length === 0)
6440
- return aggregate === 'SUM' ? 0 : null;
6441
- const existing = where?.[filter.parentAttr];
6442
- where = {
6443
- ...where,
6444
- [filter.parentAttr]: existing
6445
- ? { $and: [existing, { $in: filter.values }] }
6446
- : { $in: filter.values },
6447
- };
6448
- }
6449
- }
6779
+ const aggregateWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
6780
+ if (aggregateWhere === null)
6781
+ return aggregate === 'SUM' ? 0 : null;
6782
+ where = aggregateWhere;
6450
6783
  const qualified = model.schema
6451
6784
  ? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
6452
6785
  : dialect.escapeId(model.tableName);
@@ -6457,12 +6790,191 @@ $$ LANGUAGE plpgsql;`;
6457
6790
  sql += ` WHERE ${built.sql}`;
6458
6791
  values = built.values;
6459
6792
  }
6460
- const result = await dialect.query(dialect.replaceReplacements(sql, values));
6793
+ const finalAggSql = dialect.replaceReplacements(sql, values);
6794
+ const aggStarted = Date.now();
6795
+ const result = await dialect.query(finalAggSql);
6796
+ this._logQuery({
6797
+ sql: finalAggSql,
6798
+ duration: Date.now() - aggStarted,
6799
+ parameters: values,
6800
+ model: modelName,
6801
+ type: 'SELECT',
6802
+ }, options.logging);
6461
6803
  const val = result.rows?.[0]?.__agg;
6462
6804
  if (val === null || val === undefined)
6463
6805
  return aggregate === 'SUM' ? 0 : null;
6464
6806
  return Number(val);
6465
6807
  }
6808
+ /**
6809
+ * Log one executed query, and emit the events that hang off it.
6810
+ *
6811
+ * Every statement the ORM issues should route through here. Query logging was
6812
+ * previously inlined at four call sites and only some ran it, so `count`,
6813
+ * `bulkCreate`, `increment` and the aggregates emitted nothing - turning
6814
+ * logging on showed an incomplete picture of what the ORM was doing.
6815
+ */
6816
+ _logQuery(context, queryLogging) {
6817
+ (0, logging_1.logQuery)(context, {
6818
+ instanceLogging: this.loggingOptions,
6819
+ queryLogging,
6820
+ benchmark: this.config.benchmark === true,
6821
+ logQueryParameters: this.config.logQueryParameters === true,
6822
+ });
6823
+ // Slow-query detection and the internal logger's own listeners.
6824
+ this.logger.logQuery(context.sql, context.duration);
6825
+ this.emit('query', {
6826
+ sql: context.sql,
6827
+ duration: context.duration,
6828
+ timestamp: new Date(),
6829
+ model: context.model,
6830
+ });
6831
+ const threshold = this.config.slowQueryThreshold || 1000;
6832
+ if (context.duration > threshold) {
6833
+ this.emit('slowQuery', {
6834
+ sql: context.sql,
6835
+ duration: context.duration,
6836
+ threshold,
6837
+ timestamp: new Date(),
6838
+ model: context.model,
6839
+ });
6840
+ }
6841
+ }
6842
+ /**
6843
+ * Apply `+/- delta` to numeric columns in a single UPDATE.
6844
+ *
6845
+ * Backs `Model.increment` / `Model.decrement`. The arithmetic happens in the
6846
+ * database, so two concurrent callers both land - a read-modify-write in
6847
+ * application code silently drops one of them.
6848
+ */
6849
+ async _applyDelta(model, modelName, fields, options = {}, sign) {
6850
+ if (!this.dialect)
6851
+ throw new Error('Database not connected');
6852
+ const dialect = this.dialect;
6853
+ // Normalize the three accepted shapes to column -> delta.
6854
+ const by = options.by ?? 1;
6855
+ let deltas;
6856
+ if (typeof fields === 'string') {
6857
+ deltas = { [fields]: by };
6858
+ }
6859
+ else if (Array.isArray(fields)) {
6860
+ deltas = Object.fromEntries(fields.map((f) => [f, by]));
6861
+ }
6862
+ else {
6863
+ deltas = fields;
6864
+ }
6865
+ const names = Object.keys(deltas);
6866
+ if (names.length === 0)
6867
+ return 0;
6868
+ const columns = modelColumnNames(model);
6869
+ for (const name of names) {
6870
+ if (!columns.has(name)) {
6871
+ throw new Error(`${modelName} has no attribute '${name}' to increment. ` +
6872
+ `Known attributes: ${Array.from(columns).join(', ')}.`);
6873
+ }
6874
+ }
6875
+ const qualified = model.schema
6876
+ ? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
6877
+ : dialect.escapeId(model.tableName);
6878
+ const assignments = names.map((name) => {
6879
+ const col = dialect.escapeId(name);
6880
+ const amount = deltas[name] * sign;
6881
+ // Emit `col = col + n` / `col = col - n` rather than a bound parameter
6882
+ // for the operator, since the sign is structural.
6883
+ return `${col} = ${col} ${amount < 0 ? '-' : '+'} ${Math.abs(amount)}`;
6884
+ });
6885
+ let sql = `UPDATE ${qualified} SET ${assignments.join(', ')}`;
6886
+ let values = [];
6887
+ // Respect paranoid filtering, as update() does.
6888
+ const modelOptions = model.options;
6889
+ const deletedAt = modelOptions?.deletedAt || 'deletedAt';
6890
+ let where = options.where;
6891
+ if (modelOptions?.paranoid === true && model.rawAttributes?.[deletedAt]) {
6892
+ where = { ...where, [deletedAt]: { $isNull: true } };
6893
+ }
6894
+ if (where && Object.keys(where).length > 0) {
6895
+ const built = dialect.buildWhereClause((0, operators_1.normalizeOperatorKeys)(where));
6896
+ sql += ` WHERE ${built.sql}`;
6897
+ values = built.values;
6898
+ }
6899
+ const finalDeltaSql = dialect.replaceReplacements(sql, values);
6900
+ const deltaStarted = Date.now();
6901
+ const result = await dialect.query(finalDeltaSql);
6902
+ this._logQuery({
6903
+ sql: finalDeltaSql,
6904
+ duration: Date.now() - deltaStarted,
6905
+ parameters: values,
6906
+ model: modelName,
6907
+ type: 'UPDATE',
6908
+ }, options.logging);
6909
+ return result.affectedRows ?? result.rowCount ?? 0;
6910
+ }
6911
+ /**
6912
+ * Update many rows to different values in one statement.
6913
+ *
6914
+ * Backs `Model.bulkUpdate`. Compiles one UPDATE with a CASE expression per
6915
+ * column - portable across every supported dialect - instead of N round
6916
+ * trips.
6917
+ */
6918
+ async _bulkUpdate(model, modelName, rows, options = {}) {
6919
+ if (!this.dialect)
6920
+ throw new Error('Database not connected');
6921
+ if (!rows || rows.length === 0)
6922
+ return 0;
6923
+ const dialect = this.dialect;
6924
+ const keyAttr = options.key ?? resolveModelPrimaryKeyAttr(model);
6925
+ const keys = [];
6926
+ for (const row of rows) {
6927
+ if (row[keyAttr] === undefined || row[keyAttr] === null) {
6928
+ throw new Error(`bulkUpdate on ${modelName}: every row must carry '${keyAttr}'. ` +
6929
+ `Pass { key: '<column>' } if the rows are identified by something else.`);
6930
+ }
6931
+ keys.push(row[keyAttr]);
6932
+ }
6933
+ // Columns to write: every key seen across the rows, minus the identifier.
6934
+ const columns = Array.from(new Set(rows.flatMap((r) => Object.keys(r)))).filter((c) => c !== keyAttr);
6935
+ if (columns.length === 0)
6936
+ return 0;
6937
+ const knownColumns = modelColumnNames(model);
6938
+ for (const col of columns) {
6939
+ if (!knownColumns.has(col)) {
6940
+ throw new Error(`bulkUpdate on ${modelName}: unknown attribute '${col}'. ` +
6941
+ `Known attributes: ${Array.from(knownColumns).join(', ')}.`);
6942
+ }
6943
+ }
6944
+ const qualified = model.schema
6945
+ ? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
6946
+ : dialect.escapeId(model.tableName);
6947
+ const keyCol = dialect.escapeId(keyAttr);
6948
+ const values = [];
6949
+ const assignments = columns.map((col) => {
6950
+ const escaped = dialect.escapeId(col);
6951
+ const branches = [];
6952
+ for (const row of rows) {
6953
+ // A row that omits this column keeps its current value, so it gets no
6954
+ // WHEN branch and falls through to the ELSE.
6955
+ if (!(col in row))
6956
+ continue;
6957
+ branches.push('WHEN ? THEN ?');
6958
+ values.push(row[keyAttr], row[col]);
6959
+ }
6960
+ return `${escaped} = CASE ${keyCol} ${branches.join(' ')} ELSE ${escaped} END`;
6961
+ });
6962
+ const placeholders = keys.map(() => '?').join(', ');
6963
+ values.push(...keys);
6964
+ const sql = `UPDATE ${qualified} SET ${assignments.join(', ')} ` +
6965
+ `WHERE ${keyCol} IN (${placeholders})`;
6966
+ const finalBulkSql = dialect.replaceReplacements(sql, values);
6967
+ const bulkStarted = Date.now();
6968
+ const result = await dialect.query(finalBulkSql);
6969
+ this._logQuery({
6970
+ sql: finalBulkSql,
6971
+ duration: Date.now() - bulkStarted,
6972
+ parameters: values,
6973
+ model: modelName,
6974
+ type: 'UPDATE',
6975
+ }, options.logging);
6976
+ return result.affectedRows ?? result.rowCount ?? rows.length;
6977
+ }
6466
6978
  /**
6467
6979
  * The view of this instance that the shared eager-loader needs.
6468
6980
  */
@@ -6630,6 +7142,7 @@ $$ LANGUAGE plpgsql;`;
6630
7142
  }
6631
7143
  // Table exists - let's alter it to match the model
6632
7144
  await this.alterTable(tableName, modelAttributes, modelOptions, { drop });
7145
+ await this.reconcileUniqueKeys(tableName, modelAttributes, modelOptions);
6633
7146
  }
6634
7147
  catch (error) {
6635
7148
  this.logger.error(`Failed to alter table ${tableName}`, { error });
@@ -6637,8 +7150,74 @@ $$ LANGUAGE plpgsql;`;
6637
7150
  }
6638
7151
  }
6639
7152
  }
7153
+ /**
7154
+ * Make sure the model's declared composite unique keys are enforced on an
7155
+ * existing table, as a unique index.
7156
+ *
7157
+ * `alterTable` reconciles columns only, and on SQLite reconciling a column
7158
+ * means rebuilding the table - which drops the table-level UNIQUE constraint
7159
+ * that `createTableFromModel` put in the CREATE TABLE text. Without this step
7160
+ * a single `sync({ alter: true })` would silently un-enforce a unique key the
7161
+ * model still declares, which is the same silent-data-integrity failure the
7162
+ * unique key support was added to fix.
7163
+ *
7164
+ * A unique index enforces exactly the same rule as a unique constraint and,
7165
+ * unlike `ADD CONSTRAINT`, every dialect here can create one. Existing
7166
+ * indexes are listed first so re-syncing is idempotent on dialects whose
7167
+ * `addIndex` has no IF NOT EXISTS.
7168
+ *
7169
+ * This deliberately does not remove unique keys the model no longer declares
7170
+ * (dropping a constraint is not a change to make implicitly), and it will
7171
+ * fail loudly if the table already holds rows that violate a newly declared
7172
+ * key - which is the honest outcome, not something to swallow.
7173
+ */
7174
+ async reconcileUniqueKeys(tableName, attributes, options) {
7175
+ if (!this.dialect)
7176
+ return;
7177
+ const columns = this.buildColumnDefinitions(attributes, options);
7178
+ // `buildColumnDefinitions` omits the implicit `id` that
7179
+ // `createTableFromModel` synthesizes for a model with no declared primary
7180
+ // key, so add it back before validating the declared field names against
7181
+ // the column list - otherwise a unique key over `id` looks bogus here.
7182
+ if (!Object.values(columns).some((c) => c.primaryKey) && !columns['id']) {
7183
+ columns['id'] = {
7184
+ type: 'INTEGER',
7185
+ allowNull: false,
7186
+ primaryKey: true,
7187
+ autoIncrement: true,
7188
+ };
7189
+ }
7190
+ const uniqueKeys = collectUniqueKeys(tableName, attributes, options, columns);
7191
+ if (uniqueKeys.length === 0)
7192
+ return;
7193
+ const existing = new Set();
7194
+ try {
7195
+ for (const index of await this.dialect.showIndexes(tableName)) {
7196
+ const name = index?.name ?? index?.Key_name ?? index?.index_name;
7197
+ if (typeof name === 'string')
7198
+ existing.add(name);
7199
+ }
7200
+ }
7201
+ catch {
7202
+ // A dialect that cannot list indexes just means we may re-issue a CREATE
7203
+ // that already exists; that is the addIndex implementation's problem, not
7204
+ // a reason to skip enforcing the key.
7205
+ }
7206
+ for (const uk of uniqueKeys) {
7207
+ if (existing.has(uk.name))
7208
+ continue;
7209
+ await this.dialect.addIndex(tableName, uk.name, uk.fields, { unique: true });
7210
+ this.logger.info(`Applied unique key ${uk.name} to table ${tableName}`);
7211
+ }
7212
+ }
6640
7213
  /**
6641
7214
  * Alter a table to match model definition
7215
+ *
7216
+ * Scope note: this reconciles *columns* only - it adds, changes and (with
7217
+ * `drop`) removes them. Table-level constraints are not diffed against the
7218
+ * database. The composite unique keys collected by `collectUniqueKeys` are
7219
+ * the exception, and they are handled just after this runs, by
7220
+ * `reconcileUniqueKeys`, because a column rebuild would otherwise drop them.
6642
7221
  */
6643
7222
  async alterTable(tableName, modelAttributes, modelOptions, options = {}) {
6644
7223
  if (!this.dialect) {
@@ -6916,7 +7495,7 @@ $$ LANGUAGE plpgsql;`;
6916
7495
  * })
6917
7496
  */
6918
7497
  col(tableOrColumn, column) {
6919
- return new Col(tableOrColumn, column);
7498
+ return new expressions_1.Col(tableOrColumn, column);
6920
7499
  }
6921
7500
  /**
6922
7501
  * Create a literal SQL expression
@@ -6979,7 +7558,7 @@ $$ LANGUAGE plpgsql;`;
6979
7558
  * });
6980
7559
  */
6981
7560
  get UUIDV4() {
6982
- return new Literal(data_types_1.UUIDV4.generate());
7561
+ return new expressions_1.Literal(data_types_1.UUIDV4.generate());
6983
7562
  }
6984
7563
  /**
6985
7564
  * Create a where condition for complex queries
@@ -7265,10 +7844,10 @@ exports.DataTypes = {
7265
7844
  // Static Prorm operators - these can be used without creating a Prorm instance (e.g., Prorm.literal('NOW()'))
7266
7845
  // Using type assertion to avoid TypeScript errors since these are added after class definition
7267
7846
  Prorm.literal = function (sql) {
7268
- return new Literal(sql);
7847
+ return new expressions_1.Literal(sql);
7269
7848
  };
7270
7849
  Prorm.col = function (tableOrColumn, column) {
7271
- return new Col(tableOrColumn, column);
7850
+ return new expressions_1.Col(tableOrColumn, column);
7272
7851
  };
7273
7852
  Prorm.fn = function (fnName, ...args) {
7274
7853
  return {
@@ -7285,7 +7864,7 @@ Prorm.cast = function (value, type) {
7285
7864
  };
7286
7865
  Object.defineProperty(Prorm, 'UUIDV4', {
7287
7866
  get() {
7288
- return new Literal('lower(hex(randomblob(16)))');
7867
+ return new expressions_1.Literal('lower(hex(randomblob(16)))');
7289
7868
  },
7290
7869
  });
7291
7870
  Prorm.where = function (col, value) {