@@ -11,8 +11,10 @@ import {
1111 getExternalValue ,
1212 getInternalValueFromMapping ,
1313 getInternalValueFromMappingCaseInsensitive ,
14+ sanitizeErrorMessage ,
1415 type ColumnSchema ,
1516 type FieldMappings ,
17+ type TableSchema ,
1618} from "./schema.js" ;
1719
1820describe ( "Value mapping helper functions" , ( ) => {
@@ -363,7 +365,9 @@ describe("Field mapping helper functions (runtime dynamic mappings)", () => {
363365 } ) ;
364366
365367 it ( "should return null if mapping name does not exist" , ( ) => {
366- expect ( getInternalValueFromMapping ( fieldMappings , "nonexistent" , "my-project-ref" ) ) . toBeNull ( ) ;
368+ expect (
369+ getInternalValueFromMapping ( fieldMappings , "nonexistent" , "my-project-ref" )
370+ ) . toBeNull ( ) ;
367371 } ) ;
368372
369373 it ( "should return null for empty mappings" , ( ) => {
@@ -454,3 +458,161 @@ describe("Field mapping helper functions (runtime dynamic mappings)", () => {
454458 } ) ;
455459} ) ;
456460
461+ describe ( "Error message sanitization" , ( ) => {
462+ // Test schema mimicking the real runs schema
463+ const runsSchema : TableSchema = {
464+ name : "runs" ,
465+ clickhouseName : "trigger_dev.task_runs_v2" ,
466+ description : "Task runs table" ,
467+ tenantColumns : {
468+ organizationId : "organization_id" ,
469+ projectId : "project_id" ,
470+ environmentId : "environment_id" ,
471+ } ,
472+ columns : {
473+ run_id : {
474+ name : "run_id" ,
475+ clickhouseName : "friendly_id" ,
476+ ...column ( "String" ) ,
477+ } ,
478+ triggered_at : {
479+ name : "triggered_at" ,
480+ clickhouseName : "created_at" ,
481+ ...column ( "DateTime64" ) ,
482+ } ,
483+ machine : {
484+ name : "machine" ,
485+ clickhouseName : "machine_preset" ,
486+ ...column ( "String" ) ,
487+ } ,
488+ status : {
489+ name : "status" ,
490+ // No clickhouseName - same as name
491+ ...column ( "String" ) ,
492+ } ,
493+ task_identifier : {
494+ name : "task_identifier" ,
495+ // No clickhouseName - same as name
496+ ...column ( "String" ) ,
497+ } ,
498+ } ,
499+ } ;
500+
501+ describe ( "sanitizeErrorMessage" , ( ) => {
502+ it ( "should replace fully qualified table.column references" , ( ) => {
503+ const error = "Missing column trigger_dev.task_runs_v2.friendly_id in query" ;
504+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
505+ expect ( sanitized ) . toBe ( "Missing column runs.run_id in query" ) ;
506+ } ) ;
507+
508+ it ( "should replace standalone table names" , ( ) => {
509+ const error = "Table trigger_dev.task_runs_v2 does not exist" ;
510+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
511+ expect ( sanitized ) . toBe ( "Table runs does not exist" ) ;
512+ } ) ;
513+
514+ it ( "should replace standalone column names with different clickhouseName" , ( ) => {
515+ const error = "Unknown identifier: friendly_id" ;
516+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
517+ expect ( sanitized ) . toBe ( "Unknown identifier: run_id" ) ;
518+ } ) ;
519+
520+ it ( "should replace multiple occurrences in the same message" , ( ) => {
521+ const error =
522+ "Cannot compare friendly_id with created_at: incompatible types in trigger_dev.task_runs_v2" ;
523+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
524+ expect ( sanitized ) . toBe ( "Cannot compare run_id with triggered_at: incompatible types in runs" ) ;
525+ } ) ;
526+
527+ it ( "should not replace column names that have no clickhouseName mapping" , ( ) => {
528+ const error = "Invalid value for column status" ;
529+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
530+ expect ( sanitized ) . toBe ( "Invalid value for column status" ) ;
531+ } ) ;
532+
533+ it ( "should handle error messages with quoted identifiers" , ( ) => {
534+ const error = "Column 'machine_preset' is not of type String" ;
535+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
536+ expect ( sanitized ) . toBe ( "Column 'machine' is not of type String" ) ;
537+ } ) ;
538+
539+ it ( "should handle error messages with backtick identifiers" , ( ) => {
540+ const error = "Unknown column `friendly_id` in table" ;
541+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
542+ expect ( sanitized ) . toBe ( "Unknown column `run_id` in table" ) ;
543+ } ) ;
544+
545+ it ( "should not replace partial matches within larger identifiers" , ( ) => {
546+ const error = "Column my_friendly_id_column not found" ;
547+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
548+ // Should not replace "friendly_id" within "my_friendly_id_column"
549+ expect ( sanitized ) . toBe ( "Column my_friendly_id_column not found" ) ;
550+ } ) ;
551+
552+ it ( "should return original message if no schemas provided" , ( ) => {
553+ const error = "Some error with trigger_dev.task_runs_v2" ;
554+ const sanitized = sanitizeErrorMessage ( error , [ ] ) ;
555+ expect ( sanitized ) . toBe ( "Some error with trigger_dev.task_runs_v2" ) ;
556+ } ) ;
557+
558+ it ( "should return original message if no matches found" , ( ) => {
559+ const error = "Generic database error occurred" ;
560+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
561+ expect ( sanitized ) . toBe ( "Generic database error occurred" ) ;
562+ } ) ;
563+
564+ it ( "should handle multiple tables" , ( ) => {
565+ const eventsSchema : TableSchema = {
566+ name : "events" ,
567+ clickhouseName : "trigger_dev.task_events" ,
568+ description : "Task events table" ,
569+ tenantColumns : {
570+ organizationId : "organization_id" ,
571+ projectId : "project_id" ,
572+ environmentId : "environment_id" ,
573+ } ,
574+ columns : {
575+ event_id : {
576+ name : "event_id" ,
577+ clickhouseName : "internal_event_id" ,
578+ ...column ( "String" ) ,
579+ } ,
580+ } ,
581+ } ;
582+
583+ const error =
584+ "Cannot join trigger_dev.task_runs_v2 with trigger_dev.task_events on internal_event_id" ;
585+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema , eventsSchema ] ) ;
586+ expect ( sanitized ) . toBe ( "Cannot join runs with events on event_id" ) ;
587+ } ) ;
588+
589+ it ( "should handle real ClickHouse error format" , ( ) => {
590+ const error =
591+ "Unable to query clickhouse: Code: 47. DB::Exception: Missing columns: 'friendly_id' while processing query" ;
592+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
593+ expect ( sanitized ) . toBe (
594+ "Unable to query clickhouse: Code: 47. DB::Exception: Missing columns: 'run_id' while processing query"
595+ ) ;
596+ } ) ;
597+
598+ it ( "should handle error with column in parentheses" , ( ) => {
599+ const error = "Function count(friendly_id) expects different arguments" ;
600+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
601+ expect ( sanitized ) . toBe ( "Function count(run_id) expects different arguments" ) ;
602+ } ) ;
603+
604+ it ( "should handle error with column after comma" , ( ) => {
605+ const error = "SELECT friendly_id, created_at FROM table" ;
606+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
607+ expect ( sanitized ) . toBe ( "SELECT run_id, triggered_at FROM table" ) ;
608+ } ) ;
609+
610+ it ( "should prioritize longer matches (table.column before standalone column)" , ( ) => {
611+ // This tests that we replace "trigger_dev.task_runs_v2.friendly_id" as a unit,
612+ // not "trigger_dev.task_runs_v2" and then "friendly_id" separately
613+ const error = "Error in trigger_dev.task_runs_v2.friendly_id" ;
614+ const sanitized = sanitizeErrorMessage ( error , [ runsSchema ] ) ;
615+ expect ( sanitized ) . toBe ( "Error in runs.run_id" ) ;
616+ } ) ;
617+ } ) ;
618+ } ) ;
0 commit comments