Generate implicit column names when using functions with AS, like count()

This commit is contained in:
Matt Aitken
2025-12-20 18:35:30 +00:00
parent 3c253a3391
commit 126cf1d43e
2 changed files with 172 additions and 4 deletions
@@ -1515,6 +1515,100 @@ describe("Column metadata", () => {
expect(columns[2].customRenderType).toBeUndefined();
});
});
describe("Implicit column names for expressions without aliases", () => {
it("should generate implicit name for COUNT()", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery("SELECT COUNT() FROM runs", ctx);
expect(columns).toHaveLength(1);
expect(columns[0].name).toBe("count()");
expect(columns[0].type).toBe("UInt64");
});
it("should generate implicit name for COUNT(*)", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery("SELECT COUNT(*) FROM runs", ctx);
expect(columns).toHaveLength(1);
expect(columns[0].name).toBe("count(*)");
expect(columns[0].type).toBe("UInt64");
});
it("should generate implicit name for COUNT with column argument", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery("SELECT COUNT(run_id) FROM runs", ctx);
expect(columns).toHaveLength(1);
expect(columns[0].name).toBe("count(run_id)");
expect(columns[0].type).toBe("UInt64");
});
it("should generate implicit name for SUM", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery("SELECT SUM(usage_duration_ms) FROM runs", ctx);
expect(columns).toHaveLength(1);
expect(columns[0].name).toBe("sum(usage_duration_ms)");
expect(columns[0].type).toBe("Int64");
});
it("should generate implicit name for AVG", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery("SELECT AVG(usage_duration_ms) FROM runs", ctx);
expect(columns).toHaveLength(1);
expect(columns[0].name).toBe("avg(usage_duration_ms)");
expect(columns[0].type).toBe("Float64");
});
it("should generate implicit names for multiple aggregations without aliases", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery(
"SELECT COUNT(), status FROM runs GROUP BY status",
ctx
);
expect(columns).toHaveLength(2);
expect(columns[0].name).toBe("count()");
expect(columns[0].type).toBe("UInt64");
expect(columns[1].name).toBe("status");
expect(columns[1].type).toBe("LowCardinality(String)");
});
it("should generate implicit name for arithmetic expressions", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery(
"SELECT usage_duration_ms + 100 FROM runs",
ctx
);
expect(columns).toHaveLength(1);
expect(columns[0].name).toBe("plus(usage_duration_ms, 100)");
});
it("should generate implicit name for constant values", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery("SELECT 42, 'hello' FROM runs", ctx);
expect(columns).toHaveLength(2);
expect(columns[0].name).toBe("42");
expect(columns[1].name).toBe("'hello'");
});
it("should mix explicit aliases with implicit names", () => {
const ctx = createMetadataTestContext();
const { columns } = printQuery(
"SELECT COUNT() AS total, AVG(usage_duration_ms), status AS run_status FROM runs GROUP BY status",
ctx
);
expect(columns).toHaveLength(3);
expect(columns[0].name).toBe("total");
expect(columns[1].name).toBe("avg(usage_duration_ms)");
expect(columns[2].name).toBe("run_status");
});
});
});
describe("Field Mapping Value Transformation", () => {
+78 -4
View File
@@ -241,7 +241,10 @@ export class ClickHousePrinter {
break;
default:
throw new NotImplementedError(
`Unknown expression type: ${nodeType}. Node: ${JSON.stringify(node, null, 2).slice(0, 200)}`
`Unknown expression type: ${nodeType}. Node: ${JSON.stringify(node, null, 2).slice(
0,
200
)}`
);
}
@@ -561,7 +564,7 @@ export class ClickHousePrinter {
const call = col as Call;
const inferredType = this.inferCallType(call);
return {
outputName: null, // Computed columns without alias get auto-named by ClickHouse
outputName: this.generateImplicitName(call),
sourceColumn: null,
inferredType,
};
@@ -572,7 +575,7 @@ export class ClickHousePrinter {
const arith = col as ArithmeticOperation;
const inferredType = this.inferArithmeticType(arith);
return {
outputName: null,
outputName: this.generateImplicitName(arith),
sourceColumn: null,
inferredType,
};
@@ -583,7 +586,7 @@ export class ClickHousePrinter {
const constant = col as Constant;
const inferredType = this.inferConstantType(constant);
return {
outputName: null,
outputName: this.generateImplicitName(constant),
sourceColumn: null,
inferredType,
};
@@ -597,6 +600,77 @@ export class ClickHousePrinter {
};
}
/**
* Generate an implicit column name for an expression without an explicit alias.
* This matches how ClickHouse auto-names result columns.
*
* Examples:
* - COUNT() -> "count()"
* - COUNT(id) -> "count(id)"
* - SUM(duration_ms) -> "sum(duration_ms)"
* - 1 + 2 -> "plus(1, 2)"
*/
private generateImplicitName(expr: Expression): string {
// Handle Call (function/aggregation)
if ((expr as Call).expression_type === "call") {
const call = expr as Call;
const args = call.args.map((arg) => this.generateImplicitName(arg));
// Use lowercase function name to match ClickHouse behavior
return `${call.name.toLowerCase()}(${args.join(", ")})`;
}
// Handle Field
if ((expr as Field).expression_type === "field") {
const field = expr as Field;
// Return the last part of the chain (column name)
const parts = field.chain.filter((p): p is string => typeof p === "string");
return parts.length > 0 ? parts[parts.length - 1] : "*";
}
// Handle ArithmeticOperation
if ((expr as ArithmeticOperation).expression_type === "arithmetic_operation") {
const arith = expr as ArithmeticOperation;
const left = this.generateImplicitName(arith.left);
const right = this.generateImplicitName(arith.right);
switch (arith.op) {
case ArithmeticOperationOp.Add:
return `plus(${left}, ${right})`;
case ArithmeticOperationOp.Sub:
return `minus(${left}, ${right})`;
case ArithmeticOperationOp.Mult:
return `multiply(${left}, ${right})`;
case ArithmeticOperationOp.Div:
return `divide(${left}, ${right})`;
case ArithmeticOperationOp.Mod:
return `modulo(${left}, ${right})`;
default:
return "expression";
}
}
// Handle Constant
if ((expr as Constant).expression_type === "constant") {
const constant = expr as Constant;
if (constant.value === null) {
return "NULL";
}
if (typeof constant.value === "string") {
return `'${constant.value}'`;
}
return String(constant.value);
}
// Handle Alias (shouldn't normally reach here since aliases have explicit names)
if ((expr as Alias).expression_type === "alias") {
const alias = expr as Alias;
return alias.alias;
}
// Default fallback
return "expression";
}
/**
* Resolve a field chain to its column schema and output name
*/