mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-23 14:30:37 +01:00
Compare commits
12 Commits
b3-search
...
issue_5721
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22833ea8fe | ||
|
|
8eb0a24492 | ||
|
|
7e0b19800c | ||
|
|
3bc476c2ed | ||
|
|
6a834ee944 | ||
|
|
ba6a78aea7 | ||
|
|
ca98231b18 | ||
|
|
1644ecd6fc | ||
|
|
f1e2e9f4f7 | ||
|
|
a59e372f07 | ||
|
|
ca368a5b38 | ||
|
|
dab5ceee0d |
@@ -1496,6 +1496,7 @@ components:
|
||||
- redis
|
||||
- cloudsql_postgres
|
||||
- memorystore_redis
|
||||
- computeengine
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
|
||||
@@ -2815,6 +2815,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
redis = 'redis',
|
||||
cloudsql_postgres = 'cloudsql_postgres',
|
||||
memorystore_redis = 'memorystore_redis',
|
||||
computeengine = 'computeengine',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
|
||||
@@ -52,8 +52,6 @@ import type { SignalType } from 'types/api/v5/queryRange';
|
||||
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getRecentOptions,
|
||||
renderRecentDeleteButton,
|
||||
} from './utils';
|
||||
@@ -224,12 +222,6 @@ function QuerySearch({
|
||||
QueryKeyDataSuggestionsProps[] | null
|
||||
>(null);
|
||||
|
||||
// dedupe keySuggestions by label/name
|
||||
const dedupedKeySuggestions = useMemo(
|
||||
() => dedupeOptionsByLabel(keySuggestions || []),
|
||||
[keySuggestions],
|
||||
);
|
||||
|
||||
const [showExamples] = useState(false);
|
||||
|
||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||
@@ -254,11 +246,9 @@ function QuerySearch({
|
||||
[key: string]: QueryKeyDataSuggestionsProps[];
|
||||
}): any[] =>
|
||||
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
items.map(({ name, fieldDataType }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
fieldContext,
|
||||
fieldDataType,
|
||||
info: '',
|
||||
details: '',
|
||||
})),
|
||||
@@ -317,17 +307,13 @@ function QuerySearch({
|
||||
if (response.data.data) {
|
||||
const { keys } = response.data.data;
|
||||
const options = generateOptions(keys);
|
||||
// Deduplicate by full variant identity (name + context + data type), NOT by
|
||||
// label. deduping by label removes varient which is not expected. If we need
|
||||
// to dedupe by label use dedupedKeySuggestions not dedupe the source itself
|
||||
const variantId = (opt: QueryKeyDataSuggestionsProps): string =>
|
||||
`${opt.label}|${opt.fieldContext ?? ''}|${opt.fieldDataType ?? ''}`;
|
||||
// Use a Map to deduplicate by label and preserve order: new options take precedence
|
||||
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
|
||||
options.forEach((opt) => merged.set(variantId(opt), opt));
|
||||
options.forEach((opt) => merged.set(opt.label, opt));
|
||||
if (searchText && lastKeyRef.current !== searchText) {
|
||||
(keySuggestions || []).forEach((opt) => {
|
||||
if (!merged.has(variantId(opt))) {
|
||||
merged.set(variantId(opt), opt);
|
||||
if (!merged.has(opt.label)) {
|
||||
merged.set(opt.label, opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -933,55 +919,8 @@ function QuerySearch({
|
||||
|
||||
if (queryContext.isInKey) {
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
const fieldContextMatch = getFieldContextPrefix(searchText);
|
||||
|
||||
if (fieldContextMatch) {
|
||||
const { context: fieldContext, remainder } = fieldContextMatch;
|
||||
|
||||
// Fetch the context's page when the prefix is typed exactly eg.("attribute.")
|
||||
if (remainder === '' && lastFetchedKeyRef.current !== searchText) {
|
||||
debouncedFetchKeySuggestions(searchText);
|
||||
}
|
||||
|
||||
//suggestions that actually do start with <fieldContext>.
|
||||
const nameMatches = (keySuggestions || [])
|
||||
.filter((option) => option.label.toLowerCase().includes(searchText))
|
||||
.map((option) => ({ ...option, boost: 100 }));
|
||||
|
||||
//suggestions which do not start with the prefix but qualifies for suggestion
|
||||
const contextQualified = (keySuggestions || [])
|
||||
.filter(
|
||||
(option) =>
|
||||
option.fieldContext === fieldContext &&
|
||||
option.label.toLowerCase().includes(remainder),
|
||||
)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
label: `${fieldContext}.${option.label}`,
|
||||
boost: 0,
|
||||
}));
|
||||
|
||||
const contextOptions = dedupeOptionsByLabel([
|
||||
...nameMatches,
|
||||
...contextQualified,
|
||||
]);
|
||||
|
||||
// If contextOptions is empty fetch again.
|
||||
if (
|
||||
contextOptions.length === 0 &&
|
||||
lastFetchedKeyRef.current !== searchText
|
||||
) {
|
||||
debouncedFetchKeySuggestions(searchText);
|
||||
}
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: addSpaceToOptions(contextOptions),
|
||||
};
|
||||
}
|
||||
|
||||
options = dedupedKeySuggestions.filter((option) =>
|
||||
options = (keySuggestions || []).filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
@@ -1030,26 +969,9 @@ function QuerySearch({
|
||||
|
||||
// If we have a key context, add that info to the operator suggestions
|
||||
if (keyName) {
|
||||
const keyContextMatch = getFieldContextPrefix(keyName);
|
||||
// key-suggestion can contain multiple variants of a single key
|
||||
// In variants we capture ones that match the label to typed keyName exactly or,
|
||||
// if it has a prefix fieldContext remove it and then match.
|
||||
const variants = (keySuggestions || []).filter(
|
||||
(k) =>
|
||||
k.label === keyName ||
|
||||
(keyContextMatch !== null &&
|
||||
k.fieldContext === keyContextMatch.context &&
|
||||
k.label === keyContextMatch.remainder),
|
||||
);
|
||||
const variantTypes = new Set(
|
||||
variants
|
||||
.map((k) =>
|
||||
k.type === 'keyword' ? QUERY_BUILDER_KEY_TYPES.STRING : k.type,
|
||||
)
|
||||
.filter(Boolean),
|
||||
);
|
||||
//if there are multi-variant, show all suggestions else just the one
|
||||
const keyType = variantTypes.size === 1 ? [...variantTypes][0] : '';
|
||||
// Find the key details from suggestions
|
||||
const keyDetails = (keySuggestions || []).find((k) => k.label === keyName);
|
||||
const keyType = keyDetails?.type || '';
|
||||
|
||||
// Filter operators based on key type
|
||||
if (keyType) {
|
||||
@@ -1292,7 +1214,7 @@ function QuerySearch({
|
||||
if (curChar === '(') {
|
||||
// In expression context, suggest keys, functions, or nested parentheses
|
||||
options = [
|
||||
...dedupedKeySuggestions,
|
||||
...(keySuggestions || []),
|
||||
{ label: '(', type: 'parenthesis', info: 'Open nested group' },
|
||||
{ label: 'NOT', type: 'operator', info: 'Negate expression' },
|
||||
...options.filter((opt) => opt.type === 'function'),
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
describe('combineInitialAndUserExpression', () => {
|
||||
it('returns user when initial is empty', () => {
|
||||
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
|
||||
'body contains error',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns initial when user is empty', () => {
|
||||
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
|
||||
'k8s.pod.name = "x"',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps user in parentheses with AND', () => {
|
||||
expect(
|
||||
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
|
||||
).toBe('k8s.pod.name = "x" AND (body = "a")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserExpressionFromCombined', () => {
|
||||
it('returns empty when combined equals initial', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('extracts user from wrapped form', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND (body = "a")',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('extracts user from legacy AND without parens', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND body = "a"',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('returns full combined when initial is empty', () => {
|
||||
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
|
||||
'service.name = "a"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldContextPrefix', () => {
|
||||
it('matches a complete context prefix with a remainder', () => {
|
||||
expect(getFieldContextPrefix('attribute.status')).toStrictEqual({
|
||||
context: 'attribute',
|
||||
remainder: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches a bare context prefix with empty remainder', () => {
|
||||
expect(getFieldContextPrefix('resource.')).toStrictEqual({
|
||||
context: 'resource',
|
||||
remainder: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches every backend field context', () => {
|
||||
['attribute', 'resource', 'span', 'body', 'log', 'metric'].forEach((ctx) => {
|
||||
expect(getFieldContextPrefix(`${ctx}.x`)).toStrictEqual({
|
||||
context: ctx,
|
||||
remainder: 'x',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not match a partial context name', () => {
|
||||
expect(getFieldContextPrefix('attr')).toBeNull();
|
||||
expect(getFieldContextPrefix('attribute')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not match a non-context key with dots', () => {
|
||||
expect(getFieldContextPrefix('status.code')).toBeNull();
|
||||
});
|
||||
|
||||
it('matches context case-insensitively but keeps remainder casing', () => {
|
||||
expect(getFieldContextPrefix('Attribute.Status')).toStrictEqual({
|
||||
context: 'attribute',
|
||||
remainder: 'Status',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dedupeOptionsByLabel', () => {
|
||||
it('keeps the first occurrence per label, preserving order', () => {
|
||||
expect(
|
||||
dedupeOptionsByLabel([
|
||||
{ label: 'status.code', type: 'keyword' },
|
||||
{ label: 'status.code', type: 'number' },
|
||||
{ label: 'duration', type: 'number' },
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{ label: 'status.code', type: 'keyword' },
|
||||
{ label: 'duration', type: 'number' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array for empty input', () => {
|
||||
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
describe('combineInitialAndUserExpression', () => {
|
||||
it('returns user when initial is empty', () => {
|
||||
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
|
||||
'body contains error',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns initial when user is empty', () => {
|
||||
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
|
||||
'k8s.pod.name = "x"',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps user in parentheses with AND', () => {
|
||||
expect(
|
||||
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
|
||||
).toBe('k8s.pod.name = "x" AND (body = "a")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserExpressionFromCombined', () => {
|
||||
it('returns empty when combined equals initial', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('extracts user from wrapped form', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND (body = "a")',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('extracts user from legacy AND without parens', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND body = "a"',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('returns full combined when initial is empty', () => {
|
||||
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
|
||||
'service.name = "a"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,6 @@
|
||||
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
|
||||
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
|
||||
|
||||
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
|
||||
export const FIELD_CONTEXTS = [
|
||||
'attribute',
|
||||
'resource',
|
||||
'span',
|
||||
'body',
|
||||
'log',
|
||||
'metric',
|
||||
] as const;
|
||||
|
||||
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
|
||||
// recents from regular autocomplete completions in renderers and event handlers.
|
||||
export const RECENT_COMPLETION_TYPE = 'recent';
|
||||
|
||||
@@ -9,45 +9,11 @@ import type { SignalType } from 'types/api/v5/queryRange';
|
||||
import 'utils/timeUtils';
|
||||
|
||||
import {
|
||||
FIELD_CONTEXTS,
|
||||
RECENT_COMPLETION_TYPE,
|
||||
RECENTS_DISPLAY_CAP,
|
||||
RECENTS_SECTION,
|
||||
} from './constants';
|
||||
|
||||
export interface FieldContextPrefixMatch {
|
||||
context: string;
|
||||
remainder: string;
|
||||
}
|
||||
|
||||
// This function checks if the text(query key) starts with a fieldContext
|
||||
// This util strictly checks that and returns context and remainder back.
|
||||
// This helps differentiate if the typed key was prefixed with a context or
|
||||
// was it an actual queryKey like (attribute.abc)
|
||||
export function getFieldContextPrefix(
|
||||
text: string,
|
||||
): FieldContextPrefixMatch | null {
|
||||
const lower = text.toLowerCase();
|
||||
const context = FIELD_CONTEXTS.find((ctx) => lower.startsWith(`${ctx}.`));
|
||||
return context ? { context, remainder: text.slice(context.length + 1) } : null;
|
||||
}
|
||||
|
||||
// Keeps the first occurrence per label, preserving order. Key suggestions hold
|
||||
// one entry per (name, fieldContext, fieldDataType) variant; This means query builder
|
||||
// could show multiple labels and this avoids that.
|
||||
export function dedupeOptionsByLabel<T extends { label: string }>(
|
||||
options: T[],
|
||||
): T[] {
|
||||
const seen = new Set<string>();
|
||||
return options.filter((option) => {
|
||||
if (seen.has(option.label)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.label);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function combineInitialAndUserExpression(
|
||||
initial: string,
|
||||
user: string,
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
const CM_OPTION_SELECTOR = '.cm-tooltip-autocomplete .cm-completionLabel';
|
||||
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
});
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
const handleRunQuery = jest.fn();
|
||||
return {
|
||||
__esModule: true,
|
||||
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
|
||||
handleRunQuery,
|
||||
};
|
||||
});
|
||||
|
||||
// Keys fixture: status.code exists as 3 variants (attribute string/number +
|
||||
// resource string); attribute.a.b.c is a key literally named with the prefix.
|
||||
const KEYS_FIXTURE = {
|
||||
'status.code': [
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'number',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'resource',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
'attribute.a.b.c': [
|
||||
{
|
||||
name: 'attribute.a.b.c',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
'duration.nano': [
|
||||
{
|
||||
name: 'duration.nano',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'number',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fetchedSearchTexts: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
fetchedSearchTexts.length = 0;
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/keys', (req, res, ctx) => {
|
||||
fetchedSearchTexts.push(req.url.searchParams.get('searchText') ?? '');
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: KEYS_FIXTURE },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { values: { stringValues: [], numberValues: [] } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
async function renderAndType(text: string): Promise<HTMLElement> {
|
||||
render(
|
||||
<QuerySearch
|
||||
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||
queryData={initialQueriesMap.logs.builder.queryData[0]}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(CM_EDITOR_SELECTOR)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||
await userEvent.click(editor);
|
||||
await userEvent.type(editor, text);
|
||||
return editor;
|
||||
}
|
||||
|
||||
// Types a key, waits for its suggestion to render (proving the debounced key
|
||||
// fetch resolved into state), then types a space to enter operator context.
|
||||
async function typeKeyThenEnterOperatorContext(
|
||||
key: string,
|
||||
expectedKeyLabel: string,
|
||||
): Promise<void> {
|
||||
const editor = await renderAndType(key);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain(expectedKeyLabel);
|
||||
});
|
||||
|
||||
// skipClick: a click would reset the caret to position 0 under the mocked
|
||||
// DOM rects; we need the space appended at the end of the typed key.
|
||||
await userEvent.type(editor, ' ', { skipClick: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('=');
|
||||
});
|
||||
}
|
||||
|
||||
function visibleOptionLabels(): string[] {
|
||||
return Array.from(document.querySelectorAll(CM_OPTION_SELECTOR)).map(
|
||||
(el) => el.textContent ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
describe('QuerySearch context-prefixed key suggestions', () => {
|
||||
it('shows one deduped suggestion per key name in normal mode', async () => {
|
||||
await renderAndType('statu');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('status.code');
|
||||
});
|
||||
|
||||
const statusOptions = visibleOptionLabels().filter(
|
||||
(label) => label === 'status.code',
|
||||
);
|
||||
expect(statusOptions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows context-scoped suggestions for a complete context prefix', async () => {
|
||||
await renderAndType('attribute.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('attribute.status.code');
|
||||
});
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
// Literal key name match ranks first
|
||||
expect(labels[0]).toBe('attribute.a.b.c');
|
||||
// Context-qualified form of the literal key is also present
|
||||
expect(labels).toContain('attribute.attribute.a.b.c');
|
||||
// span-only key is not suggested under attribute.
|
||||
expect(labels).not.toContain('attribute.duration.nano');
|
||||
});
|
||||
|
||||
it('fetches the context page when the prefix is typed exactly', async () => {
|
||||
await renderAndType('attribute.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchedSearchTexts).toContain('attribute.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('QuerySearch operator suggestions by key type', () => {
|
||||
it('shows all operators for a key with ambiguous types', async () => {
|
||||
// status.code is string + number across variants → no type filtering
|
||||
await typeKeyThenEnterOperatorContext('status.code', 'status.code');
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('>');
|
||||
expect(labels).toContain('LIKE');
|
||||
});
|
||||
|
||||
it('shows numeric operators for a single-type number key', async () => {
|
||||
await typeKeyThenEnterOperatorContext('duration.nano', 'duration.nano');
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('>');
|
||||
expect(labels).not.toContain('LIKE');
|
||||
});
|
||||
|
||||
it('narrows operators when a context prefix disambiguates the type', async () => {
|
||||
// status.code is ambiguous globally, but resource.status.code is string-only
|
||||
await typeKeyThenEnterOperatorContext(
|
||||
'resource.status.code',
|
||||
'resource.status.code',
|
||||
);
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('LIKE');
|
||||
expect(labels).not.toContain('>');
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,78 @@ import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/ty
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
|
||||
// Mock DOM APIs that CodeMirror needs
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
// Mock getClientRects and getBoundingClientRect for Range objects
|
||||
const mockRect: DOMRect = {
|
||||
width: 100,
|
||||
height: 20,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: (): DOMRect => mockRect,
|
||||
} as DOMRect;
|
||||
|
||||
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||
const createMockRange = (): Range => {
|
||||
let startContainer: Node = document.createTextNode('');
|
||||
let endContainer: Node = document.createTextNode('');
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
const mockRange = {
|
||||
// CodeMirror uses these for text measurement
|
||||
getClientRects: (): DOMRectList =>
|
||||
({
|
||||
length: 1,
|
||||
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||
0: mockRect,
|
||||
*[Symbol.iterator](): Generator<DOMRect> {
|
||||
yield mockRect;
|
||||
},
|
||||
}) as unknown as DOMRectList,
|
||||
getBoundingClientRect: (): DOMRect => mockRect,
|
||||
// CodeMirror calls these to set up text ranges
|
||||
setStart: (node: Node, offset: number): void => {
|
||||
startContainer = node;
|
||||
startOffset = offset;
|
||||
},
|
||||
setEnd: (node: Node, offset: number): void => {
|
||||
endContainer = node;
|
||||
endOffset = offset;
|
||||
},
|
||||
// Minimal Range properties (TypeScript requires these)
|
||||
get startContainer(): Node {
|
||||
return startContainer;
|
||||
},
|
||||
get endContainer(): Node {
|
||||
return endContainer;
|
||||
},
|
||||
get startOffset(): number {
|
||||
return startOffset;
|
||||
},
|
||||
get endOffset(): number {
|
||||
return endOffset;
|
||||
},
|
||||
get collapsed(): boolean {
|
||||
return startContainer === endContainer && startOffset === endOffset;
|
||||
},
|
||||
commonAncestorContainer: document.body,
|
||||
};
|
||||
return mockRange as unknown as Range;
|
||||
};
|
||||
|
||||
// Mock document.createRange to return a new Range instance each time
|
||||
document.createRange = (): Range => createMockRange();
|
||||
|
||||
// Mock getBoundingClientRect for elements
|
||||
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||
});
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Mocks the DOM measurement APIs CodeMirror needs to render in jsdom
|
||||
// (Range client rects + element bounding rects). Call from a beforeAll in
|
||||
// specs that render the real CodeMirror editor.
|
||||
export function mockCodeMirrorDomApis(): void {
|
||||
const mockRect: DOMRect = {
|
||||
width: 100,
|
||||
height: 20,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: (): DOMRect => mockRect,
|
||||
} as DOMRect;
|
||||
|
||||
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||
const createMockRange = (): Range => {
|
||||
let startContainer: Node = document.createTextNode('');
|
||||
let endContainer: Node = document.createTextNode('');
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
const mockRange = {
|
||||
// CodeMirror uses these for text measurement
|
||||
getClientRects: (): DOMRectList =>
|
||||
({
|
||||
length: 1,
|
||||
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||
0: mockRect,
|
||||
*[Symbol.iterator](): Generator<DOMRect> {
|
||||
yield mockRect;
|
||||
},
|
||||
}) as unknown as DOMRectList,
|
||||
getBoundingClientRect: (): DOMRect => mockRect,
|
||||
// CodeMirror calls these to set up text ranges
|
||||
setStart: (node: Node, offset: number): void => {
|
||||
startContainer = node;
|
||||
startOffset = offset;
|
||||
},
|
||||
setEnd: (node: Node, offset: number): void => {
|
||||
endContainer = node;
|
||||
endOffset = offset;
|
||||
},
|
||||
// Minimal Range properties (TypeScript requires these)
|
||||
get startContainer(): Node {
|
||||
return startContainer;
|
||||
},
|
||||
get endContainer(): Node {
|
||||
return endContainer;
|
||||
},
|
||||
get startOffset(): number {
|
||||
return startOffset;
|
||||
},
|
||||
get endOffset(): number {
|
||||
return endOffset;
|
||||
},
|
||||
get collapsed(): boolean {
|
||||
return startContainer === endContainer && startOffset === endOffset;
|
||||
},
|
||||
commonAncestorContainer: document.body,
|
||||
};
|
||||
return mockRange as unknown as Range;
|
||||
};
|
||||
|
||||
// Mock document.createRange to return a new Range instance each time
|
||||
document.createRange = (): Range => createMockRange();
|
||||
|
||||
// Mock getBoundingClientRect for elements
|
||||
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||
}
|
||||
@@ -32,12 +32,11 @@ unaryExpression
|
||||
;
|
||||
|
||||
// Primary constructs: grouped expressions, a comparison (key op value),
|
||||
// a function call, a search() full-text call, or a free-text string
|
||||
// a function call, or a full-text string
|
||||
primary
|
||||
: LPAREN orExpression RPAREN
|
||||
| comparison
|
||||
| functionCall
|
||||
| searchCall
|
||||
| fullText
|
||||
| key
|
||||
| value
|
||||
@@ -111,18 +110,6 @@ functionCall
|
||||
: (HASTOKEN | HAS | HASANY | HASALL) LPAREN functionParamList RPAREN
|
||||
;
|
||||
|
||||
/*
|
||||
* Full-text search call: search('needle')
|
||||
*
|
||||
* Uses the shared functionParamList so future scoped forms like
|
||||
* search(body, 'abc') / search(attribute, 'abc') need no grammar change. Today
|
||||
* only a single needle is supported. Unlike bare/quoted free text (`fullText`),
|
||||
* which only targets the body column, search() fans out across every field.
|
||||
*/
|
||||
searchCall
|
||||
: SEARCH LPAREN functionParamList RPAREN
|
||||
;
|
||||
|
||||
// Function parameters can be keys, single scalar values, or arrays
|
||||
functionParamList
|
||||
: functionParam ( COMMA functionParam )*
|
||||
@@ -197,7 +184,6 @@ HASTOKEN : [Hh][Aa][Ss][Tt][Oo][Kk][Ee][Nn];
|
||||
HAS : [Hh][Aa][Ss] ;
|
||||
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
|
||||
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
|
||||
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
|
||||
|
||||
// Potential boolean constants
|
||||
BOOL
|
||||
|
||||
@@ -101,12 +101,12 @@ func PrepareFilterExpression(labels map[string]string, whereClause string, group
|
||||
}
|
||||
|
||||
// Visit implements the visitor for the query rule.
|
||||
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) any {
|
||||
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) interface{} {
|
||||
return tree.Accept(r)
|
||||
}
|
||||
|
||||
// VisitQuery visits the query node.
|
||||
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) any {
|
||||
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) interface{} {
|
||||
if ctx.Expression() != nil {
|
||||
ctx.Expression().Accept(r)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) any {
|
||||
}
|
||||
|
||||
// VisitExpression visits the expression node.
|
||||
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) interface{} {
|
||||
if ctx.OrExpression() != nil {
|
||||
ctx.OrExpression().Accept(r)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) any
|
||||
}
|
||||
|
||||
// VisitOrExpression visits OR expressions.
|
||||
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) interface{} {
|
||||
for i, andExpr := range ctx.AllAndExpression() {
|
||||
if i > 0 {
|
||||
r.rewritten.WriteString(" OR ")
|
||||
@@ -133,7 +133,7 @@ func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext)
|
||||
}
|
||||
|
||||
// VisitAndExpression visits AND expressions.
|
||||
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) interface{} {
|
||||
unaryExprs := ctx.AllUnaryExpression()
|
||||
for i, unaryExpr := range unaryExprs {
|
||||
if i > 0 {
|
||||
@@ -151,7 +151,7 @@ func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContex
|
||||
}
|
||||
|
||||
// VisitUnaryExpression visits unary expressions (with optional NOT).
|
||||
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) interface{} {
|
||||
if ctx.NOT() != nil {
|
||||
r.rewritten.WriteString("NOT ")
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionCo
|
||||
}
|
||||
|
||||
// VisitPrimary visits primary expressions.
|
||||
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
|
||||
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface{} {
|
||||
if ctx.LPAREN() != nil && ctx.RPAREN() != nil {
|
||||
r.rewritten.WriteString("(")
|
||||
if ctx.OrExpression() != nil {
|
||||
@@ -173,8 +173,6 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
|
||||
ctx.Comparison().Accept(r)
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
ctx.FunctionCall().Accept(r)
|
||||
} else if ctx.SearchCall() != nil {
|
||||
ctx.SearchCall().Accept(r)
|
||||
} else if ctx.FullText() != nil {
|
||||
ctx.FullText().Accept(r)
|
||||
} else if ctx.Key() != nil {
|
||||
@@ -186,7 +184,7 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
|
||||
}
|
||||
|
||||
// VisitComparison visits comparison expressions.
|
||||
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any {
|
||||
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) interface{} {
|
||||
if ctx.Key() == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -199,7 +197,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
|
||||
if _, partOfGroup := r.groupBySet[key]; partOfGroup {
|
||||
// Case 1: Replace with actual value
|
||||
escapedValue := escapeValueIfNeeded(value)
|
||||
fmt.Fprintf(&r.rewritten, "%s=%s", key, escapedValue)
|
||||
r.rewritten.WriteString(fmt.Sprintf("%s=%s", key, escapedValue))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -307,7 +305,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
|
||||
}
|
||||
|
||||
// VisitInClause visits IN clauses.
|
||||
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) any {
|
||||
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) interface{} {
|
||||
r.rewritten.WriteString("IN ")
|
||||
if ctx.LPAREN() != nil {
|
||||
r.rewritten.WriteString("(")
|
||||
@@ -328,7 +326,7 @@ func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) any {
|
||||
}
|
||||
|
||||
// VisitNotInClause visits NOT IN clauses.
|
||||
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) any {
|
||||
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) interface{} {
|
||||
r.rewritten.WriteString("NOT IN ")
|
||||
if ctx.LPAREN() != nil {
|
||||
r.rewritten.WriteString("(")
|
||||
@@ -349,7 +347,7 @@ func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) a
|
||||
}
|
||||
|
||||
// VisitValueList visits value lists.
|
||||
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) any {
|
||||
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) interface{} {
|
||||
values := ctx.AllValue()
|
||||
for i, val := range values {
|
||||
if i > 0 {
|
||||
@@ -361,20 +359,13 @@ func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) any {
|
||||
}
|
||||
|
||||
// VisitFullText visits full text expressions.
|
||||
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) any {
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitSearchCall visits search() calls. It has no keys to rewrite, so it is
|
||||
// preserved verbatim.
|
||||
func (r *WhereClauseRewriter) VisitSearchCall(ctx *parser.SearchCallContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) interface{} {
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitFunctionCall visits function calls.
|
||||
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) interface{} {
|
||||
// Write function name
|
||||
if ctx.HAS() != nil {
|
||||
r.rewritten.WriteString("has")
|
||||
@@ -395,7 +386,7 @@ func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext)
|
||||
}
|
||||
|
||||
// VisitFunctionParamList visits function parameter lists.
|
||||
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) interface{} {
|
||||
params := ctx.AllFunctionParam()
|
||||
for i, param := range params {
|
||||
if i > 0 {
|
||||
@@ -407,7 +398,7 @@ func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamLi
|
||||
}
|
||||
|
||||
// VisitFunctionParam visits function parameters.
|
||||
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) interface{} {
|
||||
if ctx.Key() != nil {
|
||||
ctx.Key().Accept(r)
|
||||
} else if ctx.Value() != nil {
|
||||
@@ -419,7 +410,7 @@ func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContex
|
||||
}
|
||||
|
||||
// VisitArray visits array expressions.
|
||||
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) any {
|
||||
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) interface{} {
|
||||
r.rewritten.WriteString("[")
|
||||
if ctx.ValueList() != nil {
|
||||
ctx.ValueList().Accept(r)
|
||||
@@ -429,13 +420,13 @@ func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) any {
|
||||
}
|
||||
|
||||
// VisitValue visits value expressions.
|
||||
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) any {
|
||||
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) interface{} {
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitKey visits key expressions.
|
||||
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) any {
|
||||
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) interface{} {
|
||||
r.keysSeen[ctx.GetText()] = struct{}{}
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
|
||||
@@ -29,19 +29,13 @@ func New(t *testing.T) flagger.Flagger {
|
||||
|
||||
// WithUseJSONBody returns a Flagger with use_json_body set to the given value.
|
||||
func WithUseJSONBody(t *testing.T, enabled bool) flagger.Flagger {
|
||||
return WithBooleanFlags(t, map[string]bool{
|
||||
flagger.FeatureUseJSONBody.String(): enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// WithBooleanFlags returns a Flagger with the given boolean feature flags set to
|
||||
// the provided values (keyed by feature name, e.g. flagger.FeatureX.String()).
|
||||
func WithBooleanFlags(t *testing.T, flags map[string]bool) flagger.Flagger {
|
||||
t.Helper()
|
||||
registry := flagger.MustNewRegistry()
|
||||
cfg := flagger.Config{}
|
||||
if len(flags) > 0 {
|
||||
cfg.Config.Boolean = flags
|
||||
if enabled {
|
||||
cfg.Config.Boolean = map[string]bool{
|
||||
flagger.FeatureUseJSONBody.String(): true,
|
||||
}
|
||||
}
|
||||
fl, err := flagger.New(
|
||||
context.Background(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}</style></defs><title>Icon_24px_ComputeEngine_Color</title><g data-name="Product Icons"><rect class="cls-1" x="9" y="9" width="6" height="6"/><rect class="cls-2" x="11" y="2" width="2" height="4"/><rect class="cls-2" x="7" y="2" width="2" height="4"/><rect class="cls-2" x="15" y="2" width="2" height="4"/><rect class="cls-3" x="11" y="18" width="2" height="4"/><rect class="cls-3" x="7" y="18" width="2" height="4"/><rect class="cls-3" x="15" y="18" width="2" height="4"/><rect class="cls-3" x="19" y="10" width="2" height="4" transform="translate(8 32) rotate(-90)"/><rect class="cls-3" x="19" y="14" width="2" height="4" transform="translate(4 36) rotate(-90)"/><rect class="cls-3" x="19" y="6" width="2" height="4" transform="translate(12 28) rotate(-90)"/><rect class="cls-2" x="3" y="10" width="2" height="4" transform="translate(-8 16) rotate(-90)"/><rect class="cls-2" x="3" y="14" width="2" height="4" transform="translate(-12 20) rotate(-90)"/><rect class="cls-2" x="3" y="6" width="2" height="4" transform="translate(-4 12) rotate(-90)"/><path class="cls-1" d="M5,5V19H19V5ZM17,17H7V7H17Z"/><polygon class="cls-2" points="9 15 15 15 12 12 9 15"/><polygon class="cls-3" points="12 12 15 15 15 9 12 12"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"id": "computeengine",
|
||||
"title": "GCP Compute Engine",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/uptime_total",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/cpu/utilization",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/guest/memory/bytes_used",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/guest/memory/percent_used",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "agent.googleapis.com/memory/percent_used",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/average_io_latency",
|
||||
"unit": "Microseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/read_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/write_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/read_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/write_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/performance_status",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/received_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/sent_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/received_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/sent_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/firewall/dropped_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "GCP Compute Engine Overview",
|
||||
"description": "Overview of GCP Compute Engine metrics",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor GCP Compute Engine with SigNoz
|
||||
|
||||
Collect key GCP Compute Engine metrics and view them with an out of the box dashboard.
|
||||
@@ -238,7 +238,7 @@
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"title": "Signoz GCP Memorystore Redis overview",
|
||||
"title": "Signoz GCP Memorystory Redis overview",
|
||||
"uploadedGrafana": false,
|
||||
"uuid": "019f85e9-3846-7dc9-9bce-b626b8a3eaff",
|
||||
"variables": {
|
||||
@@ -262,7 +262,7 @@
|
||||
"type": "DYNAMIC"
|
||||
},
|
||||
"dabcf41a-4121-4a03-8738-927576cc90bf": {
|
||||
"allSelected": false,
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"defaultValue": "",
|
||||
"description": "GCP Project ID",
|
||||
@@ -390,7 +390,7 @@
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
"query": "rate({\"redis.googleapis.com/server/uptime\", project_id=\"$project_id\"}[5m])"
|
||||
}
|
||||
],
|
||||
"queryType": "builder",
|
||||
|
||||
@@ -35,7 +35,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for rule state history.
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for rule state history.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -45,17 +45,17 @@ func (m *module) createMany(ctx context.Context, orgID valuer.UUID, kind coretyp
|
||||
return []*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
ordered, toCreate, err := m.resolve(ctx, orgID, kind, postable)
|
||||
toCreate, matched, err := m.resolve(ctx, orgID, kind, postable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// CreateOrGet fills new rows' IDs in place; ordered shares those pointers.
|
||||
if _, err := m.store.CreateOrGet(ctx, toCreate); err != nil {
|
||||
created, err := m.store.CreateOrGet(ctx, toCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ordered, nil
|
||||
return append(matched, created...), nil
|
||||
}
|
||||
|
||||
func (m *module) syncLinksForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) error {
|
||||
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
// the existing tags for an org. Lookup is case-insensitive on both key and
|
||||
// value (matching the storage uniqueness rule); when an existing row matches,
|
||||
// its display casing is reused. Inputs are deduped on (LOWER(key), LOWER(value));
|
||||
// the first input's casing wins on collisions. Returns the resolved tags in
|
||||
// request order (deduped) plus the new subset to insert; the new tags share
|
||||
// pointers with the ordered slice, so their IDs populate in place on insert.
|
||||
// the first input's casing wins on collisions. Returns:
|
||||
// - toCreate: new Tag rows the caller should insert (with pre-generated IDs)
|
||||
// - matched: existing rows the caller's input already pointed to. They
|
||||
// already carry authoritative IDs from the store.
|
||||
func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
if len(postable) == 0 {
|
||||
return nil, nil, nil
|
||||
@@ -33,8 +34,8 @@ func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.
|
||||
}
|
||||
|
||||
seenInRequestAlready := make(map[string]struct{}, len(postable)) // postable can have the same tag multiple times
|
||||
ordered := make([]*tagtypes.Tag, 0, len(postable))
|
||||
toCreate := make([]*tagtypes.Tag, 0)
|
||||
matched := make([]*tagtypes.Tag, 0)
|
||||
|
||||
for _, p := range postable {
|
||||
key, value, err := tagtypes.ValidatePostableTag(p)
|
||||
@@ -48,13 +49,11 @@ func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.
|
||||
seenInRequestAlready[lookup] = struct{}{}
|
||||
|
||||
if existingTag, ok := lowercaseTagsMap[lookup]; ok {
|
||||
ordered = append(ordered, existingTag)
|
||||
matched = append(matched, existingTag)
|
||||
continue
|
||||
}
|
||||
newTag := tagtypes.NewTag(orgID, kind, key, value)
|
||||
ordered = append(ordered, newTag)
|
||||
toCreate = append(toCreate, newTag)
|
||||
toCreate = append(toCreate, tagtypes.NewTag(orgID, kind, key, value))
|
||||
}
|
||||
|
||||
return ordered, toCreate, nil
|
||||
return toCreate, matched, nil
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
toCreate, matched, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, ordered)
|
||||
assert.Empty(t, toCreate)
|
||||
assert.Empty(t, matched)
|
||||
assert.Zero(t, store.ListCallCount, "should not hit store when input is empty")
|
||||
})
|
||||
|
||||
t.Run("creates missing pairs and reuses existing, in request order", func(t *testing.T) {
|
||||
t.Run("creates missing pairs and reuses existing", func(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dbTag := tagtypes.NewTag(orgID, testKind, "team", "Pulse")
|
||||
dbTag2 := tagtypes.NewTag(orgID, testKind, "Database", "redis")
|
||||
@@ -35,28 +35,22 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store.Tags = []*tagtypes.Tag{dbTag, dbTag2}
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "events"}, // new
|
||||
{Key: "DATABASE", Value: "REDIS"}, // case-only conflict
|
||||
{Key: "Brand", Value: "New"}, // new
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ordered mirrors the request: new, existing (reused pointer), new.
|
||||
require.Len(t, ordered, 3)
|
||||
assert.Equal(t, "team", ordered[0].Key)
|
||||
assert.Equal(t, "events", ordered[0].Value)
|
||||
assert.Same(t, dbTag2, ordered[1], "case-only conflict reuses the existing pointer with its authoritative ID")
|
||||
assert.Equal(t, "Brand", ordered[2].Key)
|
||||
assert.Equal(t, "New", ordered[2].Value)
|
||||
|
||||
createdLowerKVs := []string{}
|
||||
for _, tg := range toCreate {
|
||||
createdLowerKVs = append(createdLowerKVs, strings.ToLower(tg.Key)+"\x00"+strings.ToLower(tg.Value))
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"team\x00events", "brand\x00new"}, createdLowerKVs,
|
||||
"only the two missing pairs should be returned for insertion")
|
||||
assert.Same(t, ordered[0], toCreate[0], "toCreate shares pointers with ordered so inserted IDs propagate")
|
||||
|
||||
require.Len(t, matched, 1, "DATABASE:REDIS should hit the existing 'Database:redis' tag")
|
||||
assert.Same(t, dbTag2, matched[0], "matched should return the existing pointer with its authoritative ID")
|
||||
})
|
||||
|
||||
t.Run("dedupes inputs that map to the same lower(key)+lower(value)", func(t *testing.T) {
|
||||
@@ -64,14 +58,14 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "Foo", Value: "Bar"},
|
||||
{Key: "foo", Value: "bar"},
|
||||
{Key: "FOO", Value: "BAR"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ordered, 1, "duplicate inputs must collapse into a single tag")
|
||||
require.Empty(t, matched)
|
||||
require.Len(t, toCreate, 1, "duplicate inputs must collapse into a single insert")
|
||||
assert.Equal(t, "Foo", toCreate[0].Key, "first input's casing wins")
|
||||
assert.Equal(t, "Bar", toCreate[0].Value, "first input's casing wins")
|
||||
@@ -84,15 +78,15 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store.Tags = []*tagtypes.Tag{dbTag}
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "PULSE"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, toCreate)
|
||||
require.Len(t, ordered, 1)
|
||||
assert.Equal(t, "Team", ordered[0].Key)
|
||||
assert.Equal(t, "Pulse", ordered[0].Value)
|
||||
require.Len(t, matched, 1)
|
||||
assert.Equal(t, "Team", matched[0].Key)
|
||||
assert.Equal(t, "Pulse", matched[0].Value)
|
||||
})
|
||||
|
||||
t.Run("propagates validation error from any input", func(t *testing.T) {
|
||||
|
||||
@@ -44,7 +44,6 @@ func (s *store) ListByResource(ctx context.Context, orgID valuer.UUID, kind core
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id = ?", resourceID).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
OrderExpr("tr.rank ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -72,7 +71,6 @@ func (s *store) ListByResources(ctx context.Context, orgID valuer.UUID, kind cor
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id IN (?)", bun.In(resourceIDs)).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
OrderExpr("tr.rank ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -114,14 +112,11 @@ func (s *store) CreateRelations(ctx context.Context, relations []*tagtypes.TagRe
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
// On re-link (same tag, same resource) overwrite rank with the incoming
|
||||
// position, so a reordered tag set persists its new order.
|
||||
_, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(&relations).
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO UPDATE").
|
||||
Set("rank = EXCLUDED.rank").
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO NOTHING").
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,12 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,12 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -93,12 +93,6 @@ func (s *BaseFilterQueryListener) EnterFunctionCall(ctx *FunctionCallContext) {}
|
||||
// ExitFunctionCall is called when production functionCall is exited.
|
||||
func (s *BaseFilterQueryListener) ExitFunctionCall(ctx *FunctionCallContext) {}
|
||||
|
||||
// EnterSearchCall is called when production searchCall is entered.
|
||||
func (s *BaseFilterQueryListener) EnterSearchCall(ctx *SearchCallContext) {}
|
||||
|
||||
// ExitSearchCall is called when production searchCall is exited.
|
||||
func (s *BaseFilterQueryListener) ExitSearchCall(ctx *SearchCallContext) {}
|
||||
|
||||
// EnterFunctionParamList is called when production functionParamList is entered.
|
||||
func (s *BaseFilterQueryListener) EnterFunctionParamList(ctx *FunctionParamListContext) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -56,10 +56,6 @@ func (v *BaseFilterQueryVisitor) VisitFunctionCall(ctx *FunctionCallContext) int
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFilterQueryVisitor) VisitSearchCall(ctx *SearchCallContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFilterQueryVisitor) VisitFunctionParamList(ctx *FunctionParamListContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
// Suppress unused import error
|
||||
@@ -50,174 +51,170 @@ func filterquerylexerLexerInit() {
|
||||
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
|
||||
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
|
||||
"HASALL", "SEARCH", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
|
||||
"HASALL", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
|
||||
}
|
||||
staticData.RuleNames = []string{
|
||||
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
|
||||
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
|
||||
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
|
||||
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
|
||||
"HASALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
|
||||
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
|
||||
}
|
||||
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
|
||||
staticData.serializedATN = []int32{
|
||||
4, 0, 33, 329, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 0, 32, 320, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
|
||||
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
|
||||
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
|
||||
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
|
||||
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
|
||||
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
|
||||
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
|
||||
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
|
||||
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
|
||||
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1,
|
||||
13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15,
|
||||
1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 134, 8, 15, 1, 16, 1, 16, 1, 16, 1,
|
||||
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
|
||||
1, 17, 3, 17, 151, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1,
|
||||
19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22,
|
||||
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1,
|
||||
24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
|
||||
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
|
||||
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 210,
|
||||
8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 215, 8, 29, 1, 29, 4, 29, 218, 8, 29,
|
||||
11, 29, 12, 29, 219, 1, 29, 1, 29, 5, 29, 224, 8, 29, 10, 29, 12, 29, 227,
|
||||
9, 29, 3, 29, 229, 8, 29, 1, 29, 1, 29, 3, 29, 233, 8, 29, 1, 29, 4, 29,
|
||||
236, 8, 29, 11, 29, 12, 29, 237, 3, 29, 240, 8, 29, 1, 29, 3, 29, 243,
|
||||
8, 29, 1, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 1, 29, 1,
|
||||
29, 3, 29, 253, 8, 29, 1, 29, 4, 29, 256, 8, 29, 11, 29, 12, 29, 257, 3,
|
||||
29, 260, 8, 29, 3, 29, 262, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 268,
|
||||
8, 30, 10, 30, 12, 30, 271, 9, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 5,
|
||||
30, 278, 8, 30, 10, 30, 12, 30, 281, 9, 30, 1, 30, 3, 30, 284, 8, 30, 1,
|
||||
31, 1, 31, 5, 31, 288, 8, 31, 10, 31, 12, 31, 291, 9, 31, 1, 32, 1, 32,
|
||||
1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1,
|
||||
34, 1, 34, 4, 34, 307, 8, 34, 11, 34, 12, 34, 308, 5, 34, 311, 8, 34, 10,
|
||||
34, 12, 34, 314, 9, 34, 1, 35, 4, 35, 317, 8, 35, 11, 35, 12, 35, 318,
|
||||
1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 4, 37, 326, 8, 37, 11, 37, 12, 37, 327,
|
||||
0, 0, 38, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
|
||||
7, 36, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5,
|
||||
1, 5, 1, 5, 3, 5, 89, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1,
|
||||
8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1,
|
||||
12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14,
|
||||
1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1,
|
||||
15, 1, 15, 3, 15, 132, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16,
|
||||
1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 149,
|
||||
8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1,
|
||||
20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22,
|
||||
1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1,
|
||||
24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25,
|
||||
1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 201,
|
||||
8, 26, 1, 27, 1, 27, 1, 28, 3, 28, 206, 8, 28, 1, 28, 4, 28, 209, 8, 28,
|
||||
11, 28, 12, 28, 210, 1, 28, 1, 28, 5, 28, 215, 8, 28, 10, 28, 12, 28, 218,
|
||||
9, 28, 3, 28, 220, 8, 28, 1, 28, 1, 28, 3, 28, 224, 8, 28, 1, 28, 4, 28,
|
||||
227, 8, 28, 11, 28, 12, 28, 228, 3, 28, 231, 8, 28, 1, 28, 3, 28, 234,
|
||||
8, 28, 1, 28, 1, 28, 4, 28, 238, 8, 28, 11, 28, 12, 28, 239, 1, 28, 1,
|
||||
28, 3, 28, 244, 8, 28, 1, 28, 4, 28, 247, 8, 28, 11, 28, 12, 28, 248, 3,
|
||||
28, 251, 8, 28, 3, 28, 253, 8, 28, 1, 29, 1, 29, 1, 29, 1, 29, 5, 29, 259,
|
||||
8, 29, 10, 29, 12, 29, 262, 9, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 5,
|
||||
29, 269, 8, 29, 10, 29, 12, 29, 272, 9, 29, 1, 29, 3, 29, 275, 8, 29, 1,
|
||||
30, 1, 30, 5, 30, 279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 31, 1, 31,
|
||||
1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1,
|
||||
33, 1, 33, 4, 33, 298, 8, 33, 11, 33, 12, 33, 299, 5, 33, 302, 8, 33, 10,
|
||||
33, 12, 33, 305, 9, 33, 1, 34, 4, 34, 308, 8, 34, 11, 34, 12, 34, 309,
|
||||
1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 4, 36, 317, 8, 36, 11, 36, 12, 36, 318,
|
||||
0, 0, 37, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
|
||||
10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37,
|
||||
19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55,
|
||||
28, 57, 0, 59, 29, 61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0,
|
||||
75, 33, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0,
|
||||
75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84,
|
||||
84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88,
|
||||
88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71,
|
||||
71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79,
|
||||
111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104,
|
||||
104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102,
|
||||
102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92,
|
||||
4, 0, 35, 36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64,
|
||||
90, 95, 95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57,
|
||||
8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 353,
|
||||
0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0,
|
||||
0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0,
|
||||
0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0,
|
||||
0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1,
|
||||
0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39,
|
||||
1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0,
|
||||
47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0,
|
||||
0, 55, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0,
|
||||
0, 0, 71, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0,
|
||||
0, 0, 5, 81, 1, 0, 0, 0, 7, 83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90,
|
||||
1, 0, 0, 0, 13, 92, 1, 0, 0, 0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0,
|
||||
19, 100, 1, 0, 0, 0, 21, 103, 1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108,
|
||||
1, 0, 0, 0, 27, 113, 1, 0, 0, 0, 29, 119, 1, 0, 0, 0, 31, 127, 1, 0, 0,
|
||||
0, 33, 135, 1, 0, 0, 0, 35, 142, 1, 0, 0, 0, 37, 152, 1, 0, 0, 0, 39, 155,
|
||||
1, 0, 0, 0, 41, 159, 1, 0, 0, 0, 43, 163, 1, 0, 0, 0, 45, 166, 1, 0, 0,
|
||||
0, 47, 175, 1, 0, 0, 0, 49, 179, 1, 0, 0, 0, 51, 186, 1, 0, 0, 0, 53, 193,
|
||||
1, 0, 0, 0, 55, 209, 1, 0, 0, 0, 57, 211, 1, 0, 0, 0, 59, 261, 1, 0, 0,
|
||||
0, 61, 283, 1, 0, 0, 0, 63, 285, 1, 0, 0, 0, 65, 292, 1, 0, 0, 0, 67, 295,
|
||||
1, 0, 0, 0, 69, 299, 1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 322, 1, 0, 0,
|
||||
0, 75, 325, 1, 0, 0, 0, 77, 78, 5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80,
|
||||
5, 41, 0, 0, 80, 4, 1, 0, 0, 0, 81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0,
|
||||
83, 84, 5, 93, 0, 0, 84, 8, 1, 0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1,
|
||||
0, 0, 0, 87, 91, 5, 61, 0, 0, 88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0,
|
||||
90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5,
|
||||
33, 0, 0, 93, 94, 5, 61, 0, 0, 94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0,
|
||||
96, 97, 5, 62, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1,
|
||||
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0,
|
||||
0, 103, 104, 5, 62, 0, 0, 104, 22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
|
||||
107, 5, 61, 0, 0, 107, 24, 1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110,
|
||||
7, 1, 0, 0, 110, 111, 7, 2, 0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0,
|
||||
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1, 0, 0,
|
||||
116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 28, 1, 0, 0, 0, 119, 120,
|
||||
7, 4, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 5, 0, 0, 122, 123, 7, 6,
|
||||
0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 3, 0, 0, 125, 126, 7, 7, 0, 0,
|
||||
126, 30, 1, 0, 0, 0, 127, 128, 7, 3, 0, 0, 128, 129, 7, 8, 0, 0, 129, 130,
|
||||
7, 1, 0, 0, 130, 131, 7, 9, 0, 0, 131, 133, 7, 5, 0, 0, 132, 134, 7, 9,
|
||||
0, 0, 133, 132, 1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 32, 1, 0, 0, 0,
|
||||
135, 136, 7, 10, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 11, 0, 0, 138,
|
||||
139, 7, 3, 0, 0, 139, 140, 7, 8, 0, 0, 140, 141, 7, 12, 0, 0, 141, 34,
|
||||
1, 0, 0, 0, 142, 143, 7, 13, 0, 0, 143, 144, 7, 14, 0, 0, 144, 145, 7,
|
||||
7, 0, 0, 145, 146, 7, 5, 0, 0, 146, 147, 7, 15, 0, 0, 147, 148, 7, 1, 0,
|
||||
0, 148, 150, 7, 7, 0, 0, 149, 151, 7, 9, 0, 0, 150, 149, 1, 0, 0, 0, 150,
|
||||
151, 1, 0, 0, 0, 151, 36, 1, 0, 0, 0, 152, 153, 7, 1, 0, 0, 153, 154, 7,
|
||||
7, 0, 0, 154, 38, 1, 0, 0, 0, 155, 156, 7, 7, 0, 0, 156, 157, 7, 14, 0,
|
||||
0, 157, 158, 7, 5, 0, 0, 158, 40, 1, 0, 0, 0, 159, 160, 7, 15, 0, 0, 160,
|
||||
161, 7, 7, 0, 0, 161, 162, 7, 16, 0, 0, 162, 42, 1, 0, 0, 0, 163, 164,
|
||||
7, 14, 0, 0, 164, 165, 7, 10, 0, 0, 165, 44, 1, 0, 0, 0, 166, 167, 7, 17,
|
||||
0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 5, 0, 0,
|
||||
170, 171, 7, 14, 0, 0, 171, 172, 7, 2, 0, 0, 172, 173, 7, 3, 0, 0, 173,
|
||||
174, 7, 7, 0, 0, 174, 46, 1, 0, 0, 0, 175, 176, 7, 17, 0, 0, 176, 177,
|
||||
7, 15, 0, 0, 177, 178, 7, 9, 0, 0, 178, 48, 1, 0, 0, 0, 179, 180, 7, 17,
|
||||
0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 9, 0, 0, 182, 183, 7, 15, 0,
|
||||
0, 183, 184, 7, 7, 0, 0, 184, 185, 7, 18, 0, 0, 185, 50, 1, 0, 0, 0, 186,
|
||||
187, 7, 17, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 9, 0, 0, 189, 190,
|
||||
7, 15, 0, 0, 190, 191, 7, 0, 0, 0, 191, 192, 7, 0, 0, 0, 192, 52, 1, 0,
|
||||
0, 0, 193, 194, 7, 9, 0, 0, 194, 195, 7, 3, 0, 0, 195, 196, 7, 15, 0, 0,
|
||||
196, 197, 7, 10, 0, 0, 197, 198, 7, 13, 0, 0, 198, 199, 7, 17, 0, 0, 199,
|
||||
54, 1, 0, 0, 0, 200, 201, 7, 5, 0, 0, 201, 202, 7, 10, 0, 0, 202, 203,
|
||||
7, 19, 0, 0, 203, 210, 7, 3, 0, 0, 204, 205, 7, 20, 0, 0, 205, 206, 7,
|
||||
15, 0, 0, 206, 207, 7, 0, 0, 0, 207, 208, 7, 9, 0, 0, 208, 210, 7, 3, 0,
|
||||
0, 209, 200, 1, 0, 0, 0, 209, 204, 1, 0, 0, 0, 210, 56, 1, 0, 0, 0, 211,
|
||||
212, 7, 21, 0, 0, 212, 58, 1, 0, 0, 0, 213, 215, 3, 57, 28, 0, 214, 213,
|
||||
1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 217, 1, 0, 0, 0, 216, 218, 3, 73,
|
||||
36, 0, 217, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 217, 1, 0, 0, 0,
|
||||
219, 220, 1, 0, 0, 0, 220, 228, 1, 0, 0, 0, 221, 225, 5, 46, 0, 0, 222,
|
||||
224, 3, 73, 36, 0, 223, 222, 1, 0, 0, 0, 224, 227, 1, 0, 0, 0, 225, 223,
|
||||
1, 0, 0, 0, 225, 226, 1, 0, 0, 0, 226, 229, 1, 0, 0, 0, 227, 225, 1, 0,
|
||||
0, 0, 228, 221, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0,
|
||||
230, 232, 7, 3, 0, 0, 231, 233, 3, 57, 28, 0, 232, 231, 1, 0, 0, 0, 232,
|
||||
233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 73, 36, 0, 235, 234,
|
||||
1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0,
|
||||
0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0,
|
||||
240, 262, 1, 0, 0, 0, 241, 243, 3, 57, 28, 0, 242, 241, 1, 0, 0, 0, 242,
|
||||
243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 5, 46, 0, 0, 245, 247,
|
||||
3, 73, 36, 0, 246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1,
|
||||
0, 0, 0, 248, 249, 1, 0, 0, 0, 249, 259, 1, 0, 0, 0, 250, 252, 7, 3, 0,
|
||||
0, 251, 253, 3, 57, 28, 0, 252, 251, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0,
|
||||
253, 255, 1, 0, 0, 0, 254, 256, 3, 73, 36, 0, 255, 254, 1, 0, 0, 0, 256,
|
||||
257, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 260,
|
||||
1, 0, 0, 0, 259, 250, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0,
|
||||
0, 0, 261, 214, 1, 0, 0, 0, 261, 242, 1, 0, 0, 0, 262, 60, 1, 0, 0, 0,
|
||||
263, 269, 5, 34, 0, 0, 264, 268, 8, 22, 0, 0, 265, 266, 5, 92, 0, 0, 266,
|
||||
268, 9, 0, 0, 0, 267, 264, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 271,
|
||||
1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 272, 1, 0,
|
||||
0, 0, 271, 269, 1, 0, 0, 0, 272, 284, 5, 34, 0, 0, 273, 279, 5, 39, 0,
|
||||
0, 274, 278, 8, 23, 0, 0, 275, 276, 5, 92, 0, 0, 276, 278, 9, 0, 0, 0,
|
||||
277, 274, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 1, 0, 0, 0, 279,
|
||||
277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 282, 1, 0, 0, 0, 281, 279,
|
||||
1, 0, 0, 0, 282, 284, 5, 39, 0, 0, 283, 263, 1, 0, 0, 0, 283, 273, 1, 0,
|
||||
0, 0, 284, 62, 1, 0, 0, 0, 285, 289, 7, 24, 0, 0, 286, 288, 7, 25, 0, 0,
|
||||
287, 286, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289,
|
||||
290, 1, 0, 0, 0, 290, 64, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 293, 5,
|
||||
91, 0, 0, 293, 294, 5, 93, 0, 0, 294, 66, 1, 0, 0, 0, 295, 296, 5, 91,
|
||||
0, 0, 296, 297, 5, 42, 0, 0, 297, 298, 5, 93, 0, 0, 298, 68, 1, 0, 0, 0,
|
||||
299, 312, 3, 63, 31, 0, 300, 301, 5, 46, 0, 0, 301, 311, 3, 63, 31, 0,
|
||||
302, 311, 3, 65, 32, 0, 303, 311, 3, 67, 33, 0, 304, 306, 5, 46, 0, 0,
|
||||
305, 307, 3, 73, 36, 0, 306, 305, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308,
|
||||
306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 300,
|
||||
1, 0, 0, 0, 310, 302, 1, 0, 0, 0, 310, 303, 1, 0, 0, 0, 310, 304, 1, 0,
|
||||
0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0,
|
||||
313, 70, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 317, 7, 26, 0, 0, 316,
|
||||
315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0, 0, 318, 319,
|
||||
1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 321, 6, 35, 0, 0, 321, 72, 1, 0,
|
||||
0, 0, 322, 323, 7, 27, 0, 0, 323, 74, 1, 0, 0, 0, 324, 326, 8, 28, 0, 0,
|
||||
325, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 327,
|
||||
328, 1, 0, 0, 0, 328, 76, 1, 0, 0, 0, 29, 0, 90, 133, 150, 209, 214, 219,
|
||||
225, 228, 232, 237, 239, 242, 248, 252, 257, 259, 261, 267, 269, 277, 279,
|
||||
283, 289, 308, 310, 312, 318, 327, 1, 6, 0, 0,
|
||||
0, 57, 28, 59, 29, 61, 0, 63, 0, 65, 0, 67, 30, 69, 31, 71, 0, 73, 32,
|
||||
1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75,
|
||||
107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84, 116,
|
||||
116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88, 120,
|
||||
120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103,
|
||||
103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111, 111,
|
||||
2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2,
|
||||
0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 2,
|
||||
0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0, 35,
|
||||
36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64, 90, 95,
|
||||
95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0,
|
||||
9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 344, 0,
|
||||
1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0,
|
||||
9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0,
|
||||
0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0,
|
||||
0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0,
|
||||
0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1,
|
||||
0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47,
|
||||
1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0,
|
||||
57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0,
|
||||
0, 73, 1, 0, 0, 0, 1, 75, 1, 0, 0, 0, 3, 77, 1, 0, 0, 0, 5, 79, 1, 0, 0,
|
||||
0, 7, 81, 1, 0, 0, 0, 9, 83, 1, 0, 0, 0, 11, 88, 1, 0, 0, 0, 13, 90, 1,
|
||||
0, 0, 0, 15, 93, 1, 0, 0, 0, 17, 96, 1, 0, 0, 0, 19, 98, 1, 0, 0, 0, 21,
|
||||
101, 1, 0, 0, 0, 23, 103, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 111, 1,
|
||||
0, 0, 0, 29, 117, 1, 0, 0, 0, 31, 125, 1, 0, 0, 0, 33, 133, 1, 0, 0, 0,
|
||||
35, 140, 1, 0, 0, 0, 37, 150, 1, 0, 0, 0, 39, 153, 1, 0, 0, 0, 41, 157,
|
||||
1, 0, 0, 0, 43, 161, 1, 0, 0, 0, 45, 164, 1, 0, 0, 0, 47, 173, 1, 0, 0,
|
||||
0, 49, 177, 1, 0, 0, 0, 51, 184, 1, 0, 0, 0, 53, 200, 1, 0, 0, 0, 55, 202,
|
||||
1, 0, 0, 0, 57, 252, 1, 0, 0, 0, 59, 274, 1, 0, 0, 0, 61, 276, 1, 0, 0,
|
||||
0, 63, 283, 1, 0, 0, 0, 65, 286, 1, 0, 0, 0, 67, 290, 1, 0, 0, 0, 69, 307,
|
||||
1, 0, 0, 0, 71, 313, 1, 0, 0, 0, 73, 316, 1, 0, 0, 0, 75, 76, 5, 40, 0,
|
||||
0, 76, 2, 1, 0, 0, 0, 77, 78, 5, 41, 0, 0, 78, 4, 1, 0, 0, 0, 79, 80, 5,
|
||||
91, 0, 0, 80, 6, 1, 0, 0, 0, 81, 82, 5, 93, 0, 0, 82, 8, 1, 0, 0, 0, 83,
|
||||
84, 5, 44, 0, 0, 84, 10, 1, 0, 0, 0, 85, 89, 5, 61, 0, 0, 86, 87, 5, 61,
|
||||
0, 0, 87, 89, 5, 61, 0, 0, 88, 85, 1, 0, 0, 0, 88, 86, 1, 0, 0, 0, 89,
|
||||
12, 1, 0, 0, 0, 90, 91, 5, 33, 0, 0, 91, 92, 5, 61, 0, 0, 92, 14, 1, 0,
|
||||
0, 0, 93, 94, 5, 60, 0, 0, 94, 95, 5, 62, 0, 0, 95, 16, 1, 0, 0, 0, 96,
|
||||
97, 5, 60, 0, 0, 97, 18, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 100, 5, 61,
|
||||
0, 0, 100, 20, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 22, 1, 0, 0, 0,
|
||||
103, 104, 5, 62, 0, 0, 104, 105, 5, 61, 0, 0, 105, 24, 1, 0, 0, 0, 106,
|
||||
107, 7, 0, 0, 0, 107, 108, 7, 1, 0, 0, 108, 109, 7, 2, 0, 0, 109, 110,
|
||||
7, 3, 0, 0, 110, 26, 1, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 0,
|
||||
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 2, 0, 0, 115, 116, 7, 3, 0, 0,
|
||||
116, 28, 1, 0, 0, 0, 117, 118, 7, 4, 0, 0, 118, 119, 7, 3, 0, 0, 119, 120,
|
||||
7, 5, 0, 0, 120, 121, 7, 6, 0, 0, 121, 122, 7, 3, 0, 0, 122, 123, 7, 3,
|
||||
0, 0, 123, 124, 7, 7, 0, 0, 124, 30, 1, 0, 0, 0, 125, 126, 7, 3, 0, 0,
|
||||
126, 127, 7, 8, 0, 0, 127, 128, 7, 1, 0, 0, 128, 129, 7, 9, 0, 0, 129,
|
||||
131, 7, 5, 0, 0, 130, 132, 7, 9, 0, 0, 131, 130, 1, 0, 0, 0, 131, 132,
|
||||
1, 0, 0, 0, 132, 32, 1, 0, 0, 0, 133, 134, 7, 10, 0, 0, 134, 135, 7, 3,
|
||||
0, 0, 135, 136, 7, 11, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 8, 0, 0,
|
||||
138, 139, 7, 12, 0, 0, 139, 34, 1, 0, 0, 0, 140, 141, 7, 13, 0, 0, 141,
|
||||
142, 7, 14, 0, 0, 142, 143, 7, 7, 0, 0, 143, 144, 7, 5, 0, 0, 144, 145,
|
||||
7, 15, 0, 0, 145, 146, 7, 1, 0, 0, 146, 148, 7, 7, 0, 0, 147, 149, 7, 9,
|
||||
0, 0, 148, 147, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 36, 1, 0, 0, 0,
|
||||
150, 151, 7, 1, 0, 0, 151, 152, 7, 7, 0, 0, 152, 38, 1, 0, 0, 0, 153, 154,
|
||||
7, 7, 0, 0, 154, 155, 7, 14, 0, 0, 155, 156, 7, 5, 0, 0, 156, 40, 1, 0,
|
||||
0, 0, 157, 158, 7, 15, 0, 0, 158, 159, 7, 7, 0, 0, 159, 160, 7, 16, 0,
|
||||
0, 160, 42, 1, 0, 0, 0, 161, 162, 7, 14, 0, 0, 162, 163, 7, 10, 0, 0, 163,
|
||||
44, 1, 0, 0, 0, 164, 165, 7, 17, 0, 0, 165, 166, 7, 15, 0, 0, 166, 167,
|
||||
7, 9, 0, 0, 167, 168, 7, 5, 0, 0, 168, 169, 7, 14, 0, 0, 169, 170, 7, 2,
|
||||
0, 0, 170, 171, 7, 3, 0, 0, 171, 172, 7, 7, 0, 0, 172, 46, 1, 0, 0, 0,
|
||||
173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176, 7, 9, 0, 0, 176,
|
||||
48, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15, 0, 0, 179, 180,
|
||||
7, 9, 0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 7, 0, 0, 182, 183, 7, 18,
|
||||
0, 0, 183, 50, 1, 0, 0, 0, 184, 185, 7, 17, 0, 0, 185, 186, 7, 15, 0, 0,
|
||||
186, 187, 7, 9, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 0, 0, 0, 189,
|
||||
190, 7, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 5, 0, 0, 192, 193, 7,
|
||||
10, 0, 0, 193, 194, 7, 19, 0, 0, 194, 201, 7, 3, 0, 0, 195, 196, 7, 20,
|
||||
0, 0, 196, 197, 7, 15, 0, 0, 197, 198, 7, 0, 0, 0, 198, 199, 7, 9, 0, 0,
|
||||
199, 201, 7, 3, 0, 0, 200, 191, 1, 0, 0, 0, 200, 195, 1, 0, 0, 0, 201,
|
||||
54, 1, 0, 0, 0, 202, 203, 7, 21, 0, 0, 203, 56, 1, 0, 0, 0, 204, 206, 3,
|
||||
55, 27, 0, 205, 204, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 208, 1, 0,
|
||||
0, 0, 207, 209, 3, 71, 35, 0, 208, 207, 1, 0, 0, 0, 209, 210, 1, 0, 0,
|
||||
0, 210, 208, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 219, 1, 0, 0, 0, 212,
|
||||
216, 5, 46, 0, 0, 213, 215, 3, 71, 35, 0, 214, 213, 1, 0, 0, 0, 215, 218,
|
||||
1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 220, 1, 0,
|
||||
0, 0, 218, 216, 1, 0, 0, 0, 219, 212, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0,
|
||||
220, 230, 1, 0, 0, 0, 221, 223, 7, 3, 0, 0, 222, 224, 3, 55, 27, 0, 223,
|
||||
222, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226, 1, 0, 0, 0, 225, 227,
|
||||
3, 71, 35, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 226, 1,
|
||||
0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 231, 1, 0, 0, 0, 230, 221, 1, 0, 0,
|
||||
0, 230, 231, 1, 0, 0, 0, 231, 253, 1, 0, 0, 0, 232, 234, 3, 55, 27, 0,
|
||||
233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235,
|
||||
237, 5, 46, 0, 0, 236, 238, 3, 71, 35, 0, 237, 236, 1, 0, 0, 0, 238, 239,
|
||||
1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 250, 1, 0,
|
||||
0, 0, 241, 243, 7, 3, 0, 0, 242, 244, 3, 55, 27, 0, 243, 242, 1, 0, 0,
|
||||
0, 243, 244, 1, 0, 0, 0, 244, 246, 1, 0, 0, 0, 245, 247, 3, 71, 35, 0,
|
||||
246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1, 0, 0, 0, 248,
|
||||
249, 1, 0, 0, 0, 249, 251, 1, 0, 0, 0, 250, 241, 1, 0, 0, 0, 250, 251,
|
||||
1, 0, 0, 0, 251, 253, 1, 0, 0, 0, 252, 205, 1, 0, 0, 0, 252, 233, 1, 0,
|
||||
0, 0, 253, 58, 1, 0, 0, 0, 254, 260, 5, 34, 0, 0, 255, 259, 8, 22, 0, 0,
|
||||
256, 257, 5, 92, 0, 0, 257, 259, 9, 0, 0, 0, 258, 255, 1, 0, 0, 0, 258,
|
||||
256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261,
|
||||
1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 275, 5, 34,
|
||||
0, 0, 264, 270, 5, 39, 0, 0, 265, 269, 8, 23, 0, 0, 266, 267, 5, 92, 0,
|
||||
0, 267, 269, 9, 0, 0, 0, 268, 265, 1, 0, 0, 0, 268, 266, 1, 0, 0, 0, 269,
|
||||
272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273,
|
||||
1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 39, 0, 0, 274, 254, 1, 0,
|
||||
0, 0, 274, 264, 1, 0, 0, 0, 275, 60, 1, 0, 0, 0, 276, 280, 7, 24, 0, 0,
|
||||
277, 279, 7, 25, 0, 0, 278, 277, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0, 280,
|
||||
278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 62, 1, 0, 0, 0, 282, 280, 1,
|
||||
0, 0, 0, 283, 284, 5, 91, 0, 0, 284, 285, 5, 93, 0, 0, 285, 64, 1, 0, 0,
|
||||
0, 286, 287, 5, 91, 0, 0, 287, 288, 5, 42, 0, 0, 288, 289, 5, 93, 0, 0,
|
||||
289, 66, 1, 0, 0, 0, 290, 303, 3, 61, 30, 0, 291, 292, 5, 46, 0, 0, 292,
|
||||
302, 3, 61, 30, 0, 293, 302, 3, 63, 31, 0, 294, 302, 3, 65, 32, 0, 295,
|
||||
297, 5, 46, 0, 0, 296, 298, 3, 71, 35, 0, 297, 296, 1, 0, 0, 0, 298, 299,
|
||||
1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0,
|
||||
0, 0, 301, 291, 1, 0, 0, 0, 301, 293, 1, 0, 0, 0, 301, 294, 1, 0, 0, 0,
|
||||
301, 295, 1, 0, 0, 0, 302, 305, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 303,
|
||||
304, 1, 0, 0, 0, 304, 68, 1, 0, 0, 0, 305, 303, 1, 0, 0, 0, 306, 308, 7,
|
||||
26, 0, 0, 307, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 307, 1, 0, 0,
|
||||
0, 309, 310, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 6, 34, 0, 0, 312,
|
||||
70, 1, 0, 0, 0, 313, 314, 7, 27, 0, 0, 314, 72, 1, 0, 0, 0, 315, 317, 8,
|
||||
28, 0, 0, 316, 315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0,
|
||||
0, 318, 319, 1, 0, 0, 0, 319, 74, 1, 0, 0, 0, 29, 0, 88, 131, 148, 200,
|
||||
205, 210, 216, 219, 223, 228, 230, 233, 239, 243, 248, 250, 252, 258, 260,
|
||||
268, 270, 274, 280, 299, 301, 303, 309, 318, 1, 6, 0, 0,
|
||||
}
|
||||
deserializer := antlr.NewATNDeserializer(nil)
|
||||
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
|
||||
@@ -284,11 +281,10 @@ const (
|
||||
FilterQueryLexerHAS = 24
|
||||
FilterQueryLexerHASANY = 25
|
||||
FilterQueryLexerHASALL = 26
|
||||
FilterQueryLexerSEARCH = 27
|
||||
FilterQueryLexerBOOL = 28
|
||||
FilterQueryLexerNUMBER = 29
|
||||
FilterQueryLexerQUOTED_TEXT = 30
|
||||
FilterQueryLexerKEY = 31
|
||||
FilterQueryLexerWS = 32
|
||||
FilterQueryLexerFREETEXT = 33
|
||||
FilterQueryLexerBOOL = 27
|
||||
FilterQueryLexerNUMBER = 28
|
||||
FilterQueryLexerQUOTED_TEXT = 29
|
||||
FilterQueryLexerKEY = 30
|
||||
FilterQueryLexerWS = 31
|
||||
FilterQueryLexerFREETEXT = 32
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -44,9 +44,6 @@ type FilterQueryListener interface {
|
||||
// EnterFunctionCall is called when entering the functionCall production.
|
||||
EnterFunctionCall(c *FunctionCallContext)
|
||||
|
||||
// EnterSearchCall is called when entering the searchCall production.
|
||||
EnterSearchCall(c *SearchCallContext)
|
||||
|
||||
// EnterFunctionParamList is called when entering the functionParamList production.
|
||||
EnterFunctionParamList(c *FunctionParamListContext)
|
||||
|
||||
@@ -98,9 +95,6 @@ type FilterQueryListener interface {
|
||||
// ExitFunctionCall is called when exiting the functionCall production.
|
||||
ExitFunctionCall(c *FunctionCallContext)
|
||||
|
||||
// ExitSearchCall is called when exiting the searchCall production.
|
||||
ExitSearchCall(c *SearchCallContext)
|
||||
|
||||
// ExitFunctionParamList is called when exiting the functionParamList production.
|
||||
ExitFunctionParamList(c *FunctionParamListContext)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -44,9 +44,6 @@ type FilterQueryVisitor interface {
|
||||
// Visit a parse tree produced by FilterQueryParser#functionCall.
|
||||
VisitFunctionCall(ctx *FunctionCallContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FilterQueryParser#searchCall.
|
||||
VisitSearchCall(ctx *SearchCallContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FilterQueryParser#functionParamList.
|
||||
VisitFunctionParamList(ctx *FunctionParamListContext) interface{}
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
const estimateTimeout = 5 * time.Second
|
||||
|
||||
const traceOutsideRangeWarn = "Query %s references a trace_id that exists between %s and %s (UTC) but lies outside the selected time range; adjust the time range to see results"
|
||||
|
||||
type builderQuery[T any] struct {
|
||||
@@ -249,10 +247,6 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.enforceEstimate(ctx, stmt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Execute the query with proper context for partial value detection
|
||||
result, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
|
||||
if err != nil {
|
||||
@@ -264,70 +258,6 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// estimateRows runs EXPLAIN ESTIMATE for a cost-guarded statement and returns its
|
||||
// estimated total scan rows. guarded=false means there is nothing to enforce (no
|
||||
// CostGuard or a non-positive budget). A non-nil error means the query must be
|
||||
// rejected: either the estimate itself failed (fail closed — we cannot honor the
|
||||
// budget without it) or the parent context was cancelled (surfaced as-is). Callers
|
||||
// enforce the budget so it can be applied per-statement or cumulatively.
|
||||
func (q *builderQuery[T]) estimateRows(ctx context.Context, stmt *qbtypes.Statement) (int64, bool, error) {
|
||||
if stmt.CostGuard == nil || stmt.CostGuard.MaxScanRows <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
estCtx, cancel := context.WithTimeout(ctx, estimateTimeout)
|
||||
defer cancel()
|
||||
|
||||
entries, err := q.telemetryStore.Estimate(estCtx, stmt.Query, stmt.Args...)
|
||||
if err != nil {
|
||||
// Parent cancellation isn't the budget's concern — surface it unchanged.
|
||||
if ctx.Err() != nil {
|
||||
return 0, true, ctx.Err()
|
||||
}
|
||||
// Fail closed: this is a scan-heavy statement and without an estimate we
|
||||
// cannot bound its cost, so running it unbounded is exactly what the guard
|
||||
// exists to prevent.
|
||||
reason := fmt.Sprintf("This query is too broad to plan within %s; narrow the time range or add a more selective filter.", estimateTimeout)
|
||||
if estCtx.Err() != context.DeadlineExceeded {
|
||||
reason = "Could not estimate this query's scan cost; narrow the time range or add a more selective filter."
|
||||
q.logger.WarnContext(ctx, "EXPLAIN ESTIMATE failed; rejecting scan-heavy query (fail-closed)", errors.Attr(err))
|
||||
}
|
||||
return 0, true, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s", withAdvisory(stmt.CostGuard.Warning, reason))
|
||||
}
|
||||
|
||||
var rows int64
|
||||
for _, e := range entries {
|
||||
rows += e.Rows
|
||||
}
|
||||
return rows, true, nil
|
||||
}
|
||||
|
||||
// enforceEstimate rejects a single scan-heavy statement (Statement.CostGuard) whose
|
||||
// EXPLAIN ESTIMATE rows exceed its budget, before executing. Budget 0 disables.
|
||||
func (q *builderQuery[T]) enforceEstimate(ctx context.Context, stmt *qbtypes.Statement) error {
|
||||
rows, guarded, err := q.estimateRows(ctx, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !guarded {
|
||||
return nil
|
||||
}
|
||||
if budget := stmt.CostGuard.MaxScanRows; rows > budget {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "%s",
|
||||
withAdvisory(stmt.CostGuard.Warning, fmt.Sprintf("This query would scan about %d rows in this range, over the limit of %d; narrow the time range or add a more selective filter.", rows, budget)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// withAdvisory prefixes reason with the requirement's advisory (e.g. the search()
|
||||
// warning) when present, so the rejection leads with why the query is expensive.
|
||||
func withAdvisory(advisory, reason string) string {
|
||||
if advisory == "" {
|
||||
return reason
|
||||
}
|
||||
return strings.TrimRight(advisory, ". ") + ". " + reason
|
||||
}
|
||||
|
||||
// narrowWindowByTraceID inspects the filter for trace_id predicates and clamps
|
||||
// [fromMS,toMS] to the time range stored in signoz_traces.distributed_trace_summary.
|
||||
// Returns the (possibly narrowed) window, overlap=false when the trace lies
|
||||
@@ -561,11 +491,6 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
|
||||
var warnings []string
|
||||
var warningsDocURL string
|
||||
|
||||
// Cost guard is enforced against the cumulative estimate across the buckets we
|
||||
// actually visit — a broad search() that fans every bucket must be bounded by
|
||||
// the per-query budget, not let each bucket pass its slice independently.
|
||||
var estimatedScan int64
|
||||
|
||||
for _, r := range buckets {
|
||||
q.spec.Offset = 0
|
||||
q.spec.Limit = need
|
||||
@@ -576,17 +501,6 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
|
||||
}
|
||||
warnings = stmt.Warnings
|
||||
warningsDocURL = stmt.WarningsDocURL
|
||||
rowsEst, guarded, err := q.estimateRows(ctx, stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if guarded {
|
||||
estimatedScan += rowsEst
|
||||
if budget := stmt.CostGuard.MaxScanRows; estimatedScan > budget {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s",
|
||||
withAdvisory(stmt.CostGuard.Warning, fmt.Sprintf("This query would scan about %d rows across the time range, over the limit of %d; narrow the time range or add a more selective filter.", estimatedScan, budget)))
|
||||
}
|
||||
}
|
||||
// Execute with proper context for partial value detection
|
||||
res, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,8 +27,6 @@ type Config struct {
|
||||
SkipResourceFingerprint SkipResourceFingerprint `yaml:"skip_resource_fingerprint" mapstructure:"skip_resource_fingerprint"`
|
||||
// LogTraceIDWindowPadding is the padding added to narrowed down timerange from trace summary to logs with trace_id filter.
|
||||
LogTraceIDWindowPadding time.Duration `yaml:"log_trace_id_window_padding" mapstructure:"log_trace_id_window_padding"`
|
||||
// SearchMaxScanRows caps the rows a search() query may scan, enforced via
|
||||
SearchMaxScanRows int64 `yaml:"search_max_scan_rows" mapstructure:"search_max_scan_rows"`
|
||||
}
|
||||
|
||||
// NewConfigFactory creates a new config factory for querier.
|
||||
@@ -47,7 +45,6 @@ func newConfig() factory.Config {
|
||||
Threshold: 100000,
|
||||
},
|
||||
LogTraceIDWindowPadding: 5 * time.Minute,
|
||||
SearchMaxScanRows: 100_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +65,6 @@ func (c Config) Validate() error {
|
||||
if c.LogTraceIDWindowPadding < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "log_trace_id_window_padding must not be negative, got %v", c.LogTraceIDWindowPadding)
|
||||
}
|
||||
if c.SearchMaxScanRows < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "search_max_scan_rows must not be negative, got %v", c.SearchMaxScanRows)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,6 @@ func newProvider(
|
||||
telemetryStore,
|
||||
cfg.SkipResourceFingerprint.Enabled,
|
||||
cfg.SkipResourceFingerprint.Threshold,
|
||||
telemetrylogs.WithSearchMaxScanRows(cfg.SearchMaxScanRows),
|
||||
)
|
||||
|
||||
// Create audit statement builder
|
||||
|
||||
@@ -8,10 +8,6 @@ const (
|
||||
// BodyFullTextSearchDefaultWarning is emitted when a full-text search or "body" searches are hit
|
||||
// with New JSON Body enhancements.
|
||||
BodyFullTextSearchDefaultWarning = "Full text searches default to `body.message:string`. Use `body.<key>` to search a different field inside body"
|
||||
|
||||
// SearchWarning is emitted on every search() call. search() scans all fields,
|
||||
// so it is slow and expensive; a specific field is cheaper.
|
||||
SearchWarning = "search() runs across all fields and can be slow and expensive. Prefer a specific field, e.g. `<context>.<field_key>:<type>`"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -32,7 +32,7 @@ var friendly = map[string]string{
|
||||
"BETWEEN": "BETWEEN", "IN": "IN", "EXISTS": "EXISTS",
|
||||
"REGEXP": "REGEXP", "CONTAINS": "CONTAINS",
|
||||
"HAS": "has()", "HASANY": "hasAny()", "HASALL": "hasAll()",
|
||||
"HASTOKEN": "hasToken()", "SEARCH": "search()",
|
||||
"HASTOKEN": "hasToken()",
|
||||
|
||||
// literals / identifiers
|
||||
"NUMBER": "number",
|
||||
|
||||
@@ -161,16 +161,14 @@ func inferDataTypesFromList(values []any) []telemetrytypes.FieldDataType {
|
||||
return out
|
||||
}
|
||||
|
||||
// NewFunctionUnsupportedError returns the error for a has/hasAny/hasAll/hasToken/search
|
||||
// operator on a builder that doesn't support it (logs only), or nil for other operators.
|
||||
// NewFunctionUnsupportedError returns the error for a has/hasAny/hasAll/hasToken operator
|
||||
// on a builder that doesn't support it (logs body only), or nil for other operators.
|
||||
func NewFunctionUnsupportedError(operator qbtypes.FilterOperator) error {
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHasToken:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `hasToken` only supports body field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
case qbtypes.FilterOperatorHas, qbtypes.FilterOperatorHasAny, qbtypes.FilterOperatorHasAll:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` supports only body JSON search", operator.FunctionName()).WithUrl(functionBodyJSONSearchDocURL)
|
||||
case qbtypes.FilterOperatorSearch:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `search` is only supported for logs")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
type FieldConstraint struct {
|
||||
Field string
|
||||
Operator qbtypes.FilterOperator
|
||||
Value any
|
||||
Values []any // For IN, NOT IN operations
|
||||
Value interface{}
|
||||
Values []interface{} // For IN, NOT IN operations
|
||||
}
|
||||
|
||||
// ConstraintSet represents a set of constraints that must all be true (AND).
|
||||
@@ -103,7 +103,7 @@ func (d *LogicalContradictionDetector) popNotContext() {
|
||||
}
|
||||
|
||||
// Visit dispatches to the appropriate visit method.
|
||||
func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) any {
|
||||
func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) interface{} {
|
||||
if tree == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) any {
|
||||
}
|
||||
|
||||
// VisitQuery is the entry point.
|
||||
func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) interface{} {
|
||||
d.Visit(ctx.Expression())
|
||||
// Check final constraints
|
||||
d.checkContradictions(d.currentConstraints())
|
||||
@@ -119,12 +119,12 @@ func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) any
|
||||
}
|
||||
|
||||
// VisitExpression just passes through to OrExpression.
|
||||
func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) interface{} {
|
||||
return d.Visit(ctx.OrExpression())
|
||||
}
|
||||
|
||||
// VisitOrExpression handles OR logic.
|
||||
func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) interface{} {
|
||||
andExpressions := ctx.AllAndExpression()
|
||||
|
||||
if len(andExpressions) == 1 {
|
||||
@@ -149,7 +149,7 @@ func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressi
|
||||
}
|
||||
|
||||
// VisitAndExpression handles AND logic (including implicit AND).
|
||||
func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) interface{} {
|
||||
unaryExpressions := ctx.AllUnaryExpression()
|
||||
|
||||
// Visit each unary expression, accumulating constraints
|
||||
@@ -161,7 +161,7 @@ func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpres
|
||||
}
|
||||
|
||||
// VisitUnaryExpression handles NOT operator.
|
||||
func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) interface{} {
|
||||
hasNot := ctx.NOT() != nil
|
||||
|
||||
if hasNot {
|
||||
@@ -180,7 +180,7 @@ func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryEx
|
||||
}
|
||||
|
||||
// VisitPrimary handles different primary expressions.
|
||||
func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) interface{} {
|
||||
if ctx.OrExpression() != nil {
|
||||
// Parenthesized expression
|
||||
// If we're in an AND context, we continue with the same constraint set
|
||||
@@ -191,9 +191,6 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
// Handle function calls if needed
|
||||
return nil
|
||||
} else if ctx.SearchCall() != nil {
|
||||
// search() spans all fields; it can never be a logical contradiction
|
||||
return nil
|
||||
} else if ctx.FullText() != nil {
|
||||
// Handle full text search if needed
|
||||
return nil
|
||||
@@ -203,7 +200,7 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
|
||||
}
|
||||
|
||||
// VisitComparison extracts constraints from comparisons.
|
||||
func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) interface{} {
|
||||
if ctx.Key() == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -277,7 +274,7 @@ func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonCo
|
||||
constraint := FieldConstraint{
|
||||
Field: field,
|
||||
Operator: operator,
|
||||
Values: []any{val1, val2},
|
||||
Values: []interface{}{val1, val2},
|
||||
}
|
||||
d.addConstraint(constraint)
|
||||
}
|
||||
@@ -346,7 +343,7 @@ func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonCo
|
||||
}
|
||||
|
||||
// extractValue extracts the actual value from a ValueContext.
|
||||
func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) any {
|
||||
func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) interface{} {
|
||||
if ctx.QUOTED_TEXT() != nil {
|
||||
text := ctx.QUOTED_TEXT().GetText()
|
||||
// Remove quotes
|
||||
@@ -365,12 +362,12 @@ func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) a
|
||||
}
|
||||
|
||||
// extractValueList extracts values from a ValueListContext.
|
||||
func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []any {
|
||||
func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []interface{} {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := []any{}
|
||||
values := []interface{}{}
|
||||
for _, val := range ctx.AllValue() {
|
||||
values = append(values, d.extractValue(val))
|
||||
}
|
||||
@@ -766,7 +763,7 @@ func (d *LogicalContradictionDetector) checkRangeContradictions(constraints []Fi
|
||||
}
|
||||
|
||||
// valuesSatisfyRanges checks if a value satisfies all range constraints.
|
||||
func (d *LogicalContradictionDetector) valuesSatisfyRanges(value any, constraints []FieldConstraint) bool {
|
||||
func (d *LogicalContradictionDetector) valuesSatisfyRanges(value interface{}, constraints []FieldConstraint) bool {
|
||||
val, err := parseNumericValue(value)
|
||||
if err != nil {
|
||||
return true // If not numeric, we can't check
|
||||
@@ -802,7 +799,7 @@ func (d *LogicalContradictionDetector) valuesSatisfyRanges(value any, constraint
|
||||
}
|
||||
|
||||
// valueSatisfiesBetween checks if a value is within a BETWEEN range.
|
||||
func (d *LogicalContradictionDetector) valueSatisfiesBetween(value any, between FieldConstraint) bool {
|
||||
func (d *LogicalContradictionDetector) valueSatisfiesBetween(value interface{}, between FieldConstraint) bool {
|
||||
if len(between.Values) != 2 {
|
||||
return false
|
||||
}
|
||||
@@ -851,7 +848,7 @@ func (d *LogicalContradictionDetector) cloneConstraintSet(set *ConstraintSet) *C
|
||||
}
|
||||
|
||||
// parseNumericValue attempts to parse a value as a number.
|
||||
func parseNumericValue(value any) (float64, error) {
|
||||
func parseNumericValue(value interface{}) (float64, error) {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
|
||||
@@ -43,8 +43,6 @@ type filterExpressionVisitor struct {
|
||||
keysWithWarnings map[string]bool
|
||||
startNs uint64
|
||||
endNs uint64
|
||||
|
||||
requiresCostGuard bool
|
||||
}
|
||||
|
||||
type FilterExprVisitorOpts struct {
|
||||
@@ -83,10 +81,9 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
|
||||
}
|
||||
|
||||
type PreparedWhereClause struct {
|
||||
WhereClause *sqlbuilder.WhereClause
|
||||
Warnings []string
|
||||
WarningsDocURL string
|
||||
RequiresCostGuard bool
|
||||
WhereClause *sqlbuilder.WhereClause
|
||||
Warnings []string
|
||||
WarningsDocURL string
|
||||
}
|
||||
|
||||
func (p PreparedWhereClause) IsEmpty() bool {
|
||||
@@ -168,12 +165,12 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhere
|
||||
|
||||
// Return empty where clause so callers can skip the WHERE clause
|
||||
if cond == "" || cond == SkipConditionLiteral {
|
||||
return PreparedWhereClause{WhereClause: nil, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL, RequiresCostGuard: visitor.requiresCostGuard}, nil
|
||||
return PreparedWhereClause{WhereClause: nil, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
}
|
||||
|
||||
whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)
|
||||
|
||||
return PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL, RequiresCostGuard: visitor.requiresCostGuard}, nil
|
||||
return PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
}
|
||||
|
||||
// Visit dispatches to the specific visit method based on node type.
|
||||
@@ -206,8 +203,6 @@ func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any {
|
||||
return v.VisitValueList(t)
|
||||
case *grammar.FullTextContext:
|
||||
return v.VisitFullText(t)
|
||||
case *grammar.SearchCallContext:
|
||||
return v.VisitSearchCall(t)
|
||||
case *grammar.FunctionCallContext:
|
||||
return v.VisitFunctionCall(t)
|
||||
case *grammar.FunctionParamListContext:
|
||||
@@ -322,8 +317,6 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
return v.Visit(ctx.Comparison())
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
return v.Visit(ctx.FunctionCall())
|
||||
} else if ctx.SearchCall() != nil {
|
||||
return v.Visit(ctx.SearchCall())
|
||||
} else if ctx.FullText() != nil {
|
||||
return v.Visit(ctx.FullText())
|
||||
}
|
||||
@@ -779,62 +772,6 @@ func normalizeFunctionValue(operator qbtypes.FilterOperator, functionName string
|
||||
return valueParams, nil
|
||||
}
|
||||
|
||||
// VisitSearchCall handles search('needle'): a keyless full-text search. The
|
||||
// visitor emits FilterOperatorSearch; the signal's condition builder owns the
|
||||
// behavior (logs fan out across all columns, other signals reject it).
|
||||
func (v *filterExpressionVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
|
||||
// Flag scan-heavy so the statement builder attaches the cost guard.
|
||||
v.requiresCostGuard = true
|
||||
|
||||
paramList := ctx.FunctionParamList()
|
||||
if paramList == nil {
|
||||
v.errors = append(v.errors, "function `search` expects a single value parameter, e.g. search('error')")
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
// Single needle today; functionParamList leaves room for scoped forms
|
||||
// (search(body, 'abc')) without a grammar change.
|
||||
params := paramList.AllFunctionParam()
|
||||
if len(params) != 1 {
|
||||
v.errors = append(v.errors, fmt.Sprintf("function `search` currently supports a single argument, e.g. search('error'), but got %d", len(params)))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
// Use the raw needle: a bare word parses as a key but we take its literal text
|
||||
// (not the normalized key, which would strip a `context.` prefix or `:type`).
|
||||
param := params[0]
|
||||
var searchText string
|
||||
switch {
|
||||
case param.Value() != nil:
|
||||
// The search needle is always a literal string: take the token text rather
|
||||
// than visiting (which parses NUMBER to float64 and would render large/round
|
||||
// integers as scientific notation, e.g. search(1000000) -> "1e+06").
|
||||
valCtx := param.Value()
|
||||
if valCtx.QUOTED_TEXT() != nil {
|
||||
searchText = trimQuotes(valCtx.QUOTED_TEXT().GetText())
|
||||
} else {
|
||||
searchText = valCtx.GetText()
|
||||
}
|
||||
case param.Key() != nil:
|
||||
searchText = param.Key().GetText()
|
||||
default:
|
||||
v.errors = append(v.errors, "function `search` expects a quoted string, e.g. search('error')")
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
// Keyless: pass an empty key as a placeholder for the ConditionFor signature.
|
||||
conds, ok := v.buildConditions(&telemetrytypes.TelemetryFieldKey{}, nil, qbtypes.FilterOperatorSearch, searchText)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
if len(conds) == 1 {
|
||||
return conds[0]
|
||||
}
|
||||
return v.builder.Or(conds...)
|
||||
}
|
||||
|
||||
// VisitFunctionParamList handles the parameter list for function calls.
|
||||
func (v *filterExpressionVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
|
||||
params := ctx.AllFunctionParam()
|
||||
|
||||
@@ -221,7 +221,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddRoleTransactionGroupsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddTagRelationRankFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addTagRelationRank struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddTagRelationRankFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_tag_relation_rank"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addTagRelationRank{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("tag_relation"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rankColumn := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("rank"),
|
||||
DataType: sqlschema.DataTypeInteger,
|
||||
Nullable: false,
|
||||
}
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, rankColumn, 0)
|
||||
|
||||
// AddColumn recreates the table for a NOT NULL column on SQLite, which drops
|
||||
// standalone indices. GetTable only reports inline table constraints, so
|
||||
// restore the standalone unique index from migration 082.
|
||||
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
|
||||
TableName: sqlschema.TableName("tag_relation"),
|
||||
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
|
||||
})...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for audit.
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for audit.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package telemetrylogs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -28,66 +27,6 @@ func NewConditionBuilder(fm qbtypes.FieldMapper, fl flagger.Flagger) *conditionB
|
||||
return &conditionBuilder{fm: fm, fl: fl}
|
||||
}
|
||||
|
||||
// conditionForSearch ORs a case-insensitive match of the needle across every
|
||||
// searchable column: log columns, the body (body_v2 JSON when use_json_body is
|
||||
// on, else the body string), and the attribute/resource maps (keys + values,
|
||||
// non-string values cast to string; JSON columns matched on their serialized
|
||||
// form). Cost is bounded by the querier's EXPLAIN ESTIMATE gate, not a window cap.
|
||||
func (c *conditionBuilder) conditionForSearch(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
// use_json_body picks the body column below; the scan budget rides on CostGuard.
|
||||
// search is a literal, case-insensitive substring match: QuoteMeta so regex
|
||||
// metacharacters in the needle match literally, and LOWER both sides on every
|
||||
// column. Keeping the body path as LOWER(toString(body_v2)) lets it use the
|
||||
// LOWER(toString(body_v2)) skip index (a (?i) regex could use neither the index
|
||||
// nor be ngram-extractable).
|
||||
needle := regexp.QuoteMeta(fmt.Sprintf("%v", value))
|
||||
|
||||
contexts := []telemetrytypes.FieldContext{
|
||||
telemetrytypes.FieldContextLog,
|
||||
telemetrytypes.FieldContextBody,
|
||||
telemetrytypes.FieldContextAttribute,
|
||||
telemetrytypes.FieldContextResource,
|
||||
}
|
||||
|
||||
useJSONBody := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID))
|
||||
|
||||
var conditions []string
|
||||
for _, fieldContext := range contexts {
|
||||
for _, col := range searchColumns(fieldContext, useJSONBody) {
|
||||
switch col.Type.GetType() {
|
||||
case schema.ColumnTypeEnumMap:
|
||||
keysExpr := fmt.Sprintf("mapKeys(%s)", col.Name)
|
||||
valsExpr := fmt.Sprintf("mapValues(%s)", col.Name)
|
||||
// match() needs a String array; cast non-string map values first.
|
||||
if mc, ok := col.Type.(schema.MapColumnType); ok && mc.ValueType.GetType() != schema.ColumnTypeEnumString {
|
||||
valsExpr = fmt.Sprintf("arrayMap(x -> toString(x), mapValues(%s))", col.Name)
|
||||
}
|
||||
conditions = append(conditions, sb.Or(
|
||||
fmt.Sprintf("arrayExists(x -> match(LOWER(x), LOWER(%s)), %s)", sb.Var(needle), keysExpr),
|
||||
fmt.Sprintf("arrayExists(x -> match(LOWER(x), LOWER(%s)), %s)", sb.Var(needle), valsExpr),
|
||||
))
|
||||
case schema.ColumnTypeEnumJSON:
|
||||
conditions = append(conditions, fmt.Sprintf("match(LOWER(toString(%s)), LOWER(%s))", col.Name, sb.Var(needle)))
|
||||
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumLowCardinality:
|
||||
conditions = append(conditions, fmt.Sprintf("match(LOWER(%s), LOWER(%s))", col.Name, sb.Var(needle)))
|
||||
default:
|
||||
return nil, nil, errors.NewInternalf(errors.CodeInternal, "search does not support the column type of %q", col.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(conditions) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
// The advisory rides on CostGuard (set by the visitor), not warnings.
|
||||
return []string{sb.Or(conditions...)}, nil, nil
|
||||
}
|
||||
|
||||
// isBodyJSONSearch reports whether a key addresses a path within the body JSON. Only
|
||||
// an explicit Body context qualifies; a bare, context-less `body` (e.g. full-text
|
||||
// `count_distinct(body)` or `body EXISTS`) is a full-text match, not a `$.body` path.
|
||||
@@ -439,11 +378,6 @@ func (c *conditionBuilder) ConditionFor(
|
||||
matches := querybuilder.MatchingFieldKeys(key, fieldKeys)
|
||||
skipResourceFilter := options.SkipResourceFilter
|
||||
|
||||
// search() is keyless; handle it before key resolution.
|
||||
if operator == qbtypes.FilterOperatorSearch {
|
||||
return c.conditionForSearch(ctx, orgID, value, sb)
|
||||
}
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, matches)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
|
||||
@@ -635,33 +635,3 @@ func (m *fieldMapper) existsExpressionFor(
|
||||
}
|
||||
return querybuilder.ExistsExpression(columns, key, tsStart, tsEnd, fieldExpression, exists)
|
||||
}
|
||||
|
||||
// searchColumns is the single source of truth for the columns search() fans out
|
||||
// across, by field context. Body is body_v2 JSON when useJSONBody, else body string.
|
||||
func searchColumns(fieldContext telemetrytypes.FieldContext, useJSONBody bool) []*schema.Column {
|
||||
switch fieldContext {
|
||||
case telemetrytypes.FieldContextLog:
|
||||
return []*schema.Column{
|
||||
logsV2Columns[LogsV2SeverityTextColumn],
|
||||
logsV2Columns[LogsV2TraceIDColumn],
|
||||
logsV2Columns[LogsV2SpanIDColumn],
|
||||
}
|
||||
case telemetrytypes.FieldContextBody:
|
||||
if useJSONBody {
|
||||
return []*schema.Column{logsV2Columns[LogsV2BodyV2Column]}
|
||||
}
|
||||
return []*schema.Column{logsV2Columns[LogsV2BodyColumn]}
|
||||
case telemetrytypes.FieldContextAttribute:
|
||||
return []*schema.Column{
|
||||
logsV2Columns[LogsV2AttributesStringColumn],
|
||||
logsV2Columns[LogsV2AttributesNumberColumn],
|
||||
logsV2Columns[LogsV2AttributesBoolColumn],
|
||||
}
|
||||
case telemetrytypes.FieldContextResource:
|
||||
return []*schema.Column{
|
||||
logsV2Columns[LogsV2ResourcesStringColumn],
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
category: "Single word",
|
||||
query: "<script>alert('xss')</script>",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '<'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '<'",
|
||||
},
|
||||
|
||||
// Single word searches with spaces
|
||||
@@ -181,7 +181,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
category: "Special characters",
|
||||
query: "[tracing]",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '['",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '['",
|
||||
},
|
||||
{
|
||||
category: "Special characters",
|
||||
@@ -211,7 +211,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
category: "Special characters",
|
||||
query: "ERROR: cannot execute update() in a read-only context",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got ')'",
|
||||
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got ')'",
|
||||
},
|
||||
{
|
||||
category: "Special characters",
|
||||
@@ -633,7 +633,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -641,7 +641,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -649,7 +649,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF",
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -657,7 +657,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'like'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'like'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -665,7 +665,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -673,7 +673,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -681,7 +681,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'exists'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'exists'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -689,7 +689,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'regexp'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'regexp'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -697,7 +697,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'contains'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'contains'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -2052,9 +2052,9 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
|
||||
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'AND'"},
|
||||
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'OR'"},
|
||||
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF"},
|
||||
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'AND'"},
|
||||
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'OR'"},
|
||||
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF"},
|
||||
|
||||
{category: "Only functions", query: "has", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
|
||||
{category: "Only functions", query: "hasAny", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
|
||||
@@ -2196,7 +2196,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2204,7 +2204,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2212,7 +2212,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '='",
|
||||
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '='",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2220,7 +2220,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2228,7 +2228,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
|
||||
},
|
||||
|
||||
// Using function keywords as keys
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
package telemetrylogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// searchFanOut returns the WHERE fragment search() fans out to. bodyExpr is the
|
||||
// body match expression, which differs between the legacy string body and the
|
||||
// body_v2 JSON column.
|
||||
func searchFanOut(bodyExpr string) string {
|
||||
return "(match(LOWER(severity_text), LOWER(?)) OR match(LOWER(trace_id), LOWER(?)) OR match(LOWER(span_id), LOWER(?)) OR " +
|
||||
bodyExpr + " OR " +
|
||||
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(attributes_string)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), mapValues(attributes_string))) OR " +
|
||||
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(attributes_number)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), arrayMap(x -> toString(x), mapValues(attributes_number)))) OR " +
|
||||
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(attributes_bool)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), arrayMap(x -> toString(x), mapValues(attributes_bool)))) OR " +
|
||||
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(resources_string)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), mapValues(resources_string))))"
|
||||
}
|
||||
|
||||
// searchArgs returns v repeated once per bound parameter search() emits — one per
|
||||
// searchable column expression (currently 12).
|
||||
func searchArgs(v any) []any {
|
||||
const searchColumnParams = 12
|
||||
args := make([]any, searchColumnParams)
|
||||
for i := range args {
|
||||
args[i] = v
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// TestFilterExprSearch covers the search('needle') function, which fans out
|
||||
// across every searchable column via FilterOperatorSearch.
|
||||
func TestFilterExprSearch(t *testing.T) {
|
||||
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
|
||||
inWindowStart := uint64(releaseTime.Add(-5 * time.Minute).UnixNano())
|
||||
inWindowEnd := uint64(releaseTime.Add(5 * time.Minute).UnixNano())
|
||||
|
||||
legacyBody := "match(LOWER(body), LOWER(?))"
|
||||
jsonBody := "match(LOWER(toString(body_v2)), LOWER(?))"
|
||||
|
||||
serviceNameEq := "(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? " +
|
||||
"AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)"
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
jsonBodyEnabled bool
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
startNs uint64
|
||||
endNs uint64
|
||||
shouldPass bool
|
||||
expectedQuery string
|
||||
expectedArgs []any
|
||||
expectWarning bool
|
||||
expectedErrorContains string
|
||||
}{
|
||||
{
|
||||
name: "quoted, legacy body",
|
||||
query: "search('error')",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(legacyBody),
|
||||
expectedArgs: searchArgs("error"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
name: "quoted, json body",
|
||||
query: "search('error')",
|
||||
jsonBodyEnabled: true,
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(jsonBody),
|
||||
expectedArgs: searchArgs("error"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
name: "bare word",
|
||||
query: "search(timeout)",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(legacyBody),
|
||||
expectedArgs: searchArgs("timeout"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
name: "negated",
|
||||
query: "NOT search('error')",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE NOT (" + searchFanOut(legacyBody) + ")",
|
||||
expectedArgs: searchArgs("error"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
name: "combined with field filter",
|
||||
query: "search('error') AND service.name=\"api\"",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE (" + searchFanOut(legacyBody) + " AND " + serviceNameEq + ")",
|
||||
expectedArgs: append(searchArgs("error"), "api"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
// A wide window is allowed at build time; scan cost is bounded by the
|
||||
// querier's EXPLAIN ESTIMATE gate, not a window cap in the builder.
|
||||
name: "wide window builds (estimate gate lives in querier)",
|
||||
query: "search('error')",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: uint64(releaseTime.Add(-10 * time.Hour).UnixNano()),
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(legacyBody),
|
||||
expectedArgs: searchArgs("error"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
// search() is keyless and independent of fullTextColumn (which only
|
||||
// governs bare/quoted free text). It must work even when unset.
|
||||
name: "independent of full text column",
|
||||
query: "search('error')",
|
||||
fullTextColumn: nil,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(legacyBody),
|
||||
expectedArgs: searchArgs("error"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
// A context-prefixed bare word must be used as the literal needle, not
|
||||
// normalized into a field key (Normalize would strip "resource.").
|
||||
name: "bare word with context prefix is not normalized",
|
||||
query: "search(resource.deployment)",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(legacyBody),
|
||||
expectedArgs: searchArgs("resource\\.deployment"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
// A numeric needle must be the literal digits, not the %v rendering of a
|
||||
// parsed float64 (which would make search(1000000) scan for "1e+06").
|
||||
name: "numeric needle is not scientific notation",
|
||||
query: "search(1000000)",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: true,
|
||||
expectedQuery: "WHERE " + searchFanOut(legacyBody),
|
||||
expectedArgs: searchArgs("1000000"),
|
||||
expectWarning: true,
|
||||
},
|
||||
{
|
||||
name: "too many parameters",
|
||||
query: "search('error', 'timeout')",
|
||||
fullTextColumn: DefaultFullTextColumn,
|
||||
startNs: inWindowStart,
|
||||
endNs: inWindowEnd,
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "currently supports a single argument",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fl := flaggertest.WithBooleanFlags(t, map[string]bool{
|
||||
flagger.FeatureUseJSONBody.String(): tc.jsonBodyEnabled,
|
||||
})
|
||||
fm := NewFieldMapper(fl)
|
||||
cb := NewConditionBuilder(fm, fl)
|
||||
keys := buildCompleteFieldKeyMap(releaseTime)
|
||||
|
||||
opts := querybuilder.FilterExprVisitorOpts{
|
||||
Context: context.Background(),
|
||||
Logger: instrumentationtest.New().Logger(),
|
||||
FieldMapper: fm,
|
||||
ConditionBuilder: cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: tc.fullTextColumn,
|
||||
StartNs: tc.startNs,
|
||||
EndNs: tc.endNs,
|
||||
}
|
||||
|
||||
clause, err := querybuilder.PrepareWhereClause(tc.query, opts)
|
||||
|
||||
if !tc.shouldPass {
|
||||
require.Error(t, err)
|
||||
require.True(t, detailContains(err, tc.expectedErrorContains),
|
||||
"error %v should contain %q", err, tc.expectedErrorContains)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, clause.IsEmpty())
|
||||
|
||||
sql, args := clause.WhereClause.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
require.Equal(t, tc.expectedQuery, sql)
|
||||
require.Equal(t, tc.expectedArgs, args)
|
||||
|
||||
if tc.expectWarning {
|
||||
// The visitor only flags the cost guard; the statement builder
|
||||
// materializes the advisory + budget from config downstream.
|
||||
require.True(t, clause.RequiresCostGuard)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchCostGuard covers the search() path end-to-end through Build: the
|
||||
// statement carries a CostGuard with its scan budget and the advisory warning.
|
||||
func TestSearchCostGuard(t *testing.T) {
|
||||
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
|
||||
ctx := context.Background()
|
||||
start := uint64(releaseTime.Add(-5 * time.Minute).UnixMilli())
|
||||
end := uint64(releaseTime.UnixMilli())
|
||||
|
||||
fl := flaggertest.WithBooleanFlags(t, map[string]bool{})
|
||||
fm := NewFieldMapper(fl)
|
||||
cb := NewConditionBuilder(fm, fl)
|
||||
store := telemetrytypestest.NewMockMetadataStore()
|
||||
store.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
||||
rewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, fl)
|
||||
sb := NewLogQueryStatementBuilder(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
store, fm, cb, rewriter, DefaultFullTextColumn, fl, nil, false, 100000,
|
||||
WithSearchMaxScanRows(100000),
|
||||
)
|
||||
query := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "search('error')"},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
stmt, err := sb.Build(ctx, valuer.UUID{}, start, end, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stmt.CostGuard)
|
||||
require.Contains(t, stmt.Warnings, querybuilder.SearchWarning)
|
||||
}
|
||||
@@ -29,20 +29,11 @@ type logQueryStatementBuilder struct {
|
||||
fl flagger.Flagger
|
||||
skipResourceFingerprintEnabled bool
|
||||
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
searchMaxScanRows int64
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
}
|
||||
|
||||
var _ qbtypes.StatementBuilder[qbtypes.LogAggregation] = (*logQueryStatementBuilder)(nil)
|
||||
|
||||
type LogQueryStatementBuilderOption func(*logQueryStatementBuilder)
|
||||
|
||||
// WithSearchMaxScanRows sets the estimated-rows budget the querier enforces for
|
||||
// search() statements (0 disables the gate).
|
||||
func WithSearchMaxScanRows(n int64) LogQueryStatementBuilderOption {
|
||||
return func(b *logQueryStatementBuilder) { b.searchMaxScanRows = n }
|
||||
}
|
||||
|
||||
func NewLogQueryStatementBuilder(
|
||||
settings factory.ProviderSettings,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
@@ -54,7 +45,6 @@ func NewLogQueryStatementBuilder(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
skipResourceFingerprintEnable bool,
|
||||
skipResourceFingerprintThreshold uint64,
|
||||
opts ...LogQueryStatementBuilderOption,
|
||||
) *logQueryStatementBuilder {
|
||||
logsSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrylogs")
|
||||
|
||||
@@ -71,7 +61,7 @@ func NewLogQueryStatementBuilder(
|
||||
skipResourceFingerprintThreshold,
|
||||
)
|
||||
|
||||
b := &logQueryStatementBuilder{
|
||||
return &logQueryStatementBuilder{
|
||||
logger: logsSettings.Logger(),
|
||||
metadataStore: metadataStore,
|
||||
fm: fieldMapper,
|
||||
@@ -82,10 +72,6 @@ func NewLogQueryStatementBuilder(
|
||||
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
|
||||
fullTextColumn: fullTextColumn,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Build builds a SQL query for logs based on the given parameters.
|
||||
@@ -131,23 +117,9 @@ func (b *logQueryStatementBuilder) Build(
|
||||
}
|
||||
|
||||
stmt.Warnings = append(stmt.Warnings, warnings...)
|
||||
// The search flag is gated in the condition builder; here the advisory rides on
|
||||
// the statement so the querier can enforce the scan budget from the CostGuard.
|
||||
if stmt.CostGuard != nil && stmt.CostGuard.Warning != "" {
|
||||
stmt.Warnings = append(stmt.Warnings, stmt.CostGuard.Warning)
|
||||
}
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
// costGuardFor builds the cost guard for a scan-heavy (search()) statement,
|
||||
// pairing the advisory with the configured scan budget. Returns nil otherwise.
|
||||
func (b *logQueryStatementBuilder) costGuardFor(required bool) *qbtypes.CostGuard {
|
||||
if !required {
|
||||
return nil
|
||||
}
|
||||
return &qbtypes.CostGuard{Warning: querybuilder.SearchWarning, MaxScanRows: b.searchMaxScanRows}
|
||||
}
|
||||
|
||||
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation], bodyJSONEnabled bool) ([]*telemetrytypes.FieldKeySelector, []string) {
|
||||
var keySelectors []*telemetrytypes.FieldKeySelector
|
||||
var warnings []string
|
||||
@@ -385,7 +357,6 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
CostGuard: b.costGuardFor(preparedWhereClause.RequiresCostGuard),
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -552,7 +523,6 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
CostGuard: b.costGuardFor(preparedWhereClause.RequiresCostGuard),
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -680,7 +650,6 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
CostGuard: b.costGuardFor(preparedWhereClause.RequiresCostGuard),
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
|
||||
@@ -157,7 +157,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for metrics.
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for metrics.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for traces.
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for traces.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ var (
|
||||
// GCP services.
|
||||
GCPServiceCloudSQLPostgres = ServiceID{valuer.NewString("cloudsql_postgres")}
|
||||
GCPServiceMemorystoreRedis = ServiceID{valuer.NewString("memorystore_redis")}
|
||||
GCPServiceComputeEngine = ServiceID{valuer.NewString("computeengine")}
|
||||
)
|
||||
|
||||
func (ServiceID) Enum() []any {
|
||||
@@ -76,6 +77,7 @@ func (ServiceID) Enum() []any {
|
||||
AzureServiceRedis,
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
GCPServiceComputeEngine,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +117,7 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
CloudProviderTypeGCP: {
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
GCPServiceComputeEngine,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -118,10 +118,6 @@ const (
|
||||
FilterOperatorHasToken
|
||||
FilterOperatorHasAny
|
||||
FilterOperatorHasAll
|
||||
|
||||
// FilterOperatorSearch backs search('needle'): a keyless search the condition
|
||||
// builder fans out across every searchable column.
|
||||
FilterOperatorSearch
|
||||
)
|
||||
|
||||
var operatorInverseMapping = map[FilterOperator]FilterOperator{
|
||||
@@ -190,8 +186,7 @@ func (f FilterOperator) IsNegativeOperator() bool {
|
||||
FilterOperatorIn,
|
||||
FilterOperatorExists,
|
||||
FilterOperatorRegexp,
|
||||
FilterOperatorContains,
|
||||
FilterOperatorSearch:
|
||||
FilterOperatorContains:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -248,11 +243,10 @@ func (f FilterOperator) IsArrayFunctionOperator() bool {
|
||||
}
|
||||
|
||||
// IsFunctionOperator reports whether the operator is a query function
|
||||
// (has/hasAny/hasAll/hasToken/search). These are logs-only; other signals reject
|
||||
// them and the resource-fingerprint builder skips them.
|
||||
// (has/hasAny/hasAll/hasToken); these apply to the logs body column only.
|
||||
func (f FilterOperator) IsFunctionOperator() bool {
|
||||
switch f {
|
||||
case FilterOperatorHas, FilterOperatorHasAny, FilterOperatorHasAll, FilterOperatorHasToken, FilterOperatorSearch:
|
||||
case FilterOperatorHas, FilterOperatorHasAny, FilterOperatorHasAll, FilterOperatorHasToken:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -271,8 +265,6 @@ func (f FilterOperator) FunctionName() string {
|
||||
return "hasAll"
|
||||
case FilterOperatorHasToken:
|
||||
return "hasToken"
|
||||
case FilterOperatorSearch:
|
||||
return "search"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -57,12 +57,6 @@ type Statement struct {
|
||||
Args []any
|
||||
Warnings []string
|
||||
WarningsDocURL string
|
||||
CostGuard *CostGuard
|
||||
}
|
||||
|
||||
type CostGuard struct {
|
||||
Warning string
|
||||
MaxScanRows int64
|
||||
}
|
||||
|
||||
// StatementBuilder builds the query.
|
||||
|
||||
@@ -16,27 +16,23 @@ type TagRelation struct {
|
||||
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
|
||||
ResourceID valuer.UUID `json:"resourceId" required:"true" bun:"resource_id,type:text,notnull"`
|
||||
TagID valuer.UUID `json:"tagId" required:"true" bun:"tag_id,type:text,notnull"`
|
||||
// Rank is the tag's position within its resource; reads order by it.
|
||||
Rank int `json:"rank" bun:"rank,notnull"`
|
||||
CreatedAt time.Time `json:"createdAt" bun:"created_at,notnull"`
|
||||
CreatedAt time.Time `json:"createdAt" bun:"created_at,notnull"`
|
||||
}
|
||||
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID, rank int) *TagRelation {
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID) *TagRelation {
|
||||
return &TagRelation{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
Kind: kind,
|
||||
ResourceID: resourceID,
|
||||
TagID: tagID,
|
||||
Rank: rank,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTagRelations ranks each tag by its position so reads preserve order.
|
||||
func NewTagRelations(kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) []*TagRelation {
|
||||
relations := make([]*TagRelation, 0, len(tagIDs))
|
||||
for rank, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID, rank))
|
||||
for _, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID))
|
||||
}
|
||||
return relations
|
||||
}
|
||||
|
||||
@@ -109,8 +109,6 @@ func (v *variableReplacementVisitor) Visit(tree antlr.ParseTree) any {
|
||||
return v.VisitValueList(t)
|
||||
case *grammar.FullTextContext:
|
||||
return v.VisitFullText(t)
|
||||
case *grammar.SearchCallContext:
|
||||
return v.VisitSearchCall(t)
|
||||
case *grammar.FunctionCallContext:
|
||||
return v.VisitFunctionCall(t)
|
||||
case *grammar.FunctionParamListContext:
|
||||
@@ -205,8 +203,6 @@ func (v *variableReplacementVisitor) VisitPrimary(ctx *grammar.PrimaryContext) a
|
||||
return v.Visit(ctx.Comparison())
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
return v.Visit(ctx.FunctionCall())
|
||||
} else if ctx.SearchCall() != nil {
|
||||
return v.Visit(ctx.SearchCall())
|
||||
} else if ctx.FullText() != nil {
|
||||
return v.Visit(ctx.FullText())
|
||||
}
|
||||
@@ -411,13 +407,6 @@ func (v *variableReplacementVisitor) VisitFunctionCall(ctx *grammar.FunctionCall
|
||||
return functionName + "(" + params + ")"
|
||||
}
|
||||
|
||||
func (v *variableReplacementVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
|
||||
if ctx.FunctionParamList() == nil {
|
||||
return "search()"
|
||||
}
|
||||
return "search(" + v.Visit(ctx.FunctionParamList()).(string) + ")"
|
||||
}
|
||||
|
||||
func (v *variableReplacementVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
|
||||
params := ctx.AllFunctionParam()
|
||||
parts := make([]string, 0, len(params))
|
||||
|
||||
@@ -570,11 +570,7 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
assert alpha["schemaVersion"] == "v6"
|
||||
assert alpha["source"] == "user"
|
||||
assert alpha["locked"] is False
|
||||
# tags round-trip in the order sent — no reordering, no drift
|
||||
assert alpha["tags"] == [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
]
|
||||
assert {"key": "team", "value": "pulse"} in alpha["tags"]
|
||||
|
||||
# ── stage 3: list everything ─────────────────────────────────────────────
|
||||
response = requests.get(
|
||||
@@ -594,13 +590,6 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
"Epsilon Metrics",
|
||||
"Zeta Overview",
|
||||
}
|
||||
# per-dashboard tags also round-trip in the order sent
|
||||
delta = next(d for d in body["data"]["dashboards"] if d["spec"]["display"]["name"] == "Delta Storage")
|
||||
assert delta["tags"] == [
|
||||
{"key": "team", "value": "storage"},
|
||||
{"key": "env", "value": "dev"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
]
|
||||
# top-level tags = org-wide distinct tag set, sorted case-insensitively
|
||||
# by (key, value). Asserting the exact list (not a set) locks in the sort.
|
||||
assert body["data"]["tags"] == [
|
||||
@@ -1024,99 +1013,6 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
assert response.json()["data"]["id"] == clone["id"]
|
||||
|
||||
|
||||
def test_dashboard_v2_tag_order_round_trips(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Tags must round-trip in the order the client sent them across every write
|
||||
path — create, update, patch — so GitOps/Terraform state never drifts."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
def tags_of(response: requests.Response) -> list[dict]:
|
||||
return response.json()["data"]["tags"]
|
||||
|
||||
# ── create with tags in a deliberate order; "env" repeats with two values ─
|
||||
created_order = [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
]
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}}, "tags": created_order},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
dashboard_id = response.json()["data"]["id"]
|
||||
assert tags_of(response) == created_order
|
||||
|
||||
# ── get echoes the same order ────────────────────────────────────────────
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == created_order
|
||||
|
||||
# ── update reordered; the two "env" values land at non-adjacent positions ─
|
||||
reordered = [
|
||||
{"key": "tier", "value": "critical"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}}, "tags": reordered},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == reordered
|
||||
|
||||
# ── patch appends a new tag (a third "env" value); it lands at the end ────
|
||||
response = requests.patch(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json=[{"op": "add", "path": "/tags/-", "value": {"key": "env", "value": "dev"}}],
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == reordered + [{"key": "env", "value": "dev"}]
|
||||
|
||||
# ── update reorders everything and adds another new tag → all in the new order ─
|
||||
# "env" now carries three interleaved values
|
||||
new_order = [
|
||||
{"key": "env", "value": "prod"},
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "dev"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "team", "value": "storage"},
|
||||
]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}}, "tags": new_order},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == new_order
|
||||
|
||||
# ── and the final order persists on a fresh read ─────────────────────────
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == new_order
|
||||
|
||||
|
||||
def test_dashboard_v2_pin_limit(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
|
||||
Reference in New Issue
Block a user