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
@@ -22,6 +22,11 @@
22
22
  * });
23
23
  * ```
24
24
  */
25
+ // Type-only import, erased at runtime, so it cannot introduce a circular
26
+ // runtime dependency (the local `camelToSnakeLocal` copy avoids that for the
27
+ // value-level helper).
28
+ import { ValidationError } from './errors.js';
29
+ import { suggestKey } from './query/utils.js';
25
30
  /** Maps shorthand names to actual Postgres type strings */
26
31
  const TYPE_MAP = {
27
32
  // `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
@@ -56,16 +61,42 @@ const TYPE_MAP = {
56
61
  enum: 'ENUM',
57
62
  vector: 'VECTOR',
58
63
  };
64
+ /** What a value is, for a message: `an array`, `null`, `a string`. */
65
+ function describeKind(value) {
66
+ if (value === null)
67
+ return 'null';
68
+ if (Array.isArray(value))
69
+ return 'an array';
70
+ return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
71
+ }
72
+ /**
73
+ * A declared DEFAULT as the SQL text the DDL generators take. A string is raw
74
+ * SQL, kept as written. A finite number or a boolean is the literal it is,
75
+ * where it used to reach the generator as a non-string and crash it with a
76
+ * TypeError. `undefined` and `null` mean no default.
77
+ */
78
+ function declaredDefault(value, where) {
79
+ if (value === undefined || value === null)
80
+ return null;
81
+ if (typeof value === 'string')
82
+ return value;
83
+ if (typeof value === 'boolean')
84
+ return value ? 'true' : 'false';
85
+ if ((typeof value === 'number' && Number.isFinite(value)) || typeof value === 'bigint')
86
+ return String(value);
87
+ throw new ValidationError(`Invalid default for ${where}: received ${describeKind(value)}${typeof value === 'number' ? ` (${value})` : ''}. ` +
88
+ `Write it as a string of raw SQL such as 'now()' or "'active'", or as a finite number or a boolean.`);
89
+ }
59
90
  /** Convert a user-facing ColumnDef to the internal ColumnConfig */
60
- function resolveColumn(def) {
61
- if (!(def.type in TYPE_MAP)) {
62
- throw new Error(`Invalid column type "${def.type}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
91
+ function resolveColumn(def, where = 'a column') {
92
+ if (!Object.hasOwn(TYPE_MAP, def.type)) {
93
+ throw new ValidationError(`Invalid column type "${def.type}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
63
94
  }
64
95
  if (def.type === 'enum' && !def.enumName) {
65
- throw new Error(`Column of type "enum" requires an "enumName" (the CREATE TYPE name).`);
96
+ throw new ValidationError(`Column of type "enum" requires an "enumName" (the CREATE TYPE name).`);
66
97
  }
67
98
  if (def.type === 'vector' && (def.dimensions == null || def.dimensions <= 0)) {
68
- throw new Error(`Column of type "vector" requires a positive "dimensions" count.`);
99
+ throw new ValidationError(`Column of type "vector" requires a positive "dimensions" count.`);
69
100
  }
70
101
  // `references` is either the "table.column" string or a { target, onDelete, onUpdate } object.
71
102
  let referencesTarget = null;
@@ -74,7 +105,11 @@ function resolveColumn(def) {
74
105
  if (typeof def.references === 'string') {
75
106
  referencesTarget = def.references;
76
107
  }
77
- else if (def.references) {
108
+ else if (def.references !== undefined && def.references !== null) {
109
+ if (typeof def.references !== 'object' || typeof def.references.target !== 'string') {
110
+ throw new ValidationError(`defineSchema: ${where} has a "references" that is neither a "table.column" string nor ` +
111
+ `{ target: "table.column", onDelete?, onUpdate? }.`);
112
+ }
78
113
  referencesTarget = def.references.target;
79
114
  onDelete = def.references.onDelete ?? null;
80
115
  onUpdate = def.references.onUpdate ?? null;
@@ -85,7 +120,7 @@ function resolveColumn(def) {
85
120
  isNotNull: def.notNull ?? false,
86
121
  isNullable: def.nullable ?? false,
87
122
  isUnique: def.unique ?? false,
88
- defaultValue: def.default ?? null,
123
+ defaultValue: declaredDefault(def.default, where),
89
124
  referencesTarget,
90
125
  maxLength: def.maxLength ?? null,
91
126
  onDelete,
@@ -96,15 +131,306 @@ function resolveColumn(def) {
96
131
  check: def.check ?? null,
97
132
  pii: def.pii ?? false,
98
133
  updatedAt: def.updatedAt ?? false,
134
+ tsType: checkedTsType(def.tsType),
99
135
  };
100
136
  }
137
+ /** Characters a declared type may not contain (the generator refuses the same set). */
138
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: refusing control characters is this pattern's purpose
139
+ const UNSAFE_TS_TYPE_TEXT = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
140
+ /** Characters a type expression uses outside a string literal and a bracket. */
141
+ const TS_TYPE_CHAR = /^[\p{L}\p{N}_$.:?|&!+\-= \t]$/u;
142
+ const TS_TYPE_CLOSER = { '(': ')', '[': ']', '{': '}', '<': '>' };
143
+ /**
144
+ * Why `text` cannot be written into a generated file as a TypeScript type, or
145
+ * null when it can. The text lands verbatim as a member type or an alias body,
146
+ * so what has to hold is that it stays ONE type: brackets balance, nothing at
147
+ * the top level ends the member (`;` or `,`), and it holds no comment, template
148
+ * literal or character a type does not use. A `//` would swallow the rest of the
149
+ * line and a stray `}` would close the interface and start a statement.
150
+ *
151
+ * It does not check that the type is well formed or that its names exist; the
152
+ * compiler of the generated file does that.
153
+ */
154
+ export function tsTypeTextProblem(text) {
155
+ if (text.trim() === '')
156
+ return 'it is empty';
157
+ if (UNSAFE_TS_TYPE_TEXT.test(text))
158
+ return 'it contains a control or line-separator character';
159
+ const chars = Array.from(text);
160
+ const open = [];
161
+ for (let i = 0; i < chars.length; i++) {
162
+ const c = chars[i];
163
+ if (c === "'" || c === '"') {
164
+ let end = i + 1;
165
+ while (end < chars.length && chars[end] !== c)
166
+ end += chars[end] === '\\' ? 2 : 1;
167
+ if (end >= chars.length)
168
+ return `a ${c} string literal is not closed`;
169
+ if (chars.slice(i, end).join('').includes('*/'))
170
+ return 'a string literal contains "*/"';
171
+ i = end;
172
+ }
173
+ else if (c === '=' && chars[i + 1] === '>') {
174
+ i++;
175
+ }
176
+ else if (TS_TYPE_CLOSER[c] !== undefined) {
177
+ open.push(TS_TYPE_CLOSER[c]);
178
+ }
179
+ else if (c === ')' || c === ']' || c === '}' || c === '>') {
180
+ if (open.pop() !== c)
181
+ return `"${c}" closes a bracket that was never opened`;
182
+ }
183
+ else if (c === ';' || c === ',') {
184
+ if (open.length === 0)
185
+ return `"${c}" outside any brackets would end the type`;
186
+ }
187
+ else if (c === '`') {
188
+ return 'a template literal type is not supported here, name it with a type alias and use import()';
189
+ }
190
+ else if (!TS_TYPE_CHAR.test(c)) {
191
+ return `"${c}" cannot appear in a type${c === '/' ? ' (no comments)' : ''}`;
192
+ }
193
+ }
194
+ return open.length > 0 ? 'a bracket is not closed' : null;
195
+ }
196
+ /**
197
+ * A `tsType` is emitted verbatim into generated source, so it must be one type
198
+ * on one line (see {@link tsTypeTextProblem}).
199
+ */
200
+ function checkedTsType(tsType) {
201
+ if (tsType === undefined)
202
+ return null;
203
+ const problem = typeof tsType === 'string' ? tsTypeTextProblem(tsType) : 'it is not a string';
204
+ if (problem !== null) {
205
+ throw new ValidationError(`Invalid column "tsType" ${JSON.stringify(tsType)}: ${problem}. ` +
206
+ `Give one TypeScript type on one line, for example '{ theme: string }' or "import('../src/types.js').Settings".`);
207
+ }
208
+ return tsType;
209
+ }
101
210
  /** Type guard: is this index declaration a doc-field expression index? */
102
211
  export function isDocFieldIndexDef(idx) {
103
212
  return 'docField' in idx && typeof idx.docField === 'string';
104
213
  }
105
- /** Check if a value is a TableDef (from legacy table() builder) */
214
+ // ---------------------------------------------------------------------------
215
+ // Known keys: one table per declaration shape
216
+ // ---------------------------------------------------------------------------
217
+ //
218
+ // A schema is static code, and a key `defineSchema` does not know is not
219
+ // harmless: it is an option the caller meant and did not get (`defaultValue`
220
+ // for `default` emits a column with no DEFAULT, `uniq` for `unique` a
221
+ // non-unique index). So an unknown key is refused, at compile time in a
222
+ // literal and at runtime for JSON or plain JS.
223
+ //
224
+ // Each table is a `Record<keyof Shape, true>`: adding an option to the shape
225
+ // without listing it here fails the build, and listing a key the shape does
226
+ // not have fails as an excess property. The compile-time check types below
227
+ // and the runtime refusal both read these, so they cannot disagree.
228
+ const COLUMN_KEYS = {
229
+ type: true,
230
+ primaryKey: true,
231
+ notNull: true,
232
+ nullable: true,
233
+ unique: true,
234
+ default: true,
235
+ references: true,
236
+ maxLength: true,
237
+ enumName: true,
238
+ dimensions: true,
239
+ array: true,
240
+ check: true,
241
+ pii: true,
242
+ updatedAt: true,
243
+ tsType: true,
244
+ };
245
+ const REFERENCE_KEYS = { target: true, onDelete: true, onUpdate: true };
246
+ const CHECK_KEYS = { name: true, expression: true };
247
+ const MANY_TO_MANY_KEYS = {
248
+ name: true,
249
+ target: true,
250
+ through: true,
251
+ sourceKey: true,
252
+ targetKey: true,
253
+ references: true,
254
+ };
255
+ const INDEX_KEYS = {
256
+ columns: true,
257
+ docField: true,
258
+ path: true,
259
+ unique: true,
260
+ name: true,
261
+ };
262
+ const TABLE_OPTION_KEYS = {
263
+ primaryKey: true,
264
+ manyToMany: true,
265
+ checks: true,
266
+ indexes: true,
267
+ };
268
+ const SCHEMA_OPTION_KEYS = { enums: true };
269
+ /**
270
+ * Refuse every key of `value` that `known` does not list (E003), naming where
271
+ * it sits and the key it most likely meant.
272
+ */
273
+ function assertKnownKeys(value, known, where) {
274
+ for (const key of Object.keys(value)) {
275
+ if (Object.hasOwn(known, key))
276
+ continue;
277
+ // suggestKey covers typos and the metadata spellings (`defaultValue`,
278
+ // `isUnique`) alike: the second pass matches a key that adds words.
279
+ const suggestion = suggestKey(key, Object.keys(known));
280
+ throw new ValidationError(`defineSchema: unknown key "${key}" in ${where}.` +
281
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
282
+ ` Valid keys: ${Object.keys(known).join(', ')}.`);
283
+ }
284
+ }
285
+ /** Each element of an option array, checked as `what` (non-objects are left to the consumer's own checks). */
286
+ function assertKnownElementKeys(list, known, where) {
287
+ list.forEach((item, i) => {
288
+ if (typeof item === 'object' && item !== null)
289
+ assertKnownKeys(item, known, `${where}[${i}]`);
290
+ });
291
+ }
292
+ /**
293
+ * Refuse `__proto__` as a table or column name. `JSON.parse` can produce it as
294
+ * an own key, but assigning to it on a record sets the record's prototype
295
+ * instead of adding a key, so the column would vanish without a word.
296
+ */
297
+ function assertOrdinaryName(name, what) {
298
+ if (name === '__proto__') {
299
+ throw new ValidationError(`defineSchema: ${what} is named "__proto__", which JavaScript treats as the prototype setter rather than a ` +
300
+ `property name, so it could not be stored or queried. Rename it.`);
301
+ }
302
+ }
303
+ /**
304
+ * An index is either a column index (`columns`) or a document-field index
305
+ * (`docField` and `path`), never both. A declaration carrying both was taken
306
+ * for the document-field kind, which only PowDB creates, so the SQL engines
307
+ * skipped it without saying so.
308
+ */
309
+ function assertIndexKinds(list, accessor) {
310
+ list.forEach((item, i) => {
311
+ if (typeof item === 'object' && item !== null && 'columns' in item && 'docField' in item) {
312
+ throw new ValidationError(`defineSchema: index ${i} of table "${accessor}" declares both "columns" and "docField". ` +
313
+ `Use "columns" for a column index, or "docField" with "path" for an index into a json document. ` +
314
+ `Declare two indexes if you need both.`);
315
+ }
316
+ });
317
+ }
318
+ /**
319
+ * Is this value a TableDef (the legacy `table()` builder's output)?
320
+ *
321
+ * A TableDef carries a STRING `name` and a `columns` record of column configs.
322
+ * In the object format a key called `name` or `columns` is a column, and a
323
+ * column definition is an object, so the string is what tells the two apart: a
324
+ * table with columns named `name` and `columns` used to be read as a TableDef
325
+ * and emitted as a different table. A table that has a string `name` but is not
326
+ * a TableDef falls through to the object format, which refuses the string.
327
+ */
106
328
  function isTableDef(v) {
107
- return typeof v === 'object' && v !== null && 'columns' in v && 'name' in v;
329
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
330
+ return false;
331
+ const t = v;
332
+ if (typeof t.name !== 'string')
333
+ return false;
334
+ const columns = t.columns;
335
+ if (typeof columns !== 'object' || columns === null || Array.isArray(columns))
336
+ return false;
337
+ return Object.values(columns).every((c) => typeof c === 'object' && c !== null);
338
+ }
339
+ /** A name list for a message: `a, b, c`, or `(none)`. */
340
+ function nameList(names) {
341
+ const list = [...names];
342
+ return list.length > 0 ? list.join(', ') : '(none)';
343
+ }
344
+ /** The DDL column name of a field: what `CREATE TABLE` and every index write for it. */
345
+ const ddlColumnName = camelToSnakeLocal;
346
+ /**
347
+ * Refuse a schema that names something it does not contain. Every one of these
348
+ * used to be accepted and then went wrong somewhere else, or nowhere:
349
+ *
350
+ * - `userId` and `user_id` in one table are the SAME column to the database, so
351
+ * the DDL declared it twice.
352
+ * - A `references` that is not `table.column` was dropped (no foreign key, no
353
+ * error), and one naming a column its target table does not have became a
354
+ * `REFERENCES` clause the database rejects at push time. A target table that
355
+ * is not in this schema is left alone, since it may be owned elsewhere.
356
+ * - An index on a column the table does not have failed the same way.
357
+ *
358
+ * It runs once, after every table is built, because a reference may name a
359
+ * table declared later.
360
+ */
361
+ function assertSchemaIntegrity(tables) {
362
+ const byDdlName = new Map();
363
+ const byName = new Map();
364
+ for (const [accessor, table] of Object.entries(tables)) {
365
+ const owner = byDdlName.get(table.name);
366
+ if (owner !== undefined) {
367
+ throw new ValidationError(`defineSchema: tables "${owner}" and "${accessor}" are both the table "${table.name}" in the database ` +
368
+ `(a camelCase name is written in snake_case). Keep one, or rename one.`);
369
+ }
370
+ byDdlName.set(table.name, accessor);
371
+ byName.set(accessor, table);
372
+ byName.set(table.name, table);
373
+ }
374
+ for (const [accessor, table] of Object.entries(tables)) {
375
+ const columnOf = new Map();
376
+ for (const field of Object.keys(table.columns)) {
377
+ const ddl = ddlColumnName(field);
378
+ const other = columnOf.get(ddl);
379
+ if (other !== undefined) {
380
+ throw new ValidationError(`defineSchema: table "${accessor}" has columns "${other}" and "${field}", which are the same column ` +
381
+ `"${ddl}" in the database. Keep one.`);
382
+ }
383
+ columnOf.set(ddl, field);
384
+ }
385
+ const hasColumn = (name) => Object.hasOwn(table.columns, name) || columnOf.has(name);
386
+ const columnNames = () => Object.keys(table.columns);
387
+ for (const [i, idx] of (table.indexes ?? []).entries()) {
388
+ if (!isDocFieldIndexDef(idx) && !Array.isArray(idx.columns)) {
389
+ throw new ValidationError(`defineSchema: index ${i} of table "${accessor}" declares neither "columns" (an array of column names) ` +
390
+ `nor "docField" with "path".`);
391
+ }
392
+ const named = isDocFieldIndexDef(idx) ? [idx.docField] : idx.columns;
393
+ for (const name of named) {
394
+ if (typeof name === 'string' && hasColumn(name))
395
+ continue;
396
+ const suggestion = typeof name === 'string' ? suggestKey(name, columnNames()) : null;
397
+ throw new ValidationError(`defineSchema: index ${i} of table "${accessor}" names ${typeof name === 'string' ? `column "${name}"` : `a ${describeKind(name)} instead of a column name`}, ` +
398
+ `which the table does not have.${suggestion ? ` Did you mean "${suggestion}"?` : ''} ` +
399
+ `Columns: ${nameList(columnNames())}.`);
400
+ }
401
+ }
402
+ for (const [field, config] of Object.entries(table.columns)) {
403
+ if (config.referencesTarget === null || config.referencesTarget === undefined)
404
+ continue;
405
+ const where = `column "${accessor}.${field}"`;
406
+ const target = config.referencesTarget;
407
+ const parts = typeof target === 'string' ? target.split('.') : [];
408
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
409
+ throw new ValidationError(`defineSchema: ${where} has references ${JSON.stringify(target)}, which is not in "table.column" form, so no foreign ` +
410
+ `key would be created. Write it as "users.id".` +
411
+ (parts.length > 2 ? ' A schema-qualified name ("auth.users.id") is not supported.' : ''));
412
+ }
413
+ const [refTable, refColumn] = parts;
414
+ // A table outside this schema is allowed, since something else may own
415
+ // it. Its columns cannot be checked here.
416
+ const targetTable = byName.get(refTable);
417
+ if (!targetTable)
418
+ continue;
419
+ const targetDdl = new Set(Object.keys(targetTable.columns).map(ddlColumnName));
420
+ const isField = Object.hasOwn(targetTable.columns, refColumn);
421
+ if (!isField && !targetDdl.has(refColumn)) {
422
+ const suggestion = suggestKey(refColumn, Object.keys(targetTable.columns));
423
+ throw new ValidationError(`defineSchema: ${where} references "${target}", but table "${refTable}" has no column "${refColumn}".` +
424
+ (suggestion ? ` Did you mean "${suggestion}"?` : '') +
425
+ ` Columns: ${nameList(Object.keys(targetTable.columns))}.`);
426
+ }
427
+ // The DDL names the column as written, so a field name (`displayName`)
428
+ // becomes the column it is (`display_name`) here, where the table is known.
429
+ if (isField && ddlColumnName(refColumn) !== refColumn) {
430
+ config.referencesTarget = `${refTable}.${ddlColumnName(refColumn)}`;
431
+ }
432
+ }
433
+ }
108
434
  }
109
435
  /**
110
436
  * Define the full database schema using plain objects.
@@ -127,7 +453,10 @@ function isTableDef(v) {
127
453
  */
128
454
  export function defineSchema(input, options) {
129
455
  const tables = {};
456
+ if (options !== undefined)
457
+ assertKnownKeys(options, SCHEMA_OPTION_KEYS, 'the defineSchema options');
130
458
  for (const [accessor, value] of Object.entries(input)) {
459
+ assertOrdinaryName(accessor, `table "${accessor}"`);
131
460
  // The user-facing key is the camelCase JS accessor; the DDL-facing
132
461
  // table name is its snake_case form (e.g. `postTags` → `post_tags`).
133
462
  const dbName = camelToSnakeLocal(accessor);
@@ -150,8 +479,10 @@ export function defineSchema(input, options) {
150
479
  if (fieldName === 'indexes') {
151
480
  if (def !== undefined) {
152
481
  if (!Array.isArray(def)) {
153
- throw new Error(`Table "${accessor}": "indexes" must be an array of index declarations`);
482
+ throw new ValidationError(`Table "${accessor}": "indexes" must be an array of index declarations`);
154
483
  }
484
+ assertKnownElementKeys(def, INDEX_KEYS, `table "${accessor}" indexes`);
485
+ assertIndexKinds(def, accessor);
155
486
  indexes = def;
156
487
  }
157
488
  continue;
@@ -159,8 +490,9 @@ export function defineSchema(input, options) {
159
490
  if (fieldName === 'manyToMany') {
160
491
  if (def !== undefined) {
161
492
  if (!Array.isArray(def)) {
162
- throw new Error(`Table "${accessor}": "manyToMany" must be an array of relation declarations`);
493
+ throw new ValidationError(`Table "${accessor}": "manyToMany" must be an array of relation declarations`);
163
494
  }
495
+ assertKnownElementKeys(def, MANY_TO_MANY_KEYS, `table "${accessor}" manyToMany`);
164
496
  m2m = def;
165
497
  }
166
498
  continue;
@@ -168,8 +500,9 @@ export function defineSchema(input, options) {
168
500
  if (fieldName === 'checks') {
169
501
  if (def !== undefined) {
170
502
  if (!Array.isArray(def)) {
171
- throw new Error(`Table "${accessor}": "checks" must be an array of { name?, expression } objects`);
503
+ throw new ValidationError(`Table "${accessor}": "checks" must be an array of { name?, expression } objects`);
172
504
  }
505
+ assertKnownElementKeys(def, CHECK_KEYS, `table "${accessor}" checks`);
173
506
  checks = def;
174
507
  }
175
508
  continue;
@@ -178,14 +511,35 @@ export function defineSchema(input, options) {
178
511
  // Top-level composite primary key declaration
179
512
  if (def !== undefined) {
180
513
  if (!Array.isArray(def)) {
181
- throw new Error(`Table "${accessor}": top-level "primaryKey" must be an array of column names (string[])`);
514
+ throw new ValidationError(`Table "${accessor}": top-level "primaryKey" must be an array of column names (string[])`);
182
515
  }
183
516
  pk = def;
184
517
  }
185
518
  continue;
186
519
  }
187
- // Anything else is a ColumnDef
188
- columns[fieldName] = resolveColumn(def);
520
+ // Anything else is a ColumnDef. An array here is a table option under a
521
+ // name defineSchema does not know (`primarykey`, `index`): refuse it by
522
+ // that name rather than as a column with no type.
523
+ if (Array.isArray(def)) {
524
+ const suggestion = suggestKey(fieldName, Object.keys(TABLE_OPTION_KEYS));
525
+ throw new ValidationError(`defineSchema: table "${accessor}" has an array under "${fieldName}", which is not a table option ` +
526
+ `(${Object.keys(TABLE_OPTION_KEYS).join(', ')}) and cannot be a column.` +
527
+ (suggestion ? ` Did you mean "${suggestion}"?` : ''));
528
+ }
529
+ assertOrdinaryName(fieldName, `column "${accessor}.${fieldName}"`);
530
+ // An `undefined` column is absent, like an `undefined` option anywhere else.
531
+ if (def === undefined)
532
+ continue;
533
+ if (typeof def !== 'object' || def === null) {
534
+ throw new ValidationError(`defineSchema: column "${accessor}.${fieldName}" must be an object such as { type: 'text' }, ` +
535
+ `received ${describeKind(def)}.`);
536
+ }
537
+ assertKnownKeys(def, COLUMN_KEYS, `column "${accessor}.${fieldName}"`);
538
+ const ref = def.references;
539
+ if (typeof ref === 'object' && ref !== null) {
540
+ assertKnownKeys(ref, REFERENCE_KEYS, `the references of column "${accessor}.${fieldName}"`);
541
+ }
542
+ columns[fieldName] = resolveColumn(def, `column "${accessor}.${fieldName}"`);
189
543
  }
190
544
  // Validate composite PK references real columns and clear column-level PKs
191
545
  // for those columns so we don't double-emit `PRIMARY KEY` clauses.
@@ -194,7 +548,7 @@ export function defineSchema(input, options) {
194
548
  if (pk && pk.length > 0) {
195
549
  for (const colName of pk) {
196
550
  if (!(colName in columns)) {
197
- throw new Error(`Table "${accessor}": composite primaryKey references unknown column "${colName}". ` +
551
+ throw new ValidationError(`Table "${accessor}": composite primaryKey references unknown column "${colName}". ` +
198
552
  `Known columns: ${Object.keys(columns).join(', ') || '(none)'}`);
199
553
  }
200
554
  // A composite PK at the table level supersedes any column-level
@@ -219,6 +573,7 @@ export function defineSchema(input, options) {
219
573
  };
220
574
  }
221
575
  }
576
+ assertSchemaIntegrity(tables);
222
577
  return { tables, ...(options?.enums ? { enums: options.enums } : {}) };
223
578
  }
224
579
  /**
@@ -251,6 +606,7 @@ export class ColumnBuilder {
251
606
  check: null,
252
607
  pii: false,
253
608
  updatedAt: false,
609
+ tsType: null,
254
610
  };
255
611
  }
256
612
  serial() {
@@ -344,10 +700,12 @@ export class ColumnBuilder {
344
700
  return this;
345
701
  }
346
702
  default(val) {
347
- this._config.defaultValue = val;
703
+ this._config.defaultValue = declaredDefault(val, 'ColumnBuilder.default()');
348
704
  return this;
349
705
  }
350
706
  references(target, opts) {
707
+ if (opts !== undefined)
708
+ assertKnownKeys(opts, { onDelete: true, onUpdate: true }, `references("${target}") options`);
351
709
  this._config.referencesTarget = target;
352
710
  this._config.onDelete = opts?.onDelete ?? null;
353
711
  this._config.onUpdate = opts?.onUpdate ?? null;
@@ -366,6 +724,11 @@ export class ColumnBuilder {
366
724
  this._config.updatedAt = true;
367
725
  return this;
368
726
  }
727
+ /** The TypeScript type `turbine generate` emits for this column. See {@link ColumnDef.tsType}. */
728
+ tsType(type) {
729
+ this._config.tsType = checkedTsType(type);
730
+ return this;
731
+ }
369
732
  array() {
370
733
  this._config.isArray = true;
371
734
  return this;
@@ -406,7 +769,7 @@ export const column = new Proxy({}, {
406
769
  };
407
770
  }
408
771
  return () => {
409
- throw new Error(`Unknown column type: ${prop}`);
772
+ throw new ValidationError(`Unknown column type: ${prop}`);
410
773
  };
411
774
  },
412
775
  });
@@ -45,7 +45,27 @@
45
45
  * ```
46
46
  */
47
47
  import { type SchemaMetadata } from './schema.js';
48
- import { type SchemaDef } from './schema-builder.js';
48
+ import { type ColumnConfig, type SchemaDef } from './schema-builder.js';
49
+ /** The code-first-only flags a column can declare. */
50
+ export interface CodeFirstColumnTags {
51
+ pii?: true;
52
+ updatedAt?: true;
53
+ maxLength?: number;
54
+ /**
55
+ * The declared TypeScript type, WITHOUT nullability. On metadata built by
56
+ * {@link schemaDefToMetadata} it is the column's `declaredTsType`, never its
57
+ * `tsType`; `turbine generate` passes it to the generator as a `columnTypes`
58
+ * entry.
59
+ */
60
+ tsType?: string;
61
+ }
62
+ /**
63
+ * The tags a column declares, as the fields they take on {@link ColumnMetadata}.
64
+ * The one place the tag set is listed, so `schemaDefToMetadata` and the CLI's
65
+ * overlay of these tags onto an introspected schema (`cli/code-first-tags.ts`)
66
+ * cannot disagree about what counts as one.
67
+ */
68
+ export declare function declaredTagsOf(config: ColumnConfig): CodeFirstColumnTags;
49
69
  /**
50
70
  * Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
51
71
  *
@@ -157,6 +157,20 @@ function mapIndexes(tableDef, declared) {
157
157
  }
158
158
  return out;
159
159
  }
160
+ /**
161
+ * The tags a column declares, as the fields they take on {@link ColumnMetadata}.
162
+ * The one place the tag set is listed, so `schemaDefToMetadata` and the CLI's
163
+ * overlay of these tags onto an introspected schema (`cli/code-first-tags.ts`)
164
+ * cannot disagree about what counts as one.
165
+ */
166
+ export function declaredTagsOf(config) {
167
+ return {
168
+ ...(config.pii ? { pii: true } : {}),
169
+ ...(config.updatedAt ? { updatedAt: true } : {}),
170
+ ...(config.maxLength != null ? { maxLength: config.maxLength } : {}),
171
+ ...(config.tsType != null ? { tsType: config.tsType } : {}),
172
+ };
173
+ }
160
174
  // ---------------------------------------------------------------------------
161
175
  // The converter
162
176
  // ---------------------------------------------------------------------------
@@ -325,23 +339,30 @@ export function schemaDefToMetadata(def) {
325
339
  const serial = isSerialType(config.type);
326
340
  // PK members and serials are NOT NULL in Postgres regardless of flags.
327
341
  const nullable = !(config.isPrimaryKey || config.isNotNull || serial);
342
+ const { tsType: declaredTsType, ...declaredTags } = declaredTagsOf(config);
328
343
  const col = {
329
344
  name,
330
345
  field,
331
346
  dialectType: wireType,
332
347
  pgType: wireType,
333
- tsType: pgTypeToTs(wireType, nullable),
334
348
  nullable,
335
349
  hasDefault: config.defaultValue != null || serial,
350
+ // The expression itself, as written in the schema (`now()`, `'draft'`).
351
+ // Engines with no column defaults (PowDB) apply it client-side on
352
+ // create, which they cannot do from `hasDefault` alone.
353
+ ...(config.defaultValue != null ? { defaultValue: config.defaultValue } : {}),
336
354
  isArray: config.isArray,
337
355
  arrayType: pgArrayType(base),
338
356
  pgArrayType: pgArrayType(base),
339
357
  ...(serial ? { isGenerated: true } : {}),
340
- // PII is a code-first declaration; carry it through so query-layer
341
- // default-projection exclusion and Studio redaction can honor it.
342
- // Only set when true, keeping output byte-stable for untagged schemas.
343
- ...(config.pii ? { pii: true } : {}),
344
- ...(config.maxLength != null ? { maxLength: config.maxLength } : {}),
358
+ // PII, updatedAt and maxLength are code-first declarations; carry them
359
+ // through so query-layer projection, the updatedAt stamp and Studio
360
+ // redaction can honor them. Only set when declared, keeping output
361
+ // byte-stable for untagged schemas.
362
+ ...declaredTags,
363
+ // Always the mapped type. A declared `tsType` rides along separately.
364
+ tsType: pgTypeToTs(wireType, nullable),
365
+ ...(declaredTsType !== undefined ? { declaredTsType } : {}),
345
366
  };
346
367
  columns.push(col);
347
368
  columnMap[field] = name;
@@ -370,7 +391,7 @@ export function schemaDefToMetadata(def) {
370
391
  allColumns,
371
392
  primaryKey: pk,
372
393
  uniqueColumns,
373
- relations: relationsByTable.get(tableDef.name) ?? {},
394
+ relations: { ...relationsByTable.get(tableDef.name) },
374
395
  // Declared `indexes` (plain column + doc-field expression) carry through;
375
396
  // an undeclared table gets `[]`, which keeps schemaHasIndexInfo() false so
376
397
  // the index advisor stays silent. Doc-field (docPath) indexes are ignored