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
@@ -10,6 +10,8 @@
10
10
  * Output goes to the specified directory (default: ./generated/turbine/).
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.columnTsType = columnTsType;
14
+ exports.resolveColumnTypeOverrides = resolveColumnTypeOverrides;
13
15
  exports.generate = generate;
14
16
  exports.resolveImportExtension = resolveImportExtension;
15
17
  exports.detectTsconfigExtension = detectTsconfigExtension;
@@ -26,18 +28,28 @@ const node_fs_1 = require("node:fs");
26
28
  const node_path_1 = require("node:path");
27
29
  const errors_js_1 = require("./errors.js");
28
30
  const introspect_js_1 = require("./introspect.js");
31
+ const logger_js_1 = require("./logger.js");
32
+ const utils_js_1 = require("./query/utils.js");
29
33
  const schema_js_1 = require("./schema.js");
34
+ const schema_builder_js_1 = require("./schema-builder.js");
30
35
  /** Get the TypeScript type name for a table (singularized PascalCase) */
31
36
  function entityName(tableName) {
32
37
  return (0, schema_js_1.snakeToPascal)((0, schema_js_1.singularize)(tableName));
33
38
  }
39
+ /** The override for one column, or `undefined`. */
40
+ function overrideFor(overrides, table, column) {
41
+ return overrides?.get(table)?.get(column);
42
+ }
34
43
  /**
35
44
  * Resolve the TypeScript type for a column, mapping enum-typed columns to their
36
45
  * generated string-literal union (PascalCase enum name) instead of the
37
- * `unknown` that {@link pgTypeToTs} yields for user-defined types. Falls back to
38
- * the introspected `col.tsType` for every non-enum column.
46
+ * `unknown` that {@link pgTypeToTs} yields for user-defined types. A
47
+ * `columnTypes` override replaces the type outright (nullability is still
48
+ * added). Falls back to the introspected `col.tsType` for everything else.
39
49
  */
40
- function columnTsType(col, enums) {
50
+ function columnTsType(col, enums, override) {
51
+ if (override !== undefined)
52
+ return col.nullable ? `${override} | null` : override;
41
53
  const dt = col.dialectType ?? col.pgType;
42
54
  const isArray = col.isArray || dt.startsWith('_');
43
55
  const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
@@ -51,27 +63,78 @@ function columnTsType(col, enums) {
51
63
  }
52
64
  /**
53
65
  * Resolve the TypeScript type a column accepts on the WRITE path (the `*Create`
54
- * / `*Update` input types), which is wider than the row type for time-of-day
55
- * columns.
66
+ * / `*Update` input types). It differs from the row type where the runtime
67
+ * accepts more than one spelling or cannot take the parsed read value back
68
+ * ({@link pgTypeToTsInput}), and for time-of-day columns.
56
69
  *
57
70
  * A `time` / `timetz` column reads back as a `string` (`'09:00:00'`), so the
58
71
  * row type stays `string`. On write the runtime also accepts a JS `Date` and
59
72
  * narrows it to its UTC time of day (see `coerceWriteValue` in
60
73
  * query/writes.ts), matching Prisma, which types a `DateTime @db.Time(6)`
61
- * field as a `Date`. Consumers porting from Prisma pass Dates, so the input
62
- * type has to admit them. Every other column is unchanged.
74
+ * field as a `Date`.
63
75
  */
64
- function writeColumnTsType(col, enums) {
65
- const tsType = columnTsType(col, enums);
76
+ function writeColumnTsType(col, enums, override) {
77
+ const tsType = columnTsType(col, enums, override);
78
+ if (override !== undefined)
79
+ return tsType;
66
80
  const dt = col.dialectType ?? col.pgType;
67
81
  const isArray = col.isArray || dt.startsWith('_');
68
82
  const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
69
- if (!(0, schema_js_1.timeOfDayKind)(base))
83
+ if (Object.hasOwn(enums, base))
70
84
  return tsType;
71
- // A `time[]` column accepts a Date per element (the bind rewrite runs
72
- // element-wise, see coerceTemporalValue in query/utils.ts).
73
- const widened = isArray ? '(string | Date)[]' : 'string | Date';
74
- return col.nullable ? `${widened} | null` : widened;
85
+ if ((0, schema_js_1.timeOfDayKind)(base)) {
86
+ // A `time[]` column accepts a Date per element (the bind rewrite runs
87
+ // element-wise, see coerceTemporalValue in query/utils.ts).
88
+ const widened = isArray ? '(string | Date)[]' : 'string | Date';
89
+ return col.nullable ? `${widened} | null` : widened;
90
+ }
91
+ // Only rewrite a type the read map produced: hand-built or engine metadata
92
+ // carrying its own tsType keeps it.
93
+ const pgType = isArray && !dt.startsWith('_') ? `_${dt}` : dt;
94
+ const read = (0, schema_js_1.pgTypeToTs)(pgType, col.nullable);
95
+ const input = (0, schema_js_1.pgTypeToTsInput)(pgType, col.nullable);
96
+ return input !== read && tsType === read ? input : tsType;
97
+ }
98
+ /**
99
+ * Validate {@link GenerateOptions.columnTypes} against the schema and index it.
100
+ * A key naming no table or column is a `ValidationError`, not a silent no-op,
101
+ * since an override that applies to nothing leaves the column typed `unknown`.
102
+ */
103
+ function resolveColumnTypeOverrides(schema, columnTypes) {
104
+ if (columnTypes === undefined)
105
+ return undefined;
106
+ const out = new Map();
107
+ for (const [key, tsType] of Object.entries(columnTypes)) {
108
+ const dot = key.lastIndexOf('.');
109
+ const tableName = dot > 0 ? key.slice(0, dot) : '';
110
+ const columnName = dot > 0 ? key.slice(dot + 1) : '';
111
+ if (!tableName || !columnName) {
112
+ throw new errors_js_1.ValidationError(`columnTypes key ${(0, introspect_js_1.printableCatalogName)(JSON.stringify(key))} must be "<table>.<column>", using database names, ` +
113
+ 'for example "users.settings".');
114
+ }
115
+ const table = Object.hasOwn(schema.tables, tableName) ? schema.tables[tableName] : undefined;
116
+ if (!table) {
117
+ const hint = (0, utils_js_1.closestName)(tableName, Object.keys(schema.tables));
118
+ throw new errors_js_1.ValidationError(`columnTypes key ${(0, introspect_js_1.printableCatalogName)(JSON.stringify(key))} names no table${hint ? `. Did you mean "${(0, introspect_js_1.printableCatalogName)(hint)}"?` : '.'}`);
119
+ }
120
+ if (!table.columns.some((c) => c.name === columnName)) {
121
+ const hint = (0, utils_js_1.closestName)(columnName, table.columns.map((c) => c.name));
122
+ throw new errors_js_1.ValidationError(`columnTypes key ${(0, introspect_js_1.printableCatalogName)(JSON.stringify(key))} names no column of "${(0, introspect_js_1.printableCatalogName)(tableName)}"` +
123
+ `${hint ? `. Did you mean "${(0, introspect_js_1.printableCatalogName)(`${tableName}.${hint}`)}"?` : '.'}`);
124
+ }
125
+ const problem = typeof tsType === 'string' ? (0, schema_builder_js_1.tsTypeTextProblem)(tsType) : 'it is not a string';
126
+ if (problem !== null) {
127
+ throw new errors_js_1.ValidationError(`columnTypes[${(0, introspect_js_1.printableCatalogName)(JSON.stringify(key))}] must be one TypeScript type on one line, for example ` +
128
+ `"import('../src/types.js').Settings", but ${problem}: ${(0, introspect_js_1.printableCatalogName)(JSON.stringify(tsType))}.`);
129
+ }
130
+ let perTable = out.get(tableName);
131
+ if (!perTable) {
132
+ perTable = new Map();
133
+ out.set(tableName, perTable);
134
+ }
135
+ perTable.set(columnName, tsType.trim());
136
+ }
137
+ return out;
75
138
  }
76
139
  /**
77
140
  * Characters that must never reach generated source verbatim: the two that end
@@ -145,7 +208,13 @@ function generate(options) {
145
208
  // F3: resolve which extension sibling imports get in index.ts.
146
209
  const { ext: importExt, mode: importMode } = resolveImportExtension(outDir, options.importExtension ?? 'auto');
147
210
  const files = [];
148
- const fileOptions = { noTimestamp: options.noTimestamp, importExt, importMode };
211
+ const fileOptions = {
212
+ noTimestamp: options.noTimestamp,
213
+ importExt,
214
+ importMode,
215
+ ...(options.columnTypes !== undefined ? { columnTypes: options.columnTypes } : {}),
216
+ ...(options.logger !== undefined ? { logger: options.logger } : {}),
217
+ };
149
218
  // Generate types.ts
150
219
  const typesContent = generateTypes(schema, fileOptions);
151
220
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'types.ts'), typesContent, 'utf-8');
@@ -175,6 +244,7 @@ function generate(options) {
175
244
  * mode label for the generated comment.
176
245
  *
177
246
  * - `'js'` → `.js`
247
+ * - `'ts'` → `.ts`
178
248
  * - `'none'` → `''`
179
249
  * - `'auto'` → tsconfig-driven ({@link detectTsconfigExtension}); falls back
180
250
  * to `.js` (the pre-0.41 default) when detection is uncertain, so NodeNext
@@ -183,6 +253,8 @@ function generate(options) {
183
253
  function resolveImportExtension(outDir, requested) {
184
254
  if (requested === 'js')
185
255
  return { ext: '.js', mode: 'js' };
256
+ if (requested === 'ts')
257
+ return { ext: '.ts', mode: 'ts' };
186
258
  if (requested === 'none')
187
259
  return { ext: '', mode: 'none' };
188
260
  const detected = detectTsconfigExtension((0, node_path_1.resolve)(outDir));
@@ -335,14 +407,14 @@ function generatedFileHeader(options) {
335
407
  * skip those relations here with a warning instead of emitting broken types.
336
408
  * The runtime metadata (metadata.ts) still carries every relation.
337
409
  */
338
- function typeSafeRelations(table, warn = true) {
410
+ function typeSafeRelations(table, logger) {
339
411
  const columnFields = new Set(table.columns.map((c) => c.field));
340
412
  const usable = [];
341
413
  for (const [relName, rel] of Object.entries(table.relations)) {
342
414
  if (columnFields.has(relName)) {
343
- if (warn) {
344
- console.warn(`[turbine] Relation "${relName}" on table "${table.name}" shadows a column field of the same name, ` +
345
- `omitting it from the generated types. Rename the relation (or the column) to expose it.`);
415
+ if (logger) {
416
+ (0, logger_js_1.logTo)(logger, 'warn', (0, introspect_js_1.printableCatalogName)(`Relation "${relName}" on table "${table.name}" shadows a column field of the same name, ` +
417
+ 'omitting it from the generated types. Rename the relation (or the column) to expose it.'), { code: 'generate-relation-shadows-column', table: table.name, relation: relName });
346
418
  }
347
419
  continue;
348
420
  }
@@ -378,10 +450,42 @@ function isEmittableIdentifier(name) {
378
450
  function requireEmittable(derived, subject, role) {
379
451
  if (isEmittableIdentifier(derived))
380
452
  return;
381
- throw new errors_js_1.ValidationError(`Cannot generate code for ${subject}: it produces ${JSON.stringify(derived)} as ${role}, ` +
453
+ throw new errors_js_1.ValidationError(`Cannot generate code for ${(0, introspect_js_1.printableCatalogName)(subject)}: it produces ${(0, introspect_js_1.printableCatalogName)(JSON.stringify(derived))} as ${role}, ` +
382
454
  `which is not a valid TypeScript identifier. Rename the database object, or exclude it from generation ` +
383
455
  `(introspect \`exclude\`).`);
384
456
  }
457
+ /** Table accessors the generated `TurbineClient` class cannot declare as members. */
458
+ const RESERVED_ACCESSORS = new Set(['constructor', '__proto__']);
459
+ /** The type a defineSchema file declared for a column (bare, no `| null`), if any. */
460
+ function declaredTsTypeOf(col) {
461
+ return col.declaredTsType;
462
+ }
463
+ /**
464
+ * The metadata `types.ts` is written from. A column whose schema file declared a
465
+ * TypeScript type takes it, with `| null` for a nullable column, in place of the
466
+ * type mapped from its database type. The runtime keeps the mapped type
467
+ * (`metadata.ts` is written from the input), because the engines branch on it.
468
+ * Returns `schema` itself when no column declared one.
469
+ */
470
+ function withDeclaredTypes(schema) {
471
+ let declaredAny = false;
472
+ const tables = Object.create(null);
473
+ for (const [name, table] of Object.entries(schema.tables)) {
474
+ if (!table.columns.some((c) => declaredTsTypeOf(c) !== undefined)) {
475
+ tables[name] = table;
476
+ continue;
477
+ }
478
+ declaredAny = true;
479
+ tables[name] = {
480
+ ...table,
481
+ columns: table.columns.map((col) => {
482
+ const declared = declaredTsTypeOf(col);
483
+ return declared === undefined ? col : { ...col, tsType: col.nullable ? `${declared} | null` : declared };
484
+ }),
485
+ };
486
+ }
487
+ return declaredAny ? { ...schema, tables } : schema;
488
+ }
385
489
  /**
386
490
  * Refuse a schema whose names cannot be emitted as TypeScript identifiers.
387
491
  *
@@ -403,11 +507,29 @@ function assertEmittableSchema(schema) {
403
507
  for (const table of Object.values(schema.tables)) {
404
508
  requireEmittable(entityName(table.name), `table "${table.name}"`, 'the generated entity type name');
405
509
  requireEmittable(snakeToCamelStr(table.name), `table "${table.name}"`, 'the generated client accessor');
510
+ if (RESERVED_ACCESSORS.has(snakeToCamelStr(table.name))) {
511
+ throw new errors_js_1.ValidationError(`Cannot generate code for table "${(0, introspect_js_1.printableCatalogName)(table.name)}": its client accessor ` +
512
+ `"${snakeToCamelStr(table.name)}" is a reserved class member name, so the generated class could not declare it. ` +
513
+ 'Rename the table, or exclude it from generation (introspect `exclude`).');
514
+ }
406
515
  // Two columns on one field would emit a duplicate interface member (TS2300
407
516
  // in the consumer's build) and a `columnMap` missing a column. The rule is
408
517
  // introspect.ts's; re-asserting it here covers a hand-built or
409
518
  // engine-introspected schema that never went through that path.
410
519
  (0, introspect_js_1.assertDistinctColumnFields)(table.name, table.columns);
520
+ // A column's type is emitted verbatim. Introspection only produces mapped
521
+ // types, but hand-built or code-first metadata (`tsType` on defineSchema)
522
+ // can carry anything, and a line break or a stray bracket would end the type
523
+ // and start a statement in the generated file.
524
+ for (const col of table.columns) {
525
+ for (const text of [col.tsType, declaredTsTypeOf(col)]) {
526
+ const problem = text === undefined ? null : (0, schema_builder_js_1.tsTypeTextProblem)(text);
527
+ if (problem !== null) {
528
+ throw new errors_js_1.ValidationError(`Cannot generate code for column "${(0, introspect_js_1.printableCatalogName)(col.name)}" on table "${(0, introspect_js_1.printableCatalogName)(table.name)}": ` +
529
+ `its TypeScript type ${(0, introspect_js_1.printableCatalogName)(JSON.stringify(text))} cannot be emitted, because ${problem}. Give one type on one line.`);
530
+ }
531
+ }
532
+ }
411
533
  // Only the relations that reach the TYPE layer: a relation shadowing a
412
534
  // column field is already dropped from types.ts, so refusing on its name
413
535
  // would refuse a schema that generates fine.
@@ -419,38 +541,33 @@ function assertEmittableSchema(schema) {
419
541
  }
420
542
  /**
421
543
  * Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
422
- * and *Relations brand-field interfaces). Exported so tests can pin the
423
- * generator output without writing files to disk.
544
+ * *Relations brand-field interfaces, and the write input types). Exported so
545
+ * tests can pin the generator output without writing files to disk.
424
546
  */
425
- function generateTypes(schema, options) {
426
- assertEmittableSchema(schema);
547
+ function generateTypes(input, options) {
548
+ assertEmittableSchema(input);
549
+ const schema = withDeclaredTypes(input);
550
+ const overrides = resolveColumnTypeOverrides(schema, options?.columnTypes);
427
551
  const lines = [...generatedFileHeader(options)];
428
- // We import UpdateOperatorInput so generated *Update types can express
429
- // atomic increment / decrement / multiply / divide / set operators on
430
- // numeric columns (TASK-3.4).
431
- //
432
- // RelationDescriptor is the brand-field interface that lets `WithResult`
433
- // recurse through nested `with` clauses at any depth. The generator emits
434
- // each `*Relations` member as a `RelationDescriptor<Target, Cardinality,
435
- // TargetRelations>` so users get full deep `with`-clause type inference
436
- // out of the box (TASK-2.1).
437
- lines.push("import type { RelationDescriptor, UpdateOperatorInput } from 'turbine-orm';");
552
+ // UpdateOperatorInput, SqlExpr and ColumnRef let the generated *Update
553
+ // types express atomic operators, `expr.*` update expressions and `col()`.
554
+ // RelationDescriptor is the brand that lets `WithResult` recurse through
555
+ // nested `with` clauses at any depth.
556
+ lines.push("import type { ColumnRef, RelationDescriptor, SqlExpr, UpdateOperatorInput } from 'turbine-orm';");
438
557
  lines.push('');
439
- // Pre-compute which tables have relations so we know whether to thread
440
- // `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
441
- // default) into each `RelationDescriptor`. Built once up-front because
442
- // relations can point at tables we haven't iterated to yet.
443
558
  // Relations that can be surfaced in the type layer, computed once per table
444
559
  // (relations that would shadow a scalar column field are excluded + warned).
445
560
  const safeRelationsByTable = new Map();
446
561
  for (const t of Object.values(schema.tables)) {
447
- safeRelationsByTable.set(t.name, typeSafeRelations(t));
562
+ safeRelationsByTable.set(t.name, typeSafeRelations(t, options?.logger ?? logger_js_1.consoleLogger));
448
563
  }
449
564
  const tablesWithRelations = new Set();
450
565
  for (const t of Object.values(schema.tables)) {
451
566
  if ((safeRelationsByTable.get(t.name) ?? []).length > 0)
452
567
  tablesWithRelations.add(t.name);
453
568
  }
569
+ const rowType = (table, col) => columnTsType(col, schema.enums, overrideFor(overrides, table.name, col.name));
570
+ const writeType = (table, col) => writeColumnTsType(col, schema.enums, overrideFor(overrides, table.name, col.name));
454
571
  // Generate enum types
455
572
  for (const [enumName, labels] of Object.entries(schema.enums)) {
456
573
  const typeName = (0, schema_js_1.snakeToPascal)(enumName);
@@ -468,62 +585,57 @@ function generateTypes(schema, options) {
468
585
  const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
469
586
  const nullNote = col.nullable ? ' (nullable)' : '';
470
587
  // PII columns are excluded from default projections, so the field is
471
- // absent unless the query names it in `select` or passes `includePii`.
472
- // The emitted type marks it optional so it tells the truth about absence.
473
- const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
588
+ // absent unless the read passes `includePii: UNSAFE` (a `select` naming
589
+ // it without the flag throws, and a write never returns it). The emitted
590
+ // type marks it optional so it tells the truth about absence.
591
+ const piiNote = col.pii ? ' (PII: absent unless the read passes includePii: UNSAFE)' : '';
474
592
  const optional = col.pii ? '?' : '';
475
593
  lines.push(` /** Column: ${docSafe(col.name)}, ${docSafe(col.pgType)}${pkNote}${nullNote}${piiNote} */`);
476
- lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${columnTsType(col, schema.enums)};`);
594
+ lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${rowType(table, col)};`);
477
595
  }
478
596
  lines.push('}');
479
597
  lines.push('');
480
598
  // --- Create input type ---
481
599
  // Optional: server-generated columns (serial / identity), columns with a
482
600
  // default, and nullable columns (default to NULL). Everything else is
483
- // required, PRIMARY KEY MEMBERSHIP INCLUDED. A text natural key, a
601
+ // required, PRIMARY KEY MEMBERSHIP INCLUDED: a text natural key, a
484
602
  // client-supplied uuid with no default, or a composite key of plain
485
- // integers must be supplied, and the database rejects the row otherwise
486
- // (E010). Marking every PK column optional let `create({ data: {} })` on
487
- // a junction table typecheck and fail at runtime, which is the opposite of
488
- // what a generated type is for.
603
+ // integers must be supplied, or the database rejects the row (E010).
489
604
  lines.push(`/** Input type for creating a row in \`${docSafe(table.name)}\` */`);
490
605
  lines.push(`export type ${typeName}Create = {`);
491
606
  for (const col of table.columns) {
492
607
  // STORED generated columns are computed by the database, never writable.
493
608
  if (col.isGeneratedStored)
494
609
  continue;
495
- const isGenerated = col.isGenerated === true;
496
- const isOptional = isGenerated || col.hasDefault || col.nullable;
497
- if (isOptional) {
498
- const reason = isGenerated ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
610
+ if (isOptionalOnCreate(col)) {
611
+ const reason = col.isGenerated === true ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
499
612
  lines.push(` /** Optional: ${reason} */`);
500
- lines.push(` ${quoteIfNeeded(col.field)}?: ${writeColumnTsType(col, schema.enums)};`);
613
+ lines.push(` ${quoteIfNeeded(col.field)}?: ${writeType(table, col)} | undefined;`);
501
614
  }
502
615
  else {
503
- lines.push(` ${quoteIfNeeded(col.field)}: ${writeColumnTsType(col, schema.enums)};`);
616
+ lines.push(` ${quoteIfNeeded(col.field)}: ${writeType(table, col)};`);
504
617
  }
505
618
  }
506
619
  lines.push('};');
507
620
  lines.push('');
508
621
  // --- Update input type (all fields optional except PK) ---
509
- // Numeric columns additionally accept `UpdateOperatorInput<number>` so
510
- // users can write `{ viewCount: { increment: 1 } }` without an `as any`.
622
+ // Every field also takes `{ set: value }`, and numeric fields the atomic
623
+ // operators (`{ viewCount: { increment: 1 } }`).
511
624
  const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
512
625
  lines.push(`/** Input type for updating a row in \`${docSafe(table.name)}\` */`);
513
626
  lines.push(`export type ${typeName}Update = {`);
514
627
  for (const col of nonPkCols) {
515
- lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(writeColumnTsType(col, schema.enums))};`);
628
+ lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(writeType(table, col))} | undefined;`);
516
629
  }
517
630
  lines.push('};');
518
631
  lines.push('');
519
632
  // --- Relations map (for type-safe `with` clauses) ---
520
633
  //
521
- // Each relation is emitted as a `RelationDescriptor<Target, Cardinality,
522
- // TargetRelations>` brand-field interface. This is what enables the
523
- // recursive `WithResult` type to walk through nested `with` clauses at
524
- // any depth, `RelationRelations<R[K]>` reads the third type parameter
525
- // and threads it into the next recursion step. If the target table has
526
- // no relations of its own, the descriptor uses `{}` (the default).
634
+ // Each relation is a `RelationDescriptor<Target, Cardinality,
635
+ // TargetRelations, Kind, Nullable>`. `WithResult` reads the third
636
+ // parameter to recurse into nested `with` clauses and the fifth to type a
637
+ // to-one relation whose foreign key is NOT NULL as never null; the write
638
+ // input types read the kind.
527
639
  const safeRelations = safeRelationsByTable.get(table.name) ?? [];
528
640
  const hasRelations = safeRelations.length > 0;
529
641
  if (hasRelations) {
@@ -532,168 +644,367 @@ function generateTypes(schema, options) {
532
644
  for (const [relName, rel] of safeRelations) {
533
645
  const targetType = entityName(rel.to);
534
646
  // manyToMany is a collection too → 'many' cardinality (same as hasMany).
535
- const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
647
+ const cardinality = isToMany(rel) ? "'many'" : "'one'";
536
648
  const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
537
- lines.push(` ${quoteIfNeeded(relName)}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
649
+ const notNull = isRequiredBelongsTo(table, rel) ? ', false' : '';
650
+ lines.push(` ${quoteIfNeeded(relName)}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}, '${escSQ(rel.type)}'${notNull}>;`);
538
651
  }
539
652
  lines.push('}');
540
653
  lines.push('');
541
654
  // --- Legacy per-relation interfaces (kept for backward compatibility) ---
542
655
  for (const [relName, rel] of safeRelations) {
543
656
  const targetType = entityName(rel.to);
544
- if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
545
- lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
546
- lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
547
- lines.push(` ${quoteIfNeeded(relName)}: ${targetType}[];`);
548
- lines.push('}');
549
- }
550
- else {
551
- lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
552
- lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
553
- lines.push(` ${quoteIfNeeded(relName)}: ${targetType} | null;`);
554
- lines.push('}');
555
- }
657
+ const loaded = isToMany(rel)
658
+ ? `${targetType}[]`
659
+ : isRequiredBelongsTo(table, rel)
660
+ ? targetType
661
+ : `${targetType} | null`;
662
+ lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
663
+ lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
664
+ lines.push(` ${quoteIfNeeded(relName)}: ${loaded};`);
665
+ lines.push('}');
556
666
  lines.push('');
557
667
  }
558
668
  }
559
669
  }
560
670
  // ---------------------------------------------------------------------------
561
- // Nested write types (WhereUnique, NestedCreateInput, NestedUpdateInput,
562
- // ConnectOrCreate, CreateInput, UpdateInput)
671
+ // Write input types: WhereUnique, CreateInput, UpdateInput, the nested
672
+ // relation write inputs, and the per-table Inputs map the client accessor
673
+ // takes.
563
674
  // ---------------------------------------------------------------------------
564
675
  for (const table of Object.values(schema.tables)) {
565
- const typeName = entityName(table.name);
566
- const safeRelations = safeRelationsByTable.get(table.name) ?? [];
567
- const hasRels = safeRelations.length > 0;
568
- // WhereUnique, union of unique constraint shapes, deduplicating PK
569
- const seen = new Set();
570
- const uniqueSets = [];
571
- // Always include the primary key first
572
- const pkKey = table.primaryKey.join(',');
573
- seen.add(pkKey);
574
- uniqueSets.push(table.primaryKey);
575
- // Add unique indexes that aren't duplicates of the PK
576
- for (const uc of table.uniqueColumns) {
577
- const ucKey = uc.join(',');
578
- if (!seen.has(ucKey)) {
579
- seen.add(ucKey);
580
- uniqueSets.push(uc);
581
- }
676
+ emitWhereUnique(lines, table, (col) => rowType(table, { ...col, nullable: false, tsType: stripNull(col.tsType) }));
677
+ }
678
+ const variantsByTable = createVariantsByTable(schema, safeRelationsByTable);
679
+ for (const table of Object.values(schema.tables)) {
680
+ emitWriteInputs(lines, schema, table, safeRelationsByTable.get(table.name) ?? [], variantsByTable, (col) => writeType(table, col));
681
+ }
682
+ return lines.join('\n');
683
+ }
684
+ /** A to-many relation (an array on the row). */
685
+ function isToMany(rel) {
686
+ return rel.type === 'hasMany' || rel.type === 'manyToMany';
687
+ }
688
+ /** Whether a column may be omitted on create. */
689
+ function isOptionalOnCreate(col) {
690
+ return col.isGenerated === true || col.hasDefault || col.nullable;
691
+ }
692
+ /** `T | null` -> `T`. */
693
+ function stripNull(tsType) {
694
+ return tsType.replace(/\s*\|\s*null$/, '');
695
+ }
696
+ /** The foreign-key columns of a relation, as columns of `table` (the side that holds them). */
697
+ function fkColumns(table, rel) {
698
+ const out = [];
699
+ for (const name of Array.isArray(rel.foreignKey) ? rel.foreignKey : [rel.foreignKey]) {
700
+ const col = table.columns.find((c) => c.name === name);
701
+ if (col)
702
+ out.push(col);
703
+ }
704
+ return out;
705
+ }
706
+ /**
707
+ * A belongsTo relation whose foreign key is NOT NULL, so a row always points
708
+ * at a target row and reading the relation never yields null (absent a
709
+ * per-relation `where` or a global filter on the target, see
710
+ * `RelationDescriptor`).
711
+ */
712
+ function isRequiredBelongsTo(table, rel) {
713
+ if (rel.type !== 'belongsTo')
714
+ return false;
715
+ const fks = fkColumns(table, rel);
716
+ return fks.length > 0 && fks.length === normalizeFk(rel).length && fks.every((c) => !c.nullable);
717
+ }
718
+ function normalizeFk(rel) {
719
+ return Array.isArray(rel.foreignKey) ? rel.foreignKey : [rel.foreignKey];
720
+ }
721
+ /**
722
+ * The keys a nested create through `rel` (a hasMany / hasOne on `parent`) must
723
+ * NOT require from the child: the child's foreign-key fields, which the
724
+ * runtime fills from the parent row, and the child's matching belongsTo
725
+ * relation, which would point the row at a second parent.
726
+ */
727
+ function suppliedByParent(schema, rel) {
728
+ if (rel.type !== 'hasMany' && rel.type !== 'hasOne')
729
+ return [];
730
+ const child = Object.hasOwn(schema.tables, rel.to) ? schema.tables[rel.to] : undefined;
731
+ if (!child)
732
+ return [];
733
+ const fks = normalizeFk(rel);
734
+ const keys = fkColumns(child, rel).map((c) => c.field);
735
+ const fkKey = [...fks].sort().join(',');
736
+ for (const [name, back] of Object.entries(child.relations)) {
737
+ if (back.type === 'belongsTo' && back.to === rel.from && [...normalizeFk(back)].sort().join(',') === fkKey) {
738
+ keys.push(name);
582
739
  }
583
- if (uniqueSets.length > 0) {
584
- const memberType = (colName) => {
585
- const col = table.columns.find((c) => c.name === colName);
586
- return { field: col?.field ?? colName, tsType: col?.tsType ?? 'unknown' };
587
- };
588
- // Flat branches: one object per unique constraint carrying its columns.
589
- const flatBranches = uniqueSets.map((cols) => {
590
- const fields = cols.map((colName) => {
591
- const m = memberType(colName);
592
- return `${quoteIfNeeded(m.field)}: ${m.tsType}`;
593
- });
594
- return `{ ${fields.join('; ')} }`;
595
- });
596
- // Prisma-style compound-unique SELECTOR branches: a synthetic key
597
- // (`orgId_userId`) whose value holds the member columns, emitted for every
598
- // COMPOSITE unique (PK, composite UNIQUE constraint, or composite UNIQUE
599
- // index). Runtime expansion lives in query/compound-unique.ts.
600
- const compoundSeen = new Set();
601
- const compoundSets = [];
602
- const addCompound = (cols) => {
603
- if (cols.length < 2)
604
- return;
605
- const key = cols.join(',');
606
- if (compoundSeen.has(key))
607
- return;
608
- compoundSeen.add(key);
609
- compoundSets.push(cols);
610
- };
611
- addCompound(table.primaryKey);
612
- for (const uc of table.uniqueColumns)
613
- addCompound(uc);
614
- for (const idx of table.indexes) {
615
- // A partial unique index does not guarantee table-wide row uniqueness,
616
- // so it must not become a compound-unique selector (matches the runtime
617
- // exclusion in query/compound-unique.ts).
618
- if (idx.unique && !idx.docPath && !idx.partial)
619
- addCompound(idx.columns);
620
- }
621
- const selectorEntries = compoundSets.map((cols) => {
622
- const members = cols.map(memberType);
623
- return {
624
- // The selector NAME is the underscore-join of the member FIELD names.
625
- // It is normally a valid identifier (`orgId_userId`), but a
626
- // junction-style column that is not a valid identifier (e.g. a quoted
627
- // uppercase `"A"` / `"B"`) would join into a broken object key. Emit
628
- // any non-identifier name as ONE quoted string-literal key so the
629
- // generated types.ts always parses; the runtime selector map in
630
- // query/compound-unique.ts registers the same name spelling.
631
- selectorName: quoteIfNeeded(members.map((m) => m.field).join('_')),
632
- memberType: `{ ${members.map((m) => `${quoteIfNeeded(m.field)}: ${m.tsType}`).join('; ')} }`,
633
- };
634
- });
635
- // Named helper type for annotating a compound selector by hand (Prisma parity).
636
- if (selectorEntries.length > 0) {
637
- const cuFields = selectorEntries.map((e) => `${e.selectorName}: ${e.memberType}`);
638
- lines.push(`export type ${typeName}CompoundUniques = { ${cuFields.join('; ')} };`);
639
- }
640
- const selectorBranches = selectorEntries.map((e) => `{ ${e.selectorName}: ${e.memberType} }`);
641
- const branches = [...flatBranches, ...selectorBranches];
642
- lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
643
- lines.push('');
740
+ }
741
+ return keys;
742
+ }
743
+ const FULL_CREATE_VARIANT = { omitted: new Set(), suffix: '' };
744
+ function variantKey(omitted) {
745
+ return JSON.stringify([...omitted].sort());
746
+ }
747
+ /** A PascalCase identifier fragment for a type name, or `''`. */
748
+ function identifierPart(name) {
749
+ return (0, schema_js_1.snakeToPascal)(name).replace(/[^A-Za-z0-9_$]/g, '');
750
+ }
751
+ /**
752
+ * Every create shape each table needs: the full one, plus one per distinct
753
+ * set of parent-supplied keys among the hasMany / hasOne relations pointing at
754
+ * it. Keyed by table name, then by {@link variantKey}.
755
+ */
756
+ function createVariantsByTable(schema, relationsByTable) {
757
+ const out = new Map();
758
+ for (const table of Object.values(schema.tables)) {
759
+ out.set(table.name, new Map([[variantKey([]), FULL_CREATE_VARIANT]]));
760
+ }
761
+ for (const table of Object.values(schema.tables)) {
762
+ for (const [, rel] of relationsByTable.get(table.name) ?? []) {
763
+ const omitted = suppliedByParent(schema, rel);
764
+ const variants = out.get(rel.to);
765
+ if (omitted.length === 0 || !variants)
766
+ continue;
767
+ const key = variantKey(omitted);
768
+ if (variants.has(key))
769
+ continue;
770
+ const child = schema.tables[rel.to];
771
+ // Name the variant after the child's relation back to the parent when
772
+ // there is one (`PostCreateWithoutAuthorInput`), else after the keys.
773
+ const back = omitted.find((k) => child !== undefined && Object.hasOwn(child.relations, k));
774
+ const base = `Without${identifierPart(back ?? omitted.join('_')) || 'Parent'}`;
775
+ const taken = new Set([...variants.values()].map((v) => v.suffix));
776
+ let suffix = base;
777
+ for (let n = 2; taken.has(suffix); n++)
778
+ suffix = `${base}${n}`;
779
+ variants.set(key, { omitted: new Set(omitted), suffix });
644
780
  }
645
- // CreateInput / UpdateInput, extends base type with optional relation fields
646
- if (hasRels) {
647
- lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
648
- for (const [relName, rel] of safeRelations) {
649
- const targetType = entityName(rel.to);
650
- lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
651
- }
652
- lines.push('};');
653
- lines.push('');
654
- lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
655
- for (const [relName, rel] of safeRelations) {
656
- const targetType = entityName(rel.to);
657
- if (rel.type === 'hasMany') {
658
- lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedUpdateInput;`);
659
- }
660
- else {
661
- lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
662
- }
663
- }
664
- lines.push('};');
665
- lines.push('');
781
+ }
782
+ return out;
783
+ }
784
+ /**
785
+ * Required belongsTo relations beyond this many keep their foreign keys
786
+ * optional on create instead of becoming an either-or union: TypeScript
787
+ * multiplies an intersection of unions out, so N either-or pairs are 2^N
788
+ * object types for every create on the table.
789
+ */
790
+ const MAX_FK_CHOICES = 4;
791
+ /** `XWhereUnique`: one object per unique key, plus compound selector branches. */
792
+ function emitWhereUnique(lines, table, memberTs) {
793
+ const typeName = entityName(table.name);
794
+ // Union of unique constraint shapes, deduplicating PK
795
+ const seen = new Set();
796
+ const uniqueSets = [];
797
+ const pkKey = table.primaryKey.join(',');
798
+ seen.add(pkKey);
799
+ uniqueSets.push(table.primaryKey);
800
+ for (const uc of table.uniqueColumns) {
801
+ const ucKey = uc.join(',');
802
+ if (!seen.has(ucKey)) {
803
+ seen.add(ucKey);
804
+ uniqueSets.push(uc);
666
805
  }
667
806
  }
668
- // Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
669
- for (const table of Object.values(schema.tables)) {
670
- const typeName = entityName(table.name);
671
- const hasRels = (safeRelationsByTable.get(table.name) ?? []).length > 0;
672
- // NestedCreateInput uses *CreateInput (which includes relation fields) when
673
- // the table has relations, otherwise falls back to the plain *Create type.
674
- const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
675
- lines.push(`export interface ${typeName}NestedCreateInput {`);
676
- lines.push(` create?: ${createRefType} | ${createRefType}[];`);
677
- lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
678
- lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
807
+ const memberType = (colName) => {
808
+ const col = table.columns.find((c) => c.name === colName);
809
+ return { field: col?.field ?? colName, tsType: col ? memberTs(col) : 'unknown' };
810
+ };
811
+ // Flat branches: one object per unique constraint carrying its columns.
812
+ const flatBranches = uniqueSets.map((cols) => {
813
+ const fields = cols.map((colName) => {
814
+ const m = memberType(colName);
815
+ return `${quoteIfNeeded(m.field)}: ${m.tsType}`;
816
+ });
817
+ return `{ ${fields.join('; ')} }`;
818
+ });
819
+ // Prisma-style compound-unique SELECTOR branches: a synthetic key
820
+ // (`orgId_userId`) whose value holds the member columns, emitted for every
821
+ // COMPOSITE unique (PK, composite UNIQUE constraint, or composite UNIQUE
822
+ // index). Runtime expansion lives in query/compound-unique.ts.
823
+ const compoundSeen = new Set();
824
+ const compoundSets = [];
825
+ const addCompound = (cols) => {
826
+ if (cols.length < 2)
827
+ return;
828
+ const key = cols.join(',');
829
+ if (compoundSeen.has(key))
830
+ return;
831
+ compoundSeen.add(key);
832
+ compoundSets.push(cols);
833
+ };
834
+ addCompound(table.primaryKey);
835
+ for (const uc of table.uniqueColumns)
836
+ addCompound(uc);
837
+ for (const idx of table.indexes) {
838
+ // A partial unique index does not guarantee table-wide row uniqueness,
839
+ // so it must not become a compound-unique selector (matches the runtime
840
+ // exclusion in query/compound-unique.ts).
841
+ if (idx.unique && !idx.docPath && !idx.partial)
842
+ addCompound(idx.columns);
843
+ }
844
+ const selectorEntries = compoundSets.map((cols) => {
845
+ const members = cols.map(memberType);
846
+ return {
847
+ // The selector NAME is the underscore-join of the member FIELD names.
848
+ // A junction-style column that is not a valid identifier would join
849
+ // into a broken object key, so a non-identifier name is emitted as ONE
850
+ // quoted key; query/compound-unique.ts registers the same spelling.
851
+ selectorName: quoteIfNeeded(members.map((m) => m.field).join('_')),
852
+ memberType: `{ ${members.map((m) => `${quoteIfNeeded(m.field)}: ${m.tsType}`).join('; ')} }`,
853
+ };
854
+ });
855
+ // Named helper type for annotating a compound selector by hand (Prisma parity).
856
+ if (selectorEntries.length > 0) {
857
+ const cuFields = selectorEntries.map((e) => `${e.selectorName}: ${e.memberType}`);
858
+ lines.push(`export type ${typeName}CompoundUniques = { ${cuFields.join('; ')} };`);
859
+ }
860
+ const selectorBranches = selectorEntries.map((e) => `{ ${e.selectorName}: ${e.memberType} }`);
861
+ const branches = [...flatBranches, ...selectorBranches];
862
+ lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
863
+ lines.push('');
864
+ }
865
+ /**
866
+ * The write inputs for one table:
867
+ *
868
+ * - `XCreateInput`: the create data. A required belongsTo is an either-or:
869
+ * give the foreign key, or the relation (`connect` / `create` /
870
+ * `connectOrCreate`).
871
+ * - `XCreateWithout<Relation>Input`: the same data for a row created through
872
+ * a parent's hasMany / hasOne, without the keys that parent supplies (see
873
+ * {@link CreateVariant}).
874
+ * - `XUpdateInput`: update data, scalars plus nested relation writes.
875
+ * - `XNestedCreateInput` / `XNestedUpdateInput` / `XConnectOrCreate` (and
876
+ * their `Without<Relation>` forms): the ops a to-many (or hasOne) relation
877
+ * pointing at X takes.
878
+ * - `XNestedToOneCreateInput` / `XNestedToOneUpdateInput`: the ops a
879
+ * belongsTo relation pointing at X takes. Single objects; `disconnect` and
880
+ * `delete` take `true` for the connected row.
881
+ * - `XInputs`: the map the generated client hands to `QueryInterface`.
882
+ */
883
+ function emitWriteInputs(lines, schema, table, relations, variantsByTable, writeTs) {
884
+ const typeName = entityName(table.name);
885
+ const relationField = (rel, update) => {
886
+ const target = entityName(rel.to);
887
+ if (rel.type === 'belongsTo')
888
+ return `${target}NestedToOne${update ? 'Update' : 'Create'}Input`;
889
+ const variant = variantsByTable.get(rel.to)?.get(variantKey(suppliedByParent(schema, rel))) ?? FULL_CREATE_VARIANT;
890
+ return `${target}Nested${update ? 'Update' : 'Create'}${variant.suffix}Input`;
891
+ };
892
+ // Required belongsTo relations whose foreign keys are themselves required on
893
+ // create become either-or choices.
894
+ const choices = [];
895
+ const relaxed = [];
896
+ for (const [relName, rel] of relations) {
897
+ if (!isRequiredBelongsTo(table, rel))
898
+ continue;
899
+ const fks = fkColumns(table, rel).filter((c) => !c.isGeneratedStored && !isOptionalOnCreate(c));
900
+ if (fks.length === 0)
901
+ continue;
902
+ if (choices.length < MAX_FK_CHOICES)
903
+ choices.push({ relName, rel, fks });
904
+ else
905
+ relaxed.push(...fks);
906
+ }
907
+ const choiceFields = new Set(choices.flatMap((c) => c.fks).map((c) => c.field));
908
+ const relaxedFields = new Set(relaxed.map((c) => c.field));
909
+ const where = `${typeName}WhereUnique`;
910
+ const filter = `${typeName}WhereUnique | Partial<${typeName}>`;
911
+ const variants = [...(variantsByTable.get(table.name)?.values() ?? [FULL_CREATE_VARIANT])];
912
+ for (const { omitted, suffix } of variants) {
913
+ // --- CreateInput (one concrete type per variant) ---
914
+ const members = [];
915
+ for (const col of table.columns) {
916
+ if (col.isGeneratedStored || choiceFields.has(col.field) || omitted.has(col.field))
917
+ continue;
918
+ const optional = isOptionalOnCreate(col) || relaxedFields.has(col.field);
919
+ members.push(optional
920
+ ? `${quoteIfNeeded(col.field)}?: ${writeTs(col)} | undefined`
921
+ : `${quoteIfNeeded(col.field)}: ${writeTs(col)}`);
922
+ }
923
+ for (const [relName, rel] of relations) {
924
+ if (omitted.has(relName))
925
+ continue;
926
+ members.push(`${quoteIfNeeded(relName)}?: ${relationField(rel, false)} | undefined`);
927
+ }
928
+ const parts = [members.length > 0 ? `{ ${members.join('; ')} }` : 'Record<string, never>'];
929
+ for (const { relName, rel, fks } of choices) {
930
+ if (omitted.has(relName) || fks.every((c) => omitted.has(c.field)))
931
+ continue;
932
+ const viaKey = fks.map((c) => `${quoteIfNeeded(c.field)}: ${writeTs(c)}`).join('; ');
933
+ const viaRelation = [
934
+ ...fks.map((c) => `${quoteIfNeeded(c.field)}?: ${writeTs(c)} | undefined`),
935
+ `${quoteIfNeeded(relName)}: ${relationField(rel, false)}`,
936
+ ].join('; ');
937
+ parts.push(`({ ${viaKey} } | { ${viaRelation} })`);
938
+ }
939
+ const supplied = [...omitted].map((k) => `\`${docSafe(k)}\``).join(', ');
940
+ lines.push(suffix === ''
941
+ ? `/** Data for creating a \`${docSafe(table.name)}\` row */`
942
+ : `/** Data for creating a \`${docSafe(table.name)}\` row through a parent relation, which supplies ${supplied} */`);
943
+ lines.push(`export type ${typeName}Create${suffix}Input = ${parts.join(' & ')};`);
944
+ lines.push('');
945
+ // --- Ops a to-many (or hasOne) relation pointing AT this table takes ---
946
+ const create = `${typeName}Create${suffix}Input`;
947
+ const connectOrCreate = `${typeName}ConnectOrCreate${suffix}`;
948
+ lines.push(`export interface ${typeName}NestedCreate${suffix}Input {`);
949
+ lines.push(` create?: ${create} | ${create}[] | undefined;`);
950
+ lines.push(` connect?: ${where} | ${where}[] | undefined;`);
951
+ lines.push(` connectOrCreate?: ${connectOrCreate} | ${connectOrCreate}[] | undefined;`);
679
952
  lines.push('}');
680
953
  lines.push('');
681
- lines.push(`export interface ${typeName}NestedUpdateInput {`);
682
- lines.push(` create?: ${createRefType} | ${createRefType}[];`);
683
- lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
684
- lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
685
- lines.push(` disconnect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
686
- lines.push(` set?: ${typeName}WhereUnique[];`);
687
- lines.push(` delete?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
954
+ lines.push(`export interface ${typeName}NestedUpdate${suffix}Input extends ${typeName}NestedCreate${suffix}Input {`);
955
+ lines.push(` disconnect?: ${filter} | (${filter})[] | undefined;`);
956
+ lines.push(` set?: ${where}[] | undefined;`);
957
+ lines.push(` delete?: ${filter} | (${filter})[] | undefined;`);
958
+ const updateItem = `{ where: ${filter}; data: ${typeName}UpdateInput }`;
959
+ lines.push(` update?: ${updateItem} | ${updateItem}[] | undefined;`);
960
+ const upsertItem = `{ where: ${filter}; create: ${create}; update: ${typeName}UpdateInput }`;
961
+ lines.push(` upsert?: ${upsertItem} | ${upsertItem}[] | undefined;`);
688
962
  lines.push('}');
689
963
  lines.push('');
690
- lines.push(`export interface ${typeName}ConnectOrCreate {`);
691
- lines.push(` where: ${typeName}WhereUnique;`);
692
- lines.push(` create: ${createRefType};`);
964
+ lines.push(`export interface ${connectOrCreate} {`);
965
+ lines.push(` where: ${where};`);
966
+ lines.push(` create: ${create};`);
693
967
  lines.push('}');
694
968
  lines.push('');
695
969
  }
696
- return lines.join('\n');
970
+ // --- UpdateInput ---
971
+ if (relations.length > 0) {
972
+ lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
973
+ for (const [relName, rel] of relations) {
974
+ lines.push(` ${quoteIfNeeded(relName)}?: ${relationField(rel, true)} | undefined;`);
975
+ }
976
+ lines.push('};');
977
+ }
978
+ else {
979
+ lines.push(`export type ${typeName}UpdateInput = ${typeName}Update;`);
980
+ }
981
+ lines.push('');
982
+ // A belongsTo points at ONE row: single objects, no `set`, and `true` (or
983
+ // `{}`) for "the connected row" on disconnect / delete.
984
+ const connected = `boolean | Record<string, never> | ${where}`;
985
+ lines.push(`export interface ${typeName}NestedToOneCreateInput {`);
986
+ lines.push(` create?: ${typeName}CreateInput | undefined;`);
987
+ lines.push(` connect?: ${where} | undefined;`);
988
+ lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | undefined;`);
989
+ lines.push('}');
990
+ lines.push('');
991
+ lines.push(`export interface ${typeName}NestedToOneUpdateInput extends ${typeName}NestedToOneCreateInput {`);
992
+ lines.push(` disconnect?: ${connected} | undefined;`);
993
+ lines.push(` delete?: ${connected} | undefined;`);
994
+ lines.push(` update?: { data: ${typeName}UpdateInput } | undefined;`);
995
+ lines.push(` upsert?: { where: ${filter}; create: ${typeName}CreateInput; update: ${typeName}UpdateInput } | undefined;`);
996
+ lines.push('}');
997
+ lines.push('');
998
+ // --- The accessor's input map ---
999
+ lines.push(`/** Write and lookup input types for the \`${docSafe(table.name)}\` client accessor */`);
1000
+ lines.push(`export interface ${typeName}Inputs {`);
1001
+ lines.push(` create: ${typeName}CreateInput;`);
1002
+ lines.push(` createMany: ${typeName}Create;`);
1003
+ lines.push(` update: ${typeName}UpdateInput;`);
1004
+ lines.push(` updateMany: ${typeName}Update;`);
1005
+ lines.push(` whereUnique: ${where};`);
1006
+ lines.push('}');
1007
+ lines.push('');
697
1008
  }
698
1009
  // ---------------------------------------------------------------------------
699
1010
  // zod.ts generator (H1, `turbine generate --zod`)
@@ -721,6 +1032,19 @@ function zodScalar(ts) {
721
1032
  case 'number[]':
722
1033
  // pgvector, `pgTypeToTs('vector')` yields `number[]`.
723
1034
  return 'z.array(z.number())';
1035
+ case 'number | string | bigint':
1036
+ // int8 write input.
1037
+ return 'z.union([z.number(), z.string(), z.bigint()])';
1038
+ case 'string | number':
1039
+ // numeric write input.
1040
+ return 'z.union([z.string(), z.number()])';
1041
+ case '{ x: number; y: number }':
1042
+ return 'z.object({ x: z.number(), y: z.number() })';
1043
+ case '{ x: number; y: number; radius: number }':
1044
+ return 'z.object({ x: z.number(), y: z.number(), radius: z.number() })';
1045
+ case '{ years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number }':
1046
+ // interval: the driver's parsed object, zero fields absent.
1047
+ return `z.object({ ${['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'].map((f) => `${f}: z.number().optional()`).join(', ')} })`;
724
1048
  default:
725
1049
  // json/jsonb and any unmapped user-defined type.
726
1050
  return 'z.unknown()';
@@ -749,7 +1073,14 @@ function zodBaseType(col, enums, forWrite = false) {
749
1073
  expr = 'z.union([z.string(), z.date()])';
750
1074
  }
751
1075
  else {
752
- expr = zodScalar((0, schema_js_1.pgTypeToTs)(base, false));
1076
+ // The read (or write) map decides the element type, which for an array
1077
+ // is not always the scalar's (`bigint[]` reads as strings).
1078
+ const full = (forWrite ? schema_js_1.pgTypeToTsInput : schema_js_1.pgTypeToTs)(isArray ? `_${base}` : base, false);
1079
+ const element = isArray && full.endsWith('[]') ? full.slice(0, -2).replace(/^\((.*)\)$/, '$1') : full;
1080
+ expr = zodScalar(element);
1081
+ if (isArray)
1082
+ expr += '.array()';
1083
+ return expr;
753
1084
  }
754
1085
  if (isArray)
755
1086
  expr += '.array()';
@@ -778,7 +1109,7 @@ function generateZod(schema, options) {
778
1109
  let expr = zodBaseType(col, schema.enums);
779
1110
  if (col.nullable)
780
1111
  expr += '.nullable()';
781
- lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
1112
+ lines.push(` ${objectKey(col.field)}: ${expr},`);
782
1113
  }
783
1114
  lines.push('});');
784
1115
  lines.push('');
@@ -795,7 +1126,7 @@ function generateZod(schema, options) {
795
1126
  expr += '.nullable()';
796
1127
  if (col.isGenerated === true || col.hasDefault || col.nullable)
797
1128
  expr += '.optional()';
798
- lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
1129
+ lines.push(` ${objectKey(col.field)}: ${expr},`);
799
1130
  }
800
1131
  lines.push('});');
801
1132
  lines.push('');
@@ -811,7 +1142,7 @@ function generateZod(schema, options) {
811
1142
  if (col.nullable)
812
1143
  expr += '.nullable()';
813
1144
  expr += '.optional()';
814
- lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
1145
+ lines.push(` ${objectKey(col.field)}: ${expr},`);
815
1146
  }
816
1147
  lines.push('});');
817
1148
  lines.push('');
@@ -835,7 +1166,7 @@ function generateMetadata(schema, options) {
835
1166
  ' tables: {',
836
1167
  ];
837
1168
  for (const table of Object.values(schema.tables)) {
838
- lines.push(` ${quoteIfNeeded(table.name)}: {`);
1169
+ lines.push(` ${objectKey(table.name)}: {`);
839
1170
  lines.push(` name: '${escSQ(table.name)}',`);
840
1171
  // columns
841
1172
  lines.push(' columns: [');
@@ -846,13 +1177,13 @@ function generateMetadata(schema, options) {
846
1177
  // columnMap
847
1178
  lines.push(' columnMap: {');
848
1179
  for (const [field, col] of Object.entries(table.columnMap)) {
849
- lines.push(` ${quoteIfNeeded(field)}: '${escSQ(col)}',`);
1180
+ lines.push(` ${objectKey(field)}: '${escSQ(col)}',`);
850
1181
  }
851
1182
  lines.push(' },');
852
1183
  // reverseColumnMap
853
1184
  lines.push(' reverseColumnMap: {');
854
1185
  for (const [col, field] of Object.entries(table.reverseColumnMap)) {
855
- lines.push(` ${quoteIfNeeded(col)}: '${escSQ(field)}',`);
1186
+ lines.push(` ${objectKey(col)}: '${escSQ(field)}',`);
856
1187
  }
857
1188
  lines.push(' },');
858
1189
  // dateColumns
@@ -862,12 +1193,12 @@ function generateMetadata(schema, options) {
862
1193
  const dialectTypes = table.dialectTypes ?? table.pgTypes;
863
1194
  lines.push(' dialectTypes: {');
864
1195
  for (const [col, dialectType] of Object.entries(dialectTypes)) {
865
- lines.push(` ${quoteIfNeeded(col)}: '${escSQ(dialectType)}',`);
1196
+ lines.push(` ${objectKey(col)}: '${escSQ(dialectType)}',`);
866
1197
  }
867
1198
  lines.push(' },');
868
1199
  lines.push(' pgTypes: {');
869
1200
  for (const [col, pgType] of Object.entries(table.pgTypes)) {
870
- lines.push(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
1201
+ lines.push(` ${objectKey(col)}: '${escSQ(pgType)}',`);
871
1202
  }
872
1203
  lines.push(' },');
873
1204
  // allColumns
@@ -896,7 +1227,7 @@ function generateMetadata(schema, options) {
896
1227
  `sourceKey: ${keyLiteral(rel.through.sourceKey)}, ` +
897
1228
  `targetKey: ${keyLiteral(rel.through.targetKey)} }`;
898
1229
  }
899
- lines.push(` ${quoteIfNeeded(relName)}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
1230
+ lines.push(` ${objectKey(relName)}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
900
1231
  }
901
1232
  lines.push(' },');
902
1233
  // indexes
@@ -929,7 +1260,7 @@ function generateMetadata(schema, options) {
929
1260
  // enums
930
1261
  lines.push(' enums: {');
931
1262
  for (const [enumName, labels] of Object.entries(schema.enums)) {
932
- lines.push(` ${quoteIfNeeded(enumName)}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
1263
+ lines.push(` ${objectKey(enumName)}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
933
1264
  }
934
1265
  lines.push(' },');
935
1266
  lines.push('};');
@@ -960,7 +1291,7 @@ function generateIndex(schema, options) {
960
1291
  // drove it, so direct generateIndex() callers stay byte-identical).
961
1292
  ...(options?.importMode ? [`// Sibling imports resolved with importExtension: ${options.importMode}.`, ''] : []),
962
1293
  "import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
963
- "import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
1294
+ "import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults, WhereClause } from 'turbine-orm';",
964
1295
  `import { SCHEMA } from './metadata${ext}';`,
965
1296
  ];
966
1297
  // Import all entity types and relations maps
@@ -970,9 +1301,36 @@ function generateIndex(schema, options) {
970
1301
  if (hasSafeRelations.get(t.name)) {
971
1302
  typeImports.push(`${entityName(t.name)}Relations`);
972
1303
  }
1304
+ typeImports.push(`${entityName(t.name)}Inputs`);
973
1305
  }
974
1306
  lines.push(`import type { ${typeImports.join(', ')} } from './types${ext}';`);
975
1307
  lines.push('');
1308
+ // Global filters keyed by table, each checked against its own row type, so a
1309
+ // misspelled table or column is a compile error. A table is keyed by its
1310
+ // client accessor, and also by its database name when the two differ, the
1311
+ // two spellings the runtime accepts.
1312
+ lines.push('/** `globalFilters` for this schema: a where clause (or a function returning one) per table. */');
1313
+ lines.push('export interface TurbineGlobalFilters {');
1314
+ for (const table of tableEntries) {
1315
+ const typeName = entityName(table.name);
1316
+ const where = `WhereClause<${typeName}${hasSafeRelations.get(table.name) ? `, ${typeName}Relations` : ''}>`;
1317
+ // `null` (or a function returning it) applies no filter on purpose.
1318
+ const filter = `${where} | null | (() => ${where} | null)`;
1319
+ const accessor = snakeToCamelStr(table.name);
1320
+ lines.push(` ${quoteIfNeeded(accessor)}?: ${filter} | undefined;`);
1321
+ if (accessor !== table.name)
1322
+ lines.push(` ${quoteIfNeeded(table.name)}?: ${filter} | undefined;`);
1323
+ }
1324
+ lines.push('}');
1325
+ lines.push('');
1326
+ // Each field also admits an explicit `undefined` (read as unset), so
1327
+ // `turbine({ connectionString: process.env.DATABASE_URL })` compiles under
1328
+ // exactOptionalPropertyTypes.
1329
+ lines.push('/** Client configuration with `globalFilters` typed against this schema. */');
1330
+ lines.push('export type TypedTurbineConfig = {');
1331
+ lines.push(" [K in Exclude<keyof TurbineConfig, 'globalFilters'>]?: TurbineConfig[K] | undefined;");
1332
+ lines.push('} & { globalFilters?: TurbineGlobalFilters | undefined };');
1333
+ lines.push('');
976
1334
  // -------------------------------------------------------------------------
977
1335
  // TypedTransactionClient, same typed table accessors as TurbineClient,
978
1336
  // but scoped to a single transaction connection. The runtime instance is
@@ -990,22 +1348,23 @@ function generateIndex(schema, options) {
990
1348
  const typeName = entityName(table.name);
991
1349
  const accessor = snakeToCamelStr(table.name);
992
1350
  const hasRelations = hasSafeRelations.get(table.name) === true;
993
- const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
1351
+ const genericArgs = `${typeName}, ${hasRelations ? `${typeName}Relations` : '{}'}, ${typeName}Inputs`;
994
1352
  lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table (transaction-scoped) */`);
995
1353
  lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
996
1354
  }
997
- lines.push('}');
1355
+ // The nested `$transaction` (SAVEPOINT) hands its callback a typed client.
1356
+ // Declared as a real overriding method rather than through a same-named
1357
+ // interface (declaration merging), which strict lint configs reject
1358
+ // (no-unsafe-declaration-merging). It only narrows the callback's type:
1359
+ // the body is the base method.
998
1360
  lines.push('');
999
- // Augment the class with a typed `$transaction` overload via interface
1000
- // merging. This adds an additional callable signature whose callback
1001
- // parameter is narrowed to `TypedTransactionClient`, while the base
1002
- // signature (callback parameter `BaseTransactionClient`) remains valid.
1003
- lines.push('export interface TypedTransactionClient {');
1004
1361
  lines.push(' /**');
1005
1362
  lines.push(' * Nested transaction via SAVEPOINT. The callback receives a typed');
1006
1363
  lines.push(' * `TypedTransactionClient` so all table accessors auto-complete.');
1007
1364
  lines.push(' */');
1008
- lines.push(' $transaction<R>(fn: (tx: TypedTransactionClient) => Promise<R>): Promise<R>;');
1365
+ lines.push(' override $transaction<R>(fn: (tx: TypedTransactionClient) => Promise<R>): Promise<R> {');
1366
+ lines.push(' return super.$transaction(fn as (tx: BaseTransactionClient) => Promise<R>);');
1367
+ lines.push(' }');
1009
1368
  lines.push('}');
1010
1369
  lines.push('');
1011
1370
  // Generate the client class with JSDoc
@@ -1032,37 +1391,30 @@ function generateIndex(schema, options) {
1032
1391
  const typeName = entityName(table.name);
1033
1392
  const accessor = snakeToCamelStr(table.name);
1034
1393
  const hasRelations = hasSafeRelations.get(table.name) === true;
1035
- const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
1394
+ const genericArgs = `${typeName}, ${hasRelations ? `${typeName}Relations` : '{}'}, ${typeName}Inputs`;
1036
1395
  lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table */`);
1037
1396
  lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
1038
1397
  }
1039
1398
  lines.push('');
1040
- lines.push(' constructor(config?: TurbineConfig) {');
1041
- lines.push(' super(config, SCHEMA);');
1399
+ lines.push(' constructor(config?: TypedTurbineConfig) {');
1400
+ lines.push(' super(config as TurbineConfig | undefined, SCHEMA);');
1042
1401
  lines.push(' }');
1043
- lines.push('}');
1044
- lines.push('');
1045
- // Augment TurbineClient via interface merging with typed $transaction and
1046
- // $withSession overloads. The callback parameter is narrowed to
1047
- // `TypedTransactionClient` so users get autocomplete on `tx.users`,
1048
- // `tx.posts`, etc. $withSession is the RLS shorthand for
1049
- // `$transaction(fn, { sessionContext })`; it used to be left out, so its
1050
- // callback stayed the untyped base `TransactionClient` and the documented
1051
- // `tx.<table>.findMany()` did not compile on a generated client.
1402
+ // Typed `$transaction` / `$withSession`: the callback parameter is narrowed
1403
+ // to `TypedTransactionClient`, so `tx.users` etc. autocomplete. Real
1404
+ // overriding methods whose bodies are the base methods, NOT a same-named
1405
+ // interface merged onto the class: strict lint configs reject that merge
1406
+ // (no-unsafe-declaration-merging), and generated code must pass them.
1052
1407
  //
1053
- // IMPORTANT: each merged member must be compatible with the base class's
1054
- // member ON ITS OWN (TS2415), since v0.26 the base $transaction also has a
1055
- // batch-array overload (`$transaction([...queries])`), so the merged
1056
- // interface must redeclare BOTH signatures. Emitting only the callback form
1057
- // makes every generated client fail `tsc` with "incorrectly extends". The
1058
- // $withSession overload mirrors the base parameter list exactly for the same
1059
- // reason; src/test/generate-typecheck.test.ts compiles the result.
1060
- lines.push('export interface TurbineClient {');
1408
+ // Each overload must stay compatible with the base class's (TS2416): the
1409
+ // base `$transaction` has a callback form AND a batch-array form, so both
1410
+ // are redeclared, and `$withSession` mirrors the base parameter list.
1411
+ // src/test/generate-typecheck.test.ts compiles the result.
1412
+ lines.push('');
1061
1413
  lines.push(' /**');
1062
1414
  lines.push(' * Run a callback inside a transaction. The callback receives a typed');
1063
1415
  lines.push(' * `TypedTransactionClient` with autocompletion for every table accessor.');
1064
1416
  lines.push(' */');
1065
- lines.push(' $transaction<R>(');
1417
+ lines.push(' override $transaction<R>(');
1066
1418
  lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
1067
1419
  lines.push(' options?: TransactionOptions,');
1068
1420
  lines.push(' ): Promise<R>;');
@@ -1070,18 +1422,28 @@ function generateIndex(schema, options) {
1070
1422
  lines.push(' * Batch form: run several deferred queries in one transaction and get');
1071
1423
  lines.push(' * their results as a tuple (same as the base client).');
1072
1424
  lines.push(' */');
1073
- lines.push(' $transaction<T extends readonly DeferredQuery<unknown>[]>(');
1425
+ lines.push(' override $transaction<T extends readonly DeferredQuery<unknown>[]>(');
1074
1426
  lines.push(' queries: readonly [...T],');
1427
+ lines.push(' options?: TransactionOptions,');
1075
1428
  lines.push(' ): Promise<PipelineResults<T>>;');
1429
+ lines.push(' override $transaction(');
1430
+ lines.push(' fnOrQueries: ((tx: TypedTransactionClient) => Promise<unknown>) | readonly DeferredQuery<unknown>[],');
1431
+ lines.push(' options?: TransactionOptions,');
1432
+ lines.push(' ): Promise<unknown> {');
1433
+ lines.push(" return super.$transaction(fnOrQueries as Parameters<BaseTurbineClient['$transaction']>[0], options);");
1434
+ lines.push(' }');
1435
+ lines.push('');
1076
1436
  lines.push(' /**');
1077
1437
  lines.push(' * Run a callback inside a transaction with the given session GUCs applied');
1078
1438
  lines.push(' * via `set_config(..., true)` (the RLS / multi-tenant shorthand). The');
1079
1439
  lines.push(' * callback receives a typed `TypedTransactionClient`, same as `$transaction`.');
1080
1440
  lines.push(' */');
1081
- lines.push(' $withSession<R>(');
1441
+ lines.push(' override $withSession<R>(');
1082
1442
  lines.push(' context: Record<string, string | number | boolean>,');
1083
1443
  lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
1084
- lines.push(' ): Promise<R>;');
1444
+ lines.push(' ): Promise<R> {');
1445
+ lines.push(' return super.$withSession(context, fn as (tx: BaseTransactionClient) => Promise<R>);');
1446
+ lines.push(' }');
1085
1447
  lines.push('}');
1086
1448
  lines.push('');
1087
1449
  // Factory function with JSDoc
@@ -1092,7 +1454,7 @@ function generateIndex(schema, options) {
1092
1454
  lines.push(' * fields) to fall back to the `DATABASE_URL` environment variable.');
1093
1455
  lines.push(' * @returns A fully-typed TurbineClient with table accessors.');
1094
1456
  lines.push(' */');
1095
- lines.push('export function turbine(config?: TurbineConfig): TurbineClient {');
1457
+ lines.push('export function turbine(config?: TypedTurbineConfig): TurbineClient {');
1096
1458
  lines.push(' return new TurbineClient(config);');
1097
1459
  lines.push('}');
1098
1460
  lines.push('');
@@ -1129,13 +1491,13 @@ function generatePrismaMap(map, options) {
1129
1491
  ' models: {',
1130
1492
  ];
1131
1493
  for (const [modelName, model] of Object.entries(map.models)) {
1132
- lines.push(` ${quoteIfNeeded(modelName)}: {`);
1494
+ lines.push(` ${objectKey(modelName)}: {`);
1133
1495
  lines.push(` table: '${escSQ(model.table)}',`);
1134
1496
  lines.push(` accessor: '${escSQ(model.accessor)}',`);
1135
1497
  lines.push(` fields: ${serializeStringRecord(model.fields)},`);
1136
1498
  lines.push(' relations: {');
1137
1499
  for (const [rel, r] of Object.entries(model.relations)) {
1138
- lines.push(` ${quoteIfNeeded(rel)}: { name: '${escSQ(r.name)}', cardinality: '${r.cardinality}' },`);
1500
+ lines.push(` ${objectKey(rel)}: { name: '${escSQ(r.name)}', cardinality: '${r.cardinality}' },`);
1139
1501
  }
1140
1502
  lines.push(' },');
1141
1503
  lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
@@ -1160,14 +1522,14 @@ function serializeStringRecord(rec) {
1160
1522
  const entries = Object.entries(rec);
1161
1523
  if (entries.length === 0)
1162
1524
  return '{}';
1163
- return `{ ${entries.map(([k, v]) => `${quoteIfNeeded(k)}: '${escSQ(v)}'`).join(', ')} }`;
1525
+ return `{ ${entries.map(([k, v]) => `${objectKey(k)}: '${escSQ(v)}'`).join(', ')} }`;
1164
1526
  }
1165
1527
  /** Serialize a `Record<string, string[]>` as an inline object literal. */
1166
1528
  function serializeStringArrayRecord(rec) {
1167
1529
  const entries = Object.entries(rec);
1168
1530
  if (entries.length === 0)
1169
1531
  return '{}';
1170
- return `{ ${entries.map(([k, v]) => `${quoteIfNeeded(k)}: [${v.map((s) => `'${escSQ(s)}'`).join(', ')}]`).join(', ')} }`;
1532
+ return `{ ${entries.map(([k, v]) => `${objectKey(k)}: [${v.map((s) => `'${escSQ(s)}'`).join(', ')}]`).join(', ')} }`;
1171
1533
  }
1172
1534
  // ---------------------------------------------------------------------------
1173
1535
  // Helpers
@@ -1221,6 +1583,11 @@ function serializeColumn(col) {
1221
1583
  parts.push(`updatedAt: true`);
1222
1584
  if (col.maxLength !== undefined)
1223
1585
  parts.push(`maxLength: ${col.maxLength}`);
1586
+ // The default expression, so an engine without column defaults (PowDB) can
1587
+ // apply it on create. Arbitrary SQL text, so written as a JSON string
1588
+ // literal, which escapes every quote, backslash and line break.
1589
+ if (col.defaultValue != null)
1590
+ parts.push(`defaultValue: ${JSON.stringify(col.defaultValue)}`);
1224
1591
  return `{ ${parts.join(', ')} }`;
1225
1592
  }
1226
1593
  /**
@@ -1245,44 +1612,33 @@ const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1245
1612
  * return `'${s}'` verbatim, so a name containing a quote closed the key and
1246
1613
  * reopened in expression position.
1247
1614
  *
1248
- * KNOWN LIMIT, deliberately not handled here: a database object named
1249
- * `__proto__` still sets the emitted object literal's PROTOTYPE rather than
1250
- * adding a property, because the object-literal special case applies to the
1251
- * quoted spelling too. Quoting is not a fix for it, so there is no branch for
1252
- * it; the consequence is a metadata map that silently omits that one entry, not
1253
- * code execution.
1615
+ * A name of `__proto__` is safe in an interface but not in an object literal;
1616
+ * see {@link objectKey}.
1254
1617
  */
1255
1618
  function quoteIfNeeded(s) {
1256
1619
  return BARE_KEY_RE.test(s) ? s : `'${escSQ(s)}'`;
1257
1620
  }
1621
+ /**
1622
+ * {@link quoteIfNeeded} for a key in an OBJECT LITERAL that is evaluated at
1623
+ * runtime (metadata.ts, the Prisma map). `{ '__proto__': v }` sets the literal's
1624
+ * prototype instead of defining a property, quoted or not, so that one name is
1625
+ * emitted as a computed key, whose string literal is just as inert and which does
1626
+ * define the property. Interface members (types.ts) have no such rule and keep
1627
+ * using {@link quoteIfNeeded}.
1628
+ */
1629
+ function objectKey(s) {
1630
+ return s === '__proto__' ? "['__proto__']" : quoteIfNeeded(s);
1631
+ }
1258
1632
  function snakeToCamelStr(s) {
1259
1633
  return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1260
1634
  }
1261
1635
  /**
1262
- * Build the update-input field type for a column. Numeric columns become
1263
- * `T | UpdateOperatorInput<number> | null?` so users can write atomic
1264
- * operators (`{ increment: 1 }`, `{ multiply: 2 }`, etc.) without casts.
1265
- *
1266
- * The check is purely structural, if the column's TS type contains
1267
- * `'number'` (e.g. `number`, `number | null`), it's eligible. Other
1268
- * scalar types (`string`, `boolean`, `Date`, `unknown`, `Buffer`,
1269
- * `Date | null`, etc.) pass through unchanged.
1636
+ * Build the update-input field type for a column: the plain value, the
1637
+ * {@link UpdateOperatorInput} object (`{ set }` on every column, plus
1638
+ * `increment` / `decrement` / `multiply` / `divide` when the type includes
1639
+ * `number`), an `expr.*` update expression or a `col()` reference, matching
1640
+ * what `update()` accepts at runtime and the core `UpdateData` type.
1270
1641
  */
1271
1642
  function updateFieldType(tsType) {
1272
- // Strip parens for the regex check; preserve the original string in the output.
1273
- if (containsNumberType(tsType)) {
1274
- return `${tsType} | UpdateOperatorInput<number>`;
1275
- }
1276
- return tsType;
1277
- }
1278
- /**
1279
- * Detect whether a TypeScript type expression contains the `number` primitive
1280
- * as a top-level union member. Conservative on purpose, only matches
1281
- * `number`, `number | null`, `null | number`, etc., not `number[]` or
1282
- * `Record<string, number>`.
1283
- */
1284
- function containsNumberType(tsType) {
1285
- // Tokenize on `|` and check each member.
1286
- const parts = tsType.split('|').map((p) => p.trim());
1287
- return parts.some((p) => p === 'number');
1643
+ return `${tsType} | UpdateOperatorInput<${tsType}> | SqlExpr<${tsType}> | ColumnRef`;
1288
1644
  }