@@ -841,6 +841,135 @@ describe("Value mapping (valueMap)", () => {
841841 } ) ;
842842} ) ;
843843
844+ describe ( "WHERE transform (whereTransform)" , ( ) => {
845+ /**
846+ * Schema with whereTransform for batch_id column (strips prefix)
847+ */
848+ const prefixedIdSchema : TableSchema = {
849+ name : "runs" ,
850+ clickhouseName : "trigger_dev.task_runs_v2" ,
851+ columns : {
852+ id : { name : "id" , ...column ( "String" ) } ,
853+ batch_id : {
854+ name : "batch_id" ,
855+ ...column ( "String" ) ,
856+ // Transform strips the "batch_" prefix from user input
857+ whereTransform : ( value : string ) => value . replace ( / ^ b a t c h _ / , "" ) ,
858+ // Expression adds the prefix back in SELECT
859+ expression : "if(batch_id = '', NULL, concat('batch_', batch_id))" ,
860+ } ,
861+ schedule_id : {
862+ name : "schedule_id" ,
863+ ...column ( "String" ) ,
864+ // Transform strips the "sched_" prefix
865+ whereTransform : ( value : string ) => value . replace ( / ^ s c h e d _ / , "" ) ,
866+ } ,
867+ organization_id : { name : "organization_id" , ...column ( "String" ) } ,
868+ project_id : { name : "project_id" , ...column ( "String" ) } ,
869+ environment_id : { name : "environment_id" , ...column ( "String" ) } ,
870+ } ,
871+ tenantColumns : {
872+ organizationId : "organization_id" ,
873+ projectId : "project_id" ,
874+ environmentId : "environment_id" ,
875+ } ,
876+ } ;
877+
878+ function createPrefixedContext ( ) {
879+ const schema = createSchemaRegistry ( [ prefixedIdSchema ] ) ;
880+ return createPrinterContext ( {
881+ organizationId : "org_test123" ,
882+ projectId : "proj_test456" ,
883+ environmentId : "env_test789" ,
884+ schema,
885+ } ) ;
886+ }
887+
888+ it ( "should strip prefix from value in equality comparison" , ( ) => {
889+ const ctx = createPrefixedContext ( ) ;
890+ const { params } = printQuery ( "SELECT * FROM runs WHERE batch_id = 'batch_abc123'" , ctx ) ;
891+
892+ // The "batch_" prefix should be stripped, leaving just "abc123"
893+ expect ( Object . values ( params ) ) . toContain ( "abc123" ) ;
894+ expect ( Object . values ( params ) ) . not . toContain ( "batch_abc123" ) ;
895+ } ) ;
896+
897+ it ( "should strip prefix from values in IN clause" , ( ) => {
898+ const ctx = createPrefixedContext ( ) ;
899+ const { params } = printQuery (
900+ "SELECT * FROM runs WHERE batch_id IN ('batch_abc', 'batch_def', 'batch_ghi')" ,
901+ ctx
902+ ) ;
903+
904+ // All prefixes should be stripped
905+ expect ( Object . values ( params ) ) . toContain ( "abc" ) ;
906+ expect ( Object . values ( params ) ) . toContain ( "def" ) ;
907+ expect ( Object . values ( params ) ) . toContain ( "ghi" ) ;
908+ expect ( Object . values ( params ) ) . not . toContain ( "batch_abc" ) ;
909+ } ) ;
910+
911+ it ( "should strip prefix from value in NOT IN clause" , ( ) => {
912+ const ctx = createPrefixedContext ( ) ;
913+ const { params } = printQuery ( "SELECT * FROM runs WHERE batch_id NOT IN ('batch_xyz')" , ctx ) ;
914+
915+ expect ( Object . values ( params ) ) . toContain ( "xyz" ) ;
916+ expect ( Object . values ( params ) ) . not . toContain ( "batch_xyz" ) ;
917+ } ) ;
918+
919+ it ( "should strip prefix from value in != comparison" , ( ) => {
920+ const ctx = createPrefixedContext ( ) ;
921+ const { params } = printQuery ( "SELECT * FROM runs WHERE batch_id != 'batch_test'" , ctx ) ;
922+
923+ expect ( Object . values ( params ) ) . toContain ( "test" ) ;
924+ expect ( Object . values ( params ) ) . not . toContain ( "batch_test" ) ;
925+ } ) ;
926+
927+ it ( "should handle values without the prefix (pass through unchanged)" , ( ) => {
928+ const ctx = createPrefixedContext ( ) ;
929+ const { params } = printQuery ( "SELECT * FROM runs WHERE batch_id = 'raw_value'" , ctx ) ;
930+
931+ // If no prefix to strip, the value passes through unchanged
932+ expect ( Object . values ( params ) ) . toContain ( "raw_value" ) ;
933+ } ) ;
934+
935+ it ( "should not transform values for columns without whereTransform" , ( ) => {
936+ const ctx = createPrefixedContext ( ) ;
937+ const { params } = printQuery ( "SELECT * FROM runs WHERE id = 'batch_abc123'" , ctx ) ;
938+
939+ // 'id' column has no whereTransform, so value passes through unchanged
940+ expect ( Object . values ( params ) ) . toContain ( "batch_abc123" ) ;
941+ } ) ;
942+
943+ it ( "should use expression for SELECT output (virtual column)" , ( ) => {
944+ const ctx = createPrefixedContext ( ) ;
945+ const { sql } = printQuery ( "SELECT batch_id FROM runs" , ctx ) ;
946+
947+ // The expression should be used in SELECT
948+ expect ( sql ) . toContain ( "if(batch_id = '', NULL, concat('batch_', batch_id))" ) ;
949+ } ) ;
950+
951+ it ( "should work with different prefix patterns" , ( ) => {
952+ const ctx = createPrefixedContext ( ) ;
953+ const { params } = printQuery ( "SELECT * FROM runs WHERE schedule_id = 'sched_xyz789'" , ctx ) ;
954+
955+ // The "sched_" prefix should be stripped
956+ expect ( Object . values ( params ) ) . toContain ( "xyz789" ) ;
957+ expect ( Object . values ( params ) ) . not . toContain ( "sched_xyz789" ) ;
958+ } ) ;
959+
960+ it ( "should transform values in tuple/array for IN expressions" , ( ) => {
961+ const ctx = createPrefixedContext ( ) ;
962+ const { params } = printQuery (
963+ "SELECT * FROM runs WHERE batch_id IN ['batch_a', 'batch_b']" ,
964+ ctx
965+ ) ;
966+
967+ // Both prefixes should be stripped
968+ expect ( Object . values ( params ) ) . toContain ( "a" ) ;
969+ expect ( Object . values ( params ) ) . toContain ( "b" ) ;
970+ } ) ;
971+ } ) ;
972+
844973describe ( "Edge cases" , ( ) => {
845974 it ( "should handle empty string values" , ( ) => {
846975 const { sql, params } = printQuery ( "SELECT * FROM task_runs WHERE status = ''" ) ;
@@ -1180,10 +1309,7 @@ describe("Expression columns with division (cost/invocation_cost pattern)", () =
11801309
11811310 it ( "should expand compute_cost in BETWEEN correctly" , ( ) => {
11821311 const ctx = createCostExpressionContext ( ) ;
1183- const { sql } = printQuery (
1184- "SELECT * FROM runs WHERE compute_cost BETWEEN 1.0 AND 10.0" ,
1185- ctx
1186- ) ;
1312+ const { sql } = printQuery ( "SELECT * FROM runs WHERE compute_cost BETWEEN 1.0 AND 10.0" , ctx ) ;
11871313
11881314 expect ( sql ) . toContain ( "(cost_in_cents / 100.0) BETWEEN 1 AND 10" ) ;
11891315 } ) ;
@@ -1564,10 +1690,7 @@ describe("Column metadata", () => {
15641690
15651691 it ( "should generate implicit names for multiple aggregations without aliases" , ( ) => {
15661692 const ctx = createMetadataTestContext ( ) ;
1567- const { columns } = printQuery (
1568- "SELECT COUNT(), status FROM runs GROUP BY status" ,
1569- ctx
1570- ) ;
1693+ const { columns } = printQuery ( "SELECT COUNT(), status FROM runs GROUP BY status" , ctx ) ;
15711694
15721695 expect ( columns ) . toHaveLength ( 2 ) ;
15731696 expect ( columns [ 0 ] . name ) . toBe ( "count" ) ;
@@ -1578,10 +1701,7 @@ describe("Column metadata", () => {
15781701
15791702 it ( "should generate implicit name for arithmetic expressions" , ( ) => {
15801703 const ctx = createMetadataTestContext ( ) ;
1581- const { columns } = printQuery (
1582- "SELECT usage_duration_ms + 100 FROM runs" ,
1583- ctx
1584- ) ;
1704+ const { columns } = printQuery ( "SELECT usage_duration_ms + 100 FROM runs" , ctx ) ;
15851705
15861706 expect ( columns ) . toHaveLength ( 1 ) ;
15871707 expect ( columns [ 0 ] . name ) . toBe ( "plus" ) ;
@@ -1611,10 +1731,7 @@ describe("Column metadata", () => {
16111731
16121732 it ( "should add AS clause to generated SQL for implicit names" , ( ) => {
16131733 const ctx = createMetadataTestContext ( ) ;
1614- const { sql, columns } = printQuery (
1615- "SELECT COUNT(), status FROM runs GROUP BY status" ,
1616- ctx
1617- ) ;
1734+ const { sql, columns } = printQuery ( "SELECT COUNT(), status FROM runs GROUP BY status" , ctx ) ;
16181735
16191736 // The SQL should include an explicit AS clause for the COUNT()
16201737 expect ( sql ) . toContain ( "count() AS count" ) ;
@@ -1731,10 +1848,7 @@ describe("Field Mapping Value Transformation", () => {
17311848 it ( "should pass through unmapped values unchanged" , ( ) => {
17321849 const ctx = createFieldMappingContext ( ) ;
17331850 // Using a value that's not in the mapping
1734- const { params } = printQuery (
1735- "SELECT run_id FROM runs WHERE project_ref = 'unknown-ref'" ,
1736- ctx
1737- ) ;
1851+ const { params } = printQuery ( "SELECT run_id FROM runs WHERE project_ref = 'unknown-ref'" , ctx ) ;
17381852
17391853 // Value should remain unchanged since it's not in the mapping
17401854 expect ( Object . values ( params ) ) . toContain ( "unknown-ref" ) ;
0 commit comments