Skip to content

Commit 7ea2f56

Browse files
ahkcsclaude
andauthored
Fix PPL head command to show simple result count (opensearch-project#11405)
* Fix PPL histogram to exclude head clause from total hit count Signed-off-by: Kai Huang <ahkcs@amazon.com> * Simplify head command display: show only row count without total When PPL query ends with head command, display "X results" instead of "X of Y results". Remove the separate total count query infrastructure since it is no longer needed. - Replace stripHeadFromQuery with queryEndsWithHead for detection - Remove buildPPLTotalCountQuery, executeTotalCountQuery, and related logic - Delete use_total_count_results.ts hook - Update action_bar to skip total hits when head is detected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d46a945 commit 7ea2f56

5 files changed

Lines changed: 91 additions & 10 deletions

File tree

src/plugins/explore/public/application/utils/state_management/actions/query_actions.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,12 @@ export const executeQueries = createAsyncThunk<
260260
const dataTableCacheKey = defaultCacheKey;
261261
const breakdownField = state.queryEditor.breakdownField;
262262
const histogramCacheKey = prepareHistogramCacheKey(query, !!breakdownField);
263+
const queryString = defaultPrepareQueryString(query);
263264

264265
// Check what needs execution for core queries
265266
// If results exist but query status is UNINITIALIZED (after cancel), we need to re-execute
266267
const dataTableQueryStatus = state.queryEditor.queryStatusMap[dataTableCacheKey];
267268
const histogramQueryStatus = state.queryEditor.queryStatusMap[histogramCacheKey];
268-
269269
// Early exit if query should be skipped
270270
if (shouldSkipQueryExecution(query)) {
271271
return;
@@ -278,7 +278,6 @@ export const executeQueries = createAsyncThunk<
278278
query.language !== 'PROMQL' &&
279279
(!results[histogramCacheKey] ||
280280
histogramQueryStatus?.status === QueryExecutionStatus.UNINITIALIZED);
281-
282281
const promises = [];
283282
// Execute query without aggregations
284283
if (needsDataTableQuery) {
@@ -287,7 +286,7 @@ export const executeQueries = createAsyncThunk<
287286
executeDataTableQuery({
288287
services,
289288
cacheKey: dataTableCacheKey,
290-
queryString: defaultPrepareQueryString(query),
289+
queryString,
291290
})
292291
)
293292
);
@@ -300,7 +299,7 @@ export const executeQueries = createAsyncThunk<
300299
executeHistogramQuery({
301300
services,
302301
cacheKey: histogramCacheKey,
303-
queryString: defaultPrepareQueryString(query),
302+
queryString,
304303
interval,
305304
})
306305
);
@@ -538,13 +537,15 @@ const executeQueryBase = async (
538537
histogramConfig = createHistogramConfigWithInterval(dataView, interval, services, getState);
539538
}
540539

540+
let effectiveQuery = queryString;
541+
if (query.language === 'PPL' && histogramConfig && isHistogramQuery) {
542+
effectiveQuery = buildPPLHistogramQuery(queryString, histogramConfig);
543+
}
544+
541545
const preparedQueryObject = {
542546
...query,
543547
dataset,
544-
query:
545-
query.language === 'PPL' && isHistogramQuery && histogramConfig
546-
? buildPPLHistogramQuery(queryString, histogramConfig)
547-
: queryString,
548+
query: effectiveQuery,
548549
};
549550

550551
let searchSource;

src/plugins/explore/public/application/utils/state_management/actions/utils.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,46 @@ describe('Utils - Histogram Breakdown Support', () => {
113113
});
114114
});
115115

116+
describe('queryEndsWithHead', () => {
117+
it('should detect head at end of query', () => {
118+
expect(utils.queryEndsWithHead('source=logs | head 200')).toBe(true);
119+
});
120+
121+
it('should detect head with from clause', () => {
122+
expect(utils.queryEndsWithHead('source=logs | head 200 from 10')).toBe(true);
123+
});
124+
125+
it('should detect head without explicit count', () => {
126+
expect(utils.queryEndsWithHead('source=logs | head')).toBe(true);
127+
});
128+
129+
it('should detect head followed by where clause', () => {
130+
expect(utils.queryEndsWithHead('source=logs | head 200 | where status = 200')).toBe(true);
131+
});
132+
133+
it('should return false when no head is present', () => {
134+
expect(utils.queryEndsWithHead('source=logs | WHERE status = 200')).toBe(false);
135+
});
136+
137+
it('should return false when head is only inside subquery', () => {
138+
expect(utils.queryEndsWithHead('source=logs | where id in [source=other | head 10]')).toBe(
139+
false
140+
);
141+
});
142+
143+
it('should detect main query head even with subquery head', () => {
144+
expect(
145+
utils.queryEndsWithHead('source=logs | where id in [source=other | head 10] | head 200')
146+
).toBe(true);
147+
});
148+
149+
it('should return false when head is in the middle of the query', () => {
150+
expect(utils.queryEndsWithHead('source=logs | head 200 | stats count() by status')).toBe(
151+
false
152+
);
153+
});
154+
});
155+
116156
describe('buildPPLHistogramQuery', () => {
117157
it('should return original query when aggs is missing', () => {
118158
const query = 'source=logs';
@@ -144,6 +184,16 @@ describe('Utils - Histogram Breakdown Support', () => {
144184
const result = utils.buildPPLHistogramQuery(query, histogramConfig);
145185
expect(result).toBe('source=logs | stats count() by span(@timestamp, 1h)');
146186
});
187+
188+
it('should preserve head clause in histogram query', () => {
189+
const query = 'source=logs | head 200';
190+
const histogramConfig = createBaseHistogramConfig({
191+
aggs: { 2: { date_histogram: {} } },
192+
});
193+
194+
const result = utils.buildPPLHistogramQuery(query, histogramConfig);
195+
expect(result).toBe('source=logs | head 200 | stats count() by span(@timestamp, 1h)');
196+
});
147197
});
148198

149199
describe('processRawResultsForHistogram', () => {

src/plugins/explore/public/application/utils/state_management/actions/utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ export function fillMissingTimestamps(
103103
return filledSeriesMap;
104104
}
105105

106+
/**
107+
* Checks if the main query ends with a head command (optionally followed by `from N` or `| where`).
108+
* Subquery brackets [...] are masked so that head inside subqueries is ignored.
109+
*/
110+
export const queryEndsWithHead = (queryString: string): boolean => {
111+
const masked = queryString.replace(/\[.*?\]/g, (match) => '\0'.repeat(match.length));
112+
return /\|\s*head\b(\s+\d+)?(\s+from\s+\d+)?\s*(\|\s*where\b.*)?\s*$/i.test(masked);
113+
};
114+
106115
export const buildPPLHistogramQuery = (query: string, histogramConfig: HistogramConfig): string => {
107116
const { aggs, finalInterval, timeFieldName, breakdownField } = histogramConfig;
108117

@@ -223,7 +232,7 @@ export const processRawResultsForHistogram = (
223232

224233
Object.entries(responseAggs).forEach(([id, value]) => {
225234
if (aggsConfig && aggsConfig.date_histogram) {
226-
let totalHits = rawResults.hits.total;
235+
let totalHits = 0;
227236
const buckets = value as Array<{ key: string; value: number }>;
228237
tempResult.aggregations[id] = {
229238
buckets: buckets.map((bucket) => {

src/plugins/explore/public/components/tabs/action_bar/action_bar.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ jest.mock('../../../application/utils/hooks/use_histogram_results', () => ({
8787
}),
8888
}));
8989

90+
jest.mock('react-redux', () => ({
91+
...jest.requireActual('react-redux'),
92+
useSelector: () => ({ language: 'PPL', query: 'source=logs', dataset: { id: 'test' } }),
93+
}));
94+
95+
jest.mock('../../../application/utils/state_management/actions/query_actions', () => ({
96+
defaultPrepareQueryString: () => 'source=logs',
97+
}));
98+
99+
jest.mock('../../../application/utils/state_management/actions/utils', () => ({
100+
queryEndsWithHead: () => false,
101+
}));
102+
90103
describe('ActionBar', () => {
91104
test('should render the action bar component', () => {
92105
render(<ActionBar />);

src/plugins/explore/public/components/tabs/action_bar/action_bar.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import React, { memo, useMemo } from 'react';
66
import { useObservable } from 'react-use';
77
import { of } from 'rxjs';
8+
import { useSelector as useReduxSelector } from 'react-redux';
89
import { DiscoverResultsActionBar } from './results_action_bar/results_action_bar';
910
import { ExploreServices } from '../../../types';
1011
import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public';
@@ -14,6 +15,9 @@ import { useDatasetContext } from '../../../application/context';
1415
import { ExploreFlavor } from '../../../../common';
1516
import { useTabResults } from '../../../application/utils/hooks/use_tab_results';
1617
import { useHistogramResults } from '../../../application/utils/hooks/use_histogram_results';
18+
import { RootState } from '../../../application/utils/state_management/store';
19+
import { defaultPrepareQueryString } from '../../../application/utils/state_management/actions/query_actions';
20+
import { queryEndsWithHead } from '../../../application/utils/state_management/actions/utils';
1721

1822
interface ActionBarProps {
1923
filteredRowsCount?: number;
@@ -28,6 +32,7 @@ const ActionBarComponent = ({ filteredRowsCount }: ActionBarProps = {}) => {
2832
const { dataset } = useDatasetContext();
2933
const { results } = useTabResults();
3034
const { results: histogramResults } = useHistogramResults();
35+
const query = useReduxSelector((state: RootState) => state.query);
3136
const { core, inspector, inspectorAdapters, slotRegistry } = services;
3237
const savedSearch = useSelector(selectSavedSearch);
3338

@@ -45,7 +50,10 @@ const ActionBarComponent = ({ filteredRowsCount }: ActionBarProps = {}) => {
4550
};
4651

4752
const rows = results?.hits?.hits || [];
48-
const totalHits = histogramResults?.hits.total;
53+
// When query has head command, just show row count (no "X of Y" total)
54+
const queryString = query.language === 'PPL' ? defaultPrepareQueryString(query) : '';
55+
const hasHead = query.language === 'PPL' && queryEndsWithHead(queryString);
56+
const totalHits = hasHead ? undefined : histogramResults?.hits.total;
4957
const elapsedMs = results?.elapsedMs;
5058

5159
return (

0 commit comments

Comments
 (0)