turbine-orm 0.77.1 → 0.78.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/README.md +13 -9
- package/dist/cjs/cli/config.d.ts +7 -1
- package/dist/cjs/cli/config.js +11 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +307 -9
- package/dist/cjs/cli/index.js +252 -42
- package/dist/cjs/cli/mcp.d.ts +23 -0
- package/dist/cjs/cli/mcp.js +190 -152
- package/dist/cjs/cli/migrate.d.ts +243 -3
- package/dist/cjs/cli/migrate.js +432 -43
- package/dist/cjs/cli/sql-statements.js +27 -0
- package/dist/cjs/cli/studio.d.ts +0 -1
- package/dist/cjs/cli/studio.js +9 -7
- package/dist/cjs/client.d.ts +8 -1
- package/dist/cjs/client.js +7 -0
- package/dist/cjs/connection-url.d.ts +82 -0
- package/dist/cjs/connection-url.js +187 -1
- package/dist/cjs/errors.d.ts +112 -12
- package/dist/cjs/errors.js +558 -114
- package/dist/cjs/generate.js +47 -15
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/introspect.d.ts +33 -0
- package/dist/cjs/introspect.js +54 -1
- package/dist/cjs/mssql.js +21 -1
- package/dist/cjs/nested-write.js +85 -14
- package/dist/cjs/pipeline-submittable.d.ts +2 -0
- package/dist/cjs/pipeline-submittable.js +88 -3
- package/dist/cjs/pipeline.js +13 -1
- package/dist/cjs/powdb-introspect.d.ts +5 -1
- package/dist/cjs/powdb-introspect.js +5 -1
- package/dist/cjs/powql.d.ts +14 -0
- package/dist/cjs/powql.js +44 -4
- package/dist/cjs/prisma-compat.js +95 -8
- package/dist/cjs/query/aggregates.js +56 -6
- package/dist/cjs/query/builder.d.ts +76 -13
- package/dist/cjs/query/builder.js +188 -58
- package/dist/cjs/query/compound-unique.d.ts +76 -0
- package/dist/cjs/query/compound-unique.js +129 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/types.d.ts +65 -11
- package/dist/cjs/query/where.d.ts +85 -19
- package/dist/cjs/query/where.js +262 -47
- package/dist/cjs/query/writes.d.ts +11 -2
- package/dist/cjs/query/writes.js +116 -21
- package/dist/cjs/seed.d.ts +16 -0
- package/dist/cjs/seed.js +16 -0
- package/dist/cli/config.d.ts +7 -1
- package/dist/cli/config.js +11 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +307 -9
- package/dist/cli/index.js +254 -44
- package/dist/cli/mcp.d.ts +23 -0
- package/dist/cli/mcp.js +187 -150
- package/dist/cli/migrate.d.ts +243 -3
- package/dist/cli/migrate.js +423 -45
- package/dist/cli/sql-statements.js +27 -0
- package/dist/cli/studio.d.ts +0 -1
- package/dist/cli/studio.js +10 -7
- package/dist/client.d.ts +8 -1
- package/dist/client.js +7 -0
- package/dist/connection-url.d.ts +82 -0
- package/dist/connection-url.js +183 -0
- package/dist/errors.d.ts +112 -12
- package/dist/errors.js +558 -114
- package/dist/generate.js +47 -15
- package/dist/index.d.ts +1 -1
- package/dist/introspect.d.ts +33 -0
- package/dist/introspect.js +53 -1
- package/dist/mssql.js +21 -1
- package/dist/nested-write.js +85 -14
- package/dist/pipeline-submittable.d.ts +2 -0
- package/dist/pipeline-submittable.js +87 -3
- package/dist/pipeline.js +14 -2
- package/dist/powdb-introspect.d.ts +5 -1
- package/dist/powdb-introspect.js +5 -1
- package/dist/powql.d.ts +14 -0
- package/dist/powql.js +45 -5
- package/dist/prisma-compat.js +96 -9
- package/dist/query/aggregates.js +56 -6
- package/dist/query/builder.d.ts +76 -13
- package/dist/query/builder.js +188 -58
- package/dist/query/compound-unique.d.ts +76 -0
- package/dist/query/compound-unique.js +126 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +65 -11
- package/dist/query/where.d.ts +85 -19
- package/dist/query/where.js +260 -47
- package/dist/query/writes.d.ts +11 -2
- package/dist/query/writes.js +117 -22
- package/dist/seed.d.ts +16 -0
- package/dist/seed.js +16 -0
- package/package.json +3 -3
- package/skills/turbine-orm/SKILL.md +37 -10
package/dist/cjs/cli/mcp.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_STDIO_BUFFER_BYTES = void 0;
|
|
6
7
|
exports.startMcpServer = startMcpServer;
|
|
7
8
|
exports.shortestJoinPaths = shortestJoinPaths;
|
|
8
9
|
exports.buildRelations = buildRelations;
|
|
@@ -124,7 +125,7 @@ const SECRET_NAME_PATTERN = new RegExp(`(^|_)(${SECRET_WORDS.join('|')})(_|$)`,
|
|
|
124
125
|
* meaningfully be in one. It is a liveness bound, not a policy: see the check
|
|
125
126
|
* itself for why an over-long line ends the session instead of being truncated.
|
|
126
127
|
*/
|
|
127
|
-
|
|
128
|
+
exports.MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
128
129
|
/** True when tags could not be read, so nothing may be assumed to be non-PII. */
|
|
129
130
|
function tagsUnreadable(status) {
|
|
130
131
|
return status.state === 'tags-unreadable';
|
|
@@ -301,9 +302,27 @@ function startMcpServer(options, transport = {}) {
|
|
|
301
302
|
announcePiiTags(options);
|
|
302
303
|
let buffer = '';
|
|
303
304
|
let disposed = false;
|
|
305
|
+
let settleClosed = () => { };
|
|
306
|
+
const closed = new Promise((resolve) => {
|
|
307
|
+
settleClosed = resolve;
|
|
308
|
+
});
|
|
304
309
|
const write = (payload) => {
|
|
305
310
|
output.write(`${JSON.stringify(payload)}\n`);
|
|
306
311
|
};
|
|
312
|
+
// The ONE way a session ends, whichever side ends it. `closed` settles in a
|
|
313
|
+
// `finally` so a pool that fails to close still lets the process exit.
|
|
314
|
+
const close = async (reason) => {
|
|
315
|
+
if (disposed)
|
|
316
|
+
return;
|
|
317
|
+
disposed = true;
|
|
318
|
+
input.off('data', onData);
|
|
319
|
+
try {
|
|
320
|
+
await ctx.pool.end();
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
settleClosed(reason);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
307
326
|
const onData = (chunk) => {
|
|
308
327
|
buffer += chunk.toString();
|
|
309
328
|
// BOUND THE BUFFER. The framing is newline-delimited, so a peer that never
|
|
@@ -318,12 +337,16 @@ function startMcpServer(options, transport = {}) {
|
|
|
318
337
|
// parse error at best and a silently different request at worst, and
|
|
319
338
|
// draining to the next newline has the same problem in slower motion. There
|
|
320
339
|
// is no correct way to continue a stream whose framing has been lost.
|
|
321
|
-
if (buffer.length > MAX_STDIO_BUFFER_BYTES) {
|
|
322
|
-
write(errorResponse(null, -32600, 'Message too large', `A single line exceeded ${MAX_STDIO_BUFFER_BYTES} bytes without a newline. ` +
|
|
340
|
+
if (buffer.length > exports.MAX_STDIO_BUFFER_BYTES) {
|
|
341
|
+
write(errorResponse(null, -32600, 'Message too large', `A single line exceeded ${exports.MAX_STDIO_BUFFER_BYTES} bytes without a newline. ` +
|
|
323
342
|
'The stdio transport is newline-delimited; the session is being closed because ' +
|
|
324
343
|
'the framing cannot be recovered.'));
|
|
325
344
|
buffer = '';
|
|
326
|
-
|
|
345
|
+
// END THE SESSION, not just the reader. Detaching alone left the process
|
|
346
|
+
// alive with nothing listening: it could neither answer nor exit.
|
|
347
|
+
close('framing-lost').catch((err) => {
|
|
348
|
+
process.stderr.write(`[turbine] mcp close error: ${(0, ui_js_1.redactUrl)(errorMessage(err))}\n`);
|
|
349
|
+
});
|
|
327
350
|
return;
|
|
328
351
|
}
|
|
329
352
|
let newlineIndex = buffer.indexOf('\n');
|
|
@@ -340,13 +363,8 @@ function startMcpServer(options, transport = {}) {
|
|
|
340
363
|
};
|
|
341
364
|
input.on('data', onData);
|
|
342
365
|
return {
|
|
343
|
-
dispose:
|
|
344
|
-
|
|
345
|
-
return;
|
|
346
|
-
disposed = true;
|
|
347
|
-
input.off('data', onData);
|
|
348
|
-
await ctx.pool.end();
|
|
349
|
-
},
|
|
366
|
+
dispose: () => close('disposed'),
|
|
367
|
+
closed,
|
|
350
368
|
};
|
|
351
369
|
}
|
|
352
370
|
/**
|
|
@@ -647,32 +665,39 @@ function keyList(definition) {
|
|
|
647
665
|
async function migrationStatus(ctx) {
|
|
648
666
|
return withReadOnly(ctx, async (client) => {
|
|
649
667
|
const files = (0, migrate_js_1.listMigrationFiles)(ctx.options.migrationsDir);
|
|
650
|
-
// DELIBERATELY UNPINNED, unlike every other tool here.
|
|
651
|
-
//
|
|
652
668
|
// This tool's whole job is to report what `turbine migrate status` would
|
|
653
|
-
// report,
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
669
|
+
// report, so the tracking table is resolved by the RUNNER'S rule
|
|
670
|
+
// (`connectionStringForSchema` in cli/migrate.ts), not by this server's
|
|
671
|
+
// own `search_path` pin:
|
|
672
|
+
//
|
|
673
|
+
// - a configured schema other than `public` pins the runner's connection
|
|
674
|
+
// to it, so its tracking table is `"<schema>"._turbine_migrations` and
|
|
675
|
+
// is looked up qualified here;
|
|
676
|
+
// - the default `public` leaves the runner's connection unpinned, so its
|
|
677
|
+
// tracking table lives wherever the connecting role's search_path put
|
|
678
|
+
// it (often `public`, not always), and the lookup here is left
|
|
679
|
+
// unqualified for the same reason.
|
|
680
|
+
//
|
|
681
|
+
// Pinning `search_path` to `--schema` in the second case would ANSWER A
|
|
658
682
|
// DIFFERENT QUESTION: `turbine migrate status` would say "applied" while
|
|
659
683
|
// `migrate_status` said the tracking table did not exist. Between agreeing
|
|
660
684
|
// with the migration runner and imposing a rule the runner does not follow,
|
|
661
|
-
// agreeing is the only one that can be right.
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
685
|
+
// agreeing is the only one that can be right. The resolution is DISCLOSED
|
|
686
|
+
// either way: the reply names the schema the table actually resolved in,
|
|
687
|
+
// plus a note when that is not the configured schema. (`explain_query` and
|
|
688
|
+
// `sample_rows` still pin, because they read the schema's own tables, not
|
|
689
|
+
// the runner's bookkeeping.)
|
|
690
|
+
const trackingRef = ctx.options.schema === 'public'
|
|
691
|
+
? (0, index_js_1.quoteIdent)(TRACKING_TABLE)
|
|
692
|
+
: `${(0, index_js_1.quoteIdent)(ctx.options.schema)}.${(0, index_js_1.quoteIdent)(TRACKING_TABLE)}`;
|
|
668
693
|
const trackingExists = await client.query(`SELECT reg.oid IS NOT NULL AS exists, n.nspname AS table_schema
|
|
669
694
|
FROM (SELECT to_regclass($1) AS oid) reg
|
|
670
695
|
LEFT JOIN pg_class c ON c.oid = reg.oid
|
|
671
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace`, [
|
|
696
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace`, [trackingRef]);
|
|
672
697
|
const trackingSchema = trackingExists.rows[0]?.table_schema ?? null;
|
|
673
698
|
const applied = new Map();
|
|
674
699
|
if (trackingExists.rows[0]?.exists) {
|
|
675
|
-
const result = await client.query(`SELECT name, applied_at, checksum FROM ${
|
|
700
|
+
const result = await client.query(`SELECT name, applied_at, checksum FROM ${trackingRef} ORDER BY name`);
|
|
676
701
|
for (const row of result.rows) {
|
|
677
702
|
applied.set(row.name, { appliedAt: row.applied_at, checksum: row.checksum });
|
|
678
703
|
}
|
|
@@ -693,8 +718,8 @@ async function migrationStatus(ctx) {
|
|
|
693
718
|
trackingTableSchema: trackingSchema,
|
|
694
719
|
trackingTableNote: trackingSchema !== null && trackingSchema !== ctx.options.schema
|
|
695
720
|
? `Migrations are tracked in "${trackingSchema}", not the configured schema "${ctx.options.schema}". ` +
|
|
696
|
-
`This is what \`turbine migrate status\` reads too: the
|
|
697
|
-
`the connection's search_path rather than
|
|
721
|
+
`This is what \`turbine migrate status\` reads too: with the default schema the runner resolves the ` +
|
|
722
|
+
`tracking table through the connection's own search_path rather than pinning it.`
|
|
698
723
|
: undefined,
|
|
699
724
|
applied: statuses.filter((status) => status.applied).length,
|
|
700
725
|
pending: statuses.filter((status) => !status.applied).length,
|
|
@@ -1629,120 +1654,126 @@ async function withReadOnly(ctx, fn) {
|
|
|
1629
1654
|
}
|
|
1630
1655
|
}
|
|
1631
1656
|
async function loadSchemaMetadata(client, options) {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
]);
|
|
1657
|
+
// SEQUENTIAL, deliberately, on the one client `withReadOnly` checked out. These
|
|
1658
|
+
// used to run in a single `Promise.all`, which node-postgres tolerates by
|
|
1659
|
+
// queueing the calls behind one another with a `DeprecationWarning` on every
|
|
1660
|
+
// session (`Calling client.query() when the client is already executing a
|
|
1661
|
+
// query ... will be removed in pg@9.0`) and which pg 9 refuses outright, so
|
|
1662
|
+
// every schema-reading tool would throw. The queueing meant they were never
|
|
1663
|
+
// concurrent on the wire anyway; awaiting them in turn costs nothing and puts
|
|
1664
|
+
// the ordering in this file rather than in a deprecated driver behaviour.
|
|
1665
|
+
const tablesResult = await client.query(`SELECT table_name
|
|
1666
|
+
FROM information_schema.tables
|
|
1667
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
1668
|
+
ORDER BY table_name`, [options.schema]);
|
|
1669
|
+
const columnsResult = await client.query(`SELECT table_name, column_name, udt_name, data_type, is_nullable, column_default, is_identity,
|
|
1670
|
+
character_maximum_length
|
|
1671
|
+
FROM information_schema.columns
|
|
1672
|
+
WHERE table_schema = $1
|
|
1673
|
+
ORDER BY table_name, ordinal_position`, [options.schema]);
|
|
1674
|
+
const pkResult = await client.query(
|
|
1675
|
+
// Joined on the table as well as the constraint name. Not because two
|
|
1676
|
+
// primary keys can share a name (they cannot: a PK is backed by an INDEX,
|
|
1677
|
+
// and index names ARE unique per schema, so Postgres itself refuses the
|
|
1678
|
+
// second `CREATE TABLE ... CONSTRAINT pk_shared PRIMARY KEY` with
|
|
1679
|
+
// `relation "pk_shared" already exists`, verified on PG 16) but because
|
|
1680
|
+
// the join reads as if the name were the identity, which is what put the
|
|
1681
|
+
// FOREIGN KEY query below one refactor away from a silent cross product.
|
|
1682
|
+
// A foreign key has no backing index and so genuinely can collide.
|
|
1683
|
+
`SELECT tc.table_name, kcu.column_name
|
|
1684
|
+
FROM information_schema.table_constraints tc
|
|
1685
|
+
JOIN information_schema.key_column_usage kcu
|
|
1686
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1687
|
+
AND tc.table_schema = kcu.table_schema
|
|
1688
|
+
AND tc.table_name = kcu.table_name
|
|
1689
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
|
|
1690
|
+
ORDER BY tc.table_name, kcu.ordinal_position`, [options.schema]);
|
|
1691
|
+
// Foreign keys come from pg_catalog, not information_schema, and the reason
|
|
1692
|
+
// is the same one written up over `SQL_FOREIGN_KEYS` in ../introspect.ts
|
|
1693
|
+
// (KEEP THE TWO IN LOCKSTEP: this is a second copy of that query because
|
|
1694
|
+
// introspect.ts does not export it, and mcp reads through its own pooled
|
|
1695
|
+
// client inside a read-only transaction rather than opening the pool
|
|
1696
|
+
// `introspect()` owns). The information_schema formulation this replaces
|
|
1697
|
+
// joined key_column_usage (constrained columns) to constraint_column_usage
|
|
1698
|
+
// (referenced columns) on the constraint NAME, which is wrong twice:
|
|
1699
|
+
//
|
|
1700
|
+
// 1. Those two column lists have no positional link, so the join is an
|
|
1701
|
+
// N-by-N cross product: a two-column FK came back as four rows and
|
|
1702
|
+
// grouped into four AND-ed correlations, two of them pairing the wrong
|
|
1703
|
+
// columns. Every read through the relation silently returned nothing.
|
|
1704
|
+
// 2. A constraint name is unique per TABLE (conrelid, conname), not per
|
|
1705
|
+
// schema, so two tables may both have a `shared_fk`. This is specific
|
|
1706
|
+
// to foreign keys: a PRIMARY KEY or UNIQUE constraint is backed by an
|
|
1707
|
+
// index and index names ARE schema-unique, so Postgres refuses that
|
|
1708
|
+
// collision outright, while an FK has no backing index and the
|
|
1709
|
+
// collision is legal. On the name alone the two cross: measured on PG
|
|
1710
|
+
// 16, two tables with a `shared_fk` produced EIGHT rows instead of two,
|
|
1711
|
+
// which grouped by name into one entry, so one table lost its relation
|
|
1712
|
+
// entirely and the other pointed at a column its target does not have
|
|
1713
|
+
// (42703 at query time). Which one won depended on catalog row order.
|
|
1714
|
+
//
|
|
1715
|
+
// conkey and confkey are parallel arrays, so unnesting BOTH `WITH
|
|
1716
|
+
// ORDINALITY` and joining on the ordinal IS the pairing, exactly; the OID is
|
|
1717
|
+
// the grouping key because it is unique catalog-wide.
|
|
1718
|
+
//
|
|
1719
|
+
// The target is constrained to the SAME schema, which is what the old query
|
|
1720
|
+
// did implicitly (it joined on ccu.table_schema). Keeping it explicit
|
|
1721
|
+
// matters: `buildRelations` resolves targets by bare name against the
|
|
1722
|
+
// introspected table set, so a cross-schema reference to a same-named table
|
|
1723
|
+
// would silently bind to the local one.
|
|
1724
|
+
//
|
|
1725
|
+
// `conparentid = 0` (declared constraints only) and the by-NAME ordering are
|
|
1726
|
+
// both part of the lockstep: one FK against a partitioned table otherwise
|
|
1727
|
+
// yields an extra phantom relation per partition, and ordering by `con.oid`
|
|
1728
|
+
// makes relation naming a function of DDL execution order rather than of the
|
|
1729
|
+
// schema. Both are written up at length over SQL_FOREIGN_KEYS.
|
|
1730
|
+
const fkResult = await client.query(`SELECT
|
|
1731
|
+
con.oid::text AS constraint_oid,
|
|
1732
|
+
con.conname AS constraint_name,
|
|
1733
|
+
src.relname AS source_table,
|
|
1734
|
+
src_att.attname AS source_column,
|
|
1735
|
+
tgt.relname AS target_table,
|
|
1736
|
+
tgt_att.attname AS target_column
|
|
1737
|
+
FROM pg_catalog.pg_constraint con
|
|
1738
|
+
JOIN pg_catalog.pg_class src ON src.oid = con.conrelid
|
|
1739
|
+
JOIN pg_catalog.pg_namespace src_ns ON src_ns.oid = src.relnamespace
|
|
1740
|
+
JOIN pg_catalog.pg_class tgt ON tgt.oid = con.confrelid
|
|
1741
|
+
JOIN pg_catalog.pg_namespace tgt_ns ON tgt_ns.oid = tgt.relnamespace
|
|
1742
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS sk(attnum, ord) ON TRUE
|
|
1743
|
+
JOIN LATERAL unnest(con.confkey) WITH ORDINALITY AS tk(attnum, ord) ON tk.ord = sk.ord
|
|
1744
|
+
JOIN pg_catalog.pg_attribute src_att
|
|
1745
|
+
ON src_att.attrelid = con.conrelid AND src_att.attnum = sk.attnum
|
|
1746
|
+
JOIN pg_catalog.pg_attribute tgt_att
|
|
1747
|
+
ON tgt_att.attrelid = con.confrelid AND tgt_att.attnum = tk.attnum
|
|
1748
|
+
WHERE con.contype = 'f'
|
|
1749
|
+
AND con.conparentid = 0
|
|
1750
|
+
AND src_ns.nspname = $1
|
|
1751
|
+
AND tgt_ns.nspname = src_ns.nspname
|
|
1752
|
+
ORDER BY src.relname, con.conname, sk.ord`, [options.schema]);
|
|
1753
|
+
const uniqueResult = await client.query(
|
|
1754
|
+
// Joined on the table too, same reasoning as the primary-key query above:
|
|
1755
|
+
// a UNIQUE constraint is index-backed and therefore cannot collide, and
|
|
1756
|
+
// the join says so.
|
|
1757
|
+
`SELECT tc.table_name, tc.constraint_name, kcu.column_name
|
|
1758
|
+
FROM information_schema.table_constraints tc
|
|
1759
|
+
JOIN information_schema.key_column_usage kcu
|
|
1760
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1761
|
+
AND tc.table_schema = kcu.table_schema
|
|
1762
|
+
AND tc.table_name = kcu.table_name
|
|
1763
|
+
WHERE tc.constraint_type = 'UNIQUE' AND tc.table_schema = $1
|
|
1764
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`, [options.schema]);
|
|
1765
|
+
const indexResult = await client.query(
|
|
1766
|
+
// Ordered for the same reason SQL_INDEXES is: these rows feed the
|
|
1767
|
+
// unique-set detection that decides hasOne-versus-hasMany, and they are
|
|
1768
|
+
// reported verbatim by the schema tools, so physical catalog order must
|
|
1769
|
+
// not leak into either answer. Index names are unique per schema.
|
|
1770
|
+
`SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1 ORDER BY tablename, indexname`, [options.schema]);
|
|
1771
|
+
const enumResult = await client.query(`SELECT t.typname, e.enumlabel
|
|
1772
|
+
FROM pg_type t
|
|
1773
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
1774
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
1775
|
+
WHERE n.nspname = $1
|
|
1776
|
+
ORDER BY t.typname, e.enumsortorder`, [options.schema]);
|
|
1746
1777
|
let tableNames = tablesResult.rows.map((row) => row.table_name);
|
|
1747
1778
|
if (options.include?.length) {
|
|
1748
1779
|
const include = new Set(options.include);
|
|
@@ -2093,13 +2124,20 @@ function errorResponse(id, code, message, data) {
|
|
|
2093
2124
|
}
|
|
2094
2125
|
async function runMcpServer(options) {
|
|
2095
2126
|
const handle = startMcpServer(options);
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2127
|
+
const shutdown = () => {
|
|
2128
|
+
handle.dispose().catch((err) => {
|
|
2129
|
+
process.stderr.write(`[turbine] mcp shutdown error: ${(0, ui_js_1.redactUrl)(errorMessage(err))}\n`);
|
|
2130
|
+
});
|
|
2131
|
+
};
|
|
2132
|
+
process.once('SIGINT', shutdown);
|
|
2133
|
+
process.once('SIGTERM', shutdown);
|
|
2134
|
+
process.stdin.once('end', shutdown);
|
|
2135
|
+
const reason = await handle.closed;
|
|
2136
|
+
if (reason === 'framing-lost') {
|
|
2137
|
+
// The reader is detached but a paused stdin still holds the event loop
|
|
2138
|
+
// open, so release it, and exit non-zero: this end was the peer's doing and
|
|
2139
|
+
// a supervisor should restart the server rather than record a clean stop.
|
|
2140
|
+
process.exitCode = 1;
|
|
2141
|
+
process.stdin.destroy();
|
|
2142
|
+
}
|
|
2105
2143
|
}
|