turbine-orm 0.67.0 → 0.70.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.
- package/dist/cjs/cli/error-catalog.d.ts +77 -0
- package/dist/cjs/cli/error-catalog.js +388 -0
- package/dist/cjs/cli/index.js +3 -2
- package/dist/cjs/cli/mcp.d.ts +19 -3
- package/dist/cjs/cli/mcp.js +709 -22
- package/dist/cjs/cli/migrate.d.ts +19 -2
- package/dist/cjs/cli/observe.d.ts +2 -2
- package/dist/cjs/cli/observe.js +20 -2
- package/dist/cjs/cli/pii-predicate-guard.d.ts +6 -2
- package/dist/cjs/cli/pii-predicate-guard.js +6 -2
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/client.d.ts +30 -75
- package/dist/cjs/client.js +31 -11
- package/dist/cjs/introspect.d.ts +113 -17
- package/dist/cjs/introspect.js +229 -33
- package/dist/cjs/pg-types.d.ts +153 -0
- package/dist/cjs/pg-types.js +38 -0
- package/dist/cjs/pipeline.d.ts +3 -3
- package/dist/cjs/query/batched-loader.d.ts +2 -2
- package/dist/cjs/query/builder.d.ts +2 -2
- package/dist/cjs/query/builder.js +10 -1
- package/dist/cjs/query/deferred.d.ts +6 -6
- package/dist/cjs/query/filters.d.ts +13 -7
- package/dist/cjs/query/filters.js +13 -14
- package/dist/cjs/query/where.d.ts +2 -2
- package/dist/cjs/schema-sql.d.ts +18 -0
- package/dist/cjs/schema-sql.js +18 -0
- package/dist/cli/error-catalog.d.ts +77 -0
- package/dist/cli/error-catalog.js +383 -0
- package/dist/cli/index.js +3 -2
- package/dist/cli/mcp.d.ts +19 -3
- package/dist/cli/mcp.js +709 -23
- package/dist/cli/migrate.d.ts +19 -2
- package/dist/cli/observe.d.ts +2 -2
- package/dist/cli/observe.js +20 -2
- package/dist/cli/pii-predicate-guard.d.ts +6 -2
- package/dist/cli/pii-predicate-guard.js +6 -2
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/client.d.ts +30 -75
- package/dist/client.js +31 -11
- package/dist/introspect.d.ts +113 -17
- package/dist/introspect.js +227 -33
- package/dist/pg-types.d.ts +153 -0
- package/dist/pg-types.js +37 -0
- package/dist/pipeline.d.ts +3 -3
- package/dist/query/batched-loader.d.ts +2 -2
- package/dist/query/builder.d.ts +2 -2
- package/dist/query/builder.js +10 -1
- package/dist/query/deferred.d.ts +6 -6
- package/dist/query/filters.d.ts +13 -7
- package/dist/query/filters.js +13 -13
- package/dist/query/where-compile.js +1 -1
- package/dist/query/where.d.ts +2 -2
- package/dist/schema-sql.d.ts +18 -0
- package/dist/schema-sql.js +18 -0
- package/package.json +19 -7
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm CLI: the error-code catalog behind `explain_error`.
|
|
3
|
+
*
|
|
4
|
+
* A pure leaf module, the same role `cli/rate-limit.ts`, `cli/destructive.ts`
|
|
5
|
+
* and `cli/pii-predicate-guard.ts` play: no I/O, no state, and no import from a
|
|
6
|
+
* sibling CLI module. Its ONE dependency is `../errors.js`, which is where the
|
|
7
|
+
* codes and the docs URL actually live.
|
|
8
|
+
*
|
|
9
|
+
* WHY IT EXISTS. An agent that catches `[TURBINE_E003] Unknown column "titel"`
|
|
10
|
+
* has the code and nothing else. `errors.ts` knows the class and the docs
|
|
11
|
+
* anchor but carries no prose about causes or repairs, and the prose that does
|
|
12
|
+
* exist lives on the docs site, which an offline agent cannot read and which a
|
|
13
|
+
* `WebFetch` of a marketing page is a poor way to consult. This turns "I got
|
|
14
|
+
* E015" into "this is an OptimisticLockError, here is why it fired, here is what
|
|
15
|
+
* to do", with no database, no network, and no tokens spent re-deriving it.
|
|
16
|
+
*
|
|
17
|
+
* TWO RULES KEEP IT HONEST.
|
|
18
|
+
*
|
|
19
|
+
* 1. `docsUrl` is NEVER written out here. It is read off a real
|
|
20
|
+
* {@link TurbineError} instance, so the URL an agent is handed is
|
|
21
|
+
* byte-identical to the one on the error it actually caught. A second
|
|
22
|
+
* hand-maintained copy of `docsUrlForCode` is exactly the drift this repo
|
|
23
|
+
* has been bitten by before.
|
|
24
|
+
* 2. The catalog is keyed by `TurbineErrorCode`, exhaustively. Adding a code to
|
|
25
|
+
* `errors.ts` without adding a row here FAILS THE BUILD (the mapped type
|
|
26
|
+
* below requires every key), and a row for a code that no longer exists
|
|
27
|
+
* fails as an excess property. Same stance as `query/option-surface.ts`:
|
|
28
|
+
* a human classifies the new thing, the compiler notices when nobody did.
|
|
29
|
+
*/
|
|
30
|
+
import { type TurbineErrorCode } from '../errors.js';
|
|
31
|
+
/** One code's explanation, as `explain_error` returns it. */
|
|
32
|
+
export interface ErrorExplanation {
|
|
33
|
+
/** The canonical code, e.g. `TURBINE_E003`. */
|
|
34
|
+
code: TurbineErrorCode;
|
|
35
|
+
/** The exported class name, e.g. `ValidationError`. */
|
|
36
|
+
className: string;
|
|
37
|
+
/** Anchor into the published error table. Sourced from a real error instance. */
|
|
38
|
+
docsUrl: string;
|
|
39
|
+
/** True only for the two errors carrying `isRetryable: true as const`. */
|
|
40
|
+
retryable: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* How the error reaches you: raised by Turbine itself, or translated from a
|
|
43
|
+
* PostgreSQL SQLSTATE by `wrapPgError()`. An agent chasing a `wrapped` error
|
|
44
|
+
* should be reading the database's constraint, not Turbine's call site.
|
|
45
|
+
*/
|
|
46
|
+
origin: 'turbine' | 'wrapped-pg';
|
|
47
|
+
/** The SQLSTATE `wrapPgError()` maps, for `origin: 'wrapped-pg'`. */
|
|
48
|
+
sqlstate?: string;
|
|
49
|
+
/** One sentence: the condition that raises it. */
|
|
50
|
+
whenThrown: string;
|
|
51
|
+
/** The concrete situations that produce it, most common first. */
|
|
52
|
+
likelyCauses: string[];
|
|
53
|
+
/** What to change, in the same order. */
|
|
54
|
+
howToFix: string[];
|
|
55
|
+
/** Extra own properties the error carries beyond `code` / `message` / `docsUrl`. */
|
|
56
|
+
properties: string[];
|
|
57
|
+
}
|
|
58
|
+
/** Every code in the catalog, in code order. */
|
|
59
|
+
export declare const CATALOGUED_ERROR_CODES: TurbineErrorCode[];
|
|
60
|
+
/**
|
|
61
|
+
* Accept the spellings a caller actually types and return the canonical code,
|
|
62
|
+
* or `null`.
|
|
63
|
+
*
|
|
64
|
+
* A code arrives from a log line (`TURBINE_E003`), from a docs anchor (`e003`),
|
|
65
|
+
* from prose ("E3"), or as a bare number. All of them mean the same code, and
|
|
66
|
+
* refusing three of the four teaches an agent to give up rather than to
|
|
67
|
+
* normalize. What is NOT accepted is anything that resolves to no code at all:
|
|
68
|
+
* the caller gets `null` and a list, never a guess.
|
|
69
|
+
*/
|
|
70
|
+
export declare function normalizeErrorCode(input: string): TurbineErrorCode | null;
|
|
71
|
+
/**
|
|
72
|
+
* The full explanation for a code, or `null` when the input names no code.
|
|
73
|
+
*
|
|
74
|
+
* `docsUrl` is read off a real {@link TurbineError} rather than formatted here,
|
|
75
|
+
* so it cannot drift from the URL on the error an agent actually caught.
|
|
76
|
+
*/
|
|
77
|
+
export declare function explainErrorCode(input: string): ErrorExplanation | null;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm CLI: the error-code catalog behind `explain_error`.
|
|
4
|
+
*
|
|
5
|
+
* A pure leaf module, the same role `cli/rate-limit.ts`, `cli/destructive.ts`
|
|
6
|
+
* and `cli/pii-predicate-guard.ts` play: no I/O, no state, and no import from a
|
|
7
|
+
* sibling CLI module. Its ONE dependency is `../errors.js`, which is where the
|
|
8
|
+
* codes and the docs URL actually live.
|
|
9
|
+
*
|
|
10
|
+
* WHY IT EXISTS. An agent that catches `[TURBINE_E003] Unknown column "titel"`
|
|
11
|
+
* has the code and nothing else. `errors.ts` knows the class and the docs
|
|
12
|
+
* anchor but carries no prose about causes or repairs, and the prose that does
|
|
13
|
+
* exist lives on the docs site, which an offline agent cannot read and which a
|
|
14
|
+
* `WebFetch` of a marketing page is a poor way to consult. This turns "I got
|
|
15
|
+
* E015" into "this is an OptimisticLockError, here is why it fired, here is what
|
|
16
|
+
* to do", with no database, no network, and no tokens spent re-deriving it.
|
|
17
|
+
*
|
|
18
|
+
* TWO RULES KEEP IT HONEST.
|
|
19
|
+
*
|
|
20
|
+
* 1. `docsUrl` is NEVER written out here. It is read off a real
|
|
21
|
+
* {@link TurbineError} instance, so the URL an agent is handed is
|
|
22
|
+
* byte-identical to the one on the error it actually caught. A second
|
|
23
|
+
* hand-maintained copy of `docsUrlForCode` is exactly the drift this repo
|
|
24
|
+
* has been bitten by before.
|
|
25
|
+
* 2. The catalog is keyed by `TurbineErrorCode`, exhaustively. Adding a code to
|
|
26
|
+
* `errors.ts` without adding a row here FAILS THE BUILD (the mapped type
|
|
27
|
+
* below requires every key), and a row for a code that no longer exists
|
|
28
|
+
* fails as an excess property. Same stance as `query/option-surface.ts`:
|
|
29
|
+
* a human classifies the new thing, the compiler notices when nobody did.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.CATALOGUED_ERROR_CODES = void 0;
|
|
33
|
+
exports.normalizeErrorCode = normalizeErrorCode;
|
|
34
|
+
exports.explainErrorCode = explainErrorCode;
|
|
35
|
+
const errors_js_1 = require("../errors.js");
|
|
36
|
+
/**
|
|
37
|
+
* Every code, explained. Exhaustive by construction: `Record<TurbineErrorCode,
|
|
38
|
+
* …>` means a new code in `errors.ts` does not compile until it is written up
|
|
39
|
+
* here, and a code removed there leaves an excess property that also does not
|
|
40
|
+
* compile.
|
|
41
|
+
*/
|
|
42
|
+
const CATALOG = {
|
|
43
|
+
TURBINE_E001: {
|
|
44
|
+
className: 'NotFoundError',
|
|
45
|
+
retryable: false,
|
|
46
|
+
origin: 'turbine',
|
|
47
|
+
whenThrown: 'A query that promises a row did not find one.',
|
|
48
|
+
likelyCauses: [
|
|
49
|
+
'`findUniqueOrThrow` / `findFirstOrThrow` matched no row.',
|
|
50
|
+
'`update` / `delete` addressed a row that does not exist, or that a global filter excludes.',
|
|
51
|
+
'The where clause names the right column but the wrong value (a stale id, a string/number mismatch on the key).',
|
|
52
|
+
'A tenant/global filter configured on the client narrowed the query to zero rows without the caller knowing.',
|
|
53
|
+
],
|
|
54
|
+
howToFix: [
|
|
55
|
+
'Use `findUnique` / `findFirst` and branch on `null` when absence is a normal outcome.',
|
|
56
|
+
'Read `err.table`, `err.where` and `err.operation` to see exactly what was addressed; the message redacts the VALUES unless the client is on `errorMessages: "verbose"`.',
|
|
57
|
+
'If a global filter is in play, re-run the same query with `skipGlobalFilters: UNSAFE` to confirm the row exists but is filtered.',
|
|
58
|
+
],
|
|
59
|
+
properties: ['table', 'where', 'operation'],
|
|
60
|
+
},
|
|
61
|
+
TURBINE_E002: {
|
|
62
|
+
className: 'TimeoutError',
|
|
63
|
+
retryable: false,
|
|
64
|
+
origin: 'turbine',
|
|
65
|
+
whenThrown: 'A query or transaction ran past its configured timeout.',
|
|
66
|
+
likelyCauses: [
|
|
67
|
+
'A missing index turned a relation probe or a where clause into a sequential scan.',
|
|
68
|
+
'A transaction held a lock another transaction was waiting on.',
|
|
69
|
+
'The timeout is simply lower than the work (a large `createMany`, an unbounded `findMany`).',
|
|
70
|
+
],
|
|
71
|
+
howToFix: [
|
|
72
|
+
'Run `doctor_report` for missing relation indexes, and `explain_query` on the shape that timed out.',
|
|
73
|
+
'Read `err.timeoutMs` for the limit that was hit; raise it per query with the `timeout` option rather than globally.',
|
|
74
|
+
'Bound the read: add a `limit`, or paginate.',
|
|
75
|
+
],
|
|
76
|
+
properties: ['timeoutMs'],
|
|
77
|
+
},
|
|
78
|
+
TURBINE_E003: {
|
|
79
|
+
className: 'ValidationError',
|
|
80
|
+
retryable: false,
|
|
81
|
+
origin: 'turbine',
|
|
82
|
+
whenThrown: 'The query args were rejected before any SQL ran.',
|
|
83
|
+
likelyCauses: [
|
|
84
|
+
'A name that resolves to no column: in `where`, `orderBy`, `distinct`, a `groupBy` `by` key, an aggregate target, create/update `data`, or (since 0.64) `select` / `omit` at any depth.',
|
|
85
|
+
'A relation named in `select` or `omit`. Relations load through `with`, which is a SIBLING of `select`, not a member of it.',
|
|
86
|
+
'A projection shape that selects nothing: an empty or all-false `select`, or `select` and `omit` on the same query.',
|
|
87
|
+
'The empty-where guard: `update` / `delete` / `updateMany` / `deleteMany` with `{}` or an all-undefined where.',
|
|
88
|
+
'An aggregate over a PII-tagged column (`_min` / `_max`, or a PII column as a `groupBy` key) with no `includePii`.',
|
|
89
|
+
'A privilege option (`includePii`, `skipGlobalFilters`, `allowFullTableScan`) passed `true` instead of the `UNSAFE` symbol.',
|
|
90
|
+
'`forceCustomPlan` against a client pinned to `planCacheMode: "force_generic_plan"`.',
|
|
91
|
+
],
|
|
92
|
+
howToFix: [
|
|
93
|
+
'Read the message: it names the table and, where it can, the column you probably meant.',
|
|
94
|
+
'For a relation, move it out of `select` and into `with: { name: true }`.',
|
|
95
|
+
'For a mass mutation you really do want, pass `allowFullTableScan: UNSAFE` explicitly.',
|
|
96
|
+
'Import `UNSAFE` from the package root for any privilege option; `JSON.parse` cannot produce a symbol, which is the point.',
|
|
97
|
+
],
|
|
98
|
+
properties: [],
|
|
99
|
+
},
|
|
100
|
+
TURBINE_E004: {
|
|
101
|
+
className: 'ConnectionError',
|
|
102
|
+
retryable: false,
|
|
103
|
+
origin: 'turbine',
|
|
104
|
+
whenThrown: 'The pool could not establish or keep a connection.',
|
|
105
|
+
likelyCauses: [
|
|
106
|
+
'Nothing is listening (ECONNREFUSED), DNS failed (ENOTFOUND / EAI_AGAIN), or the connect timed out (ETIMEDOUT).',
|
|
107
|
+
'TLS: a self-signed or expired certificate, or a hostname the certificate does not cover.',
|
|
108
|
+
'The connection string is malformed, or points at a pooler port the server is not on.',
|
|
109
|
+
'The server closed an idle pooled connection (proxy idle timeout, restart, failover).',
|
|
110
|
+
],
|
|
111
|
+
howToFix: [
|
|
112
|
+
'Read `err.sqlstate` and the hint in the message: each driver code carries its own next step.',
|
|
113
|
+
'Check the host, port and database name; for TLS, supply the CA via `ssl: { ca }` rather than disabling verification.',
|
|
114
|
+
'For a serverless driver, pass the external pool on `TurbineConfig.pool` instead of a connection string.',
|
|
115
|
+
],
|
|
116
|
+
properties: ['sqlstate'],
|
|
117
|
+
},
|
|
118
|
+
TURBINE_E005: {
|
|
119
|
+
className: 'RelationError',
|
|
120
|
+
retryable: false,
|
|
121
|
+
origin: 'turbine',
|
|
122
|
+
whenThrown: 'A `with` clause named a relation the schema does not declare.',
|
|
123
|
+
likelyCauses: [
|
|
124
|
+
'A typo, or a Prisma-style name that Turbine derives differently.',
|
|
125
|
+
'The generated metadata is stale: the relation exists in the database but `turbine generate` has not been re-run.',
|
|
126
|
+
'A UNIQUE foreign key, which Turbine derives as a singular `hasOne` (`user.profile`) rather than a plural `hasMany` (`user.profiles`).',
|
|
127
|
+
],
|
|
128
|
+
howToFix: [
|
|
129
|
+
'Call `relation_graph` (optionally scoped with `table`) and read the relation names Turbine actually derived, rather than guessing them.',
|
|
130
|
+
'Call `find_join_path` for the `with` clause to write, instead of assembling one by hand.',
|
|
131
|
+
'Re-run `turbine generate` if the database has changed.',
|
|
132
|
+
],
|
|
133
|
+
properties: [],
|
|
134
|
+
},
|
|
135
|
+
TURBINE_E006: {
|
|
136
|
+
className: 'MigrationError',
|
|
137
|
+
retryable: false,
|
|
138
|
+
origin: 'turbine',
|
|
139
|
+
whenThrown: 'A migration could not be parsed, verified, or applied.',
|
|
140
|
+
likelyCauses: [
|
|
141
|
+
'A checksum mismatch: an already-applied migration file was edited after the fact.',
|
|
142
|
+
'A migration file with no `-- UP` section, or an unparseable one.',
|
|
143
|
+
'The advisory migration lock was already held by a concurrent runner.',
|
|
144
|
+
'The SQL itself failed; the migration is rolled back, and the underlying error is the `cause`.',
|
|
145
|
+
],
|
|
146
|
+
howToFix: [
|
|
147
|
+
'Run `migrate_status` to see applied / pending / drifted counts and which file drifted.',
|
|
148
|
+
'Never edit an applied migration. Write a new one that makes the correction.',
|
|
149
|
+
'For lock contention, wait for the other runner; `migrate deploy` is the no-prompt CI form.',
|
|
150
|
+
],
|
|
151
|
+
properties: [],
|
|
152
|
+
},
|
|
153
|
+
TURBINE_E007: {
|
|
154
|
+
className: 'CircularRelationError',
|
|
155
|
+
retryable: false,
|
|
156
|
+
origin: 'turbine',
|
|
157
|
+
whenThrown: 'A `with` clause nested more than 10 relation levels deep.',
|
|
158
|
+
likelyCauses: [
|
|
159
|
+
'A back-reference walked in a loop: `user -> posts -> user -> posts -> …`.',
|
|
160
|
+
'A genuinely deep tree built by concatenating `with` fragments programmatically.',
|
|
161
|
+
],
|
|
162
|
+
howToFix: [
|
|
163
|
+
'Read `err.path` for the exact trail that hit the cap.',
|
|
164
|
+
'Back-references are legal; the cap is on DEPTH, so re-root the query at the level you actually need instead of walking back up.',
|
|
165
|
+
'Split one very deep read into two shallower queries.',
|
|
166
|
+
],
|
|
167
|
+
properties: ['path'],
|
|
168
|
+
},
|
|
169
|
+
TURBINE_E008: {
|
|
170
|
+
className: 'UniqueConstraintError',
|
|
171
|
+
retryable: false,
|
|
172
|
+
origin: 'wrapped-pg',
|
|
173
|
+
sqlstate: '23505',
|
|
174
|
+
whenThrown: 'A write violated a unique constraint or unique index.',
|
|
175
|
+
likelyCauses: [
|
|
176
|
+
'An insert of a value that already exists (the classic duplicate email).',
|
|
177
|
+
'An update that moved a row onto an existing key.',
|
|
178
|
+
'A race: two concurrent inserts of the same key, where a read-then-insert check passed in both.',
|
|
179
|
+
],
|
|
180
|
+
howToFix: [
|
|
181
|
+
'Read `err.constraint`, `err.columns` and `err.table` and map the constraint to a user-facing message (HTTP 409).',
|
|
182
|
+
'Use `upsert`, or `createMany({ skipDuplicates: true })`, instead of check-then-insert.',
|
|
183
|
+
'Never branch on the message text; branch on `err.code` or `instanceof UniqueConstraintError`.',
|
|
184
|
+
],
|
|
185
|
+
properties: ['constraint', 'columns', 'table'],
|
|
186
|
+
},
|
|
187
|
+
TURBINE_E009: {
|
|
188
|
+
className: 'ForeignKeyError',
|
|
189
|
+
retryable: false,
|
|
190
|
+
origin: 'wrapped-pg',
|
|
191
|
+
sqlstate: '23503',
|
|
192
|
+
whenThrown: 'A write violated a foreign key constraint.',
|
|
193
|
+
likelyCauses: [
|
|
194
|
+
'An insert or update pointing a foreign key at a parent row that does not exist.',
|
|
195
|
+
'A delete of a parent row that still has children, where the constraint is `NO ACTION` / `RESTRICT`.',
|
|
196
|
+
'Rows written in the wrong order inside a transaction.',
|
|
197
|
+
],
|
|
198
|
+
howToFix: [
|
|
199
|
+
'Read `err.constraint` and `err.table` to see which side failed.',
|
|
200
|
+
'Use a nested write (`data: { child: { create: … } }`), which orders the inserts for you inside one transaction.',
|
|
201
|
+
'To delete a parent, delete or re-point its children first, or declare `ON DELETE CASCADE`.',
|
|
202
|
+
],
|
|
203
|
+
properties: ['constraint', 'table'],
|
|
204
|
+
},
|
|
205
|
+
TURBINE_E010: {
|
|
206
|
+
className: 'NotNullViolationError',
|
|
207
|
+
retryable: false,
|
|
208
|
+
origin: 'wrapped-pg',
|
|
209
|
+
sqlstate: '23502',
|
|
210
|
+
whenThrown: 'A write left a NOT NULL column with no value.',
|
|
211
|
+
likelyCauses: [
|
|
212
|
+
'A required column omitted from `data`.',
|
|
213
|
+
'An explicit `null` written to a NOT NULL column.',
|
|
214
|
+
'A migration that added a NOT NULL column with no default while old code still inserts without it.',
|
|
215
|
+
],
|
|
216
|
+
howToFix: [
|
|
217
|
+
'Read `err.column` and `err.table` for the exact column.',
|
|
218
|
+
'Supply the value, or give the column a database default.',
|
|
219
|
+
'For an existing table, add the column nullable, backfill, then `SET NOT NULL` (`turbine migrate create <name> --recipe backfill` scaffolds this).',
|
|
220
|
+
],
|
|
221
|
+
properties: ['column', 'table'],
|
|
222
|
+
},
|
|
223
|
+
TURBINE_E011: {
|
|
224
|
+
className: 'CheckConstraintError',
|
|
225
|
+
retryable: false,
|
|
226
|
+
origin: 'wrapped-pg',
|
|
227
|
+
sqlstate: '23514',
|
|
228
|
+
whenThrown: 'A write violated a CHECK constraint.',
|
|
229
|
+
likelyCauses: [
|
|
230
|
+
'A value outside the range the constraint allows (a negative quantity, an out-of-set status string).',
|
|
231
|
+
'An atomic update operator (`decrement`) driving a column past a bound the constraint enforces.',
|
|
232
|
+
],
|
|
233
|
+
howToFix: [
|
|
234
|
+
'Read `err.constraint` and `err.table`, then read the constraint body: `table_detail` reports named check constraints where the schema declares them.',
|
|
235
|
+
'Validate in application code before the write, so the user gets a field-level message rather than a 500.',
|
|
236
|
+
],
|
|
237
|
+
properties: ['constraint', 'table'],
|
|
238
|
+
},
|
|
239
|
+
TURBINE_E012: {
|
|
240
|
+
className: 'DeadlockError',
|
|
241
|
+
retryable: true,
|
|
242
|
+
origin: 'wrapped-pg',
|
|
243
|
+
sqlstate: '40P01',
|
|
244
|
+
whenThrown: 'PostgreSQL detected a deadlock and cancelled this transaction.',
|
|
245
|
+
likelyCauses: [
|
|
246
|
+
'Two transactions locking the same rows in opposite order.',
|
|
247
|
+
'A long transaction holding a lock while doing unrelated work.',
|
|
248
|
+
],
|
|
249
|
+
howToFix: [
|
|
250
|
+
'Retry. `err.isRetryable === true`, and `withRetry(fn)` / `db.$retry(fn)` retries exactly the errors carrying that flag.',
|
|
251
|
+
'Lock rows in a consistent order across code paths (for example, always ascending by primary key).',
|
|
252
|
+
'Shorten transactions: do the I/O and the computation outside, the writes inside.',
|
|
253
|
+
],
|
|
254
|
+
properties: ['isRetryable', 'constraint'],
|
|
255
|
+
},
|
|
256
|
+
TURBINE_E013: {
|
|
257
|
+
className: 'SerializationFailureError',
|
|
258
|
+
retryable: true,
|
|
259
|
+
origin: 'wrapped-pg',
|
|
260
|
+
sqlstate: '40001',
|
|
261
|
+
whenThrown: 'A SERIALIZABLE or REPEATABLE READ transaction could not be serialized.',
|
|
262
|
+
likelyCauses: [
|
|
263
|
+
'Concurrent transactions at `Serializable` touching an overlapping row set.',
|
|
264
|
+
'A read-modify-write on a hot row under `Repeatable Read`.',
|
|
265
|
+
],
|
|
266
|
+
howToFix: [
|
|
267
|
+
'Retry: this is the expected, designed outcome at these isolation levels, not a bug. `withRetry(fn)` / `db.$retry(fn)` handles it.',
|
|
268
|
+
'Where the operation is a pure increment, use an atomic update operator (`{ increment: 1 }`) instead of read-then-write.',
|
|
269
|
+
'Consider whether the transaction genuinely needs `Serializable`.',
|
|
270
|
+
],
|
|
271
|
+
properties: ['isRetryable'],
|
|
272
|
+
},
|
|
273
|
+
TURBINE_E014: {
|
|
274
|
+
className: 'PipelineError',
|
|
275
|
+
retryable: false,
|
|
276
|
+
origin: 'turbine',
|
|
277
|
+
whenThrown: 'A non-transactional pipeline (`{ transactional: false }`) had at least one failing query.',
|
|
278
|
+
likelyCauses: ['One query in the batch failed while others succeeded, so there is no single error to throw.'],
|
|
279
|
+
howToFix: [
|
|
280
|
+
'Read `err.results`: one slot per query, each `{ status: "ok", value }` or `{ status: "error", error }`, with the real typed error inside.',
|
|
281
|
+
'`err.failedIndex` and `err.failedTag` point at the first failure.',
|
|
282
|
+
'If partial success is not acceptable, drop `transactional: false`; a transactional pipeline either fully succeeds or rolls back.',
|
|
283
|
+
],
|
|
284
|
+
properties: ['results', 'failedIndex', 'failedTag'],
|
|
285
|
+
},
|
|
286
|
+
TURBINE_E015: {
|
|
287
|
+
className: 'OptimisticLockError',
|
|
288
|
+
retryable: false,
|
|
289
|
+
origin: 'turbine',
|
|
290
|
+
whenThrown: 'An `optimisticLock` update found no row at the expected version.',
|
|
291
|
+
likelyCauses: [
|
|
292
|
+
'Another transaction updated the row between your read and your write. This is the mechanism working.',
|
|
293
|
+
'The version value passed was stale (held across a user interaction, or cached).',
|
|
294
|
+
'The row was deleted.',
|
|
295
|
+
],
|
|
296
|
+
howToFix: [
|
|
297
|
+
'Re-read the row, re-apply the change to the fresh values, and write again. Do NOT blindly retry the same payload: the point of the check is that the underlying data moved.',
|
|
298
|
+
'Read `err.table`, `err.versionField` and `err.expectedVersion` to report the conflict to the user.',
|
|
299
|
+
'It is deliberately NOT flagged retryable: an automatic retry would defeat the guard.',
|
|
300
|
+
],
|
|
301
|
+
properties: ['table', 'versionField', 'expectedVersion'],
|
|
302
|
+
},
|
|
303
|
+
TURBINE_E016: {
|
|
304
|
+
className: 'ExclusionConstraintError',
|
|
305
|
+
retryable: false,
|
|
306
|
+
origin: 'wrapped-pg',
|
|
307
|
+
sqlstate: '23P01',
|
|
308
|
+
whenThrown: 'A write violated an EXCLUDE constraint.',
|
|
309
|
+
likelyCauses: [
|
|
310
|
+
'Overlapping ranges where the constraint forbids overlap (the canonical booking / reservation clash).',
|
|
311
|
+
],
|
|
312
|
+
howToFix: [
|
|
313
|
+
'Read `err.constraint` and `err.table`, then translate the clash into a domain message ("that slot is taken").',
|
|
314
|
+
'Query for the conflicting row with a range-overlap filter so the user can be shown WHAT it clashes with.',
|
|
315
|
+
],
|
|
316
|
+
properties: ['constraint', 'table'],
|
|
317
|
+
},
|
|
318
|
+
TURBINE_E017: {
|
|
319
|
+
className: 'UnsupportedFeatureError',
|
|
320
|
+
retryable: false,
|
|
321
|
+
origin: 'turbine',
|
|
322
|
+
whenThrown: 'This build cannot do that: a capability flag on the active dialect reports the feature unsupported, so Turbine refuses instead of emitting broken SQL.',
|
|
323
|
+
likelyCauses: [
|
|
324
|
+
'A Postgres-only feature on another engine: pgvector distance operators, `$listen` / `$notify`, RLS `sessionContext` / `$withSession`, `planCacheMode`.',
|
|
325
|
+
'NOT only a wrong-engine error, and reading it that way sends you looking in the wrong place. PostgreSQL raises it too: `relationLoadStrategy: "batched"` on a COMPOSITE-key relation refuses rather than loading a wrong set.',
|
|
326
|
+
'On PowDB: a nested `$transaction`, a re-entrant `$transaction`, or a feature above the connected engine version (the message carries the upgrade hint).',
|
|
327
|
+
'`limit` on `updateMany` / `deleteMany` through the prisma-compat adapter.',
|
|
328
|
+
],
|
|
329
|
+
howToFix: [
|
|
330
|
+
'Read `err.feature` and `err.dialect`: together they say exactly what was refused and by which engine.',
|
|
331
|
+
'For a composite-key relation, use the default join plan rather than `batched`.',
|
|
332
|
+
'For a version gate, upgrade the engine to the version named in the hint.',
|
|
333
|
+
],
|
|
334
|
+
properties: ['feature', 'dialect'],
|
|
335
|
+
},
|
|
336
|
+
TURBINE_E018: {
|
|
337
|
+
className: 'ReadOnlyError',
|
|
338
|
+
retryable: false,
|
|
339
|
+
origin: 'turbine',
|
|
340
|
+
whenThrown: 'A write was refused because the database or the connection is read-only.',
|
|
341
|
+
likelyCauses: [
|
|
342
|
+
'`reason: "snapshot"`: the client was opened `readonly: true`, or PowDB is serving a read-only snapshot.',
|
|
343
|
+
'`reason: "rbac"`: the connected role has no write privilege.',
|
|
344
|
+
],
|
|
345
|
+
howToFix: [
|
|
346
|
+
'Read `err.reason` first: the two causes have nothing in common except the refusal.',
|
|
347
|
+
'For `snapshot`, open a writable client; the read-only one is doing its job.',
|
|
348
|
+
'For `rbac`, grant the role the privilege, or connect as one that has it.',
|
|
349
|
+
],
|
|
350
|
+
properties: ['reason'],
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
/** Every code in the catalog, in code order. */
|
|
354
|
+
exports.CATALOGUED_ERROR_CODES = Object.keys(CATALOG).sort();
|
|
355
|
+
/**
|
|
356
|
+
* Accept the spellings a caller actually types and return the canonical code,
|
|
357
|
+
* or `null`.
|
|
358
|
+
*
|
|
359
|
+
* A code arrives from a log line (`TURBINE_E003`), from a docs anchor (`e003`),
|
|
360
|
+
* from prose ("E3"), or as a bare number. All of them mean the same code, and
|
|
361
|
+
* refusing three of the four teaches an agent to give up rather than to
|
|
362
|
+
* normalize. What is NOT accepted is anything that resolves to no code at all:
|
|
363
|
+
* the caller gets `null` and a list, never a guess.
|
|
364
|
+
*/
|
|
365
|
+
function normalizeErrorCode(input) {
|
|
366
|
+
const trimmed = input.trim().toUpperCase().replace(/\s+/g, '');
|
|
367
|
+
// `TURBINE_E003` / `TURBINE-E003` / `TURBINEE003` / `E003` / `E3` / `003` / `3`
|
|
368
|
+
const match = /^(?:TURBINE[_-]?)?E?(\d{1,4})$/.exec(trimmed);
|
|
369
|
+
if (!match)
|
|
370
|
+
return null;
|
|
371
|
+
const n = Number(match[1]);
|
|
372
|
+
if (!Number.isInteger(n) || n < 1)
|
|
373
|
+
return null;
|
|
374
|
+
const code = `TURBINE_E${String(n).padStart(3, '0')}`;
|
|
375
|
+
return Object.hasOwn(CATALOG, code) ? code : null;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The full explanation for a code, or `null` when the input names no code.
|
|
379
|
+
*
|
|
380
|
+
* `docsUrl` is read off a real {@link TurbineError} rather than formatted here,
|
|
381
|
+
* so it cannot drift from the URL on the error an agent actually caught.
|
|
382
|
+
*/
|
|
383
|
+
function explainErrorCode(input) {
|
|
384
|
+
const code = normalizeErrorCode(input);
|
|
385
|
+
if (!code)
|
|
386
|
+
return null;
|
|
387
|
+
return { code, docsUrl: new errors_js_1.TurbineError(code, '').docsUrl, ...CATALOG[code] };
|
|
388
|
+
}
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -3917,8 +3917,9 @@ function showMcpHelp() {
|
|
|
3917
3917
|
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
3918
3918
|
console.log(` npx turbine mcp ${(0, ui_js_1.dim)('[options]')}`);
|
|
3919
3919
|
(0, ui_js_1.newline)();
|
|
3920
|
-
console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes`);
|
|
3921
|
-
console.log(` schema,
|
|
3920
|
+
console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes ten`);
|
|
3921
|
+
console.log(` read-only tools: schema, relation graph, join paths, migration status,`);
|
|
3922
|
+
console.log(` doctor, EXPLAIN, table stats, sample rows, and error lookup.`);
|
|
3922
3923
|
(0, ui_js_1.newline)();
|
|
3923
3924
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
3924
3925
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
package/dist/cjs/cli/mcp.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Readable, Writable } from 'node:stream';
|
|
2
|
-
import
|
|
3
|
-
import { type ColumnMetadata, type IndexMetadata, type RelationDef } from '../schema.js';
|
|
2
|
+
import type { PgCompatPool } from '../pg-types.js';
|
|
3
|
+
import { type ColumnMetadata, type IndexMetadata, type RelationDef, type SchemaMetadata } from '../schema.js';
|
|
4
4
|
export interface McpServerOptions {
|
|
5
5
|
url: string;
|
|
6
6
|
schema: string;
|
|
@@ -24,12 +24,28 @@ export interface McpTransport {
|
|
|
24
24
|
* reason Studio exports `handleRequest`). Production never sets it: the
|
|
25
25
|
* server builds its own pool from `options.url`.
|
|
26
26
|
*/
|
|
27
|
-
pool?:
|
|
27
|
+
pool?: PgCompatPool;
|
|
28
28
|
}
|
|
29
29
|
export interface McpServerHandle {
|
|
30
30
|
dispose(): Promise<void>;
|
|
31
31
|
}
|
|
32
32
|
export declare function startMcpServer(options: McpServerOptions, transport?: McpTransport): McpServerHandle;
|
|
33
|
+
/**
|
|
34
|
+
* Every SHORTEST relation chain from `from` to `to`, in deterministic order.
|
|
35
|
+
*
|
|
36
|
+
* Enumerated over BFS distances rather than by depth-first search with a visited
|
|
37
|
+
* set: only edges that advance the distance by exactly one are followed, so
|
|
38
|
+
* every chain returned is the same (minimum) length and no chain revisits a
|
|
39
|
+
* table. Two foreign keys to the same table therefore come back as two paths of
|
|
40
|
+
* equal length, which is the case the caller most needs to see, because picking
|
|
41
|
+
* one arbitrarily is how you silently join through `editor` when you meant
|
|
42
|
+
* `author`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shortestJoinPaths(metadata: SchemaMetadata, from: string, to: string, maxDepth: number, maxPaths: number, nodeBudget?: number): {
|
|
45
|
+
paths: RelationDef[][];
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
exhausted: boolean;
|
|
48
|
+
};
|
|
33
49
|
interface ForeignKeyRow {
|
|
34
50
|
/**
|
|
35
51
|
* `pg_constraint.oid` as text. THE grouping key: a constraint NAME is unique
|