@ddt-tools/cli 0.2.0 → 0.2.4
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/advise-tests-YNMKVJCD.js +87 -0
- package/dist/advise-tests-YNMKVJCD.js.map +1 -0
- package/dist/ai-NTNPYEKZ.js +86 -0
- package/dist/ai-NTNPYEKZ.js.map +1 -0
- package/dist/anonymize-LERTWUQO.js +139 -0
- package/dist/anonymize-LERTWUQO.js.map +1 -0
- package/dist/approval-GGZGKIU4.js +73 -0
- package/dist/approval-GGZGKIU4.js.map +1 -0
- package/dist/approval-chain-GWJKZHVU.js +118 -0
- package/dist/approval-chain-GWJKZHVU.js.map +1 -0
- package/dist/audit-log-2PH55BU4.js +159 -0
- package/dist/audit-log-2PH55BU4.js.map +1 -0
- package/dist/backlog-QNXGOUF4.js +76 -0
- package/dist/backlog-QNXGOUF4.js.map +1 -0
- package/dist/bisect-W3XKKRWG.js +111 -0
- package/dist/bisect-W3XKKRWG.js.map +1 -0
- package/dist/bookmarks-XVOGXGMC.js +107 -0
- package/dist/bookmarks-XVOGXGMC.js.map +1 -0
- package/dist/branch-S3I2IJGQ.js +103 -0
- package/dist/branch-S3I2IJGQ.js.map +1 -0
- package/dist/build-MP3JQEFO.js +20 -0
- package/dist/build-MP3JQEFO.js.map +1 -0
- package/dist/catalog-3J3NFNXP.js +137 -0
- package/dist/catalog-3J3NFNXP.js.map +1 -0
- package/dist/changelog-ZQAH3ULB.js +216 -0
- package/dist/changelog-ZQAH3ULB.js.map +1 -0
- package/dist/chunk-2FT6HXKS.js +55 -0
- package/dist/chunk-2FT6HXKS.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DL3V7UJ2.js +25 -0
- package/dist/chunk-DL3V7UJ2.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-XFXG347C.js +40 -0
- package/dist/chunk-XFXG347C.js.map +1 -0
- package/dist/cli.js +499 -19402
- package/dist/cli.js.map +1 -1
- package/dist/compare-P7JOV76O.js +379 -0
- package/dist/compare-P7JOV76O.js.map +1 -0
- package/dist/compare-profiles-H33CXZPD.js +219 -0
- package/dist/compare-profiles-H33CXZPD.js.map +1 -0
- package/dist/completion-ZSNCQKJ2.js +89 -0
- package/dist/completion-ZSNCQKJ2.js.map +1 -0
- package/dist/connection-CDGVEFUC.js +148 -0
- package/dist/connection-CDGVEFUC.js.map +1 -0
- package/dist/cost-estimate-S2MKHT2H.js +321 -0
- package/dist/cost-estimate-S2MKHT2H.js.map +1 -0
- package/dist/data-compare-46ZI7KHL.js +128 -0
- package/dist/data-compare-46ZI7KHL.js.map +1 -0
- package/dist/data-fit-WGEPLD5S.js +127 -0
- package/dist/data-fit-WGEPLD5S.js.map +1 -0
- package/dist/deploy-status-4H5KJFRC.js +58 -0
- package/dist/deploy-status-4H5KJFRC.js.map +1 -0
- package/dist/design-ILX3ZSWW.js +135 -0
- package/dist/design-ILX3ZSWW.js.map +1 -0
- package/dist/diagnose-WPUL67E4.js +150 -0
- package/dist/diagnose-WPUL67E4.js.map +1 -0
- package/dist/discover-DEO2R5T6.js +78 -0
- package/dist/discover-DEO2R5T6.js.map +1 -0
- package/dist/docs-QNY3MUVO.js +183 -0
- package/dist/docs-QNY3MUVO.js.map +1 -0
- package/dist/drift-FDRNPWQA.js +233 -0
- package/dist/drift-FDRNPWQA.js.map +1 -0
- package/dist/drift-gate-6BWWWMHW.js +103 -0
- package/dist/drift-gate-6BWWWMHW.js.map +1 -0
- package/dist/error-lookup-4R3Y4RBC.js +56 -0
- package/dist/error-lookup-4R3Y4RBC.js.map +1 -0
- package/dist/errorReporting-3LPE2IJY.js +109 -0
- package/dist/errorReporting-3LPE2IJY.js.map +1 -0
- package/dist/exec-JOLH5LPT.js +122 -0
- package/dist/exec-JOLH5LPT.js.map +1 -0
- package/dist/explain-NS26WE2Y.js +189 -0
- package/dist/explain-NS26WE2Y.js.map +1 -0
- package/dist/explorer-GSYYYOAL.js +58 -0
- package/dist/explorer-GSYYYOAL.js.map +1 -0
- package/dist/extract-4LWEZG4O.js +152 -0
- package/dist/extract-4LWEZG4O.js.map +1 -0
- package/dist/features-KQV4OFIZ.js +54 -0
- package/dist/features-KQV4OFIZ.js.map +1 -0
- package/dist/feedback-CBLGXUEG.js +158 -0
- package/dist/feedback-CBLGXUEG.js.map +1 -0
- package/dist/find-SMXRCZ76.js +176 -0
- package/dist/find-SMXRCZ76.js.map +1 -0
- package/dist/format-HMGG6MY3.js +277 -0
- package/dist/format-HMGG6MY3.js.map +1 -0
- package/dist/generate-W7VLBDLI.js +160 -0
- package/dist/generate-W7VLBDLI.js.map +1 -0
- package/dist/graph-YYL5UYCJ.js +168 -0
- package/dist/graph-YYL5UYCJ.js.map +1 -0
- package/dist/history-GDRFP4PG.js +184 -0
- package/dist/history-GDRFP4PG.js.map +1 -0
- package/dist/hosts-DRFZTMIJ.js +45 -0
- package/dist/hosts-DRFZTMIJ.js.map +1 -0
- package/dist/impact-A4NU6CB2.js +63 -0
- package/dist/impact-A4NU6CB2.js.map +1 -0
- package/dist/import-2RNYDL4E.js +79 -0
- package/dist/import-2RNYDL4E.js.map +1 -0
- package/dist/index.cjs +11 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/init-EAOGNGXI.js +54 -0
- package/dist/init-EAOGNGXI.js.map +1 -0
- package/dist/install-hooks-G3Y5LVXK.js +109 -0
- package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
- package/dist/license-Z5YSC7XQ.js +43 -0
- package/dist/license-Z5YSC7XQ.js.map +1 -0
- package/dist/lineage-C5CGVP36.js +555 -0
- package/dist/lineage-C5CGVP36.js.map +1 -0
- package/dist/lint-AQFPZ3WG.js +144 -0
- package/dist/lint-AQFPZ3WG.js.map +1 -0
- package/dist/mcp-F7FND5X7.js +343 -0
- package/dist/mcp-F7FND5X7.js.map +1 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
- package/dist/migrate-platform-E7VZFPO5.js +91 -0
- package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
- package/dist/optimize-WUJ5ZN5Y.js +109 -0
- package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
- package/dist/perf-UULZSREY.js +200 -0
- package/dist/perf-UULZSREY.js.map +1 -0
- package/dist/pii-QHU32VML.js +146 -0
- package/dist/pii-QHU32VML.js.map +1 -0
- package/dist/pilot-BR6GVK32.js +29 -0
- package/dist/pilot-BR6GVK32.js.map +1 -0
- package/dist/pr-comment-2FOA3EXG.js +81 -0
- package/dist/pr-comment-2FOA3EXG.js.map +1 -0
- package/dist/preview-XNY422OU.js +46 -0
- package/dist/preview-XNY422OU.js.map +1 -0
- package/dist/profile-SQTBNKYS.js +98 -0
- package/dist/profile-SQTBNKYS.js.map +1 -0
- package/dist/promote-FSGUPIPD.js +417 -0
- package/dist/promote-FSGUPIPD.js.map +1 -0
- package/dist/publish-AYCRMCE2.js +739 -0
- package/dist/publish-AYCRMCE2.js.map +1 -0
- package/dist/purge-Y5IOTXKA.js +56 -0
- package/dist/purge-Y5IOTXKA.js.map +1 -0
- package/dist/query-log-SDDGMJLJ.js +112 -0
- package/dist/query-log-SDDGMJLJ.js.map +1 -0
- package/dist/refactor-TC7S43F2.js +5809 -0
- package/dist/refactor-TC7S43F2.js.map +1 -0
- package/dist/refresh-MDJYOYV5.js +39 -0
- package/dist/refresh-MDJYOYV5.js.map +1 -0
- package/dist/replay-E4664A5K.js +118 -0
- package/dist/replay-E4664A5K.js.map +1 -0
- package/dist/revert-QWQWCJJB.js +111 -0
- package/dist/revert-QWQWCJJB.js.map +1 -0
- package/dist/review-7CAVLD67.js +164 -0
- package/dist/review-7CAVLD67.js.map +1 -0
- package/dist/rollback-suggest-C6D5YFCA.js +79 -0
- package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
- package/dist/safer-alternative-QR4QEFUV.js +84 -0
- package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
- package/dist/safety-OFWUFLK4.js +165 -0
- package/dist/safety-OFWUFLK4.js.map +1 -0
- package/dist/savings-MEBE4TXI.js +95 -0
- package/dist/savings-MEBE4TXI.js.map +1 -0
- package/dist/scan-secrets-XCUBMLHL.js +54 -0
- package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
- package/dist/schema-7JZIG6QR.js +447 -0
- package/dist/schema-7JZIG6QR.js.map +1 -0
- package/dist/script-BMYVBHFR.js +167 -0
- package/dist/script-BMYVBHFR.js.map +1 -0
- package/dist/search-TA3C3AZT.js +151 -0
- package/dist/search-TA3C3AZT.js.map +1 -0
- package/dist/seed-W4Q3L2IU.js +101 -0
- package/dist/seed-W4Q3L2IU.js.map +1 -0
- package/dist/sketch-6B2V6FJV.js +83 -0
- package/dist/sketch-6B2V6FJV.js.map +1 -0
- package/dist/snapshot-YMVS322L.js +171 -0
- package/dist/snapshot-YMVS322L.js.map +1 -0
- package/dist/snippets-EVTN63OU.js +74 -0
- package/dist/snippets-EVTN63OU.js.map +1 -0
- package/dist/standards-FGJW3CQL.js +238 -0
- package/dist/standards-FGJW3CQL.js.map +1 -0
- package/dist/suggest-V3LVIFZ5.js +44 -0
- package/dist/suggest-V3LVIFZ5.js.map +1 -0
- package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
- package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
- package/dist/suite-YTQ3CNX5.js +85 -0
- package/dist/suite-YTQ3CNX5.js.map +1 -0
- package/dist/telemetry-KOIY3NEQ.js +90 -0
- package/dist/telemetry-KOIY3NEQ.js.map +1 -0
- package/dist/template-MUJ6X6LN.js +396 -0
- package/dist/template-MUJ6X6LN.js.map +1 -0
- package/dist/test-XFSQHR2S.js +169 -0
- package/dist/test-XFSQHR2S.js.map +1 -0
- package/dist/trial-GFTGYCR3.js +31 -0
- package/dist/trial-GFTGYCR3.js.map +1 -0
- package/dist/validate-LFDEZFFH.js +107 -0
- package/dist/validate-LFDEZFFH.js.map +1 -0
- package/dist/verify-KRDYOJCR.js +76 -0
- package/dist/verify-KRDYOJCR.js.map +1 -0
- package/dist/watch-FSG23RR3.js +80 -0
- package/dist/watch-FSG23RR3.js.map +1 -0
- package/dist/xcompare-U4TXTTIR.js +87 -0
- package/dist/xcompare-U4TXTTIR.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19298
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/cost-estimate.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\n\ninterface HistoryEntry {\n sqlText?: string;\n durationMs?: number;\n}\n\n/**\n * `ddt cost-estimate` — heuristic DBU estimator for a generated\n * migration script.\n *\n * Approach mirrors `sdt cost-estimate` but uses Databricks DBU\n * pricing instead of Snowflake credits. The \"cluster size\" ladder\n * maps to common SQL warehouse sizes (XS/S/M/L/XL/2XL/3XL).\n *\n * Ranges are conservative. Actual DBU consumption depends on file\n * counts, Z-ORDER state, and Photon enablement that this static\n * pass can't see — we say so in the output.\n */\nexport function costEstimateCommand(): Command {\n const cmd = new Command('cost-estimate');\n cmd\n .description('Heuristic Databricks-DBU estimate for a generated migration script.')\n .requiredOption(\n '--script <path>',\n 'Path to a generated migration script (.sql) from `ddt publish`.',\n )\n .option(\n '--warehouse-size <size>',\n 'SQL warehouse size: 2X-Small | X-Small | Small | Medium | Large | X-Large | 2X-Large | 3X-Large | 4X-Large.',\n 'Small',\n )\n .option('--format <fmt>', 'table | json | markdown. Default table.', 'table')\n .option('-o, --out <path>', 'Write output to file. Defaults to stdout.')\n .option(\n '--calibrate-from <path>',\n 'AI Phase 6 calibration: a JSON file of QueryHistoryEntry[] from prior deploys. Each historic statement is classified against the same cost-classes; classes with ≥3 samples adopt empirical min/max duration in place of the heuristic range.',\n )\n .action(\n async (opts: {\n script: string;\n warehouseSize?: string;\n format?: string;\n out?: string;\n calibrateFrom?: string;\n explain?: boolean;\n }) => {\n const sql = await fs.readFile(path.resolve(String(opts.script)), 'utf8');\n const whRaw = String(opts.warehouseSize ?? 'Small');\n const wh = whRaw as WarehouseSize;\n if (!(wh in DBU_PER_HOUR)) {\n throw new Error(\n `Unknown --warehouse-size: ${whRaw}. Use one of ${Object.keys(DBU_PER_HOUR).join(' | ')}.`,\n );\n }\n let calibration: ClassCalibration | undefined;\n if (opts.calibrateFrom) {\n const histRaw = await fs.readFile(path.resolve(String(opts.calibrateFrom)), 'utf8');\n const parsed = JSON.parse(histRaw);\n const entries: HistoryEntry[] = Array.isArray(parsed)\n ? parsed\n : Array.isArray(parsed.entries)\n ? parsed.entries\n : [];\n calibration = buildCalibration(entries);\n }\n const report = estimateCost(sql, wh, calibration);\n const fmt = String(opts.format ?? 'table').toLowerCase();\n let payload: string;\n if (fmt === 'json') {\n payload = JSON.stringify(report, null, 2);\n } else if (fmt === 'markdown') {\n payload = renderMarkdown(report);\n } else {\n payload = renderTable(report);\n }\n if (opts.out) {\n const p = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, payload + (payload.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${p}.`);\n } else {\n process.stdout.write(payload + (payload.endsWith('\\n') ? '' : '\\n'));\n }\n await runExplain(\n {\n feature: 'cost-estimate.explain',\n systemPrompt:\n 'You are a Databricks cost engineer. Walk the team through this DBU estimate: what is the dominant cost driver, what does the range mean in practice, and what knobs (warehouse size, Photon, sequencing, off-peak timing) would meaningfully reduce it.',\n },\n opts,\n () =>\n `Cost estimate (JSON) follows:\\n\\n${JSON.stringify(report, null, 2)}\\n\\nNarrate this for a teammate.`,\n );\n },\n );\n attachExplainFlag(cmd);\n return cmd;\n}\n\ntype WarehouseSize =\n | '2X-Small'\n | 'X-Small'\n | 'Small'\n | 'Medium'\n | 'Large'\n | 'X-Large'\n | '2X-Large'\n | '3X-Large'\n | '4X-Large';\n\nconst DBU_PER_HOUR: Record<WarehouseSize, number> = {\n '2X-Small': 4,\n 'X-Small': 6,\n Small: 12,\n Medium: 24,\n Large: 40,\n 'X-Large': 80,\n '2X-Large': 144,\n '3X-Large': 272,\n '4X-Large': 528,\n};\n\ninterface CostClass {\n id: string;\n description: string;\n minSeconds: number;\n maxSeconds: number;\n test: (sql: string) => boolean;\n}\n\nconst COST_CLASSES: CostClass[] = [\n {\n id: 'table-rebuild',\n description: 'CREATE OR REPLACE TABLE or CTAS — rewrites the entire table',\n minSeconds: 30,\n maxSeconds: 1800,\n test: (s) =>\n /CREATE\\s+(?:OR\\s+REPLACE\\s+)?TABLE\\s+.+\\s+AS\\s+SELECT/i.test(s) ||\n /CREATE\\s+OR\\s+REPLACE\\s+TABLE\\b/i.test(s),\n },\n {\n id: 'optimize',\n description: 'OPTIMIZE / OPTIMIZE … ZORDER BY — compact + cluster',\n minSeconds: 60,\n maxSeconds: 3600,\n test: (s) => /^\\s*OPTIMIZE\\b/i.test(s),\n },\n {\n id: 'vacuum',\n description: 'VACUUM — removes old files',\n minSeconds: 30,\n maxSeconds: 600,\n test: (s) => /^\\s*VACUUM\\b/i.test(s),\n },\n {\n id: 'add-column',\n description: 'ALTER TABLE ADD COLUMN — metadata-only',\n minSeconds: 1,\n maxSeconds: 5,\n test: (s) => /ALTER\\s+TABLE\\b.*\\bADD\\s+COLUMN\\b/i.test(s),\n },\n {\n id: 'drop-column',\n description: 'ALTER TABLE DROP COLUMN — metadata-only (DV-enabled)',\n minSeconds: 1,\n maxSeconds: 5,\n test: (s) => /ALTER\\s+TABLE\\b.*\\bDROP\\s+COLUMN\\b/i.test(s),\n },\n {\n id: 'alter-type-narrowing',\n description: 'ALTER COLUMN TYPE — may require rewrite',\n minSeconds: 30,\n maxSeconds: 600,\n test: (s) => /ALTER\\s+TABLE\\b.*\\bALTER\\s+COLUMN\\b.*\\bTYPE\\b/i.test(s),\n },\n {\n id: 'create-view',\n description: 'CREATE OR REPLACE VIEW — metadata-only',\n minSeconds: 1,\n maxSeconds: 3,\n test: (s) => /CREATE\\s+(?:OR\\s+REPLACE\\s+)?VIEW\\b/i.test(s) && !/MATERIALIZED/i.test(s),\n },\n {\n id: 'create-mv',\n description: 'CREATE MATERIALIZED VIEW — initial materialisation',\n minSeconds: 60,\n maxSeconds: 3600,\n test: (s) => /CREATE\\s+(?:OR\\s+REPLACE\\s+)?MATERIALIZED\\s+VIEW\\b/i.test(s),\n },\n {\n id: 'create-streaming',\n description: 'CREATE STREAMING TABLE — initial population',\n minSeconds: 60,\n maxSeconds: 1800,\n test: (s) => /CREATE\\s+(?:OR\\s+REPLACE\\s+)?STREAMING\\s+TABLE\\b/i.test(s),\n },\n {\n id: 'create-pipeline',\n description: 'CREATE PIPELINE — DLT initial run',\n minSeconds: 120,\n maxSeconds: 3600,\n test: (s) => /CREATE\\s+(?:OR\\s+REPLACE\\s+)?PIPELINE\\b/i.test(s),\n },\n {\n id: 'set-tblproperties',\n description: 'ALTER TABLE … SET TBLPROPERTIES — metadata-only',\n minSeconds: 1,\n maxSeconds: 3,\n test: (s) => /ALTER\\s+TABLE\\b.*\\bSET\\s+TBLPROPERTIES\\b/i.test(s),\n },\n {\n id: 'drop',\n description: 'DROP statement — metadata-only',\n minSeconds: 1,\n maxSeconds: 3,\n test: (s) => /^\\s*DROP\\s+/i.test(s),\n },\n {\n id: 'grant',\n description: 'GRANT / REVOKE — metadata-only',\n minSeconds: 1,\n maxSeconds: 2,\n test: (s) => /^\\s*(?:GRANT|REVOKE)\\s+/i.test(s),\n },\n {\n id: 'comment',\n description: 'COMMENT-only statement — free',\n minSeconds: 0,\n maxSeconds: 0,\n test: (s) =>\n s\n .replace(/--[^\\n]*/g, '')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .trim().length === 0,\n },\n];\n\ninterface CostReport {\n warehouseSize: WarehouseSize;\n dbuPerHour: number;\n totalStatements: number;\n classifiedStatements: number;\n unknownStatements: number;\n perClass: Array<{\n id: string;\n description: string;\n count: number;\n minSeconds: number;\n maxSeconds: number;\n minDbu: number;\n maxDbu: number;\n /** When set, the per-statement range came from this many empirical samples. */\n calibratedFromSamples?: number;\n }>;\n totals: {\n minSeconds: number;\n maxSeconds: number;\n minDbu: number;\n maxDbu: number;\n };\n}\n\n/**\n * AI Phase 6 calibration — per-class empirical duration ranges harvested\n * from prior `QueryHistoryEntry[]` (typed in `@ddt-tools/core/queryHistory`).\n * When a class has at least 3 historic samples, its heuristic min/max is\n * replaced with the observed min/max. Classes that fall back to the\n * heuristic are flagged in the report so the user can see what was\n * calibrated.\n */\ninterface ClassCalibration {\n perClass: Record<string, { sampleSize: number; minSeconds: number; maxSeconds: number }>;\n}\n\nfunction buildCalibration(entries: HistoryEntry[]): ClassCalibration {\n const samples = new Map<string, number[]>();\n for (const e of entries) {\n const sql = typeof e.sqlText === 'string' ? e.sqlText : '';\n const dur = typeof e.durationMs === 'number' ? e.durationMs : NaN;\n if (!sql || !isFinite(dur) || dur <= 0) continue;\n const matched = COST_CLASSES.find((c) => c.test(sql));\n if (!matched) continue;\n const arr = samples.get(matched.id) ?? [];\n arr.push(dur / 1000);\n samples.set(matched.id, arr);\n }\n const perClass: ClassCalibration['perClass'] = {};\n for (const [id, durs] of samples) {\n if (durs.length < 3) continue;\n durs.sort((a, b) => a - b);\n perClass[id] = {\n sampleSize: durs.length,\n minSeconds: Math.max(1, Math.round(durs[0]!)),\n maxSeconds: Math.max(1, Math.round(durs[durs.length - 1]!)),\n };\n }\n return { perClass };\n}\n\nfunction estimateCost(\n sql: string,\n warehouseSize: WarehouseSize,\n calibration?: ClassCalibration,\n): CostReport {\n const statements = splitStatements(sql);\n const classCounts = new Map<string, number>();\n let classifiedStatements = 0;\n let unknownStatements = 0;\n for (const stmt of statements) {\n const matched = COST_CLASSES.find((c) => c.test(stmt));\n if (matched) {\n classCounts.set(matched.id, (classCounts.get(matched.id) ?? 0) + 1);\n classifiedStatements++;\n } else if (stmt.trim().length > 0) {\n unknownStatements++;\n }\n }\n const dbuPerHour = DBU_PER_HOUR[warehouseSize];\n const perClass = COST_CLASSES.filter((c) => (classCounts.get(c.id) ?? 0) > 0).map((c) => {\n const count = classCounts.get(c.id) ?? 0;\n const cal = calibration?.perClass[c.id];\n const minSecondsPerStmt = cal ? cal.minSeconds : c.minSeconds;\n const maxSecondsPerStmt = cal ? cal.maxSeconds : c.maxSeconds;\n const minSeconds = minSecondsPerStmt * count;\n const maxSeconds = maxSecondsPerStmt * count;\n return {\n id: c.id,\n description: c.description,\n count,\n minSeconds,\n maxSeconds,\n minDbu: (minSeconds / 3600) * dbuPerHour,\n maxDbu: (maxSeconds / 3600) * dbuPerHour,\n ...(cal ? { calibratedFromSamples: cal.sampleSize } : {}),\n };\n });\n const totals = perClass.reduce(\n (acc, c) => ({\n minSeconds: acc.minSeconds + c.minSeconds,\n maxSeconds: acc.maxSeconds + c.maxSeconds,\n minDbu: acc.minDbu + c.minDbu,\n maxDbu: acc.maxDbu + c.maxDbu,\n }),\n { minSeconds: 0, maxSeconds: 0, minDbu: 0, maxDbu: 0 },\n );\n return {\n warehouseSize,\n dbuPerHour,\n totalStatements: statements.length,\n classifiedStatements,\n unknownStatements,\n perClass,\n totals,\n };\n}\n\nfunction splitStatements(sql: string): string[] {\n return sql\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .split(/;\\s*$/m)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction renderTable(r: CostReport): string {\n const lines: string[] = [];\n lines.push(`Cost estimate — warehouse=${r.warehouseSize} (${r.dbuPerHour} DBU/hour)`);\n lines.push('');\n lines.push(\n ` Statements: ${r.totalStatements} (${r.classifiedStatements} classified, ${r.unknownStatements} unknown).`,\n );\n lines.push('');\n const idW = Math.max(20, ...r.perClass.map((c) => c.id.length));\n for (const c of r.perClass) {\n lines.push(\n ` ${c.id.padEnd(idW)} ×${String(c.count).padStart(4)} ≈ ${c.minDbu.toFixed(3)} – ${c.maxDbu.toFixed(3)} DBU`,\n );\n }\n lines.push('');\n lines.push(\n ` TOTAL ≈ ${r.totals.minDbu.toFixed(3)} – ${r.totals.maxDbu.toFixed(3)} DBU`,\n );\n lines.push(` TOTAL (duration estimate) ≈ ${r.totals.minSeconds}s – ${r.totals.maxSeconds}s`);\n lines.push('');\n lines.push(' Note: ranges are heuristic. Actual DBU consumption depends on file count,');\n lines.push(' Z-ORDER state, deletion-vector enablement, and Photon. Treat the upper bound');\n lines.push(' as a pessimistic ceiling, not a likely outcome.');\n return lines.join('\\n');\n}\n\nfunction renderMarkdown(r: CostReport): string {\n const lines: string[] = [];\n lines.push(`# Cost estimate`);\n lines.push('');\n lines.push(`**Warehouse:** ${r.warehouseSize} (${r.dbuPerHour} DBU/hour)`);\n lines.push(\n `**Statements:** ${r.totalStatements} total, ${r.classifiedStatements} classified, ${r.unknownStatements} unknown`,\n );\n lines.push('');\n lines.push('## Estimate by statement class');\n lines.push('');\n lines.push('| Class | Count | DBU range |');\n lines.push('|---|---|---|');\n for (const c of r.perClass) {\n lines.push(\n `| \\`${c.id}\\` (${c.description}) | ${c.count} | ${c.minDbu.toFixed(3)} – ${c.maxDbu.toFixed(3)} |`,\n );\n }\n lines.push('');\n lines.push(\n `**Total:** ${r.totals.minDbu.toFixed(3)} – ${r.totals.maxDbu.toFixed(3)} DBU (${r.totals.minSeconds}–${r.totals.maxSeconds}s)`,\n );\n lines.push('');\n lines.push(\n '> Ranges are heuristic. Actual DBU consumption depends on file count, Z-ORDER state, DV enablement, and Photon.',\n );\n return lines.join('\\n');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/data-compare.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { dataCompare, createConnection, getProfile } from "@ddt-tools/core";
|
|
8
|
+
function dataCompareCommand() {
|
|
9
|
+
const cmd = new Command("data-compare");
|
|
10
|
+
cmd.description(
|
|
11
|
+
"Row-level data compare: diff two tables by primary key and emit an INSERT/UPDATE/DELETE script that converts target into source. Source/target rows read from JSON files in v1."
|
|
12
|
+
).requiredOption(
|
|
13
|
+
"--table <fqn>",
|
|
14
|
+
"Fully-qualified table name to embed in the emitted script (CATALOG.SCHEMA.TABLE)."
|
|
15
|
+
).requiredOption("--key <cols>", "Comma-separated primary-key columns.").option(
|
|
16
|
+
"--source <path>",
|
|
17
|
+
"JSON file containing the source-side rows (TableRow[]). Mutually exclusive with --source-live."
|
|
18
|
+
).option(
|
|
19
|
+
"--target <path>",
|
|
20
|
+
"JSON file containing the target-side rows (TableRow[]). Mutually exclusive with --target-live."
|
|
21
|
+
).option(
|
|
22
|
+
"--source-live <profile>",
|
|
23
|
+
"Connection profile to read source rows from live via SELECT * FROM --table. Mutually exclusive with --source."
|
|
24
|
+
).option(
|
|
25
|
+
"--target-live <profile>",
|
|
26
|
+
"Connection profile to read target rows from live via SELECT * FROM --table. Mutually exclusive with --target."
|
|
27
|
+
).option(
|
|
28
|
+
"--row-limit <n>",
|
|
29
|
+
"Cap rows fetched per side in live mode. Default 100000. Use 0 for unbounded (DANGER on prod tables).",
|
|
30
|
+
"100000"
|
|
31
|
+
).option(
|
|
32
|
+
"--columns <cols>",
|
|
33
|
+
"Comma-separated column list to consider. Defaults to all keys seen in the rows."
|
|
34
|
+
).option(
|
|
35
|
+
"--max-changes <n>",
|
|
36
|
+
"Cap the changed-row count (added/removed are not capped). Default 10000.",
|
|
37
|
+
"10000"
|
|
38
|
+
).option(
|
|
39
|
+
"--case-sensitive",
|
|
40
|
+
"Compare column names case-sensitively (default: false \u2014 UC default folding).",
|
|
41
|
+
false
|
|
42
|
+
).option("--no-transaction", "Omit BEGIN/COMMIT wrappers from the emitted script.").option("--no-header", "Omit the REVIEW header from the emitted script.").option("--format <fmt>", "Output format: sql | json. Default sql.", "sql").option("-o, --output <path>", "Write output to a file instead of stdout.").action(async (opts) => {
|
|
43
|
+
await runDataCompare(opts, "databricks");
|
|
44
|
+
});
|
|
45
|
+
return cmd;
|
|
46
|
+
}
|
|
47
|
+
async function runDataCompare(opts, dialect) {
|
|
48
|
+
const fqn = String(opts.table);
|
|
49
|
+
const keyColumns = String(opts.key).split(",").map((s) => s.trim()).filter(Boolean);
|
|
50
|
+
if (keyColumns.length === 0) {
|
|
51
|
+
throw new Error("--key must include at least one column.");
|
|
52
|
+
}
|
|
53
|
+
const columns = opts.columns ? String(opts.columns).split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
54
|
+
const sourceJson = opts.source;
|
|
55
|
+
const targetJson = opts.target;
|
|
56
|
+
const sourceLive = opts.sourceLive;
|
|
57
|
+
const targetLive = opts.targetLive;
|
|
58
|
+
if (Boolean(sourceJson) === Boolean(sourceLive)) {
|
|
59
|
+
throw new Error("Specify exactly one of --source or --source-live.");
|
|
60
|
+
}
|
|
61
|
+
if (Boolean(targetJson) === Boolean(targetLive)) {
|
|
62
|
+
throw new Error("Specify exactly one of --target or --target-live.");
|
|
63
|
+
}
|
|
64
|
+
const rowLimit = Number(opts.rowLimit ?? "100000");
|
|
65
|
+
const [sourceRows, targetRows] = await Promise.all([
|
|
66
|
+
sourceLive ? fetchLiveRows(sourceLive, fqn, columns, rowLimit, "source") : readRowsFile(String(sourceJson)),
|
|
67
|
+
targetLive ? fetchLiveRows(targetLive, fqn, columns, rowLimit, "target") : readRowsFile(String(targetJson))
|
|
68
|
+
]);
|
|
69
|
+
const result = dataCompare.diffRows(
|
|
70
|
+
{
|
|
71
|
+
fqn,
|
|
72
|
+
keyColumns,
|
|
73
|
+
...columns ? { columns } : {},
|
|
74
|
+
sourceRows,
|
|
75
|
+
targetRows
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
maxChanges: Number(opts.maxChanges ?? "10000") || 1e4,
|
|
79
|
+
caseSensitiveColumns: opts.caseSensitive === true
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
const format = String(opts.format).toLowerCase();
|
|
83
|
+
const out = format === "json" ? JSON.stringify(result, null, 2) : dataCompare.renderDataCompareScript(result, dialect, {
|
|
84
|
+
transactional: opts.transaction !== false,
|
|
85
|
+
includeHeader: opts.header !== false
|
|
86
|
+
});
|
|
87
|
+
if (opts.output) {
|
|
88
|
+
const outPath = path.resolve(String(opts.output));
|
|
89
|
+
await fs.writeFile(outPath, out, "utf8");
|
|
90
|
+
console.error(
|
|
91
|
+
`data-compare: wrote ${outPath} (added=${result.diff.added.length} removed=${result.diff.removed.length} changed=${result.diff.changed.length} unchanged=${result.diff.unchanged}${result.truncated ? " truncated" : ""})`
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
process.stdout.write(out);
|
|
95
|
+
if (format !== "json") process.stdout.write("\n");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function readRowsFile(p) {
|
|
99
|
+
const raw = await fs.readFile(path.resolve(p), "utf8");
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!Array.isArray(parsed)) {
|
|
102
|
+
throw new Error(`${p}: expected a JSON array of row objects.`);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
async function fetchLiveRows(profileName, fqn, columns, rowLimit, side) {
|
|
107
|
+
const projection = columns && columns.length > 0 ? columns.join(", ") : "*";
|
|
108
|
+
const limit = rowLimit > 0 ? ` LIMIT ${Math.floor(rowLimit)}` : "";
|
|
109
|
+
const sql = `SELECT ${projection} FROM ${fqn}${limit}`;
|
|
110
|
+
const profile = await getProfile(profileName);
|
|
111
|
+
const conn = createConnection(profile);
|
|
112
|
+
console.error(`data-compare (${side}): connecting to ${profile.auth.host}\u2026`);
|
|
113
|
+
await conn.connect();
|
|
114
|
+
try {
|
|
115
|
+
const rows = await conn.executeRows(sql);
|
|
116
|
+
console.error(
|
|
117
|
+
`data-compare (${side}): fetched ${rows.length} row(s) from ${fqn}${rowLimit > 0 && rows.length === rowLimit ? " (LIMIT reached \u2014 increase --row-limit if more rows are needed)" : ""}.`
|
|
118
|
+
);
|
|
119
|
+
return rows;
|
|
120
|
+
} finally {
|
|
121
|
+
await conn.disconnect().catch(() => void 0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
dataCompareCommand,
|
|
126
|
+
runDataCompare
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=data-compare-46ZI7KHL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/data-compare.ts"],"sourcesContent":["/**\n * `ddt data-compare` — Tier-1 SSDT-gap row-level data compare.\n *\n * Reads source + target rows from either:\n * - JSON files (`--source` / `--target`), or\n * - live warehouses (`--source-live <profile>` / `--target-live <profile>`)\n * via `DatabricksConnection.executeRows`.\n *\n * Live mode runs `SELECT <columns> FROM <fqn>` against each side and\n * pipes rows through the dialect-aware `dataCompare.diffRows` substrate.\n *\n * Usage (JSON):\n * ddt data-compare --table CAT.SCH.T --key ID \\\n * --source rows-source.json --target rows-target.json\n *\n * Usage (live):\n * ddt data-compare --table CAT.SCH.T --key ID \\\n * --source-live dev --target-live prod \\\n * [--columns ID,EMAIL,STATUS] [--row-limit 50000]\n *\n * Mirrors `sdt data-compare`.\n */\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { Command } from 'commander';\nimport { dataCompare, createConnection, getProfile } from '@ddt-tools/core';\n\nexport function dataCompareCommand(): Command {\n const cmd = new Command('data-compare');\n cmd\n .description(\n 'Row-level data compare: diff two tables by primary key and emit an INSERT/UPDATE/DELETE script ' +\n 'that converts target into source. Source/target rows read from JSON files in v1.',\n )\n .requiredOption(\n '--table <fqn>',\n 'Fully-qualified table name to embed in the emitted script (CATALOG.SCHEMA.TABLE).',\n )\n .requiredOption('--key <cols>', 'Comma-separated primary-key columns.')\n .option(\n '--source <path>',\n 'JSON file containing the source-side rows (TableRow[]). Mutually exclusive with --source-live.',\n )\n .option(\n '--target <path>',\n 'JSON file containing the target-side rows (TableRow[]). Mutually exclusive with --target-live.',\n )\n .option(\n '--source-live <profile>',\n 'Connection profile to read source rows from live via SELECT * FROM --table. Mutually exclusive with --source.',\n )\n .option(\n '--target-live <profile>',\n 'Connection profile to read target rows from live via SELECT * FROM --table. Mutually exclusive with --target.',\n )\n .option(\n '--row-limit <n>',\n 'Cap rows fetched per side in live mode. Default 100000. Use 0 for unbounded (DANGER on prod tables).',\n '100000',\n )\n .option(\n '--columns <cols>',\n 'Comma-separated column list to consider. Defaults to all keys seen in the rows.',\n )\n .option(\n '--max-changes <n>',\n 'Cap the changed-row count (added/removed are not capped). Default 10000.',\n '10000',\n )\n .option(\n '--case-sensitive',\n 'Compare column names case-sensitively (default: false — UC default folding).',\n false,\n )\n .option('--no-transaction', 'Omit BEGIN/COMMIT wrappers from the emitted script.')\n .option('--no-header', 'Omit the REVIEW header from the emitted script.')\n .option('--format <fmt>', 'Output format: sql | json. Default sql.', 'sql')\n .option('-o, --output <path>', 'Write output to a file instead of stdout.')\n .action(async (opts) => {\n await runDataCompare(opts, 'databricks');\n });\n return cmd;\n}\n\nexport async function runDataCompare(\n opts: Record<string, unknown>,\n dialect: 'snowflake' | 'databricks',\n): Promise<void> {\n const fqn = String(opts.table);\n const keyColumns = String(opts.key)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (keyColumns.length === 0) {\n throw new Error('--key must include at least one column.');\n }\n const columns = opts.columns\n ? String(opts.columns)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : undefined;\n\n const sourceJson = opts.source as string | undefined;\n const targetJson = opts.target as string | undefined;\n const sourceLive = opts.sourceLive as string | undefined;\n const targetLive = opts.targetLive as string | undefined;\n if (Boolean(sourceJson) === Boolean(sourceLive)) {\n throw new Error('Specify exactly one of --source or --source-live.');\n }\n if (Boolean(targetJson) === Boolean(targetLive)) {\n throw new Error('Specify exactly one of --target or --target-live.');\n }\n const rowLimit = Number(opts.rowLimit ?? '100000');\n\n const [sourceRows, targetRows] = await Promise.all([\n sourceLive\n ? fetchLiveRows(sourceLive, fqn, columns, rowLimit, 'source')\n : readRowsFile(String(sourceJson)),\n targetLive\n ? fetchLiveRows(targetLive, fqn, columns, rowLimit, 'target')\n : readRowsFile(String(targetJson)),\n ]);\n\n const result = dataCompare.diffRows(\n {\n fqn,\n keyColumns,\n ...(columns ? { columns } : {}),\n sourceRows,\n targetRows,\n },\n {\n maxChanges: Number(opts.maxChanges ?? '10000') || 10_000,\n caseSensitiveColumns: opts.caseSensitive === true,\n },\n );\n\n const format = String(opts.format).toLowerCase();\n const out =\n format === 'json'\n ? JSON.stringify(result, null, 2)\n : dataCompare.renderDataCompareScript(result, dialect, {\n transactional: opts.transaction !== false,\n includeHeader: opts.header !== false,\n });\n\n if (opts.output) {\n const outPath = path.resolve(String(opts.output));\n await fs.writeFile(outPath, out, 'utf8');\n console.error(\n `data-compare: wrote ${outPath} (added=${result.diff.added.length} ` +\n `removed=${result.diff.removed.length} changed=${result.diff.changed.length} ` +\n `unchanged=${result.diff.unchanged}${result.truncated ? ' truncated' : ''})`,\n );\n } else {\n process.stdout.write(out);\n if (format !== 'json') process.stdout.write('\\n');\n }\n}\n\nasync function readRowsFile(p: string): Promise<dataCompare.TableRow[]> {\n const raw = await fs.readFile(path.resolve(p), 'utf8');\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) {\n throw new Error(`${p}: expected a JSON array of row objects.`);\n }\n return parsed as dataCompare.TableRow[];\n}\n\nasync function fetchLiveRows(\n profileName: string,\n fqn: string,\n columns: readonly string[] | undefined,\n rowLimit: number,\n side: 'source' | 'target',\n): Promise<dataCompare.TableRow[]> {\n const projection = columns && columns.length > 0 ? columns.join(', ') : '*';\n const limit = rowLimit > 0 ? ` LIMIT ${Math.floor(rowLimit)}` : '';\n const sql = `SELECT ${projection} FROM ${fqn}${limit}`;\n const profile = await getProfile(profileName);\n const conn = createConnection(profile);\n console.error(`data-compare (${side}): connecting to ${profile.auth.host}…`);\n await conn.connect();\n try {\n const rows = await conn.executeRows(sql);\n console.error(\n `data-compare (${side}): fetched ${rows.length} row(s) from ${fqn}${rowLimit > 0 && rows.length === rowLimit ? ' (LIMIT reached — increase --row-limit if more rows are needed)' : ''}.`,\n );\n return rows as dataCompare.TableRow[];\n } finally {\n await conn.disconnect().catch(() => undefined);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/data-fit.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { CompareEngine, PacSource, types } from "@ddt-tools/core";
|
|
8
|
+
function getColumns(obj) {
|
|
9
|
+
if (!("columns" in obj)) return void 0;
|
|
10
|
+
const cols = obj.columns;
|
|
11
|
+
if (!Array.isArray(cols)) return void 0;
|
|
12
|
+
return cols.map((c) => ({
|
|
13
|
+
name: c.name,
|
|
14
|
+
dataType: c.dataType ?? c.type
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function dataTypeToString(dt) {
|
|
18
|
+
if (typeof dt === "string") return dt;
|
|
19
|
+
if (dt && typeof dt === "object") {
|
|
20
|
+
const d = dt;
|
|
21
|
+
if (d.raw) return d.raw;
|
|
22
|
+
if (!d.base) return JSON.stringify(dt);
|
|
23
|
+
if (d.precision !== void 0 && d.scale !== void 0) {
|
|
24
|
+
return `${d.base}(${d.precision},${d.scale})`;
|
|
25
|
+
}
|
|
26
|
+
if (d.length !== void 0) return `${d.base}(${d.length})`;
|
|
27
|
+
return d.base;
|
|
28
|
+
}
|
|
29
|
+
return String(dt ?? "");
|
|
30
|
+
}
|
|
31
|
+
function backtickFqn(obj) {
|
|
32
|
+
const fqn = obj.fqn;
|
|
33
|
+
if (!fqn) return "<unknown>";
|
|
34
|
+
return [fqn.database, fqn.schema, fqn.name].filter(Boolean).map((p) => `\`${p}\``).join(".");
|
|
35
|
+
}
|
|
36
|
+
function backtickCol(name) {
|
|
37
|
+
return `\`${name}\``;
|
|
38
|
+
}
|
|
39
|
+
function dataFitCommand() {
|
|
40
|
+
const cmd = new Command("data-fit");
|
|
41
|
+
cmd.description(
|
|
42
|
+
"Emit pre-flight SELECT count_if() probes for every narrowing type change in a pac\u2194pac compare. Run them against the live target before --apply."
|
|
43
|
+
).requiredOption("--source <path>", ".ddtpac with the desired state.").requiredOption("--target <path>", ".ddtpac with the current state.").option("-o, --out <path>", "Output SQL file. Default: stdout.").option("--format <fmt>", "Output format: sql | json. Default: sql.", "sql").action(async (opts) => {
|
|
44
|
+
const source = new PacSource(path.resolve(String(opts.source)));
|
|
45
|
+
const target = new PacSource(path.resolve(String(opts.target)));
|
|
46
|
+
const engine = new CompareEngine();
|
|
47
|
+
const result = await engine.compare(source, target);
|
|
48
|
+
const probes = [];
|
|
49
|
+
for (const obj of result.objects) {
|
|
50
|
+
if (obj.kind !== "modified" || !obj.source || !obj.target) continue;
|
|
51
|
+
const srcCols = getColumns(obj.source);
|
|
52
|
+
const tgtCols = getColumns(obj.target);
|
|
53
|
+
if (!srcCols || !tgtCols) continue;
|
|
54
|
+
const byName = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const c of srcCols) byName.set(c.name.toUpperCase(), { source: c, target: c });
|
|
56
|
+
for (const c of tgtCols) {
|
|
57
|
+
const slot = byName.get(c.name.toUpperCase());
|
|
58
|
+
if (slot) slot.target = c;
|
|
59
|
+
}
|
|
60
|
+
for (const [, pair] of byName) {
|
|
61
|
+
if (pair.source === pair.target) continue;
|
|
62
|
+
const fromStr = dataTypeToString(pair.target.dataType);
|
|
63
|
+
const toStr = dataTypeToString(pair.source.dataType);
|
|
64
|
+
if (fromStr === toStr) continue;
|
|
65
|
+
const check = types.classifyTypeChange(fromStr, toStr);
|
|
66
|
+
if (check.verdict !== "data-fit-required") continue;
|
|
67
|
+
const fqn = backtickFqn(obj.source);
|
|
68
|
+
const colName = pair.source.name;
|
|
69
|
+
const probeSql = types.dataFitProbeSql(check, fqn, backtickCol(colName));
|
|
70
|
+
if (!probeSql) continue;
|
|
71
|
+
probes.push({
|
|
72
|
+
fqn,
|
|
73
|
+
column: colName,
|
|
74
|
+
from: fromStr,
|
|
75
|
+
to: toStr,
|
|
76
|
+
reason: check.reason,
|
|
77
|
+
sql: probeSql
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (opts.format === "json") {
|
|
82
|
+
const out = JSON.stringify({ probeCount: probes.length, probes }, null, 2);
|
|
83
|
+
await emit(out, opts.out);
|
|
84
|
+
} else {
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`-- Generated by \`ddt data-fit\` at ${(/* @__PURE__ */ new Date()).toISOString()}.`);
|
|
87
|
+
lines.push(`-- ${probes.length} narrowing type change(s) need a pre-flight check.`);
|
|
88
|
+
lines.push(`-- Run each query against the target; WOULD_FAIL must be 0 before --apply.`);
|
|
89
|
+
lines.push("");
|
|
90
|
+
for (const p of probes) {
|
|
91
|
+
lines.push(`-- PROBE: ${p.fqn}.${backtickCol(p.column)} (${p.from} \u2192 ${p.to})`);
|
|
92
|
+
lines.push(`-- Reason: ${p.reason}`);
|
|
93
|
+
lines.push(`${p.sql};`);
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
if (probes.length === 0) {
|
|
97
|
+
lines.push(
|
|
98
|
+
"-- No narrowing type changes detected. Compare result has only safe or destructive changes;"
|
|
99
|
+
);
|
|
100
|
+
lines.push("-- data-fit probes do not apply.");
|
|
101
|
+
}
|
|
102
|
+
await emit(lines.join("\n"), opts.out);
|
|
103
|
+
}
|
|
104
|
+
if (probes.length === 0) {
|
|
105
|
+
console.error("No narrowing type changes detected \u2014 nothing to probe.");
|
|
106
|
+
} else {
|
|
107
|
+
console.error(
|
|
108
|
+
`Generated ${probes.length} data-fit probe(s). Run each against the target and confirm WOULD_FAIL = 0 before \`ddt publish --apply\`.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return cmd;
|
|
113
|
+
}
|
|
114
|
+
async function emit(text, out) {
|
|
115
|
+
if (out) {
|
|
116
|
+
const p = path.resolve(String(out));
|
|
117
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
118
|
+
await fs.writeFile(p, text + (text.endsWith("\n") ? "" : "\n"), "utf8");
|
|
119
|
+
console.error(`Wrote ${p}.`);
|
|
120
|
+
} else {
|
|
121
|
+
process.stdout.write(text + (text.endsWith("\n") ? "" : "\n"));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
dataFitCommand
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=data-fit-WGEPLD5S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/data-fit.ts"],"sourcesContent":["/**\n * `ddt data-fit --source <pac> --target <pac>` — pre-flight data-fit\n * probe queries for narrowing type changes.\n *\n * Mirrors `Snowflake/packages/cli/src/commands/data-fit.ts`. Walks the\n * source-vs-target compare, finds every table-like object whose\n * column-level diff includes a type change, classifies each change\n * via `@ddt-tools/core/types.classifyTypeChange`, and for every\n * `data-fit-required` verdict emits a `SELECT count_if(...)` probe via\n * `dataFitProbeSql`. The probe queries are read-only — run them before\n * `ddt publish --apply` to confirm zero rows overflow the new type.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { CompareEngine, PacSource, types, type DatabricksObject } from '@ddt-tools/core';\n\ninterface ColumnWithType {\n name: string;\n dataType: unknown;\n}\n\nfunction getColumns(obj: DatabricksObject): ColumnWithType[] | undefined {\n if (!('columns' in obj)) return undefined;\n const cols = (obj as { columns?: unknown }).columns;\n if (!Array.isArray(cols)) return undefined;\n // DDT columns store the type under `.type`; normalize so the shared\n // ColumnWithType reader can find it under `.dataType` like the SDT\n // mirror. Defensive: keep `.dataType` if a producer happens to pass\n // SDT-shaped objects through this CLI.\n return (cols as { name: string; type?: unknown; dataType?: unknown }[]).map((c) => ({\n name: c.name,\n dataType: c.dataType ?? c.type,\n }));\n}\n\nfunction dataTypeToString(dt: unknown): string {\n if (typeof dt === 'string') return dt;\n if (dt && typeof dt === 'object') {\n const d = dt as {\n base?: string;\n precision?: number;\n scale?: number;\n length?: number;\n raw?: string;\n };\n if (d.raw) return d.raw;\n if (!d.base) return JSON.stringify(dt);\n if (d.precision !== undefined && d.scale !== undefined) {\n return `${d.base}(${d.precision},${d.scale})`;\n }\n if (d.length !== undefined) return `${d.base}(${d.length})`;\n return d.base;\n }\n return String(dt ?? '');\n}\n\nfunction backtickFqn(obj: DatabricksObject): string {\n const fqn = (obj as { fqn?: { database?: string; schema?: string; name: string } }).fqn;\n if (!fqn) return '<unknown>';\n return [fqn.database, fqn.schema, fqn.name]\n .filter(Boolean)\n .map((p) => `\\`${p}\\``)\n .join('.');\n}\n\nfunction backtickCol(name: string): string {\n return `\\`${name}\\``;\n}\n\nexport function dataFitCommand(): Command {\n const cmd = new Command('data-fit');\n cmd\n .description(\n 'Emit pre-flight SELECT count_if() probes for every narrowing type change in a pac↔pac compare. Run them against the live target before --apply.',\n )\n .requiredOption('--source <path>', '.ddtpac with the desired state.')\n .requiredOption('--target <path>', '.ddtpac with the current state.')\n .option('-o, --out <path>', 'Output SQL file. Default: stdout.')\n .option('--format <fmt>', 'Output format: sql | json. Default: sql.', 'sql')\n .action(async (opts) => {\n const source = new PacSource(path.resolve(String(opts.source)));\n const target = new PacSource(path.resolve(String(opts.target)));\n const engine = new CompareEngine();\n const result = await engine.compare(source, target);\n\n interface Probe {\n fqn: string;\n column: string;\n from: string;\n to: string;\n reason: string;\n sql: string;\n }\n const probes: Probe[] = [];\n\n for (const obj of result.objects) {\n if (obj.kind !== 'modified' || !obj.source || !obj.target) continue;\n const srcCols = getColumns(obj.source);\n const tgtCols = getColumns(obj.target);\n if (!srcCols || !tgtCols) continue;\n const byName = new Map<string, { source: ColumnWithType; target: ColumnWithType }>();\n for (const c of srcCols) byName.set(c.name.toUpperCase(), { source: c, target: c });\n for (const c of tgtCols) {\n const slot = byName.get(c.name.toUpperCase());\n if (slot) slot.target = c;\n }\n for (const [, pair] of byName) {\n if (pair.source === pair.target) continue;\n const fromStr = dataTypeToString(pair.target.dataType);\n const toStr = dataTypeToString(pair.source.dataType);\n if (fromStr === toStr) continue;\n const check = types.classifyTypeChange(fromStr, toStr);\n if (check.verdict !== 'data-fit-required') continue;\n const fqn = backtickFqn(obj.source);\n const colName = pair.source.name;\n const probeSql = types.dataFitProbeSql(check, fqn, backtickCol(colName));\n if (!probeSql) continue;\n probes.push({\n fqn,\n column: colName,\n from: fromStr,\n to: toStr,\n reason: check.reason,\n sql: probeSql,\n });\n }\n }\n\n if (opts.format === 'json') {\n const out = JSON.stringify({ probeCount: probes.length, probes }, null, 2);\n await emit(out, opts.out);\n } else {\n const lines: string[] = [];\n lines.push(`-- Generated by \\`ddt data-fit\\` at ${new Date().toISOString()}.`);\n lines.push(`-- ${probes.length} narrowing type change(s) need a pre-flight check.`);\n lines.push(`-- Run each query against the target; WOULD_FAIL must be 0 before --apply.`);\n lines.push('');\n for (const p of probes) {\n lines.push(`-- PROBE: ${p.fqn}.${backtickCol(p.column)} (${p.from} → ${p.to})`);\n lines.push(`-- Reason: ${p.reason}`);\n lines.push(`${p.sql};`);\n lines.push('');\n }\n if (probes.length === 0) {\n lines.push(\n '-- No narrowing type changes detected. Compare result has only safe or destructive changes;',\n );\n lines.push('-- data-fit probes do not apply.');\n }\n await emit(lines.join('\\n'), opts.out);\n }\n\n if (probes.length === 0) {\n console.error('No narrowing type changes detected — nothing to probe.');\n } else {\n console.error(\n `Generated ${probes.length} data-fit probe(s). Run each against the target and confirm WOULD_FAIL = 0 before \\`ddt publish --apply\\`.`,\n );\n }\n });\n return cmd;\n}\n\nasync function emit(text: string, out: unknown): Promise<void> {\n if (out) {\n const p = path.resolve(String(out));\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, text + (text.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${p}.`);\n } else {\n process.stdout.write(text + (text.endsWith('\\n') ? '' : '\\n'));\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/deploy-status.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { deployCheckpoint } from "@ddt-tools/core";
|
|
7
|
+
var DEFAULT_STATE_DIR = path.join(process.cwd(), deployCheckpoint.DEFAULT_STATE_DIR_REL);
|
|
8
|
+
function deployStatusCommand() {
|
|
9
|
+
const cmd = new Command("deploy-status");
|
|
10
|
+
cmd.description("Show or list resumable deploy checkpoints written by `ddt publish`.").argument("[deployId]", "Deploy id to inspect. Omit to list every checkpoint.").option("--state-dir <path>", "Directory containing checkpoint JSON files.", DEFAULT_STATE_DIR).option("--remove", "Delete the checkpoint (after status display). Requires <deployId>.", false).option("--format <fmt>", "text | json. Default text.", "text").action(
|
|
11
|
+
async (deployId, opts) => {
|
|
12
|
+
const stateDir = path.resolve(opts.stateDir);
|
|
13
|
+
const fmt = (opts.format ?? "text").toLowerCase();
|
|
14
|
+
if (!deployId) {
|
|
15
|
+
const list = await deployCheckpoint.listCheckpoints(stateDir);
|
|
16
|
+
if (fmt === "json") {
|
|
17
|
+
process.stdout.write(JSON.stringify(list, null, 2) + "\n");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (list.length === 0) {
|
|
21
|
+
process.stdout.write("No resumable deploys.\n");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const item of list) {
|
|
25
|
+
process.stdout.write(
|
|
26
|
+
`${item.deployId} ${item.state} ${item.completedSteps}/${item.totalSteps} steps profile=${item.profile}${item.env ? ` env=${item.env}` : ""} updated=${item.lastUpdatedAt}
|
|
27
|
+
`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const state = await deployCheckpoint.loadCheckpoint(stateDir, deployId);
|
|
33
|
+
if (!state) {
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
`No checkpoint found for deploy id ${JSON.stringify(deployId)} under ${stateDir}.
|
|
36
|
+
`
|
|
37
|
+
);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (fmt === "json") {
|
|
42
|
+
process.stdout.write(JSON.stringify(state, null, 2) + "\n");
|
|
43
|
+
} else {
|
|
44
|
+
process.stdout.write(deployCheckpoint.formatCheckpointStatus(state) + "\n");
|
|
45
|
+
}
|
|
46
|
+
if (opts.remove) {
|
|
47
|
+
await deployCheckpoint.removeCheckpoint(stateDir, deployId);
|
|
48
|
+
process.stderr.write(`Removed checkpoint ${deployId}.
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
return cmd;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
deployStatusCommand
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=deploy-status-4H5KJFRC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/deploy-status.ts"],"sourcesContent":["/**\n * `ddt deploy-status` — inspect resumable deploy checkpoints (DSR.3).\n *\n * Usage:\n * ddt deploy-status # list every resumable deploy\n * ddt deploy-status <deployId> # show one checkpoint\n * ddt deploy-status <deployId> --remove # delete a checkpoint\n * ddt deploy-status --format json # machine-readable list\n *\n * Mirrors `sdt deploy-status`.\n */\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { deployCheckpoint } from '@ddt-tools/core';\n\nconst DEFAULT_STATE_DIR = path.join(process.cwd(), deployCheckpoint.DEFAULT_STATE_DIR_REL);\n\nexport function deployStatusCommand(): Command {\n const cmd = new Command('deploy-status');\n cmd\n .description('Show or list resumable deploy checkpoints written by `ddt publish`.')\n .argument('[deployId]', 'Deploy id to inspect. Omit to list every checkpoint.')\n .option('--state-dir <path>', 'Directory containing checkpoint JSON files.', DEFAULT_STATE_DIR)\n .option('--remove', 'Delete the checkpoint (after status display). Requires <deployId>.', false)\n .option('--format <fmt>', 'text | json. Default text.', 'text')\n .action(\n async (\n deployId: string | undefined,\n opts: { stateDir: string; remove?: boolean; format?: string },\n ) => {\n const stateDir = path.resolve(opts.stateDir);\n const fmt = (opts.format ?? 'text').toLowerCase();\n if (!deployId) {\n const list = await deployCheckpoint.listCheckpoints(stateDir);\n if (fmt === 'json') {\n process.stdout.write(JSON.stringify(list, null, 2) + '\\n');\n return;\n }\n if (list.length === 0) {\n process.stdout.write('No resumable deploys.\\n');\n return;\n }\n for (const item of list) {\n process.stdout.write(\n `${item.deployId} ${item.state} ${item.completedSteps}/${item.totalSteps} steps profile=${item.profile}${item.env ? ` env=${item.env}` : ''} updated=${item.lastUpdatedAt}\\n`,\n );\n }\n return;\n }\n const state = await deployCheckpoint.loadCheckpoint(stateDir, deployId);\n if (!state) {\n process.stderr.write(\n `No checkpoint found for deploy id ${JSON.stringify(deployId)} under ${stateDir}.\\n`,\n );\n process.exitCode = 1;\n return;\n }\n if (fmt === 'json') {\n process.stdout.write(JSON.stringify(state, null, 2) + '\\n');\n } else {\n process.stdout.write(deployCheckpoint.formatCheckpointStatus(state) + '\\n');\n }\n if (opts.remove) {\n await deployCheckpoint.removeCheckpoint(stateDir, deployId);\n process.stderr.write(`Removed checkpoint ${deployId}.\\n`);\n }\n },\n );\n return cmd;\n}\n"],"mappings":";;;AAWA,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAEjC,IAAM,oBAAoB,KAAK,KAAK,QAAQ,IAAI,GAAG,iBAAiB,qBAAqB;AAElF,SAAS,sBAA+B;AAC7C,QAAM,MAAM,IAAI,QAAQ,eAAe;AACvC,MACG,YAAY,qEAAqE,EACjF,SAAS,cAAc,sDAAsD,EAC7E,OAAO,sBAAsB,+CAA+C,iBAAiB,EAC7F,OAAO,YAAY,sEAAsE,KAAK,EAC9F,OAAO,kBAAkB,8BAA8B,MAAM,EAC7D;AAAA,IACC,OACE,UACA,SACG;AACH,YAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ;AAC3C,YAAM,OAAO,KAAK,UAAU,QAAQ,YAAY;AAChD,UAAI,CAAC,UAAU;AACb,cAAM,OAAO,MAAM,iBAAiB,gBAAgB,QAAQ;AAC5D,YAAI,QAAQ,QAAQ;AAClB,kBAAQ,OAAO,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,IAAI;AACzD;AAAA,QACF;AACA,YAAI,KAAK,WAAW,GAAG;AACrB,kBAAQ,OAAO,MAAM,yBAAyB;AAC9C;AAAA,QACF;AACA,mBAAW,QAAQ,MAAM;AACvB,kBAAQ,OAAO;AAAA,YACb,GAAG,KAAK,QAAQ,KAAK,KAAK,KAAK,KAAK,KAAK,cAAc,IAAI,KAAK,UAAU,mBAAmB,KAAK,OAAO,GAAG,KAAK,MAAM,QAAQ,KAAK,GAAG,KAAK,EAAE,aAAa,KAAK,aAAa;AAAA;AAAA,UAC/K;AAAA,QACF;AACA;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,iBAAiB,eAAe,UAAU,QAAQ;AACtE,UAAI,CAAC,OAAO;AACV,gBAAQ,OAAO;AAAA,UACb,qCAAqC,KAAK,UAAU,QAAQ,CAAC,UAAU,QAAQ;AAAA;AAAA,QACjF;AACA,gBAAQ,WAAW;AACnB;AAAA,MACF;AACA,UAAI,QAAQ,QAAQ;AAClB,gBAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,IAAI;AAAA,MAC5D,OAAO;AACL,gBAAQ,OAAO,MAAM,iBAAiB,uBAAuB,KAAK,IAAI,IAAI;AAAA,MAC5E;AACA,UAAI,KAAK,QAAQ;AACf,cAAM,iBAAiB,iBAAiB,UAAU,QAAQ;AAC1D,gBAAQ,OAAO,MAAM,sBAAsB,QAAQ;AAAA,CAAK;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AACF,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/design.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { ai, designSnowflakeSchema, designStar } from "@ddt-tools/core";
|
|
6
|
+
var KINDS = ["star"];
|
|
7
|
+
function designCommand() {
|
|
8
|
+
const cmd = new Command("design");
|
|
9
|
+
cmd.description(
|
|
10
|
+
"AI-assist: generative schema-design scaffolds. Output always carries a REVIEW BEFORE RUNNING header."
|
|
11
|
+
);
|
|
12
|
+
cmd.command("star").description(
|
|
13
|
+
"Propose a star-schema (fact + dimensions) for the described domain. Returns a `ddt template star` invocation you can review and run."
|
|
14
|
+
).requiredOption(
|
|
15
|
+
"--description <text>",
|
|
16
|
+
'Free-form domain description. Use "-" to read from stdin.'
|
|
17
|
+
).option("--target <fqn>", "Optional target FQN (e.g. main.bronze).").option("--context <text>", "Optional extra constraints.").option("--format <fmt>", "Output format: text | json. Default text.", "text").option(
|
|
18
|
+
"--ai-max-spend <usd>",
|
|
19
|
+
"Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
|
|
20
|
+
"0"
|
|
21
|
+
).action(async (opts) => {
|
|
22
|
+
await runDesign("star", opts);
|
|
23
|
+
});
|
|
24
|
+
cmd.command("snowflake-schema").description(
|
|
25
|
+
'Refactor a star into a snowflake schema. AI proposes which dims to normalize into parent/child pairs based on a prose refinement (e.g. "normalize geography down to country"). Returns a reviewable script with `ddt template scd1` calls + manual bridge-wiring steps.'
|
|
26
|
+
).requiredOption("--fact <name>", "Fact-table name (lowercase identifier).").requiredOption("--dims <list>", "Comma-separated list of existing dimensions on the star.").requiredOption(
|
|
27
|
+
"--refinement <text>",
|
|
28
|
+
'Free-form description of the desired snowflake refactor. Use "-" to read from stdin.'
|
|
29
|
+
).option("--target <fqn>", "Optional target FQN (e.g. main.bronze).").option("--context <text>", "Optional extra constraints.").option("--format <fmt>", "Output format: text | json. Default text.", "text").option(
|
|
30
|
+
"--ai-max-spend <usd>",
|
|
31
|
+
"Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
|
|
32
|
+
"0"
|
|
33
|
+
).action(async (opts) => {
|
|
34
|
+
await runDesignSnowflakeSchema(opts);
|
|
35
|
+
});
|
|
36
|
+
return cmd;
|
|
37
|
+
}
|
|
38
|
+
async function runDesignSnowflakeSchema(opts) {
|
|
39
|
+
const refinement = String(opts.refinement) === "-" ? await readStdin() : String(opts.refinement);
|
|
40
|
+
const targetFqn = opts.target ? splitFqn(String(opts.target)) : void 0;
|
|
41
|
+
const dimensions = String(opts.dims).split(",").map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
if (dimensions.length === 0) {
|
|
43
|
+
throw new Error("--dims must include at least one dimension.");
|
|
44
|
+
}
|
|
45
|
+
const result = await designSnowflakeSchema.designSnowflakeSchema(
|
|
46
|
+
{
|
|
47
|
+
factName: String(opts.fact),
|
|
48
|
+
dimensions,
|
|
49
|
+
refinement,
|
|
50
|
+
...targetFqn ? { targetFqn } : {},
|
|
51
|
+
...opts.context ? { additionalContext: String(opts.context) } : {}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
completeFn: async (prompt) => {
|
|
55
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
56
|
+
feature: "design-snowflake-schema",
|
|
57
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
58
|
+
});
|
|
59
|
+
return r.text;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"ddt"
|
|
63
|
+
);
|
|
64
|
+
if (String(opts.format).toLowerCase() === "json") {
|
|
65
|
+
const { rawModelText: _omit, ...keep } = result;
|
|
66
|
+
console.log(JSON.stringify(keep, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(result.proposedInvocation);
|
|
70
|
+
if (result.assumptions.length > 0) {
|
|
71
|
+
console.error("");
|
|
72
|
+
console.error("Model assumptions:");
|
|
73
|
+
for (const a of result.assumptions) console.error(` - ${a}`);
|
|
74
|
+
}
|
|
75
|
+
if (result.parseFailed) {
|
|
76
|
+
console.warn("Model output could not be parsed \u2014 see the raw text via --format json.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function runDesign(kind, opts) {
|
|
80
|
+
if (!KINDS.includes(kind)) {
|
|
81
|
+
throw new Error(`Unknown kind "${kind}". Use one of: ${KINDS.join(" | ")}`);
|
|
82
|
+
}
|
|
83
|
+
const description = String(opts.description) === "-" ? await readStdin() : String(opts.description);
|
|
84
|
+
const targetFqn = opts.target ? splitFqn(String(opts.target)) : void 0;
|
|
85
|
+
const result = await designStar.designStar(
|
|
86
|
+
{
|
|
87
|
+
description,
|
|
88
|
+
...targetFqn ? { targetFqn } : {},
|
|
89
|
+
...opts.context ? { additionalContext: String(opts.context) } : {}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
completeFn: async (prompt) => {
|
|
93
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
94
|
+
feature: "design-star",
|
|
95
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
96
|
+
});
|
|
97
|
+
return r.text;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"ddt"
|
|
101
|
+
);
|
|
102
|
+
if (String(opts.format).toLowerCase() === "json") {
|
|
103
|
+
const { rawModelText: _omit, ...keep } = result;
|
|
104
|
+
console.log(JSON.stringify(keep, null, 2));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log(result.proposedInvocation);
|
|
108
|
+
if (result.assumptions.length > 0) {
|
|
109
|
+
console.error("");
|
|
110
|
+
console.error("Model assumptions:");
|
|
111
|
+
for (const a of result.assumptions) console.error(` - ${a}`);
|
|
112
|
+
}
|
|
113
|
+
if (result.parseFailed) {
|
|
114
|
+
console.warn("Model output could not be parsed \u2014 see the raw text via --format json.");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function splitFqn(fqn) {
|
|
118
|
+
const parts = fqn.split(".");
|
|
119
|
+
if (parts.length === 1) return { database: parts[0] };
|
|
120
|
+
if (parts.length === 2) return { database: parts[0], schema: parts[1] };
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Invalid --target "${fqn}": expected 1 or 2 dot-separated parts (catalog[.schema]).`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
async function readStdin() {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
for await (const chunk of process.stdin) {
|
|
128
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
129
|
+
}
|
|
130
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
designCommand
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=design-ILX3ZSWW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/design.ts"],"sourcesContent":["/**\n * `ddt design <kind>` — AI-assisted schema-design surface.\n *\n * v1 supports `star` — generative star-schema designer that returns a\n * `template star` invocation tuned to a prose domain description.\n * Composes `@ddt-tools/core/designStar.designStar` with `ai.complete` so the\n * CLI is a thin adapter; the prompt-building + parser live in core for\n * shared testability with the MCP/LSP/VS Code hosts.\n *\n * Mirrors `sdt design`.\n */\nimport { Command } from 'commander';\nimport { ai, designSnowflakeSchema, designStar } from '@ddt-tools/core';\n\nconst KINDS = ['star'] as const;\ntype Kind = (typeof KINDS)[number];\n\nexport function designCommand(): Command {\n const cmd = new Command('design');\n cmd.description(\n 'AI-assist: generative schema-design scaffolds. Output always carries a REVIEW BEFORE RUNNING header.',\n );\n\n cmd\n .command('star')\n .description(\n 'Propose a star-schema (fact + dimensions) for the described domain. ' +\n 'Returns a `ddt template star` invocation you can review and run.',\n )\n .requiredOption(\n '--description <text>',\n 'Free-form domain description. Use \"-\" to read from stdin.',\n )\n .option('--target <fqn>', 'Optional target FQN (e.g. main.bronze).')\n .option('--context <text>', 'Optional extra constraints.')\n .option('--format <fmt>', 'Output format: text | json. Default text.', 'text')\n .option(\n '--ai-max-spend <usd>',\n \"Refuse the call if today's estimated spend ≥ this (USD). 0 = no cap.\",\n '0',\n )\n .action(async (opts) => {\n await runDesign('star', opts);\n });\n\n cmd\n .command('snowflake-schema')\n .description(\n 'Refactor a star into a snowflake schema. AI proposes which dims to normalize ' +\n 'into parent/child pairs based on a prose refinement (e.g. \"normalize geography down to country\"). ' +\n 'Returns a reviewable script with `ddt template scd1` calls + manual bridge-wiring steps.',\n )\n .requiredOption('--fact <name>', 'Fact-table name (lowercase identifier).')\n .requiredOption('--dims <list>', 'Comma-separated list of existing dimensions on the star.')\n .requiredOption(\n '--refinement <text>',\n 'Free-form description of the desired snowflake refactor. Use \"-\" to read from stdin.',\n )\n .option('--target <fqn>', 'Optional target FQN (e.g. main.bronze).')\n .option('--context <text>', 'Optional extra constraints.')\n .option('--format <fmt>', 'Output format: text | json. Default text.', 'text')\n .option(\n '--ai-max-spend <usd>',\n \"Refuse the call if today's estimated spend ≥ this (USD). 0 = no cap.\",\n '0',\n )\n .action(async (opts) => {\n await runDesignSnowflakeSchema(opts);\n });\n\n return cmd;\n}\n\nasync function runDesignSnowflakeSchema(opts: Record<string, unknown>): Promise<void> {\n const refinement = String(opts.refinement) === '-' ? await readStdin() : String(opts.refinement);\n const targetFqn = opts.target ? splitFqn(String(opts.target)) : undefined;\n const dimensions = String(opts.dims)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n if (dimensions.length === 0) {\n throw new Error('--dims must include at least one dimension.');\n }\n\n const result = await designSnowflakeSchema.designSnowflakeSchema(\n {\n factName: String(opts.fact),\n dimensions,\n refinement,\n ...(targetFqn ? { targetFqn } : {}),\n ...(opts.context ? { additionalContext: String(opts.context) } : {}),\n },\n {\n completeFn: async (prompt) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'design-snowflake-schema',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n });\n return r.text;\n },\n },\n 'ddt',\n );\n\n if (String(opts.format).toLowerCase() === 'json') {\n const { rawModelText: _omit, ...keep } = result;\n console.log(JSON.stringify(keep, null, 2));\n return;\n }\n\n console.log(result.proposedInvocation);\n if (result.assumptions.length > 0) {\n console.error('');\n console.error('Model assumptions:');\n for (const a of result.assumptions) console.error(` - ${a}`);\n }\n if (result.parseFailed) {\n console.warn('Model output could not be parsed — see the raw text via --format json.');\n }\n}\n\nasync function runDesign(kind: Kind, opts: Record<string, unknown>): Promise<void> {\n if (!KINDS.includes(kind)) {\n throw new Error(`Unknown kind \"${kind}\". Use one of: ${KINDS.join(' | ')}`);\n }\n const description =\n String(opts.description) === '-' ? await readStdin() : String(opts.description);\n const targetFqn = opts.target ? splitFqn(String(opts.target)) : undefined;\n\n const result = await designStar.designStar(\n {\n description,\n ...(targetFqn ? { targetFqn } : {}),\n ...(opts.context ? { additionalContext: String(opts.context) } : {}),\n },\n {\n completeFn: async (prompt) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'design-star',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n });\n return r.text;\n },\n },\n 'ddt',\n );\n\n if (String(opts.format).toLowerCase() === 'json') {\n const { rawModelText: _omit, ...keep } = result;\n console.log(JSON.stringify(keep, null, 2));\n return;\n }\n\n console.log(result.proposedInvocation);\n if (result.assumptions.length > 0) {\n console.error('');\n console.error('Model assumptions:');\n for (const a of result.assumptions) console.error(` - ${a}`);\n }\n if (result.parseFailed) {\n console.warn('Model output could not be parsed — see the raw text via --format json.');\n }\n}\n\nfunction splitFqn(fqn: string): { database?: string; schema?: string } {\n const parts = fqn.split('.');\n if (parts.length === 1) return { database: parts[0] };\n if (parts.length === 2) return { database: parts[0]!, schema: parts[1] };\n throw new Error(\n `Invalid --target \"${fqn}\": expected 1 or 2 dot-separated parts (catalog[.schema]).`,\n );\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"],"mappings":"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","names":[]}
|