Skip to content

Commit 4b9ba53

Browse files
committed
Select * support, better small number formatting
1 parent 9b452b3 commit 4b9ba53

File tree

5 files changed

+375
-33
lines changed

5 files changed

+375
-33
lines changed

apps/webapp/app/utils/numberFormatter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ export const formatNumberCompact = (num: number): string => {
66

77
const formatter = Intl.NumberFormat("en");
88

9+
// Formatter for small decimal values that need more precision
10+
const preciseFormatter = Intl.NumberFormat("en", {
11+
minimumSignificantDigits: 1,
12+
maximumSignificantDigits: 6,
13+
});
14+
915
export const formatNumber = (num: number): string => {
16+
// For very small numbers (between -1 and 1, exclusive), use precise formatting
17+
// to avoid rounding 0.000025 to 0
18+
if (num !== 0 && Math.abs(num) < 1) {
19+
return preciseFormatter.format(num);
20+
}
1021
return formatter.format(num);
1122
};
1223

apps/webapp/app/v3/querySchemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export const runsSchema: TableSchema = {
164164
},
165165
region: {
166166
name: "region",
167-
clickhouseName: "region",
167+
clickhouseName: "worker_queue",
168168
...column("String", { description: "Region", example: "us-east-1" }),
169169
},
170170

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

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,30 @@ function printQuery(query: string, context?: PrinterContext) {
104104

105105
describe("ClickHousePrinter", () => {
106106
describe("Basic SELECT statements", () => {
107-
it("should print a simple SELECT *", () => {
108-
const { sql, params } = printQuery("SELECT * FROM task_runs");
107+
it("should expand SELECT * to individual columns", () => {
108+
const { sql, params, columns } = printQuery("SELECT * FROM task_runs");
109109

110-
expect(sql).toContain("SELECT *");
110+
// SELECT * should be expanded to individual columns
111+
expect(sql).toContain("SELECT ");
112+
expect(sql).not.toContain("SELECT *"); // Should NOT contain literal *
111113
expect(sql).toContain("FROM trigger_dev.task_runs_v2");
112-
// Should include tenant guards
114+
115+
// Should include all columns from the schema
116+
expect(sql).toContain("id");
117+
expect(sql).toContain("status");
118+
expect(sql).toContain("task_identifier");
119+
expect(sql).toContain("created_at");
120+
expect(sql).toContain("is_test");
121+
122+
// Should include tenant guards in WHERE
113123
expect(sql).toContain("organization_id");
114124
expect(sql).toContain("project_id");
115125
expect(sql).toContain("environment_id");
126+
127+
// Should return column metadata for all expanded columns
128+
expect(columns.length).toBeGreaterThan(0);
129+
expect(columns.some((c) => c.name === "id")).toBe(true);
130+
expect(columns.some((c) => c.name === "status")).toBe(true);
116131
});
117132

118133
it("should print SELECT with specific columns", () => {
@@ -134,6 +149,93 @@ describe("ClickHousePrinter", () => {
134149
expect(sql).toContain("id AS run_id");
135150
expect(sql).toContain("status AS run_status");
136151
});
152+
153+
it("should expand SELECT * with column name mapping", () => {
154+
const schema = createSchemaRegistry([runsSchema]);
155+
const ctx = createPrinterContext({
156+
organizationId: "org_test",
157+
projectId: "proj_test",
158+
environmentId: "env_test",
159+
schema,
160+
});
161+
162+
const { sql, columns } = printQuery("SELECT * FROM runs", ctx);
163+
164+
// Should expand to all columns from runsSchema with proper aliases
165+
expect(sql).not.toContain("SELECT *");
166+
// Should have AS clauses for columns with different clickhouseName
167+
expect(sql).toContain("run_id AS id"); // id -> run_id with alias back to id
168+
expect(sql).toContain("created_at AS created"); // created -> created_at with alias back
169+
expect(sql).toContain("status"); // status stays as-is
170+
171+
// Should return column metadata with user-facing names
172+
expect(columns.length).toBeGreaterThan(0);
173+
expect(columns.some((c) => c.name === "id")).toBe(true);
174+
expect(columns.some((c) => c.name === "created")).toBe(true);
175+
expect(columns.some((c) => c.name === "status")).toBe(true);
176+
});
177+
178+
it("should expand table.* for specific table", () => {
179+
const { sql, columns } = printQuery("SELECT task_runs.* FROM task_runs");
180+
181+
// Should expand to all columns from task_runs
182+
expect(sql).not.toContain("task_runs.*");
183+
expect(sql).toContain("id");
184+
expect(sql).toContain("status");
185+
186+
// Should return column metadata
187+
expect(columns.length).toBeGreaterThan(0);
188+
});
189+
190+
it("should include virtual columns in SELECT * expansion", () => {
191+
// Schema with virtual columns
192+
const schemaWithVirtual: TableSchema = {
193+
name: "runs",
194+
clickhouseName: "trigger_dev.task_runs_v2",
195+
columns: {
196+
id: { name: "id", ...column("String") },
197+
started_at: { name: "started_at", ...column("Nullable(DateTime64)") },
198+
completed_at: { name: "completed_at", ...column("Nullable(DateTime64)") },
199+
// Virtual column with expression
200+
duration: {
201+
name: "duration",
202+
...column("Nullable(Int64)"),
203+
expression: "dateDiff('millisecond', started_at, completed_at)",
204+
description: "Execution duration in ms",
205+
},
206+
org_id: { name: "org_id", clickhouseName: "organization_id", ...column("String") },
207+
proj_id: { name: "proj_id", clickhouseName: "project_id", ...column("String") },
208+
env_id: { name: "env_id", clickhouseName: "environment_id", ...column("String") },
209+
},
210+
tenantColumns: {
211+
organizationId: "organization_id",
212+
projectId: "project_id",
213+
environmentId: "environment_id",
214+
},
215+
};
216+
217+
const schema = createSchemaRegistry([schemaWithVirtual]);
218+
const ctx = createPrinterContext({
219+
organizationId: "org_test",
220+
projectId: "proj_test",
221+
environmentId: "env_test",
222+
schema,
223+
});
224+
225+
const { sql, columns } = printQuery("SELECT * FROM runs", ctx);
226+
227+
// Should include virtual column with its expression
228+
expect(sql).toContain("dateDiff('millisecond', started_at, completed_at)");
229+
expect(sql).toContain("AS duration");
230+
231+
// Should include regular columns
232+
expect(sql).toContain("id");
233+
234+
// Metadata should include the virtual column
235+
expect(columns.some((c) => c.name === "duration")).toBe(true);
236+
const durationCol = columns.find((c) => c.name === "duration");
237+
expect(durationCol?.description).toBe("Execution duration in ms");
238+
});
137239
});
138240

139241
describe("Table and column name mapping", () => {
@@ -1680,14 +1782,60 @@ describe("Column metadata", () => {
16801782
expect(columns[0].name).toBe("status");
16811783
expect(columns[0].customRenderType).toBe("runStatus");
16821784

1683-
// count - aggregation inferred type
1785+
// count - aggregation inferred type, COUNT doesn't preserve customRenderType
16841786
expect(columns[1].name).toBe("count");
16851787
expect(columns[1].type).toBe("UInt64");
16861788
expect(columns[1].customRenderType).toBeUndefined();
16871789

1688-
// avg_duration - aggregation inferred type
1790+
// avg_duration - aggregation inferred type, AVG preserves customRenderType from source column
1791+
// (average duration is still a duration)
16891792
expect(columns[2].name).toBe("avg_duration");
16901793
expect(columns[2].type).toBe("Float64");
1794+
expect(columns[2].customRenderType).toBe("duration");
1795+
});
1796+
1797+
it("should propagate customRenderType for value-preserving aggregates (SUM, AVG, MIN, MAX)", () => {
1798+
const ctx = createMetadataTestContext();
1799+
const { columns } = printQuery(
1800+
"SELECT SUM(usage_duration_ms) AS total_duration, AVG(cost_in_cents) AS avg_cost, MIN(usage_duration_ms) AS min_duration, MAX(cost_in_cents) AS max_cost FROM runs",
1801+
ctx
1802+
);
1803+
1804+
expect(columns).toHaveLength(4);
1805+
1806+
// SUM preserves customRenderType
1807+
expect(columns[0].name).toBe("total_duration");
1808+
expect(columns[0].customRenderType).toBe("duration");
1809+
1810+
// AVG preserves customRenderType
1811+
expect(columns[1].name).toBe("avg_cost");
1812+
expect(columns[1].customRenderType).toBe("cost");
1813+
1814+
// MIN preserves customRenderType
1815+
expect(columns[2].name).toBe("min_duration");
1816+
expect(columns[2].customRenderType).toBe("duration");
1817+
1818+
// MAX preserves customRenderType
1819+
expect(columns[3].name).toBe("max_cost");
1820+
expect(columns[3].customRenderType).toBe("cost");
1821+
});
1822+
1823+
it("should NOT propagate customRenderType for COUNT aggregates", () => {
1824+
const ctx = createMetadataTestContext();
1825+
const { columns } = printQuery(
1826+
"SELECT COUNT(*), COUNT(usage_duration_ms), COUNT(DISTINCT status) FROM runs",
1827+
ctx
1828+
);
1829+
1830+
expect(columns).toHaveLength(3);
1831+
1832+
// COUNT(*) - no customRenderType
1833+
expect(columns[0].customRenderType).toBeUndefined();
1834+
1835+
// COUNT(duration_column) - still no customRenderType (it's a count, not a duration)
1836+
expect(columns[1].customRenderType).toBeUndefined();
1837+
1838+
// COUNT(DISTINCT ...) - no customRenderType
16911839
expect(columns[2].customRenderType).toBeUndefined();
16921840
});
16931841
});

0 commit comments

Comments
 (0)