Skip to content

Commit da367e2

Browse files
committed
Fix for aliases with transforms
1 parent 663ea74 commit da367e2

File tree

3 files changed

+126
-5
lines changed

3 files changed

+126
-5
lines changed

internal-packages/clickhouse/src/client/tsql.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
116116
fieldMappings: options.fieldMappings,
117117
});
118118

119+
// DEBUG: Log the generated SQL and params
120+
console.log("[TSQL DEBUG] Input query:", options.query);
121+
console.log("[TSQL DEBUG] Generated SQL:", sql);
122+
console.log("[TSQL DEBUG] Params:", JSON.stringify(params, null, 2));
123+
119124
// 2. Execute the query with stats
120125
const queryFn = reader.queryWithStats({
121126
name: options.name,
@@ -127,6 +132,11 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
127132

128133
const [error, result] = await queryFn(params);
129134

135+
// DEBUG: Log query result
136+
console.log("[TSQL DEBUG] Query error:", error);
137+
console.log("[TSQL DEBUG] Raw rows count:", result?.rows?.length ?? 0);
138+
console.log("[TSQL DEBUG] Raw rows:", JSON.stringify(result?.rows?.slice(0, 5), null, 2));
139+
130140
if (error) {
131141
return [error, null];
132142
}

internal-packages/tsql/src/query/printer.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,25 @@ describe("WHERE transform (whereTransform)", () => {
948948
expect(sql).toContain("if(batch_id = '', NULL, concat('batch_', batch_id))");
949949
});
950950

951+
it("should use raw column in WHERE but expression in SELECT", () => {
952+
const ctx = createPrefixedContext();
953+
const { sql, params } = printQuery(
954+
"SELECT batch_id FROM runs WHERE batch_id = 'batch_abc123'",
955+
ctx
956+
);
957+
958+
// SELECT should use the expression (adds prefix)
959+
expect(sql).toContain("if(batch_id = '', NULL, concat('batch_', batch_id))");
960+
961+
// WHERE should use table-qualified raw column (not the expression), and value should be stripped
962+
// The WHERE clause should compare `runs.batch_id` directly (table-qualified to avoid alias conflict)
963+
expect(sql).toMatch(/WHERE.*equals\(`?runs`?\.`?batch_id`?,/);
964+
expect(sql).not.toMatch(/WHERE.*concat\('batch_'/);
965+
966+
// The value should have prefix stripped
967+
expect(Object.values(params)).toContain("abc123");
968+
});
969+
951970
it("should work with different prefix patterns", () => {
952971
const ctx = createPrefixedContext();
953972
const { params } = printQuery("SELECT * FROM runs WHERE schedule_id = 'sched_xyz789'", ctx);

internal-packages/tsql/src/query/printer.ts

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,15 +1088,20 @@ export class ClickHousePrinter {
10881088

10891089
// Look up table schema and get ClickHouse table name
10901090
const tableSchema = this.lookupTable(tableName);
1091-
joinStrings.push(tableSchema.clickhouseName);
1091+
1092+
// Always add the TSQL table name as an alias if no explicit alias is provided
1093+
// This ensures table-qualified column references work in WHERE clauses
1094+
// (needed to avoid alias conflicts when columns have expressions)
1095+
const effectiveAlias = node.alias || tableName;
1096+
joinStrings.push(
1097+
`${tableSchema.clickhouseName} AS ${this.printIdentifier(effectiveAlias)}`
1098+
);
10921099

10931100
// Register this table context for column name resolution
1094-
// Use the alias if provided, otherwise use the TSQL table name
1095-
const contextKey = node.alias || tableName;
1096-
this.tableContexts.set(contextKey, tableSchema);
1101+
this.tableContexts.set(effectiveAlias, tableSchema);
10971102

10981103
// Add tenant isolation guard
1099-
extraWhere = this.createTenantGuard(tableSchema, node.alias || tableName);
1104+
extraWhere = this.createTenantGuard(tableSchema, effectiveAlias);
11001105
} else if (
11011106
(tableExpr as SelectQuery).expression_type === "select_query" ||
11021107
(tableExpr as SelectSetQuery).expression_type === "select_set_query"
@@ -1638,6 +1643,18 @@ export class ClickHousePrinter {
16381643
}
16391644

16401645
// Check if this field is a virtual column
1646+
// BUT: if the column has whereTransform and we're in a comparison context,
1647+
// use the raw column instead of the expression (for efficient index usage)
1648+
const columnSchema = this.resolveFieldToColumnSchema(node.chain);
1649+
const inComparisonContext = this.isInComparisonContext();
1650+
1651+
if (columnSchema?.whereTransform && inComparisonContext) {
1652+
// Use raw column for WHERE comparisons when whereTransform is defined
1653+
// Must table-qualify to avoid alias conflicts with SELECT expressions
1654+
const tableQualifiedChain = this.resolveFieldChainWithTableAlias(node.chain);
1655+
return tableQualifiedChain.map((part) => this.printIdentifierOrIndex(part)).join(".");
1656+
}
1657+
16411658
const virtualExpression = this.getVirtualColumnExpressionForField(node.chain);
16421659
if (virtualExpression !== null) {
16431660
// Return the expression wrapped in parentheses
@@ -1651,6 +1668,81 @@ export class ClickHousePrinter {
16511668
return resolvedChain.map((part) => this.printIdentifierOrIndex(part)).join(".");
16521669
}
16531670

1671+
/**
1672+
* Check if we're currently inside a comparison operation (WHERE context)
1673+
*/
1674+
private isInComparisonContext(): boolean {
1675+
for (const node of this.stack) {
1676+
if ((node as CompareOperation).expression_type === "compare_operation") {
1677+
return true;
1678+
}
1679+
}
1680+
return false;
1681+
}
1682+
1683+
/**
1684+
* Resolve field chain with table alias prefix to avoid alias conflicts.
1685+
* This is used in WHERE clauses when a column has whereTransform to ensure
1686+
* we reference the raw column, not a SELECT alias with the same name.
1687+
*/
1688+
private resolveFieldChainWithTableAlias(chain: Array<string | number>): Array<string | number> {
1689+
if (chain.length === 0) return chain;
1690+
1691+
const firstPart = chain[0];
1692+
if (typeof firstPart !== "string") return chain;
1693+
1694+
// If already qualified (table.column), use normal resolution
1695+
if (chain.length >= 2) {
1696+
return this.resolveFieldChain(chain);
1697+
}
1698+
1699+
// Unqualified reference - need to find the table and add its alias
1700+
const columnName = firstPart;
1701+
for (const [tableAlias, tableSchema] of this.tableContexts.entries()) {
1702+
const columnSchema = tableSchema.columns[columnName];
1703+
if (columnSchema) {
1704+
const resolvedColumnName = columnSchema.clickhouseName || columnSchema.name;
1705+
return [tableAlias, resolvedColumnName, ...chain.slice(1)];
1706+
}
1707+
}
1708+
1709+
// Not found in any table - return as-is
1710+
return chain;
1711+
}
1712+
1713+
/**
1714+
* Resolve a field chain to its column schema (if it references a known column)
1715+
*/
1716+
private resolveFieldToColumnSchema(chain: Array<string | number>): ColumnSchema | null {
1717+
if (chain.length === 0) return null;
1718+
1719+
const firstPart = chain[0];
1720+
if (typeof firstPart !== "string") return null;
1721+
1722+
// Qualified reference: table.column
1723+
if (chain.length >= 2) {
1724+
const tableAlias = firstPart;
1725+
const tableSchema = this.tableContexts.get(tableAlias);
1726+
if (!tableSchema) return null;
1727+
1728+
const columnName = chain[1];
1729+
if (typeof columnName !== "string") return null;
1730+
1731+
return tableSchema.columns[columnName] || null;
1732+
}
1733+
1734+
// Unqualified reference
1735+
const columnName = firstPart;
1736+
for (const tableSchema of this.tableContexts.values()) {
1737+
const columnSchema = tableSchema.columns[columnName];
1738+
if (columnSchema) {
1739+
return columnSchema;
1740+
}
1741+
}
1742+
1743+
return null;
1744+
}
1745+
16541746
/**
16551747
* Check if a field chain references a virtual column and return its expression
16561748
* @returns The virtual column expression, or null if not a virtual column

0 commit comments

Comments
 (0)