Generate implicit column names when using functions with AS, like count()
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user