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
@@ -19,7 +19,9 @@
19
19
  * impossible to string-concatenate a value into the query through this API.
20
20
  * This is the whole point of the tagged-template shape: the literal segments
21
21
  * are frozen by the compiler (`TemplateStringsArray`), and the only way to
22
- * get a runtime value into the query is via `${...}`, which we bind.
22
+ * get a runtime value into the query is via `${...}`, which we bind. A
23
+ * plain array passed where the template belongs is refused at runtime
24
+ * ({@link assertTemplateStrings}).
23
25
  *
24
26
  * 3. **Rows are returned as-is (no snake→camel mapping).** This matches the
25
27
  * existing `client.raw()` behavior: a typed raw query is a literal escape
@@ -49,6 +51,7 @@
49
51
  */
50
52
  import type { PgCompatPool } from './client.js';
51
53
  import { type Dialect } from './dialect.js';
54
+ import { type TurbineLogger } from './logger.js';
52
55
  import type { QueryEvent } from './query/deferred.js';
53
56
  /**
54
57
  * Build a `(sql, params)` pair from a tagged-template invocation.
@@ -61,10 +64,37 @@ import type { QueryEvent } from './query/deferred.js';
61
64
  *
62
65
  * Exported for unit testing the parameterization invariant without a database.
63
66
  */
64
- export declare function buildTypedSql(strings: TemplateStringsArray, values: readonly unknown[], dialect?: Pick<Dialect, 'paramPlaceholder'>): {
67
+ export declare function buildTypedSql(strings: TemplateStringsArray, values: readonly unknown[], dialect?: Pick<Dialect, 'paramPlaceholder'>, api?: string): {
65
68
  sql: string;
66
69
  params: unknown[];
67
70
  };
71
+ /**
72
+ * Refuse a "template" that did not come from a tagged-template call.
73
+ *
74
+ * Only the static segments of a template reach the SQL text, and that is safe
75
+ * because the compiler wrote them. A plain array passed in their place (a
76
+ * JSON-parsed request field, a string split by hand) puts its text into the
77
+ * SQL with nothing escaped. A real `TemplateStringsArray` is frozen and carries
78
+ * a frozen `raw` array of the same length; `JSON.parse` and array literals
79
+ * produce neither.
80
+ *
81
+ * `unsafeApi` names the method that takes statement text instead, for the
82
+ * error message.
83
+ */
84
+ export declare function assertTemplateStrings(strings: unknown, api: string, unsafeApi?: string): asserts strings is TemplateStringsArray;
85
+ /**
86
+ * Raw-statement params with every `Date` (in arrays too) bound as UTC text,
87
+ * for a client reading zone-less temporal types as UTC (`utcTimestamps`).
88
+ *
89
+ * node-postgres renders a Date in the process's zone with its offset. A
90
+ * `timestamptz` keeps the instant either way, but a `timestamp` or `date`
91
+ * drops the offset and stores the local wall time, which then reads back
92
+ * shifted by the process offset. UTC text stores the instant's UTC fields,
93
+ * which is what a UTC read returns. Returns `params` itself when it holds no
94
+ * Date. An Invalid Date is refused (E003) instead of reaching the server as
95
+ * unparseable text.
96
+ */
97
+ export declare function utcDateParams(params: unknown[], api: string): unknown[];
68
98
  /**
69
99
  * A pending typed raw SQL query. Implements the thenable contract, so it can be
70
100
  * `await`ed directly to get `T[]`, or refined via `.one()` / `.scalar()` first.
@@ -100,6 +130,8 @@ export declare class TypedSqlQuery<T extends Record<string, unknown>> implements
100
130
  * built outside a client, which then emits nothing, as before.
101
131
  */
102
132
  private readonly onQuery?;
133
+ /** Where the `logging` preview line goes. Defaults to the console logger. */
134
+ private readonly logger;
103
135
  constructor(pool: PgCompatPool, sql: string, params: unknown[], logging: boolean,
104
136
  /**
105
137
  * Runs the execution under the owning client's `errorMessages` mode. Passed
@@ -120,7 +152,9 @@ export declare class TypedSqlQuery<T extends Record<string, unknown>> implements
120
152
  * with model `'$raw'` and action `'sql'`. Absent for a `TypedSqlQuery`
121
153
  * built outside a client, which then emits nothing, as before.
122
154
  */
123
- onQuery?: ((event: QueryEvent) => void) | undefined);
155
+ onQuery?: ((event: QueryEvent) => void) | undefined,
156
+ /** Where the `logging` preview line goes. Defaults to the console logger. */
157
+ logger?: TurbineLogger);
124
158
  /** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
125
159
  private run;
126
160
  /**
package/dist/typed-sql.js CHANGED
@@ -19,7 +19,9 @@
19
19
  * impossible to string-concatenate a value into the query through this API.
20
20
  * This is the whole point of the tagged-template shape: the literal segments
21
21
  * are frozen by the compiler (`TemplateStringsArray`), and the only way to
22
- * get a runtime value into the query is via `${...}`, which we bind.
22
+ * get a runtime value into the query is via `${...}`, which we bind. A
23
+ * plain array passed where the template belongs is refused at runtime
24
+ * ({@link assertTemplateStrings}).
23
25
  *
24
26
  * 3. **Rows are returned as-is (no snake→camel mapping).** This matches the
25
27
  * existing `client.raw()` behavior: a typed raw query is a literal escape
@@ -49,6 +51,7 @@
49
51
  */
50
52
  import { postgresDialect } from './dialect.js';
51
53
  import { ValidationError, wrapPgError } from './errors.js';
54
+ import { consoleLogger, logTo } from './logger.js';
52
55
  import { rawIdentity, runReportedStatement } from './query-events.js';
53
56
  /**
54
57
  * Build a `(sql, params)` pair from a tagged-template invocation.
@@ -61,11 +64,10 @@ import { rawIdentity, runReportedStatement } from './query-events.js';
61
64
  *
62
65
  * Exported for unit testing the parameterization invariant without a database.
63
66
  */
64
- export function buildTypedSql(strings, values, dialect = postgresDialect) {
65
- // The tagged-template API guarantees `strings.length === values.length + 1`.
66
- // Guard the directly-callable (exported) surface so a mismatched call can't
67
- // silently desync placeholders from params (pg would reject it, but fail
68
- // loudly here instead).
67
+ export function buildTypedSql(strings, values, dialect = postgresDialect, api = 'sql') {
68
+ assertTemplateStrings(strings, api);
69
+ // A tagged template always has `strings.length === values.length + 1`; a
70
+ // hand-built frozen one may not, and would desync placeholders from params.
69
71
  if (strings.length !== values.length + 1) {
70
72
  throw new ValidationError(`sql template segment/value count mismatch: ${strings.length} segments, ${values.length} values.`);
71
73
  }
@@ -78,6 +80,92 @@ export function buildTypedSql(strings, values, dialect = postgresDialect) {
78
80
  }
79
81
  return { sql, params: values.slice() };
80
82
  }
83
+ /**
84
+ * Refuse a "template" that did not come from a tagged-template call.
85
+ *
86
+ * Only the static segments of a template reach the SQL text, and that is safe
87
+ * because the compiler wrote them. A plain array passed in their place (a
88
+ * JSON-parsed request field, a string split by hand) puts its text into the
89
+ * SQL with nothing escaped. A real `TemplateStringsArray` is frozen and carries
90
+ * a frozen `raw` array of the same length; `JSON.parse` and array literals
91
+ * produce neither.
92
+ *
93
+ * `unsafeApi` names the method that takes statement text instead, for the
94
+ * error message.
95
+ */
96
+ export function assertTemplateStrings(strings, api, unsafeApi = 'rawUnsafe') {
97
+ if (Array.isArray(strings) && Object.isFrozen(strings)) {
98
+ const raw = strings.raw;
99
+ if (Array.isArray(raw) && Object.isFrozen(raw) && raw.length === strings.length) {
100
+ if (strings.every((s) => typeof s === 'string'))
101
+ return;
102
+ throw new ValidationError(`${api}: a segment of the template contains an invalid escape sequence, so it has no text. ` +
103
+ 'Fix the escape (write a literal backslash as `\\\\`).');
104
+ }
105
+ }
106
+ const received = Array.isArray(strings) ? 'a plain array' : strings === null ? 'null' : typeof strings;
107
+ throw new ValidationError(`${api} must be called as a tagged template, for example ${api}\`SELECT * FROM users WHERE id = \${id}\`, ` +
108
+ `so that every interpolated value is sent as a bound parameter. It received ${received} instead, whose ` +
109
+ 'text would reach the SQL unescaped. To run statement text built at runtime (DDL, a migration file), use ' +
110
+ `\`${unsafeApi}(text, ...values)\` and keep untrusted input out of the text.`);
111
+ }
112
+ /**
113
+ * A `Date` in the form node-postgres sends under `parseInputDatesAsUTC`:
114
+ * `YYYY-MM-DDTHH:MM:SS.mmm+00:00`, with ` BC` for years before 1.
115
+ */
116
+ function utcDateText(date) {
117
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
118
+ let year = date.getUTCFullYear();
119
+ const bc = year < 1;
120
+ if (bc)
121
+ year = 1 - year;
122
+ return (`${pad(year, 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}` +
123
+ `T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}` +
124
+ `.${pad(date.getUTCMilliseconds(), 3)}+00:00${bc ? ' BC' : ''}`);
125
+ }
126
+ function utcDateValue(value, api, position, nested) {
127
+ if (value instanceof Date) {
128
+ if (Number.isNaN(value.getTime())) {
129
+ throw new ValidationError(`${api}: parameter $${position} ${nested ? 'contains' : 'is'} an Invalid Date, which has no SQL value. ` +
130
+ 'Pass a valid Date, or null for SQL NULL.');
131
+ }
132
+ return utcDateText(value);
133
+ }
134
+ if (!Array.isArray(value))
135
+ return value;
136
+ let out;
137
+ for (let i = 0; i < value.length; i++) {
138
+ const converted = utcDateValue(value[i], api, position, true);
139
+ if (converted !== value[i]) {
140
+ out ??= value.slice();
141
+ out[i] = converted;
142
+ }
143
+ }
144
+ return out ?? value;
145
+ }
146
+ /**
147
+ * Raw-statement params with every `Date` (in arrays too) bound as UTC text,
148
+ * for a client reading zone-less temporal types as UTC (`utcTimestamps`).
149
+ *
150
+ * node-postgres renders a Date in the process's zone with its offset. A
151
+ * `timestamptz` keeps the instant either way, but a `timestamp` or `date`
152
+ * drops the offset and stores the local wall time, which then reads back
153
+ * shifted by the process offset. UTC text stores the instant's UTC fields,
154
+ * which is what a UTC read returns. Returns `params` itself when it holds no
155
+ * Date. An Invalid Date is refused (E003) instead of reaching the server as
156
+ * unparseable text.
157
+ */
158
+ export function utcDateParams(params, api) {
159
+ let out;
160
+ for (let i = 0; i < params.length; i++) {
161
+ const converted = utcDateValue(params[i], api, i + 1, false);
162
+ if (converted !== params[i]) {
163
+ out ??= params.slice();
164
+ out[i] = converted;
165
+ }
166
+ }
167
+ return out ?? params;
168
+ }
81
169
  /**
82
170
  * A pending typed raw SQL query. Implements the thenable contract, so it can be
83
171
  * `await`ed directly to get `T[]`, or refined via `.one()` / `.scalar()` first.
@@ -95,6 +183,7 @@ export class TypedSqlQuery {
95
183
  logging;
96
184
  runScoped;
97
185
  onQuery;
186
+ logger;
98
187
  constructor(pool, sql, params, logging,
99
188
  /**
100
189
  * Runs the execution under the owning client's `errorMessages` mode. Passed
@@ -115,19 +204,22 @@ export class TypedSqlQuery {
115
204
  * with model `'$raw'` and action `'sql'`. Absent for a `TypedSqlQuery`
116
205
  * built outside a client, which then emits nothing, as before.
117
206
  */
118
- onQuery) {
207
+ onQuery,
208
+ /** Where the `logging` preview line goes. Defaults to the console logger. */
209
+ logger = consoleLogger) {
119
210
  this.pool = pool;
120
211
  this.sql = sql;
121
212
  this.params = params;
122
213
  this.logging = logging;
123
214
  this.runScoped = runScoped;
124
215
  this.onQuery = onQuery;
216
+ this.logger = logger;
125
217
  }
126
218
  /** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
127
219
  run() {
128
220
  return this.runScoped(async () => {
129
221
  if (this.logging) {
130
- console.log(`[turbine] Typed SQL: ${this.sql.trim().substring(0, 120)}...`);
222
+ logTo(this.logger, 'debug', `Typed SQL: ${this.sql.trim().substring(0, 120)}...`, { code: 'typed-sql' });
131
223
  }
132
224
  const result = await runReportedStatement(this.onQuery, rawIdentity('sql'), this.sql, this.params, () => this.pool.query(this.sql, this.params), wrapPgError);
133
225
  return result.rows;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.81.0",
3
+ "version": "0.90.0",
4
4
  "description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
@@ -139,9 +139,9 @@
139
139
  "//check:coverage-sync": "The three-way coupling between a coverage collection and the floors that hold it. A file can be added to coverage:cli:collect and then be held by NOTHING, which is not a missing gate but a slower lane enforcing a floor of zero, and it has happened twice (mcp.ts and error-catalog.ts each spent a release that way). The invariant is that every file in the collection is named by a per-file gate OR by the aggregate, NOT that the collection and the aggregate are equal: cli/index.ts is deliberately outside the aggregate, see //coverage:cli:gate:aggregate. It also refuses a gate that names a file nobody collects, which measures nothing and passes, and it fails closed on an empty parse. It runs FIRST in test:coverage:cli, ahead of the collection, because it needs no coverage data and its failure names the missing wiring instead of a number.",
140
140
  "check:coverage-sync": "node scripts/check-coverage-gate-sync.mjs",
141
141
  "//coverage:cli": "The CLI coverage gate, split into ONE collection run plus a per-file threshold check for every file in it, and an aggregate. c8 enforces a single threshold set per invocation and its --per-file applies the SAME numbers to every file, neither of which can express 'destructive.ts holds 100 while migrate.ts holds 70'. An aggregate-only floor lets the least-covered file spend the whole slack the best-covered file earned: at the measured 3623/2951 lines, migrate.ts could fall from 70.3% to 66.6% with the aggregate still green. So each file gets its OWN floor, checked by re-reporting the coverage already on disk (c8 report re-reads ./coverage/tmp, so this costs no extra test run). The aggregate check is kept as well: it catches all three sagging together inside their individual margins. Per-file gates run FIRST because their failure names the file. DATABASE_URL is neutralized on the collection run: these test files include live migration tests that create and drop tables, and this script runs from prepublishOnly.",
142
- "test:coverage:cli": "npm run check:coverage-sync && npm run coverage:cli:collect && npm run coverage:cli:gate:destructive && npm run coverage:cli:gate:sql-statements && npm run coverage:cli:gate:pii-guard && npm run coverage:cli:gate:error-catalog && npm run coverage:cli:gate:mcp && npm run coverage:cli:gate:compile-query && npm run coverage:cli:gate:studio && npm run coverage:cli:gate:migrate && npm run coverage:cli:gate:index && npm run coverage:cli:gate:aggregate",
142
+ "test:coverage:cli": "npm run check:coverage-sync && npm run coverage:cli:collect && npm run coverage:cli:gate:destructive && npm run coverage:cli:gate:sql-statements && npm run coverage:cli:gate:pii-guard && npm run coverage:cli:gate:error-catalog && npm run coverage:cli:gate:mcp && npm run coverage:cli:gate:compile-query && npm run coverage:cli:gate:local-server-guard && npm run coverage:cli:gate:observe && npm run coverage:cli:gate:studio && npm run coverage:cli:gate:migrate && npm run coverage:cli:gate:index && npm run coverage:cli:gate:aggregate",
143
143
  "//coverage:cli:collect": "--check-coverage=false is load-bearing, not tidying. c8 reads .c8rc.json for defaults, so this COLLECTION step was silently enforcing the MAIN gate's global thresholds against a src/cli-only file set. That is a gate nobody wrote and nobody wanted, and it went unnoticed only because the main floor was low enough (75) for the CLI aggregate (89.29) to clear it by accident. When the main floors were re-baselined to 93/89/78 (see //merge-bug in .c8rc.json) it started failing here, several steps before the per-file gates that are supposed to decide. Thresholds for these files belong to the coverage:cli:gate:* scripts, which pass their own explicitly.",
144
- "coverage:cli:collect": "DATABASE_URL= c8 --all --check-coverage=false --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts --include src/cli/mcp.ts --include src/cli/compile-query.ts --include src/cli/error-catalog.ts --include src/cli/index.ts tsx --test src/test/studio-write.test.ts src/test/pii-guard-symmetry.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/tracking-table-race.test.ts src/test/destructive-dynamic-assembly.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts src/test/mcp.test.ts src/test/mcp-relations.test.ts src/test/mcp-pii.test.ts src/test/mcp-pii-round2.test.ts src/test/mcp-agent-tools.test.ts src/test/mcp-compile-query.test.ts src/test/mcp-perimeter-bounds.test.ts src/test/destructive-obfuscation.test.ts src/test/cli-init-schema.test.ts src/test/cli-introspect-options.test.ts src/test/mcp-transport-bounds.test.ts src/test/seed-schema.test.ts src/test/migrate-preview-and-report.test.ts src/test/destructive-quoting-forms.test.ts",
144
+ "coverage:cli:collect": "DATABASE_URL= c8 --all --check-coverage=false --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts --include src/cli/mcp.ts --include src/cli/compile-query.ts --include src/cli/error-catalog.ts --include src/cli/index.ts --include src/cli/local-server-guard.ts --include src/cli/observe.ts tsx --test src/test/studio-write.test.ts src/test/pii-guard-symmetry.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrations-pure.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/tracking-table-race.test.ts src/test/destructive-dynamic-assembly.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts src/test/mcp.test.ts src/test/mcp-relations.test.ts src/test/mcp-pii.test.ts src/test/mcp-pii-round2.test.ts src/test/mcp-agent-tools.test.ts src/test/mcp-compile-query.test.ts src/test/mcp-perimeter-bounds.test.ts src/test/destructive-obfuscation.test.ts src/test/cli-init-schema.test.ts src/test/cli-introspect-options.test.ts src/test/mcp-transport-bounds.test.ts src/test/seed-schema.test.ts src/test/migrate-preview-and-report.test.ts src/test/destructive-quoting-forms.test.ts src/test/codegen-cli-destructive.test.ts src/test/codegen-cli-config.test.ts src/test/security-perimeter-local-servers.test.ts src/test/security-perimeter-mcp-secret-names.test.ts src/test/observe-cli.test.ts",
145
145
  "coverage:cli:gate": "c8 report --all --exclude 'src/test/**' --reporter text --check-coverage",
146
146
  "coverage:cli:gate:destructive": "npm run coverage:cli:gate -- --include src/cli/destructive.ts --lines 98 --statements 98 --branches 84 --functions 98",
147
147
  "coverage:cli:gate:sql-statements": "npm run coverage:cli:gate -- --include src/cli/sql-statements.ts --lines 100 --statements 100 --branches 98 --functions 100",
@@ -153,6 +153,9 @@
153
153
  "coverage:cli:gate:error-catalog": "npm run coverage:cli:gate -- --include src/cli/error-catalog.ts --lines 100 --statements 100 --branches 90 --functions 100",
154
154
  "//coverage:cli:gate:index": "cli/index.ts is the largest file in the repo (5,945 lines) and was in no coverage gate at all. The .c8rc.json //cli-gate argument for the other CLI files applies verbatim: coverage could fall to zero without anything noticing. The floors are LOW on purpose and are not a target: this file is argument parsing and command dispatch whose `process.exit` paths dominate any figure set over it, and the point of the gate is that the number cannot fall silently, not that it is high. MEASURED 2026-08-23 by `npm run coverage:cli:collect` (no database), read from the json-summary reporter rather than the text table, per the //cli-gate-measurements note: 32.62 lines / 32.62 statements / 75.62 branches / 20 functions, identical in isolation and in the nine-file collection. Floors sit about two points under. Raise as coverage improves; never lower one without recording why. It is deliberately NOT in coverage:cli:gate:aggregate, see that key's note.",
155
155
  "coverage:cli:gate:index": "npm run coverage:cli:gate -- --include src/cli/index.ts --lines 30 --statements 30 --branches 73 --functions 18",
156
+ "coverage:cli:gate:local-server-guard": "npm run coverage:cli:gate -- --include src/cli/local-server-guard.ts --lines 100 --statements 100 --branches 96 --functions 100",
157
+ "//coverage:cli:gate:observe": "cli/observe.ts is the read-only metrics dashboard behind `turbine observe`: a loopback HTTP server with its own session token, one-time login ticket, Host allowlist, Origin check, rate limit and CSP, in front of two queries that run in read-only transactions. It was in no coverage gate at all (about 48 percent of lines were executed by the suites that happened to touch it), while Studio, which shares its perimeter, was. Floors MEASURED 2026-10-13 by `npm run coverage:cli:collect`, read from the json-summary reporter: 100 lines / 100 statements / 99.13 branches / 100 functions. THE COLLECTION BLANKS DATABASE_URL, so the figures are the no-database ones: the live-Postgres tests in observe-cli.test.ts skip here and are not counted, and with a database the figure is 99.14 branches and otherwise identical. That is by design: the fake pool and the fake Postgres wire server in that file reach every line without a database. The one uncovered branch is the non-Error arm of the server's 500 handler (`String(err)`), which a real pg pool never produces. Floors sit 2 points under. Held by this per-file gate only and NOT listed in coverage:cli:gate:aggregate, the same arrangement as local-server-guard.ts: that aggregate was baselined over the larger files and adding a 100 percent file would only move it. Raise as coverage improves; never lower one without recording why.",
158
+ "coverage:cli:gate:observe": "npm run coverage:cli:gate -- --include src/cli/observe.ts --lines 98 --statements 98 --branches 97 --functions 98",
156
159
  "coverage:cli:gate:studio": "npm run coverage:cli:gate -- --include src/cli/studio.ts --lines 82 --statements 82 --branches 81 --functions 86",
157
160
  "coverage:cli:gate:migrate": "npm run coverage:cli:gate -- --include src/cli/migrate.ts --lines 69 --statements 69 --branches 91 --functions 73",
158
161
  "//coverage:cli:gate:aggregate": "Re-baselined 2026-08-15 when compile-query.ts joined the --include list: measured 89.28 lines / 89.28 statements / 86.77 branches / 89.18 functions, up from a 87/87/85/86 floor set over seven files (itself up from 78/78/84/82 over five). Raise as coverage improves, never lower one without recording why. WHY cli/index.ts IS NOT HERE, although it is in coverage:cli:collect and has its own per-file gate: it is 5,945 lines at 32.62%, so folding it in drops this aggregate from a measured 89.63 lines / 87.15 branches / 89.18 functions to 66.32 / 85.76 / 63.38, which would mean LOWERING two floors by about 25 points. That is the rule above broken, and it also empties the aggregate of its purpose. This check exists only as the backstop that catches every file sagging together inside its individual margin; with one 5,945-line file at 32% dominating the denominator, the other eight could collectively shed well over a thousand covered lines with this still green. The invariant the collection actually needs is that every collected file is held by SOME floor, and index.ts is held by coverage:cli:gate:index. That is what check:coverage-sync asserts.",
@@ -209,7 +212,7 @@
209
212
  "@size-limit/esbuild": "^12.1.0",
210
213
  "@size-limit/file": "^12.1.0",
211
214
  "@swc/core": "1.16.2",
212
- "@types/node": "^26.1.0",
215
+ "@types/node": "^22.20.4",
213
216
  "@types/pg": "^8.11.11",
214
217
  "@zvndev/powdb-client": "^0.20.0",
215
218
  "@zvndev/powdb-embedded": "^0.20.0",
@@ -101,6 +101,16 @@ Operators: `equals`, `not`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `contains`,
101
101
  `startsWith`, `endsWith`, plus `mode: 'insensitive'`. `AND`, `OR` and `NOT` nest
102
102
  at any depth; `AND` and `OR` take arrays.
103
103
 
104
+ `not` also takes an operator object: `givenName: { not: { contains: 'Roux' } }`.
105
+ To compare two columns, wrap the other one in `col()`, imported from
106
+ `turbine-orm`: `rindScore: { gt: col('aromaScore') }`. A plain `{ col: 'x' }`
107
+ object is refused with `TURBINE_E003`, and so is an operand that does not fit
108
+ its operator, such as an array for `equals` or `null` for `gt`.
109
+
110
+ A key whose value is `undefined` is left out, as in Prisma. When that empties
111
+ the whole `where`, `findFirst` throws `TURBINE_E003` instead of returning an
112
+ arbitrary row.
113
+
104
114
  Values are always bound as parameters, and `contains` / `startsWith` /
105
115
  `endsWith` escape LIKE wildcards for you. There is no code path in the typed API
106
116
  that concatenates a value into SQL.
@@ -160,7 +170,10 @@ them. Passing both spellings of one bound with different values throws.
160
170
  { take: 20, skip: 40 }
161
171
  ```
162
172
 
163
- For deep pages, prefer a `cursor` over a large `offset`.
173
+ For deep pages, prefer a `cursor` over a large `offset`. A cursor names the
174
+ last row you have by a unique key, `cursor: { id: 41 }`; the page excludes that
175
+ row and continues in the query's `orderBy`, with the primary key added as a
176
+ tiebreaker.
164
177
 
165
178
  ## JSON columns
166
179
 
@@ -174,8 +187,11 @@ where: { credentialBlob: { path: ['tier'], equals: 'master' } }
174
187
  A path takes a DIFFERENT operator set from a plain column: `equals`, `gt`,
175
188
  `gte`, `lt`, `lte`, `hasKey`, `contains`, `stringContains`, `stringStartsWith`,
176
189
  `stringEndsWith`, plus `mode` (which applies to the three `string*` operators).
177
- `contains` on a JSON column is containment (`@>`, a whole sub-document such as
178
- `{ contains: { panel: { seats: 4 } } }`), NOT a substring test; the substring
190
+ `equals` at a path compares JSON values, so `equals: null` matches a JSON
191
+ `null` and not the string `"null"`. Without a `path`, `equals` compares the
192
+ whole document exactly. `contains` on a JSON column is containment (`@>`, a
193
+ whole sub-document such as `{ contains: { panel: { seats: 4 } } }`), NOT a
194
+ substring test; the substring
179
195
  operators on a path are `stringContains`, `stringStartsWith` and
180
196
  `stringEndsWith`. `not`, `in`, `notIn`, `startsWith` and `endsWith` are refused
181
197
  with `TURBINE_E003` listing the accepted set (the last two point at their
@@ -189,8 +205,9 @@ comes back as `_sum.score`. `_sum` / `_avg` cast the value to numeric; `_min` /
189
205
  `_max` compare as text unless `type: 'numeric'`. **`aggregate()` has no JSON-path
190
206
  form**: every key there must be a column (anything else is `TURBINE_E003`) and
191
207
  every value `true`; an object value is read as `true`, so the whole column
192
- reaches the database as `avg(jsonb)` and fails there with a database error, not
193
- a Turbine code. **`_count` never takes a path**, in `groupBy` or `aggregate()`:
208
+ reaches the database as `avg(jsonb)`, which the database refuses. That surfaces
209
+ as `TURBINE_E003` with `sqlstate` `42883`, not as a Turbine check naming the
210
+ path. **`_count` never takes a path**, in `groupBy` or `aggregate()`:
194
211
  an object under a `_count` key is read as `true` (a count of that column's
195
212
  non-null values) and any path in it is ignored. To count per JSON value, group
196
213
  by the path and read `_count`.
@@ -211,6 +228,11 @@ PostgreSQL's exact text as a **string** (every value, not only large ones; `AVG`
211
228
  is not float-cast there); over `int4` / `float` columns and over a JSON path they
212
229
  return a number. `_min` / `_max` return the column's own type.
213
230
 
231
+ `_percentile: { aromaScore: { p: 0.9 } }` returns the 90th percentile, on
232
+ PostgreSQL only. `p` may be an array of fractions, and the result is then an
233
+ array in the same order. The default `method: 'cont'` interpolates and is
234
+ refused on a date column with `TURBINE_E003`; use `method: 'disc'` there.
235
+
214
236
  ## groupBy and having
215
237
 
216
238
  `by` lists the grouping columns. The `having` shape is **column first, aggregate
@@ -247,7 +269,35 @@ the `ON CONFLICT` target when `create` carries the same values, so a non-unique
247
269
  predicate is refused rather than sent. When `create` does not carry them (a
248
270
  `create` that leaves `id` to its sequence, say), `upsert` looks the row up by
249
271
  `where` first, inside a transaction. Use `updateMany` and `deleteMany` for
250
- "every row matching a filter".
272
+ "every row matching a filter". They take no `limit`, `take`, `offset` or `skip`
273
+ (`TURBINE_E003`): to write a bounded batch, `findMany` the ids first, then pass
274
+ `where: { id: { in: ids } }`.
275
+
276
+ ## Computed updates and bulk upserts
277
+
278
+ To set a column from the row's own values, build the value with `expr` and
279
+ `col()`, both imported from `turbine-orm`:
280
+
281
+ ```ts
282
+ await db.cheeseWheels.update({
283
+ where: { id: 7 },
284
+ data: { wheelCount: expr.add(col('wheelCount'), 1) },
285
+ });
286
+ ```
287
+
288
+ A plain object shaped like an expression (`{ kind: 'add', args: [1, 2] }`) is
289
+ refused with `TURBINE_E003`, and so is an expression in `create` data.
290
+
291
+ `createMany` with `onConflict: { target: ['guildId', 'batchRef'], update:
292
+ ['status'] }` inserts the new keys and updates the rows that already exist. The
293
+ target must be the primary key or one whole unique key; any other column list
294
+ is refused with `TURBINE_E003`.
295
+
296
+ ## Locking rows
297
+
298
+ `lock: 'update'` on `findMany`, `findFirst` or `findUnique` locks the rows it
299
+ returns until the transaction ends. It must run on the `tx` inside
300
+ `db.$transaction(async (tx) => ...)`; on `db` it throws `TURBINE_E003`.
251
301
 
252
302
  ## `distinct`
253
303