Skip to content

Commit 28fc2ef

Browse files
committed
Time grain working
1 parent d236f8a commit 28fc2ef

4 files changed

Lines changed: 132 additions & 48 deletions

File tree

exec/java-exec/src/main/resources/webapp/src/api/queries.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,40 @@ export async function executeQuery(request: QueryRequest): Promise<QueryResult>
4141
}
4242
const response = await apiClient.post<QueryResult>('/query.json', body);
4343

44-
// Drill returns HTTP 200 even for failed queries — check queryState
45-
if (response.data.queryState === 'FAILED') {
44+
// Drill returns HTTP 200 even for failed queries — check queryState.
45+
// However, when autoLimit is used, Drill may set queryState to FAILED
46+
// even though it successfully returned partial results (the auto-limit
47+
// interrupted the query). If we got rows back, treat it as a success.
48+
const hasData = response.data.rows && response.data.rows.length > 0;
49+
if (response.data.queryState === 'FAILED' && !hasData) {
4650
const queryId = response.data.queryId;
51+
console.error('[executeQuery] FAILED — queryId=%s, queryState=%s, errorMessage=%s, exception=%s, cols=%d, rows=%d, allKeys=%s, query=%s',
52+
queryId, response.data.queryState, response.data.errorMessage,
53+
response.data.exception,
54+
response.data.columns?.length ?? -1,
55+
response.data.rows?.length ?? -1,
56+
Object.keys(response.data).join(','),
57+
query.substring(0, 200));
4758
let detail = response.data.errorMessage || response.data.exception || '';
4859

4960
// If no error detail in the query response, try fetching the profile
5061
if (!detail && queryId) {
5162
try {
5263
const profile = await apiClient.get(`/profiles/${queryId}.json`);
5364
const profileData = profile.data;
65+
console.error('[executeQuery] profile keys=%s', Object.keys(profileData || {}));
66+
console.error('[executeQuery] profile verboseError=%s', profileData?.verboseError);
67+
console.error('[executeQuery] profile error=%s', profileData?.error);
68+
console.error('[executeQuery] profile errorId=%s planEnd=%s state=%s',
69+
profileData?.errorId, profileData?.planEnd, profileData?.state);
5470
// Prefer verboseError (detailed) over error (concise)
5571
if (profileData?.verboseError) {
5672
detail = profileData.verboseError;
5773
} else if (profileData?.error) {
5874
detail = profileData.error;
5975
}
60-
} catch {
76+
} catch (profileErr) {
77+
console.error('[executeQuery] profile fetch failed:', profileErr);
6178
// Profile fetch failed — fall through to generic message
6279
}
6380
}

exec/java-exec/src/main/resources/webapp/src/components/visualization/VisualizationEditor.tsx

Lines changed: 82 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { executeQuery } from '../../api/queries';
3838
import ChartTypeSelector from './ChartTypeSelector';
3939
import ColumnMapper from './ColumnMapper';
4040
import ChartPreview from './ChartPreview';
41-
import { getEffectiveQuery } from '../../utils/sqlTransformations';
41+
import { computeEffectiveQuery } from '../../utils/sqlTransformations';
4242
import type { ChartType, VisualizationConfig, QueryResult, VisualizationCreate, Visualization } from '../../types';
4343

4444
const { Text } = Typography;
@@ -118,13 +118,16 @@ export default function VisualizationEditor({
118118
}
119119
setDataLoading(true);
120120
setDataError(null);
121+
console.debug('[VizEditor:fetchData] sending base query, defaultSchema=%s, sql=%s',
122+
visualization?.defaultSchema, editedSql.substring(0, 120));
121123
try {
122124
const result = await executeQuery({
123125
query: editedSql,
124126
queryType: 'SQL',
125127
autoLimitRowCount: 10000,
126128
defaultSchema: visualization?.defaultSchema,
127129
});
130+
console.debug('[VizEditor:fetchData] SUCCESS rows=%d cols=%s', result.rows?.length, result.columns);
128131
setBaseData(result);
129132
setSqlDirty(false);
130133
} catch (err: unknown) {
@@ -148,7 +151,7 @@ export default function VisualizationEditor({
148151
}, [open, visualization, editedSql, fetchData]);
149152

150153
// Extract column info from base data (always the full column set)
151-
const columns = useMemo(() => {
154+
const baseColumns = useMemo(() => {
152155
if (!baseData || !baseData.columns || !baseData.metadata) {
153156
return [];
154157
}
@@ -158,6 +161,45 @@ export default function VisualizationEditor({
158161
}));
159162
}, [baseData]);
160163

164+
// Fallback columns: when the base query fails, derive columns from
165+
// the aggregated query result or the saved config so the ColumnMapper
166+
// (and time grain selector) can still render.
167+
const columns = useMemo(() => {
168+
if (baseColumns.length > 0) {
169+
return baseColumns;
170+
}
171+
// Try aggregated data first — it has real type metadata
172+
if (aggregatedData?.columns && aggregatedData?.metadata) {
173+
return aggregatedData.columns.map((name, idx) => ({
174+
name,
175+
type: aggregatedData.metadata[idx] || 'VARCHAR',
176+
}));
177+
}
178+
// Last resort: reconstruct from saved config with assumed types
179+
const cols: { name: string; type: string }[] = [];
180+
if (config.xAxis) {
181+
cols.push({ name: config.xAxis, type: 'DATE' });
182+
}
183+
if (config.yAxis) {
184+
cols.push({ name: config.yAxis, type: 'DOUBLE' });
185+
}
186+
if (config.metrics) {
187+
config.metrics.forEach(m => {
188+
if (!cols.some(c => c.name === m)) {
189+
cols.push({ name: m, type: 'DOUBLE' });
190+
}
191+
});
192+
}
193+
if (config.dimensions) {
194+
config.dimensions.forEach(d => {
195+
if (!cols.some(c => c.name === d)) {
196+
cols.push({ name: d, type: 'VARCHAR' });
197+
}
198+
});
199+
}
200+
return cols;
201+
}, [baseColumns, aggregatedData, config.xAxis, config.yAxis, config.metrics, config.dimensions]);
202+
161203
// Detect stale column mappings after data changes
162204
useEffect(() => {
163205
if (columns.length === 0) {
@@ -182,34 +224,33 @@ export default function VisualizationEditor({
182224
setStaleMapping(hasStale);
183225
}, [columns, config.xAxis, config.yAxis, config.metrics, config.dimensions]);
184226

185-
const [effectiveQuery, setEffectiveQuery] = useState<string>('');
186-
187-
// Compute effective query asynchronously — gated on configReady so we don't
188-
// fire with stale/incomplete config before the saved visualization is loaded.
189-
useEffect(() => {
227+
// Compute effective query synchronously — gated on configReady so we don't
228+
// compute with stale/incomplete config before the saved visualization is loaded.
229+
const effectiveQuery = useMemo(() => {
190230
if (!configReady || !editedSql) {
191-
setEffectiveQuery('');
192-
return;
231+
return '';
193232
}
194-
let cancelled = false;
195-
getEffectiveQuery(editedSql, config)
196-
.then((result) => {
197-
if (!cancelled) {
198-
setEffectiveQuery(result);
199-
}
200-
})
201-
.catch((err) => {
202-
console.error('[VisualizationEditor] getEffectiveQuery failed:', err);
203-
if (!cancelled) {
204-
setEffectiveQuery(editedSql);
205-
}
206-
});
207-
return () => { cancelled = true; };
233+
return computeEffectiveQuery(editedSql, config);
208234
}, [configReady, editedSql, config]);
209235

210236
// Data for chart preview: prefer aggregated data when available
211237
const previewData = aggregatedData || baseData;
212238

239+
// Loading state for chart: when aggregation is active, only wait for the
240+
// aggregated query (don't block the chart on the base-data fetch which is
241+
// only needed for ColumnMapper's column list). Matches VisualizationBuilder.
242+
const chartLoading = (effectiveQuery && effectiveQuery !== editedSql)
243+
? aggregatedLoading
244+
: dataLoading;
245+
246+
console.debug('[VizEditor] render — chartType=%s, xAxis=%s, metrics=%s, dims=%s, timeGrain=%s, dataLoading=%s, aggLoading=%s, chartLoading=%s, previewData=%s, rows=%d, effectiveQuery=%s',
247+
chartType, config.xAxis, JSON.stringify(config.metrics), JSON.stringify(config.dimensions),
248+
config.chartOptions?.timeGrain || '(none)',
249+
dataLoading, aggregatedLoading, chartLoading,
250+
aggregatedData ? 'aggregated' : baseData ? 'base' : 'none',
251+
previewData?.rows?.length ?? 0,
252+
effectiveQuery ? effectiveQuery.substring(0, 80) : '(empty)');
253+
213254
// Fetch aggregated data when the effective query differs from the base SQL
214255
useEffect(() => {
215256
if (!open || !editedSql) {
@@ -344,28 +385,30 @@ export default function VisualizationEditor({
344385

345386
{/* Data Mapping Section */}
346387
<Card size="small" title="Data Mapping" style={{ marginBottom: 12 }}>
347-
{dataLoading ? (
388+
{dataLoading && columns.length === 0 ? (
348389
<div style={{ padding: 16, textAlign: 'center' }}>
349390
<Spin size="small" />
350391
<div style={{ marginTop: 8 }}>
351392
<Text type="secondary">Loading columns...</Text>
352393
</div>
353394
</div>
354-
) : dataError ? (
355-
<Alert
356-
type="warning"
357-
message="Could not load data"
358-
description={dataError}
359-
showIcon
360-
style={{ marginBottom: 8 }}
361-
action={
362-
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData}>
363-
Retry
364-
</Button>
365-
}
366-
/>
367395
) : (
368396
<>
397+
{dataError && (
398+
<Alert
399+
type="warning"
400+
message="Could not load base data"
401+
description={dataError}
402+
showIcon
403+
closable
404+
style={{ marginBottom: 8 }}
405+
action={
406+
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData}>
407+
Retry
408+
</Button>
409+
}
410+
/>
411+
)}
369412
{staleMapping && (
370413
<Alert
371414
type="warning"
@@ -476,7 +519,7 @@ export default function VisualizationEditor({
476519
chartType={chartType}
477520
config={{ ...config, colorScheme: selectedColorScheme }}
478521
data={previewData}
479-
loading={dataLoading || aggregatedLoading}
522+
loading={chartLoading}
480523
height="100%"
481524
/>
482525
</div>
@@ -505,7 +548,7 @@ export default function VisualizationEditor({
505548
<WarningOutlined /> SQL modified but not yet run. Click Run to refresh the preview.
506549
</Text>
507550
)}
508-
{effectiveQuery !== editedSql && editedSql && (
551+
{effectiveQuery && effectiveQuery !== editedSql && editedSql && (
509552
<>
510553
<Text type="secondary" style={{ fontSize: 11, display: 'block', marginBottom: 4 }}>
511554
Effective Query (with aggregation)

exec/java-exec/src/main/resources/webapp/src/utils/sqlTransformations.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -489,22 +489,31 @@ export function buildAggregationQuery(
489489
return parts.join('\n');
490490
}
491491

492+
export type EffectiveQueryConfig = {
493+
xAxis?: string;
494+
metrics?: string[];
495+
dimensions?: string[];
496+
chartOptions?: Record<string, unknown>;
497+
};
498+
492499
/**
493-
* Returns the effective SQL query for a visualization, wrapping the original
494-
* SQL with aggregation/time grain when the config requires it.
500+
* Synchronous version of getEffectiveQuery. Computes the effective SQL query
501+
* for a visualization, wrapping the original SQL with aggregation/time grain
502+
* when the config requires it.
495503
*
496504
* When time grain is set, data is aggregated with DATE_TRUNC + GROUP BY.
497505
* If the user hasn't set explicit aggregation functions, SUM is used as the
498506
* default for each metric so the chart shows meaningful grouped results.
499507
*
500508
* All transformations are done client-side (no backend API dependency).
501509
*
502-
* Shared by VisualizationBuilder, VisualizationEditor, and VisualizationsPage.
510+
* Prefer this over getEffectiveQuery in useMemo / render paths to avoid
511+
* unnecessary async state transitions.
503512
*/
504-
export async function getEffectiveQuery(
513+
export function computeEffectiveQuery(
505514
originalSql: string,
506-
cfg: { xAxis?: string; metrics?: string[]; dimensions?: string[]; chartOptions?: Record<string, unknown> }
507-
): Promise<string> {
515+
cfg: EffectiveQueryConfig
516+
): string {
508517
const timeGrain = cfg.chartOptions?.timeGrain as TimeGrain | undefined;
509518
const hasExplicitAgg = hasCompleteAggregationConfig(cfg.chartOptions, cfg.metrics);
510519

@@ -551,6 +560,17 @@ export async function getEffectiveQuery(
551560
return wrapped || originalSql;
552561
}
553562

563+
/**
564+
* Async wrapper around computeEffectiveQuery for use in async contexts
565+
* (useEffect chains, event handlers, etc.).
566+
*/
567+
export async function getEffectiveQuery(
568+
originalSql: string,
569+
cfg: EffectiveQueryConfig
570+
): Promise<string> {
571+
return computeEffectiveQuery(originalSql, cfg);
572+
}
573+
554574
export function calculateColumnStats(
555575
rowData: Record<string, unknown>[],
556576
columnName: string

exec/java-exec/src/main/resources/webapp/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export default defineConfig({
5151
target: 'http://localhost:8047',
5252
changeOrigin: true,
5353
},
54+
'/profiles': {
55+
target: 'http://localhost:8047',
56+
changeOrigin: true,
57+
},
5458
},
5559
},
5660
});

0 commit comments

Comments
 (0)