turbine-orm 0.81.0-next.d9b06dd → 0.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -24,12 +24,22 @@ export interface TurbineCliConfig {
24
24
  * to hand-edit every call site. A typo here is an error, not a silent no-op.
25
25
  */
26
26
  relationNames?: Record<string, Record<string, string>>;
27
+ /**
28
+ * TypeScript type overrides for generated row and input types, as
29
+ * `{ '<table>.<column>': '<type>' }` with database names. The main use is a
30
+ * `json` / `jsonb` column, which otherwise generates as `unknown`:
31
+ * `{ 'users.settings': '{ theme: string; beta?: boolean }' }`. The type text
32
+ * is emitted verbatim. A key naming no table or column is an error, not a
33
+ * silent no-op. A code-first column's `tsType` feeds the same map.
34
+ */
35
+ columnTypes?: Record<string, string>;
27
36
  /**
28
37
  * Extension for the generated `index.ts` sibling imports (F3):
29
- * `'js'` (`./types.js`), `'none'` (`./types`), or `'auto'` (default:
30
- * tsconfig-detected, falling back to `'js'`).
38
+ * `'js'` (`./types.js`), `'ts'` (`./types.ts`, for Node's native type
39
+ * stripping), `'none'` (`./types`), or `'auto'` (default: tsconfig-detected,
40
+ * falling back to `'js'`; never picks `'ts'`).
31
41
  */
32
- importExtension?: 'js' | 'none' | 'auto';
42
+ importExtension?: 'js' | 'ts' | 'none' | 'auto';
33
43
  /**
34
44
  * Keep raw database column names as generated field names (snake_case)
35
45
  * instead of camelCase (F4). Opt-in; default `false`.
@@ -54,7 +64,11 @@ export interface TurbineCliConfig {
54
64
  * are set.
55
65
  */
56
66
  seed?: string;
57
- /** Schema builder file path (for push command) */
67
+ /**
68
+ * The `defineSchema()` file (default `./turbine/schema.ts`): what `push` applies,
69
+ * and where `generate` reads the `pii` / `updatedAt` / `maxLength` tags the
70
+ * database cannot store.
71
+ */
58
72
  schemaFile?: string;
59
73
  /**
60
74
  * Database adapter for PostgreSQL-compatible databases that need
@@ -159,14 +173,22 @@ export interface ResolvedConfig {
159
173
  migrationsDir: string;
160
174
  seedFile?: string;
161
175
  schemaFile: string;
176
+ /**
177
+ * True when `--schema-file` or the config `schemaFile` key named the file, as
178
+ * opposed to the `./turbine/schema.ts` default. A named file that is missing is
179
+ * an error; a missing default just means a database-first project.
180
+ */
181
+ schemaFileConfigured?: boolean;
162
182
  /** Resolved generator import-extension mode (F3). */
163
- importExtension: 'js' | 'none' | 'auto';
183
+ importExtension: 'js' | 'ts' | 'none' | 'auto';
164
184
  /** Resolved keep-column-names generator flag (F4). */
165
185
  keepColumnNames: boolean;
166
186
  /** Resolved opt-out of the unique-FK → hasOne introspection flip (F2). */
167
187
  legacyToManyUniques: boolean;
168
188
  /** Resolved derived-relation rename map, or undefined when none is declared. */
169
189
  relationNames?: Record<string, Record<string, string>>;
190
+ /** Column type overrides for the generator, or undefined when none is declared. */
191
+ columnTypes?: Record<string, string>;
170
192
  }
171
193
  export interface CliOverrides {
172
194
  url?: string;
@@ -176,10 +198,17 @@ export interface CliOverrides {
176
198
  schemaFile?: string;
177
199
  include?: string[];
178
200
  exclude?: string[];
179
- importExtension?: 'js' | 'none' | 'auto';
201
+ importExtension?: 'js' | 'ts' | 'none' | 'auto';
180
202
  keepColumnNames?: boolean;
181
203
  legacyToManyUniques?: boolean;
182
204
  }
205
+ /**
206
+ * The first candidate that is a non-blank string. An empty or whitespace-only
207
+ * value counts as unset, because an exported-but-empty variable (`DATABASE_URL=`
208
+ * in a shell profile or a `.env` template) is how "not configured" is usually
209
+ * spelled, and `??` would let it beat a real value further down the chain.
210
+ */
211
+ export declare function firstNonEmpty(...candidates: Array<string | undefined>): string | undefined;
183
212
  /**
184
213
  * Merge config file values with CLI overrides and env vars.
185
214
  * Priority: CLI flags > env vars > config file > defaults.
@@ -46,6 +46,7 @@ exports.unwrapConfigModule = unwrapConfigModule;
46
46
  exports.loadConfigResult = loadConfigResult;
47
47
  exports.loadConfig = loadConfig;
48
48
  exports.findConfigFile = findConfigFile;
49
+ exports.firstNonEmpty = firstNonEmpty;
49
50
  exports.resolveConfig = resolveConfig;
50
51
  exports.resolveSeedFile = resolveSeedFile;
51
52
  exports.connectionStringHasPassword = connectionStringHasPassword;
@@ -195,23 +196,38 @@ function findConfigFile(cwd) {
195
196
  }
196
197
  return null;
197
198
  }
199
+ /**
200
+ * The first candidate that is a non-blank string. An empty or whitespace-only
201
+ * value counts as unset, because an exported-but-empty variable (`DATABASE_URL=`
202
+ * in a shell profile or a `.env` template) is how "not configured" is usually
203
+ * spelled, and `??` would let it beat a real value further down the chain.
204
+ */
205
+ function firstNonEmpty(...candidates) {
206
+ for (const value of candidates) {
207
+ if (typeof value === 'string' && value.trim() !== '')
208
+ return value;
209
+ }
210
+ return undefined;
211
+ }
198
212
  /**
199
213
  * Merge config file values with CLI overrides and env vars.
200
214
  * Priority: CLI flags > env vars > config file > defaults.
201
215
  */
202
216
  function resolveConfig(fileConfig, overrides) {
203
217
  return {
204
- url: overrides.url ?? process.env.DATABASE_URL ?? fileConfig.url ?? '',
218
+ url: firstNonEmpty(overrides.url, process.env.DATABASE_URL, fileConfig.url) ?? '',
205
219
  out: overrides.out ?? fileConfig.out ?? './generated/turbine',
206
220
  schema: overrides.schema ?? fileConfig.schema ?? 'public',
207
221
  include: overrides.include ?? fileConfig.include ?? [],
208
222
  relationNames: fileConfig.relationNames,
223
+ columnTypes: fileConfig.columnTypes,
209
224
  exclude: overrides.exclude ?? fileConfig.exclude ?? [],
210
225
  migrationsDir: fileConfig.migrationsDir ?? './turbine/migrations',
211
226
  // `seedFile` is canonical (what the docs and `turbine init` use); `seed` is a
212
227
  // back-compat alias kept working for configs scaffolded before 0.50.
213
228
  seedFile: fileConfig.seedFile ?? fileConfig.seed,
214
229
  schemaFile: overrides.schemaFile ?? fileConfig.schemaFile ?? './turbine/schema.ts',
230
+ schemaFileConfigured: (overrides.schemaFile ?? fileConfig.schemaFile) !== undefined,
215
231
  importExtension: overrides.importExtension ?? fileConfig.importExtension ?? 'auto',
216
232
  keepColumnNames: overrides.keepColumnNames ?? fileConfig.keepColumnNames ?? false,
217
233
  legacyToManyUniques: overrides.legacyToManyUniques ?? fileConfig.legacyToManyUniques ?? false,
@@ -19,16 +19,19 @@
19
19
  * the optional-`COLUMN` shorthand (`ALTER TABLE t DROP email`), data-modifying
20
20
  * CTEs (`WITH d AS (DELETE ...) SELECT ...`), `MERGE ... THEN DELETE`, dynamic
21
21
  * SQL inside a `DO`/function body, an `UPDATE` whose only WHERE sits inside a
22
- * subquery (which restricts nothing), a `DROP ... CASCADE` of a non-table
23
- * object (which takes dependent COLUMNS with it), a detached partition, and an
24
- * `EXPLAIN ANALYZE` of any of the above (which really executes it).
22
+ * subquery (which restricts nothing) or is a constant-true predicate (`WHERE
23
+ * true`, `WHERE 1 = 1`), a `DROP ... CASCADE` of a non-table object (which takes
24
+ * dependent COLUMNS with it), a detached partition, an `ON CONFLICT DO UPDATE`
25
+ * with no `WHERE` (it rewrites every conflicting row), a `PREPARE`d destructive
26
+ * statement, `lo_unlink` (it deletes large objects), and an `EXPLAIN ANALYZE` of
27
+ * any of the above (which really executes it).
25
28
  *
26
29
  * The lexing, "where does a statement end" and "which characters are code",
27
30
  * is NOT done here: it lives in `sql-statements.ts` and is shared with the
28
31
  * migration runner. Two lexers is how this guard came to disagree with the
29
32
  * executor about what a file contained (see that module's header).
30
33
  */
31
- export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-database' | 'drop-owned' | 'drop-matview' | 'drop-column' | 'drop-cascade' | 'detach-partition' | 'truncate' | 'delete' | 'update-without-where' | 'alter-column-type' | 'merge-delete' | 'rename' | 'rewrite-rule' | 'dynamic-destructive' | 'dynamic-unclassified';
34
+ export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-database' | 'drop-owned' | 'drop-matview' | 'drop-column' | 'drop-cascade' | 'detach-partition' | 'truncate' | 'delete' | 'update-without-where' | 'overwrite-on-conflict' | 'large-object-unlink' | 'alter-column-type' | 'merge-delete' | 'rename' | 'rewrite-rule' | 'dynamic-destructive' | 'dynamic-unclassified';
32
35
  export interface DestructiveStatement {
33
36
  /** The offending SQL statement (trimmed, possibly long, display truncated) */
34
37
  statement: string;
@@ -20,9 +20,12 @@
20
20
  * the optional-`COLUMN` shorthand (`ALTER TABLE t DROP email`), data-modifying
21
21
  * CTEs (`WITH d AS (DELETE ...) SELECT ...`), `MERGE ... THEN DELETE`, dynamic
22
22
  * SQL inside a `DO`/function body, an `UPDATE` whose only WHERE sits inside a
23
- * subquery (which restricts nothing), a `DROP ... CASCADE` of a non-table
24
- * object (which takes dependent COLUMNS with it), a detached partition, and an
25
- * `EXPLAIN ANALYZE` of any of the above (which really executes it).
23
+ * subquery (which restricts nothing) or is a constant-true predicate (`WHERE
24
+ * true`, `WHERE 1 = 1`), a `DROP ... CASCADE` of a non-table object (which takes
25
+ * dependent COLUMNS with it), a detached partition, an `ON CONFLICT DO UPDATE`
26
+ * with no `WHERE` (it rewrites every conflicting row), a `PREPARE`d destructive
27
+ * statement, `lo_unlink` (it deletes large objects), and an `EXPLAIN ANALYZE` of
28
+ * any of the above (which really executes it).
26
29
  *
27
30
  * The lexing, "where does a statement end" and "which characters are code",
28
31
  * is NOT done here: it lives in `sql-statements.ts` and is shared with the
@@ -45,7 +48,9 @@ exports.DESTRUCTIVE_KIND_LABEL = {
45
48
  'detach-partition': 'detaches a partition, every row in it leaves the table',
46
49
  truncate: 'deletes every row',
47
50
  delete: 'deletes rows',
48
- 'update-without-where': 'rewrites every row (no WHERE clause)',
51
+ 'update-without-where': 'rewrites every row (no WHERE clause, or one that is always true)',
52
+ 'overwrite-on-conflict': 'overwrites every conflicting row (ON CONFLICT DO UPDATE with no restricting WHERE)',
53
+ 'large-object-unlink': 'permanently deletes large objects (lo_unlink)',
49
54
  'alter-column-type': 'rewrites a column type (cast may truncate or fail)',
50
55
  'merge-delete': 'deletes matched rows (MERGE ... THEN DELETE)',
51
56
  rename: 'renames a table or column, every query and view referencing the old name breaks',
@@ -56,6 +61,12 @@ exports.DESTRUCTIVE_KIND_LABEL = {
56
61
  /** Unquote a "quoted" identifier for display. */
57
62
  const ident = (raw) => (raw ?? '?').replace(/^"|"$/g, '');
58
63
  const IDENT = String.raw `("[^"]+"|[a-zA-Z_][\w$]*)(\.("[^"]+"|[a-zA-Z_][\w$]*))?`;
64
+ /**
65
+ * Ends an `IDENT` match. Not `\b`: after a closing `"` the next character is
66
+ * not a word character either, so `\b` fails and `UPDATE "users" SET ...` was
67
+ * never matched.
68
+ */
69
+ const NOT_IDENT_CHAR = String.raw `(?![\w$])`;
59
70
  /**
60
71
  * Render one `IDENT` capture triple (name, `.qualifier`, qualifier) as text,
61
72
  * where `base` is the 1-based index of the triple's first group.
@@ -106,10 +117,11 @@ const RULES = [
106
117
  // CASCADE these are safe: Postgres refuses the drop while a dependency
107
118
  // exists. With it, the dependents go too, and a dependent of a TYPE or a
108
119
  // DOMAIN is typically a COLUMN, so `DROP TYPE order_status CASCADE`
109
- // silently removes `orders.status` and every value in it. The presence of
110
- // CASCADE is therefore the whole rule, not an extra detail.
120
+ // silently removes `orders.status` and every value in it, and a dependent of
121
+ // a VIEW can be a materialized view, whose stored rows go with it. The
122
+ // presence of CASCADE is therefore the whole rule, not an extra detail.
111
123
  kind: 'drop-cascade',
112
- regex: new RegExp(String.raw `^DROP\s+(TYPE|DOMAIN|EXTENSION|SEQUENCE|FUNCTION|PROCEDURE|ROUTINE|AGGREGATE)\s+(IF\s+EXISTS\s+)?${IDENT}${OUTSIDE_QUOTES}\bCASCADE\b`, 'i'),
124
+ regex: new RegExp(String.raw `^DROP\s+(TYPE|DOMAIN|EXTENSION|SEQUENCE|FUNCTION|PROCEDURE|ROUTINE|AGGREGATE|VIEW)\s+(IF\s+EXISTS\s+)?${IDENT}${OUTSIDE_QUOTES}\bCASCADE\b`, 'i'),
113
125
  target: (m) => `${m[1].toUpperCase()} ${qualified(m, 3)}`,
114
126
  },
115
127
  {
@@ -145,16 +157,43 @@ const RULES = [
145
157
  {
146
158
  // MERGE's DELETE action removes rows from the target table.
147
159
  kind: 'merge-delete',
148
- regex: new RegExp(String.raw `^MERGE\s+INTO\s+(ONLY\s+)?${IDENT}\b${OUTSIDE_QUOTES}\bTHEN\s+DELETE\b`, 'i'),
160
+ regex: new RegExp(String.raw `^MERGE\s+INTO\s+(ONLY\s+)?${IDENT}${NOT_IDENT_CHAR}${OUTSIDE_QUOTES}\bTHEN\s+DELETE\b`, 'i'),
149
161
  target: (m) => (m[4] ? `${ident(m[2])}.${ident(m[4])}` : ident(m[2])),
150
162
  },
151
163
  {
152
164
  // A WHERE inside a scalar subquery (`SET x = (SELECT ... WHERE ...)`) does
153
- // NOT restrict the rows updated, so the guard tests only the TOP level.
165
+ // NOT restrict the rows updated, so the guard tests only the TOP level, and
166
+ // a top-level WHERE that is always true restricts nothing either.
154
167
  kind: 'update-without-where',
155
- regex: new RegExp(String.raw `^UPDATE\s+(ONLY\s+)?${IDENT}\b`, 'i'),
168
+ regex: new RegExp(String.raw `^UPDATE\s+(ONLY\s+)?${IDENT}${NOT_IDENT_CHAR}`, 'i'),
156
169
  target: (m) => (m[4] ? `${ident(m[2])}.${ident(m[4])}` : ident(m[2])),
157
- also: (stmt) => !hasTopLevelWhere(stmt),
170
+ also: (stmt) => !hasRestrictingWhere(stmt),
171
+ },
172
+ {
173
+ // `INSERT ... ON CONFLICT ... DO UPDATE SET ...` rewrites every row the
174
+ // insert conflicts with. The WHERE that narrows it sits AFTER `DO UPDATE`;
175
+ // the one before it is the conflict target's index predicate and restricts
176
+ // nothing. Add `WHERE t.col IS DISTINCT FROM EXCLUDED.col` to say which rows
177
+ // may change.
178
+ kind: 'overwrite-on-conflict',
179
+ regex: new RegExp(String.raw `^INSERT\s+INTO\s+${IDENT}${NOT_IDENT_CHAR}`, 'i'),
180
+ target: (m) => (m[3] ? `${ident(m[1])}.${ident(m[3])}` : ident(m[1])),
181
+ also: (stmt) => {
182
+ const code = stmt.replace(/"(?:[^"]|"")*"/g, '""');
183
+ if (!/\bON\s+CONFLICT\b/i.test(code))
184
+ return false;
185
+ const at = code.search(/\bDO\s+UPDATE\b/i);
186
+ return at !== -1 && !hasRestrictingWhere(code.slice(at));
187
+ },
188
+ },
189
+ {
190
+ // `SELECT lo_unlink(oid) FROM pg_largeobject_metadata` deletes the large
191
+ // objects whatever they hold, and there is no WHERE to look for. Statements
192
+ // that only NAME the function (GRANT, COMMENT, ALTER, DROP) call nothing.
193
+ kind: 'large-object-unlink',
194
+ regex: /\blo_unlink\s*\(/i,
195
+ target: () => 'large objects',
196
+ also: (stmt) => !/^(?:GRANT|REVOKE|COMMENT|ALTER|DROP)\b/i.test(stmt),
158
197
  },
159
198
  {
160
199
  // `CREATE RULE r AS ON INSERT TO t DO INSTEAD DELETE FROM u`. The rule
@@ -173,7 +212,7 @@ const RULES = [
173
212
  // that appears in ordinary migrations. DELETE, TRUNCATE and DROP stay
174
213
  // unconditional: a DELETE inside a rule destroys on every matching
175
214
  // statement whether or not it is narrowed, and the other two take no WHERE.
176
- also: (stmt) => !/\bDO\s+(?:ALSO\s+|INSTEAD\s+)?\(?\s*UPDATE\b/i.test(stmt) || !hasTopLevelWhere(stmt),
215
+ also: (stmt) => !/\bDO\s+(?:ALSO\s+|INSTEAD\s+)?\(?\s*UPDATE\b/i.test(stmt) || !hasRestrictingWhere(stmt),
177
216
  },
178
217
  // Renames come LAST: they destroy no data, so any statement that is BOTH a
179
218
  // rename and a data-loss operation should report the data loss instead.
@@ -195,21 +234,165 @@ const RULES = [
195
234
  target: (m) => `${qualified(m, 3)}.${qualified(m, 7)}`,
196
235
  },
197
236
  ];
198
- /** True when the statement has a `WHERE` outside every parenthesized group. */
199
- function hasTopLevelWhere(stmt) {
200
- const re = /[()]|\bWHERE\b/gi;
237
+ /**
238
+ * The predicate of the statement's top-level `WHERE` (outside every
239
+ * parenthesized group), up to a top-level `RETURNING` or the end, or `null` when
240
+ * there is none.
241
+ */
242
+ function topLevelWherePredicate(stmt) {
243
+ // A quoted identifier is consumed whole, so a column named "where" is not one.
244
+ const re = /"(?:[^"]|"")*"|[()]|\bWHERE\b|\bRETURNING\b/gi;
201
245
  let depth = 0;
246
+ let from = -1;
202
247
  let m = re.exec(stmt);
203
248
  while (m !== null) {
204
- if (m[0] === '(')
249
+ if (m[0].startsWith('"')) {
250
+ // not a keyword
251
+ }
252
+ else if (m[0] === '(')
205
253
  depth++;
206
254
  else if (m[0] === ')')
207
255
  depth = Math.max(0, depth - 1);
208
- else if (depth === 0)
209
- return true;
256
+ else if (depth === 0) {
257
+ if (from === -1 && /^WHERE$/i.test(m[0]))
258
+ from = m.index + m[0].length;
259
+ else if (from !== -1 && /^RETURNING$/i.test(m[0]))
260
+ return stmt.slice(from, m.index).trim();
261
+ }
210
262
  m = re.exec(stmt);
211
263
  }
212
- return false;
264
+ return from === -1 ? null : stmt.slice(from).trim();
265
+ }
266
+ /**
267
+ * True when the statement has a top-level `WHERE` that can leave some rows out.
268
+ * A predicate proven always true (`true`, `1 = 1`, `x OR true`) restricts nothing
269
+ * and counts as no WHERE. A predicate this cannot read is assumed to restrict,
270
+ * and that statement is then NOT flagged, so only a proof of constant-true may
271
+ * turn a WHERE into none.
272
+ */
273
+ function hasRestrictingWhere(stmt) {
274
+ const predicate = topLevelWherePredicate(stmt);
275
+ return predicate !== null && constantValue(predicate) !== true;
276
+ }
277
+ /**
278
+ * Split on a top-level (paren depth 0, outside `CASE ... END`) `AND` or `OR`.
279
+ * The `AND` of a `BETWEEN a AND b` belongs to the BETWEEN and splits nothing.
280
+ */
281
+ function splitTopLevel(text, keyword) {
282
+ const re = /"(?:[^"]|"")*"|[()]|\b(?:AND|OR|BETWEEN|CASE|END)\b/gi;
283
+ const parts = [];
284
+ let depth = 0;
285
+ let caseDepth = 0;
286
+ let betweenOpen = 0;
287
+ let last = 0;
288
+ let m = re.exec(text);
289
+ while (m !== null) {
290
+ const token = m[0].toUpperCase();
291
+ if (token.startsWith('"')) {
292
+ // a quoted identifier, never a keyword
293
+ }
294
+ else if (token === '(')
295
+ depth++;
296
+ else if (token === ')')
297
+ depth = Math.max(0, depth - 1);
298
+ else if (depth === 0) {
299
+ if (token === 'CASE')
300
+ caseDepth++;
301
+ else if (token === 'END')
302
+ caseDepth = Math.max(0, caseDepth - 1);
303
+ else if (caseDepth === 0) {
304
+ if (token === 'BETWEEN')
305
+ betweenOpen++;
306
+ else if (token === 'AND' && betweenOpen > 0)
307
+ betweenOpen--;
308
+ else if (token === keyword) {
309
+ parts.push(text.slice(last, m.index));
310
+ last = m.index + m[0].length;
311
+ }
312
+ }
313
+ }
314
+ m = re.exec(text);
315
+ }
316
+ parts.push(text.slice(last));
317
+ return parts.map((part) => part.trim());
318
+ }
319
+ /** Strip parentheses that wrap the WHOLE text: `((a = 1))` becomes `a = 1`. */
320
+ function unwrapParens(text) {
321
+ let out = text.trim();
322
+ while (out.startsWith('(') && closingParenIndex(out, 0) === out.length - 1)
323
+ out = out.slice(1, -1).trim();
324
+ return out;
325
+ }
326
+ const NUMBER_LITERAL = String.raw `[+-]?\d+(?:\.\d+)?`;
327
+ const COMPARISON = new RegExp(String.raw `^(${NUMBER_LITERAL}|'')\s*(=|<>|!=|<=|>=|<|>)\s*(${NUMBER_LITERAL}|'')$`);
328
+ const SELF_COMPARISON = new RegExp(String.raw `^(${IDENT})\s*(=|<>|!=|<=|>=|<|>)\s*(${IDENT})$`);
329
+ /**
330
+ * The truth value of a predicate when it does not depend on any row: `true`,
331
+ * `false`, or `undefined` when it does (or when this cannot tell). Strings reach
332
+ * here already emptied to `''`, so two string literals compare equal; the only
333
+ * mistake that costs is calling a false predicate true, a prompt on a statement
334
+ * that updates nothing.
335
+ */
336
+ function constantValue(predicate) {
337
+ const text = unwrapParens(predicate);
338
+ if (text === '')
339
+ return undefined;
340
+ const ors = splitTopLevel(text, 'OR');
341
+ if (ors.length > 1) {
342
+ const values = ors.map(constantValue);
343
+ if (values.includes(true))
344
+ return true;
345
+ return values.every((v) => v === false) ? false : undefined;
346
+ }
347
+ const ands = splitTopLevel(text, 'AND');
348
+ if (ands.length > 1) {
349
+ const values = ands.map(constantValue);
350
+ if (values.includes(false))
351
+ return false;
352
+ return values.every((v) => v === true) ? true : undefined;
353
+ }
354
+ const negated = /^NOT\s+([\s\S]+)$/i.exec(text);
355
+ if (negated) {
356
+ const inner = constantValue(negated[1]);
357
+ return inner === undefined ? undefined : !inner;
358
+ }
359
+ if (/^TRUE$/i.test(text))
360
+ return true;
361
+ if (/^(?:FALSE|NULL)$/i.test(text))
362
+ return false;
363
+ const literals = COMPARISON.exec(text);
364
+ if (literals)
365
+ return compareConstants(literals[1], literals[2], literals[3]);
366
+ // `id = id` is true for every row whose id is not null; calling it true errs
367
+ // toward the prompt, which is the safe side.
368
+ const self = SELF_COMPARISON.exec(text);
369
+ if (self && self[1].toLowerCase() === self[6].toLowerCase())
370
+ return compareConstants('0', self[5], '0');
371
+ return undefined;
372
+ }
373
+ /** Compare two literals, where two `''` (emptied strings) are taken as equal. */
374
+ function compareConstants(left, op, right) {
375
+ const a = left === "''" ? 0 : Number(left);
376
+ const b = right === "''" ? 0 : Number(right);
377
+ if (Number.isNaN(a) || Number.isNaN(b))
378
+ return undefined;
379
+ switch (op) {
380
+ case '=':
381
+ return a === b;
382
+ case '<>':
383
+ case '!=':
384
+ return a !== b;
385
+ case '<':
386
+ return a < b;
387
+ case '<=':
388
+ return a <= b;
389
+ case '>':
390
+ return a > b;
391
+ case '>=':
392
+ return a >= b;
393
+ default:
394
+ return undefined;
395
+ }
213
396
  }
214
397
  /**
215
398
  * A leading CTE list is a prefix, not a statement: `WITH c AS (SELECT 1) DELETE
@@ -269,6 +452,25 @@ function copySubstatements(stmt) {
269
452
  // above rather than being judged as one blob here.
270
453
  return /^(?:DELETE|UPDATE|INSERT|MERGE|WITH)\b/i.test(inner) ? [inner] : [];
271
454
  }
455
+ /**
456
+ * The statement a `PREPARE name [(types)] AS <statement>` stores. Nothing runs at
457
+ * PREPARE time, but `EXECUTE name` runs it later on the same connection, so the
458
+ * stored statement is judged as if it had been written out. A prepared `DELETE`
459
+ * or unrestricted `UPDATE` followed by `EXECUTE` was reported as clean.
460
+ */
461
+ function prepareSubstatements(stmt) {
462
+ const head = /^PREPARE\s+(?:"[^"]+"|[A-Za-z_][\w$]*)\s*/i.exec(stmt);
463
+ if (!head)
464
+ return [];
465
+ let rest = stmt.slice(head[0].length);
466
+ if (rest.startsWith('('))
467
+ rest = rest.slice(closingParenIndex(rest, 0) + 1).trimStart();
468
+ const as = /^AS\s+/i.exec(rest);
469
+ if (!as)
470
+ return [];
471
+ const inner = rest.slice(as[0].length);
472
+ return [stripLeadingCtes(inner), ...cteSubstatements(inner)];
473
+ }
272
474
  /**
273
475
  * Data-modifying CTE bodies: `WITH d AS (DELETE FROM t ...) SELECT ...` runs a
274
476
  * real DELETE even though the statement reads as a SELECT. Each candidate is cut
@@ -338,7 +540,8 @@ function proceduralCandidates(body) {
338
540
  // Literals have to survive: they are where dynamic SQL keeps its payload,
339
541
  // and `stripped` would have emptied exactly them.
340
542
  const text = statement.code;
341
- const re = /\b(?:DROP|TRUNCATE|DELETE|ALTER|UPDATE|MERGE)\s/gi;
543
+ // `DO UPDATE` is the tail of an `ON CONFLICT` clause, judged with its INSERT.
544
+ const re = /\b(?:(?:DROP|TRUNCATE|DELETE|ALTER|MERGE|INSERT)\s|(?<!\bDO\s+)UPDATE\s|lo_unlink\s*\()/gi;
342
545
  let m = re.exec(text);
343
546
  while (m !== null) {
344
547
  out.push({ text: text.slice(m.index), before: text.slice(0, m.index), statementCode: text });
@@ -370,6 +573,7 @@ function scanDestructiveSql(sql) {
370
573
  stripLeadingCtes(body),
371
574
  ...cteSubstatements(body),
372
575
  ...copySubstatements(body).flatMap((inner) => [inner, ...cteSubstatements(inner)]),
576
+ ...prepareSubstatements(body),
373
577
  ].map((text) => ({
374
578
  text,
375
579
  display,
@@ -107,11 +107,13 @@ const CATALOG = {
107
107
  'TLS: a self-signed or expired certificate, or a hostname the certificate does not cover.',
108
108
  'The connection string is malformed, or points at a pooler port the server is not on.',
109
109
  'The server closed an idle pooled connection (proxy idle timeout, restart, failover).',
110
+ 'A transaction-pooling endpoint (PgBouncer in transaction mode, a managed `-pooler` host, port 6432 or 6543) with named prepared statements: sqlstate 42P05 or 26000.',
110
111
  ],
111
112
  howToFix: [
112
113
  'Read `err.sqlstate` and the hint in the message: each driver code carries its own next step.',
113
114
  'Check the host, port and database name; for TLS, supply the CA via `ssl: { ca }` rather than disabling verification.',
114
115
  'For a serverless driver, pass the external pool on `TurbineConfig.pool` instead of a connection string.',
116
+ 'For 42P05 / 26000 behind a pooler, connect to the direct endpoint, set `preparedStatements: false`, or run PgBouncer 1.21+ with `max_prepared_statements` above 0.',
115
117
  ],
116
118
  properties: ['sqlstate'],
117
119
  },
@@ -349,6 +351,40 @@ const CATALOG = {
349
351
  ],
350
352
  properties: ['reason'],
351
353
  },
354
+ TURBINE_E019: {
355
+ className: 'TransactionRolledBackError',
356
+ retryable: false,
357
+ origin: 'turbine',
358
+ whenThrown: 'A transaction callback returned or committed after work inside it had already failed, so the server rolled the transaction back.',
359
+ likelyCauses: [
360
+ 'The callback caught an error from a statement (a unique violation, a timeout) and carried on, then returned. PostgreSQL refuses every later statement in an aborted transaction and answers COMMIT with ROLLBACK.',
361
+ 'A query inside `$transaction` hit its per-query `timeout`; the transaction cannot continue after the cancel.',
362
+ 'Raw `transaction()`: a statement failed and the code returned without a SAVEPOINT around it.',
363
+ ],
364
+ howToFix: [
365
+ 'Let the first error propagate so the whole transaction fails with its own class, or wrap the step that may fail in a nested `tx.$transaction` (a SAVEPOINT) and catch around that.',
366
+ 'On raw `transaction()`, issue `SAVEPOINT` before the statement that may fail and `ROLLBACK TO SAVEPOINT` when it does.',
367
+ 'Deadlock, serialization and lock-not-available failures keep their own classes and stay retryable; E019 is not, because retrying the same callback repeats the same swallowed failure.',
368
+ ],
369
+ properties: [],
370
+ },
371
+ TURBINE_E020: {
372
+ className: 'InvariantError',
373
+ retryable: false,
374
+ origin: 'turbine',
375
+ whenThrown: 'Turbine caught a bug in itself before sending a statement it knew was wrong: two internal paths that must agree did not, or a branch no argument can reach was reached.',
376
+ likelyCauses: [
377
+ '`invariant: "sql-cache-lockstep"`: the SQL-template cache built a statement whose parameters, or whose cached text, disagree with what a fresh build of the same query produces.',
378
+ '`invariant: "sql-cache-param-count"`: a cached statement was about to run with a different number of parameters than its placeholders.',
379
+ 'Any other `invariant` id names one internal rule; the message says which two things disagreed.',
380
+ ],
381
+ howToFix: [
382
+ 'Report it at https://github.com/zvndev/turbine-orm/issues with the message and `err.invariant`. The message carries the query shape and no row or parameter value.',
383
+ 'Nothing in the call is wrong, so rewriting the arguments is a workaround at best. There is no option that turns these checks off: each one fires only where Turbine would otherwise have sent the wrong SQL or the wrong parameters.',
384
+ 'Do not retry: the same call fails the same way.',
385
+ ],
386
+ properties: ['invariant'],
387
+ },
352
388
  };
353
389
  /** Every code in the catalog, in code order. */
354
390
  exports.CATALOGUED_ERROR_CODES = Object.keys(CATALOG).sort();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The text the CLI prints for a caught error. A zero-import leaf, so every
3
+ * command can use it.
4
+ *
5
+ * Reading `.message` alone is not enough. When `localhost` resolves to both
6
+ * `::1` and `127.0.0.1` and both refuse, Node's `net` rejects with an
7
+ * `AggregateError` whose message is EMPTY: the reasons are on its `errors`.
8
+ * Every command then printed its prefix and nothing else ("Failed to start
9
+ * Studio: "), and the top-level handler, which classifies by message text,
10
+ * could not tell that it was a refused connection.
11
+ *
12
+ * Order: the error's own message; else the messages of the errors it
13
+ * aggregates (each once); else its `cause`; else its `code`; else its name.
14
+ */
15
+ export declare function errorText(err: unknown, depth?: number): string;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.errorText = errorText;
4
+ /**
5
+ * The text the CLI prints for a caught error. A zero-import leaf, so every
6
+ * command can use it.
7
+ *
8
+ * Reading `.message` alone is not enough. When `localhost` resolves to both
9
+ * `::1` and `127.0.0.1` and both refuse, Node's `net` rejects with an
10
+ * `AggregateError` whose message is EMPTY: the reasons are on its `errors`.
11
+ * Every command then printed its prefix and nothing else ("Failed to start
12
+ * Studio: "), and the top-level handler, which classifies by message text,
13
+ * could not tell that it was a refused connection.
14
+ *
15
+ * Order: the error's own message; else the messages of the errors it
16
+ * aggregates (each once); else its `cause`; else its `code`; else its name.
17
+ */
18
+ function errorText(err, depth = 0) {
19
+ if (!(err instanceof Error))
20
+ return String(err);
21
+ if (err.message)
22
+ return err.message;
23
+ if (depth < 4) {
24
+ const inner = err.errors;
25
+ if (Array.isArray(inner)) {
26
+ const parts = [...new Set(inner.map((e) => errorText(e, depth + 1)).filter((t) => t !== ''))];
27
+ if (parts.length > 0)
28
+ return parts.join('; ');
29
+ }
30
+ if (err.cause !== undefined) {
31
+ const cause = errorText(err.cause, depth + 1);
32
+ if (cause !== '')
33
+ return cause;
34
+ }
35
+ }
36
+ const code = err.code;
37
+ if (typeof code === 'string' && code !== '')
38
+ return code;
39
+ return err.name || 'Error';
40
+ }