@sdt-tools/cli 0.2.0 → 0.2.6

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.
Files changed (207) hide show
  1. package/dist/advise-tests-6DRSZMBL.js +87 -0
  2. package/dist/advise-tests-6DRSZMBL.js.map +1 -0
  3. package/dist/ai-G4MJWHTM.js +89 -0
  4. package/dist/ai-G4MJWHTM.js.map +1 -0
  5. package/dist/anonymize-QR6JGXA7.js +123 -0
  6. package/dist/anonymize-QR6JGXA7.js.map +1 -0
  7. package/dist/approval-YVHYTV53.js +73 -0
  8. package/dist/approval-YVHYTV53.js.map +1 -0
  9. package/dist/approval-chain-54KKJZS3.js +120 -0
  10. package/dist/approval-chain-54KKJZS3.js.map +1 -0
  11. package/dist/audit-log-QZFH7LUX.js +159 -0
  12. package/dist/audit-log-QZFH7LUX.js.map +1 -0
  13. package/dist/backlog-V2YUIQDL.js +76 -0
  14. package/dist/backlog-V2YUIQDL.js.map +1 -0
  15. package/dist/bisect-GEVYAVL5.js +111 -0
  16. package/dist/bisect-GEVYAVL5.js.map +1 -0
  17. package/dist/bookmarks-57LKS7P6.js +107 -0
  18. package/dist/bookmarks-57LKS7P6.js.map +1 -0
  19. package/dist/branch-W2MGMPSH.js +88 -0
  20. package/dist/branch-W2MGMPSH.js.map +1 -0
  21. package/dist/build-VNIQFKSP.js +23 -0
  22. package/dist/build-VNIQFKSP.js.map +1 -0
  23. package/dist/catalog-JLB5VCEV.js +137 -0
  24. package/dist/catalog-JLB5VCEV.js.map +1 -0
  25. package/dist/changelog-M7XGDYSY.js +220 -0
  26. package/dist/changelog-M7XGDYSY.js.map +1 -0
  27. package/dist/chunk-DGUM43GV.js +11 -0
  28. package/dist/chunk-DGUM43GV.js.map +1 -0
  29. package/dist/chunk-EWXM4KJN.js +25 -0
  30. package/dist/chunk-EWXM4KJN.js.map +1 -0
  31. package/dist/chunk-JP2EZLR5.js +50 -0
  32. package/dist/chunk-JP2EZLR5.js.map +1 -0
  33. package/dist/chunk-VM2H4LAO.js +15 -0
  34. package/dist/chunk-VM2H4LAO.js.map +1 -0
  35. package/dist/chunk-ZWY4ZRHL.js +44 -0
  36. package/dist/chunk-ZWY4ZRHL.js.map +1 -0
  37. package/dist/cli.js +511 -19014
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-5O6UTWPJ.js +405 -0
  40. package/dist/compare-5O6UTWPJ.js.map +1 -0
  41. package/dist/compare-profiles-7ZSNIW7B.js +218 -0
  42. package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
  43. package/dist/completion-I5U5VVAX.js +82 -0
  44. package/dist/completion-I5U5VVAX.js.map +1 -0
  45. package/dist/connection-GNTZDHXF.js +133 -0
  46. package/dist/connection-GNTZDHXF.js.map +1 -0
  47. package/dist/cost-estimate-TJDDH6TO.js +328 -0
  48. package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
  49. package/dist/data-compare-UK2UXAS3.js +134 -0
  50. package/dist/data-compare-UK2UXAS3.js.map +1 -0
  51. package/dist/data-fit-Q45ENBRL.js +125 -0
  52. package/dist/data-fit-Q45ENBRL.js.map +1 -0
  53. package/dist/deploy-status-UUHKVDTI.js +58 -0
  54. package/dist/deploy-status-UUHKVDTI.js.map +1 -0
  55. package/dist/design-PO6UPBL7.js +138 -0
  56. package/dist/design-PO6UPBL7.js.map +1 -0
  57. package/dist/diagnose-6IFMELFR.js +145 -0
  58. package/dist/diagnose-6IFMELFR.js.map +1 -0
  59. package/dist/discover-A7OSZAHK.js +78 -0
  60. package/dist/discover-A7OSZAHK.js.map +1 -0
  61. package/dist/docs-CVRKGUSW.js +177 -0
  62. package/dist/docs-CVRKGUSW.js.map +1 -0
  63. package/dist/drift-XDA3BDYN.js +226 -0
  64. package/dist/drift-XDA3BDYN.js.map +1 -0
  65. package/dist/drift-gate-V7QSIOGZ.js +94 -0
  66. package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
  67. package/dist/error-lookup-7ZWCZJ44.js +56 -0
  68. package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
  69. package/dist/errorReporting-AQXKKGZH.js +109 -0
  70. package/dist/errorReporting-AQXKKGZH.js.map +1 -0
  71. package/dist/exec-PKBHLI7T.js +121 -0
  72. package/dist/exec-PKBHLI7T.js.map +1 -0
  73. package/dist/explain-LWKJOTL7.js +192 -0
  74. package/dist/explain-LWKJOTL7.js.map +1 -0
  75. package/dist/explorer-QOVM6VBD.js +61 -0
  76. package/dist/explorer-QOVM6VBD.js.map +1 -0
  77. package/dist/export-IYYBZ5HE.js +42 -0
  78. package/dist/export-IYYBZ5HE.js.map +1 -0
  79. package/dist/extract-VMMVRQVT.js +102 -0
  80. package/dist/extract-VMMVRQVT.js.map +1 -0
  81. package/dist/features-LE6BDZ2S.js +59 -0
  82. package/dist/features-LE6BDZ2S.js.map +1 -0
  83. package/dist/feedback-M7DM2EQC.js +161 -0
  84. package/dist/feedback-M7DM2EQC.js.map +1 -0
  85. package/dist/find-EME2JG2I.js +176 -0
  86. package/dist/find-EME2JG2I.js.map +1 -0
  87. package/dist/format-TRLWLMGS.js +141 -0
  88. package/dist/format-TRLWLMGS.js.map +1 -0
  89. package/dist/generate-6NAZGZDV.js +152 -0
  90. package/dist/generate-6NAZGZDV.js.map +1 -0
  91. package/dist/graph-QNQDAUO7.js +161 -0
  92. package/dist/graph-QNQDAUO7.js.map +1 -0
  93. package/dist/history-RONA7ZTI.js +199 -0
  94. package/dist/history-RONA7ZTI.js.map +1 -0
  95. package/dist/hosts-YBXY2ZG5.js +49 -0
  96. package/dist/hosts-YBXY2ZG5.js.map +1 -0
  97. package/dist/impact-T2JSANHS.js +59 -0
  98. package/dist/impact-T2JSANHS.js.map +1 -0
  99. package/dist/import-AELYLY6A.js +32 -0
  100. package/dist/import-AELYLY6A.js.map +1 -0
  101. package/dist/import-script-2OF5BI6A.js +83 -0
  102. package/dist/import-script-2OF5BI6A.js.map +1 -0
  103. package/dist/index.cjs +71 -12
  104. package/dist/index.cjs.map +1 -1
  105. package/dist/index.js +95 -31
  106. package/dist/index.js.map +1 -1
  107. package/dist/init-SWRRJMGI.js +57 -0
  108. package/dist/init-SWRRJMGI.js.map +1 -0
  109. package/dist/install-hooks-6SIAGTAF.js +109 -0
  110. package/dist/install-hooks-6SIAGTAF.js.map +1 -0
  111. package/dist/license-OAF22PLZ.js +46 -0
  112. package/dist/license-OAF22PLZ.js.map +1 -0
  113. package/dist/lineage-EW66XJ6O.js +552 -0
  114. package/dist/lineage-EW66XJ6O.js.map +1 -0
  115. package/dist/lint-FQ2OTYTQ.js +143 -0
  116. package/dist/lint-FQ2OTYTQ.js.map +1 -0
  117. package/dist/mcp-SARDMCDV.js +344 -0
  118. package/dist/mcp-SARDMCDV.js.map +1 -0
  119. package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
  120. package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
  121. package/dist/migrate-platform-NTRTOGNR.js +91 -0
  122. package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
  123. package/dist/optimize-CJYWMAWA.js +105 -0
  124. package/dist/optimize-CJYWMAWA.js.map +1 -0
  125. package/dist/perf-LL2CPCJF.js +205 -0
  126. package/dist/perf-LL2CPCJF.js.map +1 -0
  127. package/dist/pii-FBDRDQ2E.js +136 -0
  128. package/dist/pii-FBDRDQ2E.js.map +1 -0
  129. package/dist/pilot-CCQERKPH.js +29 -0
  130. package/dist/pilot-CCQERKPH.js.map +1 -0
  131. package/dist/pr-comment-S5FF4QRX.js +79 -0
  132. package/dist/pr-comment-S5FF4QRX.js.map +1 -0
  133. package/dist/preview-5U4YVCRM.js +47 -0
  134. package/dist/preview-5U4YVCRM.js.map +1 -0
  135. package/dist/profile-7VC57KD2.js +101 -0
  136. package/dist/profile-7VC57KD2.js.map +1 -0
  137. package/dist/promote-AASEFTIA.js +408 -0
  138. package/dist/promote-AASEFTIA.js.map +1 -0
  139. package/dist/publish-UMVIWH6H.js +721 -0
  140. package/dist/publish-UMVIWH6H.js.map +1 -0
  141. package/dist/purge-QMXZKCMD.js +57 -0
  142. package/dist/purge-QMXZKCMD.js.map +1 -0
  143. package/dist/query-log-6OM4GI7W.js +112 -0
  144. package/dist/query-log-6OM4GI7W.js.map +1 -0
  145. package/dist/refactor-LTZQLJ35.js +5799 -0
  146. package/dist/refactor-LTZQLJ35.js.map +1 -0
  147. package/dist/refresh-4TY2AGOU.js +38 -0
  148. package/dist/refresh-4TY2AGOU.js.map +1 -0
  149. package/dist/replay-OOC25FZN.js +117 -0
  150. package/dist/replay-OOC25FZN.js.map +1 -0
  151. package/dist/revert-ODMUVJW6.js +110 -0
  152. package/dist/revert-ODMUVJW6.js.map +1 -0
  153. package/dist/review-XXPWOBFP.js +158 -0
  154. package/dist/review-XXPWOBFP.js.map +1 -0
  155. package/dist/rollback-suggest-6G2HEKFR.js +79 -0
  156. package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
  157. package/dist/safer-alternative-QFVNLG3L.js +89 -0
  158. package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
  159. package/dist/safety-7QWRSUEZ.js +168 -0
  160. package/dist/safety-7QWRSUEZ.js.map +1 -0
  161. package/dist/savings-RHIXP6IT.js +95 -0
  162. package/dist/savings-RHIXP6IT.js.map +1 -0
  163. package/dist/scan-secrets-5YCQ4UCU.js +54 -0
  164. package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
  165. package/dist/schema-CIZXCQD2.js +429 -0
  166. package/dist/schema-CIZXCQD2.js.map +1 -0
  167. package/dist/script-K7CIN2P6.js +153 -0
  168. package/dist/script-K7CIN2P6.js.map +1 -0
  169. package/dist/search-BUZ5NXZZ.js +151 -0
  170. package/dist/search-BUZ5NXZZ.js.map +1 -0
  171. package/dist/seed-76QAK276.js +96 -0
  172. package/dist/seed-76QAK276.js.map +1 -0
  173. package/dist/sketch-PTLKDIK3.js +88 -0
  174. package/dist/sketch-PTLKDIK3.js.map +1 -0
  175. package/dist/snapshot-XLPR2OZ5.js +177 -0
  176. package/dist/snapshot-XLPR2OZ5.js.map +1 -0
  177. package/dist/snippets-EK4DK5CN.js +74 -0
  178. package/dist/snippets-EK4DK5CN.js.map +1 -0
  179. package/dist/standards-7T2UY6DD.js +241 -0
  180. package/dist/standards-7T2UY6DD.js.map +1 -0
  181. package/dist/suggest-VGRYSAR6.js +39 -0
  182. package/dist/suggest-VGRYSAR6.js.map +1 -0
  183. package/dist/suggest-constraints-MY5WKUHA.js +160 -0
  184. package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
  185. package/dist/suite-TRNGZWQM.js +88 -0
  186. package/dist/suite-TRNGZWQM.js.map +1 -0
  187. package/dist/telemetry-3U2QLA2S.js +75 -0
  188. package/dist/telemetry-3U2QLA2S.js.map +1 -0
  189. package/dist/template-ZERIXVXF.js +403 -0
  190. package/dist/template-ZERIXVXF.js.map +1 -0
  191. package/dist/test-5M2ED3WT.js +169 -0
  192. package/dist/test-5M2ED3WT.js.map +1 -0
  193. package/dist/trial-U732FONV.js +31 -0
  194. package/dist/trial-U732FONV.js.map +1 -0
  195. package/dist/validate-T6D2WCOK.js +106 -0
  196. package/dist/validate-T6D2WCOK.js.map +1 -0
  197. package/dist/verify-KXVASEEG.js +76 -0
  198. package/dist/verify-KXVASEEG.js.map +1 -0
  199. package/dist/watch-I6K4BNMA.js +80 -0
  200. package/dist/watch-I6K4BNMA.js.map +1 -0
  201. package/dist/xcompare-TPFLQO6W.js +87 -0
  202. package/dist/xcompare-TPFLQO6W.js.map +1 -0
  203. package/package.json +2 -2
  204. package/dist/cli.cjs +0 -19040
  205. package/dist/cli.cjs.map +0 -1
  206. package/dist/cli.d.cts +0 -1
  207. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,403 @@
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("--db <database>", "Database name. Default DEMO_DB.", "DEMO_DB").option("--schema <schema>", "Schema name. Default PUBLIC.", "PUBLIC").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 DEMO_DB.PUBLIC.<name>_STAGE."
25
+ ).option("--retention-days <n>", "[time-series] Retention window in days. Default 90.", "90").option(
26
+ "--hybrid",
27
+ "Emit CREATE HYBRID TABLE (Unistore row-store) instead of CREATE OR ALTER TABLE. Constraints become enforced; CLUSTER BY is suppressed (not supported on Hybrid Tables)."
28
+ ).action(async (kindArg, nameArg, opts) => {
29
+ const kind = String(kindArg).toLowerCase();
30
+ const name = String(nameArg);
31
+ const db = String(opts.db);
32
+ const schema = String(opts.schema);
33
+ const hybrid = Boolean(opts.hybrid);
34
+ const out = opts.out ? path.resolve(String(opts.out)) : path.resolve(`${kind}-${name.toLowerCase()}`);
35
+ await fs.mkdir(out, { recursive: true });
36
+ if (hybrid && (kind === "scd2-merge" || kind === "current-view")) {
37
+ console.error(`Warning: --hybrid has no effect on ${kind} (no table DDL emitted).`);
38
+ }
39
+ const ctx = { db, schema, name, hybrid };
40
+ let files;
41
+ const trackCols = String(opts.track).split(",").map((s) => s.trim()).filter(Boolean);
42
+ const dims = String(opts.dims).split(",").map((s) => s.trim()).filter(Boolean);
43
+ if (kind === "scd1") files = renderScd1(ctx);
44
+ else if (kind === "scd2") files = renderScd2(ctx);
45
+ else if (kind === "scd3") files = renderScd3(ctx, trackCols);
46
+ else if (kind === "scd4") files = renderScd4(ctx);
47
+ else if (kind === "scd6") files = renderScd6(ctx, trackCols);
48
+ else if (kind === "star") files = renderStar(ctx, dims);
49
+ else if (kind === "fact") files = renderFact(ctx, dims);
50
+ else if (kind === "scd2-merge")
51
+ files = renderScd2Merge(
52
+ ctx,
53
+ opts.source ? String(opts.source) : `${db}.${schema}.${name.toUpperCase()}_STAGE`
54
+ );
55
+ else if (kind === "current-view") files = renderCurrentView(ctx);
56
+ else if (kind === "time-series") files = renderTimeSeries(ctx, Number(opts.retentionDays));
57
+ else if (kind === "audit") files = renderAudit(ctx);
58
+ else
59
+ throw new Error(
60
+ `Unknown template kind: ${kind}. Use scd1 | scd2 | scd3 | scd4 | scd6 | star | fact | scd2-merge | current-view | time-series | audit.`
61
+ );
62
+ for (const [rel, contents] of Object.entries(files)) {
63
+ const filePath = path.join(out, rel);
64
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
65
+ await fs.writeFile(filePath, contents, "utf8");
66
+ console.error(
67
+ ` wrote ${path.relative(process.cwd(), filePath)} (${contents.length} bytes)`
68
+ );
69
+ }
70
+ console.error(
71
+ `Done. ${Object.keys(files).length} file(s) written to ${path.relative(process.cwd(), out)}.`
72
+ );
73
+ });
74
+ return cmd;
75
+ }
76
+ function header(why) {
77
+ return `-- Generated by \`sdt template\`. Why this pattern is the right shape:
78
+ -- ${why}
79
+ --
80
+ -- Edit freely; this is just a starting point.
81
+
82
+ `;
83
+ }
84
+ function tableKw(c, tableName) {
85
+ return c.hybrid ? `CREATE HYBRID TABLE ${c.db}.${c.schema}.${tableName}` : `CREATE OR ALTER TABLE ${c.db}.${c.schema}.${tableName}`;
86
+ }
87
+ function pkConstraint(name, cols, c) {
88
+ return c.hybrid ? `CONSTRAINT ${name} PRIMARY KEY (${cols})` : `CONSTRAINT ${name} PRIMARY KEY (${cols}) NOT ENFORCED`;
89
+ }
90
+ function ukConstraint(name, cols, c) {
91
+ return c.hybrid ? `CONSTRAINT ${name} UNIQUE (${cols})` : `CONSTRAINT ${name} UNIQUE (${cols}) NOT ENFORCED`;
92
+ }
93
+ function clusterBy(expr, c) {
94
+ return c.hybrid ? "" : `
95
+ CLUSTER BY (${expr})`;
96
+ }
97
+ function renderScd1(c) {
98
+ const upper = c.name.toUpperCase();
99
+ return {
100
+ [`${c.db}/${c.schema}/Tables/${upper}.sql`]: header(
101
+ `SCD1 overwrites the row in place when an attribute changes. No history is
102
+ -- preserved. Use this when downstream consumers only care about the *current*
103
+ -- state and the dimension is small enough that losing history is fine.
104
+ -- LOAD_TS/UPDATED_TS give you minimal auditability without history-table cost.`
105
+ ) + `${tableKw(c, upper)} (
106
+ ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key \u2014 stable across overwrites',
107
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',
108
+ ATTRIBUTE_1 VARCHAR COMMENT 'Replace with real columns',
109
+ ATTRIBUTE_2 VARCHAR COMMENT 'Replace with real columns',
110
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the row was first inserted',
111
+ UPDATED_TS TIMESTAMP_NTZ COMMENT 'When the row was last overwritten',
112
+ ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)},
113
+ ${ukConstraint(`UK_${upper}_BK`, `${upper}_BK`, c)}
114
+ )
115
+ COMMENT = 'SCD1 dimension \u2014 overwrite-on-change. No history kept; join on ${upper}_BK.';
116
+ `
117
+ };
118
+ }
119
+ function renderScd2(c) {
120
+ const upper = c.name.toUpperCase();
121
+ return {
122
+ [`${c.db}/${c.schema}/Tables/${upper}.sql`]: header(
123
+ `SCD2 preserves every historical version of a row by closing the previous row's
124
+ -- VALID_TO when the natural key reappears with new attribute values. Use this
125
+ -- when downstream analytics needs "what did this row look like on date X".`
126
+ ) + `${tableKw(c, upper)} (
127
+ ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key \u2014 unique per version',
128
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key \u2014 stable across versions',
129
+ ATTRIBUTE_1 VARCHAR COMMENT 'Replace with real columns',
130
+ ATTRIBUTE_2 VARCHAR COMMENT 'Replace with real columns',
131
+ VALID_FROM TIMESTAMP_NTZ NOT NULL COMMENT 'When this version became current',
132
+ VALID_TO TIMESTAMP_NTZ COMMENT 'When this version was superseded; NULL while current',
133
+ IS_CURRENT BOOLEAN NOT NULL COMMENT 'TRUE only on the latest row per BK',
134
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',
135
+ ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)}
136
+ )
137
+ COMMENT = 'SCD2 dimension \u2014 preserves history. Treat business key + IS_CURRENT as the natural join target.';
138
+ `,
139
+ [`${c.db}/${c.schema}/Streams/${upper}_STREAM.sql`]: header(
140
+ `Stream over the SCD2 table lets downstream tasks consume only the rows that
141
+ -- changed since they last read. Drop this if you don't have a downstream
142
+ -- consumer that needs incremental updates.`
143
+ ) + `CREATE OR ALTER STREAM ${c.db}.${c.schema}.${upper}_STREAM ON TABLE ${c.db}.${c.schema}.${upper}
144
+ COMMENT = 'CDC stream over ${upper}. Read by downstream tasks for incremental processing.';
145
+ `
146
+ };
147
+ }
148
+ function renderScd3(c, tracked) {
149
+ const upper = c.name.toUpperCase();
150
+ const cols = (tracked.length ? tracked : ["attribute_1"]).map((t) => t.toUpperCase());
151
+ const prevCols = cols.map(
152
+ (t) => ` PREV_${t} VARCHAR COMMENT 'Prior value of ${t}, captured on overwrite'`
153
+ ).join(",\n");
154
+ return {
155
+ [`${c.db}/${c.schema}/Tables/${upper}.sql`]: header(
156
+ `SCD3 keeps the current value plus one prior value per tracked column. Use
157
+ -- this when downstream needs "current vs immediately prior" but doesn't need
158
+ -- the full history. Cheaper than SCD2 but lossy beyond one change.`
159
+ ) + `${tableKw(c, upper)} (
160
+ ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',
161
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',
162
+ ${cols.map((t) => ` ${t} VARCHAR COMMENT 'Current value'`).join(",\n")},
163
+ ${prevCols},
164
+ PREV_CHANGED_TS TIMESTAMP_NTZ COMMENT 'When prior values were captured',
165
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',
166
+ UPDATED_TS TIMESTAMP_NTZ COMMENT 'When this row was last overwritten',
167
+ ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)},
168
+ ${ukConstraint(`UK_${upper}_BK`, `${upper}_BK`, c)}
169
+ )
170
+ COMMENT = 'SCD3 dimension \u2014 current + one prior value for ${cols.join(", ")}.';
171
+ `
172
+ };
173
+ }
174
+ function renderScd4(c) {
175
+ const upper = c.name.toUpperCase();
176
+ return {
177
+ [`${c.db}/${c.schema}/Tables/${upper}.sql`]: header(
178
+ `SCD4 keeps a small, fast "current" table for joins and a separate history
179
+ -- table for slow analytics. Use this when the current-state lookups are hot
180
+ -- (every fact query joins to it) and you want to keep that join skinny.`
181
+ ) + `${tableKw(c, upper)} (
182
+ ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',
183
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',
184
+ ATTRIBUTE_1 VARCHAR COMMENT 'Replace with real columns',
185
+ ATTRIBUTE_2 VARCHAR COMMENT 'Replace with real columns',
186
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the row was inserted',
187
+ UPDATED_TS TIMESTAMP_NTZ COMMENT 'When the row was last overwritten',
188
+ ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)},
189
+ ${ukConstraint(`UK_${upper}_BK`, `${upper}_BK`, c)}
190
+ )
191
+ COMMENT = 'SCD4 current-state table. Pair with ${upper}_HISTORY for change tracking.';
192
+ `,
193
+ [`${c.db}/${c.schema}/Tables/${upper}_HISTORY.sql`]: header(
194
+ `Companion history table for SCD4. Rows are appended (never updated) when
195
+ -- the current table is overwritten \u2014 same columns plus a CHANGED_TS so you
196
+ -- can replay "what did this BK look like at time T".`
197
+ ) + `${tableKw(c, `${upper}_HISTORY`)} (
198
+ ${upper}_HISTORY_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate id for each historical snapshot',
199
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',
200
+ ATTRIBUTE_1 VARCHAR COMMENT 'Snapshot of column at CHANGED_TS',
201
+ ATTRIBUTE_2 VARCHAR COMMENT 'Snapshot of column at CHANGED_TS',
202
+ CHANGED_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When this snapshot was taken',
203
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the row was appended',
204
+ ${pkConstraint(`PK_${upper}_HISTORY`, `${upper}_HISTORY_SK`, c)}
205
+ )${clusterBy(`${upper}_BK, CHANGED_TS`, c)}
206
+ COMMENT = 'SCD4 history-side. Append-only snapshots of ${upper}; reconstruct as-of state via window functions.';
207
+ `
208
+ };
209
+ }
210
+ function renderScd6(c, tracked) {
211
+ const upper = c.name.toUpperCase();
212
+ const cols = (tracked.length ? tracked : ["attribute_1"]).map((t) => t.toUpperCase());
213
+ const prevCols = cols.map(
214
+ (t) => ` PREV_${t} VARCHAR COMMENT 'Prior value of ${t}'`
215
+ ).join(",\n");
216
+ return {
217
+ [`${c.db}/${c.schema}/Tables/${upper}.sql`]: header(
218
+ `SCD6 = 1 + 2 + 3 hybrid. You get full SCD2 history (rows per version),
219
+ -- SCD3 "current vs prior" lookback on hot columns, and SCD1 "current value"
220
+ -- denormalized onto every historical row. Heavyweight; use when downstream
221
+ -- consumers ask both "what was it at time T?" and "what's the current value
222
+ -- for the BK on this old row?" in the same query.`
223
+ ) + `${tableKw(c, upper)} (
224
+ ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key \u2014 unique per version',
225
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key \u2014 stable across versions',
226
+ ${cols.map((t) => ` ${t} VARCHAR COMMENT 'Value on THIS version'`).join(",\n")},
227
+ ${cols.map((t) => ` CURRENT_${t} VARCHAR COMMENT 'Current value for the BK (SCD1 denorm)'`).join(",\n")},
228
+ ${prevCols},
229
+ VALID_FROM TIMESTAMP_NTZ NOT NULL COMMENT 'When this version became current',
230
+ VALID_TO TIMESTAMP_NTZ COMMENT 'When this version was superseded; NULL while current',
231
+ IS_CURRENT BOOLEAN NOT NULL COMMENT 'TRUE only on the latest row per BK',
232
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',
233
+ ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)}
234
+ )
235
+ COMMENT = 'SCD6 dimension \u2014 hybrid 1+2+3. Heavy but answers every common BI question without re-joining.';
236
+ `
237
+ };
238
+ }
239
+ function renderStar(c, dims) {
240
+ const fact = c.name.toUpperCase();
241
+ const out = {};
242
+ for (const d of dims) {
243
+ const upper = d.toUpperCase();
244
+ out[`${c.db}/${c.schema}/Tables/DIM_${upper}.sql`] = header(`Dimension table \u2014 describes the WHAT. Joined by the fact via DIM_${upper}_SK.`) + `${tableKw(c, `DIM_${upper}`)} (
245
+ DIM_${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',
246
+ ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',
247
+ ${upper}_NAME VARCHAR COMMENT 'Replace with real attributes',
248
+ COUNTRY VARCHAR COMMENT 'Example attribute',
249
+ ${pkConstraint(`PK_DIM_${upper}`, `DIM_${upper}_SK`, c)}
250
+ )
251
+ COMMENT = 'Dimension table for the ${fact} star schema.';
252
+ `;
253
+ }
254
+ out[`${c.db}/${c.schema}/Tables/FACT_${fact}.sql`] = header(
255
+ `Fact table \u2014 the WHO/WHAT/WHEN/HOW-MUCH. One row per business event.
256
+ -- Surrogate-key FKs to every dimension; measure columns hold the numbers.`
257
+ ) + `${tableKw(c, `FACT_${fact}`)} (
258
+ FACT_${fact}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',
259
+ ${dims.map((d) => ` DIM_${d.toUpperCase()}_SK NUMBER(38,0) COMMENT 'FK to DIM_${d.toUpperCase()}'`).join(",\n")},
260
+ EVENT_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When the event occurred',
261
+ MEASURE_AMOUNT NUMBER(38,4) COMMENT 'Primary measure \u2014 replace with real columns',
262
+ MEASURE_QUANTITY NUMBER(38,0) COMMENT 'Secondary measure',
263
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',
264
+ ${pkConstraint(`PK_FACT_${fact}`, `FACT_${fact}_SK`, c)}
265
+ )${clusterBy("EVENT_TS", c)}
266
+ COMMENT = 'Fact table for the ${fact} star. CLUSTER BY date keeps recent-event scans cheap.';
267
+ `;
268
+ return out;
269
+ }
270
+ function renderFact(c, dims) {
271
+ const fact = c.name.toUpperCase();
272
+ return {
273
+ [`${c.db}/${c.schema}/Tables/FACT_${fact}.sql`]: header(
274
+ `Fact-only template \u2014 assumes the named dimensions already exist. Use this
275
+ -- when adding a new fact to an existing star, rather than scaffolding the
276
+ -- dims a second time. Surrogate-key FKs follow the DIM_<NAME>_SK convention.`
277
+ ) + `${tableKw(c, `FACT_${fact}`)} (
278
+ FACT_${fact}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',
279
+ ${dims.map((d) => ` DIM_${d.toUpperCase()}_SK NUMBER(38,0) COMMENT 'FK to DIM_${d.toUpperCase()}'`).join(",\n")},
280
+ EVENT_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When the event occurred',
281
+ MEASURE_AMOUNT NUMBER(38,4) COMMENT 'Primary measure \u2014 replace with real columns',
282
+ MEASURE_QUANTITY NUMBER(38,0) COMMENT 'Secondary measure',
283
+ LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',
284
+ ${pkConstraint(`PK_FACT_${fact}`, `FACT_${fact}_SK`, c)}
285
+ )${clusterBy("EVENT_TS", c)}
286
+ COMMENT = 'Fact table FACT_${fact}. Add this to an existing star \u2014 dimensions assumed to exist.';
287
+ `
288
+ };
289
+ }
290
+ function renderScd2Merge(c, source) {
291
+ const upper = c.name.toUpperCase();
292
+ return {
293
+ [`${c.db}/${c.schema}/Scripts/MERGE_${upper}.sql`]: header(
294
+ `Canonical SCD2 load. Step 1 closes the prior row when the BK reappears with
295
+ -- different attributes (sets VALID_TO + IS_CURRENT=FALSE). Step 2 inserts the
296
+ -- new version. Wrap both in a transaction so you don't end up with two current
297
+ -- rows. Source: ${source}.`
298
+ ) + `-- Step 1 \u2014 close the previous version when attributes change for an existing BK.
299
+ BEGIN TRANSACTION;
300
+
301
+ MERGE INTO ${c.db}.${c.schema}.${upper} AS tgt
302
+ USING (
303
+ SELECT
304
+ src.${upper}_BK,
305
+ src.ATTRIBUTE_1,
306
+ src.ATTRIBUTE_2
307
+ FROM ${source} AS src
308
+ ) AS src
309
+ ON tgt.${upper}_BK = src.${upper}_BK
310
+ AND tgt.IS_CURRENT = TRUE
311
+ WHEN MATCHED
312
+ AND (tgt.ATTRIBUTE_1 IS DISTINCT FROM src.ATTRIBUTE_1
313
+ OR tgt.ATTRIBUTE_2 IS DISTINCT FROM src.ATTRIBUTE_2)
314
+ THEN UPDATE SET
315
+ VALID_TO = CURRENT_TIMESTAMP(),
316
+ IS_CURRENT = FALSE;
317
+
318
+ -- Step 2 \u2014 insert the new version for changed BKs + brand-new BKs.
319
+ INSERT INTO ${c.db}.${c.schema}.${upper} (
320
+ ${upper}_BK, ATTRIBUTE_1, ATTRIBUTE_2,
321
+ VALID_FROM, VALID_TO, IS_CURRENT
322
+ )
323
+ SELECT
324
+ src.${upper}_BK,
325
+ src.ATTRIBUTE_1,
326
+ src.ATTRIBUTE_2,
327
+ CURRENT_TIMESTAMP(),
328
+ NULL,
329
+ TRUE
330
+ FROM ${source} AS src
331
+ LEFT JOIN ${c.db}.${c.schema}.${upper} AS tgt
332
+ ON tgt.${upper}_BK = src.${upper}_BK
333
+ AND tgt.IS_CURRENT = TRUE
334
+ WHERE tgt.${upper}_BK IS NULL
335
+ OR (tgt.ATTRIBUTE_1 IS DISTINCT FROM src.ATTRIBUTE_1
336
+ OR tgt.ATTRIBUTE_2 IS DISTINCT FROM src.ATTRIBUTE_2);
337
+
338
+ COMMIT;
339
+ `
340
+ };
341
+ }
342
+ function renderCurrentView(c) {
343
+ const upper = c.name.toUpperCase();
344
+ return {
345
+ [`${c.db}/${c.schema}/Views/V_${upper}_CURRENT.sql`]: header(
346
+ `Current-state view over an SCD2 table. Lets downstream consumers join on
347
+ -- one canonical "current row per BK" target without remembering the
348
+ -- IS_CURRENT predicate. Cheap \u2014 Snowflake filter-pushdown handles it.`
349
+ ) + `CREATE OR REPLACE VIEW ${c.db}.${c.schema}.V_${upper}_CURRENT
350
+ COMMENT = 'Current rows from ${upper} (SCD2). Join here instead of filtering IS_CURRENT manually.'
351
+ AS
352
+ SELECT *
353
+ FROM ${c.db}.${c.schema}.${upper}
354
+ WHERE IS_CURRENT = TRUE;
355
+ `
356
+ };
357
+ }
358
+ function renderTimeSeries(c, retentionDays) {
359
+ const upper = c.name.toUpperCase();
360
+ return {
361
+ [`${c.db}/${c.schema}/Tables/${upper}.sql`]: header(
362
+ `Time-series table \u2014 append-only, clustered by event timestamp so recent-event
363
+ -- scans prune to a small number of micropartitions. DATA_RETENTION_TIME_IN_DAYS
364
+ -- bounds Time Travel cost; aged-out rows must be archived elsewhere.`
365
+ ) + `${tableKw(c, upper)} (
366
+ EVENT_ID NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate id',
367
+ EVENT_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When the event occurred',
368
+ EVENT_TYPE VARCHAR COMMENT 'Discriminator \u2014 index of partition by event_type if useful',
369
+ ENTITY_ID VARCHAR COMMENT 'Foreign key to the entity the event is about',
370
+ PAYLOAD VARIANT COMMENT 'Event-specific payload (semistructured)',
371
+ INGESTED_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When SDT loaded the row',
372
+ ${pkConstraint(`PK_${upper}`, "EVENT_ID", c)}
373
+ )${clusterBy("EVENT_TS", c)}
374
+ DATA_RETENTION_TIME_IN_DAYS = ${Math.max(1, Math.min(retentionDays, 90))}
375
+ COMMENT = 'Time-series ${upper}. Aged out beyond DATA_RETENTION_TIME_IN_DAYS; archive separately if you need longer history.';
376
+ `
377
+ };
378
+ }
379
+ function renderAudit(c) {
380
+ const upper = c.name.toUpperCase();
381
+ return {
382
+ [`${c.db}/${c.schema}/Tables/${upper}_AUDIT.sql`]: header(
383
+ `Audit log \u2014 append-only by convention. INSERT-only INSERT-only permissions on
384
+ -- downstream roles enforce the "append-only" property; this table itself doesn't
385
+ -- block updates, but the role grants do. Pair with a row-access policy if
386
+ -- stronger guarantees are needed.`
387
+ ) + `${tableKw(c, `${upper}_AUDIT`)} (
388
+ AUDIT_ID NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate id',
389
+ AUDIT_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the event was logged',
390
+ ACTOR VARCHAR NOT NULL COMMENT 'Who performed the action (user / service-account)',
391
+ ACTION VARCHAR NOT NULL COMMENT 'What happened (e.g. CREATE_USER, ROLE_GRANT)',
392
+ TARGET VARCHAR COMMENT 'What it was performed on',
393
+ DETAILS VARIANT COMMENT 'Free-form payload \u2014 keep it small',
394
+ ${pkConstraint(`PK_${upper}_AUDIT`, "AUDIT_ID", c)}
395
+ )
396
+ COMMENT = 'Audit log for ${upper}. Append-only by convention \u2014 grant INSERT only to writer roles.';
397
+ `
398
+ };
399
+ }
400
+ export {
401
+ templateCommand
402
+ };
403
+ //# sourceMappingURL=template-ZERIXVXF.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 * `sdt template <kind> <name>` — schema cookbook generator. Produces\n * a complete set of `.sql` files implementing a common pattern, ready\n * to drop into a `.sdtproj`'s object tree. Saves a junior engineer\n * from re-inventing the SCD2 pattern from scratch.\n *\n * Supported kinds:\n * - `scd1` — Slowly-changing-dimension type-1 (overwrite, no history).\n * - `scd2` — Slowly-changing-dimension type-2 (history-preserving).\n * - `scd3` — Slowly-changing-dimension type-3 (one previous value).\n * - `scd4` — Slowly-changing-dimension type-4 (current + history table).\n * - `scd6` — Hybrid 1+2+3 (current + valid_from/to + previous value).\n * - `star` — Star schema fact + N dimensions.\n * - `fact` — Fact table only, with surrogate-key FKs to named dims.\n * - `scd2-merge` — Canonical MERGE statement to load an SCD2 dimension.\n * - `current-view` — `CREATE VIEW` over an SCD2 table where IS_CURRENT = TRUE.\n * - `time-series` — Hash-partitioned time-series with retention.\n * - `audit` — Generic audit-log table with append-only constraints.\n *\n * Mirrors `ddt template`. Patterns are conservative: every template\n * sets a PK, populates COMMENT fields, and prefers `CREATE OR ALTER`\n * over `CREATE OR REPLACE` to play well with the safety classifier.\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('--db <database>', 'Database name. Default DEMO_DB.', 'DEMO_DB')\n .option('--schema <schema>', 'Schema name. Default PUBLIC.', 'PUBLIC')\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 DEMO_DB.PUBLIC.<name>_STAGE.',\n )\n .option('--retention-days <n>', '[time-series] Retention window in days. Default 90.', '90')\n .option(\n '--hybrid',\n 'Emit CREATE HYBRID TABLE (Unistore row-store) instead of CREATE OR ALTER TABLE. Constraints become enforced; CLUSTER BY is suppressed (not supported on Hybrid Tables).',\n )\n .action(async (kindArg, nameArg, opts) => {\n const kind = String(kindArg).toLowerCase();\n const name = String(nameArg);\n const db = String(opts.db);\n const schema = String(opts.schema);\n const hybrid = Boolean(opts.hybrid);\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 if (hybrid && (kind === 'scd2-merge' || kind === 'current-view')) {\n console.error(`Warning: --hybrid has no effect on ${kind} (no table DDL emitted).`);\n }\n\n const ctx = { db, schema, name, hybrid };\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) : `${db}.${schema}.${name.toUpperCase()}_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 db: string;\n schema: string;\n name: string;\n /** When true, emit CREATE HYBRID TABLE with enforced constraints and no CLUSTER BY. */\n hybrid?: boolean;\n}\n\nfunction header(why: string): string {\n return `-- Generated by \\`sdt template\\`. Why this pattern is the right shape:\\n-- ${why}\\n--\\n-- Edit freely; this is just a starting point.\\n\\n`;\n}\n\n/** DDL verb for table creation — Hybrid Tables can't use CREATE OR ALTER. */\nfunction tableKw(c: Ctx, tableName: string): string {\n return c.hybrid\n ? `CREATE HYBRID TABLE ${c.db}.${c.schema}.${tableName}`\n : `CREATE OR ALTER TABLE ${c.db}.${c.schema}.${tableName}`;\n}\n\n/** Primary-key constraint. Hybrid Tables enforce PKs, regular tables don't. */\nfunction pkConstraint(name: string, cols: string, c: Ctx): string {\n return c.hybrid\n ? `CONSTRAINT ${name} PRIMARY KEY (${cols})`\n : `CONSTRAINT ${name} PRIMARY KEY (${cols}) NOT ENFORCED`;\n}\n\n/** Unique constraint. Hybrid Tables enforce UKs, regular tables don't. */\nfunction ukConstraint(name: string, cols: string, c: Ctx): string {\n return c.hybrid\n ? `CONSTRAINT ${name} UNIQUE (${cols})`\n : `CONSTRAINT ${name} UNIQUE (${cols}) NOT ENFORCED`;\n}\n\n/** CLUSTER BY clause — suppressed on Hybrid Tables (not supported). */\nfunction clusterBy(expr: string, c: Ctx): string {\n return c.hybrid ? '' : `\\nCLUSTER BY (${expr})`;\n}\n\nfunction renderScd1(c: Ctx): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Tables/${upper}.sql`]:\n header(\n `SCD1 overwrites the row in place when an attribute changes. No history is\n-- preserved. Use this when downstream consumers only care about the *current*\n-- state and the dimension is small enough that losing history is fine.\n-- LOAD_TS/UPDATED_TS give you minimal auditability without history-table cost.`,\n ) +\n `${tableKw(c, upper)} (\n ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key — stable across overwrites',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',\n ATTRIBUTE_1 VARCHAR COMMENT 'Replace with real columns',\n ATTRIBUTE_2 VARCHAR COMMENT 'Replace with real columns',\n LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the row was first inserted',\n UPDATED_TS TIMESTAMP_NTZ COMMENT 'When the row was last overwritten',\n ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)},\n ${ukConstraint(`UK_${upper}_BK`, `${upper}_BK`, c)}\n)\nCOMMENT = 'SCD1 dimension — overwrite-on-change. No history kept; join on ${upper}_BK.';\n`,\n };\n}\n\nfunction renderScd2(c: Ctx): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Tables/${upper}.sql`]:\n header(\n `SCD2 preserves every historical version of a row by closing the previous row's\n-- VALID_TO when the natural key reappears with new attribute values. Use this\n-- when downstream analytics needs \"what did this row look like on date X\".`,\n ) +\n `${tableKw(c, upper)} (\n ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key — unique per version',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key — stable across versions',\n ATTRIBUTE_1 VARCHAR COMMENT 'Replace with real columns',\n ATTRIBUTE_2 VARCHAR COMMENT 'Replace with real columns',\n VALID_FROM TIMESTAMP_NTZ NOT NULL COMMENT 'When this version became current',\n VALID_TO TIMESTAMP_NTZ 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_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',\n ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)}\n)\nCOMMENT = 'SCD2 dimension — preserves history. Treat business key + IS_CURRENT as the natural join target.';\n`,\n [`${c.db}/${c.schema}/Streams/${upper}_STREAM.sql`]:\n header(\n `Stream over the SCD2 table lets downstream tasks consume only the rows that\n-- changed since they last read. Drop this if you don't have a downstream\n-- consumer that needs incremental updates.`,\n ) +\n `CREATE OR ALTER STREAM ${c.db}.${c.schema}.${upper}_STREAM ON TABLE ${c.db}.${c.schema}.${upper}\nCOMMENT = 'CDC stream over ${upper}. Read by downstream tasks for incremental processing.';\n`,\n };\n}\n\nfunction renderScd3(c: Ctx, tracked: string[]): Record<string, string> {\n const upper = c.name.toUpperCase();\n const cols = (tracked.length ? tracked : ['attribute_1']).map((t) => t.toUpperCase());\n const prevCols = cols\n .map(\n (t) =>\n ` PREV_${t} VARCHAR COMMENT 'Prior value of ${t}, captured on overwrite'`,\n )\n .join(',\\n');\n return {\n [`${c.db}/${c.schema}/Tables/${upper}.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 `${tableKw(c, upper)} (\n ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',\n${cols.map((t) => ` ${t} VARCHAR COMMENT 'Current value'`).join(',\\n')},\n${prevCols},\n PREV_CHANGED_TS TIMESTAMP_NTZ COMMENT 'When prior values were captured',\n LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',\n UPDATED_TS TIMESTAMP_NTZ COMMENT 'When this row was last overwritten',\n ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)},\n ${ukConstraint(`UK_${upper}_BK`, `${upper}_BK`, c)}\n)\nCOMMENT = 'SCD3 dimension — current + one prior value for ${cols.join(', ')}.';\n`,\n };\n}\n\nfunction renderScd4(c: Ctx): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Tables/${upper}.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 the current-state lookups are hot\n-- (every fact query joins to it) and you want to keep that join skinny.`,\n ) +\n `${tableKw(c, upper)} (\n ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',\n ATTRIBUTE_1 VARCHAR COMMENT 'Replace with real columns',\n ATTRIBUTE_2 VARCHAR COMMENT 'Replace with real columns',\n LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the row was inserted',\n UPDATED_TS TIMESTAMP_NTZ COMMENT 'When the row was last overwritten',\n ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)},\n ${ukConstraint(`UK_${upper}_BK`, `${upper}_BK`, c)}\n)\nCOMMENT = 'SCD4 current-state table. Pair with ${upper}_HISTORY for change tracking.';\n`,\n [`${c.db}/${c.schema}/Tables/${upper}_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 `${tableKw(c, `${upper}_HISTORY`)} (\n ${upper}_HISTORY_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate id for each historical snapshot',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',\n ATTRIBUTE_1 VARCHAR COMMENT 'Snapshot of column at CHANGED_TS',\n ATTRIBUTE_2 VARCHAR COMMENT 'Snapshot of column at CHANGED_TS',\n CHANGED_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When this snapshot was taken',\n LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the row was appended',\n ${pkConstraint(`PK_${upper}_HISTORY`, `${upper}_HISTORY_SK`, c)}\n)${clusterBy(`${upper}_BK, CHANGED_TS`, c)}\nCOMMENT = 'SCD4 history-side. Append-only snapshots of ${upper}; reconstruct as-of state via window functions.';\n`,\n };\n}\n\nfunction renderScd6(c: Ctx, tracked: string[]): Record<string, string> {\n const upper = c.name.toUpperCase();\n const cols = (tracked.length ? tracked : ['attribute_1']).map((t) => t.toUpperCase());\n const prevCols = cols\n .map(\n (t) =>\n ` PREV_${t} VARCHAR COMMENT 'Prior value of ${t}'`,\n )\n .join(',\\n');\n return {\n [`${c.db}/${c.schema}/Tables/${upper}.sql`]:\n header(\n `SCD6 = 1 + 2 + 3 hybrid. You get full SCD2 history (rows per version),\n-- SCD3 \"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 `${tableKw(c, upper)} (\n ${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key — unique per version',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key — stable across versions',\n${cols.map((t) => ` ${t} VARCHAR COMMENT 'Value on THIS version'`).join(',\\n')},\n${cols.map((t) => ` CURRENT_${t} VARCHAR COMMENT 'Current value for the BK (SCD1 denorm)'`).join(',\\n')},\n${prevCols},\n VALID_FROM TIMESTAMP_NTZ NOT NULL COMMENT 'When this version became current',\n VALID_TO TIMESTAMP_NTZ 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_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',\n ${pkConstraint(`PK_${upper}`, `${upper}_SK`, c)}\n)\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.toUpperCase();\n const out: Record<string, string> = {};\n for (const d of dims) {\n const upper = d.toUpperCase();\n out[`${c.db}/${c.schema}/Tables/DIM_${upper}.sql`] =\n header(`Dimension table — describes the WHAT. Joined by the fact via DIM_${upper}_SK.`) +\n `${tableKw(c, `DIM_${upper}`)} (\n DIM_${upper}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',\n ${upper}_BK VARCHAR NOT NULL COMMENT 'Business key',\n ${upper}_NAME VARCHAR COMMENT 'Replace with real attributes',\n COUNTRY VARCHAR COMMENT 'Example attribute',\n ${pkConstraint(`PK_DIM_${upper}`, `DIM_${upper}_SK`, c)}\n)\nCOMMENT = 'Dimension table for the ${fact} star schema.';\n`;\n }\n out[`${c.db}/${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 ) +\n `${tableKw(c, `FACT_${fact}`)} (\n FACT_${fact}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',\n${dims.map((d) => ` DIM_${d.toUpperCase()}_SK NUMBER(38,0) COMMENT 'FK to DIM_${d.toUpperCase()}'`).join(',\\n')},\n EVENT_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When the event occurred',\n MEASURE_AMOUNT NUMBER(38,4) COMMENT 'Primary measure — replace with real columns',\n MEASURE_QUANTITY NUMBER(38,0) COMMENT 'Secondary measure',\n LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',\n ${pkConstraint(`PK_FACT_${fact}`, `FACT_${fact}_SK`, c)}\n)${clusterBy('EVENT_TS', c)}\nCOMMENT = 'Fact table for the ${fact} star. CLUSTER BY date keeps recent-event scans cheap.';\n`;\n return out;\n}\n\nfunction renderFact(c: Ctx, dims: string[]): Record<string, string> {\n const fact = c.name.toUpperCase();\n return {\n [`${c.db}/${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. Surrogate-key FKs follow the DIM_<NAME>_SK convention.`,\n ) +\n `${tableKw(c, `FACT_${fact}`)} (\n FACT_${fact}_SK NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate key',\n${dims.map((d) => ` DIM_${d.toUpperCase()}_SK NUMBER(38,0) COMMENT 'FK to DIM_${d.toUpperCase()}'`).join(',\\n')},\n EVENT_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When the event occurred',\n MEASURE_AMOUNT NUMBER(38,4) COMMENT 'Primary measure — replace with real columns',\n MEASURE_QUANTITY NUMBER(38,0) COMMENT 'Secondary measure',\n LOAD_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'Insertion timestamp',\n ${pkConstraint(`PK_FACT_${fact}`, `FACT_${fact}_SK`, c)}\n)${clusterBy('EVENT_TS', c)}\nCOMMENT = 'Fact table FACT_${fact}. Add this to an existing star — dimensions assumed to exist.';\n`,\n };\n}\n\nfunction renderScd2Merge(c: Ctx, source: string): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Scripts/MERGE_${upper}.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. Wrap both in a transaction so you don't end up with two current\n-- rows. Source: ${source}.`,\n ) +\n `-- Step 1 — close the previous version when attributes change for an existing BK.\nBEGIN TRANSACTION;\n\nMERGE INTO ${c.db}.${c.schema}.${upper} AS tgt\nUSING (\n SELECT\n src.${upper}_BK,\n src.ATTRIBUTE_1,\n src.ATTRIBUTE_2\n FROM ${source} AS src\n) AS src\nON tgt.${upper}_BK = src.${upper}_BK\n AND tgt.IS_CURRENT = TRUE\nWHEN MATCHED\n AND (tgt.ATTRIBUTE_1 IS DISTINCT FROM src.ATTRIBUTE_1\n OR tgt.ATTRIBUTE_2 IS DISTINCT FROM src.ATTRIBUTE_2)\n THEN UPDATE SET\n VALID_TO = CURRENT_TIMESTAMP(),\n IS_CURRENT = FALSE;\n\n-- Step 2 — insert the new version for changed BKs + brand-new BKs.\nINSERT INTO ${c.db}.${c.schema}.${upper} (\n ${upper}_BK, ATTRIBUTE_1, ATTRIBUTE_2,\n VALID_FROM, VALID_TO, IS_CURRENT\n)\nSELECT\n src.${upper}_BK,\n src.ATTRIBUTE_1,\n src.ATTRIBUTE_2,\n CURRENT_TIMESTAMP(),\n NULL,\n TRUE\nFROM ${source} AS src\nLEFT JOIN ${c.db}.${c.schema}.${upper} AS tgt\n ON tgt.${upper}_BK = src.${upper}_BK\n AND tgt.IS_CURRENT = TRUE\nWHERE tgt.${upper}_BK IS NULL\n OR (tgt.ATTRIBUTE_1 IS DISTINCT FROM src.ATTRIBUTE_1\n OR tgt.ATTRIBUTE_2 IS DISTINCT FROM src.ATTRIBUTE_2);\n\nCOMMIT;\n`,\n };\n}\n\nfunction renderCurrentView(c: Ctx): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Views/V_${upper}_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 — Snowflake filter-pushdown handles it.`,\n ) +\n `CREATE OR REPLACE VIEW ${c.db}.${c.schema}.V_${upper}_CURRENT\nCOMMENT = 'Current rows from ${upper} (SCD2). Join here instead of filtering IS_CURRENT manually.'\nAS\nSELECT *\nFROM ${c.db}.${c.schema}.${upper}\nWHERE IS_CURRENT = TRUE;\n`,\n };\n}\n\nfunction renderTimeSeries(c: Ctx, retentionDays: number): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Tables/${upper}.sql`]:\n header(\n `Time-series table — append-only, clustered by event timestamp so recent-event\n-- scans prune to a small number of micropartitions. DATA_RETENTION_TIME_IN_DAYS\n-- bounds Time Travel cost; aged-out rows must be archived elsewhere.`,\n ) +\n `${tableKw(c, upper)} (\n EVENT_ID NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate id',\n EVENT_TS TIMESTAMP_NTZ NOT NULL COMMENT 'When the event occurred',\n EVENT_TYPE VARCHAR COMMENT 'Discriminator — index of partition by event_type if useful',\n ENTITY_ID VARCHAR COMMENT 'Foreign key to the entity the event is about',\n PAYLOAD VARIANT COMMENT 'Event-specific payload (semistructured)',\n INGESTED_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When SDT loaded the row',\n ${pkConstraint(`PK_${upper}`, 'EVENT_ID', c)}\n)${clusterBy('EVENT_TS', c)}\nDATA_RETENTION_TIME_IN_DAYS = ${Math.max(1, Math.min(retentionDays, 90))}\nCOMMENT = 'Time-series ${upper}. Aged out beyond DATA_RETENTION_TIME_IN_DAYS; archive separately if you need longer history.';\n`,\n };\n}\n\nfunction renderAudit(c: Ctx): Record<string, string> {\n const upper = c.name.toUpperCase();\n return {\n [`${c.db}/${c.schema}/Tables/${upper}_AUDIT.sql`]:\n header(\n `Audit log — append-only by convention. INSERT-only INSERT-only permissions on\n-- downstream roles enforce the \"append-only\" property; this table itself doesn't\n-- block updates, but the role grants do. Pair with a row-access policy if\n-- stronger guarantees are needed.`,\n ) +\n `${tableKw(c, `${upper}_AUDIT`)} (\n AUDIT_ID NUMBER(38,0) NOT NULL AUTOINCREMENT COMMENT 'Surrogate id',\n AUDIT_TS TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT 'When the event was logged',\n ACTOR VARCHAR NOT NULL COMMENT 'Who performed the action (user / service-account)',\n ACTION VARCHAR NOT NULL COMMENT 'What happened (e.g. CREATE_USER, ROLE_GRANT)',\n TARGET VARCHAR COMMENT 'What it was performed on',\n DETAILS VARIANT COMMENT 'Free-form payload — keep it small',\n ${pkConstraint(`PK_${upper}_AUDIT`, 'AUDIT_ID', c)}\n)\nCOMMENT = 'Audit log for ${upper}. Append-only by convention — grant INSERT only to writer roles.';\n`,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,169 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/test.ts
7
+ import { promises as fs } from "fs";
8
+ import path from "path";
9
+ import { Command } from "commander";
10
+ import { project, testFramework } from "@sdt-tools/core";
11
+ import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
12
+ function testCommand() {
13
+ const cmd = new Command("test").description(
14
+ "Declarative tests (unique / not_null / accepted_values / relationships / expression) compiled from YAML to SQL."
15
+ );
16
+ cmd.command("list").description("Enumerate test files and their test counts.").requiredOption("-p, --project <path>", "Path to the .sdtproj file.").action(async (opts) => {
17
+ const loaded = await project.loadProject(String(opts.project));
18
+ const files = await testFramework.discoverTests(loaded.rootDir);
19
+ if (files.length === 0) {
20
+ console.log(
21
+ "No tests found. Add files under <project>/tests/<db>/<schema>/<table>.test.yaml."
22
+ );
23
+ return;
24
+ }
25
+ for (const f of files) {
26
+ console.log(` ${f.fqn.padEnd(40)} ${String(f.tests.length).padStart(3)} test(s)`);
27
+ for (const t of f.tests) {
28
+ const cols = "columns" in t ? `(${(t.columns ?? []).join(", ")})` : "column" in t ? `(${t.column})` : "";
29
+ console.log(` - ${t.kind}${cols}${t.name ? ` ${t.name}` : ""}`);
30
+ }
31
+ }
32
+ console.log("");
33
+ console.log(
34
+ `${files.length} file(s), ${files.reduce((acc, f) => acc + f.tests.length, 0)} test(s) total.`
35
+ );
36
+ });
37
+ cmd.command("render").description("Compile every test YAML into SQL assertions. Offline; no account contact.").requiredOption("-p, --project <path>", "Path to the .sdtproj file.").option("-o, --out <path>", "Write to file. Defaults to stdout.").action(async (opts) => {
38
+ const loaded = await project.loadProject(String(opts.project));
39
+ const files = await testFramework.discoverTests(loaded.rootDir);
40
+ const sql = testFramework.renderAllTests(files);
41
+ if (opts.out) {
42
+ const out = path.resolve(String(opts.out));
43
+ await fs.mkdir(path.dirname(out), { recursive: true });
44
+ await fs.writeFile(out, sql, "utf8");
45
+ console.error(
46
+ `Wrote ${out} (${sql.length} bytes, ${files.reduce((a, f) => a + f.tests.length, 0)} test(s)).`
47
+ );
48
+ } else {
49
+ process.stdout.write(sql);
50
+ }
51
+ });
52
+ cmd.command("run").description(
53
+ "Execute every test assertion against a live Snowflake account 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."
54
+ ).requiredOption("-p, --project <path>", "Path to the .sdtproj file.").requiredOption("-c, --connection <profile>", "Connection profile name.").option(
55
+ "--select <pattern>",
56
+ 'Filter tests to files whose FQN matches a glob pattern. E.g. "mydb.public.*".'
57
+ ).option("--fail-fast", "Stop on the first error-severity failure.", false).option(
58
+ "--sample <n>",
59
+ "Cap failing-row samples captured per test. Default 10. 0 to skip.",
60
+ "10"
61
+ ).option(
62
+ "--update-snapshots",
63
+ "TEST.3: overwrite every saved snapshot under .sdt/tests/__snapshots__/ with the live rows. Use when intentional schema change makes drift expected.",
64
+ false
65
+ ).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) => {
66
+ const loaded = await project.loadProject(String(opts.project));
67
+ let files = await testFramework.discoverTests(loaded.rootDir);
68
+ if (opts.select) {
69
+ const re = globToRegex(String(opts.select));
70
+ files = files.filter((f) => re.test(f.fqn));
71
+ }
72
+ if (files.length === 0) {
73
+ console.log(
74
+ "No tests found. Add files under <project>/tests/<db>/<schema>/<table>.test.yaml."
75
+ );
76
+ return;
77
+ }
78
+ const profile = await getProfile(String(opts.connection));
79
+ const conn = new SnowflakeConnection(profile);
80
+ logger.step(`test run: connecting to ${profile.account} as ${profile.auth.username}\u2026`);
81
+ await conn.connect();
82
+ const snapshotsDir = path.join(loaded.rootDir, ".sdt", "tests", "__snapshots__");
83
+ const snapshotStorage = {
84
+ async read(name) {
85
+ try {
86
+ return await fs.readFile(path.join(snapshotsDir, `${name}.json`), "utf8");
87
+ } catch (e) {
88
+ if (e.code === "ENOENT") return null;
89
+ throw e;
90
+ }
91
+ },
92
+ async write(name, body) {
93
+ await fs.mkdir(snapshotsDir, { recursive: true });
94
+ await fs.writeFile(path.join(snapshotsDir, `${name}.json`), body, "utf8");
95
+ }
96
+ };
97
+ let report;
98
+ try {
99
+ report = await testFramework.runTestFiles(files, conn, {
100
+ failFast: opts.failFast === true,
101
+ failingRowSampleCap: Number(opts.sample ?? "10"),
102
+ snapshotStorage,
103
+ updateSnapshots: opts.updateSnapshots === true
104
+ });
105
+ } finally {
106
+ await conn.disconnect().catch(() => void 0);
107
+ }
108
+ const output = String(opts.format).toLowerCase() === "json" ? testFramework.renderJsonReport(report) : testFramework.renderTextReport(report);
109
+ if (opts.out) {
110
+ const out = path.resolve(String(opts.out));
111
+ await fs.mkdir(path.dirname(out), { recursive: true });
112
+ await fs.writeFile(out, output, "utf8");
113
+ logger.info(
114
+ `Wrote ${out} \u2014 ${report.summary.pass} pass / ${report.summary.fail} fail / ${report.summary.error} error.`
115
+ );
116
+ } else {
117
+ process.stdout.write(output);
118
+ }
119
+ if (report.summary.blocking) {
120
+ process.exitCode = 2;
121
+ }
122
+ });
123
+ cmd.command("coverage").description(
124
+ "Report which project objects have at least one declarative test (TEST.4). Offline; no account contact. Emits a Markdown table (default) or JSON."
125
+ ).requiredOption("-p, --project <path>", "Path to the .sdtproj 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) => {
126
+ const loaded = await project.loadProject(String(opts.project));
127
+ const files = await testFramework.discoverTests(loaded.rootDir);
128
+ const model = await project.parseProjectModel(loaded);
129
+ const testable = /* @__PURE__ */ new Set([
130
+ "TABLE",
131
+ "EXTERNAL_TABLE",
132
+ "ICEBERG_TABLE",
133
+ "HYBRID_TABLE",
134
+ "DYNAMIC_TABLE",
135
+ "EVENT_TABLE",
136
+ "VIEW",
137
+ "MATERIALIZED_VIEW"
138
+ ]);
139
+ const fqns = [];
140
+ for (const obj of model) {
141
+ if (!testable.has(obj.objectType)) continue;
142
+ const db = obj.fqn.database;
143
+ const sc = obj.fqn.schema;
144
+ if (!db || !sc) continue;
145
+ fqns.push(`${db}.${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
+ logger.info(
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-5M2ED3WT.js.map