Skip to content

Commit dadc5e8

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 4dfa658 commit dadc5e8

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
@@ -1244,6 +1244,70 @@ describe("ClickHousePrinter", () => {
12441244
});
12451245
});
12461246

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

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

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

2858-
const args = node.args.map((arg) => this.visit(arg));
2858+
const args = this.visitCallArgs(name, node.args);
28592859
const params = node.params ? node.params.map((p) => this.visit(p)) : null;
28602860
const paramsPart = params ? `(${params.join(", ")})` : "";
28612861
return `${funcMeta.clickhouseName}${paramsPart}(${args.join(", ")})`;
@@ -2865,6 +2865,85 @@ export class ClickHousePrinter {
28652865
throw new QueryError(`Unknown function: ${name}`);
28662866
}
28672867

2868+
/**
2869+
* Valid ClickHouse interval unit keywords used by date functions like dateAdd, dateDiff, etc.
2870+
*/
2871+
private static readonly INTERVAL_UNITS = new Set([
2872+
"second",
2873+
"minute",
2874+
"hour",
2875+
"day",
2876+
"week",
2877+
"month",
2878+
"quarter",
2879+
"year",
2880+
]);
2881+
2882+
/**
2883+
* Date functions whose first argument is an interval unit keyword.
2884+
* ClickHouse requires the unit as a bare keyword (e.g., `dateAdd(day, 7, col)`),
2885+
* not a string literal (e.g., `dateAdd('day', 7, col)` fails).
2886+
*/
2887+
private static readonly DATE_FUNCTIONS_WITH_INTERVAL_UNIT = new Set([
2888+
"dateadd",
2889+
"datesub",
2890+
"datediff",
2891+
"datetrunc",
2892+
"date_add",
2893+
"date_sub",
2894+
"date_diff",
2895+
"date_trunc",
2896+
]);
2897+
2898+
/**
2899+
* Visit function call arguments, handling date functions that require an interval unit
2900+
* keyword as their first argument. For these functions, the first arg is output as a
2901+
* bare keyword instead of being parameterized or resolved as a column reference.
2902+
*/
2903+
private visitCallArgs(functionName: string, args: Expression[]): string[] {
2904+
const lowerName = functionName.toLowerCase();
2905+
2906+
if (
2907+
ClickHousePrinter.DATE_FUNCTIONS_WITH_INTERVAL_UNIT.has(lowerName) &&
2908+
args.length > 0
2909+
) {
2910+
const firstArg = args[0];
2911+
const intervalUnit = this.extractIntervalUnit(firstArg);
2912+
2913+
if (intervalUnit) {
2914+
return [intervalUnit, ...args.slice(1).map((arg) => this.visit(arg))];
2915+
}
2916+
}
2917+
2918+
return args.map((arg) => this.visit(arg));
2919+
}
2920+
2921+
/**
2922+
* Try to extract a valid interval unit keyword from an expression.
2923+
* Handles both string constants ('day') and bare identifiers (day).
2924+
* Returns the bare keyword string if valid, or null if not an interval unit.
2925+
*/
2926+
private extractIntervalUnit(expr: Expression): string | null {
2927+
if (expr.expression_type === "constant") {
2928+
const value = (expr as Constant).value;
2929+
if (typeof value === "string" && ClickHousePrinter.INTERVAL_UNITS.has(value.toLowerCase())) {
2930+
return value.toLowerCase();
2931+
}
2932+
}
2933+
2934+
if (expr.expression_type === "field") {
2935+
const chain = (expr as Field).chain;
2936+
if (chain.length === 1 && typeof chain[0] === "string") {
2937+
const name = chain[0].toLowerCase();
2938+
if (ClickHousePrinter.INTERVAL_UNITS.has(name)) {
2939+
return name;
2940+
}
2941+
}
2942+
}
2943+
2944+
return null;
2945+
}
2946+
28682947
private visitJoinConstraint(node: JoinConstraint): string {
28692948
return this.visit(node.expr);
28702949
}

0 commit comments

Comments
 (0)