turbine-orm 0.81.0 → 0.90.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 (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -58,6 +58,19 @@ export interface CheckoutGuard extends ConnectionGuard {
58
58
  * event (an engine shim) is left alone.
59
59
  */
60
60
  export declare function absorbCheckedOutErrors(pool: unknown): void;
61
+ export interface CheckoutWatch {
62
+ /** Connections currently checked out of the pool, in checkout order. */
63
+ readonly checkedOut: ReadonlySet<object>;
64
+ }
65
+ /**
66
+ * Track which connections of a pool Turbine owns are checked out, from
67
+ * pg-pool's public `acquire` / `release` / `remove` events, and call `onLost`
68
+ * once per connection that fails WHILE checked out (an idle connection's
69
+ * failure is pg-pool's own `'error'` event). Used for the client's
70
+ * `connectionLost` event and for force-closing what `disconnect({ timeoutMs })`
71
+ * is still waiting on. A pool without those events is left alone.
72
+ */
73
+ export declare function watchCheckouts(pool: unknown, onLost?: (err: Error) => void): CheckoutWatch;
61
74
  /**
62
75
  * Guard a connection for as long as the caller holds it. Use {@link guardCheckout}
63
76
  * for a pooled checkout; this form is for a client whose whole lifetime the
@@ -46,6 +46,33 @@ export function absorbCheckedOutErrors(pool) {
46
46
  client.on?.('error', () => { });
47
47
  });
48
48
  }
49
+ /**
50
+ * Track which connections of a pool Turbine owns are checked out, from
51
+ * pg-pool's public `acquire` / `release` / `remove` events, and call `onLost`
52
+ * once per connection that fails WHILE checked out (an idle connection's
53
+ * failure is pg-pool's own `'error'` event). Used for the client's
54
+ * `connectionLost` event and for force-closing what `disconnect({ timeoutMs })`
55
+ * is still waiting on. A pool without those events is left alone.
56
+ */
57
+ export function watchCheckouts(pool, onLost) {
58
+ const checkedOut = new Set();
59
+ const emitter = pool;
60
+ if (typeof emitter.on !== 'function')
61
+ return { checkedOut };
62
+ const reported = new WeakSet();
63
+ emitter.on('acquire', (client) => checkedOut.add(client));
64
+ emitter.on('release', (_err, client) => checkedOut.delete(client));
65
+ emitter.on('remove', (client) => checkedOut.delete(client));
66
+ emitter.on('connect', (client) => {
67
+ client.on?.('error', (err) => {
68
+ if (!checkedOut.has(client) || reported.has(client))
69
+ return;
70
+ reported.add(client);
71
+ onLost?.(err);
72
+ });
73
+ });
74
+ return { checkedOut };
75
+ }
49
76
  /**
50
77
  * Guard a connection for as long as the caller holds it. Use {@link guardCheckout}
51
78
  * for a pooled checkout; this form is for a client whose whole lifetime the
@@ -107,12 +107,33 @@ export interface PoolerDetection {
107
107
  * blocks a legitimate database, so precision wins and the caller is expected to
108
108
  * offer an override.
109
109
  */
110
- export declare function detectPooler(connectionString: string): PoolerDetection;
110
+ export declare function detectPooler(connectionString: string, environment?: PgEnvironmentTarget): PoolerDetection;
111
+ /**
112
+ * The `PGHOST` / `PGPORT` values a connection target falls back to. node-postgres
113
+ * reads a field from the connection string or the config first and the
114
+ * environment only when that leaves it unset, so a string that names no port (or
115
+ * no host) is still connecting to wherever these point.
116
+ */
117
+ export interface PgEnvironmentTarget {
118
+ readonly host?: string | undefined;
119
+ readonly port?: string | undefined;
120
+ }
121
+ /** `target` with the host and port it leaves unset taken from `environment`, the way node-postgres does. */
122
+ export declare function fillTargetFromEnvironment(target: ConnectionTarget, environment: PgEnvironmentTarget): ConnectionTarget;
123
+ /** {@link detectPooler} for a target that is already parsed. */
124
+ export declare function detectPoolerTarget(target: ConnectionTarget): PoolerDetection;
111
125
  export interface PoolerRefusalOptions {
112
126
  /** The command being refused, e.g. `turbine doctor`. */
113
127
  command: string;
114
128
  /** The flag that overrides the refusal, e.g. `--allow-pooler`. */
115
129
  allowFlag: string;
130
+ /**
131
+ * Why THIS command cannot use a pooler, in place of the default paragraph
132
+ * (written for `doctor`: session state, session-scoped views, the read bound).
133
+ * Lines are printed as given, between the endpoint and the advice to use the
134
+ * direct endpoint.
135
+ */
136
+ explanation?: readonly string[];
116
137
  }
117
138
  /**
118
139
  * The lines a command prints when it refuses a pooler endpoint.
@@ -158,6 +179,30 @@ export interface ConnectionOptionsConfig {
158
179
  * a value that can carry a space can carry a second `-c`.
159
180
  */
160
181
  export declare function withStatementTimeoutOption(config: ConnectionOptionsConfig, statementTimeoutMs: number): ConnectionOptionsConfig;
182
+ /**
183
+ * The `-c statement_timeout=<ms>` startup setting, or `null` when the value is
184
+ * not a non-negative safe integer (see the injection note on
185
+ * {@link withStatementTimeoutOption}). No unit suffix: the GUC's default unit
186
+ * is milliseconds.
187
+ */
188
+ export declare function statementTimeoutSetting(statementTimeoutMs: number): string | null;
189
+ /**
190
+ * `config` with one `-c name=value` startup setting appended to its libpq
191
+ * `options`, in whichever place pg will read them from: an `options` query
192
+ * parameter already on the connection string wins over the `options` field,
193
+ * and the field falls back to `PGOPTIONS` only while it is unset, so both are
194
+ * read first and appended to rather than replaced.
195
+ *
196
+ * `setting` is emitted verbatim. The caller must have built it from a closed
197
+ * set or a narrowed number: the `options` string is split on whitespace, so a
198
+ * caller-controlled value could carry a second `-c`.
199
+ */
200
+ export declare function withStartupSetting<T extends {
201
+ connectionString?: string;
202
+ options?: string;
203
+ }>(config: T, setting: string): T & {
204
+ options?: string;
205
+ };
161
206
  /**
162
207
  * `connectionString` with `setting` appended to an existing `options` query
163
208
  * parameter, or `null` when it carries none (in which case the caller uses the
@@ -170,22 +215,12 @@ export declare function withStatementTimeoutOption(config: ConnectionOptionsConf
170
215
  * password is not parseable by pg either, so there is no shape this handles
171
216
  * differently from the driver.
172
217
  *
173
- * Shared with `client.ts`, which needs the identical merge for
174
- * `plan_cache_mode`. It was duplicated there until 2026-08-16, and the reason
175
- * is worth keeping because it was wrong in an instructive way: importing it
176
- * from `client.ts` adds an import edge from a module ~100 test processes load,
177
- * and this file's merged coverage read 40% while measuring 100% in isolation,
178
- * which moved the aggregate gate 75.48% -> 75.03% against a 75% floor. The
179
- * conclusion drawn was that the refactor cost the gate's headroom.
180
- *
181
- * The 40% was a c8 merge defect, not an import cost: c8's V8-level merge is not
182
- * monotonic, and once the report merges at the istanbul level instead this file
183
- * reads 100% with the edge in place (see `//merge-bug` in .c8rc.json). So the
184
- * duplication was paying for an artifact, and the two copies are now one.
218
+ * Used by {@link withStartupSetting} (the client's pool configuration) and
219
+ * {@link withSearchPathOption}.
185
220
  *
186
- * Keep this module import-free regardless. That part was never about coverage:
187
- * `query/` and `cli/` both reach it, and an edge back to `client.ts` would be
188
- * the cycle `scripts/check-import-cycles.mjs` exists to refuse.
221
+ * Keep this module import-free: `query/` and `cli/` both reach it, and an
222
+ * edge back to `client.ts` would be the cycle `scripts/check-import-cycles.mjs`
223
+ * exists to refuse.
189
224
  */
190
225
  export declare function mergeConnectionStringOptions(connectionString: string, setting: string): string | null;
191
226
  /**
@@ -106,8 +106,22 @@ function noPooler(target) {
106
106
  * blocks a legitimate database, so precision wins and the caller is expected to
107
107
  * offer an override.
108
108
  */
109
- export function detectPooler(connectionString) {
110
- const target = parseConnectionTarget(connectionString);
109
+ export function detectPooler(connectionString, environment) {
110
+ const parsed = parseConnectionTarget(connectionString);
111
+ return detectPoolerTarget(environment === undefined ? parsed : fillTargetFromEnvironment(parsed, environment));
112
+ }
113
+ /** `target` with the host and port it leaves unset taken from `environment`, the way node-postgres does. */
114
+ export function fillTargetFromEnvironment(target, environment) {
115
+ const host = target.host ?? (environment.host ? environment.host.trim().toLowerCase() || null : null);
116
+ let port = target.port;
117
+ if (port === null && environment.port) {
118
+ const parsed = Number.parseInt(environment.port, 10);
119
+ port = Number.isInteger(parsed) ? parsed : null;
120
+ }
121
+ return { host, port };
122
+ }
123
+ /** {@link detectPooler} for a target that is already parsed. */
124
+ export function detectPoolerTarget(target) {
111
125
  if (target.host !== null) {
112
126
  for (const label of target.host.split('.')) {
113
127
  for (const token of label.split(/[-_]/)) {
@@ -160,6 +174,21 @@ function deriveDirectHost(host, token) {
160
174
  });
161
175
  return changed ? rewritten.join('.') : null;
162
176
  }
177
+ /** Why `turbine doctor` refuses a pooler; the paragraph {@link poolerRefusalMessage} prints by default. */
178
+ const DEFAULT_POOLER_EXPLANATION = [
179
+ 'A transaction pooler (PgBouncer, a Neon "-pooler" endpoint, Supabase\'s pooler) does not',
180
+ 'give a client its own server backend. It multiplexes many clients onto a few shared',
181
+ 'backends and reuses one as soon as a transaction ends, so:',
182
+ '',
183
+ ' 1. Session state is not private. Anything this command sets on the session can be left',
184
+ ' behind for another client, and whatever an earlier client left behind can be in',
185
+ ' force for this one. That is how a "read-only guardrail" becomes an outage.',
186
+ ' 2. Session-scoped views describe the wrong session. pg_prepared_statements, which this',
187
+ ' report tells you to read to confirm a cached plan, belongs to whichever backend',
188
+ ' answered, not to your application.',
189
+ ' 3. The read bounds may not apply. This command asks for its statement_timeout as a',
190
+ ' connection parameter, and a pooler is free to drop or reject one.',
191
+ ];
163
192
  /**
164
193
  * The lines a command prints when it refuses a pooler endpoint.
165
194
  *
@@ -177,18 +206,7 @@ export function poolerRefusalMessage(detection, options) {
177
206
  ` Endpoint: ${where}${detection.port === null ? '' : `:${detection.port}`}`,
178
207
  ` Detected: ${what}`,
179
208
  '',
180
- 'A transaction pooler (PgBouncer, a Neon "-pooler" endpoint, Supabase\'s pooler) does not',
181
- 'give a client its own server backend. It multiplexes many clients onto a few shared',
182
- 'backends and reuses one as soon as a transaction ends, so:',
183
- '',
184
- ' 1. Session state is not private. Anything this command sets on the session can be left',
185
- ' behind for another client, and whatever an earlier client left behind can be in',
186
- ' force for this one. That is how a "read-only guardrail" becomes an outage.',
187
- ' 2. Session-scoped views describe the wrong session. pg_prepared_statements, which this',
188
- ' report tells you to read to confirm a cached plan, belongs to whichever backend',
189
- ' answered, not to your application.',
190
- ' 3. The read bounds may not apply. This command asks for its statement_timeout as a',
191
- ' connection parameter, and a pooler is free to drop or reject one.',
209
+ ...(options.explanation ?? DEFAULT_POOLER_EXPLANATION),
192
210
  '',
193
211
  'Use the DIRECT (non-pooled) endpoint for the same database instead.',
194
212
  ];
@@ -233,12 +251,33 @@ export function poolerRefusalMessage(detection, options) {
233
251
  * a value that can carry a space can carry a second `-c`.
234
252
  */
235
253
  export function withStatementTimeoutOption(config, statementTimeoutMs) {
254
+ const setting = statementTimeoutSetting(statementTimeoutMs);
255
+ return setting === null ? config : withStartupSetting(config, setting);
256
+ }
257
+ /**
258
+ * The `-c statement_timeout=<ms>` startup setting, or `null` when the value is
259
+ * not a non-negative safe integer (see the injection note on
260
+ * {@link withStatementTimeoutOption}). No unit suffix: the GUC's default unit
261
+ * is milliseconds.
262
+ */
263
+ export function statementTimeoutSetting(statementTimeoutMs) {
236
264
  if (!Number.isSafeInteger(statementTimeoutMs) || statementTimeoutMs < 0)
237
- return config;
238
- // No unit suffix: statement_timeout's default unit IS milliseconds, which is
239
- // what `SET statement_timeout = <n>` meant before this replaced it.
240
- const setting = `-c statement_timeout=${statementTimeoutMs}`;
241
- const merged = mergeConnectionStringOptions(config.connectionString, setting);
265
+ return null;
266
+ return `-c statement_timeout=${statementTimeoutMs}`;
267
+ }
268
+ /**
269
+ * `config` with one `-c name=value` startup setting appended to its libpq
270
+ * `options`, in whichever place pg will read them from: an `options` query
271
+ * parameter already on the connection string wins over the `options` field,
272
+ * and the field falls back to `PGOPTIONS` only while it is unset, so both are
273
+ * read first and appended to rather than replaced.
274
+ *
275
+ * `setting` is emitted verbatim. The caller must have built it from a closed
276
+ * set or a narrowed number: the `options` string is split on whitespace, so a
277
+ * caller-controlled value could carry a second `-c`.
278
+ */
279
+ export function withStartupSetting(config, setting) {
280
+ const merged = config.connectionString ? mergeConnectionStringOptions(config.connectionString, setting) : null;
242
281
  if (merged !== null)
243
282
  return { ...config, connectionString: merged };
244
283
  const existing = config.options || (typeof process !== 'undefined' ? process.env?.PGOPTIONS : undefined);
@@ -256,22 +295,12 @@ export function withStatementTimeoutOption(config, statementTimeoutMs) {
256
295
  * password is not parseable by pg either, so there is no shape this handles
257
296
  * differently from the driver.
258
297
  *
259
- * Shared with `client.ts`, which needs the identical merge for
260
- * `plan_cache_mode`. It was duplicated there until 2026-08-16, and the reason
261
- * is worth keeping because it was wrong in an instructive way: importing it
262
- * from `client.ts` adds an import edge from a module ~100 test processes load,
263
- * and this file's merged coverage read 40% while measuring 100% in isolation,
264
- * which moved the aggregate gate 75.48% -> 75.03% against a 75% floor. The
265
- * conclusion drawn was that the refactor cost the gate's headroom.
266
- *
267
- * The 40% was a c8 merge defect, not an import cost: c8's V8-level merge is not
268
- * monotonic, and once the report merges at the istanbul level instead this file
269
- * reads 100% with the edge in place (see `//merge-bug` in .c8rc.json). So the
270
- * duplication was paying for an artifact, and the two copies are now one.
298
+ * Used by {@link withStartupSetting} (the client's pool configuration) and
299
+ * {@link withSearchPathOption}.
271
300
  *
272
- * Keep this module import-free regardless. That part was never about coverage:
273
- * `query/` and `cli/` both reach it, and an edge back to `client.ts` would be
274
- * the cycle `scripts/check-import-cycles.mjs` exists to refuse.
301
+ * Keep this module import-free: `query/` and `cli/` both reach it, and an
302
+ * edge back to `client.ts` would be the cycle `scripts/check-import-cycles.mjs`
303
+ * exists to refuse.
275
304
  */
276
305
  export function mergeConnectionStringOptions(connectionString, setting) {
277
306
  const q = connectionString.indexOf('?');
@@ -0,0 +1,81 @@
1
+ /**
2
+ * turbine-orm, the `diagnostics` switch.
3
+ *
4
+ * One client option decides what Turbine checks and says about itself at run
5
+ * time: per-query advice (unknown options, unordered pages, deep `with`,
6
+ * strategy notes, missing FK indexes, redundant sort terms) and the SQL-cache
7
+ * self-check. It is resolved ONCE, when a client is constructed, and travels to
8
+ * every query interface the client builds, so no hot path reads `process.env`.
9
+ *
10
+ * This module is the only place in the library (outside `cli/`) that reads
11
+ * `NODE_ENV` or the two `TURBINE_*` cache-check variables; a source scan
12
+ * (`fleet-diagnostics-env-scan.test.ts`) fails the build on a second reader.
13
+ *
14
+ * Zero imports besides errors.ts, so client.ts, query/ and the engines can all
15
+ * depend on it without a new cycle.
16
+ */
17
+ /** `'development'`: advice on, cache self-check on every hit. `'production'`: both off. */
18
+ export type DiagnosticsMode = 'development' | 'production';
19
+ /** The SQL-cache self-check: rebuild on every cache hit, a sampled fraction of hits, or never. */
20
+ export type CacheCheckMode = 'every-hit' | 'off' | {
21
+ readonly sample: number;
22
+ };
23
+ /** Per-part overrides for {@link DiagnosticsConfig}. Unset parts follow the mode. */
24
+ export interface DiagnosticsOptions {
25
+ /**
26
+ * Per-query advice: unknown query options, unordered pages, deep `with`
27
+ * trees, relation-strategy notes, missing FK indexes, redundant sort terms.
28
+ * Default: on in development, off in production.
29
+ */
30
+ advice?: boolean | undefined;
31
+ /**
32
+ * SQL-cache self-check. `'every-hit'` rebuilds the SQL on each cache hit
33
+ * and compares; `{ sample }` compares that fraction of hits, in `(0, 1]`;
34
+ * `'off'` never compares. Default: `'every-hit'` in development, `'off'` in
35
+ * production. A mismatch means Turbine has a bug, and throws.
36
+ */
37
+ cacheCheck?: CacheCheckMode | undefined;
38
+ }
39
+ /**
40
+ * The `diagnostics` client option: a mode, or per-part overrides on top of
41
+ * one. When no mode is given it comes from `NODE_ENV`, read once when the
42
+ * client is constructed: `'production'` when `NODE_ENV` is `'production'` (or
43
+ * when there is no `process` at all, as on an edge runtime), else
44
+ * `'development'`.
45
+ */
46
+ export type DiagnosticsConfig = DiagnosticsMode | (DiagnosticsOptions & {
47
+ mode?: DiagnosticsMode | undefined;
48
+ });
49
+ /** What a client actually runs with. Frozen; built only by {@link resolveDiagnostics}. */
50
+ export interface ResolvedDiagnostics {
51
+ readonly mode: DiagnosticsMode;
52
+ /** Whether per-query advice is emitted. */
53
+ readonly advice: boolean;
54
+ readonly cacheCheck: CacheCheckMode;
55
+ }
56
+ /**
57
+ * The environment variables this module reads. `null` in their place means
58
+ * there is no `process` to read (an edge runtime), which resolves to
59
+ * production.
60
+ */
61
+ export interface DiagnosticsEnv {
62
+ NODE_ENV?: string | undefined;
63
+ TURBINE_DISABLE_CACHE_CHECK?: string | undefined;
64
+ TURBINE_CACHE_CHECK_SAMPLE?: string | undefined;
65
+ }
66
+ /**
67
+ * Resolve the `diagnostics` option, once, at client construction.
68
+ *
69
+ * Precedence for each part: the explicit option, then (for `cacheCheck` only)
70
+ * the legacy `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE`
71
+ * variables, then the mode's default. The mode itself is the option's, else
72
+ * `NODE_ENV`'s.
73
+ *
74
+ * Throws `ValidationError` (E003) for an unknown mode, an unknown key, a
75
+ * non-boolean `advice`, or a `sample` outside `(0, 1]`. A value this function
76
+ * returned is handed back by reference.
77
+ *
78
+ * @param env - The environment to read; defaults to `process.env`. Tests pass
79
+ * one explicitly; `null` stands for a runtime with no `process`.
80
+ */
81
+ export declare function resolveDiagnostics(config?: unknown, env?: DiagnosticsEnv | null): ResolvedDiagnostics;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * turbine-orm, the `diagnostics` switch.
3
+ *
4
+ * One client option decides what Turbine checks and says about itself at run
5
+ * time: per-query advice (unknown options, unordered pages, deep `with`,
6
+ * strategy notes, missing FK indexes, redundant sort terms) and the SQL-cache
7
+ * self-check. It is resolved ONCE, when a client is constructed, and travels to
8
+ * every query interface the client builds, so no hot path reads `process.env`.
9
+ *
10
+ * This module is the only place in the library (outside `cli/`) that reads
11
+ * `NODE_ENV` or the two `TURBINE_*` cache-check variables; a source scan
12
+ * (`fleet-diagnostics-env-scan.test.ts`) fails the build on a second reader.
13
+ *
14
+ * Zero imports besides errors.ts, so client.ts, query/ and the engines can all
15
+ * depend on it without a new cycle.
16
+ */
17
+ import { ValidationError } from './errors.js';
18
+ const MODES = ['development', 'production'];
19
+ const OPTION_KEYS = new Set(['mode', 'advice', 'cacheCheck']);
20
+ /**
21
+ * Marks objects {@link resolveDiagnostics} built, so resolving a resolved value
22
+ * hands it back by reference. A `TransactionClient` builds a query interface
23
+ * per table per transaction from the client's options; re-resolving there would
24
+ * put the environment reads back on a per-transaction path.
25
+ */
26
+ const resolvedSet = new WeakSet();
27
+ /**
28
+ * Read the variables once. Every read is guarded: no `process` (edge runtimes)
29
+ * and no `process.env` (some sandboxes) both mean "no environment".
30
+ */
31
+ function readEnv() {
32
+ if (typeof process === 'undefined' || process.env === undefined)
33
+ return null;
34
+ const env = process.env;
35
+ return {
36
+ NODE_ENV: env.NODE_ENV,
37
+ TURBINE_DISABLE_CACHE_CHECK: env.TURBINE_DISABLE_CACHE_CHECK,
38
+ TURBINE_CACHE_CHECK_SAMPLE: env.TURBINE_CACHE_CHECK_SAMPLE,
39
+ };
40
+ }
41
+ function invalid(detail) {
42
+ return new ValidationError(`Invalid diagnostics option: ${detail}. Expected 'development', 'production', or ` +
43
+ "{ mode?, advice?: boolean, cacheCheck?: 'every-hit' | 'off' | { sample: number in (0, 1] } }.");
44
+ }
45
+ function checkSample(value) {
46
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {
47
+ throw invalid(`cacheCheck.sample must be a number in (0, 1], got ${describe(value)}`);
48
+ }
49
+ return value;
50
+ }
51
+ function describe(value) {
52
+ if (typeof value === 'string')
53
+ return JSON.stringify(value);
54
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null || value === undefined) {
55
+ return String(value);
56
+ }
57
+ return Array.isArray(value) ? 'an array' : `a ${typeof value}`;
58
+ }
59
+ function resolveCacheCheck(value) {
60
+ if (value === 'every-hit' || value === 'off')
61
+ return value;
62
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
63
+ const keys = Object.keys(value);
64
+ if (keys.length !== 1 || keys[0] !== 'sample') {
65
+ throw invalid(`cacheCheck object takes exactly one key, sample; got { ${keys.join(', ')} }`);
66
+ }
67
+ return Object.freeze({ sample: checkSample(value.sample) });
68
+ }
69
+ throw invalid(`cacheCheck ${describe(value)}`);
70
+ }
71
+ /**
72
+ * The legacy cache-check variables, under an explicit `cacheCheck`.
73
+ * `TURBINE_DISABLE_CACHE_CHECK=1` turns the check off in any mode. In
74
+ * production mode, `TURBINE_CACHE_CHECK_SAMPLE` sets a sample rate: a rate of
75
+ * 1 or more checks every hit, and 0, a negative or an unparseable value leaves
76
+ * the check off, as these variables always behaved. Development mode already
77
+ * checks every hit, so the sample variable is not read there.
78
+ */
79
+ function legacyCacheCheck(mode, env) {
80
+ if (env === null)
81
+ return undefined;
82
+ if (env.TURBINE_DISABLE_CACHE_CHECK === '1')
83
+ return 'off';
84
+ if (mode !== 'production' || env.TURBINE_CACHE_CHECK_SAMPLE === undefined)
85
+ return undefined;
86
+ const rate = Number.parseFloat(env.TURBINE_CACHE_CHECK_SAMPLE);
87
+ if (!Number.isFinite(rate) || rate <= 0)
88
+ return 'off';
89
+ return Object.freeze({ sample: Math.min(rate, 1) });
90
+ }
91
+ /**
92
+ * Resolve the `diagnostics` option, once, at client construction.
93
+ *
94
+ * Precedence for each part: the explicit option, then (for `cacheCheck` only)
95
+ * the legacy `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE`
96
+ * variables, then the mode's default. The mode itself is the option's, else
97
+ * `NODE_ENV`'s.
98
+ *
99
+ * Throws `ValidationError` (E003) for an unknown mode, an unknown key, a
100
+ * non-boolean `advice`, or a `sample` outside `(0, 1]`. A value this function
101
+ * returned is handed back by reference.
102
+ *
103
+ * @param env - The environment to read; defaults to `process.env`. Tests pass
104
+ * one explicitly; `null` stands for a runtime with no `process`.
105
+ */
106
+ export function resolveDiagnostics(config, env = readEnv()) {
107
+ if (typeof config === 'object' && config !== null && resolvedSet.has(config)) {
108
+ return config;
109
+ }
110
+ let options = {};
111
+ if (typeof config === 'string') {
112
+ if (!MODES.includes(config))
113
+ throw invalid(`unknown mode ${describe(config)}`);
114
+ options = { mode: config };
115
+ }
116
+ else if (typeof config === 'object' && config !== null && !Array.isArray(config)) {
117
+ for (const key of Object.keys(config)) {
118
+ if (!OPTION_KEYS.has(key))
119
+ throw invalid(`unknown key "${key}"`);
120
+ }
121
+ options = config;
122
+ if (options.mode !== undefined && !MODES.includes(options.mode)) {
123
+ throw invalid(`unknown mode ${describe(options.mode)}`);
124
+ }
125
+ if (options.advice !== undefined && typeof options.advice !== 'boolean') {
126
+ throw invalid(`advice must be a boolean, got ${describe(options.advice)}`);
127
+ }
128
+ }
129
+ else if (config !== undefined && config !== null) {
130
+ throw invalid(describe(config));
131
+ }
132
+ const mode = options.mode ??
133
+ (env === null || env.NODE_ENV === 'production' ? 'production' : 'development');
134
+ const cacheCheck = options.cacheCheck !== undefined
135
+ ? resolveCacheCheck(options.cacheCheck)
136
+ : (legacyCacheCheck(mode, env) ?? (mode === 'production' ? 'off' : 'every-hit'));
137
+ const resolved = Object.freeze({
138
+ mode,
139
+ advice: options.advice ?? mode === 'development',
140
+ cacheCheck,
141
+ });
142
+ resolvedSet.add(resolved);
143
+ return resolved;
144
+ }