@@ -497,6 +497,8 @@ export class ClickHousePrinter {
497497
498498 // Extract output name and source column before visiting
499499 const { outputName, sourceColumn, inferredType } = this . analyzeSelectColumn ( col ) ;
500+ // effectiveOutputName may be overridden for JSON subfields
501+ let effectiveOutputName = outputName ;
500502
501503 // Check if this is a bare Field (not wrapped in Alias)
502504 let sqlResult : string ;
@@ -513,9 +515,19 @@ export class ClickHousePrinter {
513515 // Visit the field to get the ClickHouse SQL
514516 const visited = this . visit ( col ) ;
515517
518+ // Check if this is a JSON subfield access (will have .:String type hint)
519+ // If so, add an alias to preserve the nice column name (dots → underscores)
520+ const isJsonSubfield = this . isJsonSubfieldAccess ( field . chain ) ;
521+ if ( isJsonSubfield ) {
522+ // Build the alias using underscores (e.g., "error_data_name")
523+ const aliasName = field . chain . filter ( ( p ) : p is string => typeof p === "string" ) . join ( "_" ) ;
524+ sqlResult = `${ visited } AS ${ this . printIdentifier ( aliasName ) } ` ;
525+ // Override output name for metadata
526+ effectiveOutputName = aliasName ;
527+ }
516528 // Check if the column has a different clickhouseName - if so, add an alias
517529 // to ensure results come back with the user-facing name
518- if (
530+ else if (
519531 outputName &&
520532 sourceColumn ?. clickhouseName &&
521533 sourceColumn . clickhouseName !== outputName
@@ -546,9 +558,9 @@ export class ClickHousePrinter {
546558 }
547559
548560 // Collect metadata for top-level queries
549- if ( collectMetadata && outputName ) {
561+ if ( collectMetadata && effectiveOutputName ) {
550562 const metadata : OutputColumnMetadata = {
551- name : outputName ,
563+ name : effectiveOutputName ,
552564 type : sourceColumn ?. type ?? inferredType ?? "String" ,
553565 } ;
554566
@@ -1882,7 +1894,21 @@ export class ClickHousePrinter {
18821894 const resolvedChain = this . resolveFieldChain ( node . chain ) ;
18831895
18841896 // Print each chain element
1885- return resolvedChain . map ( ( part ) => this . printIdentifierOrIndex ( part ) ) . join ( "." ) ;
1897+ let result = resolvedChain . map ( ( part ) => this . printIdentifierOrIndex ( part ) ) . join ( "." ) ;
1898+
1899+ // For JSON column subfield access (e.g., error.data.name), add .:String type hint
1900+ // This is required because ClickHouse's Dynamic/Variant types are not allowed in
1901+ // GROUP BY without type casting, and SELECT/GROUP BY expressions must match
1902+ if ( resolvedChain . length > 1 ) {
1903+ // Check if the root column (first part) is a JSON column
1904+ const rootColumnSchema = this . resolveFieldToColumnSchema ( [ node . chain [ 0 ] ] ) ;
1905+ if ( rootColumnSchema ?. type === "JSON" ) {
1906+ // Add .:String type hint for JSON subfield access
1907+ result = `${ result } .:String` ;
1908+ }
1909+ }
1910+
1911+ return result ;
18861912 }
18871913
18881914 /**
@@ -1934,6 +1960,17 @@ export class ClickHousePrinter {
19341960 return chain ;
19351961 }
19361962
1963+ /**
1964+ * Check if a field chain represents JSON subfield access (e.g., error.data.name)
1965+ * Returns true if the root column is JSON type and there are additional path parts
1966+ */
1967+ private isJsonSubfieldAccess ( chain : Array < string | number > ) : boolean {
1968+ if ( chain . length <= 1 ) return false ;
1969+
1970+ const rootColumnSchema = this . resolveFieldToColumnSchema ( [ chain [ 0 ] ] ) ;
1971+ return rootColumnSchema ?. type === "JSON" ;
1972+ }
1973+
19371974 /**
19381975 * Resolve a field chain to its column schema (if it references a known column)
19391976 */
0 commit comments