turbine-orm 0.81.0-next.d9b06dd → 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
package/README.md CHANGED
@@ -138,7 +138,7 @@ await db.$transaction(async (tx) => {
138
138
  });
139
139
  ```
140
140
 
141
- Nested `$transaction` calls become SAVEPOINTs. Isolation levels, timeouts, and `sessionContext` (transaction-local GUCs for Postgres RLS) are options. `tx` deliberately exposes a smaller surface than `db`; reaching for a client-only member is a compile error, not a production surprise. Reference: [turbineorm.dev/transactions](https://turbineorm.dev/transactions).
141
+ Nested `$transaction` calls become SAVEPOINTs. Isolation levels, timeouts, and `sessionContext` (transaction-local GUCs for Postgres RLS) are options. A transaction PostgreSQL rolled back (a statement failed and the callback caught it) rejects instead of reporting a commit, and a `tx` used after its callback settles throws rather than running outside the transaction. `tx` deliberately exposes a smaller surface than `db`; reaching for a client-only member is a compile error, not a production surprise. Reference: [turbineorm.dev/transactions](https://turbineorm.dev/transactions).
142
142
 
143
143
  ### Pipelining, at the protocol level
144
144
 
@@ -191,7 +191,7 @@ const db = turbine({
191
191
  });
192
192
  ```
193
193
 
194
- A global filter is a `WhereClause` that is AND-merged into every query on a table: reads, relation subqueries targeting it, and the predicates of `update` / `delete` / `upsert`. Values are parameterized. Details: [turbineorm.dev/global-filters](https://turbineorm.dev/global-filters).
194
+ A global filter is a `WhereClause` that is AND-merged into every query on a table: reads, relation subqueries targeting it, and the predicates of `update` / `delete` / `upsert`. Values are parameterized. Keys may be table names or client accessors and are checked when the client is built, and a function filter that returns `undefined` throws instead of running unfiltered. Details: [turbineorm.dev/global-filters](https://turbineorm.dev/global-filters).
195
195
 
196
196
  ### The UNSAFE symbol
197
197
 
@@ -217,7 +217,7 @@ try {
217
217
  }
218
218
  ```
219
219
 
220
- Every error Turbine raises extends `TurbineError` with a stable code (`TURBINE_E001` through `E018`) and a `docsUrl`; a driver error whose SQLSTATE Turbine does not map stays the driver's own error, carrying that SQLSTATE on `.code`. Error messages carry keys, never values: a `NotFoundError` says `where: { id, email }` without printing the email, so errors are safe to forward to a tracker without a scrubbing rule. Retryable failures (`DeadlockError`, `SerializationFailureError`) expose `isRetryable: true` as a typed const. `err.code` is the check that survives a mixed ESM/CJS module graph, where two copies of the package hold two copies of every class; since 0.78 `instanceof` works across copies too, through a cross-copy brand on every error. Full table: [turbineorm.dev/errors](https://turbineorm.dev/errors).
220
+ Every error Turbine raises extends `TurbineError` with a stable code (`TURBINE_E001` through `E020`) and a `docsUrl`; a driver error whose SQLSTATE Turbine does not map stays the driver's own error, carrying that SQLSTATE on `.code`. Error messages carry keys, never values: a `NotFoundError` says `where: { id, email }` without printing the email, so errors are safe to forward to a tracker without a scrubbing rule. Retryable failures (`DeadlockError`, `SerializationFailureError` and its subclass `LockNotAvailableError`) expose `isRetryable: true` as a typed const. `err.code` is the check that survives a mixed ESM/CJS module graph, where two copies of the package hold two copies of every class; since 0.78 `instanceof` works across copies too, through a cross-copy brand on every error. Full table: [turbineorm.dev/errors](https://turbineorm.dev/errors).
221
221
 
222
222
  ## Built for agents
223
223
 
@@ -254,7 +254,7 @@ The rest of the agent story is structural: query args are fully typed, so a wron
254
254
 
255
255
  **Destructive migrations need consent.** `migrate up`, `migrate down`, and `push` scan for `DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, unqualified `DELETE` / `UPDATE`, and `ALTER COLUMN ... TYPE`, print an itemized report, and refuse. Interactively you type `destroy my data`, then `yes`; in CI you pass `--allow-destructive`. A refused batch applies nothing. Migrations are SQL files, checksummed with SHA-256, applied under `pg_try_advisory_lock()`.
256
256
 
257
- **PII stays in the database.** Tag a column `pii: true` in the schema and it is excluded from every default projection at the SQL level: `RETURNING "id", "name"` instead of `RETURNING *`. It is also refused as a `groupBy` key and a `_min` / `_max` target. Reading it back takes `includePii: UNSAFE`, per query. A schema with no tagged column emits byte-identical SQL.
257
+ **PII stays in the database.** Tag a column `pii: true` in the schema and it is excluded from every default projection at the SQL level: `RETURNING "id", "name"` instead of `RETURNING *`. It is also refused as a `groupBy` key and a `_min` / `_max` target. Reading it back takes `includePii: UNSAFE`, per query, and naming it in `select` without that throws. A schema with no tagged column emits byte-identical SQL.
258
258
 
259
259
  **Studio is read-only by default.** `npx turbine studio` binds loopback, authenticates with a per-process token, runs every read inside `BEGIN READ ONLY`, and has no raw-SQL surface: queries are composed in a builder validated identifier by identifier. Without `--write`, the write endpoints do not exist in the router. PII cells are redacted server-side. Try it with no database: `npx turbine-orm@latest studio --demo` boots a seeded in-memory sample. Docs: [turbineorm.dev/studio](https://turbineorm.dev/studio).
260
260
 
@@ -272,7 +272,7 @@ Going deep on one database means the parts other ORMs push to raw SQL are typed
272
272
 
273
273
  ## Serverless and edge
274
274
 
275
- The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **93 kB brotli** (edge entry under **74 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
275
+ The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **147 kB brotli** (edge entry under **116 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
276
276
 
277
277
  ```typescript
278
278
  import { Pool } from '@neondatabase/serverless';
@@ -329,7 +329,7 @@ const schema = defineSchema({
329
329
 
330
330
  const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
331
331
  for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
332
- await db.raw([stmt] as never);
332
+ await db.rawUnsafe(stmt);
333
333
  }
334
334
 
335
335
  const users = await db
@@ -355,24 +355,24 @@ It is also built to be extended rather than wrapped. All SQL generation routes t
355
355
 
356
356
  ## Comparison
357
357
 
358
- | | **Turbine** | **Prisma** | **Drizzle** | **Kysely** |
359
- |---|---|---|---|---|
360
- | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine |
361
- | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 |
362
- | **Main bundle (brotli)** | under 93 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
363
- | **Studio** | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; [Gateway](https://gateway.drizzle.team/) self-hosted, free | None |
364
- | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
365
- | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
366
- | **Edge runtime** | One import swap, under 74 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native |
367
- | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
368
- | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
369
- | **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is Preview | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
370
- | **Index advice** | `turbine doctor`, offline, `--fix` | Optimize retired March 2026 (cloud Query Insights) | None | None |
371
- | **MCP server for agents** | 11 read-only tools, PII-redacted | Official MCP server | `drizzle-kit mcp` | None |
372
- | **Vector search** | Built-in `distance` / KNN | Preview / raw | Extension API | Manual |
373
- | **LISTEN/NOTIFY** | `$listen` / `$notify` | None | None | None |
374
-
375
- Competitor columns last checked 2026-08-22, against Prisma 7 and Drizzle 0.45 (the current `latest`; 1.0 is at rc.4). Cells that make a claim about someone else's pricing or licensing carry a link to that vendor's own page, because those are the cells that go stale silently: this table said Drizzle's Gateway was paid, which was true of the old Drizzle Studio subscription and is not true of Gateway, and nothing in the repo would have caught it. Features marked Preview may change; bundle sizes move release to release. The longer version of this argument, including what is not a reason to switch: [turbineorm.dev/why-turbine](https://turbineorm.dev/why-turbine).
358
+ | | **Turbine** | **Prisma** | **Drizzle** | **Kysely** | **MikroORM** |
359
+ |---|---|---|---|---|---|
360
+ | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine | Kysely as query runner; SQL built by MikroORM (v7) |
361
+ | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 | `@mikro-orm/core` (0 deps) + a driver package |
362
+ | **Main bundle (brotli)** | under 147 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
363
+ | **Studio** | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; [Gateway](https://gateway.drizzle.team/) self-hosted, free | None | AdminJS integration guide (CRUD panel) |
364
+ | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors | |
365
+ | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None | Generated by diffing entities against a snapshot or the database; TS/JS classes |
366
+ | **Edge runtime** | One import swap, under 116 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
367
+ | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual | `flush()` runs queued changes in one transaction |
368
+ | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None | `DriverException` subclasses (`UniqueConstraintViolationException`, ...) |
369
+ | **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is Preview | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) | `populate`; default `balanced`: joins to-one, one query per to-many relation |
370
+ | **Index advice** | `turbine doctor`, offline, `--fix` | Optimize retired March 2026 (cloud Query Insights) | None | None | |
371
+ | **MCP server for agents** | 11 read-only tools, PII-redacted | Official MCP server | `drizzle-kit mcp` | None | |
372
+ | **Vector search** | Built-in `distance` / KNN | Preview / raw | Extension API | Manual | |
373
+ | **LISTEN/NOTIFY** | `$listen` / `$notify` | None | None | None | |
374
+
375
+ Competitor columns last checked 2026-08-22, against Prisma 7 and Drizzle 0.45 (the current `latest`; 1.0 is at rc.4). The MikroORM column was checked 2026-10-13 against its 7.2 documentation; a blank cell is one that documentation does not settle. Cells that make a claim about someone else's pricing or licensing carry a link to that vendor's own page, because those are the cells that go stale silently: this table said Drizzle's Gateway was paid, which was true of the old Drizzle Studio subscription and is not true of Gateway, and nothing in the repo would have caught it. Features marked Preview may change; bundle sizes move release to release. The longer version of this argument, including what is not a reason to switch: [turbineorm.dev/why-turbine](https://turbineorm.dev/why-turbine).
376
376
 
377
377
  ## Limitations
378
378
 
@@ -388,13 +388,14 @@ Stated so you do not find out three weeks in:
388
388
  | Postgres | TypeScript | Notes |
389
389
  |---|---|---|
390
390
  | `int2`, `int4`, `float4`, `float8` | `number` | |
391
- | `int8` / `bigint` | `number` | Values above `Number.MAX_SAFE_INTEGER` come back as `string` to avoid precision loss |
391
+ | `int8` / `bigint` | `number` | Values above `Number.MAX_SAFE_INTEGER` come back as `string` to avoid precision loss; `bigint[]` reads as `string[]` |
392
392
  | `numeric`, `money` | `string` | Arbitrary precision, kept exact |
393
393
  | `text`, `varchar`, `uuid`, `citext` | `string` | |
394
394
  | `timestamptz`, `timestamp`, `date` | `Date` | Zone-less columns read and write as UTC by default (`utcTimestamps`); the full temporal semantics, including `temporalInfinity`, are at [turbineorm.dev/schema](https://turbineorm.dev/schema#zone-less-columns-timestamp-and-date-read-as-utc) |
395
395
  | `boolean` | `boolean` | |
396
396
  | `json`, `jsonb` | `unknown` | |
397
397
  | `bytea` | `Buffer` | |
398
+ | `interval`, `point`, `circle` | object | The driver's parsed form, for example `{ x, y }` for a point |
398
399
  | Array types | `T[]` | |
399
400
 
400
401
  ## Examples
@@ -34,10 +34,35 @@ export interface Checkout {
34
34
  * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
35
35
  * puts `TURBINE_E0NN` in.
36
36
  *
37
- * Query paths need no equivalent: `pool.query()` opens the connection itself
38
- * and rejects with the connect error, which the query boundary already wraps.
37
+ * Query paths reach the same errors through `pool.query()`, which checks out
38
+ * internally and rejects with the connect error; the query boundary wraps
39
+ * those, and {@link typeCheckoutTimeouts} types pg-pool's own checkout
40
+ * failures on the pools Turbine owns.
39
41
  */
40
42
  export declare function acquireConnection(pool: PgCompatPool): Promise<Checkout>;
43
+ /**
44
+ * A failed checkout as a typed error.
45
+ *
46
+ * pg-pool raises its two checkout failures as plain `Error`s with no code, so
47
+ * `wrapPgError` (keyed on SQLSTATE and socket codes) passes them through and
48
+ * the most common production connection failure, an exhausted pool, reached
49
+ * callers as an untyped error: `instanceof ConnectionError` and
50
+ * `code === 'TURBINE_E004'` both missed it. Everything else goes through
51
+ * `wrapPgError` as before.
52
+ */
53
+ export declare function checkoutError(err: unknown, pool?: unknown): unknown;
54
+ /**
55
+ * Make every checkout failure of a pool Turbine owns a typed error, for
56
+ * `pool.query` as well as `pool.connect`.
57
+ *
58
+ * {@link acquireConnection} covers the paths that check out explicitly
59
+ * (transactions, nested writes, streams, pipelines). An ordinary read or write
60
+ * goes through `pool.query`, which checks out internally via `this.connect`
61
+ * in callback form, so the conversion has to sit on `connect` itself. Wrapping
62
+ * the error is all it does. For pools Turbine creates only, the same rule as
63
+ * the connection guards: a caller's own pool is not Turbine's to patch.
64
+ */
65
+ export declare function typeCheckoutTimeouts(pool: unknown): void;
41
66
  /**
42
67
  * Check a connection out and run `opening` on it, retrying ONCE on a fresh
43
68
  * connection when `opening` fails with {@link isStaleConnectionError}.
package/dist/checkout.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * write to lose.
19
19
  */
20
20
  import { guardCheckout, settleEventLoop } from './connection-guard.js';
21
- import { explainConnectionLoss, isStaleConnectionError, wrapPgError } from './errors.js';
21
+ import { ConnectionError, explainConnectionLoss, isStaleConnectionError, UnsupportedFeatureError, wrapPgError, } from './errors.js';
22
22
  /**
23
23
  * Check a connection out, as a typed Turbine error when that fails.
24
24
  *
@@ -29,19 +29,90 @@ import { explainConnectionLoss, isStaleConnectionError, wrapPgError } from './er
29
29
  * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
30
30
  * puts `TURBINE_E0NN` in.
31
31
  *
32
- * Query paths need no equivalent: `pool.query()` opens the connection itself
33
- * and rejects with the connect error, which the query boundary already wraps.
32
+ * Query paths reach the same errors through `pool.query()`, which checks out
33
+ * internally and rejects with the connect error; the query boundary wraps
34
+ * those, and {@link typeCheckoutTimeouts} types pg-pool's own checkout
35
+ * failures on the pools Turbine owns.
34
36
  */
35
37
  export async function acquireConnection(pool) {
38
+ if (typeof pool.connect !== 'function') {
39
+ const dialect = pool.dialect?.name;
40
+ throw new UnsupportedFeatureError('Checking out a connection', typeof dialect === 'string' ? dialect : 'postgresql', 'A transaction, a cursor stream, a pipeline or a write that takes several statements needs a connection ' +
41
+ 'of its own, and this pool has no `connect()` (a stateless HTTP driver). Pass a pool that can check out ' +
42
+ 'a connection, or run single statements.');
43
+ }
36
44
  let client;
37
45
  try {
38
46
  client = await pool.connect();
39
47
  }
40
48
  catch (err) {
41
- throw wrapPgError(err);
49
+ throw checkoutError(err, pool);
42
50
  }
43
51
  return { client, checkout: guardCheckout(client) };
44
52
  }
53
+ /** pg-pool's own words for a checkout that waited longer than `connectionTimeoutMillis`. */
54
+ const POOL_TIMEOUT_MESSAGE = 'timeout exceeded when trying to connect';
55
+ /** pg-pool's own words for a checkout from a pool that has been ended. */
56
+ const POOL_ENDED_MESSAGE = 'Cannot use a pool after calling end on the pool';
57
+ /**
58
+ * A failed checkout as a typed error.
59
+ *
60
+ * pg-pool raises its two checkout failures as plain `Error`s with no code, so
61
+ * `wrapPgError` (keyed on SQLSTATE and socket codes) passes them through and
62
+ * the most common production connection failure, an exhausted pool, reached
63
+ * callers as an untyped error: `instanceof ConnectionError` and
64
+ * `code === 'TURBINE_E004'` both missed it. Everything else goes through
65
+ * `wrapPgError` as before.
66
+ */
67
+ export function checkoutError(err, pool) {
68
+ if (!(err instanceof Error) || err.code !== undefined)
69
+ return wrapPgError(err);
70
+ if (err.message === POOL_TIMEOUT_MESSAGE) {
71
+ const sizing = (pool ?? {});
72
+ const max = sizing.options?.max;
73
+ const waitMs = sizing.options?.connectionTimeoutMillis;
74
+ const waiting = sizing.waitingCount;
75
+ const occupancy = max !== undefined
76
+ ? ` All ${max} connections of the pool were checked out${waiting ? ` and ${waiting} other callers were waiting` : ''}.`
77
+ : '';
78
+ return new ConnectionError(`Timed out${waitMs ? ` after ${waitMs}ms` : ''} waiting for a free connection from the pool.${occupancy} ` +
79
+ 'Something is holding connections for longer than `connectionTimeoutMs`: long transactions or streams, ' +
80
+ 'or a query on the outer client inside a `$transaction` callback, which needs a second connection ' +
81
+ 'while the transaction holds the first (use `tx` there). Raise `poolSize` if the load is genuine.', { cause: err });
82
+ }
83
+ if (err.message === POOL_ENDED_MESSAGE) {
84
+ return new ConnectionError('The connection pool has been shut down (`disconnect()` or `end()` was called), so no connection can be ' +
85
+ 'checked out. Create a new client, or stop issuing queries after shutting this one down.', { cause: err });
86
+ }
87
+ return wrapPgError(err);
88
+ }
89
+ /**
90
+ * Make every checkout failure of a pool Turbine owns a typed error, for
91
+ * `pool.query` as well as `pool.connect`.
92
+ *
93
+ * {@link acquireConnection} covers the paths that check out explicitly
94
+ * (transactions, nested writes, streams, pipelines). An ordinary read or write
95
+ * goes through `pool.query`, which checks out internally via `this.connect`
96
+ * in callback form, so the conversion has to sit on `connect` itself. Wrapping
97
+ * the error is all it does. For pools Turbine creates only, the same rule as
98
+ * the connection guards: a caller's own pool is not Turbine's to patch.
99
+ */
100
+ export function typeCheckoutTimeouts(pool) {
101
+ const p = pool;
102
+ const connect = p.connect;
103
+ if (typeof connect !== 'function')
104
+ return;
105
+ p.connect = (...args) => {
106
+ const callback = args[0];
107
+ if (typeof callback === 'function') {
108
+ return connect.call(p, (err, ...rest) => callback(err ? checkoutError(err, pool) : err, ...rest));
109
+ }
110
+ const pending = connect.apply(p, args);
111
+ return pending.catch((err) => {
112
+ throw checkoutError(err, pool);
113
+ });
114
+ };
115
+ }
45
116
  /**
46
117
  * Check a connection out and run `opening` on it, retrying ONCE on a fresh
47
118
  * connection when `opening` fails with {@link isStaleConnectionError}.
@@ -34,10 +34,35 @@ export interface Checkout {
34
34
  * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
35
35
  * puts `TURBINE_E0NN` in.
36
36
  *
37
- * Query paths need no equivalent: `pool.query()` opens the connection itself
38
- * and rejects with the connect error, which the query boundary already wraps.
37
+ * Query paths reach the same errors through `pool.query()`, which checks out
38
+ * internally and rejects with the connect error; the query boundary wraps
39
+ * those, and {@link typeCheckoutTimeouts} types pg-pool's own checkout
40
+ * failures on the pools Turbine owns.
39
41
  */
40
42
  export declare function acquireConnection(pool: PgCompatPool): Promise<Checkout>;
43
+ /**
44
+ * A failed checkout as a typed error.
45
+ *
46
+ * pg-pool raises its two checkout failures as plain `Error`s with no code, so
47
+ * `wrapPgError` (keyed on SQLSTATE and socket codes) passes them through and
48
+ * the most common production connection failure, an exhausted pool, reached
49
+ * callers as an untyped error: `instanceof ConnectionError` and
50
+ * `code === 'TURBINE_E004'` both missed it. Everything else goes through
51
+ * `wrapPgError` as before.
52
+ */
53
+ export declare function checkoutError(err: unknown, pool?: unknown): unknown;
54
+ /**
55
+ * Make every checkout failure of a pool Turbine owns a typed error, for
56
+ * `pool.query` as well as `pool.connect`.
57
+ *
58
+ * {@link acquireConnection} covers the paths that check out explicitly
59
+ * (transactions, nested writes, streams, pipelines). An ordinary read or write
60
+ * goes through `pool.query`, which checks out internally via `this.connect`
61
+ * in callback form, so the conversion has to sit on `connect` itself. Wrapping
62
+ * the error is all it does. For pools Turbine creates only, the same rule as
63
+ * the connection guards: a caller's own pool is not Turbine's to patch.
64
+ */
65
+ export declare function typeCheckoutTimeouts(pool: unknown): void;
41
66
  /**
42
67
  * Check a connection out and run `opening` on it, retrying ONCE on a fresh
43
68
  * connection when `opening` fails with {@link isStaleConnectionError}.
@@ -20,6 +20,8 @@
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.acquireConnection = acquireConnection;
23
+ exports.checkoutError = checkoutError;
24
+ exports.typeCheckoutTimeouts = typeCheckoutTimeouts;
23
25
  exports.openCheckout = openCheckout;
24
26
  const connection_guard_js_1 = require("./connection-guard.js");
25
27
  const errors_js_1 = require("./errors.js");
@@ -33,19 +35,90 @@ const errors_js_1 = require("./errors.js");
33
35
  * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
34
36
  * puts `TURBINE_E0NN` in.
35
37
  *
36
- * Query paths need no equivalent: `pool.query()` opens the connection itself
37
- * and rejects with the connect error, which the query boundary already wraps.
38
+ * Query paths reach the same errors through `pool.query()`, which checks out
39
+ * internally and rejects with the connect error; the query boundary wraps
40
+ * those, and {@link typeCheckoutTimeouts} types pg-pool's own checkout
41
+ * failures on the pools Turbine owns.
38
42
  */
39
43
  async function acquireConnection(pool) {
44
+ if (typeof pool.connect !== 'function') {
45
+ const dialect = pool.dialect?.name;
46
+ throw new errors_js_1.UnsupportedFeatureError('Checking out a connection', typeof dialect === 'string' ? dialect : 'postgresql', 'A transaction, a cursor stream, a pipeline or a write that takes several statements needs a connection ' +
47
+ 'of its own, and this pool has no `connect()` (a stateless HTTP driver). Pass a pool that can check out ' +
48
+ 'a connection, or run single statements.');
49
+ }
40
50
  let client;
41
51
  try {
42
52
  client = await pool.connect();
43
53
  }
44
54
  catch (err) {
45
- throw (0, errors_js_1.wrapPgError)(err);
55
+ throw checkoutError(err, pool);
46
56
  }
47
57
  return { client, checkout: (0, connection_guard_js_1.guardCheckout)(client) };
48
58
  }
59
+ /** pg-pool's own words for a checkout that waited longer than `connectionTimeoutMillis`. */
60
+ const POOL_TIMEOUT_MESSAGE = 'timeout exceeded when trying to connect';
61
+ /** pg-pool's own words for a checkout from a pool that has been ended. */
62
+ const POOL_ENDED_MESSAGE = 'Cannot use a pool after calling end on the pool';
63
+ /**
64
+ * A failed checkout as a typed error.
65
+ *
66
+ * pg-pool raises its two checkout failures as plain `Error`s with no code, so
67
+ * `wrapPgError` (keyed on SQLSTATE and socket codes) passes them through and
68
+ * the most common production connection failure, an exhausted pool, reached
69
+ * callers as an untyped error: `instanceof ConnectionError` and
70
+ * `code === 'TURBINE_E004'` both missed it. Everything else goes through
71
+ * `wrapPgError` as before.
72
+ */
73
+ function checkoutError(err, pool) {
74
+ if (!(err instanceof Error) || err.code !== undefined)
75
+ return (0, errors_js_1.wrapPgError)(err);
76
+ if (err.message === POOL_TIMEOUT_MESSAGE) {
77
+ const sizing = (pool ?? {});
78
+ const max = sizing.options?.max;
79
+ const waitMs = sizing.options?.connectionTimeoutMillis;
80
+ const waiting = sizing.waitingCount;
81
+ const occupancy = max !== undefined
82
+ ? ` All ${max} connections of the pool were checked out${waiting ? ` and ${waiting} other callers were waiting` : ''}.`
83
+ : '';
84
+ return new errors_js_1.ConnectionError(`Timed out${waitMs ? ` after ${waitMs}ms` : ''} waiting for a free connection from the pool.${occupancy} ` +
85
+ 'Something is holding connections for longer than `connectionTimeoutMs`: long transactions or streams, ' +
86
+ 'or a query on the outer client inside a `$transaction` callback, which needs a second connection ' +
87
+ 'while the transaction holds the first (use `tx` there). Raise `poolSize` if the load is genuine.', { cause: err });
88
+ }
89
+ if (err.message === POOL_ENDED_MESSAGE) {
90
+ return new errors_js_1.ConnectionError('The connection pool has been shut down (`disconnect()` or `end()` was called), so no connection can be ' +
91
+ 'checked out. Create a new client, or stop issuing queries after shutting this one down.', { cause: err });
92
+ }
93
+ return (0, errors_js_1.wrapPgError)(err);
94
+ }
95
+ /**
96
+ * Make every checkout failure of a pool Turbine owns a typed error, for
97
+ * `pool.query` as well as `pool.connect`.
98
+ *
99
+ * {@link acquireConnection} covers the paths that check out explicitly
100
+ * (transactions, nested writes, streams, pipelines). An ordinary read or write
101
+ * goes through `pool.query`, which checks out internally via `this.connect`
102
+ * in callback form, so the conversion has to sit on `connect` itself. Wrapping
103
+ * the error is all it does. For pools Turbine creates only, the same rule as
104
+ * the connection guards: a caller's own pool is not Turbine's to patch.
105
+ */
106
+ function typeCheckoutTimeouts(pool) {
107
+ const p = pool;
108
+ const connect = p.connect;
109
+ if (typeof connect !== 'function')
110
+ return;
111
+ p.connect = (...args) => {
112
+ const callback = args[0];
113
+ if (typeof callback === 'function') {
114
+ return connect.call(p, (err, ...rest) => callback(err ? checkoutError(err, pool) : err, ...rest));
115
+ }
116
+ const pending = connect.apply(p, args);
117
+ return pending.catch((err) => {
118
+ throw checkoutError(err, pool);
119
+ });
120
+ };
121
+ }
49
122
  /**
50
123
  * Check a connection out and run `opening` on it, retrying ONCE on a fresh
51
124
  * connection when `opening` fails with {@link isStaleConnectionError}.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Code-first column tags for `turbine generate`.
3
+ *
4
+ * `pii`, `updatedAt`, `maxLength` and `tsType` exist only in the `defineSchema()`
5
+ * file: the database has no column for them, so a client generated from
6
+ * introspection alone returns PII by default, never stamps `updatedAt`, and
7
+ * types a `jsonb` column as `unknown`. `generate` reads the schema file and
8
+ * overlays its tags onto the introspected schema with the two functions here.
9
+ *
10
+ * Lives under `cli/` because only the CLI uses it. `schemaDefToMetadata` (the
11
+ * library path) shares the tag list through `declaredTagsOf`.
12
+ */
13
+ import { type SchemaMetadata } from '../schema.js';
14
+ import type { SchemaDef } from '../schema-builder.js';
15
+ import { type CodeFirstColumnTags } from '../schema-metadata.js';
16
+ /** Declared tags per table (database name), per column (database name). */
17
+ export type CodeFirstTagMap = Map<string, Map<string, CodeFirstColumnTags>>;
18
+ /**
19
+ * Collect the code-first-only tags a {@link SchemaDef} declares, keyed by the
20
+ * database table and column they land on (the same `camelToSnake(field)` rule
21
+ * {@link schemaDefToMetadata} uses). Tables and columns that declare no tag are
22
+ * absent from the result.
23
+ *
24
+ * Introspection can not see these flags, because the database has no column for
25
+ * them. `turbine generate` overlays them onto the introspected schema with
26
+ * {@link applyCodeFirstColumnTags}.
27
+ */
28
+ export declare function extractCodeFirstColumnTags(def: SchemaDef): CodeFirstTagMap;
29
+ /** What {@link applyCodeFirstColumnTags} changed. */
30
+ export interface AppliedColumnTags {
31
+ /** Tables that received at least one tag. */
32
+ tables: number;
33
+ pii: number;
34
+ updatedAt: number;
35
+ maxLength: number;
36
+ /**
37
+ * Declared `tsType`s, as generator `columnTypes` entries
38
+ * (`'<table>.<column>'` by database name). Returned rather than written onto
39
+ * the schema so they go through the generator's own key and type-text
40
+ * validation, the same as a `columnTypes` entry in the config file.
41
+ */
42
+ columnTypes: Record<string, string>;
43
+ }
44
+ /**
45
+ * Overlay declared tags onto an introspected schema, in place, matching tables
46
+ * and columns by their database names.
47
+ *
48
+ * `pii` and `updatedAt` are set wherever declared. `maxLength` only fills a gap:
49
+ * the live column's own length wins when introspection read one.
50
+ *
51
+ * A `pii` or `updatedAt` tag that names a table or column the database does not
52
+ * have is an error, never a skip: a typo or an un-pushed schema would otherwise
53
+ * generate a client that silently returns the column it was tagged to hide.
54
+ * Every problem is reported at once. `maxLength` is also in the DDL, so a
55
+ * missing table or column only costs that fill-in. `inScope` lets the caller say
56
+ * that a missing table was deliberately left out of the client (an `exclude`
57
+ * filter), which is not an error either.
58
+ */
59
+ export declare function applyCodeFirstColumnTags(schema: SchemaMetadata, tags: CodeFirstTagMap, options?: {
60
+ source?: string;
61
+ inScope?: (table: string) => boolean;
62
+ }): AppliedColumnTags;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * Code-first column tags for `turbine generate`.
4
+ *
5
+ * `pii`, `updatedAt`, `maxLength` and `tsType` exist only in the `defineSchema()`
6
+ * file: the database has no column for them, so a client generated from
7
+ * introspection alone returns PII by default, never stamps `updatedAt`, and
8
+ * types a `jsonb` column as `unknown`. `generate` reads the schema file and
9
+ * overlays its tags onto the introspected schema with the two functions here.
10
+ *
11
+ * Lives under `cli/` because only the CLI uses it. `schemaDefToMetadata` (the
12
+ * library path) shares the tag list through `declaredTagsOf`.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.extractCodeFirstColumnTags = extractCodeFirstColumnTags;
16
+ exports.applyCodeFirstColumnTags = applyCodeFirstColumnTags;
17
+ const errors_js_1 = require("../errors.js");
18
+ const introspect_js_1 = require("../introspect.js");
19
+ const utils_js_1 = require("../query/utils.js");
20
+ const schema_js_1 = require("../schema.js");
21
+ const schema_metadata_js_1 = require("../schema-metadata.js");
22
+ /**
23
+ * Collect the code-first-only tags a {@link SchemaDef} declares, keyed by the
24
+ * database table and column they land on (the same `camelToSnake(field)` rule
25
+ * {@link schemaDefToMetadata} uses). Tables and columns that declare no tag are
26
+ * absent from the result.
27
+ *
28
+ * Introspection can not see these flags, because the database has no column for
29
+ * them. `turbine generate` overlays them onto the introspected schema with
30
+ * {@link applyCodeFirstColumnTags}.
31
+ */
32
+ function extractCodeFirstColumnTags(def) {
33
+ const out = new Map();
34
+ for (const tableDef of Object.values(def.tables)) {
35
+ for (const [field, config] of Object.entries(tableDef.columns)) {
36
+ const tags = (0, schema_metadata_js_1.declaredTagsOf)(config);
37
+ if (Object.keys(tags).length === 0)
38
+ continue;
39
+ let columns = out.get(tableDef.name);
40
+ if (!columns) {
41
+ columns = new Map();
42
+ out.set(tableDef.name, columns);
43
+ }
44
+ columns.set((0, schema_js_1.camelToSnake)(field), tags);
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+ /** A tag the database has no column for, so only the schema file can supply it. */
50
+ function isCodeFirstOnly(tags) {
51
+ return tags.pii === true || tags.updatedAt === true;
52
+ }
53
+ /**
54
+ * Overlay declared tags onto an introspected schema, in place, matching tables
55
+ * and columns by their database names.
56
+ *
57
+ * `pii` and `updatedAt` are set wherever declared. `maxLength` only fills a gap:
58
+ * the live column's own length wins when introspection read one.
59
+ *
60
+ * A `pii` or `updatedAt` tag that names a table or column the database does not
61
+ * have is an error, never a skip: a typo or an un-pushed schema would otherwise
62
+ * generate a client that silently returns the column it was tagged to hide.
63
+ * Every problem is reported at once. `maxLength` is also in the DDL, so a
64
+ * missing table or column only costs that fill-in. `inScope` lets the caller say
65
+ * that a missing table was deliberately left out of the client (an `exclude`
66
+ * filter), which is not an error either.
67
+ */
68
+ function applyCodeFirstColumnTags(schema, tags, options = {}) {
69
+ const applied = { tables: 0, pii: 0, updatedAt: 0, maxLength: 0, columnTypes: {} };
70
+ const problems = [];
71
+ for (const [tableName, columns] of tags) {
72
+ const shown = (0, introspect_js_1.printableCatalogName)(tableName);
73
+ if (!Object.hasOwn(schema.tables, tableName)) {
74
+ if (options.inScope && !options.inScope(tableName))
75
+ continue;
76
+ if (![...columns.values()].some(isCodeFirstOnly))
77
+ continue;
78
+ const near = (0, utils_js_1.closestName)(tableName, Object.keys(schema.tables));
79
+ problems.push(`table "${shown}" is not in the database${near ? ` (did you mean "${(0, introspect_js_1.printableCatalogName)(near)}"?)` : ''}`);
80
+ continue;
81
+ }
82
+ const table = schema.tables[tableName];
83
+ let touched = false;
84
+ for (const [columnName, declared] of columns) {
85
+ const column = table.columns.find((c) => c.name === columnName);
86
+ if (!column) {
87
+ if (!isCodeFirstOnly(declared))
88
+ continue;
89
+ const near = (0, utils_js_1.closestName)(columnName, table.columns.map((c) => c.name));
90
+ problems.push(`column "${(0, introspect_js_1.printableCatalogName)(columnName)}" on table "${shown}" is not in the database` +
91
+ `${near ? ` (did you mean "${(0, introspect_js_1.printableCatalogName)(near)}"?)` : ''}`);
92
+ continue;
93
+ }
94
+ if (declared.pii) {
95
+ column.pii = true;
96
+ applied.pii++;
97
+ touched = true;
98
+ }
99
+ if (declared.updatedAt) {
100
+ column.updatedAt = true;
101
+ applied.updatedAt++;
102
+ touched = true;
103
+ }
104
+ if (declared.maxLength !== undefined && column.maxLength === undefined) {
105
+ column.maxLength = declared.maxLength;
106
+ applied.maxLength++;
107
+ touched = true;
108
+ }
109
+ if (declared.tsType !== undefined) {
110
+ applied.columnTypes[`${tableName}.${columnName}`] = declared.tsType;
111
+ touched = true;
112
+ }
113
+ }
114
+ if (touched)
115
+ applied.tables++;
116
+ }
117
+ if (problems.length > 0) {
118
+ const from = options.source ? ` in ${options.source}` : '';
119
+ throw new errors_js_1.ValidationError(`Cannot apply the column tags declared${from}: ${problems.join('; ')}. ` +
120
+ 'Run `turbine push` so the database has them, or fix the name in the schema file. ' +
121
+ 'Generating without the tags would return the columns they mark as PII.');
122
+ }
123
+ return applied;
124
+ }
@@ -108,6 +108,9 @@ exports.COLUMN_NAMING_ARG_KEYS = [
108
108
  '_avg',
109
109
  '_min',
110
110
  '_max',
111
+ // Keys are column names and the result is a stored value (or one equal to it),
112
+ // the same reason `_min` / `_max` are listed.
113
+ '_percentile',
111
114
  ];
112
115
  /**
113
116
  * Does this args object carry a key that names a column, AT ANY DEPTH?
@@ -154,6 +157,12 @@ function carriesColumnNamingArg(args, depth = 0) {
154
157
  // ---------------------------------------------------------------------------
155
158
  // Compile
156
159
  // ---------------------------------------------------------------------------
160
+ /**
161
+ * Any non-array object, class instances included. Deliberately NOT the shared
162
+ * `isPlainRecord`: the builder walks a `with` (and a `where`) by its own keys
163
+ * whatever its prototype, so a report that skipped a class-instance `with`
164
+ * would list no relation while the SQL loads one.
165
+ */
157
166
  function isPlainObject(value) {
158
167
  return typeof value === 'object' && value !== null && !Array.isArray(value);
159
168
  }