Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit 5d1d897

Browse files
committed
Unwrap JSON array values in ad-hoc equality filters
When Grafana passes a stringified array like ["paperclip"] as a filter value, detect it as JSON array and generate term queries on the individual elements instead of a phrase query on the literal string. Single element: attributes.tags:paperclip Multiple elements: attributes.tags:paperclip OR attributes.tags:stapler Empty array: no-op Fixes quickwit-oss/quickwit-datasource#179
1 parent 4921df4 commit 5d1d897

2 files changed

Lines changed: 64 additions & 13 deletions

File tree

src/modifyQuery.test.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,53 @@
11
import { addAddHocFilter } from './modifyQuery';
22

33
describe('addAddHocFilter', () => {
4-
describe('current behavior with array values', () => {
5-
it('wraps equality filter value in quotes (phrase query)', () => {
4+
describe('array values', () => {
5+
it('unwraps single-element array into a term query', () => {
66
const result = addAddHocFilter('', {
77
key: 'attributes.tags',
88
operator: '=',
99
value: '["paperclip"]',
1010
});
11-
// Current behavior: generates a phrase query with the stringified array
12-
expect(result).toBe('attributes.tags:"[\\"paperclip\\"]"');
11+
expect(result).toBe('attributes.tags:paperclip');
1312
});
1413

15-
it('wraps negated equality filter value in quotes', () => {
14+
it('unwraps multi-element array into OR of term queries', () => {
15+
const result = addAddHocFilter('', {
16+
key: 'attributes.tags',
17+
operator: '=',
18+
value: '["paperclip","stapler"]',
19+
});
20+
expect(result).toBe('attributes.tags:paperclip OR attributes.tags:stapler');
21+
});
22+
23+
it('negated array produces negated term queries', () => {
1624
const result = addAddHocFilter('', {
1725
key: 'attributes.tags',
1826
operator: '!=',
1927
value: '["paperclip"]',
2028
});
21-
expect(result).toBe('-attributes.tags:"[\\"paperclip\\"]"');
29+
expect(result).toBe('-attributes.tags:paperclip');
2230
});
2331

24-
it('term operator produces unquoted query', () => {
32+
it('appends array filter to existing query with AND', () => {
33+
const result = addAddHocFilter('status:200', {
34+
key: 'attributes.tags',
35+
operator: '=',
36+
value: '["paperclip"]',
37+
});
38+
expect(result).toBe('status:200 AND attributes.tags:paperclip');
39+
});
40+
41+
it('passes through non-array bracket strings unchanged', () => {
42+
const result = addAddHocFilter('', {
43+
key: 'attributes.message',
44+
operator: '=',
45+
value: '[not json',
46+
});
47+
expect(result).toBe('attributes.message:"[not json"');
48+
});
49+
50+
it('term operator still produces unquoted query', () => {
2551
const result = addAddHocFilter('', {
2652
key: 'attributes.tags',
2753
operator: 'term',
@@ -30,7 +56,7 @@ describe('addAddHocFilter', () => {
3056
expect(result).toBe('attributes.tags:paperclip');
3157
});
3258

33-
it('not term operator produces negated unquoted query', () => {
59+
it('not term operator still produces negated unquoted query', () => {
3460
const result = addAddHocFilter('', {
3561
key: 'attributes.tags',
3662
operator: 'not term',
@@ -124,14 +150,13 @@ describe('addAddHocFilter', () => {
124150
expect(result).toBe('existing');
125151
});
126152

127-
it('handles multi-element array value with equality', () => {
128-
const result = addAddHocFilter('', {
153+
it('treats empty JSON array as no-op', () => {
154+
const result = addAddHocFilter('existing', {
129155
key: 'attributes.tags',
130156
operator: '=',
131-
value: '["paperclip","stapler"]',
157+
value: '[]',
132158
});
133-
// Current behavior: entire stringified array becomes the phrase
134-
expect(result).toBe('attributes.tags:"[\\"paperclip\\",\\"stapler\\"]"');
159+
expect(result).toBe('existing');
135160
});
136161
});
137162
});

src/modifyQuery.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import { escapeFilter, escapeFilterValue, concatenate, LuceneQuery } from 'utils/lucene';
22
import { AdHocVariableFilter } from '@grafana/data';
33

4+
function tryParseJsonArray(value: string): string[] | null {
5+
if (!value.startsWith('[')) {
6+
return null;
7+
}
8+
try {
9+
const parsed = JSON.parse(value);
10+
if (Array.isArray(parsed) && parsed.every((el) => typeof el === 'string')) {
11+
return parsed;
12+
}
13+
} catch {
14+
// not valid JSON
15+
}
16+
return null;
17+
}
18+
419
/**
520
* Adds a label:"value" expression to the query.
621
*/
@@ -18,6 +33,17 @@ export function addAddHocFilter(query: string, filter: AdHocVariableFilter): str
1833

1934
const equalityFilters = ['=', '!='];
2035
if (equalityFilters.includes(filter.operator)) {
36+
const arrayElements = tryParseJsonArray(filter.value);
37+
if (arrayElements !== null) {
38+
if (arrayElements.length === 0) {
39+
return query;
40+
}
41+
const modifier = filter.operator === '=' ? '' : '-';
42+
const key = escapeFilter(filter.key);
43+
const termFilters = arrayElements.map((el) => `${modifier}${key}:${escapeFilterValue(el)}`);
44+
const combined = termFilters.join(' OR ');
45+
return concatenate(query, combined, 'AND');
46+
}
2147
return LuceneQuery.parse(query).addFilter(filter.key, filter.value, filter.operator === '=' ? '' : '-').toString();
2248
}
2349
/**

0 commit comments

Comments
 (0)