@@ -599,6 +599,192 @@ describe("ClickHousePrinter", () => {
599599 } ) ;
600600 } ) ;
601601
602+ describe ( "textColumn optimization for JSON columns" , ( ) => {
603+ // Create a schema with JSON columns that have textColumn set
604+ const textColumnSchema : TableSchema = {
605+ name : "runs" ,
606+ clickhouseName : "trigger_dev.task_runs_v2" ,
607+ columns : {
608+ id : { name : "id" , ...column ( "String" ) } ,
609+ output : {
610+ name : "output" ,
611+ ...column ( "JSON" ) ,
612+ nullValue : "'{}'" ,
613+ textColumn : "output_text" ,
614+ } ,
615+ error : {
616+ name : "error" ,
617+ ...column ( "JSON" ) ,
618+ nullValue : "'{}'" ,
619+ textColumn : "error_text" ,
620+ } ,
621+ status : { name : "status" , ...column ( "String" ) } ,
622+ organization_id : { name : "organization_id" , ...column ( "String" ) } ,
623+ project_id : { name : "project_id" , ...column ( "String" ) } ,
624+ environment_id : { name : "environment_id" , ...column ( "String" ) } ,
625+ } ,
626+ tenantColumns : {
627+ organizationId : "organization_id" ,
628+ projectId : "project_id" ,
629+ environmentId : "environment_id" ,
630+ } ,
631+ } ;
632+
633+ function createTextColumnContext ( ) {
634+ const schema = createSchemaRegistry ( [ textColumnSchema ] ) ;
635+ return createPrinterContext ( {
636+ organizationId : "org_test" ,
637+ projectId : "proj_test" ,
638+ environmentId : "env_test" ,
639+ schema,
640+ } ) ;
641+ }
642+
643+ describe ( "SELECT clause" , ( ) => {
644+ it ( "should use text column when selecting bare JSON column" , ( ) => {
645+ const ctx = createTextColumnContext ( ) ;
646+ const { sql } = printQuery ( "SELECT output FROM runs" , ctx ) ;
647+
648+ // Should use the text column with an alias to preserve the column name
649+ expect ( sql ) . toContain ( "output_text AS output" ) ;
650+ } ) ;
651+
652+ it ( "should use text column for multiple JSON columns" , ( ) => {
653+ const ctx = createTextColumnContext ( ) ;
654+ const { sql } = printQuery ( "SELECT output, error FROM runs" , ctx ) ;
655+
656+ expect ( sql ) . toContain ( "output_text AS output" ) ;
657+ expect ( sql ) . toContain ( "error_text AS error" ) ;
658+ } ) ;
659+
660+ it ( "should use JSON column for subfield access" , ( ) => {
661+ const ctx = createTextColumnContext ( ) ;
662+ const { sql } = printQuery ( "SELECT output.data.name FROM runs" , ctx ) ;
663+
664+ // Should use the original JSON column with .:String type hint
665+ expect ( sql ) . toContain ( "output.data.name.:String" ) ;
666+ expect ( sql ) . not . toContain ( "output_text" ) ;
667+ } ) ;
668+ } ) ;
669+
670+ describe ( "SELECT * expansion" , ( ) => {
671+ it ( "should use text columns when expanding SELECT *" , ( ) => {
672+ const ctx = createTextColumnContext ( ) ;
673+ const { sql } = printQuery ( "SELECT * FROM runs" , ctx ) ;
674+
675+ // Should use text columns for JSON columns
676+ expect ( sql ) . toContain ( "output_text AS output" ) ;
677+ expect ( sql ) . toContain ( "error_text AS error" ) ;
678+ } ) ;
679+ } ) ;
680+
681+ describe ( "WHERE clause" , ( ) => {
682+ it ( "should use text column for exact equality comparison" , ( ) => {
683+ const ctx = createTextColumnContext ( ) ;
684+ const { sql } = printQuery ( "SELECT id FROM runs WHERE output = '{}'" , ctx ) ;
685+
686+ expect ( sql ) . toContain ( "equals(output_text," ) ;
687+ expect ( sql ) . not . toMatch ( / e q u a l s \( o u t p u t , / ) ;
688+ } ) ;
689+
690+ it ( "should use text column for inequality comparison" , ( ) => {
691+ const ctx = createTextColumnContext ( ) ;
692+ const { sql } = printQuery ( "SELECT id FROM runs WHERE output != '{}'" , ctx ) ;
693+
694+ expect ( sql ) . toContain ( "notEquals(output_text," ) ;
695+ } ) ;
696+
697+ it ( "should use text column for LIKE comparison" , ( ) => {
698+ const ctx = createTextColumnContext ( ) ;
699+ const { sql } = printQuery ( "SELECT id FROM runs WHERE output LIKE '%error%'" , ctx ) ;
700+
701+ expect ( sql ) . toContain ( "like(output_text," ) ;
702+ expect ( sql ) . not . toMatch ( / l i k e \( o u t p u t , / ) ;
703+ } ) ;
704+
705+ it ( "should use text column for ILIKE comparison" , ( ) => {
706+ const ctx = createTextColumnContext ( ) ;
707+ const { sql } = printQuery ( "SELECT id FROM runs WHERE error ILIKE '%failed%'" , ctx ) ;
708+
709+ expect ( sql ) . toContain ( "ilike(error_text," ) ;
710+ } ) ;
711+
712+ it ( "should use text column for NOT LIKE comparison" , ( ) => {
713+ const ctx = createTextColumnContext ( ) ;
714+ const { sql } = printQuery ( "SELECT id FROM runs WHERE output NOT LIKE '%test%'" , ctx ) ;
715+
716+ expect ( sql ) . toContain ( "notLike(output_text," ) ;
717+ } ) ;
718+
719+ it ( "should use JSON column for subfield comparison" , ( ) => {
720+ const ctx = createTextColumnContext ( ) ;
721+ const { sql } = printQuery (
722+ "SELECT id FROM runs WHERE output.data.name = 'test'" ,
723+ ctx
724+ ) ;
725+
726+ // Should use the original JSON column, not the text column
727+ expect ( sql ) . toContain ( "equals(output.data.name.:String," ) ;
728+ expect ( sql ) . not . toContain ( "output_text" ) ;
729+ } ) ;
730+
731+ it ( "should still use nullValue transformation for IS NULL" , ( ) => {
732+ const ctx = createTextColumnContext ( ) ;
733+ const { sql } = printQuery ( "SELECT id FROM runs WHERE output IS NULL" , ctx ) ;
734+
735+ // NULL check should use the text column with nullValue
736+ expect ( sql ) . toContain ( "equals(output_text, '{}')" ) ;
737+ } ) ;
738+
739+ it ( "should still use nullValue transformation for IS NOT NULL" , ( ) => {
740+ const ctx = createTextColumnContext ( ) ;
741+ const { sql } = printQuery ( "SELECT id FROM runs WHERE error IS NOT NULL" , ctx ) ;
742+
743+ expect ( sql ) . toContain ( "notEquals(error_text, '{}')" ) ;
744+ } ) ;
745+ } ) ;
746+
747+ describe ( "edge cases" , ( ) => {
748+ it ( "should work with columns without textColumn defined" , ( ) => {
749+ const ctx = createTextColumnContext ( ) ;
750+ const { sql } = printQuery ( "SELECT status FROM runs WHERE status = 'completed'" , ctx ) ;
751+
752+ // Regular column should work as before
753+ expect ( sql ) . toContain ( "status" ) ;
754+ expect ( sql ) . not . toContain ( "status_text" ) ;
755+ } ) ;
756+
757+ it ( "should use text column for aliased JSON columns in SELECT" , ( ) => {
758+ const ctx = createTextColumnContext ( ) ;
759+ const { sql } = printQuery ( "SELECT output AS result FROM runs" , ctx ) ;
760+
761+ // Should use text column with user's alias
762+ expect ( sql ) . toContain ( "output_text AS result" ) ;
763+ } ) ;
764+
765+ it ( "should use text column for table-qualified JSON columns in SELECT" , ( ) => {
766+ const ctx = createTextColumnContext ( ) ;
767+ const { sql } = printQuery ( "SELECT runs.output FROM runs" , ctx ) ;
768+
769+ // Should use text column
770+ expect ( sql ) . toContain ( "output_text AS output" ) ;
771+ } ) ;
772+
773+ it ( "should use text column in both SELECT and WHERE for same query" , ( ) => {
774+ const ctx = createTextColumnContext ( ) ;
775+ const { sql } = printQuery (
776+ "SELECT output FROM runs WHERE output LIKE '%test%'" ,
777+ ctx
778+ ) ;
779+
780+ // SELECT should use text column
781+ expect ( sql ) . toContain ( "output_text AS output" ) ;
782+ // WHERE should use text column
783+ expect ( sql ) . toContain ( "like(output_text," ) ;
784+ } ) ;
785+ } ) ;
786+ } ) ;
787+
602788 describe ( "ORDER BY clauses" , ( ) => {
603789 it ( "should print ORDER BY ASC" , ( ) => {
604790 const { sql } = printQuery ( "SELECT * FROM task_runs ORDER BY created_at ASC" ) ;
0 commit comments