turbine-orm 0.65.0 → 0.66.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 (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
@@ -1,16 +1,22 @@
1
1
  "use strict";
2
- /**
3
- * turbine-orm, Error types
4
- *
5
- * Typed errors with error codes for programmatic handling.
6
- * All Turbine errors extend TurbineError which includes a `code` property.
7
- */
8
2
  Object.defineProperty(exports, "__esModule", { value: true });
9
3
  exports.ReadOnlyError = exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.REDACTED_DETAIL = exports.TurbineError = exports.TurbineErrorCode = void 0;
4
+ exports.registerClientErrorMessageMode = registerClientErrorMessageMode;
5
+ exports.errorMessageModesDiverged = errorMessageModesDiverged;
6
+ exports.runWithErrorMessageMode = runWithErrorMessageMode;
10
7
  exports.setErrorMessageMode = setErrorMessageMode;
11
8
  exports.getErrorMessageMode = getErrorMessageMode;
9
+ exports.markValueBearingMessage = markValueBearingMessage;
12
10
  exports.describeTargetForMessage = describeTargetForMessage;
11
+ exports.malformedConnectionStringMessage = malformedConnectionStringMessage;
13
12
  exports.wrapPgError = wrapPgError;
13
+ const node_async_hooks_1 = require("node:async_hooks");
14
+ /**
15
+ * turbine-orm, Error types
16
+ *
17
+ * Typed errors with error codes for programmatic handling.
18
+ * All Turbine errors extend TurbineError which includes a `code` property.
19
+ */
14
20
  /** Error codes for all Turbine errors */
15
21
  exports.TurbineErrorCode = {
16
22
  NOT_FOUND: 'TURBINE_E001',
@@ -85,6 +91,76 @@ class TurbineError extends Error {
85
91
  }
86
92
  exports.TurbineError = TurbineError;
87
93
  let errorMessageMode = 'safe';
94
+ /**
95
+ * The mode in force for the operation currently executing, when a client has
96
+ * established one. See {@link runWithErrorMessageMode}.
97
+ */
98
+ const errorMessageModeScope = new node_async_hooks_1.AsyncLocalStorage();
99
+ /** The first mode a TurbineClient registered, used to detect divergence. */
100
+ let firstRegisteredClientMode;
101
+ let clientModesDiverged = false;
102
+ /**
103
+ * The mode that applies right now: the scope a client established for this
104
+ * operation, else the process-wide default.
105
+ *
106
+ * The default is the fallback rather than the authority, which is the whole
107
+ * point. `errorMessageMode` is a module-level `let` that every TurbineClient
108
+ * constructor overwrote, so in a process with a primary client and an analytics
109
+ * or replica client, or a test harness building one client per suite, the LAST
110
+ * client constructed decided the mode for ALL of them, order-dependently. A
111
+ * client asking for `'safe'` could silently be running in `'verbose'`, which is
112
+ * the direction that leaks. (The dual ESM+CJS build makes it worse: the two
113
+ * copies hold separate `let`s.)
114
+ *
115
+ * An error constructed outside any client, directly or via
116
+ * {@link setErrorMessageMode}, still formats by the default, unchanged.
117
+ */
118
+ function currentErrorMessageMode() {
119
+ return errorMessageModeScope.getStore() ?? errorMessageMode;
120
+ }
121
+ /**
122
+ * Register a client's configured mode and report whether per-operation scoping
123
+ * is now required.
124
+ *
125
+ * The gate is DIVERGENCE, not "has a client been built": while every client in
126
+ * the process agrees (the overwhelmingly common case, including every
127
+ * single-client app), the module default is already the right answer for all of
128
+ * them and no scope has to be established, so the query path pays exactly
129
+ * nothing. The moment two clients disagree, every client starts scoping, which
130
+ * is why this is read per call rather than latched per client.
131
+ *
132
+ * @internal Used by TurbineClient; not part of the public surface.
133
+ */
134
+ function registerClientErrorMessageMode(mode) {
135
+ if (firstRegisteredClientMode === undefined)
136
+ firstRegisteredClientMode = mode;
137
+ else if (firstRegisteredClientMode !== mode)
138
+ clientModesDiverged = true;
139
+ }
140
+ /**
141
+ * Whether any two clients in this process have registered different modes, so
142
+ * the module default can no longer speak for all of them.
143
+ *
144
+ * @internal
145
+ */
146
+ function errorMessageModesDiverged() {
147
+ return clientModesDiverged;
148
+ }
149
+ /**
150
+ * Run `fn` with `mode` in force for everything it does, awaits included.
151
+ *
152
+ * Establishing the scope around the call (rather than threading a mode
153
+ * parameter into every error constructor) is what makes this reachable at all:
154
+ * the mode-sensitive errors are built deep inside the query executor, the write
155
+ * builders and the nested-write engine, none of which are handed the client.
156
+ * The async context is captured when the operation's promise chain is created
157
+ * INSIDE this call, so every continuation of it resolves the same mode.
158
+ *
159
+ * @internal
160
+ */
161
+ function runWithErrorMessageMode(mode, fn) {
162
+ return errorMessageModeScope.run(mode, fn);
163
+ }
88
164
  /**
89
165
  * Set the global NotFoundError message mode. Called from the TurbineClient
90
166
  * constructor when `TurbineConfig.errorMessages` is provided.
@@ -110,9 +186,16 @@ let errorMessageMode = 'safe';
110
186
  function setErrorMessageMode(mode) {
111
187
  errorMessageMode = mode;
112
188
  }
113
- /** Returns the current NotFoundError message mode. Exported for tests. */
189
+ /**
190
+ * Returns the NotFoundError message mode in effect right now.
191
+ *
192
+ * Called inside an operation issued through a client whose mode differs from
193
+ * another client's in the same process, that is THAT client's mode; anywhere
194
+ * else it is the process default {@link setErrorMessageMode} last set, which is
195
+ * what it has always returned. Exported for tests.
196
+ */
114
197
  function getErrorMessageMode() {
115
- return errorMessageMode;
198
+ return currentErrorMessageMode();
116
199
  }
117
200
  /**
118
201
  * The marker left where a driver `detail` string was removed in 'safe' mode.
@@ -120,6 +203,69 @@ function getErrorMessageMode() {
120
203
  * assertions can match on it without hardcoding the wording.
121
204
  */
122
205
  exports.REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
206
+ /**
207
+ * Marks a driver error whose `message` (not only its `detail`) embeds row
208
+ * VALUES, so 'safe' mode has to withhold the message text too.
209
+ *
210
+ * Postgres splits this cleanly: `message` carries relation / constraint /
211
+ * column NAMES and `detail` carries the conflicting values, so redacting
212
+ * `detail` was enough. MySQL and SQL Server do not split it at all. mysql2's
213
+ * ER_DUP_ENTRY reads `Duplicate entry 'alice@example.com' for key
214
+ * 'users.email'` and SQL Server's 2627 ends `The duplicate key value is
215
+ * (alice@example.com).`, both on `message`, with no `detail` field anywhere.
216
+ * So `redactCauseForMode` took its "nothing value-bearing to remove" early
217
+ * return and handed back the driver error untouched, and the row value reached
218
+ * every log line, Sentry event and uncaught-rejection dump that renders the
219
+ * cause chain, in the mode whose entire job is to prevent exactly that.
220
+ *
221
+ * A flag rather than a code list, and set by the ENGINE that knows its own
222
+ * message grammar, for two reasons. It keeps Postgres byte-identical (pg never
223
+ * sets it, so its cause is returned exactly as before), and it puts "this
224
+ * engine's message embeds values" next to the code that reads that engine's
225
+ * messages instead of in a table here that would silently rot.
226
+ *
227
+ * `Symbol.for` so the ESM and CJS copies of this module agree on the key: a
228
+ * dual-package consumer can hand an error marked by one build to the other.
229
+ */
230
+ const VALUE_BEARING_MESSAGE = Symbol.for('turbine.error.valueBearingMessage');
231
+ /**
232
+ * Mark a driver error as carrying row values in its `message` (see
233
+ * {@link VALUE_BEARING_MESSAGE}). Called by the engine error augmenters for the
234
+ * exact driver codes whose message grammar embeds a value; every other code is
235
+ * left alone so nothing is withheld that did not need to be.
236
+ *
237
+ * Non-enumerable so the flag itself never shows up in a serialized error, and
238
+ * best-effort so a frozen or exotic driver error cannot turn a constraint
239
+ * violation into a TypeError.
240
+ */
241
+ function markValueBearingMessage(err) {
242
+ if (!err || typeof err !== 'object')
243
+ return err;
244
+ try {
245
+ Object.defineProperty(err, VALUE_BEARING_MESSAGE, {
246
+ value: true,
247
+ enumerable: false,
248
+ writable: true,
249
+ configurable: true,
250
+ });
251
+ }
252
+ catch {
253
+ /* a frozen driver error keeps its message; the Turbine message is still safe */
254
+ }
255
+ return err;
256
+ }
257
+ /**
258
+ * The replacement for a withheld driver message. The whole message goes, not a
259
+ * pattern-matched part of it: three engines with three grammars and no
260
+ * guarantee a future driver keeps either, so "withhold it" is the only rule
261
+ * that stays true. Nothing diagnostic is lost, the constraint/column/table
262
+ * names are already on the typed Turbine error as structured fields and in its
263
+ * own message, and `errorMessages: 'verbose'` returns the driver text in full.
264
+ */
265
+ function redactedDriverMessage(code) {
266
+ const codePart = typeof code === 'string' && code.length > 0 ? ` (driver code ${code})` : '';
267
+ return `${exports.REDACTED_DETAIL}${codePart}: this engine reports the conflicting row values in the message text, so errorMessages:"safe" withholds it. Use errorMessages:"verbose" to see it.`;
268
+ }
123
269
  /**
124
270
  * Postgres puts the CONFLICTING ROW VALUES in the `detail` field of a
125
271
  * constraint error, and nowhere else: `Key (email)=(alice@example.com) already
@@ -156,17 +302,29 @@ exports.REDACTED_DETAIL = '[redacted by turbine errorMessages:"safe"]';
156
302
  *
157
303
  * In 'verbose' mode the cause passes through untouched: that mode's documented
158
304
  * job is full-fidelity debugging.
305
+ *
306
+ * ENGINES WHOSE MESSAGE CARRIES THE VALUE. MySQL and SQL Server do not have a
307
+ * `detail` field at all and put the conflicting value in `message`, so the
308
+ * early return below used to hand the raw driver error straight back (see
309
+ * {@link markValueBearingMessage}). When the engine set that flag the clone
310
+ * also withholds `message`, `sqlMessage` (mysql2's copy, which is the field
311
+ * mysql2 formats `message` FROM, so leaving it would put the value straight
312
+ * back) and the message text embedded in the rendered `stack` string. The stack
313
+ * substitution is an exact replacement of the known message string, never a
314
+ * grammar guess, so the frames survive intact.
159
315
  */
160
316
  function redactCauseForMode(cause) {
161
- if (errorMessageMode === 'verbose')
317
+ if (currentErrorMessageMode() === 'verbose')
162
318
  return cause;
163
319
  if (!cause || typeof cause !== 'object')
164
320
  return cause;
165
321
  const detail = cause.detail;
322
+ const hasDetail = typeof detail === 'string' && detail.length > 0;
323
+ const valueBearingMessage = cause[VALUE_BEARING_MESSAGE] === true;
166
324
  // Nothing value-bearing to remove: return the original object so the common
167
325
  // case (a non-pg cause, or a pg error without a detail) allocates nothing and
168
326
  // keeps object identity with what the driver threw.
169
- if (typeof detail !== 'string' || detail.length === 0)
327
+ if (!hasDetail && !valueBearingMessage)
170
328
  return cause;
171
329
  try {
172
330
  const descriptors = Object.getOwnPropertyDescriptors(cause);
@@ -174,12 +332,36 @@ function redactCauseForMode(cause) {
174
332
  // non-writable `detail` would make the assignment throw in strict mode
175
333
  // (every module here is ESM, so it always would), and losing the cause is
176
334
  // worse than paying for one descriptor literal.
177
- descriptors.detail = {
178
- value: exports.REDACTED_DETAIL,
179
- writable: true,
180
- enumerable: descriptors.detail?.enumerable ?? true,
181
- configurable: true,
182
- };
335
+ if (hasDetail) {
336
+ descriptors.detail = {
337
+ value: exports.REDACTED_DETAIL,
338
+ writable: true,
339
+ enumerable: descriptors.detail?.enumerable ?? true,
340
+ configurable: true,
341
+ };
342
+ }
343
+ // The exact message strings to scrub out of the rendered stack, collected
344
+ // BEFORE the descriptors are overwritten.
345
+ const withheldTexts = [];
346
+ if (valueBearingMessage) {
347
+ const code = cause.code;
348
+ const replacement = redactedDriverMessage(code);
349
+ for (const key of ['message', 'sqlMessage']) {
350
+ const current = cause[key];
351
+ if (typeof current !== 'string' || current.length === 0)
352
+ continue;
353
+ withheldTexts.push(current);
354
+ descriptors[key] = {
355
+ value: replacement,
356
+ writable: true,
357
+ // `message` is non-enumerable on a native Error and mysql2's
358
+ // `sqlMessage` is enumerable; keep whichever the driver chose so the
359
+ // clone serializes with the same key set as the original.
360
+ enumerable: descriptors[key]?.enumerable ?? false,
361
+ configurable: true,
362
+ };
363
+ }
364
+ }
183
365
  // Brand check rather than `instanceof Error`, so a driver error thrown from
184
366
  // another realm (a worker, a bundled duplicate of pg) is still recognized.
185
367
  const isError = Object.prototype.toString.call(cause) === '[object Error]';
@@ -197,7 +379,14 @@ function redactCauseForMode(cause) {
197
379
  // own descriptor rather than having a lie written over it.
198
380
  const originalStack = cause.stack;
199
381
  if (typeof originalStack === 'string') {
200
- descriptors.stack = { value: originalStack, writable: true, enumerable: false, configurable: true };
382
+ // V8 renders the stack as `<name>: <message>\n at …`, so a withheld
383
+ // message is still sitting in it. Substitute the exact strings that were
384
+ // withheld (split/join, so a message repeated in a nested frame goes
385
+ // too); everything else, the frames included, is untouched.
386
+ let stackText = originalStack;
387
+ for (const text of withheldTexts)
388
+ stackText = stackText.split(text).join(exports.REDACTED_DETAIL);
389
+ descriptors.stack = { value: stackText, writable: true, enumerable: false, configurable: true };
201
390
  }
202
391
  else if (descriptors.stack && typeof descriptors.stack.get === 'function') {
203
392
  // An own accessor bound to the ORIGINAL receiver would return undefined
@@ -226,7 +415,7 @@ function redactCauseForMode(cause) {
226
415
  * connect/update failures which historically embedded the raw values.
227
416
  */
228
417
  function describeTargetForMessage(target) {
229
- if (errorMessageMode === 'verbose') {
418
+ if (currentErrorMessageMode() === 'verbose') {
230
419
  try {
231
420
  return JSON.stringify(target);
232
421
  }
@@ -300,11 +489,11 @@ class NotFoundError extends TurbineError {
300
489
  let message = input.message;
301
490
  if (!message) {
302
491
  if (operation && table) {
303
- const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, errorMessageMode)}` : '';
492
+ const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
304
493
  message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
305
494
  }
306
495
  else if (table) {
307
- const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, errorMessageMode)}` : '';
496
+ const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, currentErrorMessageMode())}` : '';
308
497
  message = `[turbine] No record found in "${table}"${wherePart}`;
309
498
  }
310
499
  else {
@@ -374,6 +563,31 @@ class ConnectionError extends TurbineError {
374
563
  }
375
564
  }
376
565
  exports.ConnectionError = ConnectionError;
566
+ /**
567
+ * The message for "this engine's connection string could not be parsed".
568
+ *
569
+ * The one rule it exists to enforce: NEVER echo the value. A DSN carries a
570
+ * password, and the trigger for this error is a MALFORMED DSN, which is exactly
571
+ * when someone pastes the failure into a bug report, a CI log, or an error
572
+ * tracker. No redaction written against the URL grammar can be trusted on a
573
+ * string that just failed to parse as a URL, so the only safe amount of it to
574
+ * include is none: not the password, not the host, not a prefix.
575
+ *
576
+ * This mirrors the Postgres path (`assertUsableConnectionString` in client.ts),
577
+ * which reached the same conclusion first and stated the same reason; the
578
+ * engines used to interpolate the raw string instead, so SECURITY.md's claim of
579
+ * redaction "in all CLI error output" was true for one engine out of four.
580
+ * Shared rather than copied so the three engines cannot drift back apart.
581
+ *
582
+ * @param engine the human engine label ("MySQL", "SQL Server", "PowDB").
583
+ * @param example a well-formed connection string for that engine, with a
584
+ * placeholder password. Never derived from the caller's value.
585
+ */
586
+ function malformedConnectionStringMessage(engine, example) {
587
+ return (`[turbine] The ${engine} connection string could not be parsed as a URL. Expected something like "${example}". ` +
588
+ '(Check for a missing "//", a stray quote copied out of a .env file, or a shell-truncated value.) ' +
589
+ 'The value is not included here because it may contain a password.');
590
+ }
377
591
  /** Thrown when a relation reference is invalid */
378
592
  class RelationError extends TurbineError {
379
593
  constructor(message) {
@@ -434,7 +648,7 @@ class UniqueConstraintError extends TurbineError {
434
648
  // values straight back into any log line that prints the error object).
435
649
  // The structured `.columns`/`.constraint`/`.column` fields survive in
436
650
  // both modes, they carry NAMES, never values.
437
- const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
651
+ const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
438
652
  if (detail)
439
653
  message += `: ${detail}`;
440
654
  }
@@ -465,7 +679,7 @@ class ForeignKeyError extends TurbineError {
465
679
  // values straight back into any log line that prints the error object).
466
680
  // The structured `.columns`/`.constraint`/`.column` fields survive in
467
681
  // both modes, they carry NAMES, never values.
468
- const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
682
+ const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
469
683
  if (detail)
470
684
  message += `: ${detail}`;
471
685
  }
@@ -495,7 +709,7 @@ class NotNullViolationError extends TurbineError {
495
709
  // values straight back into any log line that prints the error object).
496
710
  // The structured `.columns`/`.constraint`/`.column` fields survive in
497
711
  // both modes, they carry NAMES, never values.
498
- const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
712
+ const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
499
713
  if (detail)
500
714
  message += `: ${detail}`;
501
715
  }
@@ -592,7 +806,7 @@ class CheckConstraintError extends TurbineError {
592
806
  // values straight back into any log line that prints the error object).
593
807
  // The structured `.columns`/`.constraint`/`.column` fields survive in
594
808
  // both modes, they carry NAMES, never values.
595
- const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
809
+ const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
596
810
  if (detail)
597
811
  message += `: ${detail}`;
598
812
  }
@@ -621,7 +835,7 @@ class ExclusionConstraintError extends TurbineError {
621
835
  // values straight back into any log line that prints the error object).
622
836
  // The structured `.columns`/`.constraint`/`.column` fields survive in
623
837
  // both modes, they carry NAMES, never values.
624
- const detail = errorMessageMode === 'verbose' ? detailFromCause(cause) : undefined;
838
+ const detail = currentErrorMessageMode() === 'verbose' ? detailFromCause(cause) : undefined;
625
839
  if (detail)
626
840
  message += `: ${detail}`;
627
841
  }
@@ -46,7 +46,19 @@ export interface CreateIndexSqlOptions {
46
46
  * it must also carry the `-- turbine:no-transaction` directive.
47
47
  */
48
48
  concurrently?: boolean;
49
- /** Emit `IF NOT EXISTS` (default true, required for idempotent no-transaction migrations). */
49
+ /**
50
+ * Emit `IF NOT EXISTS`. Defaults to true for the plain (in-transaction) form
51
+ * and FALSE when `concurrently` is set.
52
+ *
53
+ * `IF NOT EXISTS` matches on the index NAME, never on its validity, so over
54
+ * the INVALID index a failed concurrent build leaves behind it silently
55
+ * no-ops: the migration records as applied, the index doctor reported is
56
+ * still missing, and the documented remedy (DROP INDEX CONCURRENTLY, then
57
+ * rerun) is unreachable through `migrate up` because the rerun no-ops too.
58
+ * The concurrent form gets its idempotency from a preceding
59
+ * `DROP INDEX CONCURRENTLY IF EXISTS` instead; see
60
+ * {@link buildCreateIndexStatements}, which is what the CLI emits.
61
+ */
50
62
  ifNotExists?: boolean;
51
63
  /**
52
64
  * Emit a partial index `... WHERE <col> IS NOT NULL`. Only applied for a
@@ -63,6 +75,26 @@ export interface CreateIndexSqlOptions {
63
75
  * module topology-only.
64
76
  */
65
77
  export declare function buildCreateIndexSql(table: string, columns: string[], indexName: string, options?: CreateIndexSqlOptions): string;
78
+ /**
79
+ * The statement SEQUENCE that builds one fix index and converges on a VALID
80
+ * index however many times it is rerun.
81
+ *
82
+ * A no-transaction migration is recorded only after ALL its statements succeed,
83
+ * so a mid-file failure leaves earlier indexes built and the migration
84
+ * unrecorded: a rerun must be safe. The old answer was `CREATE INDEX
85
+ * CONCURRENTLY IF NOT EXISTS`, which is safe but not CONVERGENT: a concurrent
86
+ * build that fails partway leaves an INVALID index with the right name, and
87
+ * every subsequent run skips it. Measured: the index was INVALID before the fix
88
+ * migration, `migrate up` reported 1 applied and 0 errors, and the index was
89
+ * still INVALID after.
90
+ *
91
+ * So the concurrent form drops first instead. `DROP INDEX CONCURRENTLY IF
92
+ * EXISTS` is a no-op on the first run (the index is missing, which is why
93
+ * doctor proposed it), and on a rerun it clears the corpse so the CREATE
94
+ * actually rebuilds. Neither statement takes a blocking lock, and both are
95
+ * legal only outside a transaction, which this file already is.
96
+ */
97
+ export declare function buildCreateIndexStatements(table: string, columns: string[], indexName: string, options?: CreateIndexSqlOptions): string[];
66
98
  /** Build the matching `DROP INDEX` statement. `concurrently` requires no-transaction execution. */
67
99
  export declare function buildDropIndexSql(indexName: string, options?: {
68
100
  concurrently?: boolean;
@@ -18,6 +18,7 @@
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.buildCreateIndexSql = buildCreateIndexSql;
21
+ exports.buildCreateIndexStatements = buildCreateIndexStatements;
21
22
  exports.buildDropIndexSql = buildDropIndexSql;
22
23
  exports.isProbeIndexed = isProbeIndexed;
23
24
  exports.doctorIndexName = doctorIndexName;
@@ -36,7 +37,12 @@ const schema_js_1 = require("./schema.js");
36
37
  */
37
38
  function buildCreateIndexSql(table, columns, indexName, options = {}) {
38
39
  const concurrently = options.concurrently ? 'CONCURRENTLY ' : '';
39
- const ifNotExists = options.ifNotExists === false ? '' : 'IF NOT EXISTS ';
40
+ // Default differs by form: the plain statement runs inside a transaction and
41
+ // can never leave an INVALID corpse, so IF NOT EXISTS is a pure win there.
42
+ // The CONCURRENTLY form can, and IF NOT EXISTS would then match that corpse
43
+ // by name and skip the rebuild forever (see CreateIndexSqlOptions).
44
+ const wantIfNotExists = options.ifNotExists ?? !options.concurrently;
45
+ const ifNotExists = wantIfNotExists ? 'IF NOT EXISTS ' : '';
40
46
  const cols = columns.map(utils_js_1.quoteIdent).join(', ');
41
47
  let sql = `CREATE INDEX ${concurrently}${ifNotExists}${(0, utils_js_1.quoteIdent)(indexName)} ON ${(0, utils_js_1.quoteIdent)(table)} (${cols})`;
42
48
  if (options.partialNotNull && columns.length === 1 && columns[0] !== undefined) {
@@ -44,6 +50,31 @@ function buildCreateIndexSql(table, columns, indexName, options = {}) {
44
50
  }
45
51
  return `${sql};`;
46
52
  }
53
+ /**
54
+ * The statement SEQUENCE that builds one fix index and converges on a VALID
55
+ * index however many times it is rerun.
56
+ *
57
+ * A no-transaction migration is recorded only after ALL its statements succeed,
58
+ * so a mid-file failure leaves earlier indexes built and the migration
59
+ * unrecorded: a rerun must be safe. The old answer was `CREATE INDEX
60
+ * CONCURRENTLY IF NOT EXISTS`, which is safe but not CONVERGENT: a concurrent
61
+ * build that fails partway leaves an INVALID index with the right name, and
62
+ * every subsequent run skips it. Measured: the index was INVALID before the fix
63
+ * migration, `migrate up` reported 1 applied and 0 errors, and the index was
64
+ * still INVALID after.
65
+ *
66
+ * So the concurrent form drops first instead. `DROP INDEX CONCURRENTLY IF
67
+ * EXISTS` is a no-op on the first run (the index is missing, which is why
68
+ * doctor proposed it), and on a rerun it clears the corpse so the CREATE
69
+ * actually rebuilds. Neither statement takes a blocking lock, and both are
70
+ * legal only outside a transaction, which this file already is.
71
+ */
72
+ function buildCreateIndexStatements(table, columns, indexName, options = {}) {
73
+ const create = buildCreateIndexSql(table, columns, indexName, options);
74
+ if (!options.concurrently)
75
+ return [create];
76
+ return [buildDropIndexSql(indexName, { concurrently: true, ifExists: true }), create];
77
+ }
47
78
  /** Build the matching `DROP INDEX` statement. `concurrently` requires no-transaction execution. */
48
79
  function buildDropIndexSql(indexName, options = {}) {
49
80
  const concurrently = options.concurrently ? 'CONCURRENTLY ' : '';
@@ -283,6 +283,54 @@ export declare function buildRelationsFromForeignKeys(foreignKeys: ForeignKeyEnt
283
283
  * instead of silently dropping the relation.
284
284
  */
285
285
  export declare function addAutoManyToManyRelations(tableNames: Iterable<string>, foreignKeys: ForeignKeyEntry[], pkByTable: Map<string, string[]>, columnNamesByTable: Map<string, string[]>, relationsByTable: Map<string, Record<string, RelationDef>>, columnFieldsByTable?: Map<string, Set<string>>, unknownTypedFieldsByTable?: Map<string, Set<string>>, uniqueIndexColsByTable?: Map<string, string[][]>): void;
286
+ /** Everything the catalog relation derivation reads. See {@link deriveCatalogRelations}. */
287
+ export interface CatalogRelationInputs {
288
+ /** The introspected table set, post include/exclude filtering. */
289
+ tableNames: string[];
290
+ /** FK rows already grouped per constraint (one entry per declared constraint). */
291
+ foreignKeys: ForeignKeyEntry[];
292
+ /** Primary-key columns per table. */
293
+ pkByTable: Map<string, string[]>;
294
+ /** Column metadata per table (only name/field/tsType/pgType are read). */
295
+ columnsByTable: Map<string, Pick<ColumnMetadata, 'name' | 'field' | 'tsType' | 'pgType'>[]>;
296
+ /** UNIQUE-constraint column sets per table. */
297
+ uniqueByTable: Map<string, string[][]>;
298
+ /** Indexes per table, `definition` must be the raw `pg_indexes.indexdef`. */
299
+ indexesByTable: Map<string, IndexMetadata[]>;
300
+ /** Enum types in the schema, so an enum column is not mistaken for a json shadow. */
301
+ enums: Record<string, string[]>;
302
+ /** Referential actions keyed "<table>::<constraint>". Optional. */
303
+ fkActions?: Map<string, {
304
+ onDelete: ReferentialAction;
305
+ onUpdate: ReferentialAction;
306
+ }>;
307
+ /** Opt out of the unique-FK → `hasOne` flip (F2), see {@link IntrospectOptions.legacyToManyUniques}. */
308
+ legacyToManyUniques?: boolean;
309
+ }
310
+ /**
311
+ * One-stop relation derivation for every surface that reads a live PostgreSQL
312
+ * CATALOG: `turbine generate` (via {@link introspectPostgresCatalog}) and the
313
+ * MCP server, which introspects for itself because it cannot assume generated
314
+ * metadata exists.
315
+ *
316
+ * THE REASON THIS IS ONE FUNCTION and not two call sites: the pipeline it drives
317
+ * is `buildRelationsFromForeignKeys` (five parameters, two optional) plus
318
+ * `addAutoManyToManyRelations` (eight parameters, three optional), and every
319
+ * optional one CHANGES THE ANSWER while omitting it stays silently type-correct.
320
+ * Hand-mirroring them drifted exactly that way: MCP passed four arguments and so
321
+ * never received `uniqueSetsByTable`, which meant a UNIQUE foreign key produced
322
+ * `users.profile` (hasOne) under `turbine generate` and `users.profiles`
323
+ * (hasMany) under `turbine mcp`, against the same database. An MCP client
324
+ * following its own schema tool then queried `with: { profiles: true }` and got
325
+ * `TURBINE_E005 Unknown relation`. MCP also omitted `uniqueIndexColsByTable`,
326
+ * losing every auto-m2m relation through a Prisma-style PK-less junction. Adding
327
+ * an argument here now reaches both surfaces or neither.
328
+ *
329
+ * The engine introspectors (SQLite / MySQL / MSSQL) keep their own
330
+ * {@link deriveEngineRelations} because they deliberately do NOT do the
331
+ * unique-FK → `hasOne` flip.
332
+ */
333
+ export declare function deriveCatalogRelations(inputs: CatalogRelationInputs): Map<string, Record<string, RelationDef>>;
286
334
  /**
287
335
  * One-stop relation derivation for the engine introspectors (SQLite / MySQL /
288
336
  * MSSQL): filters the FK list to the introspected table set, seeds the