Skip to content

Commit 86c36f6

Browse files
committed
clean up handling of SQL result types
1 parent ca95dcd commit 86c36f6

File tree

7 files changed

+384
-379
lines changed

7 files changed

+384
-379
lines changed

src/components/SqlEditor.tsx

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ function SqlEditor({ tab }: SqlEditorProps) {
2121
const [results, setResults] = useState<Record<string, unknown>[] | null>(
2222
null,
2323
);
24+
const [columnTypes, setColumnTypes] = useState<Record<string, string> | null>(
25+
null,
26+
);
2427
const [error, setError] = useState<string | null>(null);
2528
const [loading, setLoading] = useState(false);
2629
const [expandedCell, setExpandedCell] = useState<{
@@ -73,8 +76,11 @@ function SqlEditor({ tab }: SqlEditorProps) {
7376
) => {
7477
if (value === null) return <span className="sql-null">NULL</span>;
7578

79+
// Get column type for this column
80+
const columnType = columnName && columnTypes ? columnTypes[columnName] : undefined;
81+
7682
// Use formatValue to handle Date objects properly
77-
const strValue = formatValue(value);
83+
const strValue = formatValue(value, columnType);
7884

7985
// Check if it's a protobuf config column (still in hex)
8086
const isConfigColumn =
@@ -123,12 +129,13 @@ function SqlEditor({ tab }: SqlEditorProps) {
123129
return strValue;
124130
};
125131

126-
const handleCellClick = (rowIndex: number, colIndex: number, value: unknown) => {
132+
const handleCellClick = (rowIndex: number, colIndex: number, value: unknown, columnName?: string) => {
127133
const selection = window.getSelection();
128134
const hasSelection = selection && selection.toString().length > 0;
129135
if (hasSelection) return;
130136

131-
const strValue = formatValue(value);
137+
const columnType = columnName && columnTypes ? columnTypes[columnName] : undefined;
138+
const strValue = formatValue(value, columnType);
132139
if (strValue.length > 100) {
133140
const isExpanded =
134141
expandedCell?.row === rowIndex && expandedCell?.col === colIndex;
@@ -147,10 +154,12 @@ function SqlEditor({ tab }: SqlEditorProps) {
147154
setLoading(true);
148155
setError(null);
149156
setResults(null);
157+
setColumnTypes(null);
150158

151159
try {
152-
const data = await state.workerManager.executeQuery(query);
153-
setResults(Array.isArray(data) ? data : []);
160+
const result = await state.workerManager.executeQuery(query);
161+
setResults(result.data);
162+
setColumnTypes(result.columnTypes);
154163
} catch (err) {
155164
setError(err instanceof Error ? err.message : "Query failed");
156165
} finally {
@@ -194,10 +203,12 @@ function SqlEditor({ tab }: SqlEditorProps) {
194203
setLoading(true);
195204
setError(null);
196205
setResults(null);
206+
setColumnTypes(null);
197207

198208
try {
199-
const data = await state.workerManager.executeQuery(currentQuery);
200-
setResults(Array.isArray(data) ? data : []);
209+
const result = await state.workerManager.executeQuery(currentQuery);
210+
setResults(result.data);
211+
setColumnTypes(result.columnTypes);
201212
} catch (err) {
202213
setError(err instanceof Error ? err.message : "Query failed");
203214
} finally {
@@ -356,19 +367,19 @@ function SqlEditor({ tab }: SqlEditorProps) {
356367
<th
357368
key={col}
358369
className={isCollapsed ? 'collapsed-column' : ''}
370+
onClick={() => toggleColumnWidth(col)}
371+
style={isCollapsed ? { cursor: 'pointer' } : undefined}
372+
title={isCollapsed ? `Show column: ${col}` : `Hide column: ${col}`}
359373
>
360-
<div className="column-header">
361-
<div className="column-name">
362-
<button
363-
className="column-toggle"
364-
onClick={() => toggleColumnWidth(col)}
365-
title={isCollapsed ? 'Expand column' : 'Collapse column'}
366-
>
367-
{isCollapsed ? '▶' : '▼'}
368-
</button>
369-
{col}
374+
{isCollapsed ? (
375+
`${col.charAt(0)}...`
376+
) : (
377+
<div className="column-header">
378+
<div className="column-name">
379+
{col}
380+
</div>
370381
</div>
371-
</div>
382+
)}
372383
</th>
373384
);
374385
})}
@@ -378,13 +389,14 @@ function SqlEditor({ tab }: SqlEditorProps) {
378389
{results.slice(0, 1000).map((row, i) => (
379390
<tr key={i}>
380391
{Object.entries(row).map(([colName, val], j) => {
381-
const strValue = formatValue(val);
392+
const columnType = columnTypes ? columnTypes[colName] : undefined;
393+
const strValue = formatValue(val, columnType);
382394
const isTruncatable = strValue.length > 100;
383395
return (
384396
<td
385397
key={j}
386398
className={collapsedColumns.has(colName) ? 'collapsed-column' : ''}
387-
onClick={() => handleCellClick(i, j, val)}
399+
onClick={() => handleCellClick(i, j, val, colName)}
388400
style={isTruncatable ? { cursor: 'pointer' } : undefined}
389401
>
390402
{renderCellValue(val, i, j, colName)}

src/crdb/index.ts

Lines changed: 113 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,130 @@ export function detectAndTransform(value: unknown): unknown {
2424
return value;
2525
}
2626

27-
export function formatValue(value: unknown): string {
27+
// Type-specific formatters
28+
function formatInterval(value: unknown): string {
29+
// INTERVAL values are already converted to strings in the worker
30+
return String(value);
31+
}
32+
33+
function formatTime(value: unknown): string {
34+
// TIME values are already converted to HH:MM:SS.ffffff in the worker
35+
return String(value);
36+
}
37+
38+
function formatTimestamp(value: unknown): string {
39+
if (value instanceof Date) {
40+
return value.toISOString();
41+
}
42+
// Fallback for timestamps that weren't converted
43+
if (typeof value === "number" || typeof value === "bigint") {
44+
return new Date(Number(value)).toISOString();
45+
}
46+
return String(value);
47+
}
48+
49+
function formatHugeInt(value: unknown): string {
50+
// Handle DuckDB hugeint/decimal - can be Uint32Array or object with keys "0", "1", "2", "3"
51+
let parts: { 0: number; 1: number; 2: number; 3: number } | null = null;
52+
53+
if (value instanceof Uint32Array && value.length === 4) {
54+
// Convert Uint32Array to plain object structure
55+
parts = { 0: value[0], 1: value[1], 2: value[2], 3: value[3] };
56+
} else if (
57+
typeof value === "object" &&
58+
value !== null &&
59+
"0" in value &&
60+
"1" in value &&
61+
"2" in value &&
62+
"3" in value
63+
) {
64+
parts = value as { 0: number; 1: number; 2: number; 3: number };
65+
}
66+
67+
if (parts) {
68+
// The first part (parts[0]) contains the lower 32 bits
69+
if (parts[1] === 0 && parts[2] === 0 && parts[3] === 0) {
70+
return String(parts[0]);
71+
}
72+
// For very large values, construct a BigInt from the parts
73+
// DuckDB hugeint/decimal is stored as little-endian 32-bit chunks
74+
const bigIntValue =
75+
BigInt(parts[0]) +
76+
(BigInt(parts[1]) << 32n) +
77+
(BigInt(parts[2]) << 64n) +
78+
(BigInt(parts[3]) << 96n);
79+
return bigIntValue.toString();
80+
}
81+
82+
// If it doesn't match the expected structure, just convert to string
83+
return String(value);
84+
}
85+
86+
function formatJson(value: unknown): string {
87+
if (typeof value === "object" && value !== null) {
88+
return JSON.stringify(value, null, 2);
89+
}
90+
return String(value);
91+
}
92+
93+
export function formatValue(value: unknown, columnType?: string): string {
2894
if (value === null || value === undefined) {
2995
return "";
3096
}
3197

32-
if (typeof value === "object") {
33-
// Handle Date objects
98+
// If no type information provided, log warning and use best-effort formatting
99+
if (!columnType) {
100+
console.warn("formatValue called without columnType for value:", value);
101+
102+
// Minimal fallback: handle Date and basic types
34103
if (value instanceof Date) {
35-
// Format as ISO 8601
36104
return value.toISOString();
37105
}
38-
39-
// Handle DuckDB hugeint (128-bit integer) - represented as object with keys "0", "1", "2", "3"
40-
if (
41-
typeof value === "object" &&
42-
value !== null &&
43-
"0" in value &&
44-
"1" in value &&
45-
"2" in value &&
46-
"3" in value
47-
) {
48-
const parts = value as { 0: number; 1: number; 2: number; 3: number };
49-
// The first part (parts[0]) contains the lower 32 bits, which is usually the only non-zero part for reasonable values
50-
// For very large numbers, we'd need proper BigInt handling, but for now just show the lower part
51-
if (parts[1] === 0 && parts[2] === 0 && parts[3] === 0) {
52-
return String(parts[0]);
53-
}
54-
// For very large values, construct a BigInt from the parts
55-
// DuckDB hugeint is stored as little-endian 32-bit chunks
56-
const bigIntValue =
57-
BigInt(parts[0]) +
58-
(BigInt(parts[1]) << 32n) +
59-
(BigInt(parts[2]) << 64n) +
60-
(BigInt(parts[3]) << 96n);
61-
return bigIntValue.toString();
106+
if (typeof value === "object") {
107+
return JSON.stringify(value, null, 2);
62108
}
109+
return String(value);
110+
}
111+
112+
// Type-driven dispatch based on DuckDB type
113+
const typeUpper = columnType.toUpperCase();
114+
115+
// Handle INTERVAL types
116+
if (typeUpper.includes("INTERVAL")) {
117+
return formatInterval(value);
118+
}
63119

120+
// Handle TIME types
121+
if (typeUpper.includes("TIME")) {
122+
return formatTime(value);
123+
}
124+
125+
// Handle TIMESTAMP types
126+
if (typeUpper.includes("TIMESTAMP") || typeUpper.includes("DATE")) {
127+
return formatTimestamp(value);
128+
}
129+
130+
// Handle HUGEINT (128-bit integers) and DECIMAL types
131+
if (typeUpper.includes("HUGEINT") || typeUpper.includes("INT128") || typeUpper.includes("DECIMAL")) {
132+
return formatHugeInt(value);
133+
}
134+
135+
// Handle JSON types
136+
if (typeUpper.includes("JSON") || typeUpper.includes("STRUCT") || typeUpper.includes("MAP")) {
137+
return formatJson(value);
138+
}
139+
140+
// Handle Date objects (might still appear for TIMESTAMP WITH TIME ZONE)
141+
if (value instanceof Date) {
142+
return value.toISOString();
143+
}
144+
145+
// Handle objects that weren't caught by specific type handlers
146+
if (typeof value === "object") {
147+
console.warn(`Unexpected object value for type ${columnType}:`, value);
64148
return JSON.stringify(value, null, 2);
65149
}
66150

151+
// Default: convert to string for primitive types (VARCHAR, INTEGER, DOUBLE, etc.)
67152
return String(value);
68153
}

src/services/WorkerManager.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,19 @@ export class WorkerManager implements IWorkerManager {
121121
await this.sendMessage({ type: "loadSingleTable", to: "dbWorker", table });
122122
}
123123

124-
async executeQuery(sql: string): Promise<Record<string, unknown>[]> {
124+
async executeQuery(sql: string): Promise<{
125+
data: Record<string, unknown>[];
126+
columnTypes: Record<string, string>;
127+
}> {
125128
const result = await this.sendMessage({
126129
type: "executeQuery",
127130
to: "dbWorker",
128131
sql,
129132
});
130-
return result as Record<string, unknown>[];
133+
return result as {
134+
data: Record<string, unknown>[];
135+
columnTypes: Record<string, string>;
136+
};
131137
}
132138

133139
async getTableSchema(

src/services/monacoConfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async function updateSchemaCache() {
4343

4444
// Get all table schemas in one query for efficiency
4545
try {
46-
const allSchemas = await workerManager!.executeQuery(`
46+
const queryResult = await workerManager!.executeQuery(`
4747
SELECT
4848
CASE
4949
WHEN table_schema IS NOT NULL AND table_schema != '' AND table_schema != 'main'
@@ -56,6 +56,8 @@ async function updateSchemaCache() {
5656
ORDER BY full_table_name, ordinal_position
5757
`);
5858

59+
const allSchemas = queryResult.data;
60+
5961
// Debug: log the first few results to see the format
6062
if (allSchemas.length > 0) {
6163
}

src/state/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,10 @@ export interface IWorkerManager {
201201
destroy(): void;
202202

203203
// Database operations
204-
executeQuery(sql: string): Promise<Record<string, unknown>[]>;
204+
executeQuery(sql: string): Promise<{
205+
data: Record<string, unknown>[];
206+
columnTypes: Record<string, string>;
207+
}>;
205208
getTableSchema(
206209
tableName: string,
207210
): Promise<Array<{ column_name: string; data_type: string }>>;

src/styles/components.css

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,25 +1366,23 @@
13661366

13671367
.sql-table th.collapsed-column,
13681368
.sql-table td.collapsed-column {
1369-
width: 20ch;
1370-
max-width: 20ch;
1369+
width: 2ch;
1370+
max-width: 2ch;
1371+
min-width: 2ch;
13711372
overflow: hidden;
1372-
text-overflow: ellipsis;
1373-
white-space: nowrap;
1374-
max-height: 1.5em;
1375-
line-height: 1.5;
1373+
padding: 8px 4px;
13761374
}
13771375

13781376
.sql-table td.collapsed-column {
1379-
vertical-align: top;
1377+
visibility: hidden;
1378+
}
1379+
1380+
.sql-table th.collapsed-column {
1381+
text-align: center;
13801382
}
13811383

13821384
.sql-table td.collapsed-column .sql-cell-json {
1383-
white-space: nowrap;
1384-
overflow: hidden;
1385-
text-overflow: ellipsis;
1386-
max-height: 1.5em;
1387-
display: block;
1385+
display: none;
13881386
}
13891387

13901388

@@ -1420,24 +1418,13 @@
14201418
opacity: 0.6;
14211419
}
14221420

1423-
.column-toggle {
1424-
background: none;
1425-
border: none;
1426-
color: var(--text-muted);
1421+
.sql-table th {
14271422
cursor: pointer;
1428-
padding: 1px 3px;
1429-
border-radius: 2px;
1430-
font-size: 10px;
1431-
font-weight: bold;
1432-
line-height: 1;
1433-
transition: all 0.2s ease;
1434-
opacity: 0.7;
1423+
user-select: none;
14351424
}
14361425

1437-
.column-toggle:hover {
1426+
.sql-table th:hover {
14381427
background: var(--bg-tertiary);
1439-
color: var(--text-primary);
1440-
opacity: 1;
14411428
}
14421429

14431430
.sql-table td {

0 commit comments

Comments
 (0)