turbine-orm 0.45.0 → 0.46.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/index.js +245 -52
- package/dist/cjs/cli/migrate.js +221 -16
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index-stats.js +450 -0
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.js +246 -53
- package/dist/cli/migrate.d.ts +41 -9
- package/dist/cli/migrate.js +220 -16
- package/dist/index-advisor.d.ts +30 -0
- package/dist/index-advisor.js +0 -0
- package/dist/index-stats.d.ts +208 -0
- package/dist/index-stats.js +408 -0
- package/package.json +1 -1
package/dist/cjs/cli/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* turbine migrate status — Show migration status
|
|
16
16
|
* turbine seed — Run seed file
|
|
17
17
|
* turbine status — Show schema summary
|
|
18
|
-
* turbine doctor
|
|
18
|
+
* turbine doctor - Cost-aware missing-FK-index triage (--fix, --json, --no-concurrently)
|
|
19
19
|
* turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
|
|
20
20
|
* turbine mcp — Start read-only MCP server over JSON-RPC stdio
|
|
21
21
|
* turbine observe — Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
@@ -73,6 +73,7 @@ const node_path_1 = require("node:path");
|
|
|
73
73
|
const node_url_1 = require("node:url");
|
|
74
74
|
const generate_js_1 = require("../generate.js");
|
|
75
75
|
const index_advisor_js_1 = require("../index-advisor.js");
|
|
76
|
+
const index_stats_js_1 = require("../index-stats.js");
|
|
76
77
|
const introspect_js_1 = require("../introspect.js");
|
|
77
78
|
const schema_sql_js_1 = require("../schema-sql.js");
|
|
78
79
|
const config_js_1 = require("./config.js");
|
|
@@ -164,6 +165,12 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
164
165
|
case '--fix':
|
|
165
166
|
result.fix = true;
|
|
166
167
|
break;
|
|
168
|
+
case '--json':
|
|
169
|
+
result.json = true;
|
|
170
|
+
break;
|
|
171
|
+
case '--no-concurrently':
|
|
172
|
+
result.noConcurrently = true;
|
|
173
|
+
break;
|
|
167
174
|
case '--zod':
|
|
168
175
|
result.zod = true;
|
|
169
176
|
break;
|
|
@@ -1571,12 +1578,24 @@ async function cmdMigrateUp(args, config) {
|
|
|
1571
1578
|
(0, ui_js_1.newline)();
|
|
1572
1579
|
}
|
|
1573
1580
|
const spinner = new ui_js_1.Spinner('Applying migrations').start();
|
|
1581
|
+
// A no-transaction migration (CREATE INDEX CONCURRENTLY) can wait a long time
|
|
1582
|
+
// on other transactions and otherwise looks hung. Stop the spinner and print
|
|
1583
|
+
// a loud notice the moment one is about to run.
|
|
1584
|
+
const onNoTransaction = (file) => {
|
|
1585
|
+
spinner.stop();
|
|
1586
|
+
(0, ui_js_1.warn)(`Running ${(0, ui_js_1.bold)(file.filename)} WITHOUT a transaction (-- turbine:no-transaction).`);
|
|
1587
|
+
console.log(` ${(0, ui_js_1.dim)('Each statement runs on its own. A mid-file failure leaves earlier statements')}`);
|
|
1588
|
+
console.log(` ${(0, ui_js_1.dim)('applied and the migration UNRECORDED, so every statement must be idempotent.')}`);
|
|
1589
|
+
console.log(` ${(0, ui_js_1.dim)('CREATE INDEX CONCURRENTLY can wait on long-running transactions: not a hang.')}`);
|
|
1590
|
+
(0, ui_js_1.newline)();
|
|
1591
|
+
};
|
|
1574
1592
|
let result;
|
|
1575
1593
|
try {
|
|
1576
1594
|
result = await (0, migrate_js_1.migrateUp)(url, config.migrationsDir, {
|
|
1577
1595
|
step: args.step,
|
|
1578
1596
|
allowDrift: args.allowDrift,
|
|
1579
1597
|
allowDestructive: args.allowDestructive,
|
|
1598
|
+
onNoTransaction,
|
|
1580
1599
|
});
|
|
1581
1600
|
}
|
|
1582
1601
|
catch (err) {
|
|
@@ -1593,6 +1612,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
1593
1612
|
step: args.step,
|
|
1594
1613
|
allowDrift: args.allowDrift,
|
|
1595
1614
|
allowDestructive: true,
|
|
1615
|
+
onNoTransaction,
|
|
1596
1616
|
});
|
|
1597
1617
|
}
|
|
1598
1618
|
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
@@ -2038,16 +2058,50 @@ async function cmdStatus(_args, config) {
|
|
|
2038
2058
|
(0, ui_js_1.newline)();
|
|
2039
2059
|
}
|
|
2040
2060
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2061
|
+
/** Human labels + tier ordering for the three triage buckets. */
|
|
2062
|
+
const TIER_ORDER = ['take-freely', 'take-deliberately', 'scrutinize'];
|
|
2063
|
+
const TIER_LABEL = {
|
|
2064
|
+
'take-freely': 'TAKE FREELY',
|
|
2065
|
+
'take-deliberately': 'TAKE DELIBERATELY',
|
|
2066
|
+
scrutinize: 'SCRUTINIZE',
|
|
2067
|
+
};
|
|
2068
|
+
/** Build the `CREATE INDEX` for a finding, honoring the partial-null suggestion. */
|
|
2069
|
+
function doctorCreateSql(f, opts) {
|
|
2070
|
+
return (0, index_advisor_js_1.buildCreateIndexSql)(f.missing.table, f.missing.columns, f.missing.indexName, {
|
|
2071
|
+
concurrently: opts.concurrently,
|
|
2072
|
+
partialNotNull: f.score.partialNotNull,
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
/** The commented recipe prepended to a CONCURRENTLY fix migration's UP body. */
|
|
2076
|
+
const CONCURRENTLY_RECIPE_COMMENT = [
|
|
2077
|
+
'-- CREATE INDEX CONCURRENTLY builds without holding a write lock, but it cannot',
|
|
2078
|
+
'-- run inside a transaction (that is why this file carries the',
|
|
2079
|
+
'-- "-- turbine:no-transaction" directive above). Read before applying:',
|
|
2080
|
+
'--',
|
|
2081
|
+
'-- 1. Idempotency is required. This migration is recorded only after ALL',
|
|
2082
|
+
'-- statements succeed; a mid-file failure leaves earlier indexes built and',
|
|
2083
|
+
'-- the migration unrecorded, so a rerun must be safe. IF NOT EXISTS keeps',
|
|
2084
|
+
'-- each CREATE idempotent.',
|
|
2085
|
+
'-- 2. The INVALID-index trap. A CREATE INDEX CONCURRENTLY that fails partway',
|
|
2086
|
+
'-- leaves an INVALID index behind. On rerun, IF NOT EXISTS SKIPS that',
|
|
2087
|
+
'-- corpse (the name already exists), so the index is never actually built.',
|
|
2088
|
+
'-- Fix: DROP INDEX CONCURRENTLY the invalid index, then rerun. Run',
|
|
2089
|
+
'-- "turbine doctor" to list invalid indexes.',
|
|
2090
|
+
'-- 3. Locking. CREATE INDEX CONCURRENTLY waits for every transaction that can',
|
|
2091
|
+
'-- see the table to finish. A long-running transaction makes it wait and',
|
|
2092
|
+
'-- makes "migrate up" look hung. For bounded waits, SET lock_timeout /',
|
|
2093
|
+
'-- statement_timeout in a psql session.',
|
|
2094
|
+
].join('\n');
|
|
2044
2095
|
async function cmdDoctor(args, config) {
|
|
2045
|
-
|
|
2096
|
+
const jsonMode = args.json === true;
|
|
2046
2097
|
const url = requireUrl(config);
|
|
2047
|
-
(
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2098
|
+
if (!jsonMode) {
|
|
2099
|
+
(0, ui_js_1.banner)();
|
|
2100
|
+
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
2101
|
+
(0, ui_js_1.label)('Schema', config.schema);
|
|
2102
|
+
(0, ui_js_1.newline)();
|
|
2103
|
+
}
|
|
2104
|
+
const spinner = jsonMode ? null : new ui_js_1.Spinner('Introspecting database').start();
|
|
2051
2105
|
const schema = await (0, introspect_js_1.introspect)({
|
|
2052
2106
|
connectionString: url,
|
|
2053
2107
|
schema: config.schema,
|
|
@@ -2055,62 +2109,201 @@ async function cmdDoctor(args, config) {
|
|
|
2055
2109
|
exclude: config.exclude.length ? config.exclude : undefined,
|
|
2056
2110
|
});
|
|
2057
2111
|
const missing = (0, index_advisor_js_1.findMissingRelationIndexes)(schema);
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2112
|
+
// Collect live statistics. The collector reads whole-schema indexes (for
|
|
2113
|
+
// invalid-index detection) plus per-table stats + probed-column null_frac.
|
|
2114
|
+
const probedTables = [...new Set(missing.map((m) => m.table))];
|
|
2115
|
+
const probedColumns = [];
|
|
2116
|
+
for (const m of missing) {
|
|
2117
|
+
if (m.columns.length === 1 && m.columns[0] !== undefined) {
|
|
2118
|
+
probedColumns.push({ table: m.table, column: m.columns[0] });
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
let snapshot;
|
|
2122
|
+
try {
|
|
2123
|
+
snapshot = await (0, index_stats_js_1.collectStatsSnapshot)({
|
|
2124
|
+
connectionString: url,
|
|
2125
|
+
schema: config.schema,
|
|
2126
|
+
tables: probedTables,
|
|
2127
|
+
columns: probedColumns,
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
catch (err) {
|
|
2131
|
+
snapshot = {
|
|
2132
|
+
available: false,
|
|
2133
|
+
statsReset: null,
|
|
2134
|
+
statsAgeDays: null,
|
|
2135
|
+
tables: {},
|
|
2136
|
+
indexes: [],
|
|
2137
|
+
nullFrac: {},
|
|
2138
|
+
notices: [`statistics collection failed: ${err instanceof Error ? err.message : String(err)}`],
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const invalid = (0, index_stats_js_1.findInvalidIndexes)(snapshot);
|
|
2142
|
+
const usable = (0, index_stats_js_1.isSnapshotUsable)(snapshot);
|
|
2143
|
+
const findings = missing.map((m) => ({ missing: m, score: (0, index_stats_js_1.scoreMissingIndex)(m, snapshot) }));
|
|
2144
|
+
if (jsonMode) {
|
|
2145
|
+
spinner?.stop();
|
|
2146
|
+
console.log(JSON.stringify(buildDoctorJson({ schema, findings, invalid, snapshot, usable, args }), null, 2));
|
|
2061
2147
|
return;
|
|
2062
2148
|
}
|
|
2149
|
+
await renderDoctorHuman({ spinner: spinner, schema, findings, invalid, snapshot, usable, args, config });
|
|
2150
|
+
}
|
|
2151
|
+
/** The stable, versioned JSON contract (schemaVersion: 1). First external consumer: BataDB import. */
|
|
2152
|
+
function buildDoctorJson(ctx) {
|
|
2153
|
+
const concurrently = ctx.args.noConcurrently !== true;
|
|
2154
|
+
return {
|
|
2155
|
+
schemaVersion: 1,
|
|
2156
|
+
scannedTables: Object.keys(ctx.schema.tables).length,
|
|
2157
|
+
stats: {
|
|
2158
|
+
available: ctx.snapshot.available,
|
|
2159
|
+
usable: ctx.usable,
|
|
2160
|
+
statsReset: ctx.snapshot.statsReset ? ctx.snapshot.statsReset.toISOString() : null,
|
|
2161
|
+
statsAgeDays: ctx.snapshot.statsAgeDays,
|
|
2162
|
+
notices: ctx.snapshot.notices,
|
|
2163
|
+
},
|
|
2164
|
+
thresholds: index_stats_js_1.STATS_THRESHOLDS,
|
|
2165
|
+
findings: ctx.findings.map((f) => ({
|
|
2166
|
+
table: f.missing.table,
|
|
2167
|
+
columns: f.missing.columns,
|
|
2168
|
+
indexName: f.missing.indexName,
|
|
2169
|
+
tier: f.score.tier,
|
|
2170
|
+
reasons: f.score.reasons,
|
|
2171
|
+
metrics: f.score.metrics,
|
|
2172
|
+
hotWarning: f.score.hotWarning,
|
|
2173
|
+
partialNotNull: f.score.partialNotNull,
|
|
2174
|
+
probes: f.missing.probes,
|
|
2175
|
+
createSql: doctorCreateSql(f, { concurrently }),
|
|
2176
|
+
dropSql: (0, index_advisor_js_1.buildDropIndexSql)(f.missing.indexName, { concurrently }),
|
|
2177
|
+
})),
|
|
2178
|
+
invalidIndexes: ctx.invalid,
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
async function renderDoctorHuman(ctx) {
|
|
2182
|
+
const { spinner, schema, findings, invalid, snapshot, usable, args, config } = ctx;
|
|
2063
2183
|
spinner.succeed(`Scanned ${(0, ui_js_1.bold)(String(Object.keys(schema.tables).length))} tables`);
|
|
2064
|
-
(
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
{
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
WHERE n.nspname = $1 AND c.relname = ANY($2)`, [config.schema, tables]);
|
|
2078
|
-
for (const row of res.rows)
|
|
2079
|
-
rowCounts.set(row.relname, Math.max(0, Number(row.reltuples)));
|
|
2184
|
+
if (findings.length === 0 && invalid.length === 0) {
|
|
2185
|
+
(0, ui_js_1.success)('Every relation probe is backed by an index, and no invalid indexes were found');
|
|
2186
|
+
(0, ui_js_1.newline)();
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (findings.length > 0) {
|
|
2190
|
+
(0, ui_js_1.warn)(`Found ${(0, ui_js_1.bold)(String(findings.length))} unindexed relation probe(s)`);
|
|
2191
|
+
(0, ui_js_1.newline)();
|
|
2192
|
+
console.log(` ${(0, ui_js_1.dim)('Turbine loads relations as correlated subqueries: the child table is probed')}`);
|
|
2193
|
+
console.log(` ${(0, ui_js_1.dim)('once per parent row, so an unindexed FK costs a full table scan PER PARENT.')}`);
|
|
2194
|
+
(0, ui_js_1.newline)();
|
|
2195
|
+
if (usable) {
|
|
2196
|
+
renderTiers(findings, snapshot, args);
|
|
2080
2197
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2198
|
+
else {
|
|
2199
|
+
renderTopologyFallback(findings, snapshot);
|
|
2083
2200
|
}
|
|
2084
2201
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
for (const m of missing) {
|
|
2090
|
-
const rows = rowCounts.get(m.table);
|
|
2091
|
-
const rowsLabel = rows !== undefined ? `~${rows.toLocaleString()} rows` : 'row count unknown';
|
|
2092
|
-
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(m.table))} ${(0, ui_js_1.dim)(`(${m.columns.join(', ')})`)} ${(0, ui_js_1.gray)(rowsLabel)}`);
|
|
2093
|
-
for (const p of m.probes) {
|
|
2094
|
-
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} probed by ${p.from}.${(0, ui_js_1.blue)(p.relation)} ${(0, ui_js_1.dim)(`(${p.type})`)}`);
|
|
2202
|
+
renderInvalidIndexes(invalid);
|
|
2203
|
+
if (findings.length > 0) {
|
|
2204
|
+
if (args.fix) {
|
|
2205
|
+
renderFixMigration(findings, config, args);
|
|
2095
2206
|
}
|
|
2096
|
-
|
|
2207
|
+
else {
|
|
2208
|
+
console.log(` ${(0, ui_js_1.dim)('Generate a fix migration with:')} ${(0, ui_js_1.cyan)('npx turbine doctor --fix')}`);
|
|
2209
|
+
console.log(` ${(0, ui_js_1.dim)('Machine-readable report:')} ${(0, ui_js_1.cyan)('npx turbine doctor --json')}`);
|
|
2210
|
+
(0, ui_js_1.newline)();
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
/** Cost-aware tiered output: three sections, each finding annotated with its numbers. */
|
|
2215
|
+
function renderTiers(findings, snapshot, _args) {
|
|
2216
|
+
const ageLabel = snapshot.statsAgeDays !== null ? `${Math.round(snapshot.statsAgeDays)}d` : 'unknown';
|
|
2217
|
+
console.log(` ${(0, ui_js_1.dim)(`Cost triage based on live statistics (stats reset ${ageLabel} ago). Thresholds: tiny < ${index_stats_js_1.STATS_THRESHOLDS.tinyTableRows.toLocaleString()} rows,`)}`);
|
|
2218
|
+
console.log(` ${(0, ui_js_1.dim)(`"real" write rate >= ${index_stats_js_1.STATS_THRESHOLDS.highWritesPerDay.toLocaleString()}/day, "many" indexes >= ${index_stats_js_1.STATS_THRESHOLDS.manyIndexes}.`)}`);
|
|
2219
|
+
(0, ui_js_1.newline)();
|
|
2220
|
+
const tierColor = {
|
|
2221
|
+
'take-freely': ui_js_1.green,
|
|
2222
|
+
'take-deliberately': ui_js_1.yellow,
|
|
2223
|
+
scrutinize: ui_js_1.red,
|
|
2224
|
+
};
|
|
2225
|
+
for (const tier of TIER_ORDER) {
|
|
2226
|
+
const inTier = findings
|
|
2227
|
+
.filter((f) => f.score.tier === tier)
|
|
2228
|
+
.sort((a, b) => b.score.benefitScore - a.score.benefitScore);
|
|
2229
|
+
if (inTier.length === 0)
|
|
2230
|
+
continue;
|
|
2231
|
+
console.log(` ${(0, ui_js_1.bold)(tierColor[tier](`${TIER_LABEL[tier]} (${inTier.length})`))}`);
|
|
2097
2232
|
(0, ui_js_1.newline)();
|
|
2233
|
+
for (const f of inTier) {
|
|
2234
|
+
renderFinding(f, { concurrently: true, withReasons: true });
|
|
2235
|
+
}
|
|
2098
2236
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2237
|
+
}
|
|
2238
|
+
/** Degraded output: today's size-sorted topology report when stats are absent/young. */
|
|
2239
|
+
function renderTopologyFallback(findings, snapshot) {
|
|
2240
|
+
(0, ui_js_1.warn)('Statistics unavailable or too young to score cost: showing size-sorted topology only.');
|
|
2241
|
+
for (const notice of snapshot.notices)
|
|
2242
|
+
console.log(` ${(0, ui_js_1.dim)(`- ${notice}`)}`);
|
|
2243
|
+
if (snapshot.statsAgeDays !== null && snapshot.statsAgeDays < index_stats_js_1.STATS_THRESHOLDS.minStatsAgeDays) {
|
|
2244
|
+
console.log(` ${(0, ui_js_1.dim)(`- statistics were reset less than ${index_stats_js_1.STATS_THRESHOLDS.minStatsAgeDays} day(s) ago; write rates are not yet meaningful.`)}`);
|
|
2245
|
+
}
|
|
2246
|
+
(0, ui_js_1.newline)();
|
|
2247
|
+
const sorted = [...findings].sort((a, b) => (b.score.metrics.rows ?? 0) - (a.score.metrics.rows ?? 0));
|
|
2248
|
+
for (const f of sorted) {
|
|
2249
|
+
renderFinding(f, { concurrently: false, withReasons: false });
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/** Render one finding: table + columns, probing relations, reasons, and the create SQL. */
|
|
2253
|
+
function renderFinding(f, opts) {
|
|
2254
|
+
const m = f.missing;
|
|
2255
|
+
const rows = f.score.metrics.rows;
|
|
2256
|
+
const rowsLabel = rows !== null ? `~${rows.toLocaleString()} rows` : 'row count unknown';
|
|
2257
|
+
const sizeLabel = f.score.metrics.sizeBytes !== null ? `, ${(0, index_stats_js_1.formatBytes)(f.score.metrics.sizeBytes)}` : '';
|
|
2258
|
+
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(m.table))} ${(0, ui_js_1.dim)(`(${m.columns.join(', ')})`)} ${(0, ui_js_1.gray)(`${rowsLabel}${sizeLabel}`)}`);
|
|
2259
|
+
for (const p of m.probes) {
|
|
2260
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} probed by ${p.from}.${(0, ui_js_1.blue)(p.relation)} ${(0, ui_js_1.dim)(`(${p.type})`)}`);
|
|
2261
|
+
}
|
|
2262
|
+
if (opts.withReasons) {
|
|
2263
|
+
for (const reason of f.score.reasons) {
|
|
2264
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)(reason)}`);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(doctorCreateSql(f, { concurrently: opts.concurrently }))}`);
|
|
2268
|
+
(0, ui_js_1.newline)();
|
|
2269
|
+
}
|
|
2270
|
+
/** The invalid-index report section (a failed CONCURRENTLY build leaves these behind). */
|
|
2271
|
+
function renderInvalidIndexes(invalid) {
|
|
2272
|
+
if (invalid.length === 0)
|
|
2273
|
+
return;
|
|
2274
|
+
(0, ui_js_1.warn)(`Found ${(0, ui_js_1.bold)(String(invalid.length))} INVALID index(es) (a failed CREATE INDEX CONCURRENTLY leaves these).`);
|
|
2275
|
+
console.log(` ${(0, ui_js_1.dim)('IF NOT EXISTS skips an invalid index on rerun, so it never rebuilds. Drop it, then rerun:')}`);
|
|
2276
|
+
(0, ui_js_1.newline)();
|
|
2277
|
+
for (const idx of invalid) {
|
|
2278
|
+
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(idx.table))} ${(0, ui_js_1.dim)(`(${idx.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(idx.indexName)}`);
|
|
2279
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(idx.dropSql)}`);
|
|
2104
2280
|
(0, ui_js_1.newline)();
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
/** Write the --fix migration (CONCURRENTLY + directive by default; plain with --no-concurrently). */
|
|
2284
|
+
function renderFixMigration(findings, config, args) {
|
|
2285
|
+
const concurrently = args.noConcurrently !== true;
|
|
2286
|
+
const up = findings.map((f) => doctorCreateSql(f, { concurrently })).join('\n');
|
|
2287
|
+
const down = findings.map((f) => (0, index_advisor_js_1.buildDropIndexSql)(f.missing.indexName, { concurrently })).join('\n');
|
|
2288
|
+
let file;
|
|
2289
|
+
if (concurrently) {
|
|
2290
|
+
file = (0, migrate_js_1.createMigration)(config.migrationsDir, 'add_relation_fk_indexes', { up: `${CONCURRENTLY_RECIPE_COMMENT}\n\n${up}`, down }, { header: '-- turbine:no-transaction' });
|
|
2291
|
+
}
|
|
2292
|
+
else {
|
|
2293
|
+
file = (0, migrate_js_1.createMigration)(config.migrationsDir, 'add_relation_fk_indexes', { up, down });
|
|
2294
|
+
}
|
|
2295
|
+
(0, ui_js_1.success)(`Created migration: ${(0, ui_js_1.bold)(file.filename)}`);
|
|
2296
|
+
(0, ui_js_1.newline)();
|
|
2297
|
+
if (concurrently) {
|
|
2298
|
+
console.log(` ${(0, ui_js_1.dim)('This is a')} ${(0, ui_js_1.cyan)('no-transaction')} ${(0, ui_js_1.dim)('migration (CREATE INDEX CONCURRENTLY).')}`);
|
|
2105
2299
|
console.log(` ${(0, ui_js_1.dim)('Review it, then apply with:')} ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
|
|
2106
|
-
console.log(` ${(0, ui_js_1.dim)('
|
|
2107
|
-
console.log(` ${(0, ui_js_1.dim)('(cannot run inside a transaction, so it is not emitted in the migration).')}`);
|
|
2108
|
-
(0, ui_js_1.newline)();
|
|
2300
|
+
console.log(` ${(0, ui_js_1.dim)('For a plain in-transaction migration instead, use')} ${(0, ui_js_1.cyan)('doctor --fix --no-concurrently')}`);
|
|
2109
2301
|
}
|
|
2110
2302
|
else {
|
|
2111
|
-
console.log(` ${(0, ui_js_1.dim)('
|
|
2112
|
-
(0, ui_js_1.
|
|
2303
|
+
console.log(` ${(0, ui_js_1.dim)('Review it, then apply with:')} ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
|
|
2304
|
+
console.log(` ${(0, ui_js_1.dim)('For large, hot tables prefer the CONCURRENTLY form (drop')} ${(0, ui_js_1.cyan)('--no-concurrently')}${(0, ui_js_1.dim)(').')}`);
|
|
2113
2305
|
}
|
|
2306
|
+
(0, ui_js_1.newline)();
|
|
2114
2307
|
}
|
|
2115
2308
|
// ---------------------------------------------------------------------------
|
|
2116
2309
|
// Loopback host gate (Studio / Observe)
|
|
@@ -2542,7 +2735,7 @@ function showHelp() {
|
|
|
2542
2735
|
console.log(` ${(0, ui_js_1.dim)('status')} Show applied/pending migrations`);
|
|
2543
2736
|
console.log(` ${(0, ui_js_1.cyan)('seed')} Run seed file`);
|
|
2544
2737
|
console.log(` ${(0, ui_js_1.cyan)('status')} ${(0, ui_js_1.dim)('| info')} Show schema summary`);
|
|
2545
|
-
console.log(` ${(0, ui_js_1.cyan)('doctor')}
|
|
2738
|
+
console.log(` ${(0, ui_js_1.cyan)('doctor')} Cost-aware missing-FK-index triage ${(0, ui_js_1.dim)('(--fix, --json, --no-concurrently)')}`);
|
|
2546
2739
|
console.log(` ${(0, ui_js_1.cyan)('studio')} Launch local read-only web UI ${(0, ui_js_1.dim)('(--write for writes, --demo for a sample DB)')}`);
|
|
2547
2740
|
console.log(` ${(0, ui_js_1.cyan)('mcp')} Start read-only MCP server over stdio`);
|
|
2548
2741
|
console.log(` ${(0, ui_js_1.cyan)('observe')} Launch metrics dashboard ${(0, ui_js_1.dim)('(requires TURBINE_OBSERVE_URL)')}`);
|