Skip to content

Commit c86feba

Browse files
committed
SELECT * only returns the core columns but shows info
1 parent f79f3f2 commit c86feba

File tree

4 files changed

+92
-26
lines changed

4 files changed

+92
-26
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
4040
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
4141
import { Badge } from "~/components/primitives/Badge";
4242
import { Button } from "~/components/primitives/Buttons";
43+
import { Callout } from "~/components/primitives/Callout";
4344
import { CopyableText } from "~/components/primitives/CopyableText";
4445
import { Header2, Header3 } from "~/components/primitives/Headers";
4546
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
@@ -172,23 +173,29 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
172173
);
173174
if (!canAccess) {
174175
return typedjson(
175-
{ error: "Unauthorized", rows: null, columns: null, stats: null },
176+
{ error: "Unauthorized", rows: null, columns: null, stats: null, hiddenColumns: null },
176177
{ status: 403 }
177178
);
178179
}
179180

180181
const project = await findProjectBySlug(organizationSlug, projectParam, user.id);
181182
if (!project) {
182183
return typedjson(
183-
{ error: "Project not found", rows: null, columns: null, stats: null },
184+
{ error: "Project not found", rows: null, columns: null, stats: null, hiddenColumns: null },
184185
{ status: 404 }
185186
);
186187
}
187188

188189
const environment = await findEnvironmentBySlug(project.id, envParam, user.id);
189190
if (!environment) {
190191
return typedjson(
191-
{ error: "Environment not found", rows: null, columns: null, stats: null },
192+
{
193+
error: "Environment not found",
194+
rows: null,
195+
columns: null,
196+
stats: null,
197+
hiddenColumns: null,
198+
},
192199
{ status: 404 }
193200
);
194201
}
@@ -206,6 +213,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
206213
rows: null,
207214
columns: null,
208215
stats: null,
216+
hiddenColumns: null,
209217
},
210218
{ status: 400 }
211219
);
@@ -232,7 +240,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
232240

233241
if (error) {
234242
return typedjson(
235-
{ error: error.message, rows: null, columns: null, stats: null },
243+
{ error: error.message, rows: null, columns: null, stats: null, hiddenColumns: null },
236244
{ status: 400 }
237245
);
238246
}
@@ -242,11 +250,12 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
242250
rows: result.rows,
243251
columns: result.columns,
244252
stats: result.stats,
253+
hiddenColumns: result.hiddenColumns ?? null,
245254
});
246255
} catch (err) {
247256
const errorMessage = err instanceof Error ? err.message : "Unknown error executing query";
248257
return typedjson(
249-
{ error: errorMessage, rows: null, columns: null, stats: null },
258+
{ error: errorMessage, rows: null, columns: null, stats: null, hiddenColumns: null },
250259
{ status: 500 }
251260
);
252261
}
@@ -464,11 +473,25 @@ export default function Page() {
464473
</Button>
465474
</div>
466475
) : results?.rows && results?.columns ? (
467-
<TSQLResultsTable
468-
rows={results.rows}
469-
columns={results.columns}
470-
prettyFormatting={prettyFormatting}
471-
/>
476+
<div className="flex h-full flex-col overflow-hidden">
477+
{results.hiddenColumns && results.hiddenColumns.length > 0 && (
478+
<Callout variant="warning" className="m-2 shrink-0 text-sm">
479+
<code>SELECT *</code> doesn't return all columns because it's slow. The
480+
following columns are not shown:{" "}
481+
<span className="font-mono text-xs">
482+
{results.hiddenColumns.join(", ")}
483+
</span>
484+
. Specify them explicitly to include them.
485+
</Callout>
486+
)}
487+
<div className="min-h-0 flex-1 overflow-hidden">
488+
<TSQLResultsTable
489+
rows={results.rows}
490+
columns={results.columns}
491+
prettyFormatting={prettyFormatting}
492+
/>
493+
</div>
494+
</div>
472495
) : (
473496
<Paragraph variant="small" className="p-4 text-text-dimmed">
474497
Run a query to see results here.

internal-packages/clickhouse/src/client/tsql.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ export interface TSQLQuerySuccess<T> {
7676
rows: T[];
7777
columns: OutputColumnMetadata[];
7878
stats: QueryStats;
79+
/**
80+
* Columns that were hidden when SELECT * was used.
81+
* Only populated when SELECT * is transformed to core columns only.
82+
*/
83+
hiddenColumns?: string[];
7984
}
8085

8186
/**
@@ -114,7 +119,7 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
114119

115120
try {
116121
// 1. Compile the TSQL query to ClickHouse SQL
117-
const { sql, params, columns } = compileTSQL(options.query, {
122+
const { sql, params, columns, hiddenColumns } = compileTSQL(options.query, {
118123
organizationId: options.organizationId,
119124
projectId: options.projectId,
120125
environmentId: options.environmentId,
@@ -145,17 +150,20 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
145150

146151
const { rows, stats } = result;
147152

153+
// Build the result, including hiddenColumns if present
154+
const baseResult = { columns, stats, hiddenColumns };
155+
148156
// 3. Transform result values if enabled
149157
if (shouldTransformValues && rows) {
150158
const transformedRows = transformResults(
151159
rows as Record<string, unknown>[],
152160
options.tableSchema,
153161
{ fieldMappings: options.fieldMappings }
154162
);
155-
return [null, { rows: transformedRows as z.output<TOut>[], columns, stats }];
163+
return [null, { rows: transformedRows as z.output<TOut>[], ...baseResult }];
156164
}
157165

158-
return [null, { rows: rows ?? [], columns, stats }];
166+
return [null, { rows: rows ?? [], ...baseResult }];
159167
} catch (error) {
160168
const errorMessage = error instanceof Error ? error.message : "Unknown error";
161169

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

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export interface PrintResult {
7070
params: Record<string, unknown>;
7171
/** Metadata for each column in the SELECT clause, in order */
7272
columns: OutputColumnMetadata[];
73+
/**
74+
* Columns that were hidden when SELECT * was used.
75+
* Only populated when SELECT * is transformed to core columns only.
76+
*/
77+
hiddenColumns?: string[];
7378
}
7479

7580
/**
@@ -109,6 +114,8 @@ export class ClickHousePrinter {
109114
private outputColumns: OutputColumnMetadata[] = [];
110115
/** Whether we're currently processing GROUP BY expressions */
111116
private inGroupByContext = false;
117+
/** Columns hidden when SELECT * is expanded to core columns only */
118+
private hiddenColumns: string[] = [];
112119

113120
constructor(
114121
private context: PrinterContext,
@@ -122,12 +129,17 @@ export class ClickHousePrinter {
122129
*/
123130
print(node: SelectQuery | SelectSetQuery): PrintResult {
124131
this.outputColumns = [];
132+
this.hiddenColumns = [];
125133
const sql = this.visit(node);
126-
return {
134+
const result: PrintResult = {
127135
sql,
128136
params: this.context.getParams(),
129137
columns: this.outputColumns,
130138
};
139+
if (this.hiddenColumns.length > 0) {
140+
result.hiddenColumns = this.hiddenColumns;
141+
}
142+
return result;
131143
}
132144

133145
/**
@@ -605,7 +617,8 @@ export class ClickHousePrinter {
605617
}
606618

607619
/**
608-
* Expand SELECT * to all selectable columns from all tables in context
620+
* Expand SELECT * to core columns only from all tables in context.
621+
* Non-core columns are tracked in hiddenColumns for user notification.
609622
*/
610623
private expandAllTableColumns(collectMetadata: boolean): string[] {
611624
const results: string[] = [];
@@ -615,7 +628,8 @@ export class ClickHousePrinter {
615628
const tableColumns = this.getSelectableColumnsFromSchema(
616629
tableSchema,
617630
tableAlias,
618-
collectMetadata
631+
collectMetadata,
632+
true // onlyCoreColumns - SELECT * only returns core columns
619633
);
620634
results.push(...tableColumns);
621635
}
@@ -629,7 +643,8 @@ export class ClickHousePrinter {
629643
}
630644

631645
/**
632-
* Expand table.* to all selectable columns from a specific table
646+
* Expand table.* to core columns only from a specific table.
647+
* Non-core columns are tracked in hiddenColumns for user notification.
633648
*/
634649
private expandTableColumns(tableAlias: string, collectMetadata: boolean): string[] {
635650
const tableSchema = this.tableContexts.get(tableAlias);
@@ -639,30 +654,51 @@ export class ClickHousePrinter {
639654
return [`${this.printIdentifier(tableAlias)}.*`];
640655
}
641656

642-
return this.getSelectableColumnsFromSchema(tableSchema, tableAlias, collectMetadata);
657+
return this.getSelectableColumnsFromSchema(
658+
tableSchema,
659+
tableAlias,
660+
collectMetadata,
661+
true // onlyCoreColumns - table.* only returns core columns
662+
);
643663
}
644664

645665
/**
646-
* Get all selectable columns from a table schema as SQL strings
666+
* Get selectable columns from a table schema as SQL strings
647667
*
648668
* @param tableSchema - The table schema
649669
* @param tableAlias - The alias used for the table in the query (for table-qualified columns)
650670
* @param collectMetadata - Whether to collect column metadata
671+
* @param onlyCoreColumns - If true, only return core columns and track hidden columns (but falls back to all columns if no core columns are defined)
651672
* @returns Array of SQL column strings
652673
*/
653674
private getSelectableColumnsFromSchema(
654675
tableSchema: TableSchema,
655676
tableAlias: string,
656-
collectMetadata: boolean
677+
collectMetadata: boolean,
678+
onlyCoreColumns = false
657679
): string[] {
658680
const results: string[] = [];
659681

682+
// Check if any core columns exist - if not, we'll return all columns as a fallback
683+
const hasCoreColumns = Object.values(tableSchema.columns).some(
684+
(col) => col.coreColumn === true && col.selectable !== false
685+
);
686+
687+
// Only filter to core columns if the schema defines some core columns
688+
const shouldFilterToCoreOnly = onlyCoreColumns && hasCoreColumns;
689+
660690
for (const [columnName, columnSchema] of Object.entries(tableSchema.columns)) {
661691
// Skip non-selectable columns
662692
if (columnSchema.selectable === false) {
663693
continue;
664694
}
665695

696+
// If filtering to core columns only, skip non-core and track them
697+
if (shouldFilterToCoreOnly && !columnSchema.coreColumn) {
698+
this.hiddenColumns.push(columnName);
699+
continue;
700+
}
701+
666702
// Build the SQL for this column
667703
let sqlResult: string;
668704

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,16 @@ function validateSelectQuery(node: SelectQuery, context: ValidationContext): voi
209209
coreColumns.push(...tableCoreColumns);
210210
}
211211

212-
// Build suggestion message
213-
let suggestionMsg = "SELECT * will be far slower than selecting specific columns. ";
212+
// Build info message about SELECT * behavior
213+
let suggestionMsg = "SELECT * doesn't return all columns.";
214214
if (coreColumns.length > 0) {
215-
suggestionMsg += `Consider selecting specific columns, e.g.: ${coreColumns.join(", ")}`;
216-
} else {
217-
suggestionMsg += "Consider selecting only the columns you need.";
215+
suggestionMsg += `It will return: ${coreColumns.join(", ")}. `;
218216
}
217+
suggestionMsg += "Specify columns explicitly to include other columns.";
219218

220219
context.issues.push({
221220
message: suggestionMsg,
222-
severity: "warning",
221+
severity: "info",
223222
type: "select_star",
224223
suggestedColumns: coreColumns.length > 0 ? coreColumns : undefined,
225224
});

0 commit comments

Comments
 (0)