ts-prorm-orm 1.2.2 → 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 +209 -0
  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 +13 -4
  93. package/dist/index.js +54 -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 +1142 -557
  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,50 +2960,33 @@ 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
- }
3025
- // Wrap each row with instance methods
2970
+ type: 'SELECT',
2971
+ }, findOptions.logging);
2972
+ // Wrap each row with instance methods.
2973
+ //
2974
+ // `raw: true` asks for plain rows and was accepted but ignored - every
2975
+ // row came back as a full instance carrying save/update/destroy and the
2976
+ // internal bookkeeping fields, which is both slower than asked for and
2977
+ // surprising when the result is a grouped aggregate that maps to no
2978
+ // model instance at all.
3026
2979
  const modelRef = this;
3027
2980
  const modelHooksRef = options.hooks || {};
3028
- const wrappedRows = result.rows.map((row) => {
3029
- return _wrapInstance(row, modelRef, modelHooksRef);
3030
- });
2981
+ const wrappedRows = (findOptions.raw === true
2982
+ ? result.rows.map((row) => ({ ...row }))
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
+ }
3031
2990
  // Eager-load associations. The implementation is shared with the
3032
2991
  // class-based Model API (see models/eager-load.ts) - it used to be
3033
2992
  // duplicated here and in models/model.ts, and the two copies had
@@ -3035,6 +2994,11 @@ class Prorm extends events_1.EventEmitter {
3035
2994
  if (findOptions.include && findOptions.include.length > 0) {
3036
2995
  await (0, eager_load_1.loadIncludes)(self.eagerLoadContext(), wrappedRows, model, modelName, findOptions.include);
3037
2996
  }
2997
+ if (typeof findHooks.afterFind === 'function') {
2998
+ const replaced = await findHooks.afterFind(wrappedRows, findOptions);
2999
+ if (replaced)
3000
+ return replaced;
3001
+ }
3038
3002
  return wrappedRows;
3039
3003
  },
3040
3004
  /**
@@ -3045,7 +3009,8 @@ class Prorm extends events_1.EventEmitter {
3045
3009
  // Count with an aggregate rather than by re-running the full query
3046
3010
  // without a limit and measuring the array. `limit`/`offset` are dropped
3047
3011
  // (the count describes the whole matching set), everything else that
3048
- // 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.
3049
3014
  const { limit: _limit, offset: _offset, ...countable } = this._mergeScopes(findOptions);
3050
3015
  const count = await self._countRows(model, modelName, countable);
3051
3016
  return { rows, count };
@@ -3307,6 +3272,11 @@ class Prorm extends events_1.EventEmitter {
3307
3272
  if (typeof modelHooks.beforeCreate === 'function') {
3308
3273
  await modelHooks.beforeCreate(hookInstance, createOptions);
3309
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
+ }
3310
3280
  // Build dbValues from hookInstance (after hooks may have modified it), filtering virtual fields and respecting 'fields' option
3311
3281
  const allowedFields = createOptions.fields;
3312
3282
  const dbValues = {};
@@ -3319,7 +3289,7 @@ class Prorm extends events_1.EventEmitter {
3319
3289
  if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
3320
3290
  continue;
3321
3291
  }
3322
- dbValues[key] = value;
3292
+ dbValues[key] = _serializeValue(key, value);
3323
3293
  }
3324
3294
  const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {
3325
3295
  returning: createOptions.returning,
@@ -3414,6 +3384,9 @@ class Prorm extends events_1.EventEmitter {
3414
3384
  if (typeof modelHooks.afterCreate === 'function') {
3415
3385
  await modelHooks.afterCreate(createdInstance, createOptions);
3416
3386
  }
3387
+ if (typeof modelHooks.afterSave === 'function') {
3388
+ await modelHooks.afterSave(createdInstance, createOptions);
3389
+ }
3417
3390
  return createdInstance;
3418
3391
  },
3419
3392
  /**
@@ -3426,11 +3399,27 @@ class Prorm extends events_1.EventEmitter {
3426
3399
  throw new Error('Database not connected');
3427
3400
  }
3428
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
+ }
3429
3411
  if (typeof bulkModelHooks.beforeBulkUpdate === 'function') {
3430
3412
  await bulkModelHooks.beforeBulkUpdate({ ...updateOptions, attributes: values });
3431
3413
  }
3432
3414
  const dialect = self.dialect;
3433
- 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 });
3434
3423
  const sql = dialect.replaceReplacements(updateSql, updateVals);
3435
3424
  // Handle logging
3436
3425
  const startTime = Date.now();
@@ -3460,6 +3449,9 @@ class Prorm extends events_1.EventEmitter {
3460
3449
  model: modelName,
3461
3450
  type: 'UPDATE',
3462
3451
  });
3452
+ if (!hooksAlreadyRun && typeof bulkModelHooks.afterUpdate === 'function') {
3453
+ await bulkModelHooks.afterUpdate(values, updateOptions);
3454
+ }
3463
3455
  if (typeof bulkModelHooks.afterBulkUpdate === 'function') {
3464
3456
  await bulkModelHooks.afterBulkUpdate({ ...updateOptions, attributes: values });
3465
3457
  }
@@ -3476,6 +3468,11 @@ class Prorm extends events_1.EventEmitter {
3476
3468
  if (!destroyOptions.where) {
3477
3469
  throw new Error('Missing where option in destroy');
3478
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
+ }
3479
3476
  const dialect = self.dialect;
3480
3477
  const modelOptions = this.options;
3481
3478
  const bulkDestroyHooks = modelOptions?.hooks || {};
@@ -3549,6 +3546,9 @@ class Prorm extends events_1.EventEmitter {
3549
3546
  model: modelName,
3550
3547
  type: isParanoid && !forceDelete ? 'UPDATE' : 'DELETE',
3551
3548
  });
3549
+ if (typeof destroyHooks.afterDestroy === 'function') {
3550
+ await destroyHooks.afterDestroy(destroyOptions);
3551
+ }
3552
3552
  if (typeof bulkDestroyHooks.afterBulkDestroy === 'function') {
3553
3553
  await bulkDestroyHooks.afterBulkDestroy(destroyOptions);
3554
3554
  }
@@ -3609,6 +3609,11 @@ class Prorm extends events_1.EventEmitter {
3609
3609
  const dialect = self.dialect;
3610
3610
  for (const record of records) {
3611
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 = {};
3612
3617
  for (const [key, value] of Object.entries(record)) {
3613
3618
  const attr = attributes[key];
3614
3619
  if (attr && attr.type) {
@@ -3616,14 +3621,23 @@ class Prorm extends events_1.EventEmitter {
3616
3621
  if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
3617
3622
  continue;
3618
3623
  }
3619
- dbValues[key] = value;
3624
+ dbValues[key] = _serializeValue(key, value);
3625
+ jsValues[key] = value;
3620
3626
  }
3621
3627
  const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {});
3622
3628
  const finalSql = dialect.replaceReplacements(sql, insertValues);
3629
+ const bcStarted = Date.now();
3623
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);
3624
3638
  const lastId = insertResult.lastInsertRowid;
3625
3639
  results.push({
3626
- ...dbValues,
3640
+ ...jsValues,
3627
3641
  ...(lastId !== undefined && !record.id ? { id: lastId } : {}),
3628
3642
  isNewRecord: false,
3629
3643
  });
@@ -3647,6 +3661,12 @@ class Prorm extends events_1.EventEmitter {
3647
3661
  * and `distinct`, `col` and `group` were silently discarded.
3648
3662
  */
3649
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
+ }
3650
3670
  // Apply defaultScope / named scopes exactly as findAll does, so
3651
3671
  // `User.scope('admin').count()` counts the scoped set.
3652
3672
  return self._countRows(model, modelName, this._mergeScopes(countOptions));
@@ -3920,7 +3940,148 @@ class Prorm extends events_1.EventEmitter {
3920
3940
  async avg(attribute, aggOptions = {}) {
3921
3941
  return self._aggregate(model, modelName, 'AVG', attribute, this._mergeScopes(aggOptions));
3922
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
+ },
3923
4080
  async upsert(values, upsertOptions) {
4081
+ const upsertHooks = this.options?.hooks || {};
4082
+ if (typeof upsertHooks.beforeUpsert === 'function') {
4083
+ await upsertHooks.beforeUpsert(values, upsertOptions);
4084
+ }
3924
4085
  if (!self.dialect) {
3925
4086
  throw new Error('Database not connected');
3926
4087
  }
@@ -3943,7 +4104,9 @@ class Prorm extends events_1.EventEmitter {
3943
4104
  if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
3944
4105
  continue;
3945
4106
  }
3946
- dbValues[key] = value;
4107
+ // Serialized on the way to the dialect (JSON/JSONB) - see
4108
+ // `_serializeValue`.
4109
+ dbValues[key] = _serializeValue(key, value);
3947
4110
  }
3948
4111
  const columns = Object.keys(dbValues);
3949
4112
  // Default the conflict target: prefer an explicit primary key value, but
@@ -4029,6 +4192,9 @@ class Prorm extends events_1.EventEmitter {
4029
4192
  if (!instance) {
4030
4193
  instance = _wrapInstance({ ...dbValues }, model, options.hooks || {});
4031
4194
  }
4195
+ if (typeof upsertHooks.afterUpsert === 'function') {
4196
+ await upsertHooks.afterUpsert([instance, created], upsertOptions);
4197
+ }
4032
4198
  return [instance, created];
4033
4199
  },
4034
4200
  // ============ Model-level Hooks ============
@@ -4044,7 +4210,7 @@ class Prorm extends events_1.EventEmitter {
4044
4210
  beforeSync(callback) {
4045
4211
  let modelHook = self.modelHooks.get(modelName);
4046
4212
  if (!modelHook) {
4047
- modelHook = new ModelHooks();
4213
+ modelHook = new hooks_manager_1.ModelHooks();
4048
4214
  self.modelHooks.set(modelName, modelHook);
4049
4215
  }
4050
4216
  modelHook.beforeSync(callback);
@@ -4061,7 +4227,7 @@ class Prorm extends events_1.EventEmitter {
4061
4227
  afterSync(callback) {
4062
4228
  let modelHook = self.modelHooks.get(modelName);
4063
4229
  if (!modelHook) {
4064
- modelHook = new ModelHooks();
4230
+ modelHook = new hooks_manager_1.ModelHooks();
4065
4231
  self.modelHooks.set(modelName, modelHook);
4066
4232
  }
4067
4233
  modelHook.afterSync(callback);
@@ -4176,9 +4342,16 @@ class Prorm extends events_1.EventEmitter {
4176
4342
  */
4177
4343
  addModel(modelClass) {
4178
4344
  const modelName = modelClass.name;
4179
- // Get model metadata from decorators
4180
- const modelMeta = (0, decorators_1.getModelMetadata)(modelName);
4181
- 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);
4182
4355
  // A plain class with no @Table/@Attribute decorators is still valid — register it
4183
4356
  // with no attributes rather than failing, matching a bare `class Foo extends Model {}`.
4184
4357
  // Convert attribute metadata to Prorm format
@@ -4517,9 +4690,29 @@ class Prorm extends events_1.EventEmitter {
4517
4690
  : undefined,
4518
4691
  check: c.check,
4519
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);
4520
4704
  const allTableConstraints = [
4521
4705
  ...(tableConstraints || []),
4522
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 })),
4523
4716
  ];
4524
4717
  await this.dialect.createTable(tableName, columns, {
4525
4718
  ifNotExists: true,
@@ -4529,8 +4722,7 @@ class Prorm extends events_1.EventEmitter {
4529
4722
  comment: options.comment,
4530
4723
  rowFormat: options.rowFormat,
4531
4724
  initialAutoIncrement: options.initialAutoIncrement,
4532
- uniqueKeys: options.uniqueKeys,
4533
- indexes: tableIndexes,
4725
+ indexes: allTableIndexes.length ? allTableIndexes : undefined,
4534
4726
  constraints: allTableConstraints,
4535
4727
  });
4536
4728
  }
@@ -4615,30 +4807,30 @@ class Prorm extends events_1.EventEmitter {
4615
4807
  }
4616
4808
  const trimmedSql = sql.trim().toUpperCase();
4617
4809
  if (trimmedSql.startsWith('SELECT')) {
4618
- return QueryTypes.SELECT;
4810
+ return query_types_1.QueryTypes.SELECT;
4619
4811
  }
4620
4812
  if (trimmedSql.startsWith('INSERT')) {
4621
4813
  // Check for UPSERT patterns: ON CONFLICT, ON DUPLICATE KEY, INSERT OR REPLACE
4622
4814
  if (trimmedSql.includes('ON CONFLICT') ||
4623
4815
  trimmedSql.includes('ON DUPLICATE KEY') ||
4624
4816
  trimmedSql.includes('INSERT OR REPLACE')) {
4625
- return QueryTypes.UPSERT;
4817
+ return query_types_1.QueryTypes.UPSERT;
4626
4818
  }
4627
- return QueryTypes.INSERT;
4819
+ return query_types_1.QueryTypes.INSERT;
4628
4820
  }
4629
4821
  if (trimmedSql.startsWith('UPDATE')) {
4630
- return QueryTypes.UPDATE;
4822
+ return query_types_1.QueryTypes.UPDATE;
4631
4823
  }
4632
4824
  if (trimmedSql.startsWith('DELETE')) {
4633
- return QueryTypes.DELETE;
4825
+ return query_types_1.QueryTypes.DELETE;
4634
4826
  }
4635
4827
  if (trimmedSql.startsWith('UPSERT')) {
4636
- return QueryTypes.UPSERT;
4828
+ return query_types_1.QueryTypes.UPSERT;
4637
4829
  }
4638
4830
  if (trimmedSql.startsWith('PRAGMA') || trimmedSql.startsWith('EXPLAIN')) {
4639
- return QueryTypes.RAW;
4831
+ return query_types_1.QueryTypes.RAW;
4640
4832
  }
4641
- return QueryTypes.RAW;
4833
+ return query_types_1.QueryTypes.RAW;
4642
4834
  }
4643
4835
  /**
4644
4836
  * Execute a raw query
@@ -4749,10 +4941,10 @@ class Prorm extends events_1.EventEmitter {
4749
4941
  }
4750
4942
  // Process results based on query type
4751
4943
  switch (queryType) {
4752
- case QueryTypes.SELECT:
4753
- case QueryTypes.SHOWTABLES:
4754
- case QueryTypes.DESCRIBE:
4755
- 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: {
4756
4948
  // Map raw results to model instances if requested
4757
4949
  if (options?.mapToModel && options?.model) {
4758
4950
  const ModelStatic = options.model;
@@ -4762,22 +4954,22 @@ class Prorm extends events_1.EventEmitter {
4762
4954
  // Return array of results
4763
4955
  return result.rows;
4764
4956
  }
4765
- case QueryTypes.INSERT:
4957
+ case query_types_1.QueryTypes.INSERT:
4766
4958
  // For INSERT, return [results, created]
4767
4959
  // Check if a row was inserted by examining the last insert rowid
4768
4960
  const insertId = result.rowid || result.insertId || 0;
4769
4961
  const created = insertId > 0;
4770
4962
  return [result.rows, created];
4771
- case QueryTypes.UPDATE:
4772
- case QueryTypes.DELETE:
4773
- case QueryTypes.BULKDELETE:
4774
- 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:
4775
4967
  // Return affected count
4776
4968
  return result.rowCount;
4777
- case QueryTypes.BULKINSERT:
4969
+ case query_types_1.QueryTypes.BULKINSERT:
4778
4970
  // Return number of inserted rows
4779
4971
  return result.rowCount;
4780
- case QueryTypes.RAW:
4972
+ case query_types_1.QueryTypes.RAW:
4781
4973
  default:
4782
4974
  // For RAW queries, return rows
4783
4975
  return result.rows;
@@ -4837,10 +5029,10 @@ class Prorm extends events_1.EventEmitter {
4837
5029
  // Determine query type
4838
5030
  const queryType = this.determineQueryType(processedSql, options?.type);
4839
5031
  const isSelect = [
4840
- QueryTypes.SELECT,
4841
- QueryTypes.SHOWTABLES,
4842
- QueryTypes.DESCRIBE,
4843
- 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,
4844
5036
  ].includes(queryType);
4845
5037
  // Execute the query
4846
5038
  const result = await this.dialect.query(processedSql, { raw: options?.raw });
@@ -6315,6 +6507,103 @@ $$ LANGUAGE plpgsql;`;
6315
6507
  }
6316
6508
  return Array.from(names);
6317
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
+ }
6318
6607
  /**
6319
6608
  * Issue a real `SELECT COUNT(...)` for a model.
6320
6609
  *
@@ -6323,6 +6612,9 @@ $$ LANGUAGE plpgsql;`;
6323
6612
  * - `col` picks the counted expression (`COUNT("col")`)
6324
6613
  * - `distinct: true` counts distinct values of `col`, or of the primary key
6325
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.
6326
6618
  */
6327
6619
  async _countRows(model, modelName, options = {}) {
6328
6620
  if (!this.dialect) {
@@ -6338,22 +6630,10 @@ $$ LANGUAGE plpgsql;`;
6338
6630
  where = { ...where, [deletedAt]: { $isNull: true } };
6339
6631
  }
6340
6632
  // Filtering includes restrict the counted set exactly as they restrict findAll.
6341
- if (options.include && options.include.length > 0) {
6342
- for (const rawInclude of options.include) {
6343
- const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
6344
- if (!filter)
6345
- continue;
6346
- if (filter.values.length === 0)
6347
- return 0;
6348
- const existing = where?.[filter.parentAttr];
6349
- where = {
6350
- ...where,
6351
- [filter.parentAttr]: existing
6352
- ? { $and: [existing, { $in: filter.values }] }
6353
- : { $in: filter.values },
6354
- };
6355
- }
6356
- }
6633
+ const countableWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
6634
+ if (countableWhere === null)
6635
+ return 0;
6636
+ where = countableWhere;
6357
6637
  const pkAttr = resolveModelPrimaryKeyAttr(model);
6358
6638
  const countCol = opts.col;
6359
6639
  const distinct = opts.distinct === true;
@@ -6374,18 +6654,89 @@ $$ LANGUAGE plpgsql;`;
6374
6654
  const groupFields = opts.group
6375
6655
  ? (Array.isArray(opts.group) ? opts.group : [opts.group]).map((g) => typeof g === 'string' ? g : String(g))
6376
6656
  : undefined;
6377
- 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;
6378
6711
  let values = [];
6379
- if (where && Object.keys(where).length > 0) {
6380
- const built = dialect.buildWhereClause(where);
6381
- sql += ` WHERE ${built.sql}`;
6382
- 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
+ }
6383
6726
  }
6384
6727
  if (groupFields?.length) {
6385
6728
  sql += ` GROUP BY ${groupFields.map((g) => dialect.escapeId(g)).join(', ')}`;
6386
6729
  }
6387
6730
  const finalSql = dialect.replaceReplacements(sql, values);
6731
+ const started = Date.now();
6388
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);
6389
6740
  const rows = result.rows || [];
6390
6741
  // With GROUP BY the aggregate returns one row per group; the count is the
6391
6742
  // number of groups, matching how callers use `count({ group })`.
@@ -6411,10 +6762,10 @@ $$ LANGUAGE plpgsql;`;
6411
6762
  throw new Error('Database not connected');
6412
6763
  }
6413
6764
  const dialect = this.dialect;
6414
- if (!model.rawAttributes?.[attribute]) {
6415
- const known = Object.keys(model.rawAttributes || {}).join(', ');
6765
+ const columns = modelColumnNames(model);
6766
+ if (!columns.has(attribute)) {
6416
6767
  throw new Error(`${modelName} has no attribute '${attribute}' to ${aggregate.toLowerCase()}.` +
6417
- (known ? ` Known attributes: ${known}.` : ''));
6768
+ ` Known attributes: ${Array.from(columns).join(', ')}.`);
6418
6769
  }
6419
6770
  const modelOptions = model.options;
6420
6771
  const deletedAt = modelOptions?.deletedAt || 'deletedAt';
@@ -6425,22 +6776,10 @@ $$ LANGUAGE plpgsql;`;
6425
6776
  where = { ...where, [deletedAt]: { $isNull: true } };
6426
6777
  }
6427
6778
  // Filtering includes restrict the aggregated set, as they do for findAll.
6428
- if (options.include && options.include.length > 0) {
6429
- for (const rawInclude of options.include) {
6430
- const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
6431
- if (!filter)
6432
- continue;
6433
- if (filter.values.length === 0)
6434
- return aggregate === 'SUM' ? 0 : null;
6435
- const existing = where?.[filter.parentAttr];
6436
- where = {
6437
- ...where,
6438
- [filter.parentAttr]: existing
6439
- ? { $and: [existing, { $in: filter.values }] }
6440
- : { $in: filter.values },
6441
- };
6442
- }
6443
- }
6779
+ const aggregateWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
6780
+ if (aggregateWhere === null)
6781
+ return aggregate === 'SUM' ? 0 : null;
6782
+ where = aggregateWhere;
6444
6783
  const qualified = model.schema
6445
6784
  ? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
6446
6785
  : dialect.escapeId(model.tableName);
@@ -6451,12 +6790,191 @@ $$ LANGUAGE plpgsql;`;
6451
6790
  sql += ` WHERE ${built.sql}`;
6452
6791
  values = built.values;
6453
6792
  }
6454
- 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);
6455
6803
  const val = result.rows?.[0]?.__agg;
6456
6804
  if (val === null || val === undefined)
6457
6805
  return aggregate === 'SUM' ? 0 : null;
6458
6806
  return Number(val);
6459
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
+ }
6460
6978
  /**
6461
6979
  * The view of this instance that the shared eager-loader needs.
6462
6980
  */
@@ -6624,6 +7142,7 @@ $$ LANGUAGE plpgsql;`;
6624
7142
  }
6625
7143
  // Table exists - let's alter it to match the model
6626
7144
  await this.alterTable(tableName, modelAttributes, modelOptions, { drop });
7145
+ await this.reconcileUniqueKeys(tableName, modelAttributes, modelOptions);
6627
7146
  }
6628
7147
  catch (error) {
6629
7148
  this.logger.error(`Failed to alter table ${tableName}`, { error });
@@ -6631,8 +7150,74 @@ $$ LANGUAGE plpgsql;`;
6631
7150
  }
6632
7151
  }
6633
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
+ }
6634
7213
  /**
6635
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.
6636
7221
  */
6637
7222
  async alterTable(tableName, modelAttributes, modelOptions, options = {}) {
6638
7223
  if (!this.dialect) {
@@ -6910,7 +7495,7 @@ $$ LANGUAGE plpgsql;`;
6910
7495
  * })
6911
7496
  */
6912
7497
  col(tableOrColumn, column) {
6913
- return new Col(tableOrColumn, column);
7498
+ return new expressions_1.Col(tableOrColumn, column);
6914
7499
  }
6915
7500
  /**
6916
7501
  * Create a literal SQL expression
@@ -6973,7 +7558,7 @@ $$ LANGUAGE plpgsql;`;
6973
7558
  * });
6974
7559
  */
6975
7560
  get UUIDV4() {
6976
- return new Literal(data_types_1.UUIDV4.generate());
7561
+ return new expressions_1.Literal(data_types_1.UUIDV4.generate());
6977
7562
  }
6978
7563
  /**
6979
7564
  * Create a where condition for complex queries
@@ -7259,10 +7844,10 @@ exports.DataTypes = {
7259
7844
  // Static Prorm operators - these can be used without creating a Prorm instance (e.g., Prorm.literal('NOW()'))
7260
7845
  // Using type assertion to avoid TypeScript errors since these are added after class definition
7261
7846
  Prorm.literal = function (sql) {
7262
- return new Literal(sql);
7847
+ return new expressions_1.Literal(sql);
7263
7848
  };
7264
7849
  Prorm.col = function (tableOrColumn, column) {
7265
- return new Col(tableOrColumn, column);
7850
+ return new expressions_1.Col(tableOrColumn, column);
7266
7851
  };
7267
7852
  Prorm.fn = function (fnName, ...args) {
7268
7853
  return {
@@ -7279,7 +7864,7 @@ Prorm.cast = function (value, type) {
7279
7864
  };
7280
7865
  Object.defineProperty(Prorm, 'UUIDV4', {
7281
7866
  get() {
7282
- return new Literal('lower(hex(randomblob(16)))');
7867
+ return new expressions_1.Literal('lower(hex(randomblob(16)))');
7283
7868
  },
7284
7869
  });
7285
7870
  Prorm.where = function (col, value) {