Skip to content

Commit 536731a

Browse files
matt-aitkenclaude
andauthored
feat(clickhouse): infer mixed-type JSON arrays as Array(Dynamic) on insert (#4095)
## ✅ Checklist - [ ] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [ ] I ran and tested the code works --- ## Testing `pnpm run typecheck --filter @internal/clickhouse` passes. This only adds a ClickHouse input-format setting to existing insert calls; the setting affects type inference for newly-inserted/merged data and is non-destructive to existing rows. --- ## Changelog Sets `input_format_json_infer_array_of_dynamic_from_array_of_different_types = 1` on every native-JSON insert path: - `task_runs_v2` (`output`, `error`) — `insertTaskRuns`, `insertTaskRunsCompactArrays`, and the async-insert variants - `task_events_v1` / `task_events_v2` (`attributes`) - `metrics_v1` - `sessions_v1` ### Why Our JSON columns contain arrays with mixed element types (e.g. `[{"key":"value"}, "string", "string"]`). With this setting off — which is the effective default under `24.12` compatibility — ClickHouse infers those as deeply nested unnamed `Tuple(JSON, Nullable(String), …)` types. ClickHouse 26.2 introduced `input_format_binary_max_type_complexity` (default 1000), and those tuple type trees exceeded the limit, causing background merges to fail with **Code 117**. With the setting on (the default since 25.8), mixed-type arrays are inferred as a single `Array(Dynamic)` — a simpler, flatter type representation that never approaches the complexity limit, even once the upstream default limit is restored. Setting this explicitly at insert time keeps behavior deterministic and version-controlled, so it does not depend on the server profile or a future compatibility bump. This is a forward-only change: it only affects newly inserted/merged data and does not rewrite existing parts. Our read path re-serializes these columns to strings (`toJSONString` via the materialized `*_text` columns), so the internal Tuple → Array(Dynamic) representation change is transparent to the application. ### Companion server-side setting To also apply this on the ClickHouse side (covers merges and any writes not going through these code paths), set it on the default user: ```sql ALTER USER default SETTINGS input_format_json_infer_array_of_dynamic_from_array_of_different_types = 1; ``` --- ## Screenshots _N/A_ 💯 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AaChyhestFMBYBWh6bgcCF --- _Generated by [Claude Code](https://claude.ai/code/session_01AaChyhestFMBYBWh6bgcCF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7851f5c commit 536731a

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Infer mixed-type JSON arrays as Array(Dynamic) instead of nested tuples when writing run, event, metric, and session data to avoid ClickHouse type-complexity merge failures

internal-packages/clickhouse/src/metrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function insertMetrics(ch: ClickhouseWriter) {
2222
settings: {
2323
enable_json_type: 1,
2424
type_json_skip_duplicated_paths: 1,
25+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
2526
input_format_json_throw_on_bad_escape_sequence: 0,
2627
input_format_json_use_string_type_for_ambiguous_paths_in_named_tuples_inference_from_objects: 1,
2728
},

internal-packages/clickhouse/src/sessions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export function insertSessionsCompactArrays(ch: ClickhouseWriter, settings?: Cli
118118
settings: {
119119
enable_json_type: 1,
120120
type_json_skip_duplicated_paths: 1,
121+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
121122
...settings,
122123
},
123124
});
@@ -131,6 +132,7 @@ export function insertSessions(ch: ClickhouseWriter, settings?: ClickHouseSettin
131132
settings: {
132133
enable_json_type: 1,
133134
type_json_skip_duplicated_paths: 1,
135+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
134136
...settings,
135137
},
136138
});

internal-packages/clickhouse/src/taskEvents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function insertTaskEvents(ch: ClickhouseWriter, settings?: ClickHouseSett
3131
settings: {
3232
enable_json_type: 1,
3333
type_json_skip_duplicated_paths: 1,
34+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
3435
input_format_json_throw_on_bad_escape_sequence: 0,
3536
input_format_json_use_string_type_for_ambiguous_paths_in_named_tuples_inference_from_objects: 1,
3637
...settings,
@@ -206,6 +207,7 @@ export function insertTaskEventsV2(ch: ClickhouseWriter, settings?: ClickHouseSe
206207
settings: {
207208
enable_json_type: 1,
208209
type_json_skip_duplicated_paths: 1,
210+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
209211
input_format_json_throw_on_bad_escape_sequence: 0,
210212
input_format_json_use_string_type_for_ambiguous_paths_in_named_tuples_inference_from_objects: 1,
211213
...settings,

internal-packages/clickhouse/src/taskRuns.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,111 @@ describe("Task Runs V2", () => {
156156
);
157157
});
158158

159+
clickhouseTest(
160+
"should insert and read back JSON arrays with mixed element types",
161+
async ({ clickhouseContainer }) => {
162+
// Regression test for input_format_json_infer_array_of_dynamic_from_array_of_different_types.
163+
// Arrays with mixed element types (e.g. [1, "hello", {...}, [...]]) must be inferred as
164+
// Array(Dynamic) rather than deeply nested Tuple types, which otherwise blow up the binary
165+
// type-complexity limit during background merges (ClickHouse Code 117).
166+
const client = new ClickhouseClient({
167+
name: "test",
168+
url: clickhouseContainer.getConnectionUrl(),
169+
});
170+
171+
const insert = insertTaskRunsCompactArrays(client, {
172+
async_insert: 0, // turn off async insert for this test
173+
});
174+
175+
const mixedArray = [1, "hello", { nested: "object" }, [1, 2, 3]];
176+
177+
const now = Date.now();
178+
const taskRunData: TaskRunInsertArray = [
179+
"env_mixed", // environment_id
180+
"org_mixed", // organization_id
181+
"project_mixed", // project_id
182+
"run_mixed", // run_id
183+
now, // updated_at
184+
now, // created_at
185+
"COMPLETED_SUCCESSFULLY", // status
186+
"DEVELOPMENT", // environment_type
187+
"friendly_mixed", // friendly_id
188+
1, // attempt
189+
"V2", // engine
190+
"my-task", // task_identifier
191+
"my-queue", // queue
192+
"", // schedule_id
193+
"", // batch_id
194+
null, // completed_at
195+
null, // started_at
196+
null, // executed_at
197+
null, // delay_until
198+
null, // queued_at
199+
null, // expired_at
200+
0, // usage_duration_ms
201+
0, // cost_in_cents
202+
0, // base_cost_in_cents
203+
{ data: { items: mixedArray } }, // output
204+
{ data: null }, // error
205+
"", // error_fingerprint
206+
[], // tags
207+
"", // task_version
208+
"", // sdk_version
209+
"", // cli_version
210+
"", // machine_preset
211+
"", // root_run_id
212+
"", // parent_run_id
213+
0, // depth
214+
"span_mixed", // span_id
215+
"trace_mixed", // trace_id
216+
"", // idempotency_key
217+
"", // idempotency_key_user
218+
"", // idempotency_key_scope
219+
"", // expiration_ttl
220+
true, // is_test
221+
"1", // _version
222+
0, // _is_deleted
223+
"", // concurrency_key
224+
[], // bulk_action_group_ids
225+
"", // worker_queue
226+
"", // region
227+
"", // plan_type
228+
null, // max_duration_in_seconds
229+
"", // trigger_source
230+
"", // root_trigger_source
231+
"", // task_kind
232+
null, // is_warm_start
233+
];
234+
235+
const [insertError, insertResult] = await insert([taskRunData]);
236+
237+
expect(insertError).toBeNull();
238+
expect(insertResult).toEqual(expect.objectContaining({ executed: true }));
239+
expect(insertResult?.summary?.written_rows).toEqual("1");
240+
241+
// output_text is a materialized String column that extracts the `data` field, so it
242+
// round-trips the mixed-type array back out as JSON regardless of the internal storage type.
243+
const query = client.query({
244+
name: "query-task-runs-mixed",
245+
query:
246+
"SELECT run_id, output_text FROM trigger_dev.task_runs_v2 WHERE run_id = {run_id: String}",
247+
schema: z.object({
248+
run_id: z.string(),
249+
output_text: z.string(),
250+
}),
251+
params: z.object({
252+
run_id: z.string(),
253+
}),
254+
});
255+
256+
const [queryError, result] = await query({ run_id: "run_mixed" });
257+
258+
expect(queryError).toBeNull();
259+
expect(result).toHaveLength(1);
260+
expect(JSON.parse(result![0].output_text)).toEqual({ items: mixedArray });
261+
}
262+
);
263+
159264
clickhouseTest("should deduplicate on the _version column", async ({ clickhouseContainer }) => {
160265
const client = new ClickhouseClient({
161266
name: "test",

internal-packages/clickhouse/src/taskRuns.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export function insertTaskRunsCompactArrays(ch: ClickhouseWriter, settings?: Cli
211211
settings: {
212212
enable_json_type: 1,
213213
type_json_skip_duplicated_paths: 1,
214+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
214215
...settings,
215216
},
216217
});
@@ -225,6 +226,7 @@ export function insertTaskRuns(ch: ClickhouseWriter, settings?: ClickHouseSettin
225226
settings: {
226227
enable_json_type: 1,
227228
type_json_skip_duplicated_paths: 1,
229+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
228230
...settings,
229231
},
230232
});
@@ -349,6 +351,7 @@ export function insertRawTaskRunPayloadsCompactArrays(
349351
async_insert_busy_timeout_ms: 1000,
350352
enable_json_type: 1,
351353
type_json_skip_duplicated_paths: 1,
354+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
352355
...settings,
353356
},
354357
});
@@ -367,6 +370,7 @@ export function insertRawTaskRunPayloads(ch: ClickhouseWriter, settings?: ClickH
367370
async_insert_busy_timeout_ms: 1000,
368371
enable_json_type: 1,
369372
type_json_skip_duplicated_paths: 1,
373+
input_format_json_infer_array_of_dynamic_from_array_of_different_types: 1,
370374
...settings,
371375
},
372376
});

0 commit comments

Comments
 (0)