@ddt-tools/cli 0.2.0 → 0.2.5
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 +504 -19402
- package/dist/cli.js.map +1 -1
- package/dist/compare-IOEATL6G.js +435 -0
- package/dist/compare-IOEATL6G.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-LX6WT4JH.js +109 -0
- package/dist/errorReporting-LX6WT4JH.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-EGOVKTLX.js +29 -0
- package/dist/import-EGOVKTLX.js.map +1 -0
- package/dist/import-script-R5RXPDH6.js +79 -0
- package/dist/import-script-R5RXPDH6.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-6ZXOAF7S.js +343 -0
- package/dist/mcp-6ZXOAF7S.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-HLP3XHM5.js +766 -0
- package/dist/publish-HLP3XHM5.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,396 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/template.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
function templateCommand() {
|
|
8
|
+
const cmd = new Command("template");
|
|
9
|
+
cmd.description(
|
|
10
|
+
"Scaffold a common schema pattern (scd1|scd2|scd3|scd4|scd6|star|fact|scd2-merge|current-view|time-series|audit). Writes .sql files into <out>/."
|
|
11
|
+
).argument(
|
|
12
|
+
"<kind>",
|
|
13
|
+
"Pattern to scaffold: scd1 | scd2 | scd3 | scd4 | scd6 | star | fact | scd2-merge | current-view | time-series | audit"
|
|
14
|
+
).argument("<name>", "Base object name (e.g. CUSTOMER, ORDERS).").option("--catalog <catalog>", "UC catalog name. Default main.", "main").option("--schema <schema>", "Schema name. Default gold.", "gold").option("-o, --out <path>", "Output directory. Default <kind>-<name>/.").option(
|
|
15
|
+
"--dims <dims>",
|
|
16
|
+
"[star|fact] Comma-separated dimension names. Default: customer,product,date.",
|
|
17
|
+
"customer,product,date"
|
|
18
|
+
).option(
|
|
19
|
+
"--track <cols>",
|
|
20
|
+
"[scd3|scd6] Comma-separated columns to track previous-values for. Default: attribute_1.",
|
|
21
|
+
"attribute_1"
|
|
22
|
+
).option(
|
|
23
|
+
"--source <fqn>",
|
|
24
|
+
"[scd2-merge] Source/staging table fully-qualified name. Default main.gold.<name>_stage."
|
|
25
|
+
).option("--retention-days <n>", "[time-series] Retention window in days. Default 90.", "90").action(async (kindArg, nameArg, opts) => {
|
|
26
|
+
const kind = String(kindArg).toLowerCase();
|
|
27
|
+
const name = String(nameArg);
|
|
28
|
+
const catalog = String(opts.catalog);
|
|
29
|
+
const schema = String(opts.schema);
|
|
30
|
+
const out = opts.out ? path.resolve(String(opts.out)) : path.resolve(`${kind}-${name.toLowerCase()}`);
|
|
31
|
+
await fs.mkdir(out, { recursive: true });
|
|
32
|
+
const ctx = { catalog, schema, name };
|
|
33
|
+
let files;
|
|
34
|
+
const trackCols = String(opts.track).split(",").map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
const dims = String(opts.dims).split(",").map((s) => s.trim()).filter(Boolean);
|
|
36
|
+
if (kind === "scd1") files = renderScd1(ctx);
|
|
37
|
+
else if (kind === "scd2") files = renderScd2(ctx);
|
|
38
|
+
else if (kind === "scd3") files = renderScd3(ctx, trackCols);
|
|
39
|
+
else if (kind === "scd4") files = renderScd4(ctx);
|
|
40
|
+
else if (kind === "scd6") files = renderScd6(ctx, trackCols);
|
|
41
|
+
else if (kind === "star") files = renderStar(ctx, dims);
|
|
42
|
+
else if (kind === "fact") files = renderFact(ctx, dims);
|
|
43
|
+
else if (kind === "scd2-merge")
|
|
44
|
+
files = renderScd2Merge(
|
|
45
|
+
ctx,
|
|
46
|
+
opts.source ? String(opts.source) : `${catalog}.${schema}.${name.toLowerCase()}_stage`
|
|
47
|
+
);
|
|
48
|
+
else if (kind === "current-view") files = renderCurrentView(ctx);
|
|
49
|
+
else if (kind === "time-series") files = renderTimeSeries(ctx, Number(opts.retentionDays));
|
|
50
|
+
else if (kind === "audit") files = renderAudit(ctx);
|
|
51
|
+
else
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Unknown template kind: ${kind}. Use scd1 | scd2 | scd3 | scd4 | scd6 | star | fact | scd2-merge | current-view | time-series | audit.`
|
|
54
|
+
);
|
|
55
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
56
|
+
const filePath = path.join(out, rel);
|
|
57
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
58
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
59
|
+
console.error(
|
|
60
|
+
` wrote ${path.relative(process.cwd(), filePath)} (${contents.length} bytes)`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
console.error(
|
|
64
|
+
`Done. ${Object.keys(files).length} file(s) written to ${path.relative(process.cwd(), out)}.`
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
return cmd;
|
|
68
|
+
}
|
|
69
|
+
function header(why) {
|
|
70
|
+
return `-- Generated by \`ddt template\`. Why this pattern is the right shape:
|
|
71
|
+
-- ${why}
|
|
72
|
+
--
|
|
73
|
+
-- Edit freely; this is just a starting point.
|
|
74
|
+
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
function renderScd1(c) {
|
|
78
|
+
const lower = c.name.toLowerCase();
|
|
79
|
+
return {
|
|
80
|
+
[`${c.catalog}/${c.schema}/tables/${lower}.sql`]: header(
|
|
81
|
+
`SCD1 overwrites the row in place when an attribute changes. No history is
|
|
82
|
+
-- preserved. Use this when downstream only cares about the *current* state.
|
|
83
|
+
-- load_ts/updated_ts give you minimal auditability without history-table cost.`
|
|
84
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (
|
|
85
|
+
${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key \u2014 stable across overwrites',
|
|
86
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key',
|
|
87
|
+
attribute_1 STRING COMMENT 'Replace with real columns',
|
|
88
|
+
attribute_2 STRING COMMENT 'Replace with real columns',
|
|
89
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the row was first inserted',
|
|
90
|
+
updated_ts TIMESTAMP COMMENT 'When the row was last overwritten'
|
|
91
|
+
)
|
|
92
|
+
USING DELTA
|
|
93
|
+
CLUSTER BY (${lower}_bk)
|
|
94
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
95
|
+
COMMENT 'SCD1 dimension \u2014 overwrite-on-change. No history kept; join on ${lower}_bk.';
|
|
96
|
+
`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function renderScd2(c) {
|
|
100
|
+
const lower = c.name.toLowerCase();
|
|
101
|
+
return {
|
|
102
|
+
[`${c.catalog}/${c.schema}/tables/${lower}.sql`]: header(
|
|
103
|
+
`SCD2 preserves every historical version of a row by closing the previous
|
|
104
|
+
-- row's VALID_TO when the natural key reappears with new attribute values.
|
|
105
|
+
-- Use this when downstream analytics needs "what did this row look like on date X".`
|
|
106
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (
|
|
107
|
+
${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',
|
|
108
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key',
|
|
109
|
+
attribute_1 STRING COMMENT 'Replace with real columns',
|
|
110
|
+
attribute_2 STRING COMMENT 'Replace with real columns',
|
|
111
|
+
valid_from TIMESTAMP NOT NULL COMMENT 'When this version became current',
|
|
112
|
+
valid_to TIMESTAMP COMMENT 'When this version was superseded; NULL while current',
|
|
113
|
+
is_current BOOLEAN NOT NULL COMMENT 'TRUE only on the latest row per BK',
|
|
114
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'
|
|
115
|
+
)
|
|
116
|
+
USING DELTA
|
|
117
|
+
CLUSTER BY (${lower}_bk, valid_from)
|
|
118
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
119
|
+
COMMENT 'SCD2 dimension \u2014 preserves history. Join on (${lower}_bk, is_current=TRUE) for current state.';
|
|
120
|
+
`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function renderScd3(c, tracked) {
|
|
124
|
+
const lower = c.name.toLowerCase();
|
|
125
|
+
const cols = (tracked.length ? tracked : ["attribute_1"]).map((t) => t.toLowerCase());
|
|
126
|
+
const prevCols = cols.map(
|
|
127
|
+
(t) => ` prev_${t} STRING COMMENT 'Prior value of ${t}'`
|
|
128
|
+
).join(",\n");
|
|
129
|
+
return {
|
|
130
|
+
[`${c.catalog}/${c.schema}/tables/${lower}.sql`]: header(
|
|
131
|
+
`SCD3 keeps the current value plus one prior value per tracked column. Use
|
|
132
|
+
-- this when downstream needs "current vs immediately prior" but doesn't need
|
|
133
|
+
-- the full history. Cheaper than SCD2 but lossy beyond one change.`
|
|
134
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (
|
|
135
|
+
${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',
|
|
136
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key',
|
|
137
|
+
${cols.map((t) => ` ${t} STRING COMMENT 'Current value'`).join(",\n")},
|
|
138
|
+
${prevCols},
|
|
139
|
+
prev_changed_ts TIMESTAMP COMMENT 'When prior values were captured',
|
|
140
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp',
|
|
141
|
+
updated_ts TIMESTAMP COMMENT 'When this row was last overwritten'
|
|
142
|
+
)
|
|
143
|
+
USING DELTA
|
|
144
|
+
CLUSTER BY (${lower}_bk)
|
|
145
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
146
|
+
COMMENT 'SCD3 dimension \u2014 current + one prior value for ${cols.join(", ")}.';
|
|
147
|
+
`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function renderScd4(c) {
|
|
151
|
+
const lower = c.name.toLowerCase();
|
|
152
|
+
return {
|
|
153
|
+
[`${c.catalog}/${c.schema}/tables/${lower}.sql`]: header(
|
|
154
|
+
`SCD4 keeps a small, fast "current" table for joins and a separate history
|
|
155
|
+
-- table for slow analytics. Use this when current-state lookups are hot
|
|
156
|
+
-- (every fact query joins to it) and you want the join table skinny.`
|
|
157
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (
|
|
158
|
+
${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',
|
|
159
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key',
|
|
160
|
+
attribute_1 STRING COMMENT 'Replace with real columns',
|
|
161
|
+
attribute_2 STRING COMMENT 'Replace with real columns',
|
|
162
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the row was inserted',
|
|
163
|
+
updated_ts TIMESTAMP COMMENT 'When the row was last overwritten'
|
|
164
|
+
)
|
|
165
|
+
USING DELTA
|
|
166
|
+
CLUSTER BY (${lower}_bk)
|
|
167
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
168
|
+
COMMENT 'SCD4 current-state table. Pair with ${lower}_history for change tracking.';
|
|
169
|
+
`,
|
|
170
|
+
[`${c.catalog}/${c.schema}/tables/${lower}_history.sql`]: header(
|
|
171
|
+
`Companion history table for SCD4. Rows are appended (never updated) when
|
|
172
|
+
-- the current table is overwritten \u2014 same columns plus a changed_ts so you
|
|
173
|
+
-- can replay "what did this BK look like at time T".`
|
|
174
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower}_history (
|
|
175
|
+
${lower}_history_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate id for each historical snapshot',
|
|
176
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key',
|
|
177
|
+
attribute_1 STRING COMMENT 'Snapshot of column at changed_ts',
|
|
178
|
+
attribute_2 STRING COMMENT 'Snapshot of column at changed_ts',
|
|
179
|
+
changed_ts TIMESTAMP NOT NULL COMMENT 'When this snapshot was taken',
|
|
180
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the row was appended'
|
|
181
|
+
)
|
|
182
|
+
USING DELTA
|
|
183
|
+
CLUSTER BY (${lower}_bk, changed_ts)
|
|
184
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true', 'delta.enableChangeDataFeed' = 'true')
|
|
185
|
+
COMMENT 'SCD4 history-side. Append-only snapshots of ${lower}; reconstruct as-of state via window functions.';
|
|
186
|
+
`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function renderScd6(c, tracked) {
|
|
190
|
+
const lower = c.name.toLowerCase();
|
|
191
|
+
const cols = (tracked.length ? tracked : ["attribute_1"]).map((t) => t.toLowerCase());
|
|
192
|
+
const prevCols = cols.map(
|
|
193
|
+
(t) => ` prev_${t} STRING COMMENT 'Prior value of ${t}'`
|
|
194
|
+
).join(",\n");
|
|
195
|
+
return {
|
|
196
|
+
[`${c.catalog}/${c.schema}/tables/${lower}.sql`]: header(
|
|
197
|
+
`SCD6 = 1 + 2 + 3 hybrid. Full SCD2 history (rows per version), SCD3
|
|
198
|
+
-- "current vs prior" lookback on hot columns, and SCD1 "current value"
|
|
199
|
+
-- denormalized onto every historical row. Heavyweight; use when downstream
|
|
200
|
+
-- consumers ask both "what was it at time T?" and "what's the current value
|
|
201
|
+
-- for the BK on this old row?" in the same query.`
|
|
202
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (
|
|
203
|
+
${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key \u2014 unique per version',
|
|
204
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key \u2014 stable across versions',
|
|
205
|
+
${cols.map((t) => ` ${t} STRING COMMENT 'Value on THIS version'`).join(",\n")},
|
|
206
|
+
${cols.map((t) => ` current_${t} STRING COMMENT 'Current value for the BK (SCD1 denorm)'`).join(",\n")},
|
|
207
|
+
${prevCols},
|
|
208
|
+
valid_from TIMESTAMP NOT NULL COMMENT 'When this version became current',
|
|
209
|
+
valid_to TIMESTAMP COMMENT 'When this version was superseded; NULL while current',
|
|
210
|
+
is_current BOOLEAN NOT NULL COMMENT 'TRUE only on the latest row per BK',
|
|
211
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'
|
|
212
|
+
)
|
|
213
|
+
USING DELTA
|
|
214
|
+
CLUSTER BY (${lower}_bk, valid_from)
|
|
215
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
216
|
+
COMMENT 'SCD6 dimension \u2014 hybrid 1+2+3. Heavy but answers every common BI question without re-joining.';
|
|
217
|
+
`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function renderStar(c, dims) {
|
|
221
|
+
const fact = c.name.toLowerCase();
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const d of dims) {
|
|
224
|
+
const lower = d.toLowerCase();
|
|
225
|
+
out[`${c.catalog}/${c.schema}/tables/dim_${lower}.sql`] = header(`Dimension table \u2014 describes the WHAT. Joined by the fact via dim_${lower}_sk.`) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.dim_${lower} (
|
|
226
|
+
dim_${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',
|
|
227
|
+
${lower}_bk STRING NOT NULL COMMENT 'Business key',
|
|
228
|
+
${lower}_name STRING COMMENT 'Replace with real attributes',
|
|
229
|
+
country STRING COMMENT 'Example attribute'
|
|
230
|
+
)
|
|
231
|
+
USING DELTA
|
|
232
|
+
CLUSTER BY (${lower}_bk)
|
|
233
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
234
|
+
COMMENT 'Dimension table for the ${fact} star schema.';
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
out[`${c.catalog}/${c.schema}/tables/fact_${fact}.sql`] = header(
|
|
238
|
+
`Fact table \u2014 the WHO/WHAT/WHEN/HOW-MUCH. One row per business event.
|
|
239
|
+
-- Surrogate-key FKs to every dimension; measure columns hold the numbers.
|
|
240
|
+
-- CLUSTER BY (event_ts) keeps recent-event scans cheap (Delta data skipping).`
|
|
241
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.fact_${fact} (
|
|
242
|
+
fact_${fact}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',
|
|
243
|
+
${dims.map((d) => ` dim_${d.toLowerCase()}_sk BIGINT COMMENT 'FK to dim_${d.toLowerCase()}'`).join(",\n")},
|
|
244
|
+
event_ts TIMESTAMP NOT NULL COMMENT 'When the event occurred',
|
|
245
|
+
measure_amount DECIMAL(38,4) COMMENT 'Primary measure',
|
|
246
|
+
measure_quantity BIGINT COMMENT 'Secondary measure',
|
|
247
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'
|
|
248
|
+
)
|
|
249
|
+
USING DELTA
|
|
250
|
+
CLUSTER BY (event_ts)
|
|
251
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true', 'delta.enableChangeDataFeed' = 'true')
|
|
252
|
+
COMMENT 'Fact table for the ${fact} star. CLUSTER BY event_ts + Photon = cheap recent-window scans.';
|
|
253
|
+
`;
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function renderFact(c, dims) {
|
|
257
|
+
const fact = c.name.toLowerCase();
|
|
258
|
+
return {
|
|
259
|
+
[`${c.catalog}/${c.schema}/tables/fact_${fact}.sql`]: header(
|
|
260
|
+
`Fact-only template \u2014 assumes the named dimensions already exist. Use this
|
|
261
|
+
-- when adding a new fact to an existing star, rather than scaffolding the
|
|
262
|
+
-- dims a second time. FKs follow the dim_<name>_sk convention.`
|
|
263
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.fact_${fact} (
|
|
264
|
+
fact_${fact}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',
|
|
265
|
+
${dims.map((d) => ` dim_${d.toLowerCase()}_sk BIGINT COMMENT 'FK to dim_${d.toLowerCase()}'`).join(",\n")},
|
|
266
|
+
event_ts TIMESTAMP NOT NULL COMMENT 'When the event occurred',
|
|
267
|
+
measure_amount DECIMAL(38,4) COMMENT 'Primary measure',
|
|
268
|
+
measure_quantity BIGINT COMMENT 'Secondary measure',
|
|
269
|
+
load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'
|
|
270
|
+
)
|
|
271
|
+
USING DELTA
|
|
272
|
+
CLUSTER BY (event_ts)
|
|
273
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true', 'delta.enableChangeDataFeed' = 'true')
|
|
274
|
+
COMMENT 'Fact table fact_${fact}. Add to an existing star \u2014 dimensions assumed to exist.';
|
|
275
|
+
`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function renderScd2Merge(c, source) {
|
|
279
|
+
const lower = c.name.toLowerCase();
|
|
280
|
+
return {
|
|
281
|
+
[`${c.catalog}/${c.schema}/scripts/merge_${lower}.sql`]: header(
|
|
282
|
+
`Canonical SCD2 load. Step 1 closes the prior row when the BK reappears with
|
|
283
|
+
-- different attributes (sets valid_to + is_current=FALSE). Step 2 inserts the
|
|
284
|
+
-- new version. Both steps in one MERGE WHEN MATCHED / WHEN NOT MATCHED.
|
|
285
|
+
-- Source: ${source}.`
|
|
286
|
+
) + `-- Single-statement SCD2 merge.
|
|
287
|
+
-- Databricks MERGE supports multiple WHEN MATCHED clauses \u2014 we use one to close
|
|
288
|
+
-- the old row, then a follow-up INSERT to write the new version (the same MERGE
|
|
289
|
+
-- cannot both UPDATE the prior row AND INSERT the new row for the same source
|
|
290
|
+
-- key, so we split).
|
|
291
|
+
|
|
292
|
+
MERGE INTO ${c.catalog}.${c.schema}.${lower} AS tgt
|
|
293
|
+
USING (
|
|
294
|
+
SELECT
|
|
295
|
+
src.${lower}_bk,
|
|
296
|
+
src.attribute_1,
|
|
297
|
+
src.attribute_2
|
|
298
|
+
FROM ${source} AS src
|
|
299
|
+
) AS src
|
|
300
|
+
ON tgt.${lower}_bk = src.${lower}_bk
|
|
301
|
+
AND tgt.is_current = TRUE
|
|
302
|
+
WHEN MATCHED
|
|
303
|
+
AND (NOT (tgt.attribute_1 <=> src.attribute_1)
|
|
304
|
+
OR NOT (tgt.attribute_2 <=> src.attribute_2))
|
|
305
|
+
THEN UPDATE SET
|
|
306
|
+
valid_to = current_timestamp(),
|
|
307
|
+
is_current = FALSE;
|
|
308
|
+
|
|
309
|
+
INSERT INTO ${c.catalog}.${c.schema}.${lower} (
|
|
310
|
+
${lower}_bk, attribute_1, attribute_2,
|
|
311
|
+
valid_from, valid_to, is_current
|
|
312
|
+
)
|
|
313
|
+
SELECT
|
|
314
|
+
src.${lower}_bk,
|
|
315
|
+
src.attribute_1,
|
|
316
|
+
src.attribute_2,
|
|
317
|
+
current_timestamp(),
|
|
318
|
+
NULL,
|
|
319
|
+
TRUE
|
|
320
|
+
FROM ${source} AS src
|
|
321
|
+
LEFT JOIN ${c.catalog}.${c.schema}.${lower} AS tgt
|
|
322
|
+
ON tgt.${lower}_bk = src.${lower}_bk
|
|
323
|
+
AND tgt.is_current = TRUE
|
|
324
|
+
WHERE tgt.${lower}_bk IS NULL
|
|
325
|
+
OR NOT (tgt.attribute_1 <=> src.attribute_1)
|
|
326
|
+
OR NOT (tgt.attribute_2 <=> src.attribute_2);
|
|
327
|
+
`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function renderCurrentView(c) {
|
|
331
|
+
const lower = c.name.toLowerCase();
|
|
332
|
+
return {
|
|
333
|
+
[`${c.catalog}/${c.schema}/views/v_${lower}_current.sql`]: header(
|
|
334
|
+
`Current-state view over an SCD2 table. Lets downstream consumers join on
|
|
335
|
+
-- one canonical "current row per BK" target without remembering the
|
|
336
|
+
-- is_current predicate. Cheap \u2014 Delta filter-pushdown handles it.`
|
|
337
|
+
) + `CREATE OR REPLACE VIEW ${c.catalog}.${c.schema}.v_${lower}_current
|
|
338
|
+
COMMENT 'Current rows from ${lower} (SCD2). Join here instead of filtering is_current manually.'
|
|
339
|
+
AS
|
|
340
|
+
SELECT *
|
|
341
|
+
FROM ${c.catalog}.${c.schema}.${lower}
|
|
342
|
+
WHERE is_current = TRUE;
|
|
343
|
+
`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function renderTimeSeries(c, retentionDays) {
|
|
347
|
+
const lower = c.name.toLowerCase();
|
|
348
|
+
return {
|
|
349
|
+
[`${c.catalog}/${c.schema}/tables/${lower}.sql`]: header(
|
|
350
|
+
`Time-series table \u2014 append-only, clustered by event timestamp so recent-event
|
|
351
|
+
-- scans prune to a small number of files. delta.logRetentionDuration bounds
|
|
352
|
+
-- Time-Travel storage cost; aged-out rows must be archived elsewhere.`
|
|
353
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (
|
|
354
|
+
event_id BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate id',
|
|
355
|
+
event_ts TIMESTAMP NOT NULL COMMENT 'When the event occurred',
|
|
356
|
+
event_type STRING COMMENT 'Discriminator',
|
|
357
|
+
entity_id STRING COMMENT 'Foreign key to the entity the event is about',
|
|
358
|
+
payload MAP<STRING, STRING> COMMENT 'Event-specific payload',
|
|
359
|
+
ingested_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When DDT loaded the row'
|
|
360
|
+
)
|
|
361
|
+
USING DELTA
|
|
362
|
+
CLUSTER BY (event_ts)
|
|
363
|
+
TBLPROPERTIES (
|
|
364
|
+
'delta.enableDeletionVectors' = 'true',
|
|
365
|
+
'delta.logRetentionDuration' = 'interval ${Math.max(7, Math.min(retentionDays, 365))} days'
|
|
366
|
+
)
|
|
367
|
+
COMMENT 'Time-series ${lower}. Aged out beyond delta.logRetentionDuration; archive separately if you need longer history.';
|
|
368
|
+
`
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function renderAudit(c) {
|
|
372
|
+
const lower = c.name.toLowerCase();
|
|
373
|
+
return {
|
|
374
|
+
[`${c.catalog}/${c.schema}/tables/${lower}_audit.sql`]: header(
|
|
375
|
+
`Audit log \u2014 append-only by convention. INSERT-only role grants enforce the
|
|
376
|
+
-- "append-only" property; this table itself doesn't block updates, but the
|
|
377
|
+
-- grants do.`
|
|
378
|
+
) + `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower}_audit (
|
|
379
|
+
audit_id BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate id',
|
|
380
|
+
audit_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the event was logged',
|
|
381
|
+
actor STRING NOT NULL COMMENT 'Who performed the action',
|
|
382
|
+
action STRING NOT NULL COMMENT 'What happened',
|
|
383
|
+
target STRING COMMENT 'What it was performed on',
|
|
384
|
+
details MAP<STRING, STRING> COMMENT 'Free-form payload \u2014 keep it small'
|
|
385
|
+
)
|
|
386
|
+
USING DELTA
|
|
387
|
+
CLUSTER BY (audit_ts)
|
|
388
|
+
TBLPROPERTIES ('delta.enableDeletionVectors' = 'true')
|
|
389
|
+
COMMENT 'Audit log for ${lower}. Append-only by convention \u2014 GRANT INSERT only to writer roles.';
|
|
390
|
+
`
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
export {
|
|
394
|
+
templateCommand
|
|
395
|
+
};
|
|
396
|
+
//# sourceMappingURL=template-MUJ6X6LN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/template.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\n\n/**\n * `ddt template <kind> <name>` — schema cookbook generator for\n * Databricks Unity Catalog. Mirrors `sdt template` with idioms\n * adapted to Delta + UC (CLUSTER BY, deletion vectors, Photon, no\n * CONSTRAINT NOT ENFORCED — UC doesn't have that flag).\n *\n * Supported kinds:\n * - `scd1` — SCD type-1 (overwrite, no history).\n * - `scd2` — SCD type-2 (history-preserving).\n * - `scd3` — SCD type-3 (one previous value).\n * - `scd4` — SCD type-4 (current + history table).\n * - `scd6` — Hybrid 1+2+3.\n * - `star` — Star schema fact + N dimensions.\n * - `fact` — Fact table only, with FKs to named dims.\n * - `scd2-merge` — Canonical MERGE for SCD2 load.\n * - `current-view` — CREATE VIEW WHERE is_current = TRUE.\n * - `time-series` — Clustered time-series with retention.\n * - `audit` — Append-only audit log.\n */\nexport function templateCommand(): Command {\n const cmd = new Command('template');\n cmd\n .description(\n 'Scaffold a common schema pattern (scd1|scd2|scd3|scd4|scd6|star|fact|scd2-merge|current-view|time-series|audit). Writes .sql files into <out>/.',\n )\n .argument(\n '<kind>',\n 'Pattern to scaffold: scd1 | scd2 | scd3 | scd4 | scd6 | star | fact | scd2-merge | current-view | time-series | audit',\n )\n .argument('<name>', 'Base object name (e.g. CUSTOMER, ORDERS).')\n .option('--catalog <catalog>', 'UC catalog name. Default main.', 'main')\n .option('--schema <schema>', 'Schema name. Default gold.', 'gold')\n .option('-o, --out <path>', 'Output directory. Default <kind>-<name>/.')\n .option(\n '--dims <dims>',\n '[star|fact] Comma-separated dimension names. Default: customer,product,date.',\n 'customer,product,date',\n )\n .option(\n '--track <cols>',\n '[scd3|scd6] Comma-separated columns to track previous-values for. Default: attribute_1.',\n 'attribute_1',\n )\n .option(\n '--source <fqn>',\n '[scd2-merge] Source/staging table fully-qualified name. Default main.gold.<name>_stage.',\n )\n .option('--retention-days <n>', '[time-series] Retention window in days. Default 90.', '90')\n .action(async (kindArg, nameArg, opts) => {\n const kind = String(kindArg).toLowerCase();\n const name = String(nameArg);\n const catalog = String(opts.catalog);\n const schema = String(opts.schema);\n const out = opts.out\n ? path.resolve(String(opts.out))\n : path.resolve(`${kind}-${name.toLowerCase()}`);\n await fs.mkdir(out, { recursive: true });\n\n const ctx = { catalog, schema, name };\n let files: Record<string, string>;\n const trackCols = String(opts.track)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n const dims = String(opts.dims)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (kind === 'scd1') files = renderScd1(ctx);\n else if (kind === 'scd2') files = renderScd2(ctx);\n else if (kind === 'scd3') files = renderScd3(ctx, trackCols);\n else if (kind === 'scd4') files = renderScd4(ctx);\n else if (kind === 'scd6') files = renderScd6(ctx, trackCols);\n else if (kind === 'star') files = renderStar(ctx, dims);\n else if (kind === 'fact') files = renderFact(ctx, dims);\n else if (kind === 'scd2-merge')\n files = renderScd2Merge(\n ctx,\n opts.source ? String(opts.source) : `${catalog}.${schema}.${name.toLowerCase()}_stage`,\n );\n else if (kind === 'current-view') files = renderCurrentView(ctx);\n else if (kind === 'time-series') files = renderTimeSeries(ctx, Number(opts.retentionDays));\n else if (kind === 'audit') files = renderAudit(ctx);\n else\n throw new Error(\n `Unknown template kind: ${kind}. Use scd1 | scd2 | scd3 | scd4 | scd6 | star | fact | scd2-merge | current-view | time-series | audit.`,\n );\n\n for (const [rel, contents] of Object.entries(files)) {\n const filePath = path.join(out, rel);\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, contents, 'utf8');\n console.error(\n ` wrote ${path.relative(process.cwd(), filePath)} (${contents.length} bytes)`,\n );\n }\n console.error(\n `Done. ${Object.keys(files).length} file(s) written to ${path.relative(process.cwd(), out)}.`,\n );\n });\n return cmd;\n}\n\ninterface Ctx {\n catalog: string;\n schema: string;\n name: string;\n}\n\nfunction header(why: string): string {\n return `-- Generated by \\`ddt template\\`. Why this pattern is the right shape:\\n-- ${why}\\n--\\n-- Edit freely; this is just a starting point.\\n\\n`;\n}\n\nfunction renderScd1(c: Ctx): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}.sql`]:\n header(\n `SCD1 overwrites the row in place when an attribute changes. No history is\n-- preserved. Use this when downstream only cares about the *current* state.\n-- load_ts/updated_ts give you minimal auditability without history-table cost.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (\n ${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key — stable across overwrites',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key',\n attribute_1 STRING COMMENT 'Replace with real columns',\n attribute_2 STRING COMMENT 'Replace with real columns',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the row was first inserted',\n updated_ts TIMESTAMP COMMENT 'When the row was last overwritten'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'SCD1 dimension — overwrite-on-change. No history kept; join on ${lower}_bk.';\n`,\n };\n}\n\nfunction renderScd2(c: Ctx): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}.sql`]:\n header(\n `SCD2 preserves every historical version of a row by closing the previous\n-- row's VALID_TO when the natural key reappears with new attribute values.\n-- Use this when downstream analytics needs \"what did this row look like on date X\".`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (\n ${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key',\n attribute_1 STRING COMMENT 'Replace with real columns',\n attribute_2 STRING COMMENT 'Replace with real columns',\n valid_from TIMESTAMP NOT NULL COMMENT 'When this version became current',\n valid_to TIMESTAMP COMMENT 'When this version was superseded; NULL while current',\n is_current BOOLEAN NOT NULL COMMENT 'TRUE only on the latest row per BK',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk, valid_from)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'SCD2 dimension — preserves history. Join on (${lower}_bk, is_current=TRUE) for current state.';\n`,\n };\n}\n\nfunction renderScd3(c: Ctx, tracked: string[]): Record<string, string> {\n const lower = c.name.toLowerCase();\n const cols = (tracked.length ? tracked : ['attribute_1']).map((t) => t.toLowerCase());\n const prevCols = cols\n .map(\n (t) =>\n ` prev_${t} STRING COMMENT 'Prior value of ${t}'`,\n )\n .join(',\\n');\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}.sql`]:\n header(\n `SCD3 keeps the current value plus one prior value per tracked column. Use\n-- this when downstream needs \"current vs immediately prior\" but doesn't need\n-- the full history. Cheaper than SCD2 but lossy beyond one change.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (\n ${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key',\n${cols.map((t) => ` ${t} STRING COMMENT 'Current value'`).join(',\\n')},\n${prevCols},\n prev_changed_ts TIMESTAMP COMMENT 'When prior values were captured',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp',\n updated_ts TIMESTAMP COMMENT 'When this row was last overwritten'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'SCD3 dimension — current + one prior value for ${cols.join(', ')}.';\n`,\n };\n}\n\nfunction renderScd4(c: Ctx): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}.sql`]:\n header(\n `SCD4 keeps a small, fast \"current\" table for joins and a separate history\n-- table for slow analytics. Use this when current-state lookups are hot\n-- (every fact query joins to it) and you want the join table skinny.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (\n ${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key',\n attribute_1 STRING COMMENT 'Replace with real columns',\n attribute_2 STRING COMMENT 'Replace with real columns',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the row was inserted',\n updated_ts TIMESTAMP COMMENT 'When the row was last overwritten'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'SCD4 current-state table. Pair with ${lower}_history for change tracking.';\n`,\n [`${c.catalog}/${c.schema}/tables/${lower}_history.sql`]:\n header(\n `Companion history table for SCD4. Rows are appended (never updated) when\n-- the current table is overwritten — same columns plus a changed_ts so you\n-- can replay \"what did this BK look like at time T\".`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower}_history (\n ${lower}_history_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate id for each historical snapshot',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key',\n attribute_1 STRING COMMENT 'Snapshot of column at changed_ts',\n attribute_2 STRING COMMENT 'Snapshot of column at changed_ts',\n changed_ts TIMESTAMP NOT NULL COMMENT 'When this snapshot was taken',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the row was appended'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk, changed_ts)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true', 'delta.enableChangeDataFeed' = 'true')\nCOMMENT 'SCD4 history-side. Append-only snapshots of ${lower}; reconstruct as-of state via window functions.';\n`,\n };\n}\n\nfunction renderScd6(c: Ctx, tracked: string[]): Record<string, string> {\n const lower = c.name.toLowerCase();\n const cols = (tracked.length ? tracked : ['attribute_1']).map((t) => t.toLowerCase());\n const prevCols = cols\n .map(\n (t) =>\n ` prev_${t} STRING COMMENT 'Prior value of ${t}'`,\n )\n .join(',\\n');\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}.sql`]:\n header(\n `SCD6 = 1 + 2 + 3 hybrid. Full SCD2 history (rows per version), SCD3\n-- \"current vs prior\" lookback on hot columns, and SCD1 \"current value\"\n-- denormalized onto every historical row. Heavyweight; use when downstream\n-- consumers ask both \"what was it at time T?\" and \"what's the current value\n-- for the BK on this old row?\" in the same query.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (\n ${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key — unique per version',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key — stable across versions',\n${cols.map((t) => ` ${t} STRING COMMENT 'Value on THIS version'`).join(',\\n')},\n${cols.map((t) => ` current_${t} STRING COMMENT 'Current value for the BK (SCD1 denorm)'`).join(',\\n')},\n${prevCols},\n valid_from TIMESTAMP NOT NULL COMMENT 'When this version became current',\n valid_to TIMESTAMP COMMENT 'When this version was superseded; NULL while current',\n is_current BOOLEAN NOT NULL COMMENT 'TRUE only on the latest row per BK',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk, valid_from)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'SCD6 dimension — hybrid 1+2+3. Heavy but answers every common BI question without re-joining.';\n`,\n };\n}\n\nfunction renderStar(c: Ctx, dims: string[]): Record<string, string> {\n const fact = c.name.toLowerCase();\n const out: Record<string, string> = {};\n for (const d of dims) {\n const lower = d.toLowerCase();\n out[`${c.catalog}/${c.schema}/tables/dim_${lower}.sql`] =\n header(`Dimension table — describes the WHAT. Joined by the fact via dim_${lower}_sk.`) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.dim_${lower} (\n dim_${lower}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',\n ${lower}_bk STRING NOT NULL COMMENT 'Business key',\n ${lower}_name STRING COMMENT 'Replace with real attributes',\n country STRING COMMENT 'Example attribute'\n)\nUSING DELTA\nCLUSTER BY (${lower}_bk)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'Dimension table for the ${fact} star schema.';\n`;\n }\n out[`${c.catalog}/${c.schema}/tables/fact_${fact}.sql`] =\n header(\n `Fact table — the WHO/WHAT/WHEN/HOW-MUCH. One row per business event.\n-- Surrogate-key FKs to every dimension; measure columns hold the numbers.\n-- CLUSTER BY (event_ts) keeps recent-event scans cheap (Delta data skipping).`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.fact_${fact} (\n fact_${fact}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',\n${dims.map((d) => ` dim_${d.toLowerCase()}_sk BIGINT COMMENT 'FK to dim_${d.toLowerCase()}'`).join(',\\n')},\n event_ts TIMESTAMP NOT NULL COMMENT 'When the event occurred',\n measure_amount DECIMAL(38,4) COMMENT 'Primary measure',\n measure_quantity BIGINT COMMENT 'Secondary measure',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'\n)\nUSING DELTA\nCLUSTER BY (event_ts)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true', 'delta.enableChangeDataFeed' = 'true')\nCOMMENT 'Fact table for the ${fact} star. CLUSTER BY event_ts + Photon = cheap recent-window scans.';\n`;\n return out;\n}\n\nfunction renderFact(c: Ctx, dims: string[]): Record<string, string> {\n const fact = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/tables/fact_${fact}.sql`]:\n header(\n `Fact-only template — assumes the named dimensions already exist. Use this\n-- when adding a new fact to an existing star, rather than scaffolding the\n-- dims a second time. FKs follow the dim_<name>_sk convention.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.fact_${fact} (\n fact_${fact}_sk BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate key',\n${dims.map((d) => ` dim_${d.toLowerCase()}_sk BIGINT COMMENT 'FK to dim_${d.toLowerCase()}'`).join(',\\n')},\n event_ts TIMESTAMP NOT NULL COMMENT 'When the event occurred',\n measure_amount DECIMAL(38,4) COMMENT 'Primary measure',\n measure_quantity BIGINT COMMENT 'Secondary measure',\n load_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'Insertion timestamp'\n)\nUSING DELTA\nCLUSTER BY (event_ts)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true', 'delta.enableChangeDataFeed' = 'true')\nCOMMENT 'Fact table fact_${fact}. Add to an existing star — dimensions assumed to exist.';\n`,\n };\n}\n\nfunction renderScd2Merge(c: Ctx, source: string): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/scripts/merge_${lower}.sql`]:\n header(\n `Canonical SCD2 load. Step 1 closes the prior row when the BK reappears with\n-- different attributes (sets valid_to + is_current=FALSE). Step 2 inserts the\n-- new version. Both steps in one MERGE WHEN MATCHED / WHEN NOT MATCHED.\n-- Source: ${source}.`,\n ) +\n `-- Single-statement SCD2 merge.\n-- Databricks MERGE supports multiple WHEN MATCHED clauses — we use one to close\n-- the old row, then a follow-up INSERT to write the new version (the same MERGE\n-- cannot both UPDATE the prior row AND INSERT the new row for the same source\n-- key, so we split).\n\nMERGE INTO ${c.catalog}.${c.schema}.${lower} AS tgt\nUSING (\n SELECT\n src.${lower}_bk,\n src.attribute_1,\n src.attribute_2\n FROM ${source} AS src\n) AS src\nON tgt.${lower}_bk = src.${lower}_bk\n AND tgt.is_current = TRUE\nWHEN MATCHED\n AND (NOT (tgt.attribute_1 <=> src.attribute_1)\n OR NOT (tgt.attribute_2 <=> src.attribute_2))\n THEN UPDATE SET\n valid_to = current_timestamp(),\n is_current = FALSE;\n\nINSERT INTO ${c.catalog}.${c.schema}.${lower} (\n ${lower}_bk, attribute_1, attribute_2,\n valid_from, valid_to, is_current\n)\nSELECT\n src.${lower}_bk,\n src.attribute_1,\n src.attribute_2,\n current_timestamp(),\n NULL,\n TRUE\nFROM ${source} AS src\nLEFT JOIN ${c.catalog}.${c.schema}.${lower} AS tgt\n ON tgt.${lower}_bk = src.${lower}_bk\n AND tgt.is_current = TRUE\nWHERE tgt.${lower}_bk IS NULL\n OR NOT (tgt.attribute_1 <=> src.attribute_1)\n OR NOT (tgt.attribute_2 <=> src.attribute_2);\n`,\n };\n}\n\nfunction renderCurrentView(c: Ctx): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/views/v_${lower}_current.sql`]:\n header(\n `Current-state view over an SCD2 table. Lets downstream consumers join on\n-- one canonical \"current row per BK\" target without remembering the\n-- is_current predicate. Cheap — Delta filter-pushdown handles it.`,\n ) +\n `CREATE OR REPLACE VIEW ${c.catalog}.${c.schema}.v_${lower}_current\nCOMMENT 'Current rows from ${lower} (SCD2). Join here instead of filtering is_current manually.'\nAS\nSELECT *\nFROM ${c.catalog}.${c.schema}.${lower}\nWHERE is_current = TRUE;\n`,\n };\n}\n\nfunction renderTimeSeries(c: Ctx, retentionDays: number): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}.sql`]:\n header(\n `Time-series table — append-only, clustered by event timestamp so recent-event\n-- scans prune to a small number of files. delta.logRetentionDuration bounds\n-- Time-Travel storage cost; aged-out rows must be archived elsewhere.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower} (\n event_id BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate id',\n event_ts TIMESTAMP NOT NULL COMMENT 'When the event occurred',\n event_type STRING COMMENT 'Discriminator',\n entity_id STRING COMMENT 'Foreign key to the entity the event is about',\n payload MAP<STRING, STRING> COMMENT 'Event-specific payload',\n ingested_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When DDT loaded the row'\n)\nUSING DELTA\nCLUSTER BY (event_ts)\nTBLPROPERTIES (\n 'delta.enableDeletionVectors' = 'true',\n 'delta.logRetentionDuration' = 'interval ${Math.max(7, Math.min(retentionDays, 365))} days'\n)\nCOMMENT 'Time-series ${lower}. Aged out beyond delta.logRetentionDuration; archive separately if you need longer history.';\n`,\n };\n}\n\nfunction renderAudit(c: Ctx): Record<string, string> {\n const lower = c.name.toLowerCase();\n return {\n [`${c.catalog}/${c.schema}/tables/${lower}_audit.sql`]:\n header(\n `Audit log — append-only by convention. INSERT-only role grants enforce the\n-- \"append-only\" property; this table itself doesn't block updates, but the\n-- grants do.`,\n ) +\n `CREATE OR REPLACE TABLE ${c.catalog}.${c.schema}.${lower}_audit (\n audit_id BIGINT GENERATED ALWAYS AS IDENTITY COMMENT 'Surrogate id',\n audit_ts TIMESTAMP NOT NULL DEFAULT current_timestamp() COMMENT 'When the event was logged',\n actor STRING NOT NULL COMMENT 'Who performed the action',\n action STRING NOT NULL COMMENT 'What happened',\n target STRING COMMENT 'What it was performed on',\n details MAP<STRING, STRING> COMMENT 'Free-form payload — keep it small'\n)\nUSING DELTA\nCLUSTER BY (audit_ts)\nTBLPROPERTIES ('delta.enableDeletionVectors' = 'true')\nCOMMENT 'Audit log for ${lower}. Append-only by convention — GRANT INSERT only to writer roles.';\n`,\n };\n}\n"],"mappings":";;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AAqBjB,SAAS,kBAA2B;AACzC,QAAM,MAAM,IAAI,QAAQ,UAAU;AAClC,MACG;AAAA,IACC;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,SAAS,UAAU,2CAA2C,EAC9D,OAAO,uBAAuB,kCAAkC,MAAM,EACtE,OAAO,qBAAqB,8BAA8B,MAAM,EAChE,OAAO,oBAAoB,2CAA2C,EACtE;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,wBAAwB,uDAAuD,IAAI,EAC1F,OAAO,OAAO,SAAS,SAAS,SAAS;AACxC,UAAM,OAAO,OAAO,OAAO,EAAE,YAAY;AACzC,UAAM,OAAO,OAAO,OAAO;AAC3B,UAAM,UAAU,OAAO,KAAK,OAAO;AACnC,UAAM,SAAS,OAAO,KAAK,MAAM;AACjC,UAAM,MAAM,KAAK,MACb,KAAK,QAAQ,OAAO,KAAK,GAAG,CAAC,IAC7B,KAAK,QAAQ,GAAG,IAAI,IAAI,KAAK,YAAY,CAAC,EAAE;AAChD,UAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AAEvC,UAAM,MAAM,EAAE,SAAS,QAAQ,KAAK;AACpC,QAAI;AACJ,UAAM,YAAY,OAAO,KAAK,KAAK,EAChC,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACjB,UAAM,OAAO,OAAO,KAAK,IAAI,EAC1B,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACjB,QAAI,SAAS,OAAQ,SAAQ,WAAW,GAAG;AAAA,aAClC,SAAS,OAAQ,SAAQ,WAAW,GAAG;AAAA,aACvC,SAAS,OAAQ,SAAQ,WAAW,KAAK,SAAS;AAAA,aAClD,SAAS,OAAQ,SAAQ,WAAW,GAAG;AAAA,aACvC,SAAS,OAAQ,SAAQ,WAAW,KAAK,SAAS;AAAA,aAClD,SAAS,OAAQ,SAAQ,WAAW,KAAK,IAAI;AAAA,aAC7C,SAAS,OAAQ,SAAQ,WAAW,KAAK,IAAI;AAAA,aAC7C,SAAS;AAChB,cAAQ;AAAA,QACN;AAAA,QACA,KAAK,SAAS,OAAO,KAAK,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,IAAI,KAAK,YAAY,CAAC;AAAA,MAChF;AAAA,aACO,SAAS,eAAgB,SAAQ,kBAAkB,GAAG;AAAA,aACtD,SAAS,cAAe,SAAQ,iBAAiB,KAAK,OAAO,KAAK,aAAa,CAAC;AAAA,aAChF,SAAS,QAAS,SAAQ,YAAY,GAAG;AAAA;AAEhD,YAAM,IAAI;AAAA,QACR,0BAA0B,IAAI;AAAA,MAChC;AAEF,eAAW,CAAC,KAAK,QAAQ,KAAK,OAAO,QAAQ,KAAK,GAAG;AACnD,YAAM,WAAW,KAAK,KAAK,KAAK,GAAG;AACnC,YAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,YAAM,GAAG,UAAU,UAAU,UAAU,MAAM;AAC7C,cAAQ;AAAA,QACN,WAAW,KAAK,SAAS,QAAQ,IAAI,GAAG,QAAQ,CAAC,KAAK,SAAS,MAAM;AAAA,MACvE;AAAA,IACF;AACA,YAAQ;AAAA,MACN,SAAS,OAAO,KAAK,KAAK,EAAE,MAAM,uBAAuB,KAAK,SAAS,QAAQ,IAAI,GAAG,GAAG,CAAC;AAAA,IAC5F;AAAA,EACF,CAAC;AACH,SAAO;AACT;AAQA,SAAS,OAAO,KAAqB;AACnC,SAAO;AAAA,OAAgF,GAAG;AAAA;AAAA;AAAA;AAAA;AAC5F;AAEA,SAAS,WAAW,GAAgC;AAClD,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,MAAM,GAC7C;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAOK,KAAK;AAAA;AAAA,+EAEuD,KAAK;AAAA;AAAA,EAE7E;AACF;AAEA,SAAS,WAAW,GAAgC;AAClD,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,MAAM,GAC7C;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cASK,KAAK;AAAA;AAAA,6DAEqC,KAAK;AAAA;AAAA,EAE3D;AACF;AAEA,SAAS,WAAW,GAAQ,SAA2C;AACrE,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,QAAM,QAAQ,QAAQ,SAAS,UAAU,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AACpF,QAAM,WAAW,KACd;AAAA,IACC,CAAC,MACC,UAAU,CAAC,iFAAiF,CAAC;AAAA,EACjG,EACC,KAAK,KAAK;AACb,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,MAAM,GAC7C;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AAAA,EACP,KAAK,IAAI,CAAC,MAAM,KAAK,CAAC,+EAA+E,EAAE,KAAK,KAAK,CAAC;AAAA,EAClH,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMI,KAAK;AAAA;AAAA,+DAEuC,KAAK,KAAK,IAAI,CAAC;AAAA;AAAA,EAEvE;AACF;AAEA,SAAS,WAAW,GAAgC;AAClD,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,MAAM,GAC7C;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAOK,KAAK;AAAA;AAAA,+CAE4B,KAAK;AAAA;AAAA,IAEhD,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,cAAc,GACrD;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAOK,KAAK;AAAA;AAAA,uDAEoC,KAAK;AAAA;AAAA,EAE1D;AACF;AAEA,SAAS,WAAW,GAAQ,SAA2C;AACrE,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,QAAM,QAAQ,QAAQ,SAAS,UAAU,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AACpF,QAAM,WAAW,KACd;AAAA,IACC,CAAC,MACC,UAAU,CAAC,iFAAiF,CAAC;AAAA,EACjG,EACC,KAAK,KAAK;AACb,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,MAAM,GAC7C;AAAA,MACE;AAAA;AAAA;AAAA;AAAA;AAAA,IAKF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AAAA,EACP,KAAK,IAAI,CAAC,MAAM,KAAK,CAAC,uFAAuF,EAAE,KAAK,KAAK,CAAC;AAAA,EAC1H,KAAK,IAAI,CAAC,MAAM,aAAa,CAAC,qGAAqG,EAAE,KAAK,KAAK,CAAC;AAAA,EAChJ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAOI,KAAK;AAAA;AAAA;AAAA;AAAA,EAIjB;AACF;AAEA,SAAS,WAAW,GAAQ,MAAwC;AAClE,QAAM,OAAO,EAAE,KAAK,YAAY;AAChC,QAAM,MAA8B,CAAC;AACrC,aAAW,KAAK,MAAM;AACpB,UAAM,QAAQ,EAAE,YAAY;AAC5B,QAAI,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,eAAe,KAAK,MAAM,IACpD,OAAO,yEAAoE,KAAK,MAAM,IACtF,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,QAAQ,KAAK;AAAA,QAC3D,KAAK;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA;AAAA;AAAA;AAAA,cAIK,KAAK;AAAA;AAAA,mCAEgB,IAAI;AAAA;AAAA,EAErC;AACA,MAAI,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,gBAAgB,IAAI,MAAM,IACpD;AAAA,IACE;AAAA;AAAA;AAAA,EAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,SAAS,IAAI;AAAA,SACxD,IAAI;AAAA,EACX,KAAK,IAAI,CAAC,MAAM,SAAS,EAAE,YAAY,CAAC,2EAA2E,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8BAStH,IAAI;AAAA;AAEhC,SAAO;AACT;AAEA,SAAS,WAAW,GAAQ,MAAwC;AAClE,QAAM,OAAO,EAAE,KAAK,YAAY;AAChC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,gBAAgB,IAAI,MAAM,GACjD;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,SAAS,IAAI;AAAA,SAC1D,IAAI;AAAA,EACX,KAAK,IAAI,CAAC,MAAM,SAAS,EAAE,YAAY,CAAC,2EAA2E,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BASzH,IAAI;AAAA;AAAA,EAE7B;AACF;AAEA,SAAS,gBAAgB,GAAQ,QAAwC;AACvE,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,kBAAkB,KAAK,MAAM,GACpD;AAAA,MACE;AAAA;AAAA;AAAA,eAGO,MAAM;AAAA,IACf,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAMO,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA;AAAA;AAAA,UAGjC,KAAK;AAAA;AAAA;AAAA,SAGN,MAAM;AAAA;AAAA,SAEN,KAAK,aAAa,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cASlB,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,IACxC,KAAK;AAAA;AAAA;AAAA;AAAA,QAID,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAMN,MAAM;AAAA,YACD,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA,WAC/B,KAAK,aAAa,KAAK;AAAA;AAAA,YAEtB,KAAK;AAAA;AAAA;AAAA;AAAA,EAIf;AACF;AAEA,SAAS,kBAAkB,GAAgC;AACzD,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,YAAY,KAAK,cAAc,GACtD;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,0BAA0B,EAAE,OAAO,IAAI,EAAE,MAAM,MAAM,KAAK;AAAA,6BACnC,KAAK;AAAA;AAAA;AAAA,OAG3B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA;AAAA;AAAA,EAGnC;AACF;AAEA,SAAS,iBAAiB,GAAQ,eAA+C;AAC/E,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,MAAM,GAC7C;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6CAYlB,KAAK,IAAI,GAAG,KAAK,IAAI,eAAe,GAAG,CAAC,CAAC;AAAA;AAAA,uBAE/D,KAAK;AAAA;AAAA,EAE1B;AACF;AAEA,SAAS,YAAY,GAAgC;AACnD,QAAM,QAAQ,EAAE,KAAK,YAAY;AACjC,SAAO;AAAA,IACL,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,MAAM,WAAW,KAAK,YAAY,GACnD;AAAA,MACE;AAAA;AAAA;AAAA,IAGF,IACA,2BAA2B,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAWtC,KAAK;AAAA;AAAA,EAE5B;AACF;","names":[]}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/test.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import {
|
|
8
|
+
loadProject,
|
|
9
|
+
parseProjectModel,
|
|
10
|
+
testFramework,
|
|
11
|
+
createConnection,
|
|
12
|
+
getProfile
|
|
13
|
+
} from "@ddt-tools/core";
|
|
14
|
+
function testCommand() {
|
|
15
|
+
const cmd = new Command("test").description(
|
|
16
|
+
"Declarative tests (unique / not_null / accepted_values / relationships / expression) compiled from YAML to SQL."
|
|
17
|
+
);
|
|
18
|
+
cmd.command("list").description("Enumerate test files and their test counts.").requiredOption("-p, --project <path>", "Path to the .ddtproj file.").action(async (opts) => {
|
|
19
|
+
const loaded = await loadProject(String(opts.project));
|
|
20
|
+
const files = await testFramework.discoverTests(loaded.rootDir);
|
|
21
|
+
if (files.length === 0) {
|
|
22
|
+
console.log(
|
|
23
|
+
"No tests found. Add files under <project>/tests/<catalog>/<schema>/<table>.test.yaml."
|
|
24
|
+
);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
console.log(` ${f.fqn.padEnd(40)} ${String(f.tests.length).padStart(3)} test(s)`);
|
|
29
|
+
for (const t of f.tests) {
|
|
30
|
+
const cols = "columns" in t ? `(${(t.columns ?? []).join(", ")})` : "column" in t ? `(${t.column})` : "";
|
|
31
|
+
console.log(` - ${t.kind}${cols}${t.name ? ` ${t.name}` : ""}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(
|
|
36
|
+
`${files.length} file(s), ${files.reduce((acc, f) => acc + f.tests.length, 0)} test(s) total.`
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
cmd.command("render").description("Compile every test YAML into SQL assertions. Offline; no workspace contact.").requiredOption("-p, --project <path>", "Path to the .ddtproj file.").option("-o, --out <path>", "Write to file. Defaults to stdout.").action(async (opts) => {
|
|
40
|
+
const loaded = await loadProject(String(opts.project));
|
|
41
|
+
const files = await testFramework.discoverTests(loaded.rootDir);
|
|
42
|
+
const sql = testFramework.renderAllTests(files);
|
|
43
|
+
if (opts.out) {
|
|
44
|
+
const out = path.resolve(String(opts.out));
|
|
45
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
46
|
+
await fs.writeFile(out, sql, "utf8");
|
|
47
|
+
console.error(
|
|
48
|
+
`Wrote ${out} (${sql.length} bytes, ${files.reduce((a, f) => a + f.tests.length, 0)} test(s)).`
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
process.stdout.write(sql);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
cmd.command("run").description(
|
|
55
|
+
"Execute every test assertion against a live Databricks workspace and report pass/fail. Exits 2 when any error-severity test fails. Pro tier \u2014 composes DCM item 5 (data-quality expectations) with the existing dbt-test parity framework."
|
|
56
|
+
).requiredOption("-p, --project <path>", "Path to the .ddtproj file.").requiredOption("-c, --connection <profile>", "Connection profile name.").option(
|
|
57
|
+
"--select <pattern>",
|
|
58
|
+
'Filter tests to files whose FQN matches a glob pattern. E.g. "main.public.*".'
|
|
59
|
+
).option("--fail-fast", "Stop on the first error-severity failure.", false).option(
|
|
60
|
+
"--sample <n>",
|
|
61
|
+
"Cap failing-row samples captured per test. Default 10. 0 to skip.",
|
|
62
|
+
"10"
|
|
63
|
+
).option(
|
|
64
|
+
"--update-snapshots",
|
|
65
|
+
"TEST.3: overwrite every saved snapshot under .ddt/tests/__snapshots__/ with the live rows. Use when intentional schema change makes drift expected.",
|
|
66
|
+
false
|
|
67
|
+
).option("--format <fmt>", "Output format: text | json. Default text.", "text").option("-o, --out <path>", "Write report to a file instead of stdout.").action(async (opts) => {
|
|
68
|
+
const loaded = await loadProject(String(opts.project));
|
|
69
|
+
let files = await testFramework.discoverTests(loaded.rootDir);
|
|
70
|
+
if (opts.select) {
|
|
71
|
+
const re = globToRegex(String(opts.select));
|
|
72
|
+
files = files.filter((f) => re.test(f.fqn));
|
|
73
|
+
}
|
|
74
|
+
if (files.length === 0) {
|
|
75
|
+
console.log(
|
|
76
|
+
"No tests found. Add files under <project>/tests/<catalog>/<schema>/<table>.test.yaml."
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const profile = await getProfile(String(opts.connection));
|
|
81
|
+
const conn = createConnection(profile);
|
|
82
|
+
console.error(`test run: connecting to ${profile.auth.host}\u2026`);
|
|
83
|
+
await conn.connect();
|
|
84
|
+
const snapshotsDir = path.join(loaded.rootDir, ".ddt", "tests", "__snapshots__");
|
|
85
|
+
const snapshotStorage = {
|
|
86
|
+
async read(name) {
|
|
87
|
+
try {
|
|
88
|
+
return await fs.readFile(path.join(snapshotsDir, `${name}.json`), "utf8");
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.code === "ENOENT") return null;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async write(name, body) {
|
|
95
|
+
await fs.mkdir(snapshotsDir, { recursive: true });
|
|
96
|
+
await fs.writeFile(path.join(snapshotsDir, `${name}.json`), body, "utf8");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
let report;
|
|
100
|
+
try {
|
|
101
|
+
report = await testFramework.runTestFiles(files, conn, {
|
|
102
|
+
failFast: opts.failFast === true,
|
|
103
|
+
failingRowSampleCap: Number(opts.sample ?? "10"),
|
|
104
|
+
snapshotStorage,
|
|
105
|
+
updateSnapshots: opts.updateSnapshots === true
|
|
106
|
+
});
|
|
107
|
+
} finally {
|
|
108
|
+
await conn.disconnect().catch(() => void 0);
|
|
109
|
+
}
|
|
110
|
+
const output = String(opts.format).toLowerCase() === "json" ? testFramework.renderJsonReport(report) : testFramework.renderTextReport(report);
|
|
111
|
+
if (opts.out) {
|
|
112
|
+
const out = path.resolve(String(opts.out));
|
|
113
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
114
|
+
await fs.writeFile(out, output, "utf8");
|
|
115
|
+
console.error(
|
|
116
|
+
`Wrote ${out} \u2014 ${report.summary.pass} pass / ${report.summary.fail} fail / ${report.summary.error} error.`
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write(output);
|
|
120
|
+
}
|
|
121
|
+
if (report.summary.blocking) {
|
|
122
|
+
process.exitCode = 2;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
cmd.command("coverage").description(
|
|
126
|
+
"Report which project objects have at least one declarative test (TEST.4). Offline; no workspace contact. Emits a Markdown table (default) or JSON."
|
|
127
|
+
).requiredOption("-p, --project <path>", "Path to the .ddtproj file.").option("--format <fmt>", "Output format: text | json. Default text.", "text").option("-o, --out <path>", "Write report to a file instead of stdout.").action(async (opts) => {
|
|
128
|
+
const loaded = await loadProject(String(opts.project));
|
|
129
|
+
const files = await testFramework.discoverTests(loaded.rootDir);
|
|
130
|
+
const model = await parseProjectModel(loaded);
|
|
131
|
+
const testable = /* @__PURE__ */ new Set([
|
|
132
|
+
"TABLE",
|
|
133
|
+
"MANAGED_TABLE",
|
|
134
|
+
"EXTERNAL_TABLE",
|
|
135
|
+
"VIEW",
|
|
136
|
+
"MATERIALIZED_VIEW",
|
|
137
|
+
"STREAMING_TABLE"
|
|
138
|
+
]);
|
|
139
|
+
const fqns = [];
|
|
140
|
+
for (const obj of model) {
|
|
141
|
+
if (!testable.has(obj.objectType)) continue;
|
|
142
|
+
const cat = obj.fqn.database;
|
|
143
|
+
const sc = obj.fqn.schema;
|
|
144
|
+
if (!cat || !sc) continue;
|
|
145
|
+
fqns.push(`${cat}.${sc}.${obj.fqn.name}`);
|
|
146
|
+
}
|
|
147
|
+
const report = testFramework.computeTestCoverage(files, fqns);
|
|
148
|
+
const output = String(opts.format).toLowerCase() === "json" ? testFramework.renderTestCoverageJson(report) : testFramework.renderTestCoverageMarkdown(report);
|
|
149
|
+
if (opts.out) {
|
|
150
|
+
const out = path.resolve(String(opts.out));
|
|
151
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
152
|
+
await fs.writeFile(out, output, "utf8");
|
|
153
|
+
console.error(
|
|
154
|
+
`Wrote ${out} \u2014 ${report.withTests}/${report.total} object(s) covered (${report.coveragePct}%).`
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
process.stdout.write(output + "\n");
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return cmd;
|
|
161
|
+
}
|
|
162
|
+
function globToRegex(pattern) {
|
|
163
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
164
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
testCommand
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=test-XFSQHR2S.js.map
|