Skip to content

Commit 500198b

Browse files
Fix first character being trimmed during Query Snippet Insertion (#10901)
* Fix first character being trimmed during Query Snippet Insertion Signed-off-by: Suchit Sahoo <[email protected]> * Changeset file for PR #10901 created/updated --------- Signed-off-by: Suchit Sahoo <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 42e9595 commit 500198b

File tree

3 files changed

+109
-16
lines changed

3 files changed

+109
-16
lines changed

changelogs/fragments/10901.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fix:
2+
- First character being trimmed during Query Snippet Insertion ([#10901](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10901))

src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.test.ts

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ describe('PPL Query Snippet Suggestions', () => {
7979
});
8080

8181
describe('convertQueryToMonacoSuggestion', () => {
82-
it('should convert queries to Monaco suggestions without user query', () => {
83-
const result = convertQueryToMonacoSuggestion(mockQueries, '');
82+
it('should convert queries to Monaco suggestions', () => {
83+
const result = convertQueryToMonacoSuggestion(mockQueries);
8484

8585
expect(result).toHaveLength(9); // 3 queries with 3, 3, 3 segments each
8686

@@ -98,8 +98,8 @@ describe('PPL Query Snippet Suggestions', () => {
9898
);
9999
});
100100

101-
it('should convert queries to Monaco suggestions with user query', () => {
102-
const result = convertQueryToMonacoSuggestion(mockQueries, 'source = logs | ');
101+
it('should not include insertText in base conversion', () => {
102+
const result = convertQueryToMonacoSuggestion(mockQueries);
103103

104104
expect(result).toHaveLength(9);
105105

@@ -122,7 +122,7 @@ describe('PPL Query Snippet Suggestions', () => {
122122
},
123123
];
124124

125-
const result = convertQueryToMonacoSuggestion(duplicateQueries, '');
125+
const result = convertQueryToMonacoSuggestion(duplicateQueries);
126126

127127
// Should only have 2 unique suggestions (source = logs, where status = "error")
128128
expect(result).toHaveLength(2);
@@ -142,7 +142,7 @@ describe('PPL Query Snippet Suggestions', () => {
142142

143143
const whereResult = result.find((s) => s.text.startsWith('where'));
144144
expect(whereResult).toBeDefined();
145-
expect(whereResult?.insertText).toBe('re status = "error" '); // Should complete "whe" to "where status = "error"" with trailing space
145+
expect(whereResult?.insertText).toBe('where status = "error" '); // Should complete "whe" to "where status = "error"" with trailing space
146146
});
147147

148148
it('should return empty array when no matching suggestions', async () => {
@@ -158,7 +158,7 @@ describe('PPL Query Snippet Suggestions', () => {
158158

159159
const statsResult = result.find((s) => s.text.startsWith('stats'));
160160
expect(statsResult).toBeDefined();
161-
expect(statsResult?.insertText).toBe('ats count() by host ');
161+
expect(statsResult?.insertText).toBe('stats count() by host ');
162162
});
163163

164164
it('should not return suggestions that match exactly', async () => {
@@ -190,5 +190,79 @@ describe('PPL Query Snippet Suggestions', () => {
190190

191191
expect(result).toHaveLength(0);
192192
});
193+
194+
describe('token-based insertText logic', () => {
195+
it('should handle partial token matching correctly', async () => {
196+
const result = await getPPLQuerySnippetForSuggestions('source = logs | wh');
197+
198+
const whereResult = result.find((s) => s.text.startsWith('where'));
199+
expect(whereResult).toBeDefined();
200+
expect(whereResult?.insertText).toBe('where status = "error" ');
201+
});
202+
203+
it('should skip fully matched tokens and insert remaining tokens', async () => {
204+
const result = await getPPLQuerySnippetForSuggestions('source = logs | where status');
205+
206+
const whereResult = result.find((s) => s.text.startsWith('where'));
207+
expect(whereResult).toBeDefined();
208+
expect(whereResult?.insertText).toBe('= "error" ');
209+
});
210+
211+
it('should handle multiple fully matched tokens', async () => {
212+
const result = await getPPLQuerySnippetForSuggestions('source = logs | where status =');
213+
214+
const whereResult = result.find((s) => s.text.startsWith('where'));
215+
expect(whereResult).toBeDefined();
216+
expect(whereResult?.insertText).toBe('"error" ');
217+
});
218+
219+
it('should return complete suggestion when no tokens match', async () => {
220+
const result = await getPPLQuerySnippetForSuggestions('source = logs | xyz');
221+
222+
expect(result).toHaveLength(0); // No suggestions should match 'xyz'
223+
});
224+
225+
it('should handle case-insensitive token matching', async () => {
226+
const result = await getPPLQuerySnippetForSuggestions('source = logs | WHERE STATUS');
227+
228+
const whereResult = result.find((s) => s.text.startsWith('where'));
229+
expect(whereResult).toBeDefined();
230+
expect(whereResult?.insertText).toBe('= "error" ');
231+
});
232+
233+
it('should handle single character partial match', async () => {
234+
const result = await getPPLQuerySnippetForSuggestions('source = logs | w');
235+
236+
const whereResult = result.find((s) => s.text.startsWith('where'));
237+
expect(whereResult).toBeDefined();
238+
expect(whereResult?.insertText).toBe('where status = "error" ');
239+
});
240+
241+
it('should handle exact token boundary matching', async () => {
242+
const result = await getPPLQuerySnippetForSuggestions('source = logs | where');
243+
244+
const whereResult = result.find((s) => s.text.startsWith('where'));
245+
expect(whereResult).toBeDefined();
246+
expect(whereResult?.insertText).toBe('status = "error" ');
247+
});
248+
249+
it('should handle suggestions with different token structures', async () => {
250+
const result = await getPPLQuerySnippetForSuggestions('source = metrics | f');
251+
252+
const fieldsResult = result.find((s) => s.text.startsWith('fields'));
253+
expect(fieldsResult).toBeDefined();
254+
expect(fieldsResult?.insertText).toBe('fields timestamp, cpu_usage ');
255+
});
256+
257+
it('should handle when suggestion has fewer tokens than user input', async () => {
258+
// Test with a complex user query that has more tokens than any single suggestion
259+
const result = await getPPLQuerySnippetForSuggestions(
260+
'source = logs | where status = "error" extra token'
261+
);
262+
263+
// Should have no matches since no suggestion starts with this complex pattern
264+
expect(result).toHaveLength(0);
265+
});
266+
});
193267
});
194268
});

src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ export const extractSnippetsFromQuery = (query: string) => {
1616
return pplQuerySegments;
1717
};
1818

19-
export const convertQueryToMonacoSuggestion = (
20-
queries: QuerySnippetItem[],
21-
userQuery: string
22-
): QuerySuggestion[] => {
19+
export const convertQueryToMonacoSuggestion = (queries: QuerySnippetItem[]): QuerySuggestion[] => {
2320
const textMap = new Map<string, QuerySuggestion>();
2421

2522
queries.forEach((query) => {
@@ -52,11 +49,12 @@ export const getPPLQuerySnippetForSuggestions = async (
5249
// Fetfch all User Queries
5350
const userQueries = await getUserPastQueries(languageId);
5451

55-
const suggestions = convertQueryToMonacoSuggestion(userQueries, userQuery);
52+
const suggestions = convertQueryToMonacoSuggestion(userQueries);
5653

5754
// Extract the last Segment from the query
5855
const userQuerySegments = userQuery.split('|');
5956
const currentUserQuerySegment = userQuerySegments.pop()?.trim().toLowerCase();
57+
const typedTokens = currentUserQuerySegment?.split(/\s+/);
6058

6159
// Using currentUserQuery to do a prefix filtering of the Query Segments
6260
if (currentUserQuerySegment) {
@@ -67,10 +65,29 @@ export const getPPLQuerySnippetForSuggestions = async (
6765
currentUserQuerySegment !== suggestion.text
6866
);
6967
})
70-
.map((suggestion) => ({
71-
...suggestion,
72-
insertText: suggestion.text.slice(currentUserQuerySegment.length).trim() + ' ',
73-
}));
68+
.map((suggestion) => {
69+
const suggestionTokens = suggestion.text.split(/\s+/);
70+
71+
let insertIndex = 0;
72+
73+
// Skip fully matched tokens only
74+
while (
75+
typedTokens &&
76+
insertIndex < typedTokens.length &&
77+
insertIndex < suggestionTokens.length &&
78+
suggestionTokens[insertIndex].toLowerCase() === typedTokens[insertIndex].toLowerCase()
79+
) {
80+
insertIndex++;
81+
}
82+
83+
// Insert from the first partially matched token onwards
84+
const remainingTokens = suggestionTokens.slice(insertIndex).join(' ');
85+
86+
return {
87+
...suggestion,
88+
insertText: remainingTokens + ' ',
89+
};
90+
});
7491
}
7592

7693
return [];

0 commit comments

Comments
 (0)