pipework 0.282.0 → 0.284.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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/config/load.d.ts +11 -1
  3. package/dist/core/config/load.d.ts.map +1 -1
  4. package/dist/core/config/load.js +5 -0
  5. package/dist/core/config/load.js.map +1 -1
  6. package/dist/core/config/namespace.d.ts +5 -0
  7. package/dist/core/config/namespace.d.ts.map +1 -1
  8. package/dist/core/config/schema.d.ts +11 -1
  9. package/dist/core/config/schema.d.ts.map +1 -1
  10. package/dist/core/config/schema.js +7 -0
  11. package/dist/core/config/schema.js.map +1 -1
  12. package/dist/core/config/statement-observer.d.ts +58 -0
  13. package/dist/core/config/statement-observer.d.ts.map +1 -1
  14. package/dist/core/surface/types.d.ts +5 -0
  15. package/dist/core/surface/types.d.ts.map +1 -1
  16. package/dist/data/db/instrumentation.d.ts +6 -4
  17. package/dist/data/db/instrumentation.d.ts.map +1 -1
  18. package/dist/data/db/instrumentation.js +12 -14
  19. package/dist/data/db/instrumentation.js.map +1 -1
  20. package/dist/data/db/namespace.d.ts +1 -1
  21. package/dist/data/db/namespace.d.ts.map +1 -1
  22. package/dist/data/db/namespace.js +1 -1
  23. package/dist/data/db/namespace.js.map +1 -1
  24. package/dist/data/db/pool.js +5 -5
  25. package/dist/data/db/pool.js.map +1 -1
  26. package/dist/data/db/query-size.d.ts +18 -0
  27. package/dist/data/db/query-size.d.ts.map +1 -0
  28. package/dist/data/db/query-size.js +68 -0
  29. package/dist/data/db/query-size.js.map +1 -0
  30. package/dist/data/db/query-watch.d.ts +24 -0
  31. package/dist/data/db/query-watch.d.ts.map +1 -1
  32. package/dist/data/db/query-watch.js +101 -2
  33. package/dist/data/db/query-watch.js.map +1 -1
  34. package/dist/data/db/raw-client-warning.js +1 -1
  35. package/dist/data/db/raw-client-warning.js.map +1 -1
  36. package/dist/data/query/postgres-js/session.d.ts +1 -1
  37. package/dist/data/query/postgres-js/session.d.ts.map +1 -1
  38. package/dist/data/query/postgres-js/session.js +16 -9
  39. package/dist/data/query/postgres-js/session.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/test/plugin.js +7 -7
  43. package/dist/test/plugin.js.map +1 -1
  44. package/package.json +6 -2
  45. package/src/core/config/load.ts +16 -1
  46. package/src/core/config/schema.ts +8 -1
  47. package/src/core/config/statement-observer.ts +61 -0
  48. package/src/core/surface/types.ts +5 -0
  49. package/src/data/db/instrumentation.ts +15 -14
  50. package/src/data/db/namespace.ts +1 -1
  51. package/src/data/db/pool.ts +5 -5
  52. package/src/data/db/query-size.ts +63 -0
  53. package/src/data/db/query-watch.ts +115 -3
  54. package/src/data/db/raw-client-warning.ts +1 -1
  55. package/src/data/query/postgres-js/session.ts +20 -11
  56. package/src/index.ts +1 -1
  57. package/src/test/plugin.ts +7 -7
@@ -6,7 +6,7 @@ import { createTestContext as createCtx } from '../request/context/create.js';
6
6
  import { runInContext } from '../request/context/store.js';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { createDebugFn, isObserverOnly } from '../data/db/instrumentation.js';
9
- import { createStatementWatch } from '../data/db/query-watch.js';
9
+ import { createStatementWatch, largeQueryBudgets } from '../data/db/query-watch.js';
10
10
  import { markObservedConnection } from '../data/query/postgres-js/driver-error-boundary.js';
11
11
  import { collapseTransactionsToSavepoints } from './savepoints.js';
12
12
  import { splitLargeInserts } from '../data/db/insert-split.js';
@@ -80,11 +80,11 @@ export async function endTestIsolation(instance, ctx) {
80
80
  instance.isolation.markEnd();
81
81
  }
82
82
  async function createIsolatedConnection(testDb, connections, name, observability, withSystem) {
83
- const debug = createDebugFn(observability);
83
+ const watch = createStatementWatch({ database: name, url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled, budgets: largeQueryBudgets(observability) });
84
+ const debug = createDebugFn(observability, watch.noteConnection);
84
85
  const client = postgres(testDb.url, { max: 1, onnotice: () => { }, ...(debug ? { debug } : {}) });
85
86
  if (isObserverOnly(observability))
86
87
  markObservedConnection(client);
87
- const watch = createStatementWatch({ database: name, url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs });
88
88
  const db = drizzle(client, { statementWatch: watch.watch });
89
89
  await db.execute(sql.raw('BEGIN'));
90
90
  const join = collapseTransactionsToSavepoints(db, client);
@@ -119,13 +119,13 @@ export async function propagateTestSessionVars(instance, testConnections, tenant
119
119
  }
120
120
  }
121
121
  export function createSetupConnection(testDb, observability) {
122
- const debug = observability ? createDebugFn(observability) : undefined;
122
+ const watch = observability
123
+ ? createStatementWatch({ database: 'setup', url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled, budgets: largeQueryBudgets(observability) })
124
+ : undefined;
125
+ const debug = observability ? createDebugFn(observability, watch?.noteConnection) : undefined;
123
126
  const client = postgres(testDb.url, { max: 1, onnotice: () => { }, ...(debug ? { debug } : {}) });
124
127
  if (observability && isObserverOnly(observability))
125
128
  markObservedConnection(client);
126
- const watch = observability
127
- ? createStatementWatch({ database: 'setup', url: testDb.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs })
128
- : undefined;
129
129
  const db = splitLargeInserts(drizzle(client, watch ? { statementWatch: watch.watch } : {}));
130
130
  return { drizzle: db, close: async () => { await watch?.close(); await client.end(); } };
131
131
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/test/plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/test/plugin.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipework",
3
- "version": "0.282.0",
3
+ "version": "0.284.0",
4
4
  "description": "TypeScript framework for multi-tenant SaaS applications. PostgreSQL-only.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -203,6 +203,10 @@
203
203
  "test:stress": "vitest run --config vitest.stress.config.ts",
204
204
  "test:packages": "pnpm -r --filter='./packages/*' test",
205
205
  "test:all": "pnpm test:packages && vitest run && vitest run --config vitest.isolation.config.ts && vitest run --config vitest.property.config.ts && vitest run --config vitest.stress.config.ts",
206
- "mutate": "stryker run"
206
+ "mutate": "stryker run",
207
+ "rust:fmt": "cargo fmt --all --check",
208
+ "rust:clippy": "cargo clippy --workspace --all-targets -- -D warnings && cargo clippy -p pipework-query --all-targets --no-default-features --features all-types,audit,hashable-value,serde,thread-safe,postgres-driver -- -D warnings",
209
+ "rust:test": "cargo test --workspace && cargo test -p pipework-query --features all-types,hashable-value,serde",
210
+ "rust:check": "pnpm rust:fmt && pnpm rust:clippy && pnpm rust:test"
207
211
  }
208
212
  }
@@ -4,16 +4,26 @@ import { resolveEnvironment, resolveEnvironmentStrict, resolveDbUrl, resolveTest
4
4
  import { ConfigError } from './errors.js'
5
5
  import { resolveEnvVars } from './env-resolve.js'
6
6
  import { consumeConfigBaseDir } from './base-dir.js'
7
- import type { StatementObserver } from './statement-observer.js'
7
+ import type { StatementObserver, StatementSettledObserver } from './statement-observer.js'
8
8
 
9
9
  export interface ResolvedObservability {
10
10
  readonly slowQueryMs: number
11
11
  /** Interval at which a still-running statement is reported; undefined = off. */
12
12
  readonly inFlightQueryMs?: number | undefined
13
+ /** Estimated bind-parameter bytes above which a statement logs `query:large`; 0 = off. */
14
+ readonly largeQueryParamBytes: number
15
+ /** SQL text length (characters) above which a statement logs `query:large`; 0 = off. */
16
+ readonly largeQuerySqlChars: number
17
+ /** UNION/INTERSECT/EXCEPT arms (a text count) above which a statement logs `query:large`; 0 = off. */
18
+ readonly largeQueryArms: number
19
+ /** Rows returned (or affected) above which a statement logs `query:large`; 0 = off. */
20
+ readonly largeQueryRows: number
13
21
  readonly logAllQueries: boolean
14
22
  readonly poolMetricsInterval: number
15
23
  /** Caller-supplied per-statement observer; undefined when not configured. */
16
24
  readonly onStatement: StatementObserver | undefined
25
+ /** Caller-supplied per-statement completion hook; undefined when not configured. */
26
+ readonly onStatementSettled?: StatementSettledObserver | undefined
17
27
  }
18
28
 
19
29
  export interface ResolvedTrace {
@@ -244,9 +254,14 @@ function buildResolved(config: Blueprint, environment: Environment, baseDir: str
244
254
  observability: {
245
255
  slowQueryMs: config.observability?.slowQueryMs ?? 500,
246
256
  inFlightQueryMs: config.observability?.inFlightQueryMs,
257
+ largeQueryParamBytes: config.observability?.largeQueryParamBytes ?? 8_388_608,
258
+ largeQuerySqlChars: config.observability?.largeQuerySqlChars ?? 131_072,
259
+ largeQueryArms: config.observability?.largeQueryArms ?? 50,
260
+ largeQueryRows: config.observability?.largeQueryRows ?? 100_000,
247
261
  logAllQueries: config.observability?.logAllQueries ?? false,
248
262
  poolMetricsInterval: config.observability?.poolMetricsInterval ?? 0,
249
263
  onStatement: config.observability?.onStatement,
264
+ onStatementSettled: config.observability?.onStatementSettled,
250
265
  },
251
266
  trace: {
252
267
  hotRetentionDays: config.trace?.hotRetentionDays ?? 7,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod/v4'
2
- import type { StatementObserver } from './statement-observer.js'
2
+ import type { StatementObserver, StatementSettledObserver } from './statement-observer.js'
3
3
 
4
4
  const SETTING_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/
5
5
 
@@ -171,6 +171,10 @@ const fastPathSchema = z.object({
171
171
 
172
172
  const observabilitySchema = z.object({
173
173
  slowQueryMs: z.number().int().positive().optional().describe('Warn when a query exceeds this duration in ms'),
174
+ largeQueryParamBytes: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement\'s bind parameters total more than this many bytes, estimated (default 8388608; 0 = off)'),
175
+ largeQuerySqlChars: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement\'s SQL text is longer than this many characters (default 131072; 0 = off)'),
176
+ largeQueryArms: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement\'s SQL has more than this many UNION/INTERSECT/EXCEPT arms, counted in the text (default 50; 0 = off)'),
177
+ largeQueryRows: z.number().int().nonnegative().optional().describe('Log `query:large` when a statement returns (or affects) more than this many rows (default 100000; 0 = off)'),
174
178
  inFlightQueryMs: z.number().int().positive().optional().describe('While a statement is still running, log what it is waiting on every this many ms (unset = off)'),
175
179
  logAllQueries: z.boolean().optional().describe('Log every query at debug level'),
176
180
  poolMetricsInterval: z.number().int().nonnegative().optional().describe('Seconds between pool metric summaries (0 = disabled)'),
@@ -181,6 +185,9 @@ const observabilitySchema = z.object({
181
185
  onStatement: z.custom<StatementObserver>(v => typeof v === 'function', { message: 'Expected a function' })
182
186
  .optional()
183
187
  .describe('Per-statement observer — fires once per statement written to the wire, including driver-issued BEGIN/SAVEPOINT/COMMIT/ROLLBACK'),
188
+ onStatementSettled: z.custom<StatementSettledObserver>(v => typeof v === 'function', { message: 'Expected a function' })
189
+ .optional()
190
+ .describe('Per-statement completion hook — fires once per statement when it settles, with label, durationMs, rows, connectionId and (on failure) a sanitized error'),
184
191
  })
185
192
 
186
193
  const envVarDefSchema = z.discriminatedUnion('type', [
@@ -46,3 +46,64 @@ export interface StatementEvent {
46
46
  * database.
47
47
  */
48
48
  export type StatementObserver = (event: StatementEvent) => void
49
+
50
+ /**
51
+ * A failed statement's error, reduced to what is safe to hand to a hook:
52
+ * the driver's own error objects carry the statement text and bind parameters
53
+ * (and, on an observed connection, make them enumerable), so the hook never
54
+ * sees the error object itself.
55
+ */
56
+ export interface StatementSettledError {
57
+ readonly name: string
58
+ readonly message: string
59
+ /** The PostgreSQL SQLSTATE (or the driver's error code) when the error carries one. */
60
+ readonly code?: string
61
+ }
62
+
63
+ /**
64
+ * One statement as it settles — the completion counterpart to `StatementEvent`.
65
+ * Fields are only ever added to this payload, never reshaped.
66
+ */
67
+ export interface StatementSettledEvent {
68
+ /**
69
+ * The `pipe.labelled` label in effect when the statement settled; null when
70
+ * none. This is the label alone: unlike `query:slow`, it never falls back to
71
+ * the statement text.
72
+ */
73
+ readonly label: string | null
74
+ /**
75
+ * Wall-clock milliseconds the statement took, measured by the same timer as
76
+ * `query:slow` (so for one statement the two agree exactly).
77
+ */
78
+ readonly durationMs: number
79
+ /**
80
+ * Rows the statement returned, or, for a write with no `RETURNING`, rows it
81
+ * affected: the command tag's count. 0 for a statement with no count
82
+ * (DDL, `SET`, ...) and for a failed statement.
83
+ */
84
+ readonly rows: number
85
+ /**
86
+ * The same ordinal `StatementEvent.connectionId` carries for this statement;
87
+ * null when the statement never reached a connection (it failed before being
88
+ * written).
89
+ */
90
+ readonly connectionId: number | null
91
+ /** Present exactly when the statement failed. Never carries parameters. */
92
+ readonly error?: StatementSettledError
93
+ }
94
+
95
+ /**
96
+ * Caller-supplied completion hook. Fires once per statement that settles, on
97
+ * every statement pipework runs through its own query path: `pipe()`, every
98
+ * `pipe.*` write, `connection.drizzle`, transactions and savepoints, and the
99
+ * test harness's connections. It does not fire for the driver's own
100
+ * transaction control (`BEGIN`/`SAVEPOINT`/`COMMIT`/`ROLLBACK`), nor for the
101
+ * raw `ManagedConnection.client` door, nor for the migration runner's own
102
+ * connection; `onStatement` is the hook that sees wire truth.
103
+ *
104
+ * Pure observation, fired after the statement has settled and its result is on
105
+ * its way back: a hook that throws, or returns a promise that rejects, is
106
+ * logged (`statement:settled-hook-failed`) and never affects the statement.
107
+ * A returned promise is not awaited.
108
+ */
109
+ export type StatementSettledObserver = (event: StatementSettledEvent) => void | Promise<void>
@@ -70,9 +70,14 @@ export interface SurfaceContext {
70
70
  readonly observability: {
71
71
  readonly slowQueryMs: number
72
72
  readonly inFlightQueryMs?: number | undefined
73
+ readonly largeQueryParamBytes: number
74
+ readonly largeQuerySqlChars: number
75
+ readonly largeQueryArms: number
76
+ readonly largeQueryRows: number
73
77
  readonly logAllQueries: boolean
74
78
  readonly poolMetricsInterval: number
75
79
  readonly onStatement: import('../config/statement-observer.js').StatementObserver | undefined
80
+ readonly onStatementSettled?: import('../config/statement-observer.js').StatementSettledObserver | undefined
76
81
  }
77
82
  readonly raw: unknown
78
83
  }
@@ -5,6 +5,9 @@ import type { ResolvedObservability } from '../../core/config/load.js'
5
5
  const DEADLOCK_CODE = '40P01'
6
6
  const QUERY_TRUNCATE_LENGTH = 500
7
7
 
8
+ /** Records which connection a statement's parameter array was written on; see `createStatementWatch`. */
9
+ export type ConnectionNote = (connectionId: number, parameters: readonly unknown[]) => void
10
+
8
11
  /** postgres.js's `debug` hook signature. */
9
12
  type DebugFn = (connection: number, query: string, parameters: unknown[]) => void
10
13
 
@@ -15,30 +18,28 @@ type DebugFn = (connection: number, query: string, parameters: unknown[]) => voi
15
18
  * are today, that mode having already opted into verbose errors.
16
19
  */
17
20
  export function isObserverOnly(config: ResolvedObservability): boolean {
18
- return config.onStatement !== undefined && !config.logAllQueries
21
+ return (config.onStatement !== undefined || config.onStatementSettled !== undefined) && !config.logAllQueries
19
22
  }
20
23
 
21
24
  /**
22
25
  * The driver's `debug` hook for a connection, or undefined when nothing is
23
- * observing. Query logging and the caller-supplied `onStatement` observer are
24
- * independent: either alone installs the hook, and with both configured both
25
- * fire for every statement.
26
+ * observing. Query logging, the caller-supplied `onStatement` observer and the
27
+ * connection note behind `onStatementSettled` are independent: any one alone
28
+ * installs the hook, and with several configured all fire for every statement.
26
29
  */
27
- export function createDebugFn(config: ResolvedObservability): DebugFn | undefined {
30
+ export function createDebugFn(config: ResolvedObservability, noteConnection?: ConnectionNote): DebugFn | undefined {
28
31
  const logQuery = config.logAllQueries ? createQueryLogFn() : undefined
29
32
  const observer = config.onStatement
33
+ // The completion hook needs the connection a statement ran on, which only this hook is told.
34
+ const note = config.onStatementSettled !== undefined ? noteConnection : undefined
30
35
 
31
- if (!logQuery && !observer) return undefined
32
- if (!observer) return logQuery
33
- if (!logQuery) {
34
- return (connection, query, parameters) => {
35
- observer({ connectionId: connection, query, parameters })
36
- }
37
- }
36
+ if (!logQuery && !observer && !note) return undefined
37
+ if (!observer && !note) return logQuery
38
38
 
39
39
  return (connection, query, parameters) => {
40
- logQuery(connection, query, parameters)
41
- observer({ connectionId: connection, query, parameters })
40
+ logQuery?.(connection, query, parameters)
41
+ note?.(connection, parameters)
42
+ observer?.({ connectionId: connection, query, parameters })
42
43
  }
43
44
  }
44
45
 
@@ -38,7 +38,7 @@ export const pipe = Object.assign(getDb, {
38
38
  /** Query operators and aggregates — eq, gt, lt, and, or, count, sum, etc. */
39
39
  filter,
40
40
 
41
- /** Names every statement `fn` starts, in `query:slow` and `query:in-flight` log lines: `pipe.labelled('servable-footprints', async () => await ...)`. A query builder is lazy: `fn` must await it, not return it, or it runs outside the scope. */
41
+ /** Names every statement `fn` starts, in `query:slow`, `query:large` and `query:in-flight` log lines: `pipe.labelled('servable-footprints', async () => await ...)`. A query builder is lazy: `fn` must await it, not return it, or it runs outside the scope. */
42
42
  labelled,
43
43
 
44
44
  /** Raw statement tag — for a statement pipework does not model. Interpolated values are parameters; `sql.raw` is not. Runs on the caller's connection, so inside a request it is the request transaction. Writes still go through pipe.update / insert / delete, which is what audit hooks. */
@@ -6,7 +6,7 @@ import { createDebugFn, isObserverOnly } from './instrumentation.js'
6
6
  import { markObservedConnection } from '../query/postgres-js/driver-error-boundary.js'
7
7
  import { warnRawClientIsUnsanitized } from './raw-client-warning.js'
8
8
  import { splitLargeInserts } from './insert-split.js'
9
- import { createStatementWatch } from './query-watch.js'
9
+ import { createStatementWatch, largeQueryBudgets } from './query-watch.js'
10
10
 
11
11
  /** Which database role a pooled connection authenticates as. `owner` runs migrations/bootstrap and owns tables (bypasses RLS); `app` is the non-owner, non-BYPASSRLS runtime role that RLS policies enforce against; `system` is the platform role (`rls.platformRole`, via `systemUrl`), which reaches a tenant table only through a declared door. */
12
12
  export type ConnectionRole = 'owner' | 'app' | 'system'
@@ -166,7 +166,10 @@ export function createManagedConnection(
166
166
  observability?: ResolvedObservability,
167
167
  applicationName?: string,
168
168
  ): ManagedConnection {
169
- const baseDebug = observability ? createDebugFn(observability) : undefined
169
+ const statementWatch = observability
170
+ ? createStatementWatch({ database: config.name, url: config.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs, onSettled: observability.onStatementSettled, budgets: largeQueryBudgets(observability) })
171
+ : undefined
172
+ const baseDebug = observability ? createDebugFn(observability, statementWatch?.noteConnection) : undefined
170
173
  const slot: ProbeSlot | undefined = baseDebug ? { current: null } : undefined
171
174
  const debug = baseDebug && slot
172
175
  ? ((connection, query, parameters) => {
@@ -187,9 +190,6 @@ export function createManagedConnection(
187
190
  const observerOnly = observability !== undefined && isObserverOnly(observability)
188
191
  if (observerOnly) markObservedConnection(client)
189
192
 
190
- const statementWatch = observability
191
- ? createStatementWatch({ database: config.name, url: config.url, slowQueryMs: observability.slowQueryMs, inFlightQueryMs: observability.inFlightQueryMs })
192
- : undefined
193
193
  const db = splitLargeInserts(drizzle(client, statementWatch ? { statementWatch: statementWatch.watch } : {}))
194
194
  const canceller = createCanceller(config.url)
195
195
 
@@ -0,0 +1,63 @@
1
+ // Size measurements behind `query:large`. Each runs only when its budget is enabled (> 0), after the
2
+ // statement has settled, so a pool with every budget at 0 never calls into this file.
3
+
4
+ /** Bytes assumed for a number, bigint, boolean, Date or other fixed-width scalar parameter. */
5
+ const SCALAR_BYTES = 8
6
+
7
+ /**
8
+ * Estimated bytes of the bind parameters of one statement. Strings count their UTF-8 length
9
+ * (no copy is made), Buffers/typed arrays their byte length, arrays the sum of their elements,
10
+ * numbers/booleans/dates a fixed 8, null/undefined 0. A plain object is a JSON value the driver
11
+ * will serialise: it is measured by `JSON.stringify` length, which serialises it once more than
12
+ * the driver does; ORM json columns arrive already stringified (counted as strings, free), so
13
+ * only a raw object parameter pays that. An unserialisable object counts as one scalar.
14
+ */
15
+ export function parameterBytes(parameters: unknown): number {
16
+ if (!Array.isArray(parameters)) return 0
17
+ let total = 0
18
+ for (const value of parameters) total += valueBytes(value)
19
+ return total
20
+ }
21
+
22
+ function valueBytes(value: unknown): number {
23
+ if (value === null || value === undefined) return 0
24
+ switch (typeof value) {
25
+ case 'string': return Buffer.byteLength(value)
26
+ case 'object': break
27
+ default: return SCALAR_BYTES
28
+ }
29
+ if (value instanceof Date) return SCALAR_BYTES
30
+ if (ArrayBuffer.isView(value)) return value.byteLength
31
+ if (value instanceof ArrayBuffer) return value.byteLength
32
+ if (Array.isArray(value)) {
33
+ let total = 0
34
+ for (const item of value) total += valueBytes(item)
35
+ return total
36
+ }
37
+ try {
38
+ const json = JSON.stringify(value) as string | undefined
39
+ return json === undefined ? 0 : json.length
40
+ } catch {
41
+ return SCALAR_BYTES
42
+ }
43
+ }
44
+
45
+ // Single left-to-right scan: a quoted literal or quoted identifier is consumed whole, so a keyword
46
+ // inside one is never counted; what is left are bare UNION / INTERSECT / EXCEPT words.
47
+ const SET_OPERATION = /'[^']*(?:''[^']*)*'|"[^"]*(?:""[^"]*)*"|\b(?:union|intersect|except)\b/gi
48
+
49
+ /**
50
+ * Arms of the set operations in a statement's SQL text: set-operator keywords + 1, 0 when there
51
+ * are none. This is a text count at every nesting depth (a union inside a subquery or CTE costs the
52
+ * planner the same as one at the top), not a parse. Pipework sends values as bind parameters, so
53
+ * no keyword can appear inside a string value; a literal written with `sql.raw` is skipped by
54
+ * the scan. Comments and dollar-quoted bodies are not understood and can over-count.
55
+ */
56
+ export function setOperationArms(sqlText: string): number {
57
+ let operators = 0
58
+ for (const match of sqlText.matchAll(SET_OPERATION)) {
59
+ const first = match[0][0]
60
+ if (first !== "'" && first !== '"') operators++
61
+ }
62
+ return operators === 0 ? 0 : operators + 1
63
+ }
@@ -1,7 +1,10 @@
1
1
  import postgres from 'postgres'
2
2
  import { getLogger } from '../../core/logging/logger.js'
3
3
  import { currentQueryLabel } from './query-label.js'
4
+ import { DrizzleQueryError } from '../query/errors.js'
5
+ import { parameterBytes, setOperationArms } from './query-size.js'
4
6
  import type { StatementWatch } from '../query/postgres-js/session.js'
7
+ import type { StatementSettledError, StatementSettledObserver } from '../../core/config/statement-observer.js'
5
8
 
6
9
  const LABEL_SQL_CHARS = 120
7
10
  const SAMPLE_TIMEOUT_MS = 2_000
@@ -16,6 +19,24 @@ interface BackendSample {
16
19
 
17
20
  const UNKNOWN_BACKEND: BackendSample = { pid: null, state: null, waitEventType: null, waitEvent: null }
18
21
 
22
+ /** `observability.largeQuery*` budgets; 0 disables a budget and nothing is measured for it. */
23
+ export interface LargeQueryBudgets {
24
+ readonly parameterBytes: number
25
+ readonly sqlChars: number
26
+ readonly arms: number
27
+ readonly rows: number
28
+ }
29
+
30
+ /** The budgets of a resolved `observability` section. */
31
+ export function largeQueryBudgets(o: {
32
+ readonly largeQueryParamBytes: number
33
+ readonly largeQuerySqlChars: number
34
+ readonly largeQueryArms: number
35
+ readonly largeQueryRows: number
36
+ }): LargeQueryBudgets {
37
+ return { parameterBytes: o.largeQueryParamBytes, sqlChars: o.largeQuerySqlChars, arms: o.largeQueryArms, rows: o.largeQueryRows }
38
+ }
39
+
19
40
  /** What a pool's watch needs: where it logs, how slow is slow, and how to reach the server for a wait-event sample. */
20
41
  export interface StatementWatchOptions {
21
42
  readonly database: string
@@ -23,13 +44,44 @@ export interface StatementWatchOptions {
23
44
  readonly slowQueryMs: number
24
45
  /** Undefined: no timer is ever created. */
25
46
  readonly inFlightQueryMs: number | undefined
47
+ /** `observability.onStatementSettled`; undefined: nothing extra is tracked or fired. */
48
+ readonly onSettled?: StatementSettledObserver | undefined
49
+ /** Undefined: no `query:large` check. */
50
+ readonly budgets?: LargeQueryBudgets | undefined
26
51
  }
27
52
 
28
53
  export interface StatementWatchHandle {
29
54
  readonly watch: StatementWatch
55
+ /**
56
+ * Pass to `createDebugFn` for the same pool: the driver's debug hook is the only place that
57
+ * learns a statement's connection. Undefined when no `onSettled` hook is configured.
58
+ */
59
+ readonly noteConnection: ((connectionId: number, parameters: readonly unknown[]) => void) | undefined
30
60
  close(): Promise<void>
31
61
  }
32
62
 
63
+ /** Rows a settled statement returned, or affected for a write: the driver's command-tag count, else the row count. */
64
+ function rowsOf(result: unknown): number {
65
+ if (typeof result !== 'object' || result === null) return 0
66
+ const count = (result as { count?: unknown }).count
67
+ if (typeof count === 'number') return count
68
+ return Array.isArray(result) ? result.length : 0
69
+ }
70
+
71
+ /** Name, message and code only: the driver's error object carries the statement text and bind parameters. */
72
+ function settledError(raw: unknown): StatementSettledError {
73
+ // Drizzle wraps a driver failure in an error whose message is `Failed query: <sql>\nparams: <values>`: report the cause.
74
+ let err = raw
75
+ while (err instanceof DrizzleQueryError && err.cause !== undefined) err = err.cause
76
+ if (typeof err !== 'object' || err === null) return { name: 'Error', message: String(err) }
77
+ const { name, message, code } = err as { name?: unknown; message?: unknown; code?: unknown }
78
+ return {
79
+ name: typeof name === 'string' ? name : 'Error',
80
+ message: typeof message === 'string' ? message : '',
81
+ ...(typeof code === 'string' ? { code } : {}),
82
+ }
83
+ }
84
+
33
85
  function labelFor(sqlText: string): string {
34
86
  return currentQueryLabel() ?? sqlText.replace(/\s+/g, ' ').trim().slice(0, LABEL_SQL_CHARS)
35
87
  }
@@ -41,7 +93,12 @@ function labelFor(sqlText: string): string {
41
93
  * dedicated connection, never the statement's own and never the app pool.
42
94
  */
43
95
  export function createStatementWatch(options: StatementWatchOptions): StatementWatchHandle {
44
- const { database, url, slowQueryMs, inFlightQueryMs } = options
96
+ const { database, url, slowQueryMs, inFlightQueryMs, onSettled, budgets } = options
97
+ const watchesSize = budgets !== undefined && (budgets.parameterBytes > 0 || budgets.sqlChars > 0 || budgets.arms > 0 || budgets.rows > 0)
98
+ const needsOutcome = onSettled !== undefined || (budgets?.rows ?? 0) > 0
99
+ // Keyed on the parameter array the driver builds for a statement: the same array reaches the
100
+ // debug hook (with the connection ordinal) and sits on the pending query the session hands us.
101
+ const connections = onSettled ? new WeakMap<object, number>() : undefined
45
102
  let sampler: postgres.Sql | undefined
46
103
  let sampling = false
47
104
  let closed = false
@@ -72,8 +129,43 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
72
129
  return Promise.race([run, timeout]).catch(() => UNKNOWN_BACKEND).finally(() => { clearTimeout(timer) })
73
130
  }
74
131
 
75
- const watch: StatementWatch = async <T>(sqlText: string, run: () => Promise<T>): Promise<T> => {
132
+ function fireSettled(event: Parameters<StatementSettledObserver>[0]): void {
133
+ if (!onSettled) return
134
+ const failed = (err: unknown): void => {
135
+ getLogger().warn({ err, database }, 'statement:settled-hook-failed')
136
+ }
137
+ try {
138
+ const out = onSettled(event)
139
+ if (out !== undefined && typeof (out as Promise<void>).then === 'function') void Promise.resolve(out).catch(failed)
140
+ } catch (err) {
141
+ failed(err)
142
+ }
143
+ }
144
+
145
+ /** One `query:large` per statement over any enabled budget, carrying every enabled measurement. Never throws. */
146
+ function warnIfLarge(sqlText: string, parameters: unknown, outcome: { result: unknown } | { error: unknown } | undefined, b: LargeQueryBudgets): void {
147
+ try {
148
+ const measured: Record<string, number> = {}
149
+ const over: string[] = []
150
+ const check = (name: string, budget: number, measure: () => number): void => {
151
+ if (budget <= 0) return
152
+ const value = measure()
153
+ measured[name] = value
154
+ if (value > budget) over.push(name)
155
+ }
156
+ check('parameterBytes', b.parameterBytes, () => parameterBytes(parameters))
157
+ check('sqlChars', b.sqlChars, () => sqlText.length)
158
+ check('arms', b.arms, () => setOperationArms(sqlText))
159
+ check('rows', b.rows, () => (outcome !== undefined && 'result' in outcome ? rowsOf(outcome.result) : 0))
160
+ if (over.length > 0) getLogger().warn({ label: labelFor(sqlText), database, over, ...measured }, 'query:large')
161
+ } catch { /* a measurement must never affect the statement */ }
162
+ }
163
+
164
+ const watch: StatementWatch = async <T>(sqlText: string, run: (seen: (pending: object) => void) => Promise<T>): Promise<T> => {
76
165
  const startedMs = Date.now()
166
+ let pendingQuery: object | undefined
167
+ let outcome: { result: unknown } | { error: unknown } | undefined
168
+ const seen = (pending: object): void => { pendingQuery = pending }
77
169
  let interval: NodeJS.Timeout | undefined
78
170
  let settled = false
79
171
  if (inFlightQueryMs !== undefined) {
@@ -93,7 +185,12 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
93
185
  interval.unref()
94
186
  }
95
187
  try {
96
- return await run()
188
+ const result = await run(seen)
189
+ if (needsOutcome) outcome = { result }
190
+ return result
191
+ } catch (err) {
192
+ if (needsOutcome) outcome = { error: err }
193
+ throw err
97
194
  } finally {
98
195
  settled = true
99
196
  if (interval !== undefined) clearInterval(interval)
@@ -101,11 +198,26 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
101
198
  if (slowQueryMs > 0 && durationMs > slowQueryMs) {
102
199
  getLogger().warn({ label: labelFor(sqlText), durationMs, database }, 'query:slow')
103
200
  }
201
+ const parameters = pendingQuery === undefined ? undefined : (pendingQuery as { parameters?: unknown }).parameters
202
+ if (watchesSize) warnIfLarge(sqlText, parameters, outcome, budgets)
203
+ if (onSettled && outcome !== undefined) {
204
+ const connectionId = typeof parameters === 'object' && parameters !== null ? connections?.get(parameters) ?? null : null
205
+ fireSettled({
206
+ label: currentQueryLabel() ?? null,
207
+ durationMs,
208
+ rows: 'result' in outcome ? rowsOf(outcome.result) : 0,
209
+ connectionId,
210
+ ...('error' in outcome ? { error: settledError(outcome.error) } : {}),
211
+ })
212
+ }
104
213
  }
105
214
  }
106
215
 
107
216
  return {
108
217
  watch,
218
+ noteConnection: connections
219
+ ? (connectionId, parameters) => { connections.set(parameters, connectionId) }
220
+ : undefined,
109
221
  async close() {
110
222
  closed = true
111
223
  const client = sampler
@@ -23,7 +23,7 @@ export function warnRawClientIsUnsanitized(database: string): void {
23
23
 
24
24
  getBaseLogger().warn(
25
25
  { database },
26
- '[pipework] raw .client read on a connection with observability.onStatement configured. ' +
26
+ '[pipework] raw .client read on a connection with observability.onStatement or onStatementSettled configured. ' +
27
27
  'Queries issued through .client bypass pipework\'s driver error boundary, so their errors ' +
28
28
  'serialize bind parameters (postgres.js marks query/parameters/args enumerable whenever its ' +
29
29
  'debug hook is installed). Use connection.drizzle — or pipe() inside a managed context — to ' +