@proteinjs/db-driver-knex 1.15.0 → 1.15.2

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 (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +5 -3
  3. package/dist/generated/index.js +1 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/generated/test/index.js +1 -1
  6. package/dist/generated/test/index.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/src/KnexDriver.d.ts +0 -7
  12. package/dist/src/KnexDriver.d.ts.map +1 -1
  13. package/dist/src/KnexDriver.js +37 -34
  14. package/dist/src/KnexDriver.js.map +1 -1
  15. package/dist/src/KnexFailureLine.d.ts +58 -0
  16. package/dist/src/KnexFailureLine.d.ts.map +1 -0
  17. package/dist/src/KnexFailureLine.js +124 -0
  18. package/dist/src/KnexFailureLine.js.map +1 -0
  19. package/dist/test/LogValuesSwitch.test.js +13 -7
  20. package/dist/test/LogValuesSwitch.test.js.map +1 -1
  21. package/dist/test/StatementLogNeverThrows.test.js +23 -29
  22. package/dist/test/StatementLogNeverThrows.test.js.map +1 -1
  23. package/dist/test/StatementParamLogging.test.js +16 -41
  24. package/dist/test/StatementParamLogging.test.js.map +1 -1
  25. package/dist/test/VendorTextNeverOnALine.test.d.ts +2 -0
  26. package/dist/test/VendorTextNeverOnALine.test.d.ts.map +1 -0
  27. package/dist/test/VendorTextNeverOnALine.test.js +330 -0
  28. package/dist/test/VendorTextNeverOnALine.test.js.map +1 -0
  29. package/generated/index.ts +1 -1
  30. package/generated/test/index.ts +1 -1
  31. package/index.ts +1 -0
  32. package/package.json +6 -6
  33. package/src/KnexDriver.ts +23 -36
  34. package/src/KnexFailureLine.ts +127 -0
  35. package/test/LogValuesSwitch.test.ts +3 -1
  36. package/test/StatementLogNeverThrows.test.ts +24 -20
  37. package/test/StatementParamLogging.test.ts +12 -17
  38. package/test/VendorTextNeverOnALine.test.ts +257 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db-driver-knex",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "main": "./dist/generated/index.js",
5
5
  "types": "./dist/generated/index.d.ts",
6
6
  "exports": {
@@ -41,10 +41,10 @@
41
41
  "test": "jest --runInBand --passWithNoTests"
42
42
  },
43
43
  "dependencies": {
44
- "@proteinjs/db": "^1.48.0",
45
- "@proteinjs/db-query": "^1.10.1",
46
- "@proteinjs/db-transaction-context": "^0.5.36",
47
- "@proteinjs/logger": "^1.0.21",
44
+ "@proteinjs/db": "^1.49.1",
45
+ "@proteinjs/db-query": "^1.10.2",
46
+ "@proteinjs/db-transaction-context": "^0.5.38",
47
+ "@proteinjs/logger": "^1.2.0",
48
48
  "@proteinjs/reflection": "^1.2.0",
49
49
  "@proteinjs/util": "^1.6.1",
50
50
  "knex": "0.21.5",
@@ -67,5 +67,5 @@
67
67
  "ts-jest": "29.1.1",
68
68
  "typescript": "5.2.2"
69
69
  },
70
- "gitHead": "ec3fd54430dde18afd406ec70eccd857492272a0"
70
+ "gitHead": "21e5b7232ef86cfd43b47fab96e90dad1d855bf8"
71
71
  }
package/src/KnexDriver.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from '@proteinjs/db';
11
11
  import { KnexConfig } from './KnexConfig';
12
12
  import { KnexLogValues } from './KnexLogValues';
13
+ import { KnexFailureLine } from './KnexFailureLine';
13
14
  import { Logger } from '@proteinjs/logger';
14
15
  import { Statement } from '@proteinjs/db-query';
15
16
  import { KnexSchemaOperations } from './KnexSchemaOperations';
@@ -24,9 +25,6 @@ type ParamDescription = { type: string; length?: number; null?: true };
24
25
  */
25
26
  type ParamsDescription = ParamDescription[] | { [name: string]: ParamDescription } | 'unreadable';
26
27
 
27
- /** A failure as the log line carries it: the error's name, the vendor's codes and the server's own message. */
28
- type FailureCauseSummary = { name?: string; code?: string; errno?: number; sqlState?: string; sqlMessage?: string };
29
-
30
28
  /**
31
29
  * Knex driver (configured for MariaDb) for ProteinJs Db
32
30
  */
@@ -177,6 +175,8 @@ export class KnexDriver implements DbDriver {
177
175
  // returns 2 arrays, first is records, second is metadata per record
178
176
  return (await runner.raw(sql, params as any).timeout(this.getOperationDeadlineMs(), { cancel: true }))[0];
179
177
  } catch (error: unknown) {
178
+ // Rethrown as it is; marked, so no line about it — this driver's or a caller's — prints its text.
179
+ KnexFailureLine.mark(error, 'Failed when executing sql');
180
180
  this.logFailure(sql, params, error);
181
181
  throw error;
182
182
  }
@@ -196,10 +196,25 @@ export class KnexDriver implements DbDriver {
196
196
  * @returns the return value of the `fn`
197
197
  */
198
198
  async runTransaction<T>(fn: (transaction: Transaction) => Promise<T>): Promise<T> {
199
- return await this.getKnex().transaction(async (trx) => {
200
- const result = await fn(trx);
201
- return result;
202
- });
199
+ const bodyErrors = new Set<unknown>();
200
+ try {
201
+ return await this.getKnex().transaction(async (trx) => {
202
+ try {
203
+ return await fn(trx);
204
+ } catch (error: unknown) {
205
+ bodyErrors.add(error);
206
+ throw error;
207
+ }
208
+ });
209
+ } catch (error: unknown) {
210
+ // What the BODY threw is the caller's own error, or a failure already marked at the
211
+ // statement's door. Anything else was raised around the body — beginning, committing —
212
+ // and words itself as the vendor did. Rethrown as it is, either way.
213
+ if (!bodyErrors.has(error)) {
214
+ KnexFailureLine.mark(error, 'Transaction failed');
215
+ }
216
+ throw error;
217
+ }
203
218
  }
204
219
 
205
220
  /**
@@ -238,7 +253,7 @@ export class KnexDriver implements DbDriver {
238
253
  sql,
239
254
  params: this.describeParams(params),
240
255
  ...KnexLogValues.ofStatement(params),
241
- cause: this.causeSummary(error),
256
+ cause: KnexFailureLine.causeOf(error),
242
257
  },
243
258
  ...KnexLogValues.ofFailure(error),
244
259
  })
@@ -267,25 +282,6 @@ export class KnexDriver implements DbDriver {
267
282
  }
268
283
  }
269
284
 
270
- /**
271
- * The name, codes and server message of any thrown value — never the query layer's rewritten
272
- * `message` or `sql`. Total: a fact that cannot be read (vendorFact) is left out.
273
- */
274
- private causeSummary(error: unknown): FailureCauseSummary {
275
- const name = this.vendorFact(error, 'name');
276
- const code = this.vendorFact(error, 'code');
277
- const errno = this.vendorFact(error, 'errno');
278
- const sqlState = this.vendorFact(error, 'sqlState');
279
- const sqlMessage = this.vendorFact(error, 'sqlMessage');
280
- return {
281
- ...(typeof name === 'string' ? { name } : {}),
282
- ...(typeof code === 'string' ? { code } : {}),
283
- ...(typeof errno === 'number' ? { errno } : {}),
284
- ...(typeof sqlState === 'string' ? { sqlState } : {}),
285
- ...(typeof sqlMessage === 'string' ? { sqlMessage } : {}),
286
- };
287
- }
288
-
289
285
  /**
290
286
  * A statement's bound parameters as a LOG LINE carries them — the one owner of that form: by
291
287
  * position, the KIND of each value and, for strings, arrays and bytes, its LENGTH. Never a
@@ -377,13 +373,4 @@ export class KnexDriver implements DbDriver {
377
373
  const prototype = Object.getPrototypeOf(params);
378
374
  return prototype === null || Object.getPrototypeOf(prototype) === null;
379
375
  }
380
-
381
- /** One fact of a thrown value, or nothing when it cannot be read (nothing thrown, a scalar, a throwing accessor). */
382
- private vendorFact(error: unknown, fact: keyof FailureCauseSummary): unknown {
383
- try {
384
- return (error as { [fact: string]: unknown } | null | undefined)?.[fact];
385
- } catch {
386
- return undefined;
387
- }
388
- }
389
376
  }
@@ -0,0 +1,127 @@
1
+ import { ErrorLine, LogLineErrors } from '@proteinjs/logger';
2
+ import { KnexLogValues } from './KnexLogValues';
3
+
4
+ /**
5
+ * A failure as the driver's own line carries it under `cause`: the error's name, the vendor's
6
+ * codes and, as `message`, the driver's sentence — the field a reader of any driver's line parses.
7
+ */
8
+ export type FailureCause = {
9
+ name?: string;
10
+ code?: string;
11
+ errno?: number;
12
+ sqlState?: string;
13
+ message: string;
14
+ /** The server's own message — behind the dev-only values switch, never otherwise. */
15
+ sqlMessage?: string;
16
+ };
17
+
18
+ /**
19
+ * The ONE owner of how a failure of the database reads on a LOG LINE: the vendor's codes and a
20
+ * sentence of the driver's own — never the vendor's text.
21
+ *
22
+ * What the driver THROWS is the vendor's error itself, untouched, and it is not safe to print:
23
+ * the query layer rewrites its `message` to the SQL with every binding INTERPOLATED, the client
24
+ * library carries the same text as `sql`, and the server's own message (`sqlMessage`) quotes the
25
+ * value it refused (`Duplicate entry '<key>' for key 'PRIMARY'`). The driver's own line never
26
+ * carried the first two; a CALLER's line about the error it caught carried all three. So every
27
+ * failure that leaves the driver is marked with the logger (`mark`), which never touches the
28
+ * error; from then on any line about it, the driver's or a caller's, carries the codes and the
29
+ * sentence in place of the error's own text.
30
+ *
31
+ * The vendor's text rides a line only behind the dev-only values switch (KnexLogValues): with
32
+ * both gates open a marked error prints as it is, and `causeOf` adds the server's message.
33
+ * Asked at each line, like the switch itself.
34
+ */
35
+ export class KnexFailureLine {
36
+ private static readonly UNNAMED = 'the database refused the statement';
37
+ private static readonly UNREACHABLE = 'the database could not be reached';
38
+ private static readonly SENTENCES: { [code: string]: string } = {
39
+ ER_DUP_ENTRY: 'a row with that key already exists',
40
+ ER_NO_SUCH_TABLE: 'a table the statement names does not exist',
41
+ ER_BAD_FIELD_ERROR: 'a column the statement names does not exist',
42
+ ER_PARSE_ERROR: 'the statement does not parse',
43
+ ER_LOCK_DEADLOCK: 'the transaction was chosen as a deadlock victim',
44
+ ER_LOCK_WAIT_TIMEOUT: 'a lock wait timed out',
45
+ ER_DATA_TOO_LONG: 'a value is longer than its column',
46
+ ER_TRUNCATED_WRONG_VALUE: 'a value does not convert to the type of its column',
47
+ ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: 'a value does not convert to the type of its column',
48
+ ER_WARN_DATA_OUT_OF_RANGE: 'a value is out of range for its column',
49
+ ER_BAD_NULL_ERROR: 'a column that cannot be null was given null',
50
+ ER_NO_REFERENCED_ROW_2: 'a foreign key refused the statement',
51
+ ER_ROW_IS_REFERENCED_2: 'a foreign key refused the statement',
52
+ ER_ACCESS_DENIED_ERROR: 'the credentials of the caller were not accepted',
53
+ ER_NET_PACKET_TOO_LARGE: 'the statement is larger than the server accepts',
54
+ ECONNREFUSED: KnexFailureLine.UNREACHABLE,
55
+ ECONNRESET: KnexFailureLine.UNREACHABLE,
56
+ ETIMEDOUT: KnexFailureLine.UNREACHABLE,
57
+ PROTOCOL_CONNECTION_LOST: KnexFailureLine.UNREACHABLE,
58
+ };
59
+ /**
60
+ * The failures the query layer raises itself, which carry no vendor code — by the error's name.
61
+ * Its timeout is one error for the statement's operation deadline and for the wait for a free
62
+ * connection; either way it carries the statement's bindings, so it is marked like any other.
63
+ */
64
+ private static readonly SENTENCES_BY_NAME: { [name: string]: string } = {
65
+ KnexTimeoutError: 'the statement or its wait for a connection timed out',
66
+ };
67
+
68
+ /**
69
+ * Marks `error` — whatever the client library or the query layer rejected with — as never
70
+ * printing its own text, and answers the same error. `what` is the driver's words for what
71
+ * failed. An error already marked keeps its line.
72
+ */
73
+ static mark<T>(error: T, what: string): T {
74
+ if (!LogLineErrors.isMarked(error)) {
75
+ LogLineErrors.mark(error, () => (KnexLogValues.enabled() ? undefined : KnexFailureLine.lineOf(error, what)));
76
+ }
77
+ return error;
78
+ }
79
+
80
+ /**
81
+ * The failure as the driver's own line carries it under `cause` — never the query layer's
82
+ * rewritten `message` or `sql`, and the server's own message only behind the values switch.
83
+ * Total: a fact that cannot be read is left out.
84
+ */
85
+ static causeOf(error: unknown): FailureCause {
86
+ const name = KnexFailureLine.vendorFact(error, 'name');
87
+ const code = KnexFailureLine.vendorFact(error, 'code');
88
+ const errno = KnexFailureLine.vendorFact(error, 'errno');
89
+ const sqlState = KnexFailureLine.vendorFact(error, 'sqlState');
90
+ const sqlMessage = KnexLogValues.enabled() ? KnexFailureLine.vendorFact(error, 'sqlMessage') : undefined;
91
+ return {
92
+ ...(typeof name === 'string' ? { name } : {}),
93
+ ...(typeof code === 'string' ? { code } : {}),
94
+ ...(typeof errno === 'number' ? { errno } : {}),
95
+ ...(typeof sqlState === 'string' ? { sqlState } : {}),
96
+ message: KnexFailureLine.sentenceOf(code, name),
97
+ ...(typeof sqlMessage === 'string' ? { sqlMessage } : {}),
98
+ };
99
+ }
100
+
101
+ private static lineOf(error: unknown, what: string): ErrorLine {
102
+ const { name: _name, message, sqlMessage: _sqlMessage, ...codes } = KnexFailureLine.causeOf(error);
103
+ const named = [codes.code, codes.errno !== undefined ? `errno ${codes.errno}` : undefined].filter(Boolean);
104
+ return {
105
+ ...(codes.code !== undefined ? { code: codes.code } : {}),
106
+ sentence: `${what}${named.length ? ` (${named.join(', ')})` : ''}: ${message}`,
107
+ facts: codes,
108
+ };
109
+ }
110
+
111
+ private static sentenceOf(code: unknown, name: unknown): string {
112
+ return (
113
+ (typeof code === 'string' ? KnexFailureLine.SENTENCES[code] : undefined) ??
114
+ (typeof name === 'string' ? KnexFailureLine.SENTENCES_BY_NAME[name] : undefined) ??
115
+ KnexFailureLine.UNNAMED
116
+ );
117
+ }
118
+
119
+ /** One fact of a thrown value, or nothing when it cannot be read (nothing thrown, a scalar, a throwing accessor). */
120
+ private static vendorFact(error: unknown, fact: string): unknown {
121
+ try {
122
+ return (error as { [fact: string]: unknown } | null | undefined)?.[fact];
123
+ } catch {
124
+ return undefined;
125
+ }
126
+ }
127
+ }
@@ -141,12 +141,14 @@ describe('the dev-only values switch: real values ride the driver`s failure line
141
141
  expect(facts(caught)).toEqual(baseline);
142
142
  // The description and the summary ride the line at every gate.
143
143
  expect(failure.obj.params).toEqual([{ type: 'string', length: VALUE.length }, { type: 'number' }]);
144
+ // The server's own message rides the summary only behind the switch.
144
145
  expect(failure.obj.cause).toEqual({
145
146
  name: 'Error',
146
147
  code: 'ER_NO_SUCH_TABLE',
147
148
  errno: 1146,
148
149
  sqlState: '42S02',
149
- sqlMessage: `Table 'test.credential' doesn't exist`,
150
+ message: 'a table the statement names does not exist',
151
+ ...(on ? { sqlMessage: `Table 'test.credential' doesn't exist` } : {}),
150
152
  });
151
153
  if (!on) {
152
154
  for (const log of captured) {
@@ -1,6 +1,6 @@
1
1
  import knex from 'knex';
2
2
  import { inspect } from 'util';
3
- import { KnexDriver } from '@proteinjs/db-driver-knex';
3
+ import { KnexDriver, KnexFailureLine } from '@proteinjs/db-driver-knex';
4
4
  import { Logger } from '@proteinjs/logger';
5
5
  import { CapturedLog, lineOf } from './util/printedLine';
6
6
 
@@ -12,7 +12,7 @@ import { CapturedLog, lineOf } from './util/printedLine';
12
12
  *
13
13
  * Two layers hold that, and this suite holds both:
14
14
  *
15
- * - the helpers that build the line (`describeParams`, `causeSummary`) are TOTAL: whatever they
15
+ * - the helpers that build the line (`describeParams`, `KnexFailureLine.causeOf`) are TOTAL: whatever they
16
16
  * are handed — positional bindings, a bindings dictionary, nothing, null, a scalar, a Map, a
17
17
  * value whose accessor or `toJSON` throws, a proxy that refuses everything — they return a
18
18
  * description and never a value;
@@ -34,7 +34,6 @@ type ParamsDescription = ParamDescription[] | { [name: string]: ParamDescription
34
34
  type DriverInternals = {
35
35
  logger: Logger;
36
36
  describeParams: (params?: unknown) => ParamsDescription | undefined;
37
- causeSummary: (error: unknown) => { [fact: string]: unknown };
38
37
  };
39
38
  type DriverStatics = { KNEX?: unknown };
40
39
 
@@ -164,13 +163,8 @@ describe('describeParams is total: any bindings, a description, never a value',
164
163
  });
165
164
  });
166
165
 
167
- describe('causeSummary is total: anything thrown, a summary of codes, never the rewritten message', () => {
168
- const internals = new KnexDriver({
169
- host: 'localhost',
170
- user: 'root',
171
- password: '',
172
- dbName: 'test',
173
- }) as unknown as DriverInternals;
166
+ describe('KnexFailureLine.causeOf is total: anything thrown, a summary of codes and the driver`s sentence, never the vendor`s text', () => {
167
+ const UNNAMED = 'the database refused the statement';
174
168
  const vendorFacts = {
175
169
  code: 'ER_NO_SUCH_TABLE',
176
170
  errno: 1146,
@@ -182,27 +176,37 @@ describe('causeSummary is total: anything thrown, a summary of codes, never the
182
176
  [
183
177
  'a vendor error whose message and sql carry the bindings',
184
178
  () => Object.assign(new Error(`insert ... '${CANARY}'`), vendorFacts, { sql: `insert ... '${CANARY}'` }),
185
- { name: 'Error', ...vendorFacts },
179
+ {
180
+ name: 'Error',
181
+ code: 'ER_NO_SUCH_TABLE',
182
+ errno: 1146,
183
+ sqlState: '42S02',
184
+ message: 'a table the statement names does not exist',
185
+ },
186
+ ],
187
+ ['nothing', () => undefined, { message: UNNAMED }],
188
+ ['null', () => null, { message: UNNAMED }],
189
+ ['a thrown string', () => CANARY, { message: UNNAMED }],
190
+ ['a thrown number', () => 1146, { message: UNNAMED }],
191
+ [
192
+ 'facts of the wrong kind',
193
+ () => ({ name: 5, code: 1146, errno: '1146', sqlState: null, sqlMessage: {} }),
194
+ { message: UNNAMED },
186
195
  ],
187
- ['nothing', () => undefined, {}],
188
- ['null', () => null, {}],
189
- ['a thrown string', () => CANARY, {}],
190
- ['a thrown number', () => 1146, {}],
191
- ['facts of the wrong kind', () => ({ name: 5, code: 1146, errno: '1146', sqlState: null, sqlMessage: {} }), {}],
192
196
  [
193
197
  'an error with a fact whose accessor throws',
194
198
  () => throwingEntry(Object.assign(new Error(CANARY), vendorFacts), 'code'),
195
- { name: 'Error', errno: 1146, sqlState: '42S02', sqlMessage: vendorFacts.sqlMessage },
199
+ { name: 'Error', errno: 1146, sqlState: '42S02', message: UNNAMED },
196
200
  ],
197
- ['an error that refuses every read', () => refusesEverything(new Error(CANARY)), {}],
198
- ['a revoked error', () => revoked(new Error(CANARY)), {}],
201
+ ['an error that refuses every read', () => refusesEverything(new Error(CANARY)), { message: UNNAMED }],
202
+ ['a revoked error', () => revoked(new Error(CANARY)), { message: UNNAMED }],
199
203
  ];
200
204
 
201
205
  test.each(THROWN)('%s', (_shape, thrown, expected) => {
202
206
  let summary: { [fact: string]: unknown } | undefined;
203
207
 
204
208
  expect(() => {
205
- summary = internals.causeSummary(thrown());
209
+ summary = KnexFailureLine.causeOf(thrown());
206
210
  }).not.toThrow();
207
211
 
208
212
  expect(summary).toEqual(expected);
@@ -14,8 +14,8 @@ import { CapturedLog, lineOf } from './util/printedLine';
14
14
  * The contract: the failure line carries the SQL text (placeholders only), the parameters
15
15
  * DESCRIBED — each one's position, the kind of its value and, for strings, arrays and bytes, its
16
16
  * length; one private helper (`describeParams`) owns that form — and a summary of the failure (the
17
- * error's name, the vendor's codes, the server's own message). The vendor error itself no longer
18
- * rides the line. (The dev-only switch that adds the values back is LogValuesSwitch.test.ts; it is
17
+ * error's name, the vendor's codes, the driver's own sentence — KnexFailureLine). The vendor error
18
+ * itself no longer rides the line. (The dev-only switch that adds the values back is LogValuesSwitch.test.ts; it is
19
19
  * off here.) A statement's bindings are typed as an array, but the query layer also accepts a
20
20
  * bindings DICTIONARY (`:name` placeholders) and the driver forwards what it was handed: a
21
21
  * dictionary is described by entry name. (Every other shape the driver can be handed, and a log
@@ -26,13 +26,10 @@ import { CapturedLog, lineOf } from './util/printedLine';
26
26
  * that prints what it catches still prints the interpolated SQL; that is the query layer's
27
27
  * behaviour and this contract does not change it.
28
28
  *
29
- * SCOPE, by name: this suite holds what the DRIVER prints of the parameters it was handed. It does
30
- * NOT hold the vendor-echo class — the server's own message can quote a bound value (`Duplicate
31
- * entry '…' for key 'PRIMARY'`), and that text reaches the line's cause summary and the thrown
32
- * error exactly as it did before. Closing that class means changing what the driver throws; that
33
- * work is parked on the branch `fix/driver-logs-never-carry-param-values`, pending a decision. So
34
- * the canaries below are bound to parameters the server does not echo, and the one value it does
35
- * echo — the colliding key — is left out of the assertions on purpose.
29
+ * SCOPE, by name: this suite holds what the DRIVER prints of the parameters it was handed. The
30
+ * vendor-echo class — the server's own message quoting a bound value (`Duplicate entry '…' for key
31
+ * 'PRIMARY'`) — is VendorTextNeverOnALine.test.ts: no line carries the server's message, and a
32
+ * caller's line about the error it caught carries the driver's sentence in its place.
36
33
  *
37
34
  * No server is needed: the REAL query layer and dialect run over a stub connection that rejects
38
35
  * the way the client library does (its own `format` builds the interpolated `sql`), so the
@@ -140,11 +137,10 @@ describe('The driver never prints a bound value; what it throws is the vendor`s
140
137
 
141
138
  const failures = captured.filter((log) => log.logLevel === 'error' && log.message === 'Failed when executing sql');
142
139
  expect(failures).toHaveLength(1);
143
- // (`ECHOED_KEY` is NOT asserted: the server quotes the colliding key in its own message — the
144
- // vendor-echo class this suite leaves out by name; see the header.)
145
140
  for (const log of captured) {
146
141
  expect(lineOf(log)).not.toContain(SECRET_TOKEN);
147
142
  expect(lineOf(log)).not.toContain(SECRET_EMAIL);
143
+ expect(lineOf(log)).not.toContain(ECHOED_KEY);
148
144
  }
149
145
  const [failure] = failures;
150
146
  expect(failure.obj.sql).toBe(INSERT);
@@ -160,7 +156,7 @@ describe('The driver never prints a bound value; what it throws is the vendor`s
160
156
  code: 'ER_DUP_ENTRY',
161
157
  errno: 1062,
162
158
  sqlState: '23000',
163
- sqlMessage: `Duplicate entry '${ECHOED_KEY}' for key 'PRIMARY'`,
159
+ message: 'a row with that key already exists',
164
160
  });
165
161
  expect(Object.keys(failure.obj)).not.toContain('paramValues');
166
162
  // The vendor error itself — the carrier of the interpolated SQL — is not on the line.
@@ -188,11 +184,10 @@ describe('The driver never prints a bound value; what it throws is the vendor`s
188
184
  const failures = captured.filter((log) => log.logLevel === 'error');
189
185
  expect(failures).toHaveLength(1);
190
186
  expect(failures[0].obj.params).toEqual([{ type: 'string', length: SECRET_TOKEN.length }]);
191
- // This statement's first binding IS the token, so the stub server echoes it: the summary's
192
- // `sqlMessage` is the vendor-echo class. Everything else on the line is the driver's.
193
- const { sqlMessage, ...ownFacts } = failures[0].obj.cause;
194
- expect(sqlMessage).toContain(SECRET_TOKEN);
195
- expect(lineOf({ ...failures[0], obj: { ...failures[0].obj, cause: ownFacts } })).not.toContain(SECRET_TOKEN);
187
+ // This statement's first binding IS the token, so the stub server echoes it in its own
188
+ // message — which no line carries.
189
+ expect(Object.keys(failures[0].obj.cause)).not.toContain('sqlMessage');
190
+ expect(lineOf(failures[0])).not.toContain(SECRET_TOKEN);
196
191
  });
197
192
 
198
193
  test('a failed statement inside a transaction: the same line, and the transaction rejects with the vendor`s error itself', async () => {