@@ -104,15 +104,30 @@ function printQuery(query: string, context?: PrinterContext) {
104104
105105describe ( "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