dbt-js 0.1.1 → 0.1.2

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/src/batches.js CHANGED
@@ -1,120 +1,129 @@
1
- // Batch window computation for the microbatch incremental strategy.
2
- // Date math aligns to a configurable IANA timezone (default 'UTC') — windows
3
- // snap to that zone's wall-clock boundaries. No DB access, no SQL dialect concerns.
4
-
5
- const UNITS = ['hour', 'day', 'month', 'year'];
6
-
7
- // Wall-clock components of an instant as seen in `tz`: { year, month(1-12),
8
- // day, hour, minute, second }. Built on Intl so it tracks DST automatically.
9
- function partsInZone(date, tz) {
10
- const dtf = new Intl.DateTimeFormat('en-US', {
11
- timeZone: tz,
12
- hourCycle: 'h23',
13
- year: 'numeric',
14
- month: '2-digit',
15
- day: '2-digit',
16
- hour: '2-digit',
17
- minute: '2-digit',
18
- second: '2-digit',
19
- });
20
- const p = {};
21
- for (const part of dtf.formatToParts(date)) {
22
- if (part.type !== 'literal') p[part.type] = Number(part.value);
23
- }
24
- if (p.hour === 24) p.hour = 0; // some engines emit 24 for midnight under h23
25
- return p;
26
- }
27
-
28
- // The UTC instant for a wall-clock time interpreted in `tz`, DST-correct via an
29
- // offset back-solve (one correction pass resolves spring-forward/fall-back).
30
- function zonedWallToUtc({ year, month, day, hour, minute, second }, tz) {
31
- const offsetAt = (ms) => {
32
- const p = partsInZone(new Date(ms), tz);
33
- return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - ms;
34
- };
35
- const naive = Date.UTC(year, month - 1, day, hour, minute, second);
36
- let utc = naive - offsetAt(naive);
37
- utc = naive - offsetAt(utc);
38
- return new Date(utc);
39
- }
40
-
41
- // Parse a date string into a UTC instant. A string carrying an explicit zone
42
- // (Z or ±HH:MM) is an absolute instant; a naive 'YYYY-MM-DD[ HH:MM[:SS]]' is
43
- // interpreted as wall-clock in `tz`.
44
- export function parseInZone(s, tz = 'UTC') {
45
- const iso = String(s).trim().replace(' ', 'T');
46
- if (/(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(iso)) {
47
- const d = new Date(iso);
48
- if (Number.isNaN(d.getTime())) throw new Error(`Invalid date '${s}' (use YYYY-MM-DD or ISO 8601)`);
49
- return d;
50
- }
51
- const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?)?$/);
52
- if (!m) throw new Error(`Invalid date '${s}' (use YYYY-MM-DD or ISO 8601)`);
53
- return zonedWallToUtc(
54
- { year: +m[1], month: +m[2], day: +m[3], hour: +(m[4] || 0), minute: +(m[5] || 0), second: +(m[6] || 0) },
55
- tz
56
- );
57
- }
58
-
59
- // Back-compat alias — parsing in UTC.
60
- export const parseUtc = (s) => parseInZone(s, 'UTC');
61
-
62
- function truncTz(date, size, tz) {
63
- const p = partsInZone(date, tz);
64
- p.second = 0;
65
- p.minute = 0;
66
- if (size !== 'hour') p.hour = 0;
67
- if (size === 'month' || size === 'year') p.day = 1;
68
- if (size === 'year') p.month = 1;
69
- return zonedWallToUtc(p, tz);
70
- }
71
-
72
- function addBatchesTz(date, size, n, tz) {
73
- const p = partsInZone(date, tz);
74
- if (size === 'hour') p.hour += n;
75
- else if (size === 'day') p.day += n;
76
- else if (size === 'month') p.month += n;
77
- else p.year += n;
78
- return zonedWallToUtc(p, tz); // Date.UTC normalizes overflow (day 32, month 13, ...)
79
- }
80
-
81
- function fmtTz(date, tz) {
82
- const p = partsInZone(date, tz);
83
- const pad = (n) => String(n).padStart(2, '0');
84
- return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}`;
85
- }
86
-
87
- // Returns [{ start, end }] of aligned [start, end) windows as 'YYYY-MM-DD HH:MM:SS'.
88
- // first build / --full-refresh : trunc(begin) .. current batch end
89
- // normal run : trunc(now) - lookback .. current batch end
90
- // explicit backfill : trunc(start) .. ceil(end) (whole batches)
91
- // The window never starts before `begin`.
92
- export function computeBatches({ begin, batchSize, lookback = 1, start, end, firstBuild, timezone = 'UTC', now = new Date() }) {
93
- if (!UNITS.includes(batchSize)) throw new Error(`Invalid batch_size '${batchSize}' (use ${UNITS.join('|')})`);
94
- const beginAt = truncTz(parseInZone(begin, timezone), batchSize, timezone);
95
-
96
- let endAt;
97
- if (end) {
98
- const e = parseInZone(end, timezone);
99
- const t = truncTz(e, batchSize, timezone);
100
- endAt = e.getTime() === t.getTime() ? t : addBatchesTz(t, batchSize, 1, timezone);
101
- } else {
102
- endAt = addBatchesTz(truncTz(now, batchSize, timezone), batchSize, 1, timezone);
103
- }
104
-
105
- let startAt;
106
- if (start) startAt = truncTz(parseInZone(start, timezone), batchSize, timezone);
107
- else if (firstBuild) startAt = beginAt;
108
- else startAt = addBatchesTz(truncTz(now, batchSize, timezone), batchSize, -lookback, timezone);
109
- if (startAt < beginAt) startAt = beginAt;
110
-
111
- if (start && startAt >= endAt) {
112
- throw new Error(`--event-time-start (${fmtTz(startAt, timezone)}) must be before the end of the window (${fmtTz(endAt, timezone)})`);
113
- }
114
-
115
- const batches = [];
116
- for (let t = startAt; t < endAt; t = addBatchesTz(t, batchSize, 1, timezone)) {
117
- batches.push({ start: fmtTz(t, timezone), end: fmtTz(addBatchesTz(t, batchSize, 1, timezone), timezone) });
118
- }
119
- return batches;
120
- }
1
+ // Batch window computation for the microbatch incremental strategy.
2
+ // Date math aligns to a configurable IANA timezone (default 'UTC') — windows
3
+ // snap to that zone's wall-clock boundaries. No DB access, no SQL dialect concerns.
4
+
5
+ const UNITS = ['hour', 'day', 'month', 'year'];
6
+
7
+ // Wall-clock components of an instant as seen in `tz`: { year, month(1-12),
8
+ // day, hour, minute, second }. Built on Intl so it tracks DST automatically.
9
+ function partsInZone(date, tz) {
10
+ const dtf = new Intl.DateTimeFormat('en-US', {
11
+ timeZone: tz,
12
+ hourCycle: 'h23',
13
+ year: 'numeric',
14
+ month: '2-digit',
15
+ day: '2-digit',
16
+ hour: '2-digit',
17
+ minute: '2-digit',
18
+ second: '2-digit',
19
+ });
20
+ const p = {};
21
+ for (const part of dtf.formatToParts(date)) {
22
+ if (part.type !== 'literal') p[part.type] = Number(part.value);
23
+ }
24
+ if (p.hour === 24) p.hour = 0; // some engines emit 24 for midnight under h23
25
+ return p;
26
+ }
27
+
28
+ // The UTC instant for a wall-clock time interpreted in `tz`, DST-correct via an
29
+ // offset back-solve (one correction pass resolves spring-forward/fall-back).
30
+ function zonedWallToUtc({ year, month, day, hour, minute, second }, tz) {
31
+ const offsetAt = (ms) => {
32
+ const p = partsInZone(new Date(ms), tz);
33
+ return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - ms;
34
+ };
35
+ const naive = Date.UTC(year, month - 1, day, hour, minute, second);
36
+ let utc = naive - offsetAt(naive);
37
+ utc = naive - offsetAt(utc);
38
+ return new Date(utc);
39
+ }
40
+
41
+ // Parse a date string into a UTC instant. A string carrying an explicit zone
42
+ // (Z or ±HH:MM) is an absolute instant; a naive 'YYYY-MM-DD[ HH:MM[:SS]]' is
43
+ // interpreted as wall-clock in `tz`.
44
+ export function parseInZone(s, tz = 'UTC') {
45
+ const iso = String(s).trim().replace(' ', 'T');
46
+ if (/(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(iso)) {
47
+ const d = new Date(iso);
48
+ if (Number.isNaN(d.getTime())) throw new Error(`Invalid date '${s}' (use YYYY-MM-DD or ISO 8601)`);
49
+ return d;
50
+ }
51
+ const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?)?$/);
52
+ if (!m) throw new Error(`Invalid date '${s}' (use YYYY-MM-DD or ISO 8601)`);
53
+ return zonedWallToUtc(
54
+ { year: +m[1], month: +m[2], day: +m[3], hour: +(m[4] || 0), minute: +(m[5] || 0), second: +(m[6] || 0) },
55
+ tz
56
+ );
57
+ }
58
+
59
+ // Back-compat alias — parsing in UTC.
60
+ export const parseUtc = (s) => parseInZone(s, 'UTC');
61
+
62
+ function truncTz(date, size, tz) {
63
+ const p = partsInZone(date, tz);
64
+ p.second = 0;
65
+ p.minute = 0;
66
+ if (size !== 'hour') p.hour = 0;
67
+ if (size === 'month' || size === 'year') p.day = 1;
68
+ if (size === 'year') p.month = 1;
69
+ return zonedWallToUtc(p, tz);
70
+ }
71
+
72
+ function addBatchesTz(date, size, n, tz) {
73
+ const p = partsInZone(date, tz);
74
+ if (size === 'hour') p.hour += n;
75
+ else if (size === 'day') p.day += n;
76
+ else if (size === 'month') p.month += n;
77
+ else p.year += n;
78
+ return zonedWallToUtc(p, tz); // Date.UTC normalizes overflow (day 32, month 13, ...)
79
+ }
80
+
81
+ function fmtTz(date, tz) {
82
+ const p = partsInZone(date, tz);
83
+ const pad = (n) => String(n).padStart(2, '0');
84
+ return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}`;
85
+ }
86
+
87
+ // Returns [{ start, end }] of aligned [start, end) windows as 'YYYY-MM-DD HH:MM:SS'.
88
+ // first build / --full-refresh : trunc(begin) .. current batch end
89
+ // normal run : trunc(now) - lookback .. current batch end
90
+ // explicit backfill : trunc(start) .. ceil(end) (whole batches)
91
+ // The window never starts before `begin`.
92
+ export function computeBatches({ begin, batchSize, lookback = 1, start, end, firstBuild, timezone = 'UTC', now = new Date() }) {
93
+ if (!UNITS.includes(batchSize)) throw new Error(`Invalid batch_size '${batchSize}' (use ${UNITS.join('|')})`);
94
+ const beginAt = truncTz(parseInZone(begin, timezone), batchSize, timezone);
95
+
96
+ let endAt;
97
+ if (end) {
98
+ const e = parseInZone(end, timezone);
99
+ const t = truncTz(e, batchSize, timezone);
100
+ endAt = e.getTime() === t.getTime() ? t : addBatchesTz(t, batchSize, 1, timezone);
101
+ } else {
102
+ endAt = addBatchesTz(truncTz(now, batchSize, timezone), batchSize, 1, timezone);
103
+ }
104
+
105
+ let startAt;
106
+ if (start) startAt = truncTz(parseInZone(start, timezone), batchSize, timezone);
107
+ else if (firstBuild) startAt = beginAt;
108
+ else startAt = addBatchesTz(truncTz(now, batchSize, timezone), batchSize, -lookback, timezone);
109
+ if (startAt < beginAt) startAt = beginAt;
110
+
111
+ if (start && startAt >= endAt) {
112
+ throw new Error(`--event-time-start (${fmtTz(startAt, timezone)}) must be before the end of the window (${fmtTz(endAt, timezone)})`);
113
+ }
114
+ // A future (or otherwise out-of-range) `begin` clamps startAt up to or past
115
+ // endAt, yielding zero batches. Without this guard `compile` crashes on b[0]
116
+ // and `run` silently reports success while never creating the table.
117
+ if (startAt >= endAt) {
118
+ throw new Error(
119
+ `Microbatch window is empty: start ${fmtTz(startAt, timezone)} is not before end ${fmtTz(endAt, timezone)} — ` +
120
+ `check that "begin" (${fmtTz(beginAt, timezone)}) is in the past relative to now (${fmtTz(now, timezone)})`
121
+ );
122
+ }
123
+
124
+ const batches = [];
125
+ for (let t = startAt; t < endAt; t = addBatchesTz(t, batchSize, 1, timezone)) {
126
+ batches.push({ start: fmtTz(t, timezone), end: fmtTz(addBatchesTz(t, batchSize, 1, timezone), timezone) });
127
+ }
128
+ return batches;
129
+ }
package/src/cli.js CHANGED
@@ -1,175 +1,178 @@
1
- // Thin CLI over src/api.js: parse flags, format events as log lines, map
2
- // result.ok to the exit code. All orchestration lives in the api.
3
- import { parseArgs } from 'node:util';
4
- import * as api from './api.js';
5
-
6
- const USAGE = `Usage: dbt-js <command> [options]
7
-
8
- Commands:
9
- run Build models in dependency order
10
- test Run data tests (not_null, unique, accepted_values)
11
- seed Load seeds/*.csv into the target schema
12
- compile Print compiled SQL without executing (is_incremental() = false)
13
- ls List nodes in execution order
14
- debug Check config and database connectivity
15
-
16
- Options:
17
- --select SPEC Comma-separated nodes; +name includes upstream, name+ downstream
18
- --full-refresh Rebuild incremental models from scratch (run only)
19
- --vars JSON Override project vars, e.g. --vars '{"start":"2026-06-01"}'
20
- --event-time-start TS Backfill microbatch models from this time (run only)
21
- --event-time-end TS End of the backfill window (requires --event-time-start)`;
22
-
23
- export async function main(argv = process.argv.slice(2)) {
24
- let values, command;
25
- try {
26
- const parsed = parseArgs({
27
- args: argv,
28
- allowPositionals: true,
29
- options: {
30
- select: { type: 'string' },
31
- 'full-refresh': { type: 'boolean', default: false },
32
- vars: { type: 'string' },
33
- 'event-time-start': { type: 'string' },
34
- 'event-time-end': { type: 'string' },
35
- help: { type: 'boolean', default: false },
36
- },
37
- });
38
- values = parsed.values;
39
- command = parsed.positionals[0];
40
- } catch (e) {
41
- console.error(`Error: ${e.message}\n\n${USAGE}`);
42
- process.exit(2);
43
- }
44
-
45
- if (!command || values.help) {
46
- console.log(USAGE);
47
- process.exit(command ? 0 : 2);
48
- }
49
-
50
- const commands = { run, test, seed, compile, ls, debug };
51
- if (!commands[command]) {
52
- console.error(`Error: unknown command '${command}'\n\n${USAGE}`);
53
- process.exit(2);
54
- }
55
-
56
- try {
57
- const ok = await commands[command](values);
58
- process.exit(ok ? 0 : 1);
59
- } catch (e) {
60
- console.error(`Error: ${e.message}`);
61
- process.exit(1);
62
- }
63
- }
64
-
65
- const pad = (s, n) => String(s).padEnd(n);
66
-
67
- function baseOpts(values) {
68
- const opts = { select: values.select };
69
- if (values.vars) {
70
- try {
71
- opts.vars = JSON.parse(values.vars);
72
- } catch {
73
- throw new Error(`--vars must be valid JSON, got: ${values.vars}`);
74
- }
75
- }
76
- return opts;
77
- }
78
-
79
- function printModelEvent(e) {
80
- if (e.type === 'batch') {
81
- const detail = e.ok ? (e.rowCount != null ? `${e.rowCount} rows` : 'ok') : e.message;
82
- console.log(` batch ${e.start} .. ${e.end} ${e.ok ? 'OK' : 'FAIL'} (${detail})`);
83
- return;
84
- }
85
- const tag = `[${e.index}/${e.total}]`;
86
- if (e.status === 'skip') {
87
- console.log(`${tag} ${pad('SKIP', 5)} ${pad(e.materialized, 12)} ${e.name} (upstream failed)`);
88
- } else if (e.status === 'fail' && e.failedBatches?.length) {
89
- const f = e.failedBatches;
90
- console.log(
91
- `${tag} ${pad('FAIL', 5)} ${pad(e.action, 12)} ${e.name} — ${e.error}; ` +
92
- `retry with --select ${e.name} --event-time-start "${f[0].start}" --event-time-end "${f[f.length - 1].end}"`
93
- );
94
- } else if (e.status === 'fail') {
95
- console.log(`${tag} ${pad('FAIL', 5)} ${pad(e.materialized, 12)} ${e.name} — ${e.error}`);
96
- } else {
97
- const rows = e.rowCount != null ? `, ${e.rowCount} rows` : '';
98
- const batches = e.batchCount != null ? `, ${e.batchCount} batches` : '';
99
- console.log(`${tag} ${pad('OK', 5)} ${pad(e.action, 12)} ${e.name} (${e.durationMs}ms${rows}${batches})`);
100
- }
101
- }
102
-
103
- async function run(values) {
104
- if (values['event-time-end'] && !values['event-time-start']) {
105
- throw new Error('--event-time-end requires --event-time-start');
106
- }
107
- const result = await api.run({
108
- ...baseOpts(values),
109
- fullRefresh: values['full-refresh'],
110
- eventTimeStart: values['event-time-start'],
111
- eventTimeEnd: values['event-time-end'],
112
- onEvent: printModelEvent,
113
- });
114
- const counts = { ok: 0, fail: 0, skip: 0 };
115
- for (const m of result.models) counts[m.status]++;
116
- console.log(`\nDone: ${counts.ok} ok, ${counts.fail} failed, ${counts.skip} skipped`);
117
- return result.ok;
118
- }
119
-
120
- async function test(values) {
121
- const result = await api.test({
122
- ...baseOpts(values),
123
- onEvent: (e) => {
124
- if (e.pass) {
125
- console.log(`PASS ${e.id}`);
126
- } else {
127
- console.log(`FAIL ${e.id} (${e.violations} violating rows)`);
128
- for (const row of e.sample) console.log(` ${JSON.stringify(row)}`);
129
- }
130
- },
131
- });
132
- if (!result.tests.length) {
133
- console.log('No tests defined.');
134
- return true;
135
- }
136
- const failures = result.tests.filter((t) => !t.pass).length;
137
- console.log(`\nDone: ${result.tests.length - failures} passed, ${failures} failed`);
138
- return result.ok;
139
- }
140
-
141
- async function seed(values) {
142
- const result = await api.seed({
143
- select: values.select,
144
- onEvent: (e) =>
145
- console.log(`[${e.index}/${e.total}] ${pad('OK', 5)} ${pad('seed', 12)} ${e.name} (${e.durationMs}ms, ${e.rowCount} rows)`),
146
- });
147
- return result.ok;
148
- }
149
-
150
- async function compile(values) {
151
- for (const m of await api.compile(baseOpts(values))) {
152
- console.log(`-- model: ${m.name} (${m.materialized})`);
153
- m.preHookSql.forEach((sql, i) => console.log(`-- pre_hook[${i}]:\n${sql}`));
154
- console.log(m.sql);
155
- m.postHookSql.forEach((sql, i) => console.log(`-- post_hook[${i}]:\n${sql}`));
156
- console.log('');
157
- }
158
- return true;
159
- }
160
-
161
- async function ls() {
162
- for (const n of await api.ls()) {
163
- const deps = n.deps.length ? ` <- ${n.deps.join(', ')}` : '';
164
- console.log(`${pad(n.kind, 12)} ${n.name}${deps}`);
165
- }
166
- return true;
167
- }
168
-
169
- async function debug() {
170
- const d = await api.debug();
171
- console.log(`config: OK (schema "${d.schema}", ${d.modelCount} models, ${d.seedCount} seeds)`);
172
- console.log(`target: ${d.target}`);
173
- console.log(`connect: OK (${d.database}, ${d.version.split(' on ')[0]})`);
174
- return true;
175
- }
1
+ // Thin CLI over src/api.js: parse flags, format events as log lines, map
2
+ // result.ok to the exit code. All orchestration lives in the api.
3
+ import { parseArgs } from 'node:util';
4
+ import * as api from './api.js';
5
+
6
+ const USAGE = `Usage: dbt-js <command> [options]
7
+
8
+ Commands:
9
+ run Build models in dependency order
10
+ test Run data tests (not_null, unique, accepted_values)
11
+ seed Load seeds/*.csv into the target schema
12
+ compile Print compiled SQL without executing (is_incremental() = false)
13
+ ls List nodes in execution order
14
+ debug Check config and database connectivity
15
+
16
+ Options:
17
+ --select SPEC Comma-separated nodes; +name includes upstream, name+ downstream
18
+ --full-refresh Rebuild incremental models from scratch (run only)
19
+ --vars JSON Override project vars, e.g. --vars '{"start":"2026-06-01"}'
20
+ --event-time-start TS Backfill microbatch models from this time (run only)
21
+ --event-time-end TS End of the backfill window (requires --event-time-start)`;
22
+
23
+ export async function main(argv = process.argv.slice(2)) {
24
+ let values, command;
25
+ try {
26
+ const parsed = parseArgs({
27
+ args: argv,
28
+ allowPositionals: true,
29
+ options: {
30
+ select: { type: 'string' },
31
+ 'full-refresh': { type: 'boolean', default: false },
32
+ vars: { type: 'string' },
33
+ 'event-time-start': { type: 'string' },
34
+ 'event-time-end': { type: 'string' },
35
+ help: { type: 'boolean', default: false },
36
+ },
37
+ });
38
+ values = parsed.values;
39
+ command = parsed.positionals[0];
40
+ } catch (e) {
41
+ console.error(`Error: ${e.message}\n\n${USAGE}`);
42
+ process.exit(2);
43
+ }
44
+
45
+ if (!command || values.help) {
46
+ console.log(USAGE);
47
+ process.exit(command ? 0 : 2);
48
+ }
49
+
50
+ const commands = { run, test, seed, compile, ls, debug };
51
+ if (!commands[command]) {
52
+ console.error(`Error: unknown command '${command}'\n\n${USAGE}`);
53
+ process.exit(2);
54
+ }
55
+
56
+ try {
57
+ const ok = await commands[command](values);
58
+ process.exit(ok ? 0 : 1);
59
+ } catch (e) {
60
+ console.error(`Error: ${e.message}`);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ const pad = (s, n) => String(s).padEnd(n);
66
+
67
+ function baseOpts(values) {
68
+ const opts = { select: values.select };
69
+ if (values.vars) {
70
+ try {
71
+ opts.vars = JSON.parse(values.vars);
72
+ } catch {
73
+ throw new Error(`--vars must be valid JSON, got: ${values.vars}`);
74
+ }
75
+ }
76
+ return opts;
77
+ }
78
+
79
+ function printModelEvent(e) {
80
+ if (e.type === 'batch') {
81
+ const detail = e.ok ? (e.rowCount != null ? `${e.rowCount} rows` : 'ok') : e.message;
82
+ console.log(` batch ${e.start} .. ${e.end} ${e.ok ? 'OK' : 'FAIL'} (${detail})`);
83
+ return;
84
+ }
85
+ const tag = `[${e.index}/${e.total}]`;
86
+ if (e.status === 'skip') {
87
+ console.log(`${tag} ${pad('SKIP', 5)} ${pad(e.materialized, 12)} ${e.name} (upstream failed)`);
88
+ } else if (e.status === 'fail' && e.failedBatches?.length) {
89
+ const f = e.failedBatches;
90
+ console.log(
91
+ `${tag} ${pad('FAIL', 5)} ${pad(e.action, 12)} ${e.name} — ${e.error}; ` +
92
+ `retry with --select ${e.name} --event-time-start "${f[0].start}" --event-time-end "${f[f.length - 1].end}"`
93
+ );
94
+ } else if (e.status === 'fail') {
95
+ console.log(`${tag} ${pad('FAIL', 5)} ${pad(e.materialized, 12)} ${e.name} — ${e.error}`);
96
+ } else {
97
+ const rows = e.rowCount != null ? `, ${e.rowCount} rows` : '';
98
+ const batches = e.batchCount != null ? `, ${e.batchCount} batches` : '';
99
+ console.log(`${tag} ${pad('OK', 5)} ${pad(e.action, 12)} ${e.name} (${e.durationMs}ms${rows}${batches})`);
100
+ }
101
+ }
102
+
103
+ async function run(values) {
104
+ if (values['event-time-end'] && !values['event-time-start']) {
105
+ throw new Error('--event-time-end requires --event-time-start');
106
+ }
107
+ const result = await api.run({
108
+ ...baseOpts(values),
109
+ fullRefresh: values['full-refresh'],
110
+ eventTimeStart: values['event-time-start'],
111
+ eventTimeEnd: values['event-time-end'],
112
+ onEvent: printModelEvent,
113
+ });
114
+ const counts = { ok: 0, fail: 0, skip: 0 };
115
+ for (const m of result.models) counts[m.status]++;
116
+ console.log(`\nDone: ${counts.ok} ok, ${counts.fail} failed, ${counts.skip} skipped`);
117
+ return result.ok;
118
+ }
119
+
120
+ async function test(values) {
121
+ const result = await api.test({
122
+ ...baseOpts(values),
123
+ onEvent: (e) => {
124
+ if (e.pass) {
125
+ console.log(`PASS ${e.id}`);
126
+ } else {
127
+ console.log(`FAIL ${e.id} (${e.violations} violating rows)`);
128
+ for (const row of e.sample) console.log(` ${JSON.stringify(row)}`);
129
+ }
130
+ },
131
+ });
132
+ if (!result.tests.length) {
133
+ console.log('No tests defined.');
134
+ return true;
135
+ }
136
+ const failures = result.tests.filter((t) => !t.pass).length;
137
+ console.log(`\nDone: ${result.tests.length - failures} passed, ${failures} failed`);
138
+ return result.ok;
139
+ }
140
+
141
+ async function seed(values) {
142
+ const result = await api.seed({
143
+ select: values.select,
144
+ onEvent: (e) =>
145
+ console.log(`[${e.index}/${e.total}] ${pad('OK', 5)} ${pad('seed', 12)} ${e.name} (${e.durationMs}ms, ${e.rowCount} rows)`),
146
+ });
147
+ return result.ok;
148
+ }
149
+
150
+ async function compile(values) {
151
+ for (const m of await api.compile(baseOpts(values))) {
152
+ console.log(`-- model: ${m.name} (${m.materialized})`);
153
+ m.preHookSql.forEach((sql, i) => console.log(`-- pre_hook[${i}]:\n${sql}`));
154
+ console.log(m.sql);
155
+ m.postHookSql.forEach((sql, i) => console.log(`-- post_hook[${i}]:\n${sql}`));
156
+ console.log('');
157
+ }
158
+ return true;
159
+ }
160
+
161
+ async function ls() {
162
+ for (const n of await api.ls()) {
163
+ const deps = n.deps.length ? ` <- ${n.deps.join(', ')}` : '';
164
+ console.log(`${pad(n.kind, 12)} ${n.name}${deps}`);
165
+ }
166
+ return true;
167
+ }
168
+
169
+ async function debug() {
170
+ const d = await api.debug();
171
+ console.log(`config: OK (schema "${d.schema}", ${d.modelCount} models, ${d.seedCount} seeds)`);
172
+ console.log(`target: ${d.target}`);
173
+ console.log(`connect: OK (${d.database}, ${d.version.split(' on ')[0]})`);
174
+ for (const a of d.attached ?? []) {
175
+ console.log(`attach: OK (${a.alias} -> ${a.path}${a.type ? `, ${a.type}` : ''}${a.readonly ? ', read-only' : ''})`);
176
+ }
177
+ return true;
178
+ }