@@ -1664,12 +1664,21 @@ export class ClickHousePrinter {
16641664 *
16651665 * @param column - The column name
16661666 * @param condition - The condition to apply
1667+ * @param tableAlias - Optional table alias to qualify the column reference.
1668+ * When provided, constructs the field chain as [tableAlias, column]
1669+ * so resolveFieldChain will resolve to the correct table in multi-join queries.
16671670 * @returns The AST expression for the condition
16681671 */
1669- private createConditionExpression ( column : string , condition : WhereClauseCondition ) : Expression {
1672+ private createConditionExpression (
1673+ column : string ,
1674+ condition : WhereClauseCondition ,
1675+ tableAlias ?: string
1676+ ) : Expression {
1677+ // When tableAlias is provided, qualify the field chain to ensure it binds
1678+ // to the correct table in multi-join queries
16701679 const fieldExpr : Field = {
16711680 expression_type : "field" ,
1672- chain : [ column ] ,
1681+ chain : tableAlias ? [ tableAlias , column ] : [ column ] ,
16731682 } ;
16741683
16751684 if ( condition . op === "between" ) {
@@ -1705,10 +1714,11 @@ export class ClickHousePrinter {
17051714 *
17061715 * This ensures the same enforcedWhereClause can be used across different tables.
17071716 *
1708- * Note: We use just the column name without table prefix since ClickHouse
1709- * requires the actual table name (task_runs_v2), not the TSQL alias (task_runs)
1717+ * All guard expressions are qualified with the table alias to ensure they bind
1718+ * to the correct table in multi-join queries, preventing potential security
1719+ * issues where an unqualified column reference could bind to the wrong table.
17101720 */
1711- private createEnforcedGuard ( tableSchema : TableSchema , _tableAlias : string ) : Expression | null {
1721+ private createEnforcedGuard ( tableSchema : TableSchema , tableAlias : string ) : Expression | null {
17121722 const { requiredFilters, tenantColumns } = tableSchema ;
17131723 const guards : Expression [ ] = [ ] ;
17141724
@@ -1721,24 +1731,26 @@ export class ClickHousePrinter {
17211731 }
17221732
17231733 // Apply all enforced conditions for columns that exist in this table
1734+ // Pass tableAlias to ensure guards are qualified and bind to the correct table
17241735 for ( const [ column , condition ] of Object . entries ( this . context . enforcedWhereClause ) ) {
17251736 // Skip undefined/null conditions (allows conditional inclusion like project_id?: condition)
17261737 if ( condition === undefined || condition === null ) {
17271738 continue ;
17281739 }
17291740 // Only apply if column exists in this table's schema or is a tenant column
17301741 if ( validColumns . has ( column ) ) {
1731- guards . push ( this . createConditionExpression ( column , condition ) ) ;
1742+ guards . push ( this . createConditionExpression ( column , condition , tableAlias ) ) ;
17321743 }
17331744 }
17341745
17351746 // Add required filters from the table schema (e.g., engine = 'V2')
1747+ // Also qualified with table alias to ensure correct binding in multi-join queries
17361748 if ( requiredFilters && requiredFilters . length > 0 ) {
17371749 for ( const filter of requiredFilters ) {
17381750 const filterGuard : CompareOperation = {
17391751 expression_type : "compare_operation" ,
17401752 op : CompareOperationOp . Eq ,
1741- left : { expression_type : "field" , chain : [ filter . column ] } as Field ,
1753+ left : { expression_type : "field" , chain : [ tableAlias , filter . column ] } as Field ,
17421754 right : { expression_type : "constant" , value : filter . value } as Constant ,
17431755 } ;
17441756 guards . push ( filterGuard ) ;
@@ -2692,6 +2704,31 @@ export class ClickHousePrinter {
26922704 return columnSchema . clickhouseName || columnSchema . name ;
26932705 }
26942706
2707+ // Check if this is a tenant column that's not exposed in the schema's columns
2708+ // These are internal columns used for tenant isolation guards
2709+ const { tenantColumns, requiredFilters } = tableSchema ;
2710+ if ( tenantColumns ) {
2711+ if (
2712+ columnName === tenantColumns . organizationId ||
2713+ columnName === tenantColumns . projectId ||
2714+ columnName === tenantColumns . environmentId
2715+ ) {
2716+ // Tenant columns are already ClickHouse column names, return as-is
2717+ return columnName ;
2718+ }
2719+ }
2720+
2721+ // Check if this is a required filter column (e.g., engine = 'V2')
2722+ // These are internal columns used for enforced filters
2723+ if ( requiredFilters ) {
2724+ for ( const filter of requiredFilters ) {
2725+ if ( columnName === filter . column ) {
2726+ // Required filter columns are already ClickHouse column names, return as-is
2727+ return columnName ;
2728+ }
2729+ }
2730+ }
2731+
26952732 // Column not in schema - this is a security issue, block access
26962733 // Check if the user typed a ClickHouse column name instead of the TSQL name
26972734 for ( const [ tsqlName , colSchema ] of Object . entries ( tableSchema . columns ) ) {
0 commit comments