1+ import { insertErrorServerText , insertErrorType } from "@internal/clickhouse" ;
12import { detectBadJsonStrings } from "~/utils/detectBadJsonStrings" ;
23
34/**
@@ -58,17 +59,12 @@ export type SanitizeResult = {
5859
5960/**
6061 * Recognises ClickHouse's "Cannot parse JSON object" rejection — the
61- * deterministic-failure class our sanitizer is designed for. Bubbles up
62- * from `@clickhouse/client` as an `InsertError` whose `.message` retains
63- * the original ClickHouse error text.
62+ * deterministic-failure class our sanitizer is designed for. Reads the full
63+ * server text, since `InsertError.message` only carries the error type.
6464 */
6565export function isClickHouseJsonParseError ( err : unknown ) : boolean {
6666 if ( ! err ) return false ;
67- const message =
68- typeof err === "object" && err !== null && "message" in err
69- ? String ( ( err as { message ?: unknown } ) . message ?? "" )
70- : String ( err ) ;
71- return message . includes ( "Cannot parse JSON object" ) ;
67+ return insertErrorServerText ( err ) . includes ( "Cannot parse JSON object" ) ;
7268}
7369
7470/**
@@ -183,19 +179,7 @@ export function sanitizeRows<T extends object>(rows: T[]): SanitizeResult {
183179 return result ;
184180}
185181
186- function errorMessage ( err : unknown ) : string {
187- return typeof err === "object" && err !== null && "message" in err
188- ? String ( ( err as { message ?: unknown } ) . message ?? "" )
189- : String ( err ) ;
190- }
191-
192- function rawErrorMessage ( err : unknown ) : string {
193- if ( typeof err === "object" && err !== null ) {
194- const raw = ( err as { rawMessage ?: unknown } ) . rawMessage ;
195- if ( typeof raw === "string" && raw . length > 0 ) return raw ;
196- }
197- return errorMessage ( err ) ;
198- }
182+ type RowIdAccessor < T > = ( row : T ) => string | undefined ;
199183
200184/**
201185 * `error` is part of the contract because losing a whole batch is a genuine
@@ -297,6 +281,7 @@ export async function insertWithLimitedStrip<T extends object>(params: {
297281 stripJsonColumns : ( row : T ) => T ;
298282 maxPoisonStrips ?: number ;
299283 hasMaterializedViews ?: boolean ;
284+ rowId ?: RowIdAccessor < T > ;
300285} ) : Promise < JsonParseRecoveryOutcome > {
301286 const { rows, contextLabel, logger, logContext, insert, insertSync, insertAllowingBadRows } =
302287 params ;
@@ -309,7 +294,7 @@ export async function insertWithLimitedStrip<T extends object>(params: {
309294 } catch ( firstError ) {
310295 if ( ! isClickHouseJsonParseError ( firstError ) ) throw firstError ;
311296
312- const firstMessage = errorMessage ( firstError ) ;
297+ const clickhouseErrorType = insertErrorType ( firstError ) ;
313298 const { rowsTouched, fieldsSanitized } = sanitizeRows ( rows ) ;
314299
315300 if ( fieldsSanitized > 0 ) {
@@ -319,7 +304,7 @@ export async function insertWithLimitedStrip<T extends object>(params: {
319304 batchSize : rows . length ,
320305 rowsTouched,
321306 fieldsSanitized,
322- clickhouseError : firstMessage . split ( "\n" ) [ 0 ] ,
307+ clickhouseErrorType ,
323308 } ) ;
324309
325310 try {
@@ -331,6 +316,8 @@ export async function insertWithLimitedStrip<T extends object>(params: {
331316
332317 const working = rows . slice ( ) ;
333318 const stripped = new Array ( working . length ) . fill ( false ) ;
319+ // Row hints come from insertSync (no parallel parsing), so these IDs are exact.
320+ const strippedRunIds : string [ ] = [ ] ;
334321 let rowsStripped = 0 ;
335322 let bailReason : RecoveryBailReason = "strip_attempts_exhausted" ;
336323
@@ -347,7 +334,8 @@ export async function insertWithLimitedStrip<T extends object>(params: {
347334 contextLabel,
348335 batchSize : rows . length ,
349336 rowsStripped,
350- clickhouseError : firstMessage . split ( "\n" ) [ 0 ] ,
337+ strippedRunIds,
338+ clickhouseErrorType,
351339 }
352340 ) ;
353341 }
@@ -368,14 +356,16 @@ export async function insertWithLimitedStrip<T extends object>(params: {
368356 break ;
369357 }
370358
371- const hint = parseStrippableRowNumber ( rawErrorMessage ( parseError ) ) ;
359+ const hint = parseStrippableRowNumber ( insertErrorServerText ( parseError ) ) ;
372360 const index = hint === null ? - 1 : hint - 1 ;
373361
374362 if ( index < 0 || index >= working . length || stripped [ index ] ) {
375363 bailReason = "row_not_locatable" ;
376364 break ;
377365 }
378366
367+ const runId = params . rowId ?.( working [ index ] ) ;
368+ if ( runId ) strippedRunIds . push ( runId ) ;
379369 working [ index ] = stripJsonColumns ( working [ index ] ) ;
380370 stripped [ index ] = true ;
381371 rowsStripped += 1 ;
@@ -395,7 +385,8 @@ export async function insertWithLimitedStrip<T extends object>(params: {
395385 rowsStripped,
396386 capped : true ,
397387 bailReason,
398- firstMessage,
388+ clickhouseErrorType,
389+ strippedRunIds,
399390 skipError,
400391 } ) ;
401392 }
@@ -411,7 +402,8 @@ export async function insertWithLimitedStrip<T extends object>(params: {
411402 rowsDropped : dropped . rows ,
412403 rowsDroppedExact : dropped . exact ,
413404 landedRows : writtenRowCount ( insertResult ) ,
414- clickhouseError : firstMessage . split ( "\n" ) [ 0 ] ,
405+ strippedRunIds,
406+ clickhouseErrorType,
415407 } ) ;
416408
417409 return {
@@ -464,7 +456,8 @@ function wholeBatchDropped<T extends object>(params: {
464456 rowsStripped : number ;
465457 capped : boolean ;
466458 bailReason ?: RecoveryBailReason ;
467- firstMessage : string ;
459+ clickhouseErrorType ?: string ;
460+ strippedRunIds ?: string [ ] ;
468461 skipError : unknown ;
469462} ) : JsonParseRecoveryOutcome {
470463 const { rows, contextLabel, logger, logContext, rowsStripped, capped, bailReason } = params ;
@@ -476,8 +469,9 @@ function wholeBatchDropped<T extends object>(params: {
476469 batchSize : rows . length ,
477470 rowsStripped,
478471 rowsDropped : rows . length ,
479- clickhouseError : params . firstMessage . split ( "\n" ) [ 0 ] ,
480- skipInsertError : errorMessage ( params . skipError ) . split ( "\n" ) [ 0 ] ,
472+ strippedRunIds : params . strippedRunIds ,
473+ clickhouseErrorType : params . clickhouseErrorType ,
474+ skipInsertErrorType : insertErrorType ( params . skipError ) ,
481475 } ) ;
482476
483477 return {
@@ -577,7 +571,7 @@ export async function insertWithBadRowSkip<T extends object>(params: {
577571 } catch ( firstError ) {
578572 if ( ! isParseError ( firstError ) ) throw firstError ;
579573
580- const firstMessage = errorMessage ( firstError ) ;
574+ const clickhouseErrorType = insertErrorType ( firstError ) ;
581575 const { rowsTouched, fieldsSanitized } = sanitizeRows ( rows ) ;
582576
583577 if ( fieldsSanitized > 0 ) {
@@ -587,7 +581,7 @@ export async function insertWithBadRowSkip<T extends object>(params: {
587581 batchSize : rows . length ,
588582 rowsTouched,
589583 fieldsSanitized,
590- clickhouseError : firstMessage . split ( "\n" ) [ 0 ] ,
584+ clickhouseErrorType ,
591585 } ) ;
592586
593587 try {
@@ -611,7 +605,7 @@ export async function insertWithBadRowSkip<T extends object>(params: {
611605 logContext,
612606 rowsStripped : 0 ,
613607 capped : false ,
614- firstMessage ,
608+ clickhouseErrorType ,
615609 skipError,
616610 } ) ;
617611 }
@@ -627,7 +621,7 @@ export async function insertWithBadRowSkip<T extends object>(params: {
627621 rowsDropped : dropped . rows ,
628622 rowsDroppedExact : dropped . exact ,
629623 landedRows : writtenRowCount ( insertResult ) ,
630- clickhouseError : firstMessage . split ( "\n" ) [ 0 ] ,
624+ clickhouseErrorType ,
631625 }
632626 ) ;
633627
0 commit comments