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
@@ -0,0 +1,131 @@
1
+ /**
2
+ * turbine-orm: the `lock` read option (`SELECT ... FOR UPDATE OF "t"`).
3
+ *
4
+ * {@link resolveRowLock} is the one place a `lock` value is validated and
5
+ * rendered. The SQL build and the cache key both read its result, so the two
6
+ * cannot disagree about which lock a statement carries.
7
+ *
8
+ * The clause always names the queried table (`OF "t"`). That keeps the lock on
9
+ * the returned rows of that table and nothing else, on every relation
10
+ * strategy: correlated `with` subqueries are never locked anyway, and without
11
+ * `OF` PostgreSQL refuses the `flatten` and lateral-pick plans (their outer
12
+ * joins would put a table on the nullable side of the lock).
13
+ */
14
+ import { UnsupportedFeatureError, ValidationError } from '../errors.js';
15
+ import { tableAccessorName } from '../global-filters-config.js';
16
+ import { suggestKey } from './utils.js';
17
+ const MODES = ['update', 'noKeyUpdate', 'share', 'keyShare'];
18
+ const MODE_SQL = {
19
+ update: 'UPDATE',
20
+ noKeyUpdate: 'NO KEY UPDATE',
21
+ share: 'SHARE',
22
+ keyShare: 'KEY SHARE',
23
+ };
24
+ /** One letter per mode for the SQL-template cache key. */
25
+ const MODE_FP = { update: 'u', noKeyUpdate: 'n', share: 's', keyShare: 'k' };
26
+ const LOCK_KEYS = ['mode', 'noWait', 'skipLocked'];
27
+ /** Why an engine refuses a lock mode, keyed by dialect name. */
28
+ const ENGINE_HINT = {
29
+ sqlite: 'SQLite has no row locks: a write transaction locks the whole database. Remove `lock`; inside ' +
30
+ '$transaction on SQLite, writes are already serialized.',
31
+ mysql: "MySQL supports lock: 'update' and lock: 'share' only. Use 'update' (stronger) or 'share' (weaker).",
32
+ mssql: "Row locks are not available on SQL Server in this release. Use $transaction({ isolationLevel: 'RepeatableRead' }) " +
33
+ 'or a raw query with table hints.',
34
+ powdb: 'PowDB has no row locks: a transaction holds the single writer gate.',
35
+ };
36
+ /**
37
+ * Validate `lock` for a read on `table` and render its clause.
38
+ *
39
+ * `undefined` means no lock. Anything that is not a mode string or a
40
+ * `{ mode, noWait?, skipLocked? }` object is `ValidationError` (E003), and so
41
+ * is asking for both wait policies at once. A mode the dialect does not list in
42
+ * {@link Dialect.rowLockModes} is `UnsupportedFeatureError` (E017).
43
+ */
44
+ export function resolveRowLock(lock, dialect, table) {
45
+ if (lock === undefined)
46
+ return undefined;
47
+ let mode;
48
+ let noWait;
49
+ let skipLocked;
50
+ if (typeof lock === 'string') {
51
+ mode = lock;
52
+ }
53
+ else if (typeof lock === 'object' && lock !== null && !Array.isArray(lock)) {
54
+ for (const key of Object.keys(lock)) {
55
+ if (!LOCK_KEYS.includes(key)) {
56
+ const hint = suggestKey(key, LOCK_KEYS);
57
+ throw new ValidationError(`lock on "${table}": unknown key "${key}".${hint ? ` Did you mean "${hint}"?` : ''} ` +
58
+ 'A lock is a mode, or { mode, noWait: true } or { mode, skipLocked: true }.');
59
+ }
60
+ }
61
+ ({ mode, noWait, skipLocked } = lock);
62
+ assertFlag(noWait, 'noWait', table);
63
+ assertFlag(skipLocked, 'skipLocked', table);
64
+ if (noWait === true && skipLocked === true) {
65
+ throw new ValidationError(`lock on "${table}": noWait and skipLocked cannot be combined. noWait fails at once when a row is ` +
66
+ 'locked by another transaction; skipLocked leaves that row out of the result. Pick one.');
67
+ }
68
+ }
69
+ else {
70
+ throw new ValidationError(`lock on "${table}" must be a mode ('update', 'noKeyUpdate', 'share' or 'keyShare') or ` +
71
+ `{ mode, noWait?, skipLocked? }, received ${describe(lock)}. Leave it out for an unlocked read.`);
72
+ }
73
+ if (typeof mode !== 'string' || !MODES.includes(mode)) {
74
+ const hint = typeof mode === 'string' ? suggestKey(mode, MODES) : null;
75
+ throw new ValidationError(`lock on "${table}": unknown mode ${describe(mode)}.${hint ? ` Did you mean '${hint}'?` : ''} ` +
76
+ "Use 'update', 'noKeyUpdate', 'share' or 'keyShare'.");
77
+ }
78
+ const m = mode;
79
+ if (!(dialect.rowLockModes ?? []).includes(m)) {
80
+ throw new UnsupportedFeatureError(`lock: '${m}'`, dialect.name, ENGINE_HINT[dialect.name] ?? 'This engine has no row-locking clause Turbine can emit.');
81
+ }
82
+ const wait = noWait === true ? ' NOWAIT' : skipLocked === true ? ' SKIP LOCKED' : '';
83
+ return {
84
+ sql: ` FOR ${MODE_SQL[m]} OF ${dialect.quoteIdentifier(table)}${wait}`,
85
+ fp: `${MODE_FP[m]}${noWait === true ? '.nw' : skipLocked === true ? '.sl' : ''}`,
86
+ };
87
+ }
88
+ /**
89
+ * The refusal for a locking read outside a transaction. In autocommit the lock
90
+ * is released when the statement ends, before the caller sees the rows, so a
91
+ * `skipLocked` queue claim outside a transaction claims nothing, silently.
92
+ */
93
+ export function lockOutsideTransactionMessage(table, operation) {
94
+ return (`lock on "${table}".${operation}() needs a transaction: outside one the rows are unlocked again before ` +
95
+ `the call returns. Use db.$transaction(async (tx) => tx.${tableAccessorName(table)}.${operation}({ ..., lock })).`);
96
+ }
97
+ /** The refusal for `lock` on a streaming read. */
98
+ export function lockOnStreamMessage(table, operation) {
99
+ return (`lock is not supported on "${table}".${operation}(): a stream is not a locking read. Page with ` +
100
+ 'findMany({ lock, limit }) inside the transaction instead.');
101
+ }
102
+ /** The refusal for `lock` together with `distinct`. */
103
+ export function lockWithDistinctMessage(table) {
104
+ return (`lock on "${table}" cannot be combined with distinct: PostgreSQL does not lock rows of a DISTINCT query. ` +
105
+ 'Lock first (findMany with lock), then de-duplicate.');
106
+ }
107
+ /**
108
+ * Refuse a locking read in a pipeline that runs each statement in autocommit
109
+ * (`pipeline(..., { transactional: false })`). A transactional pipeline, and
110
+ * `$transaction([...])`, keep the lock for the rest of the batch.
111
+ */
112
+ export function assertNoLockInAutocommitPipeline(queries) {
113
+ const at = queries.findIndex((q) => q.rowLock === true);
114
+ if (at === -1)
115
+ return;
116
+ throw new ValidationError(`pipeline query ${at} (${queries[at]?.tag ?? 'a read'}) is a locking read, and { transactional: false } runs ` +
117
+ 'it in autocommit, which releases the lock before the call returns. Drop transactional: false, or run the ' +
118
+ 'batch with db.$transaction([...]).');
119
+ }
120
+ function assertFlag(value, name, table) {
121
+ if (value === undefined || typeof value === 'boolean')
122
+ return;
123
+ throw new ValidationError(`lock on "${table}": ${name} must be true or false, received ${describe(value)}.`);
124
+ }
125
+ function describe(value) {
126
+ if (typeof value === 'string')
127
+ return `'${value}'`;
128
+ if (value === null || typeof value !== 'object')
129
+ return String(value);
130
+ return Array.isArray(value) ? 'an array' : 'an object';
131
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * turbine-orm: `sqlComment`, sqlcommenter-style key/values on each statement
3
+ * Turbine builds, so `pg_stat_activity`, the server log, `auto_explain` and
4
+ * database APM tools can attribute a statement to a service, a model, a feature
5
+ * or a trace.
6
+ *
7
+ * SELECT ... LIMIT $2 /*action='findMany',application='billing',model='users'*\/
8
+ *
9
+ * FORMAT (the sqlcommenter spec): keys sorted, each value URL-encoded with
10
+ * `encodeURIComponent` and then `'` escaped as `\'`, each pair `key='value'`,
11
+ * pairs joined by commas, the whole comment appended after the statement with
12
+ * one space before it.
13
+ *
14
+ * WHY A VALUE CANNOT ESCAPE THE COMMENT. `encodeURIComponent` encodes `/`, so a
15
+ * value can hold neither `*\/` (the comment's end) nor `/*` (PostgreSQL and SQL
16
+ * Server nest block comments, so an opener would swallow the rest of the
17
+ * text). It also encodes every character a driver-side placeholder rewrite
18
+ * looks for (`$`, `?`, `:`, `@`) and `;`. Keys must match
19
+ * {@link SQL_COMMENT_KEY_PATTERN}, whose first-character rule also rules out
20
+ * MySQL's executable `/*!` and optimizer-hint `/*+` comments.
21
+ *
22
+ * WHERE IT IS APPENDED: at the execute seam, AFTER the SQL-template cache. The
23
+ * cache stores commentless text, so its keys, its hit rate and the lockstep
24
+ * checks are untouched by any comment, and a per-request value can never grow
25
+ * it. `$on('query')` events keep `sql` commentless and carry the comment in
26
+ * their own field.
27
+ *
28
+ * PREPARED-STATEMENT NAMES. node-postgres refuses to reuse a statement name for
29
+ * different text on one connection, and every name a connection prepares stays
30
+ * on the server until the connection closes. So:
31
+ *
32
+ * - no comment: the template's own name, as before;
33
+ * - static, model, action and tag only (constant per call site and label):
34
+ * `c_<16 hex>`, a hash of the template name and the comment, claimed from
35
+ * the same process-wide budget as every other name
36
+ * (`PREPARED_STATEMENT_BUDGET`), so the server-side cost stays bounded
37
+ * however many labels there are;
38
+ * - any dynamic value (a trace id, unique per request): unnamed, parsed on
39
+ * every execution and never kept. A per-request value under a name would
40
+ * prepare a new statement on every pooled connection for every request.
41
+ *
42
+ * Leaf apart from errors.ts, logger.ts, query-events.ts (the `$tag` scope) and
43
+ * utils.ts (the name budget); nothing here is Node-only.
44
+ */
45
+ import { type TurbineLogger } from '../logger.js';
46
+ import type { DeferredQuery } from './deferred.js';
47
+ /** `TurbineConfig.sqlComment`. */
48
+ export interface SqlCommentOptions {
49
+ /** Constant key/values on every statement this client builds: `{ application: 'billing' }`. */
50
+ static?: Readonly<Record<string, string>> | undefined;
51
+ /** Add `model` (the table) and `action` (`'findMany'`, `'update'`, ...) of the operation. */
52
+ model?: boolean | undefined;
53
+ /** Add the label of the innermost `db.$tag(label, fn)` scope as `tag`. */
54
+ tag?: boolean | undefined;
55
+ /**
56
+ * Per-statement values, computed in the caller's async context (for example
57
+ * a W3C `traceparent`). A key whose value is `undefined` is left out. A
58
+ * statement carrying any dynamic value is sent unnamed: parsed on every
59
+ * execution, never kept as a prepared statement. A key or value this
60
+ * function returns that cannot be used is dropped with a warning
61
+ * (`sql-comment-dropped`), and a throw drops all of them: a tracing hook
62
+ * never fails a query.
63
+ */
64
+ dynamic?: (() => Readonly<Record<string, string | undefined>> | undefined | null) | undefined;
65
+ }
66
+ /** What a comment key must look like. Lower case, so it can never open `/*!` or `/*+`. */
67
+ export declare const SQL_COMMENT_KEY_PATTERN: RegExp;
68
+ /** Longest value accepted, in characters before encoding. */
69
+ export declare const MAX_SQL_COMMENT_VALUE_LENGTH = 256;
70
+ /** One statement's comment. */
71
+ export interface StatementComment {
72
+ /** The comment exactly as appended, without the space before it: `/*key='value',...*\/`. */
73
+ readonly comment: string;
74
+ /** It carries a dynamic value, so the statement goes out unnamed. */
75
+ readonly dynamic: boolean;
76
+ }
77
+ /**
78
+ * Builds the comment for each statement and the name it is sent under. One per
79
+ * client, shared by every table accessor and transaction of that client.
80
+ */
81
+ export declare class SqlCommenter {
82
+ private readonly logger;
83
+ /** Static pairs, encoded and sorted. */
84
+ private readonly staticPairs;
85
+ /** Keys a dynamic value may not take: the static ones and the ones model / tag fill in. */
86
+ private readonly reservedKeys;
87
+ private readonly withModel;
88
+ private readonly withTag;
89
+ private readonly dynamic;
90
+ /** The comment when it depends on nothing per statement (no model, no tag). */
91
+ private readonly constant;
92
+ /** Rendered model / action / tag comments. Bounded: tags may come from data. */
93
+ private readonly rendered;
94
+ /** Claimed `c_` names by template name and comment. Holds only claimed names, so the budget bounds it. */
95
+ private readonly names;
96
+ /**
97
+ * Every statement gets a comment (a static key, or the model and action),
98
+ * so a template's own name is never sent and need not be claimed from the
99
+ * budget; the commented name is claimed instead.
100
+ */
101
+ readonly alwaysComments: boolean;
102
+ constructor(staticPairs: ReadonlyArray<readonly [string, string]>, withModel: boolean, withTag: boolean, dynamic: SqlCommentOptions['dynamic'], logger: TurbineLogger);
103
+ /**
104
+ * The comment for one statement of operation `action` on `model`, or
105
+ * `undefined` when it carries none. Reads the `$tag` scope and calls
106
+ * `dynamic` in the caller's async context, so call it where the statement
107
+ * is sent.
108
+ */
109
+ commentFor(model: string, action: string): StatementComment | undefined;
110
+ /**
111
+ * The name a statement with comment `c` is sent under, given its template's
112
+ * name. A template sent unnamed stays unnamed; a dynamic comment is unnamed;
113
+ * anything else gets a stable `c_` name from the process-wide budget, or
114
+ * goes unnamed once that is spent.
115
+ */
116
+ preparedName(templateName: string | undefined, c: StatementComment): string | undefined;
117
+ /** The usable dynamic pairs, encoded, or `undefined` when there are none. */
118
+ private dynamicPairs;
119
+ /** Why a dynamic key or value cannot be used, as [warning key, sentence], or `undefined`. */
120
+ private refusal;
121
+ /** One warning per reason and key, so a hook that misbehaves on every statement says it once. */
122
+ private dropped;
123
+ }
124
+ /**
125
+ * Validate `TurbineConfig.sqlComment` and build its commenter, or `undefined`
126
+ * when there is nothing to add. Throws `ValidationError` (E003) for a malformed
127
+ * option, and `UnsupportedFeatureError` (E017) where it cannot be honoured:
128
+ *
129
+ * - a backend that builds no SQL (PowDB, which injects its own query
130
+ * interface) has nothing to comment;
131
+ * - `dynamic` on an engine that prepares every statement text
132
+ * (`preparesEveryStatement`), which is MySQL: the MySQL engine sends each
133
+ * parameterized statement through mysql2's `execute()`, which keeps one
134
+ * SERVER-side prepared
135
+ * statement per distinct text per connection (up to 16,000 each), and the
136
+ * server caps the total across every client at `max_prepared_stmt_count`
137
+ * (16,382 by default). A value unique per request makes every text
138
+ * distinct, so a busy pool reaches that cap (error 1461) and every client
139
+ * of the server then fails to prepare.
140
+ */
141
+ export declare function resolveSqlComment(value: unknown, logger: TurbineLogger, engine: {
142
+ readonly dialect: string;
143
+ readonly preparesEveryStatement: boolean;
144
+ readonly buildsSql: boolean;
145
+ }): SqlCommenter | undefined;
146
+ /**
147
+ * `sql` with a comment Turbine appended removed, for code that reads statement
148
+ * text to decide how to run it (an engine shim telling a row-returning
149
+ * statement from a change count). A value can never contain `/*` (it is
150
+ * URL-encoded), so the last ` /*` of a statement ending in `*\/` is the
151
+ * comment's own opener; text Turbine did not comment is returned unchanged
152
+ * unless it, too, ends in a block comment, which is equally not code.
153
+ */
154
+ export declare function withoutTrailingComment(sql: string): string;
155
+ /**
156
+ * `q` as sent with `comment`: its statement, and every statement of its
157
+ * reselect plan (MySQL), carry the comment. Everything else on the query is
158
+ * kept. Used by the batch paths (`pipeline`, `$transaction([...])`), which run
159
+ * deferred queries without going through a query interface.
160
+ */
161
+ export declare function commentDeferred<T>(q: DeferredQuery<T>, comment: string | undefined): DeferredQuery<T>;
@@ -0,0 +1,374 @@
1
+ /**
2
+ * turbine-orm: `sqlComment`, sqlcommenter-style key/values on each statement
3
+ * Turbine builds, so `pg_stat_activity`, the server log, `auto_explain` and
4
+ * database APM tools can attribute a statement to a service, a model, a feature
5
+ * or a trace.
6
+ *
7
+ * SELECT ... LIMIT $2 /*action='findMany',application='billing',model='users'*\/
8
+ *
9
+ * FORMAT (the sqlcommenter spec): keys sorted, each value URL-encoded with
10
+ * `encodeURIComponent` and then `'` escaped as `\'`, each pair `key='value'`,
11
+ * pairs joined by commas, the whole comment appended after the statement with
12
+ * one space before it.
13
+ *
14
+ * WHY A VALUE CANNOT ESCAPE THE COMMENT. `encodeURIComponent` encodes `/`, so a
15
+ * value can hold neither `*\/` (the comment's end) nor `/*` (PostgreSQL and SQL
16
+ * Server nest block comments, so an opener would swallow the rest of the
17
+ * text). It also encodes every character a driver-side placeholder rewrite
18
+ * looks for (`$`, `?`, `:`, `@`) and `;`. Keys must match
19
+ * {@link SQL_COMMENT_KEY_PATTERN}, whose first-character rule also rules out
20
+ * MySQL's executable `/*!` and optimizer-hint `/*+` comments.
21
+ *
22
+ * WHERE IT IS APPENDED: at the execute seam, AFTER the SQL-template cache. The
23
+ * cache stores commentless text, so its keys, its hit rate and the lockstep
24
+ * checks are untouched by any comment, and a per-request value can never grow
25
+ * it. `$on('query')` events keep `sql` commentless and carry the comment in
26
+ * their own field.
27
+ *
28
+ * PREPARED-STATEMENT NAMES. node-postgres refuses to reuse a statement name for
29
+ * different text on one connection, and every name a connection prepares stays
30
+ * on the server until the connection closes. So:
31
+ *
32
+ * - no comment: the template's own name, as before;
33
+ * - static, model, action and tag only (constant per call site and label):
34
+ * `c_<16 hex>`, a hash of the template name and the comment, claimed from
35
+ * the same process-wide budget as every other name
36
+ * (`PREPARED_STATEMENT_BUDGET`), so the server-side cost stays bounded
37
+ * however many labels there are;
38
+ * - any dynamic value (a trace id, unique per request): unnamed, parsed on
39
+ * every execution and never kept. A per-request value under a name would
40
+ * prepare a new statement on every pooled connection for every request.
41
+ *
42
+ * Leaf apart from errors.ts, logger.ts, query-events.ts (the `$tag` scope) and
43
+ * utils.ts (the name budget); nothing here is Node-only.
44
+ */
45
+ import { UnsupportedFeatureError, ValidationError } from '../errors.js';
46
+ import { logTo } from '../logger.js';
47
+ import { currentQueryTag } from '../query-events.js';
48
+ import { claimPreparedName, fnv1a64Hex, LRUCache } from './utils.js';
49
+ import { shouldWarnOnce } from './warn-registry.js';
50
+ /** What a comment key must look like. Lower case, so it can never open `/*!` or `/*+`. */
51
+ export const SQL_COMMENT_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
52
+ /** Longest value accepted, in characters before encoding. */
53
+ export const MAX_SQL_COMMENT_VALUE_LENGTH = 256;
54
+ /** The keys `model: true` and `tag: true` fill in. */
55
+ const MODEL_KEYS = ['action', 'model'];
56
+ const TAG_KEY = 'tag';
57
+ const OPTION_KEYS = new Set(['static', 'model', 'tag', 'dynamic']);
58
+ /** Once-only warning namespace for dropped dynamic values. */
59
+ const DROPPED_WARN_NS = 'sqlCommentDropped';
60
+ /**
61
+ * `value` URL-encoded with `'` escaped, or `undefined` when it cannot be encoded
62
+ * (a lone surrogate). `encodeURIComponent` leaves `'` as is and never emits a
63
+ * backslash; the escape covers both anyway, so it stays complete on its own.
64
+ */
65
+ function encodeValue(value) {
66
+ try {
67
+ return encodeURIComponent(value).replace(/[\\']/g, '\\$&');
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ function render(pairs) {
74
+ let out = '/*';
75
+ for (let i = 0; i < pairs.length; i++) {
76
+ const [key, value] = pairs[i];
77
+ out += `${i === 0 ? '' : ','}${key}='${value}'`;
78
+ }
79
+ return `${out}*/`;
80
+ }
81
+ function byKey(a, b) {
82
+ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
83
+ }
84
+ function isPlainObject(value) {
85
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
86
+ return false;
87
+ const proto = Object.getPrototypeOf(value);
88
+ return proto === Object.prototype || proto === null;
89
+ }
90
+ /** A short, printable rendering of a caller's key for a message. */
91
+ function describeKey(key) {
92
+ return JSON.stringify(key.length > 64 ? `${key.slice(0, 64)}...` : key);
93
+ }
94
+ /**
95
+ * Builds the comment for each statement and the name it is sent under. One per
96
+ * client, shared by every table accessor and transaction of that client.
97
+ */
98
+ export class SqlCommenter {
99
+ logger;
100
+ /** Static pairs, encoded and sorted. */
101
+ staticPairs;
102
+ /** Keys a dynamic value may not take: the static ones and the ones model / tag fill in. */
103
+ reservedKeys;
104
+ withModel;
105
+ withTag;
106
+ dynamic;
107
+ /** The comment when it depends on nothing per statement (no model, no tag). */
108
+ constant;
109
+ /** Rendered model / action / tag comments. Bounded: tags may come from data. */
110
+ rendered = new LRUCache(1000);
111
+ /** Claimed `c_` names by template name and comment. Holds only claimed names, so the budget bounds it. */
112
+ names = new Map();
113
+ /**
114
+ * Every statement gets a comment (a static key, or the model and action),
115
+ * so a template's own name is never sent and need not be claimed from the
116
+ * budget; the commented name is claimed instead.
117
+ */
118
+ alwaysComments;
119
+ constructor(staticPairs, withModel, withTag, dynamic, logger) {
120
+ this.logger = logger;
121
+ this.staticPairs = [...staticPairs].sort(byKey);
122
+ this.withModel = withModel;
123
+ this.withTag = withTag;
124
+ this.dynamic = dynamic;
125
+ const reserved = new Set(this.staticPairs.map(([k]) => k));
126
+ if (withModel)
127
+ for (const k of MODEL_KEYS)
128
+ reserved.add(k);
129
+ if (withTag)
130
+ reserved.add(TAG_KEY);
131
+ this.reservedKeys = reserved;
132
+ this.alwaysComments = this.staticPairs.length > 0 || withModel;
133
+ this.constant =
134
+ !withModel && !withTag && this.staticPairs.length > 0
135
+ ? { comment: render(this.staticPairs), dynamic: false }
136
+ : undefined;
137
+ }
138
+ /**
139
+ * The comment for one statement of operation `action` on `model`, or
140
+ * `undefined` when it carries none. Reads the `$tag` scope and calls
141
+ * `dynamic` in the caller's async context, so call it where the statement
142
+ * is sent.
143
+ */
144
+ commentFor(model, action) {
145
+ let pairs;
146
+ let base;
147
+ if (this.withModel || this.withTag) {
148
+ const tag = this.withTag ? currentQueryTag() : undefined;
149
+ const key = `${model}\u0000${action}\u0000${tag ?? ''}`;
150
+ let hit = this.rendered.get(key);
151
+ if (hit === undefined) {
152
+ const built = [...this.staticPairs];
153
+ if (this.withModel) {
154
+ built.push(['action', encodeValue(action) ?? ''], ['model', encodeValue(model) ?? '']);
155
+ }
156
+ // A `$tag` label is 1 to 128 characters, checked when the scope opens.
157
+ const encodedTag = tag === undefined ? undefined : encodeValue(tag);
158
+ if (encodedTag !== undefined)
159
+ built.push([TAG_KEY, encodedTag]);
160
+ built.sort(byKey);
161
+ hit = { pairs: built, comment: built.length > 0 ? render(built) : '' };
162
+ this.rendered.set(key, hit);
163
+ }
164
+ pairs = hit.pairs;
165
+ base = hit.comment === '' ? undefined : { comment: hit.comment, dynamic: false };
166
+ }
167
+ else {
168
+ pairs = this.staticPairs;
169
+ base = this.constant;
170
+ }
171
+ if (this.dynamic === undefined)
172
+ return base;
173
+ const extra = this.dynamicPairs();
174
+ if (extra === undefined)
175
+ return base;
176
+ return { comment: render([...pairs, ...extra].sort(byKey)), dynamic: true };
177
+ }
178
+ /**
179
+ * The name a statement with comment `c` is sent under, given its template's
180
+ * name. A template sent unnamed stays unnamed; a dynamic comment is unnamed;
181
+ * anything else gets a stable `c_` name from the process-wide budget, or
182
+ * goes unnamed once that is spent.
183
+ */
184
+ preparedName(templateName, c) {
185
+ if (!templateName)
186
+ return templateName;
187
+ if (c.dynamic)
188
+ return '';
189
+ const key = `${templateName}\u0000${c.comment}`;
190
+ const known = this.names.get(key);
191
+ if (known !== undefined)
192
+ return known;
193
+ const name = claimPreparedName(`c_${fnv1a64Hex(key)}`);
194
+ if (name !== '')
195
+ this.names.set(key, name);
196
+ return name;
197
+ }
198
+ /** The usable dynamic pairs, encoded, or `undefined` when there are none. */
199
+ dynamicPairs() {
200
+ let values;
201
+ try {
202
+ values = this.dynamic?.();
203
+ }
204
+ catch (err) {
205
+ this.dropped('threw', '', `sqlComment.dynamic threw (${err instanceof Error ? err.message : String(err)}), so ` +
206
+ 'this statement carries no dynamic values. The statement itself ran as usual.');
207
+ return undefined;
208
+ }
209
+ if (values === undefined || values === null)
210
+ return undefined;
211
+ if (typeof values !== 'object' || Array.isArray(values)) {
212
+ this.dropped('shape', '', 'sqlComment.dynamic returned something other than an object of string values, so this statement carries ' +
213
+ 'no dynamic values. Return an object such as { traceparent: "00-..." }, or undefined for none.');
214
+ return undefined;
215
+ }
216
+ let out;
217
+ for (const key of Object.keys(values)) {
218
+ const value = values[key];
219
+ if (value === undefined)
220
+ continue;
221
+ const reason = this.refusal(key, value);
222
+ if (reason !== undefined) {
223
+ this.dropped(reason[0], key, `sqlComment.dynamic key ${describeKey(key)} was left out of the comment: ${reason[1]}`);
224
+ continue;
225
+ }
226
+ const encoded = encodeValue(value);
227
+ if (encoded === undefined) {
228
+ this.dropped('encoding', key, `sqlComment.dynamic key ${describeKey(key)} was left out of the comment: its value is not well-formed ` +
229
+ 'Unicode (a lone surrogate), so it cannot be URL-encoded.');
230
+ continue;
231
+ }
232
+ if (out === undefined)
233
+ out = [];
234
+ out.push([key, encoded]);
235
+ }
236
+ return out;
237
+ }
238
+ /** Why a dynamic key or value cannot be used, as [warning key, sentence], or `undefined`. */
239
+ refusal(key, value) {
240
+ if (!SQL_COMMENT_KEY_PATTERN.test(key)) {
241
+ return ['key', 'a key must be lower-case letters, digits and underscores, starting with a letter.'];
242
+ }
243
+ if (this.reservedKeys.has(key)) {
244
+ return ['reserved', 'that key is already set by sqlComment.static, model or tag, which take precedence.'];
245
+ }
246
+ if (typeof value !== 'string')
247
+ return ['type', `its value is ${typeof value}, not a string.`];
248
+ if (value.length > MAX_SQL_COMMENT_VALUE_LENGTH) {
249
+ return ['length', `its value is ${value.length} characters, over the ${MAX_SQL_COMMENT_VALUE_LENGTH} allowed.`];
250
+ }
251
+ return undefined;
252
+ }
253
+ /** One warning per reason and key, so a hook that misbehaves on every statement says it once. */
254
+ dropped(reason, key, message) {
255
+ if (!shouldWarnOnce(DROPPED_WARN_NS, `${reason}|${key.slice(0, 64)}`))
256
+ return;
257
+ logTo(this.logger, 'warn', message, { code: 'sql-comment-dropped', reason });
258
+ }
259
+ }
260
+ /**
261
+ * Validate `TurbineConfig.sqlComment` and build its commenter, or `undefined`
262
+ * when there is nothing to add. Throws `ValidationError` (E003) for a malformed
263
+ * option, and `UnsupportedFeatureError` (E017) where it cannot be honoured:
264
+ *
265
+ * - a backend that builds no SQL (PowDB, which injects its own query
266
+ * interface) has nothing to comment;
267
+ * - `dynamic` on an engine that prepares every statement text
268
+ * (`preparesEveryStatement`), which is MySQL: the MySQL engine sends each
269
+ * parameterized statement through mysql2's `execute()`, which keeps one
270
+ * SERVER-side prepared
271
+ * statement per distinct text per connection (up to 16,000 each), and the
272
+ * server caps the total across every client at `max_prepared_stmt_count`
273
+ * (16,382 by default). A value unique per request makes every text
274
+ * distinct, so a busy pool reaches that cap (error 1461) and every client
275
+ * of the server then fails to prepare.
276
+ */
277
+ export function resolveSqlComment(value, logger, engine) {
278
+ if (value === undefined)
279
+ return undefined;
280
+ if (!isPlainObject(value)) {
281
+ throw new ValidationError('Invalid sqlComment: expected an object with any of `static`, `model`, `tag` and `dynamic`, for example ' +
282
+ "{ static: { application: 'billing' }, model: true }.");
283
+ }
284
+ for (const key of Object.keys(value)) {
285
+ if (!OPTION_KEYS.has(key)) {
286
+ throw new ValidationError(`Invalid sqlComment: unknown key ${describeKey(key)}. The keys are \`static\`, \`model\`, \`tag\` and \`dynamic\`.`);
287
+ }
288
+ }
289
+ const { static: staticValues, model, tag, dynamic } = value;
290
+ for (const [name, flag] of [
291
+ ['model', model],
292
+ ['tag', tag],
293
+ ]) {
294
+ if (flag !== undefined && typeof flag !== 'boolean') {
295
+ throw new ValidationError(`Invalid sqlComment.${name}: expected true or false, got ${typeof flag}.`);
296
+ }
297
+ }
298
+ if (dynamic !== undefined && typeof dynamic !== 'function') {
299
+ throw new ValidationError(`Invalid sqlComment.dynamic: expected a function returning an object of string values, got ${typeof dynamic}.`);
300
+ }
301
+ const pairs = [];
302
+ if (staticValues !== undefined) {
303
+ if (!isPlainObject(staticValues)) {
304
+ throw new ValidationError("Invalid sqlComment.static: expected an object of string values, for example { application: 'billing' }.");
305
+ }
306
+ for (const key of Object.keys(staticValues)) {
307
+ const v = staticValues[key];
308
+ const where = `sqlComment.static key ${describeKey(key)}`;
309
+ if (!SQL_COMMENT_KEY_PATTERN.test(key)) {
310
+ throw new ValidationError(`Invalid ${where}: a key must be lower-case letters, digits and underscores, starting with a letter ` +
311
+ '(the comment must never open a MySQL executable or hint comment, `/*!` or `/*+`).');
312
+ }
313
+ if ((model === true && MODEL_KEYS.includes(key)) || (tag === true && key === TAG_KEY)) {
314
+ throw new ValidationError(`Invalid ${where}: \`${key}\` is filled in by sqlComment.${key === TAG_KEY ? 'tag' : 'model'}. ` +
315
+ 'Rename the static key or turn that option off.');
316
+ }
317
+ if (typeof v !== 'string') {
318
+ throw new ValidationError(`Invalid ${where}: expected a string value, got ${typeof v}.`);
319
+ }
320
+ if (v.length > MAX_SQL_COMMENT_VALUE_LENGTH) {
321
+ throw new ValidationError(`Invalid ${where}: the value is ${v.length} characters, over the ${MAX_SQL_COMMENT_VALUE_LENGTH} allowed.`);
322
+ }
323
+ const encoded = encodeValue(v);
324
+ if (encoded === undefined) {
325
+ throw new ValidationError(`Invalid ${where}: the value is not well-formed Unicode (a lone surrogate), so it cannot be URL-encoded.`);
326
+ }
327
+ pairs.push([key, encoded]);
328
+ }
329
+ }
330
+ if (pairs.length === 0 && model !== true && tag !== true && dynamic === undefined)
331
+ return undefined;
332
+ if (!engine.buildsSql) {
333
+ throw new UnsupportedFeatureError('sqlComment', engine.dialect, 'This engine does not run SQL, so there is no statement to carry a comment. Remove the option for this client.');
334
+ }
335
+ if (dynamic !== undefined && engine.preparesEveryStatement) {
336
+ throw new UnsupportedFeatureError('sqlComment.dynamic', engine.dialect, 'The MySQL engine prepares each distinct statement text on the server (mysql2 execute(), up to 16,000 per ' +
337
+ 'connection), and the server caps prepared statements across all clients at max_prepared_stmt_count ' +
338
+ '(16,382 by default). A per-statement value makes every text distinct, so a busy pool reaches that cap ' +
339
+ '(error 1461) and prepares start failing server-wide. Use `static`, `model` and `tag`, which are bounded.');
340
+ }
341
+ return new SqlCommenter(pairs, model === true, tag === true, dynamic, logger);
342
+ }
343
+ /**
344
+ * `sql` with a comment Turbine appended removed, for code that reads statement
345
+ * text to decide how to run it (an engine shim telling a row-returning
346
+ * statement from a change count). A value can never contain `/*` (it is
347
+ * URL-encoded), so the last ` /*` of a statement ending in `*\/` is the
348
+ * comment's own opener; text Turbine did not comment is returned unchanged
349
+ * unless it, too, ends in a block comment, which is equally not code.
350
+ */
351
+ export function withoutTrailingComment(sql) {
352
+ if (!sql.endsWith('*/'))
353
+ return sql;
354
+ const at = sql.lastIndexOf(' /*');
355
+ return at < 0 ? sql : sql.slice(0, at);
356
+ }
357
+ /**
358
+ * `q` as sent with `comment`: its statement, and every statement of its
359
+ * reselect plan (MySQL), carry the comment. Everything else on the query is
360
+ * kept. Used by the batch paths (`pipeline`, `$transaction([...])`), which run
361
+ * deferred queries without going through a query interface.
362
+ */
363
+ export function commentDeferred(q, comment) {
364
+ if (comment === undefined)
365
+ return q;
366
+ const reselect = q.reselect;
367
+ return {
368
+ ...q,
369
+ sql: `${q.sql} ${comment}`,
370
+ ...(reselect
371
+ ? { reselect: (exec) => reselect((sql, params, name) => exec(`${sql} ${comment}`, params, name)) }
372
+ : {}),
373
+ };
374
+ }