Skip to content

Commit 29d19a1

Browse files
committed
Some TRQL date fns weren't working correctly
dateAdd() wasn’t being converted correctly to ClickHouse. We were getting a syntax error
1 parent 97bf898 commit 29d19a1

File tree

2 files changed

+144
-1
lines changed

2 files changed

+144
-1
lines changed

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,70 @@ describe("ClickHousePrinter", () => {
12501250
});
12511251
});
12521252

1253+
describe("Date functions with interval units", () => {
1254+
it("should output dateAdd with string interval as bare keyword", () => {
1255+
const { sql } = printQuery("SELECT dateAdd('day', 7, created_at) AS week_later FROM task_runs");
1256+
1257+
expect(sql).toContain("dateAdd(day, 7, created_at)");
1258+
expect(sql).not.toContain("'day'");
1259+
});
1260+
1261+
it("should output dateAdd with bare identifier interval as keyword", () => {
1262+
const { sql } = printQuery("SELECT dateAdd(day, 7, created_at) AS week_later FROM task_runs");
1263+
1264+
expect(sql).toContain("dateAdd(day, 7, created_at)");
1265+
});
1266+
1267+
it("should output dateDiff with string interval as bare keyword", () => {
1268+
const { sql } = printQuery(
1269+
"SELECT dateDiff('minute', started_at, completed_at) AS duration_minutes FROM task_runs"
1270+
);
1271+
1272+
expect(sql).toContain("dateDiff(minute,");
1273+
expect(sql).not.toContain("'minute'");
1274+
});
1275+
1276+
it("should output dateSub with string interval as bare keyword", () => {
1277+
const { sql } = printQuery("SELECT dateSub('hour', 1, created_at) AS earlier FROM task_runs");
1278+
1279+
expect(sql).toContain("dateSub(hour, 1, created_at)");
1280+
expect(sql).not.toContain("'hour'");
1281+
});
1282+
1283+
it("should output dateTrunc with string interval as bare keyword", () => {
1284+
const { sql } = printQuery(
1285+
"SELECT dateTrunc('month', created_at) AS month_start FROM task_runs"
1286+
);
1287+
1288+
expect(sql).toContain("dateTrunc(month, created_at)");
1289+
expect(sql).not.toContain("'month'");
1290+
});
1291+
1292+
it("should output date_add (underscore variant) with bare keyword", () => {
1293+
const { sql } = printQuery(
1294+
"SELECT date_add('week', 2, created_at) AS two_weeks FROM task_runs"
1295+
);
1296+
1297+
expect(sql).toContain("date_add(week, 2, created_at)");
1298+
expect(sql).not.toContain("'week'");
1299+
});
1300+
1301+
it("should output date_diff (underscore variant) with bare keyword", () => {
1302+
const { sql } = printQuery(
1303+
"SELECT date_diff('second', started_at, completed_at) AS dur FROM task_runs"
1304+
);
1305+
1306+
expect(sql).toContain("date_diff(second,");
1307+
expect(sql).not.toContain("'second'");
1308+
});
1309+
1310+
it("should handle case-insensitive interval units", () => {
1311+
const { sql } = printQuery("SELECT dateAdd('DAY', 7, created_at) AS week_later FROM task_runs");
1312+
1313+
expect(sql).toContain("dateAdd(day, 7, created_at)");
1314+
});
1315+
});
1316+
12531317
describe("Tenant isolation", () => {
12541318
it("should inject tenant guards for single table", () => {
12551319
const context = createTestContext({

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2926,7 +2926,7 @@ export class ClickHousePrinter {
29262926
if (funcMeta) {
29272927
validateFunctionArgs(node.args, funcMeta.minArgs, funcMeta.maxArgs, name);
29282928

2929-
const args = node.args.map((arg) => this.visit(arg));
2929+
const args = this.visitCallArgs(name, node.args);
29302930
const params = node.params ? node.params.map((p) => this.visit(p)) : null;
29312931
const paramsPart = params ? `(${params.join(", ")})` : "";
29322932
return `${funcMeta.clickhouseName}${paramsPart}(${args.join(", ")})`;
@@ -2936,6 +2936,85 @@ export class ClickHousePrinter {
29362936
throw new QueryError(`Unknown function: ${name}`);
29372937
}
29382938

2939+
/**
2940+
* Valid ClickHouse interval unit keywords used by date functions like dateAdd, dateDiff, etc.
2941+
*/
2942+
private static readonly INTERVAL_UNITS = new Set([
2943+
"second",
2944+
"minute",
2945+
"hour",
2946+
"day",
2947+
"week",
2948+
"month",
2949+
"quarter",
2950+
"year",
2951+
]);
2952+
2953+
/**
2954+
* Date functions whose first argument is an interval unit keyword.
2955+
* ClickHouse requires the unit as a bare keyword (e.g., `dateAdd(day, 7, col)`),
2956+
* not a string literal (e.g., `dateAdd('day', 7, col)` fails).
2957+
*/
2958+
private static readonly DATE_FUNCTIONS_WITH_INTERVAL_UNIT = new Set([
2959+
"dateadd",
2960+
"datesub",
2961+
"datediff",
2962+
"datetrunc",
2963+
"date_add",
2964+
"date_sub",
2965+
"date_diff",
2966+
"date_trunc",
2967+
]);
2968+
2969+
/**
2970+
* Visit function call arguments, handling date functions that require an interval unit
2971+
* keyword as their first argument. For these functions, the first arg is output as a
2972+
* bare keyword instead of being parameterized or resolved as a column reference.
2973+
*/
2974+
private visitCallArgs(functionName: string, args: Expression[]): string[] {
2975+
const lowerName = functionName.toLowerCase();
2976+
2977+
if (
2978+
ClickHousePrinter.DATE_FUNCTIONS_WITH_INTERVAL_UNIT.has(lowerName) &&
2979+
args.length > 0
2980+
) {
2981+
const firstArg = args[0];
2982+
const intervalUnit = this.extractIntervalUnit(firstArg);
2983+
2984+
if (intervalUnit) {
2985+
return [intervalUnit, ...args.slice(1).map((arg) => this.visit(arg))];
2986+
}
2987+
}
2988+
2989+
return args.map((arg) => this.visit(arg));
2990+
}
2991+
2992+
/**
2993+
* Try to extract a valid interval unit keyword from an expression.
2994+
* Handles both string constants ('day') and bare identifiers (day).
2995+
* Returns the bare keyword string if valid, or null if not an interval unit.
2996+
*/
2997+
private extractIntervalUnit(expr: Expression): string | null {
2998+
if (expr.expression_type === "constant") {
2999+
const value = (expr as Constant).value;
3000+
if (typeof value === "string" && ClickHousePrinter.INTERVAL_UNITS.has(value.toLowerCase())) {
3001+
return value.toLowerCase();
3002+
}
3003+
}
3004+
3005+
if (expr.expression_type === "field") {
3006+
const chain = (expr as Field).chain;
3007+
if (chain.length === 1 && typeof chain[0] === "string") {
3008+
const name = chain[0].toLowerCase();
3009+
if (ClickHousePrinter.INTERVAL_UNITS.has(name)) {
3010+
return name;
3011+
}
3012+
}
3013+
}
3014+
3015+
return null;
3016+
}
3017+
29393018
private visitJoinConstraint(node: JoinConstraint): string {
29403019
return this.visit(node.expr);
29413020
}

0 commit comments

Comments
 (0)