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/cli/mcp.js
CHANGED
|
@@ -115,7 +115,7 @@ const SECRET_NAME_PATTERN = new RegExp(`(^|_)(${SECRET_WORDS.join('|')})(_|$)`,
|
|
|
115
115
|
* meaningfully be in one. It is a liveness bound, not a policy: see the check
|
|
116
116
|
* itself for why an over-long line ends the session instead of being truncated.
|
|
117
117
|
*/
|
|
118
|
-
const MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
118
|
+
export const MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
119
119
|
/** True when tags could not be read, so nothing may be assumed to be non-PII. */
|
|
120
120
|
function tagsUnreadable(status) {
|
|
121
121
|
return status.state === 'tags-unreadable';
|
|
@@ -292,9 +292,27 @@ export function startMcpServer(options, transport = {}) {
|
|
|
292
292
|
announcePiiTags(options);
|
|
293
293
|
let buffer = '';
|
|
294
294
|
let disposed = false;
|
|
295
|
+
let settleClosed = () => { };
|
|
296
|
+
const closed = new Promise((resolve) => {
|
|
297
|
+
settleClosed = resolve;
|
|
298
|
+
});
|
|
295
299
|
const write = (payload) => {
|
|
296
300
|
output.write(`${JSON.stringify(payload)}\n`);
|
|
297
301
|
};
|
|
302
|
+
// The ONE way a session ends, whichever side ends it. `closed` settles in a
|
|
303
|
+
// `finally` so a pool that fails to close still lets the process exit.
|
|
304
|
+
const close = async (reason) => {
|
|
305
|
+
if (disposed)
|
|
306
|
+
return;
|
|
307
|
+
disposed = true;
|
|
308
|
+
input.off('data', onData);
|
|
309
|
+
try {
|
|
310
|
+
await ctx.pool.end();
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
settleClosed(reason);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
298
316
|
const onData = (chunk) => {
|
|
299
317
|
buffer += chunk.toString();
|
|
300
318
|
// BOUND THE BUFFER. The framing is newline-delimited, so a peer that never
|
|
@@ -314,7 +332,11 @@ export function startMcpServer(options, transport = {}) {
|
|
|
314
332
|
'The stdio transport is newline-delimited; the session is being closed because ' +
|
|
315
333
|
'the framing cannot be recovered.'));
|
|
316
334
|
buffer = '';
|
|
317
|
-
|
|
335
|
+
// END THE SESSION, not just the reader. Detaching alone left the process
|
|
336
|
+
// alive with nothing listening: it could neither answer nor exit.
|
|
337
|
+
close('framing-lost').catch((err) => {
|
|
338
|
+
process.stderr.write(`[turbine] mcp close error: ${redactUrl(errorMessage(err))}\n`);
|
|
339
|
+
});
|
|
318
340
|
return;
|
|
319
341
|
}
|
|
320
342
|
let newlineIndex = buffer.indexOf('\n');
|
|
@@ -331,13 +353,8 @@ export function startMcpServer(options, transport = {}) {
|
|
|
331
353
|
};
|
|
332
354
|
input.on('data', onData);
|
|
333
355
|
return {
|
|
334
|
-
dispose:
|
|
335
|
-
|
|
336
|
-
return;
|
|
337
|
-
disposed = true;
|
|
338
|
-
input.off('data', onData);
|
|
339
|
-
await ctx.pool.end();
|
|
340
|
-
},
|
|
356
|
+
dispose: () => close('disposed'),
|
|
357
|
+
closed,
|
|
341
358
|
};
|
|
342
359
|
}
|
|
343
360
|
/**
|
|
@@ -638,32 +655,39 @@ function keyList(definition) {
|
|
|
638
655
|
async function migrationStatus(ctx) {
|
|
639
656
|
return withReadOnly(ctx, async (client) => {
|
|
640
657
|
const files = listMigrationFiles(ctx.options.migrationsDir);
|
|
641
|
-
// DELIBERATELY UNPINNED, unlike every other tool here.
|
|
642
|
-
//
|
|
643
658
|
// This tool's whole job is to report what `turbine migrate status` would
|
|
644
|
-
// report,
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
659
|
+
// report, so the tracking table is resolved by the RUNNER'S rule
|
|
660
|
+
// (`connectionStringForSchema` in cli/migrate.ts), not by this server's
|
|
661
|
+
// own `search_path` pin:
|
|
662
|
+
//
|
|
663
|
+
// - a configured schema other than `public` pins the runner's connection
|
|
664
|
+
// to it, so its tracking table is `"<schema>"._turbine_migrations` and
|
|
665
|
+
// is looked up qualified here;
|
|
666
|
+
// - the default `public` leaves the runner's connection unpinned, so its
|
|
667
|
+
// tracking table lives wherever the connecting role's search_path put
|
|
668
|
+
// it (often `public`, not always), and the lookup here is left
|
|
669
|
+
// unqualified for the same reason.
|
|
670
|
+
//
|
|
671
|
+
// Pinning `search_path` to `--schema` in the second case would ANSWER A
|
|
649
672
|
// DIFFERENT QUESTION: `turbine migrate status` would say "applied" while
|
|
650
673
|
// `migrate_status` said the tracking table did not exist. Between agreeing
|
|
651
674
|
// with the migration runner and imposing a rule the runner does not follow,
|
|
652
|
-
// agreeing is the only one that can be right.
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
675
|
+
// agreeing is the only one that can be right. The resolution is DISCLOSED
|
|
676
|
+
// either way: the reply names the schema the table actually resolved in,
|
|
677
|
+
// plus a note when that is not the configured schema. (`explain_query` and
|
|
678
|
+
// `sample_rows` still pin, because they read the schema's own tables, not
|
|
679
|
+
// the runner's bookkeeping.)
|
|
680
|
+
const trackingRef = ctx.options.schema === 'public'
|
|
681
|
+
? quoteIdent(TRACKING_TABLE)
|
|
682
|
+
: `${quoteIdent(ctx.options.schema)}.${quoteIdent(TRACKING_TABLE)}`;
|
|
659
683
|
const trackingExists = await client.query(`SELECT reg.oid IS NOT NULL AS exists, n.nspname AS table_schema
|
|
660
684
|
FROM (SELECT to_regclass($1) AS oid) reg
|
|
661
685
|
LEFT JOIN pg_class c ON c.oid = reg.oid
|
|
662
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace`, [
|
|
686
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace`, [trackingRef]);
|
|
663
687
|
const trackingSchema = trackingExists.rows[0]?.table_schema ?? null;
|
|
664
688
|
const applied = new Map();
|
|
665
689
|
if (trackingExists.rows[0]?.exists) {
|
|
666
|
-
const result = await client.query(`SELECT name, applied_at, checksum FROM ${
|
|
690
|
+
const result = await client.query(`SELECT name, applied_at, checksum FROM ${trackingRef} ORDER BY name`);
|
|
667
691
|
for (const row of result.rows) {
|
|
668
692
|
applied.set(row.name, { appliedAt: row.applied_at, checksum: row.checksum });
|
|
669
693
|
}
|
|
@@ -684,8 +708,8 @@ async function migrationStatus(ctx) {
|
|
|
684
708
|
trackingTableSchema: trackingSchema,
|
|
685
709
|
trackingTableNote: trackingSchema !== null && trackingSchema !== ctx.options.schema
|
|
686
710
|
? `Migrations are tracked in "${trackingSchema}", not the configured schema "${ctx.options.schema}". ` +
|
|
687
|
-
`This is what \`turbine migrate status\` reads too: the
|
|
688
|
-
`the connection's search_path rather than
|
|
711
|
+
`This is what \`turbine migrate status\` reads too: with the default schema the runner resolves the ` +
|
|
712
|
+
`tracking table through the connection's own search_path rather than pinning it.`
|
|
689
713
|
: undefined,
|
|
690
714
|
applied: statuses.filter((status) => status.applied).length,
|
|
691
715
|
pending: statuses.filter((status) => !status.applied).length,
|
|
@@ -1620,120 +1644,126 @@ async function withReadOnly(ctx, fn) {
|
|
|
1620
1644
|
}
|
|
1621
1645
|
}
|
|
1622
1646
|
async function loadSchemaMetadata(client, options) {
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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
|
-
]);
|
|
1647
|
+
// SEQUENTIAL, deliberately, on the one client `withReadOnly` checked out. These
|
|
1648
|
+
// used to run in a single `Promise.all`, which node-postgres tolerates by
|
|
1649
|
+
// queueing the calls behind one another with a `DeprecationWarning` on every
|
|
1650
|
+
// session (`Calling client.query() when the client is already executing a
|
|
1651
|
+
// query ... will be removed in pg@9.0`) and which pg 9 refuses outright, so
|
|
1652
|
+
// every schema-reading tool would throw. The queueing meant they were never
|
|
1653
|
+
// concurrent on the wire anyway; awaiting them in turn costs nothing and puts
|
|
1654
|
+
// the ordering in this file rather than in a deprecated driver behaviour.
|
|
1655
|
+
const tablesResult = await client.query(`SELECT table_name
|
|
1656
|
+
FROM information_schema.tables
|
|
1657
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
1658
|
+
ORDER BY table_name`, [options.schema]);
|
|
1659
|
+
const columnsResult = await client.query(`SELECT table_name, column_name, udt_name, data_type, is_nullable, column_default, is_identity,
|
|
1660
|
+
character_maximum_length
|
|
1661
|
+
FROM information_schema.columns
|
|
1662
|
+
WHERE table_schema = $1
|
|
1663
|
+
ORDER BY table_name, ordinal_position`, [options.schema]);
|
|
1664
|
+
const pkResult = await client.query(
|
|
1665
|
+
// Joined on the table as well as the constraint name. Not because two
|
|
1666
|
+
// primary keys can share a name (they cannot: a PK is backed by an INDEX,
|
|
1667
|
+
// and index names ARE unique per schema, so Postgres itself refuses the
|
|
1668
|
+
// second `CREATE TABLE ... CONSTRAINT pk_shared PRIMARY KEY` with
|
|
1669
|
+
// `relation "pk_shared" already exists`, verified on PG 16) but because
|
|
1670
|
+
// the join reads as if the name were the identity, which is what put the
|
|
1671
|
+
// FOREIGN KEY query below one refactor away from a silent cross product.
|
|
1672
|
+
// A foreign key has no backing index and so genuinely can collide.
|
|
1673
|
+
`SELECT tc.table_name, kcu.column_name
|
|
1674
|
+
FROM information_schema.table_constraints tc
|
|
1675
|
+
JOIN information_schema.key_column_usage kcu
|
|
1676
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1677
|
+
AND tc.table_schema = kcu.table_schema
|
|
1678
|
+
AND tc.table_name = kcu.table_name
|
|
1679
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
|
|
1680
|
+
ORDER BY tc.table_name, kcu.ordinal_position`, [options.schema]);
|
|
1681
|
+
// Foreign keys come from pg_catalog, not information_schema, and the reason
|
|
1682
|
+
// is the same one written up over `SQL_FOREIGN_KEYS` in ../introspect.ts
|
|
1683
|
+
// (KEEP THE TWO IN LOCKSTEP: this is a second copy of that query because
|
|
1684
|
+
// introspect.ts does not export it, and mcp reads through its own pooled
|
|
1685
|
+
// client inside a read-only transaction rather than opening the pool
|
|
1686
|
+
// `introspect()` owns). The information_schema formulation this replaces
|
|
1687
|
+
// joined key_column_usage (constrained columns) to constraint_column_usage
|
|
1688
|
+
// (referenced columns) on the constraint NAME, which is wrong twice:
|
|
1689
|
+
//
|
|
1690
|
+
// 1. Those two column lists have no positional link, so the join is an
|
|
1691
|
+
// N-by-N cross product: a two-column FK came back as four rows and
|
|
1692
|
+
// grouped into four AND-ed correlations, two of them pairing the wrong
|
|
1693
|
+
// columns. Every read through the relation silently returned nothing.
|
|
1694
|
+
// 2. A constraint name is unique per TABLE (conrelid, conname), not per
|
|
1695
|
+
// schema, so two tables may both have a `shared_fk`. This is specific
|
|
1696
|
+
// to foreign keys: a PRIMARY KEY or UNIQUE constraint is backed by an
|
|
1697
|
+
// index and index names ARE schema-unique, so Postgres refuses that
|
|
1698
|
+
// collision outright, while an FK has no backing index and the
|
|
1699
|
+
// collision is legal. On the name alone the two cross: measured on PG
|
|
1700
|
+
// 16, two tables with a `shared_fk` produced EIGHT rows instead of two,
|
|
1701
|
+
// which grouped by name into one entry, so one table lost its relation
|
|
1702
|
+
// entirely and the other pointed at a column its target does not have
|
|
1703
|
+
// (42703 at query time). Which one won depended on catalog row order.
|
|
1704
|
+
//
|
|
1705
|
+
// conkey and confkey are parallel arrays, so unnesting BOTH `WITH
|
|
1706
|
+
// ORDINALITY` and joining on the ordinal IS the pairing, exactly; the OID is
|
|
1707
|
+
// the grouping key because it is unique catalog-wide.
|
|
1708
|
+
//
|
|
1709
|
+
// The target is constrained to the SAME schema, which is what the old query
|
|
1710
|
+
// did implicitly (it joined on ccu.table_schema). Keeping it explicit
|
|
1711
|
+
// matters: `buildRelations` resolves targets by bare name against the
|
|
1712
|
+
// introspected table set, so a cross-schema reference to a same-named table
|
|
1713
|
+
// would silently bind to the local one.
|
|
1714
|
+
//
|
|
1715
|
+
// `conparentid = 0` (declared constraints only) and the by-NAME ordering are
|
|
1716
|
+
// both part of the lockstep: one FK against a partitioned table otherwise
|
|
1717
|
+
// yields an extra phantom relation per partition, and ordering by `con.oid`
|
|
1718
|
+
// makes relation naming a function of DDL execution order rather than of the
|
|
1719
|
+
// schema. Both are written up at length over SQL_FOREIGN_KEYS.
|
|
1720
|
+
const fkResult = await client.query(`SELECT
|
|
1721
|
+
con.oid::text AS constraint_oid,
|
|
1722
|
+
con.conname AS constraint_name,
|
|
1723
|
+
src.relname AS source_table,
|
|
1724
|
+
src_att.attname AS source_column,
|
|
1725
|
+
tgt.relname AS target_table,
|
|
1726
|
+
tgt_att.attname AS target_column
|
|
1727
|
+
FROM pg_catalog.pg_constraint con
|
|
1728
|
+
JOIN pg_catalog.pg_class src ON src.oid = con.conrelid
|
|
1729
|
+
JOIN pg_catalog.pg_namespace src_ns ON src_ns.oid = src.relnamespace
|
|
1730
|
+
JOIN pg_catalog.pg_class tgt ON tgt.oid = con.confrelid
|
|
1731
|
+
JOIN pg_catalog.pg_namespace tgt_ns ON tgt_ns.oid = tgt.relnamespace
|
|
1732
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS sk(attnum, ord) ON TRUE
|
|
1733
|
+
JOIN LATERAL unnest(con.confkey) WITH ORDINALITY AS tk(attnum, ord) ON tk.ord = sk.ord
|
|
1734
|
+
JOIN pg_catalog.pg_attribute src_att
|
|
1735
|
+
ON src_att.attrelid = con.conrelid AND src_att.attnum = sk.attnum
|
|
1736
|
+
JOIN pg_catalog.pg_attribute tgt_att
|
|
1737
|
+
ON tgt_att.attrelid = con.confrelid AND tgt_att.attnum = tk.attnum
|
|
1738
|
+
WHERE con.contype = 'f'
|
|
1739
|
+
AND con.conparentid = 0
|
|
1740
|
+
AND src_ns.nspname = $1
|
|
1741
|
+
AND tgt_ns.nspname = src_ns.nspname
|
|
1742
|
+
ORDER BY src.relname, con.conname, sk.ord`, [options.schema]);
|
|
1743
|
+
const uniqueResult = await client.query(
|
|
1744
|
+
// Joined on the table too, same reasoning as the primary-key query above:
|
|
1745
|
+
// a UNIQUE constraint is index-backed and therefore cannot collide, and
|
|
1746
|
+
// the join says so.
|
|
1747
|
+
`SELECT tc.table_name, tc.constraint_name, kcu.column_name
|
|
1748
|
+
FROM information_schema.table_constraints tc
|
|
1749
|
+
JOIN information_schema.key_column_usage kcu
|
|
1750
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1751
|
+
AND tc.table_schema = kcu.table_schema
|
|
1752
|
+
AND tc.table_name = kcu.table_name
|
|
1753
|
+
WHERE tc.constraint_type = 'UNIQUE' AND tc.table_schema = $1
|
|
1754
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`, [options.schema]);
|
|
1755
|
+
const indexResult = await client.query(
|
|
1756
|
+
// Ordered for the same reason SQL_INDEXES is: these rows feed the
|
|
1757
|
+
// unique-set detection that decides hasOne-versus-hasMany, and they are
|
|
1758
|
+
// reported verbatim by the schema tools, so physical catalog order must
|
|
1759
|
+
// not leak into either answer. Index names are unique per schema.
|
|
1760
|
+
`SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1 ORDER BY tablename, indexname`, [options.schema]);
|
|
1761
|
+
const enumResult = await client.query(`SELECT t.typname, e.enumlabel
|
|
1762
|
+
FROM pg_type t
|
|
1763
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
1764
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
1765
|
+
WHERE n.nspname = $1
|
|
1766
|
+
ORDER BY t.typname, e.enumsortorder`, [options.schema]);
|
|
1737
1767
|
let tableNames = tablesResult.rows.map((row) => row.table_name);
|
|
1738
1768
|
if (options.include?.length) {
|
|
1739
1769
|
const include = new Set(options.include);
|
|
@@ -2084,13 +2114,20 @@ function errorResponse(id, code, message, data) {
|
|
|
2084
2114
|
}
|
|
2085
2115
|
export async function runMcpServer(options) {
|
|
2086
2116
|
const handle = startMcpServer(options);
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2117
|
+
const shutdown = () => {
|
|
2118
|
+
handle.dispose().catch((err) => {
|
|
2119
|
+
process.stderr.write(`[turbine] mcp shutdown error: ${redactUrl(errorMessage(err))}\n`);
|
|
2120
|
+
});
|
|
2121
|
+
};
|
|
2122
|
+
process.once('SIGINT', shutdown);
|
|
2123
|
+
process.once('SIGTERM', shutdown);
|
|
2124
|
+
process.stdin.once('end', shutdown);
|
|
2125
|
+
const reason = await handle.closed;
|
|
2126
|
+
if (reason === 'framing-lost') {
|
|
2127
|
+
// The reader is detached but a paused stdin still holds the event loop
|
|
2128
|
+
// open, so release it, and exit non-zero: this end was the peer's doing and
|
|
2129
|
+
// a supervisor should restart the server rather than record a clean stop.
|
|
2130
|
+
process.exitCode = 1;
|
|
2131
|
+
process.stdin.destroy();
|
|
2132
|
+
}
|
|
2096
2133
|
}
|