@@ -441,6 +441,112 @@ describe("ClickHousePrinter", () => {
441441 } ) ;
442442 } ) ;
443443
444+ describe ( "nullValue transformation for JSON columns" , ( ) => {
445+ // Create a schema with JSON columns that have nullValue set
446+ const jsonSchema : TableSchema = {
447+ name : "runs" ,
448+ clickhouseName : "trigger_dev.task_runs_v2" ,
449+ columns : {
450+ id : { name : "id" , ...column ( "String" ) } ,
451+ error : {
452+ name : "error" ,
453+ ...column ( "JSON" ) ,
454+ nullValue : "'{}'" , // Empty object represents NULL
455+ } ,
456+ output : {
457+ name : "output" ,
458+ ...column ( "JSON" ) ,
459+ nullValue : "'{}'" , // Empty object represents NULL
460+ } ,
461+ status : { name : "status" , ...column ( "String" ) } ,
462+ organization_id : { name : "organization_id" , ...column ( "String" ) } ,
463+ project_id : { name : "project_id" , ...column ( "String" ) } ,
464+ environment_id : { name : "environment_id" , ...column ( "String" ) } ,
465+ } ,
466+ tenantColumns : {
467+ organizationId : "organization_id" ,
468+ projectId : "project_id" ,
469+ environmentId : "environment_id" ,
470+ } ,
471+ } ;
472+
473+ function createJsonContext ( ) {
474+ const schema = createSchemaRegistry ( [ jsonSchema ] ) ;
475+ return createPrinterContext ( {
476+ organizationId : "org_test" ,
477+ projectId : "proj_test" ,
478+ environmentId : "env_test" ,
479+ schema,
480+ } ) ;
481+ }
482+
483+ it ( "should transform IS NULL to equals empty object for JSON columns with nullValue" , ( ) => {
484+ const ctx = createJsonContext ( ) ;
485+ const { sql } = printQuery ( "SELECT * FROM runs WHERE error IS NULL" , ctx ) ;
486+
487+ // Should use equals with '{}' instead of isNull
488+ expect ( sql ) . toContain ( "equals(error, '{}')" ) ;
489+ expect ( sql ) . not . toContain ( "isNull(error)" ) ;
490+ } ) ;
491+
492+ it ( "should transform IS NOT NULL to notEquals empty object for JSON columns with nullValue" , ( ) => {
493+ const ctx = createJsonContext ( ) ;
494+ const { sql } = printQuery ( "SELECT * FROM runs WHERE error IS NOT NULL" , ctx ) ;
495+
496+ // Should use notEquals with '{}' instead of isNotNull
497+ expect ( sql ) . toContain ( "notEquals(error, '{}')" ) ;
498+ expect ( sql ) . not . toContain ( "isNotNull(error)" ) ;
499+ } ) ;
500+
501+ it ( "should transform = NULL to equals empty object for JSON columns with nullValue" , ( ) => {
502+ const ctx = createJsonContext ( ) ;
503+ const { sql } = printQuery ( "SELECT * FROM runs WHERE error = NULL" , ctx ) ;
504+
505+ expect ( sql ) . toContain ( "equals(error, '{}')" ) ;
506+ expect ( sql ) . not . toContain ( "isNull(error)" ) ;
507+ } ) ;
508+
509+ it ( "should transform != NULL to notEquals empty object for JSON columns with nullValue" , ( ) => {
510+ const ctx = createJsonContext ( ) ;
511+ const { sql } = printQuery ( "SELECT * FROM runs WHERE error != NULL" , ctx ) ;
512+
513+ expect ( sql ) . toContain ( "notEquals(error, '{}')" ) ;
514+ expect ( sql ) . not . toContain ( "isNotNull(error)" ) ;
515+ } ) ;
516+
517+ it ( "should not affect regular columns without nullValue" , ( ) => {
518+ const ctx = createJsonContext ( ) ;
519+ const { sql } = printQuery ( "SELECT * FROM runs WHERE status IS NULL" , ctx ) ;
520+
521+ // Regular column should still use isNull
522+ expect ( sql ) . toContain ( "isNull(status)" ) ;
523+ } ) ;
524+
525+ it ( "should work with multiple JSON column NULL checks" , ( ) => {
526+ const ctx = createJsonContext ( ) ;
527+ const { sql } = printQuery (
528+ "SELECT * FROM runs WHERE error IS NOT NULL AND output IS NULL" ,
529+ ctx
530+ ) ;
531+
532+ expect ( sql ) . toContain ( "notEquals(error, '{}')" ) ;
533+ expect ( sql ) . toContain ( "equals(output, '{}')" ) ;
534+ } ) ;
535+
536+ it ( "should allow GROUP BY on JSON columns without error" , ( ) => {
537+ const ctx = createJsonContext ( ) ;
538+ const { sql } = printQuery (
539+ "SELECT error, count() AS error_count FROM runs WHERE error IS NOT NULL GROUP BY error" ,
540+ ctx
541+ ) ;
542+
543+ // Should filter with notEquals
544+ expect ( sql ) . toContain ( "notEquals(error, '{}')" ) ;
545+ // Should group by the raw column
546+ expect ( sql ) . toContain ( "GROUP BY error" ) ;
547+ } ) ;
548+ } ) ;
549+
444550 describe ( "ORDER BY clauses" , ( ) => {
445551 it ( "should print ORDER BY ASC" , ( ) => {
446552 const { sql } = printQuery ( "SELECT * FROM task_runs ORDER BY created_at ASC" ) ;
@@ -1348,9 +1454,7 @@ describe("Virtual columns", () => {
13481454 // GROUP BY should use the alias, not the expression (ClickHouse allows this)
13491455 expect ( sql ) . toContain ( "GROUP BY is_long_running" ) ;
13501456 // Should NOT have the expression in GROUP BY
1351- expect ( sql ) . not . toContain (
1352- "GROUP BY (if(completed_at IS NOT NULL AND started_at IS NOT NULL"
1353- ) ;
1457+ expect ( sql ) . not . toContain ( "GROUP BY (if(completed_at IS NOT NULL AND started_at IS NOT NULL" ) ;
13541458 } ) ;
13551459
13561460 it ( "should use alias for virtual column in GROUP BY with WHERE and aggregation" , ( ) => {
@@ -1369,9 +1473,7 @@ describe("Virtual columns", () => {
13691473 // Should NOT have the expression in GROUP BY
13701474 expect ( sql ) . not . toContain ( "GROUP BY (dateDiff('millisecond'" ) ;
13711475 // WHERE should still use the expression
1372- expect ( sql ) . toContain (
1373- "isNotNull((dateDiff('millisecond', started_at, completed_at)))"
1374- ) ;
1476+ expect ( sql ) . toContain ( "isNotNull((dateDiff('millisecond', started_at, completed_at)))" ) ;
13751477 } ) ;
13761478 } ) ;
13771479
0 commit comments