@chkit/plugin-backfill 0.1.0-beta.9 → 0.1.1
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/README.md +130 -0
- package/dist/args.d.ts +17 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +11 -0
- package/dist/args.js.map +1 -1
- package/dist/async-backfill.d.ts +64 -0
- package/dist/async-backfill.d.ts.map +1 -0
- package/dist/async-backfill.js +251 -0
- package/dist/async-backfill.js.map +1 -0
- package/dist/check.d.ts +9 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +79 -0
- package/dist/check.js.map +1 -0
- package/dist/chunking/analyze.d.ts +8 -0
- package/dist/chunking/analyze.d.ts.map +1 -0
- package/dist/chunking/analyze.js +8 -0
- package/dist/chunking/analyze.js.map +1 -0
- package/dist/chunking/boundary-codec.d.ts +4 -0
- package/dist/chunking/boundary-codec.d.ts.map +1 -0
- package/dist/chunking/boundary-codec.js +79 -0
- package/dist/chunking/boundary-codec.js.map +1 -0
- package/dist/chunking/build.d.ts +11 -0
- package/dist/chunking/build.d.ts.map +1 -0
- package/dist/chunking/build.js +51 -0
- package/dist/chunking/build.js.map +1 -0
- package/dist/chunking/e2e/constants.d.ts +2 -0
- package/dist/chunking/e2e/constants.d.ts.map +1 -0
- package/dist/chunking/e2e/constants.js +2 -0
- package/dist/chunking/e2e/constants.js.map +1 -0
- package/dist/chunking/e2e/seed-datasets.script.d.ts +20 -0
- package/dist/chunking/e2e/seed-datasets.script.d.ts.map +1 -0
- package/dist/chunking/e2e/seed-datasets.script.js +134 -0
- package/dist/chunking/e2e/seed-datasets.script.js.map +1 -0
- package/dist/chunking/introspect.d.ts +40 -0
- package/dist/chunking/introspect.d.ts.map +1 -0
- package/dist/chunking/introspect.js +187 -0
- package/dist/chunking/introspect.js.map +1 -0
- package/dist/chunking/partition-slices.d.ts +14 -0
- package/dist/chunking/partition-slices.d.ts.map +1 -0
- package/dist/chunking/partition-slices.js +111 -0
- package/dist/chunking/partition-slices.js.map +1 -0
- package/dist/chunking/planner.d.ts +3 -0
- package/dist/chunking/planner.d.ts.map +1 -0
- package/dist/chunking/planner.js +343 -0
- package/dist/chunking/planner.js.map +1 -0
- package/dist/chunking/services/distribution-source.d.ts +11 -0
- package/dist/chunking/services/distribution-source.d.ts.map +1 -0
- package/dist/chunking/services/distribution-source.js +60 -0
- package/dist/chunking/services/distribution-source.js.map +1 -0
- package/dist/chunking/services/metadata-source.d.ts +4 -0
- package/dist/chunking/services/metadata-source.d.ts.map +1 -0
- package/dist/chunking/services/metadata-source.js +138 -0
- package/dist/chunking/services/metadata-source.js.map +1 -0
- package/dist/chunking/services/row-probe.d.ts +12 -0
- package/dist/chunking/services/row-probe.d.ts.map +1 -0
- package/dist/chunking/services/row-probe.js +49 -0
- package/dist/chunking/services/row-probe.js.map +1 -0
- package/dist/chunking/splitter.d.ts +20 -0
- package/dist/chunking/splitter.d.ts.map +1 -0
- package/dist/chunking/splitter.js +76 -0
- package/dist/chunking/splitter.js.map +1 -0
- package/dist/chunking/sql.d.ts +18 -0
- package/dist/chunking/sql.d.ts.map +1 -0
- package/dist/chunking/sql.js +304 -0
- package/dist/chunking/sql.js.map +1 -0
- package/dist/chunking/strategies/equal-width-split.d.ts +3 -0
- package/dist/chunking/strategies/equal-width-split.d.ts.map +1 -0
- package/dist/chunking/strategies/equal-width-split.js +46 -0
- package/dist/chunking/strategies/equal-width-split.js.map +1 -0
- package/dist/chunking/strategies/group-by-key-split.d.ts +3 -0
- package/dist/chunking/strategies/group-by-key-split.d.ts.map +1 -0
- package/dist/chunking/strategies/group-by-key-split.js +54 -0
- package/dist/chunking/strategies/group-by-key-split.js.map +1 -0
- package/dist/chunking/strategies/metadata-single-chunk.d.ts +3 -0
- package/dist/chunking/strategies/metadata-single-chunk.d.ts.map +1 -0
- package/dist/chunking/strategies/metadata-single-chunk.js +5 -0
- package/dist/chunking/strategies/metadata-single-chunk.js.map +1 -0
- package/dist/chunking/strategies/quantile-range-split.d.ts +5 -0
- package/dist/chunking/strategies/quantile-range-split.d.ts.map +1 -0
- package/dist/chunking/strategies/quantile-range-split.js +132 -0
- package/dist/chunking/strategies/quantile-range-split.js.map +1 -0
- package/dist/chunking/strategies/refinement.d.ts +3 -0
- package/dist/chunking/strategies/refinement.d.ts.map +1 -0
- package/dist/chunking/strategies/refinement.js +61 -0
- package/dist/chunking/strategies/refinement.js.map +1 -0
- package/dist/chunking/strategies/string-prefix-split.d.ts +4 -0
- package/dist/chunking/strategies/string-prefix-split.d.ts.map +1 -0
- package/dist/chunking/strategies/string-prefix-split.js +73 -0
- package/dist/chunking/strategies/string-prefix-split.js.map +1 -0
- package/dist/chunking/strategies/temporal-bucket-split.d.ts +3 -0
- package/dist/chunking/strategies/temporal-bucket-split.d.ts.map +1 -0
- package/dist/chunking/strategies/temporal-bucket-split.js +67 -0
- package/dist/chunking/strategies/temporal-bucket-split.js.map +1 -0
- package/dist/chunking/strategy-policy.d.ts +3 -0
- package/dist/chunking/strategy-policy.d.ts.map +1 -0
- package/dist/chunking/strategy-policy.js +4 -0
- package/dist/chunking/strategy-policy.js.map +1 -0
- package/dist/chunking/types.d.ts +137 -0
- package/dist/chunking/types.d.ts.map +1 -0
- package/dist/chunking/types.js +2 -0
- package/dist/chunking/types.js.map +1 -0
- package/dist/chunking/utils/binary-string.d.ts +8 -0
- package/dist/chunking/utils/binary-string.d.ts.map +1 -0
- package/dist/chunking/utils/binary-string.js +52 -0
- package/dist/chunking/utils/binary-string.js.map +1 -0
- package/dist/chunking/utils/ids.d.ts +4 -0
- package/dist/chunking/utils/ids.d.ts.map +1 -0
- package/dist/chunking/utils/ids.js +11 -0
- package/dist/chunking/utils/ids.js.map +1 -0
- package/dist/chunking/utils/ranges.d.ts +5 -0
- package/dist/chunking/utils/ranges.d.ts.map +1 -0
- package/dist/chunking/utils/ranges.js +19 -0
- package/dist/chunking/utils/ranges.js.map +1 -0
- package/dist/detect.d.ts +8 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +72 -0
- package/dist/detect.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logging.d.ts +12 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +61 -0
- package/dist/logging.js.map +1 -0
- package/dist/options.d.ts +193 -4
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +168 -111
- package/dist/options.js.map +1 -1
- package/dist/payload.d.ts +7 -17
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +7 -19
- package/dist/payload.js.map +1 -1
- package/dist/planner.d.ts +10 -8
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +76 -97
- package/dist/planner.js.map +1 -1
- package/dist/plugin.d.ts +4 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +375 -201
- package/dist/plugin.js.map +1 -1
- package/dist/queries.d.ts +21 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +113 -0
- package/dist/queries.js.map +1 -0
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +162 -83
- package/dist/runtime.js.map +1 -1
- package/dist/sdk.d.ts +16 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +11 -0
- package/dist/sdk.js.map +1 -0
- package/dist/state.d.ts +16 -28
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +73 -127
- package/dist/state.js.map +1 -1
- package/dist/table-config.d.ts +9 -0
- package/dist/table-config.d.ts.map +1 -0
- package/dist/table-config.js +2 -0
- package/dist/table-config.js.map +1 -0
- package/dist/types.d.ts +49 -119
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -2
package/dist/plugin.js
CHANGED
|
@@ -1,259 +1,433 @@
|
|
|
1
|
+
import { createClickHouseExecutor } from '@chkit/clickhouse';
|
|
1
2
|
import { wrapPluginRun } from '@chkit/core';
|
|
2
|
-
import {
|
|
3
|
+
import { executeBackfill } from './async-backfill.js';
|
|
4
|
+
import { buildChunkExecutionSql } from './chunking/sql.js';
|
|
5
|
+
import { generateIdempotencyToken } from './chunking/utils/ids.js';
|
|
3
6
|
import { BackfillConfigError } from './errors.js';
|
|
4
|
-
import {
|
|
5
|
-
import { planPayload,
|
|
7
|
+
import { CheckSchema, PLAN_FLAGS, PLAN_FLAG_MAP, PLAN_ID_FLAGS, PLAN_ID_FLAG_MAP, PlanSchema, PluginConfigSchema, RESUME_FLAGS, RESUME_FLAG_MAP, ResumeSchema, RUN_FLAGS, RUN_FLAG_MAP, RunSchema, StatusSchema, SUBMIT_FLAGS, SUBMIT_FLAG_MAP, SubmitSchema, } from './options.js';
|
|
8
|
+
import { planPayload, statusPayload, cancelPayload, doctorPayload } from './payload.js';
|
|
6
9
|
import { buildBackfillPlan } from './planner.js';
|
|
7
|
-
import {
|
|
10
|
+
import { evaluateBackfillCheck } from './check.js';
|
|
11
|
+
import { cancelBackfillRun, getBackfillDoctorReport, getBackfillStatus } from './queries.js';
|
|
12
|
+
import { backfillPaths, ensureEnvironmentMatch, nowIso, readPlan, readRun, summarizeRunStatus, writeJson, } from './state.js';
|
|
13
|
+
function formatBytes(bytes) {
|
|
14
|
+
if (bytes >= 1024 ** 4)
|
|
15
|
+
return `${(bytes / 1024 ** 4).toFixed(1)} TiB`;
|
|
16
|
+
if (bytes >= 1024 ** 3)
|
|
17
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
18
|
+
if (bytes >= 1024 ** 2)
|
|
19
|
+
return `${(bytes / 1024 ** 2).toFixed(1)} MiB`;
|
|
20
|
+
if (bytes >= 1024)
|
|
21
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
22
|
+
return `${bytes} B`;
|
|
23
|
+
}
|
|
24
|
+
async function runBackfill(input) {
|
|
25
|
+
const { plan, stateDir } = await readPlan({
|
|
26
|
+
planId: input.planId,
|
|
27
|
+
configPath: input.configPath,
|
|
28
|
+
config: input.config,
|
|
29
|
+
stateDir: input.stateDir,
|
|
30
|
+
});
|
|
31
|
+
ensureEnvironmentMatch({
|
|
32
|
+
plan,
|
|
33
|
+
clickhouse: input.clickhouse,
|
|
34
|
+
forceEnvironment: input.forceEnvironment,
|
|
35
|
+
});
|
|
36
|
+
const paths = backfillPaths(stateDir, plan.planId);
|
|
37
|
+
// Check for existing run state
|
|
38
|
+
const existingRun = await readRun(paths.runPath);
|
|
39
|
+
const resumeFrom = input.resumeFrom;
|
|
40
|
+
if (existingRun && !resumeFrom) {
|
|
41
|
+
// `run` command (no resumeFrom) must not silently continue an existing run.
|
|
42
|
+
// Users should use `backfill resume` instead.
|
|
43
|
+
const status = existingRun.status;
|
|
44
|
+
if (status === 'completed') {
|
|
45
|
+
throw new BackfillConfigError(`Run already completed for plan ${plan.planId}. Nothing to do.`);
|
|
46
|
+
}
|
|
47
|
+
if (status === 'cancelled') {
|
|
48
|
+
throw new BackfillConfigError(`Run is cancelled for plan ${plan.planId}. Create a new plan or inspect with backfill doctor.`);
|
|
49
|
+
}
|
|
50
|
+
throw new BackfillConfigError(`A run already exists for plan ${plan.planId} (status: ${status}). Use backfill resume to continue.`);
|
|
51
|
+
}
|
|
52
|
+
const db = createClickHouseExecutor(input.clickhouse);
|
|
53
|
+
try {
|
|
54
|
+
const runState = {
|
|
55
|
+
planId: plan.planId,
|
|
56
|
+
target: plan.target,
|
|
57
|
+
status: 'running',
|
|
58
|
+
startedAt: existingRun?.startedAt ?? nowIso(),
|
|
59
|
+
updatedAt: nowIso(),
|
|
60
|
+
progress: resumeFrom ?? {},
|
|
61
|
+
};
|
|
62
|
+
await writeJson(paths.runPath, runState);
|
|
63
|
+
const result = await executeBackfill({
|
|
64
|
+
executor: db,
|
|
65
|
+
planId: plan.planId,
|
|
66
|
+
chunks: plan.chunkPlan.chunks.map((chunk) => ({ id: chunk.id })),
|
|
67
|
+
buildQuery: (chunk) => {
|
|
68
|
+
const planChunk = plan.chunkPlan.chunks.find((candidate) => candidate.id === chunk.id);
|
|
69
|
+
if (!planChunk)
|
|
70
|
+
throw new Error(`Chunk ${chunk.id} not found in plan`);
|
|
71
|
+
return buildChunkExecutionSql({
|
|
72
|
+
planId: plan.planId,
|
|
73
|
+
chunk: planChunk,
|
|
74
|
+
target: plan.target,
|
|
75
|
+
sourceTarget: plan.execution.sourceTarget,
|
|
76
|
+
table: plan.chunkPlan.table,
|
|
77
|
+
mvAsQuery: plan.execution.mvAsQuery,
|
|
78
|
+
targetColumns: plan.execution.targetColumns,
|
|
79
|
+
idempotencyToken: plan.execution.requireIdempotencyToken
|
|
80
|
+
? generateIdempotencyToken(plan.planId, planChunk.id)
|
|
81
|
+
: '',
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
concurrency: input.concurrency,
|
|
85
|
+
pollIntervalMs: input.pollIntervalMs,
|
|
86
|
+
resumeFrom,
|
|
87
|
+
replayFailed: input.replayFailed,
|
|
88
|
+
onProgress: async (progress) => {
|
|
89
|
+
runState.progress = progress;
|
|
90
|
+
runState.updatedAt = nowIso();
|
|
91
|
+
await writeJson(paths.runPath, runState);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
runState.status = result.failed > 0 ? 'failed' : 'completed';
|
|
95
|
+
runState.completedAt = nowIso();
|
|
96
|
+
runState.updatedAt = nowIso();
|
|
97
|
+
runState.progress = result.progress;
|
|
98
|
+
if (result.failed > 0) {
|
|
99
|
+
const failedEntry = Object.values(result.progress).find((c) => c.status === 'failed');
|
|
100
|
+
runState.lastError = failedEntry?.error ?? 'One or more chunks failed';
|
|
101
|
+
}
|
|
102
|
+
await writeJson(paths.runPath, runState);
|
|
103
|
+
const summary = summarizeRunStatus(runState, paths.runPath, plan);
|
|
104
|
+
if (input.jsonMode) {
|
|
105
|
+
input.print({
|
|
106
|
+
ok: result.failed === 0,
|
|
107
|
+
planId: plan.planId,
|
|
108
|
+
status: runState.status,
|
|
109
|
+
chunkCounts: summary.totals,
|
|
110
|
+
rowsWritten: summary.rowsWritten,
|
|
111
|
+
runPath: paths.runPath,
|
|
112
|
+
lastError: runState.lastError,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
let line = `Backfill ${plan.planId}: ${runState.status} (done=${summary.totals.done}/${summary.totals.total}, ${summary.rowsWritten} rows written)`;
|
|
117
|
+
if (runState.lastError)
|
|
118
|
+
line += ` \u2014 ${runState.lastError}`;
|
|
119
|
+
input.print(line);
|
|
120
|
+
if (runState.status === 'completed' && summary.rowsWritten === 0) {
|
|
121
|
+
input.print('Warning: 0 rows written across all chunks. Verify that source data exists in the time range and passes the query\'s WHERE filters.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result.failed > 0 ? 1 : 0;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await db.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isRecord(value) {
|
|
131
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
function withFactoryDefaults(schema, defaults) {
|
|
134
|
+
return {
|
|
135
|
+
safeParse(data) {
|
|
136
|
+
return schema.safeParse({ ...defaults, ...(isRecord(data) ? data : {}) });
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
8
140
|
export function createBackfillPlugin(options = {}) {
|
|
9
|
-
const
|
|
10
|
-
|
|
141
|
+
const factoryOptions = options;
|
|
142
|
+
const planOptionsSchema = withFactoryDefaults(PlanSchema, factoryOptions);
|
|
143
|
+
const submitOptionsSchema = withFactoryDefaults(SubmitSchema, factoryOptions);
|
|
144
|
+
const runOptionsSchema = withFactoryDefaults(RunSchema, factoryOptions);
|
|
145
|
+
const resumeOptionsSchema = withFactoryDefaults(ResumeSchema, factoryOptions);
|
|
146
|
+
const statusOptionsSchema = withFactoryDefaults(StatusSchema, factoryOptions);
|
|
11
147
|
return {
|
|
12
148
|
manifest: {
|
|
13
149
|
name: 'backfill',
|
|
14
150
|
apiVersion: 1,
|
|
15
151
|
},
|
|
152
|
+
optionsSchema: withFactoryDefaults(PluginConfigSchema, factoryOptions),
|
|
16
153
|
commands: [
|
|
17
154
|
{
|
|
18
155
|
name: 'plan',
|
|
19
156
|
description: 'Build a deterministic backfill plan and persist immutable plan state',
|
|
20
157
|
flags: PLAN_FLAGS,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
158
|
+
optionsSchema: planOptionsSchema,
|
|
159
|
+
flagMapping: PLAN_FLAG_MAP,
|
|
160
|
+
run: async (context) => wrapPluginRun({
|
|
161
|
+
command: 'plan',
|
|
162
|
+
label: 'Backfill plan',
|
|
163
|
+
jsonMode: context.jsonMode,
|
|
164
|
+
print: context.print,
|
|
165
|
+
configErrorClass: BackfillConfigError,
|
|
166
|
+
fn: async () => {
|
|
167
|
+
const opts = context.options;
|
|
168
|
+
if (!context.config.clickhouse) {
|
|
169
|
+
throw new BackfillConfigError('ClickHouse connection is required for backfill planning. Configure clickhouse in your clickhouse.config.ts.');
|
|
170
|
+
}
|
|
171
|
+
const db = createClickHouseExecutor(context.config.clickhouse);
|
|
172
|
+
try {
|
|
32
173
|
const output = await buildBackfillPlan({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
config,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
174
|
+
opts,
|
|
175
|
+
configPath: context.configPath,
|
|
176
|
+
config: context.config,
|
|
177
|
+
clickhouse: context.config.clickhouse,
|
|
178
|
+
clickhouseQuery: async (sql, settings) => {
|
|
179
|
+
const result = await db.query(sql, settings);
|
|
180
|
+
return result;
|
|
181
|
+
},
|
|
182
|
+
// ObsessionDB enables parallel replicas by default, which inflates
|
|
183
|
+
// aggregate results (count, GROUP BY). Disable for planning queries
|
|
184
|
+
// until ObsessionDB handles it at the profile level.
|
|
185
|
+
querySettings: { enable_parallel_replicas: 0 },
|
|
41
186
|
});
|
|
42
187
|
const payload = planPayload(output);
|
|
43
|
-
if (jsonMode) {
|
|
44
|
-
print(payload);
|
|
188
|
+
if (context.jsonMode) {
|
|
189
|
+
context.print(payload);
|
|
45
190
|
}
|
|
46
191
|
else {
|
|
47
|
-
|
|
192
|
+
const partitionCount = output.plan.chunkPlan.partitions.length;
|
|
193
|
+
const totalBytes = formatBytes(output.plan.chunkPlan.totalBytesCompressed);
|
|
194
|
+
const primarySortKey = output.plan.chunkPlan.table.sortKeys[0];
|
|
195
|
+
const sortKeyLabel = primarySortKey
|
|
196
|
+
? `, sort key: ${primarySortKey.name} (${primarySortKey.category})`
|
|
197
|
+
: '';
|
|
198
|
+
context.print(`Backfill plan ${payload.planId} for ${payload.target} (${payload.chunkCount} chunks across ${partitionCount} partitions, ~${totalBytes}${sortKeyLabel}) -> ${payload.planPath}`);
|
|
48
199
|
}
|
|
49
200
|
return 0;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await db.close();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'submit',
|
|
210
|
+
description: 'Submit a backfill plan to a managed job backend (e.g. ObsessionDB) instead of running it locally',
|
|
211
|
+
flags: SUBMIT_FLAGS,
|
|
212
|
+
optionsSchema: submitOptionsSchema,
|
|
213
|
+
flagMapping: SUBMIT_FLAG_MAP,
|
|
214
|
+
run: async (context) => wrapPluginRun({
|
|
215
|
+
command: 'submit',
|
|
216
|
+
label: 'Backfill submit',
|
|
217
|
+
jsonMode: context.jsonMode,
|
|
218
|
+
print: context.print,
|
|
219
|
+
configErrorClass: BackfillConfigError,
|
|
220
|
+
fn: async () => {
|
|
221
|
+
// Reaching this handler means no managed backend intercepted the
|
|
222
|
+
// command. The ObsessionDB plugin handles `submit` via its
|
|
223
|
+
// onBeforePluginCommand hook when a service is selected; without
|
|
224
|
+
// one there is nowhere to submit to.
|
|
225
|
+
throw new BackfillConfigError('backfill submit requires a managed job backend. Log in and select an ObsessionDB service ' +
|
|
226
|
+
'(`chkit obsessiondb login`, then `chkit obsessiondb service select`), ' +
|
|
227
|
+
'or use `chkit backfill run --local` to execute the backfill against a direct connection.');
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
53
230
|
},
|
|
54
231
|
{
|
|
55
232
|
name: 'run',
|
|
56
|
-
description: 'Execute a planned backfill with
|
|
233
|
+
description: 'Execute a planned backfill with async query submission and polling',
|
|
57
234
|
flags: RUN_FLAGS,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const payload = {
|
|
86
|
-
...runPayload(output),
|
|
87
|
-
command: 'run',
|
|
88
|
-
};
|
|
89
|
-
if (jsonMode) {
|
|
90
|
-
print(payload);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
print(`Backfill run ${payload.planId}: ${payload.status} (done=${payload.chunkCounts.done}/${payload.chunkCounts.total})`);
|
|
94
|
-
}
|
|
95
|
-
return payload.ok ? 0 : 1;
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
},
|
|
235
|
+
optionsSchema: runOptionsSchema,
|
|
236
|
+
flagMapping: RUN_FLAG_MAP,
|
|
237
|
+
run: async (context) => wrapPluginRun({
|
|
238
|
+
command: 'run',
|
|
239
|
+
label: 'Backfill run',
|
|
240
|
+
jsonMode: context.jsonMode,
|
|
241
|
+
print: context.print,
|
|
242
|
+
configErrorClass: BackfillConfigError,
|
|
243
|
+
fn: async () => {
|
|
244
|
+
const opts = context.options;
|
|
245
|
+
if (!context.config.clickhouse) {
|
|
246
|
+
throw new BackfillConfigError('ClickHouse connection is required for backfill execution. Configure clickhouse in your clickhouse.config.ts.');
|
|
247
|
+
}
|
|
248
|
+
return runBackfill({
|
|
249
|
+
planId: opts.planId,
|
|
250
|
+
forceEnvironment: opts.forceEnvironment,
|
|
251
|
+
concurrency: opts.concurrency,
|
|
252
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
253
|
+
stateDir: opts.stateDir,
|
|
254
|
+
configPath: context.configPath,
|
|
255
|
+
config: context.config,
|
|
256
|
+
clickhouse: context.config.clickhouse,
|
|
257
|
+
print: context.print,
|
|
258
|
+
jsonMode: context.jsonMode,
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
99
262
|
},
|
|
100
263
|
{
|
|
101
264
|
name: 'resume',
|
|
102
265
|
description: 'Resume a backfill run from last checkpoint',
|
|
103
266
|
flags: RESUME_FLAGS,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (jsonMode) {
|
|
132
|
-
print(payload);
|
|
267
|
+
optionsSchema: resumeOptionsSchema,
|
|
268
|
+
flagMapping: RESUME_FLAG_MAP,
|
|
269
|
+
run: async (context) => wrapPluginRun({
|
|
270
|
+
command: 'resume',
|
|
271
|
+
label: 'Backfill resume',
|
|
272
|
+
jsonMode: context.jsonMode,
|
|
273
|
+
print: context.print,
|
|
274
|
+
configErrorClass: BackfillConfigError,
|
|
275
|
+
fn: async () => {
|
|
276
|
+
const opts = context.options;
|
|
277
|
+
if (!context.config.clickhouse) {
|
|
278
|
+
throw new BackfillConfigError('ClickHouse connection is required for backfill execution. Configure clickhouse in your clickhouse.config.ts.');
|
|
279
|
+
}
|
|
280
|
+
const { stateDir } = await readPlan({
|
|
281
|
+
planId: opts.planId,
|
|
282
|
+
configPath: context.configPath,
|
|
283
|
+
config: context.config,
|
|
284
|
+
stateDir: opts.stateDir,
|
|
285
|
+
});
|
|
286
|
+
const paths = backfillPaths(stateDir, opts.planId);
|
|
287
|
+
const existingRun = await readRun(paths.runPath);
|
|
288
|
+
if (!existingRun) {
|
|
289
|
+
throw new BackfillConfigError(`Run state not found for plan ${opts.planId}. Start with backfill run before resume.`);
|
|
290
|
+
}
|
|
291
|
+
if (existingRun.status === 'completed') {
|
|
292
|
+
if (context.jsonMode) {
|
|
293
|
+
context.print({ ok: true, noop: true, planId: opts.planId, status: 'completed', message: 'Run already completed. Nothing to resume.' });
|
|
133
294
|
}
|
|
134
295
|
else {
|
|
135
|
-
print(`Backfill
|
|
296
|
+
context.print(`Backfill ${opts.planId}: already completed. Nothing to resume.`);
|
|
136
297
|
}
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
return runBackfill({
|
|
301
|
+
planId: opts.planId,
|
|
302
|
+
forceEnvironment: opts.forceEnvironment,
|
|
303
|
+
concurrency: opts.concurrency,
|
|
304
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
305
|
+
stateDir: opts.stateDir,
|
|
306
|
+
resumeFrom: existingRun.progress,
|
|
307
|
+
replayFailed: opts.replayFailed,
|
|
308
|
+
configPath: context.configPath,
|
|
309
|
+
config: context.config,
|
|
310
|
+
clickhouse: context.config.clickhouse,
|
|
311
|
+
print: context.print,
|
|
312
|
+
jsonMode: context.jsonMode,
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
141
316
|
},
|
|
142
317
|
{
|
|
143
318
|
name: 'status',
|
|
144
319
|
description: 'Show checkpoint and chunk progress for a backfill run',
|
|
145
320
|
flags: PLAN_ID_FLAGS,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
321
|
+
optionsSchema: statusOptionsSchema,
|
|
322
|
+
flagMapping: PLAN_ID_FLAG_MAP,
|
|
323
|
+
run: async (context) => wrapPluginRun({
|
|
324
|
+
command: 'status',
|
|
325
|
+
label: 'Backfill status',
|
|
326
|
+
jsonMode: context.jsonMode,
|
|
327
|
+
print: context.print,
|
|
328
|
+
configErrorClass: BackfillConfigError,
|
|
329
|
+
fn: async () => {
|
|
330
|
+
const opts = context.options;
|
|
331
|
+
const summary = await getBackfillStatus({
|
|
332
|
+
planId: opts.planId,
|
|
333
|
+
config: context.config,
|
|
334
|
+
configPath: context.configPath,
|
|
335
|
+
stateDir: opts.stateDir,
|
|
336
|
+
});
|
|
337
|
+
const payload = statusPayload(summary);
|
|
338
|
+
if (context.jsonMode) {
|
|
339
|
+
context.print(payload);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
let line = `Backfill status ${payload.planId}: ${payload.status} (done=${payload.chunkCounts.done}/${payload.chunkCounts.total}, failed=${payload.chunkCounts.failed}, ${payload.rowsWritten} rows written)`;
|
|
343
|
+
if (payload.lastError)
|
|
344
|
+
line += ` \u2014 ${payload.lastError}`;
|
|
345
|
+
context.print(line);
|
|
346
|
+
if (payload.status === 'completed' && payload.rowsWritten === 0) {
|
|
347
|
+
context.print('Warning: 0 rows written across all chunks. Verify that source data exists in the time range and passes the query\'s WHERE filters.');
|
|
169
348
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
},
|
|
349
|
+
}
|
|
350
|
+
return payload.ok ? 0 : 1;
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
174
353
|
},
|
|
175
354
|
{
|
|
176
355
|
name: 'cancel',
|
|
177
356
|
description: 'Cancel an in-progress backfill run and prevent further chunk execution',
|
|
178
357
|
flags: PLAN_ID_FLAGS,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
},
|
|
358
|
+
optionsSchema: statusOptionsSchema,
|
|
359
|
+
flagMapping: PLAN_ID_FLAG_MAP,
|
|
360
|
+
run: async (context) => wrapPluginRun({
|
|
361
|
+
command: 'cancel',
|
|
362
|
+
label: 'Backfill cancel',
|
|
363
|
+
jsonMode: context.jsonMode,
|
|
364
|
+
print: context.print,
|
|
365
|
+
configErrorClass: BackfillConfigError,
|
|
366
|
+
fn: async () => {
|
|
367
|
+
const opts = context.options;
|
|
368
|
+
const summary = await cancelBackfillRun({
|
|
369
|
+
planId: opts.planId,
|
|
370
|
+
config: context.config,
|
|
371
|
+
configPath: context.configPath,
|
|
372
|
+
stateDir: opts.stateDir,
|
|
373
|
+
});
|
|
374
|
+
const payload = cancelPayload(summary);
|
|
375
|
+
if (context.jsonMode) {
|
|
376
|
+
context.print(payload);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
context.print(`Backfill cancel ${payload.planId}: ${payload.status} (done=${payload.chunkCounts.done}/${payload.chunkCounts.total})`);
|
|
380
|
+
}
|
|
381
|
+
return payload.ok ? 0 : 1;
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
207
384
|
},
|
|
208
385
|
{
|
|
209
386
|
name: 'doctor',
|
|
210
387
|
description: 'Provide actionable remediation steps for failed or pending backfill runs',
|
|
211
388
|
flags: PLAN_ID_FLAGS,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
print(`- ${recommendation}`);
|
|
237
|
-
}
|
|
389
|
+
optionsSchema: statusOptionsSchema,
|
|
390
|
+
flagMapping: PLAN_ID_FLAG_MAP,
|
|
391
|
+
run: async (context) => wrapPluginRun({
|
|
392
|
+
command: 'doctor',
|
|
393
|
+
label: 'Backfill doctor',
|
|
394
|
+
jsonMode: context.jsonMode,
|
|
395
|
+
print: context.print,
|
|
396
|
+
configErrorClass: BackfillConfigError,
|
|
397
|
+
fn: async () => {
|
|
398
|
+
const opts = context.options;
|
|
399
|
+
const report = await getBackfillDoctorReport({
|
|
400
|
+
planId: opts.planId,
|
|
401
|
+
config: context.config,
|
|
402
|
+
configPath: context.configPath,
|
|
403
|
+
stateDir: opts.stateDir,
|
|
404
|
+
});
|
|
405
|
+
const payload = doctorPayload(report);
|
|
406
|
+
if (context.jsonMode) {
|
|
407
|
+
context.print(payload);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
context.print(`Backfill doctor ${payload.planId}: ${payload.issueCodes.length === 0 ? 'ok' : payload.issueCodes.join(', ')}`);
|
|
411
|
+
for (const recommendation of payload.recommendations) {
|
|
412
|
+
context.print(`- ${recommendation}`);
|
|
238
413
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
},
|
|
414
|
+
}
|
|
415
|
+
return payload.ok ? 0 : 1;
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
243
418
|
},
|
|
244
419
|
],
|
|
245
420
|
hooks: {
|
|
246
|
-
onConfigLoaded(
|
|
247
|
-
|
|
248
|
-
validateBaseOptions(merged);
|
|
421
|
+
onConfigLoaded() {
|
|
422
|
+
// Plugin-level options validated softly at load time via PluginConfigSchema.
|
|
249
423
|
},
|
|
250
|
-
async onCheck({ config, configPath, options
|
|
251
|
-
const
|
|
252
|
-
validateBaseOptions(effectiveOptions);
|
|
424
|
+
async onCheck({ config: appConfig, configPath, options }) {
|
|
425
|
+
const opts = CheckSchema.parse(options);
|
|
253
426
|
return evaluateBackfillCheck({
|
|
254
427
|
configPath,
|
|
255
|
-
config,
|
|
256
|
-
|
|
428
|
+
config: appConfig,
|
|
429
|
+
stateDir: opts.stateDir,
|
|
430
|
+
failCheckOnRequiredPendingBackfill: opts.failCheckOnRequiredPendingBackfill,
|
|
257
431
|
});
|
|
258
432
|
},
|
|
259
433
|
onCheckReport({ result, print }) {
|
|
@@ -269,7 +443,7 @@ export function createBackfillPlugin(options = {}) {
|
|
|
269
443
|
}
|
|
270
444
|
export function backfill(options = {}) {
|
|
271
445
|
return {
|
|
272
|
-
plugin: createBackfillPlugin(),
|
|
446
|
+
plugin: createBackfillPlugin(options),
|
|
273
447
|
name: 'backfill',
|
|
274
448
|
enabled: true,
|
|
275
449
|
options,
|