Compare commits

...

38 Commits

Author SHA1 Message Date
Piyush Singariya
2f762a86b3 chore: changes based on review 2026-06-02 18:54:08 +05:30
Piyush Singariya
da9b7b25ed Merge branch 'main' into fts-logs 2026-06-02 14:34:27 +05:30
Piyush Singariya
0546050b82 chore: comment fixes 2026-06-02 14:33:53 +05:30
Piyush Singariya
b8de25e2eb chore: update tests 2026-06-02 14:21:55 +05:30
Nikhil Soni
e43aeb8e24 fix: add references in waterfall response (#11536)
* fix: add references in waterfall response

* chore: update openapi specs

* chore: mark reference a non nullable field
2026-06-02 08:42:26 +00:00
Aditya Singh
9074208b09 feat: added trace events (#11538) 2026-06-02 08:41:39 +00:00
Piyush Singariya
8f77b5451c chore: enforce quotes in search 2026-06-02 13:53:06 +05:30
Piyush Singariya
a55cab858d chore: make SearchFunction call first class function 2026-06-02 13:35:54 +05:30
Piyush Singariya
8f4a48ee51 Merge branch 'main' into fts-logs 2026-06-02 12:41:04 +05:30
Piyush Singariya
c1d788f59e chore: rename fullTextColumn to freeTextColumn 2026-06-02 12:40:02 +05:30
Piyush Singariya
cd720beb7d fix: revert free text search related changes 2026-06-02 12:36:28 +05:30
Vishal Sharma
571e23910e feat(ai-assistant): show descriptive Noz hover tooltip on all entry points (#11526)
Noz launched under early access and its three entry points (header button,
floating trigger, sidebar nav item) previously showed a bare "Noz" tooltip,
which does not educate users who don't yet recognize the name. Replace it with
a shared descriptive tooltip ("Noz, your AI teammate") sourced from a single
constant so the surfaces never drift.

- Add NOZ_TOOLTIP_TITLE in components/Noz/Noz.constants.ts
- Header button and floating trigger read the constant
- Add optional tooltip field to SidebarItem; NavItem wraps the whole row in a
  Tooltip (non-pinnable items only, to avoid nesting with the pin tooltip)
- Move the trigger's Noz icon to the Button prefix slot to match the codebase
  convention for icon-only buttons
2026-06-02 06:53:57 +00:00
Piyush Singariya
63c7b24eac Merge branch 'main' into fts-logs 2026-06-02 11:21:36 +05:30
Piyush Singariya
c5752829f7 fix: remove fulltext column 2026-06-02 11:19:43 +05:30
Piyush Singariya
404c10685d Merge branch 'main' into fts-logs 2026-06-01 17:14:13 +05:30
Piyush Singariya
e7c83604de fix: argument checks 2026-06-01 17:13:45 +05:30
Piyush Singariya
3b17f31948 Merge branch 'main' into fts-logs 2026-06-01 16:44:48 +05:30
Piyush Singariya
ba8c83f47e Merge branch 'main' into fts-logs 2026-05-26 11:20:46 +05:30
Piyush Singariya
ad2c46eebb Merge branch 'main' into fts-logs 2026-05-26 11:19:33 +05:30
Piyush Singariya
f9110873f8 fix: trace keyword matching failing tests 2026-05-26 11:19:22 +05:30
Piyush Singariya
70bbfdd937 fix: comment fixes 2026-05-26 09:21:47 +05:30
Piyush Singariya
10a8db013b Merge branch 'main' into fts-logs 2026-05-26 09:02:46 +05:30
Piyush Singariya
249d34a9dc fix: timeseries enabled for frequency chart 2026-05-26 09:00:57 +05:30
Piyush Singariya
9b5ad29794 chore: integration tests 2026-05-22 13:57:37 +05:30
Piyush Singariya
90c2622da9 test: remove unnecessary test 2026-05-21 12:58:21 +05:30
Piyush Singariya
36b36fa80d Merge branch 'main' into fts-logs 2026-05-21 12:53:19 +05:30
Piyush Singariya
5a547cf358 fix: lint and test 2026-05-21 12:53:01 +05:30
Piyush Singariya
937c3f9359 Merge branch 'main' into fts-logs 2026-05-21 12:19:55 +05:30
Piyush Singariya
a898225737 fix: some changes 2026-05-20 12:31:51 +05:30
Piyush Singariya
81f382e353 Merge branch 'main' into fts-logs 2026-05-20 12:15:49 +05:30
Piyush Singariya
ef0ab2fe8e Merge branch 'main' into fts-logs 2026-05-18 17:57:35 +05:30
Piyush Singariya
6c649d35cb chore: replace with bool var 2026-05-18 17:14:02 +05:30
Piyush Singariya
d5c3fe1651 fix: only raw queries using fts 2026-05-18 16:21:37 +05:30
Piyush Singariya
83c43ece31 chore: separate function for FTS 2026-05-18 14:42:08 +05:30
Piyush Singariya
a87654a614 chore: fixing field mapper 2026-05-15 12:00:42 +05:30
Piyush Singariya
952f5d6e91 fix: fts is working partially 2026-05-13 16:48:49 +05:30
Piyush Singariya
2154ea30a6 fix: working on fts 2026-05-12 16:01:56 +05:30
Piyush Singariya
6e3857b840 chore: initial fts logs setup 2026-05-12 14:02:11 +05:30
62 changed files with 2668 additions and 1010 deletions

View File

@@ -6572,6 +6572,15 @@ components:
nullable: true
type: array
type: object
SpantypesOtelSpanRef:
properties:
refType:
type: string
spanId:
type: string
traceId:
type: string
type: object
SpantypesPostableSpanMapper:
properties:
config:
@@ -6835,6 +6844,10 @@ components:
type: string
parent_span_id:
type: string
references:
items:
$ref: '#/components/schemas/SpantypesOtelSpanRef'
type: array
resource:
additionalProperties:
type: string
@@ -6860,6 +6873,8 @@ components:
type: string
trace_state:
type: string
required:
- references
type: object
TagtypesPostableTag:
properties:

View File

@@ -7768,6 +7768,21 @@ export interface SpantypesGettableTraceAggregationsDTO {
aggregations: SpantypesSpanAggregationResultDTO[];
}
export interface SpantypesOtelSpanRefDTO {
/**
* @type string
*/
refType?: string;
/**
* @type string
*/
spanId?: string;
/**
* @type string
*/
traceId?: string;
}
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
@@ -7862,6 +7877,10 @@ export interface SpantypesWaterfallSpanDTO {
* @type string
*/
parent_span_id?: string;
/**
* @type array
*/
references: SpantypesOtelSpanRefDTO[];
/**
* @type object,null
*/

View File

@@ -4,6 +4,7 @@ import { Dot } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from 'container/AIAssistant/events';
@@ -109,7 +110,7 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title="Noz">
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="solid"
color="secondary"

View File

@@ -0,0 +1,2 @@
/** Shared hover copy for every Noz entry point (header, floating trigger, sidebar). */
export const NOZ_TOOLTIP_TITLE = 'Noz, your AI teammate';

View File

@@ -5,6 +5,7 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
@@ -42,16 +43,15 @@ export default function AIAssistantTrigger(): JSX.Element | null {
}
return (
<TooltipSimple title="Noz">
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="solid"
color="primary"
className={`${styles.trigger} noz-wave`}
onClick={handleOpen}
aria-label="Open Noz"
>
<Noz size={24} />
</Button>
prefix={<Noz size={24} />}
/>
</TooltipSimple>
);
}

View File

@@ -5,7 +5,6 @@ import { Pin, PinOff } from '@signozhq/icons';
import { SidebarItem } from '../sideNav.types';
import './NavItem.styles.scss';
import './NavItem.styles.scss';
export default function NavItem({
@@ -27,7 +26,7 @@ export default function NavItem({
showIcon?: boolean;
dataTestId?: string;
}): JSX.Element {
const { label, icon, isBeta, isNew, isEarlyAccess } = item;
const { label, icon, isBeta, isNew, isEarlyAccess, tooltip } = item;
const handleTogglePinClick = (
event: React.MouseEvent<SVGSVGElement, MouseEvent>,
@@ -36,7 +35,7 @@ export default function NavItem({
onTogglePin?.(item);
};
return (
const navItem = (
<div
className={cx(
'nav-item',
@@ -107,6 +106,15 @@ export default function NavItem({
</div>
</div>
);
// Only non-pinnable items set `tooltip`; it would nest with the pin tooltip.
return tooltip ? (
<Tooltip title={tooltip} placement="right">
{navItem}
</Tooltip>
) : (
navItem
);
}
NavItem.defaultProps = {

View File

@@ -45,6 +45,7 @@ import {
} from './sideNav.types';
import { Style } from '@signozhq/design-tokens';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
export const getStartedMenuItem = {
key: ROUTES.GET_STARTED,
@@ -97,6 +98,7 @@ export const aiAssistantMenuItem = {
icon: <Noz size={16} />,
itemKey: 'ai-assistant',
isEarlyAccess: true,
tooltip: NOZ_TOOLTIP_TITLE,
};
export const shortcutMenuItem = {

View File

@@ -15,6 +15,8 @@ export interface SidebarItem {
isBeta?: boolean;
isNew?: boolean;
isEarlyAccess?: boolean;
/** Hover copy for the whole item row (e.g. Noz's early-access tagline). */
tooltip?: ReactNode;
isPinned?: boolean;
children?: SidebarItem[];
isExternal?: boolean;

View File

@@ -22,6 +22,7 @@ import styles from './AnalyticsPanel.module.scss';
interface AnalyticsPanelProps {
isOpen: boolean;
onClose: () => void;
onTabChange: (tab: string) => void;
}
const PANEL_WIDTH = 350;
@@ -32,6 +33,7 @@ const PANEL_MARGIN_BOTTOM = 50;
function AnalyticsPanel({
isOpen,
onClose,
onTabChange,
}: AnalyticsPanelProps): JSX.Element | null {
const aggregations = useTraceStore((s) => s.aggregations);
const colorByFieldName = useTraceStore((s) => s.colorByField.name);
@@ -118,7 +120,7 @@ function AnalyticsPanel({
/>
<div className={styles.body}>
<TabsRoot defaultValue="exec-time">
<TabsRoot defaultValue="exec-time" onValueChange={onTabChange}>
<TabsList variant="secondary">
<TabsTrigger value="exec-time" variant="secondary">
% exec time

View File

@@ -31,7 +31,12 @@ import Events from 'container/SpanDetailsDrawer/Events/Events';
import SpanLogs from 'container/SpanDetailsDrawer/SpanLogs/SpanLogs';
import { useSpanContextLogs } from 'container/SpanDetailsDrawer/SpanLogs/useSpanContextLogs';
import dayjs from 'dayjs';
import {
TraceDetailEventKeys,
TraceDetailEvents,
} from 'pages/TraceDetailsV3/events';
import { useMigratePinnedAttributes } from 'pages/TraceDetailsV3/hooks/useMigratePinnedAttributes';
import { useTraceDetailLogEvent } from 'pages/TraceDetailsV3/hooks/useTraceDetailLogEvent';
import {
getSpanAttribute,
getSpanDisplayData,
@@ -86,6 +91,16 @@ function SpanDetailsContent({
}): JSX.Element {
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
const spanAttributeActions = useSpanAttributeActions();
const logTraceEvent = useTraceDetailLogEvent('v3', selectedSpan.trace_id);
const handleTabChange = useCallback(
(tab: string): void => {
logTraceEvent(TraceDetailEvents.SpanPanelTabChanged, {
[TraceDetailEventKeys.Tab]: tab,
[TraceDetailEventKeys.SpanId]: selectedSpan.span_id,
});
},
[logTraceEvent, selectedSpan.span_id],
);
const percentile = useSpanPercentile(selectedSpan);
const linkedSpans = useLinkedSpans((selectedSpan as any).references);
@@ -376,7 +391,7 @@ function SpanDetailsContent({
<div className={styles.tabsSection}>
{/* Step 9: ContentTabs */}
<TabsRoot defaultValue="overview">
<TabsRoot defaultValue="overview" onValueChange={handleTabChange}>
<TabsList variant="secondary">
<TabsTrigger value="overview" variant="secondary">
<Bookmark size={14} /> Overview

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import {
@@ -29,6 +29,8 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
import { DataSource } from 'types/common/queryBuilder';
import { TraceDetailEventKeys, TraceDetailEvents } from '../events';
import { useTraceDetailLogEvent } from '../hooks/useTraceDetailLogEvent';
import { useTraceStore } from '../stores/traceStore';
import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
@@ -90,11 +92,35 @@ function TraceDetailsHeader({
const previewFields = useTraceStore((s) => s.previewFields);
const setPreviewFields = useTraceStore((s) => s.setPreviewFields);
const logTraceEvent = useTraceDetailLogEvent('v3', traceID || '');
const pageLoadedAtRef = useRef(Date.now());
const handleSwitchToOldView = useCallback((): void => {
logTraceEvent(TraceDetailEvents.ViewSwitched, {
[TraceDetailEventKeys.From]: 'v3',
[TraceDetailEventKeys.To]: 'v2',
[TraceDetailEventKeys.DwellMs]: Date.now() - pageLoadedAtRef.current,
});
setLocalStorageKey(LOCALSTORAGE.TRACE_DETAILS_PREFER_OLD_VIEW, 'true');
const oldUrl = `/trace-old/${traceID}${window.location.search}`;
history.replace(oldUrl);
}, [traceID]);
}, [traceID, logTraceEvent]);
const handleToggleAnalytics = useCallback((): void => {
logTraceEvent(TraceDetailEvents.AnalyticsPanelToggled, {
[TraceDetailEventKeys.Open]: !isAnalyticsOpen,
});
setIsAnalyticsOpen((prev) => !prev);
}, [logTraceEvent, isAnalyticsOpen]);
const handleAnalyticsTabChange = useCallback(
(tab: string): void => {
logTraceEvent(TraceDetailEvents.AnalyticsTabChanged, {
[TraceDetailEventKeys.Tab]: tab,
});
},
[logTraceEvent],
);
const handlePreviousBtnClick = useCallback((): void => {
if (hasInAppHistory()) {
@@ -167,7 +193,7 @@ function TraceDetailsHeader({
size="icon"
color="secondary"
aria-label="Analytics"
onClick={(): void => setIsAnalyticsOpen((prev) => !prev)}
onClick={handleToggleAnalytics}
>
<ChartPie size={14} />
</Button>
@@ -245,6 +271,7 @@ function TraceDetailsHeader({
<AnalyticsPanel
isOpen={isAnalyticsOpen}
onClose={(): void => setIsAnalyticsOpen(false)}
onTabChange={handleAnalyticsTabChange}
/>
</div>
);

View File

@@ -0,0 +1,38 @@
export enum TraceDetailEvents {
DataLoaded = 'Trace Detail: Data loaded',
ViewSwitched = 'Trace Detail: View switched',
FlameGraphToggled = 'Trace Detail: Flame graph toggled',
WaterfallToggled = 'Trace Detail: Waterfall toggled',
AnalyticsPanelToggled = 'Trace Detail: Analytics panel toggled',
AnalyticsTabChanged = 'Trace Detail: Analytics tab changed',
SpanPanelTabChanged = 'Trace Detail: Span panel tab changed',
}
export enum TraceDetailEventKeys {
// Injected on every event by useTraceDetailLogEvent
View = 'view',
TraceId = 'traceId',
// Data loaded — trace shape
TotalSpansCount = 'totalSpansCount',
NumServices = 'numServices',
TraceDurationMs = 'traceDurationMs',
HadErrors = 'hadErrors',
FlamegraphSampled = 'flamegraphSampled',
// Data loaded — persisted settings
SpanPanelVariant = 'spanPanelVariant',
ColorByField = 'colorByField',
PreviewFieldsCount = 'previewFieldsCount',
EntryPreferOldView = 'entryPreferOldView',
// View switched
From = 'from',
To = 'to',
DwellMs = 'dwellMs',
// Toggles / tabs
Expanded = 'expanded',
Open = 'open',
Tab = 'tab',
// Span panel tab changed
SpanId = 'spanId',
}
export type TraceDetailView = 'v2' | 'v3';

View File

@@ -0,0 +1,88 @@
import { act, renderHook } from '@testing-library/react';
import { TraceDetailEvents } from '../../events';
import { useTraceDetailLogEvent } from '../useTraceDetailLogEvent';
const logEventMock = jest.fn();
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: (...args: unknown[]): void => logEventMock(...args),
}));
describe('useTraceDetailLogEvent', () => {
beforeEach(() => {
logEventMock.mockClear();
});
it('injects view and traceId on every event', () => {
const { result } = renderHook(() =>
useTraceDetailLogEvent('v3', 'trace-123'),
);
act(() => {
result.current(TraceDetailEvents.DataLoaded, { totalSpansCount: 42 });
});
expect(logEventMock).toHaveBeenCalledTimes(1);
expect(logEventMock).toHaveBeenCalledWith(TraceDetailEvents.DataLoaded, {
view: 'v3',
traceId: 'trace-123',
totalSpansCount: 42,
});
});
it('injects view and traceId even when no attributes are passed', () => {
const { result } = renderHook(() =>
useTraceDetailLogEvent('v2', 'trace-456'),
);
act(() => {
result.current(TraceDetailEvents.ViewSwitched);
});
expect(logEventMock).toHaveBeenCalledWith(TraceDetailEvents.ViewSwitched, {
view: 'v2',
traceId: 'trace-456',
});
});
it('keeps a stable callback identity and emits the latest traceId', () => {
const { result, rerender } = renderHook(
({ traceId }) => useTraceDetailLogEvent('v3', traceId),
{ initialProps: { traceId: 'trace-1' } },
);
const firstIdentity = result.current;
rerender({ traceId: 'trace-2' });
expect(result.current).toBe(firstIdentity);
act(() => {
result.current(TraceDetailEvents.SpanPanelTabChanged, { spanId: 's1' });
});
expect(logEventMock).toHaveBeenCalledWith(
TraceDetailEvents.SpanPanelTabChanged,
{
view: 'v3',
traceId: 'trace-2',
spanId: 's1',
},
);
});
it('never throws if logEvent throws (analytics must not break the UI)', () => {
logEventMock.mockImplementationOnce(() => {
throw new Error('network down');
});
const { result } = renderHook(() =>
useTraceDetailLogEvent('v3', 'trace-123'),
);
expect(() => {
act(() => {
result.current(TraceDetailEvents.DataLoaded);
});
}).not.toThrow();
});
});

View File

@@ -0,0 +1,39 @@
import { useCallback, useRef } from 'react';
import logEvent from 'api/common/logEvent';
import {
TraceDetailEventKeys,
TraceDetailEvents,
TraceDetailView,
} from '../events';
export type TraceDetailLogEvent = (
event: TraceDetailEvents,
attributes?: Record<string, unknown>,
) => void;
export function useTraceDetailLogEvent(
view: TraceDetailView,
traceId: string,
): TraceDetailLogEvent {
const contextRef = useRef({ view, traceId });
contextRef.current = { view, traceId };
return useCallback(
(
event: TraceDetailEvents,
attributes: Record<string, unknown> = {},
): void => {
try {
void logEvent(event, {
[TraceDetailEventKeys.View]: contextRef.current.view,
[TraceDetailEventKeys.TraceId]: contextRef.current.traceId,
...attributes,
});
} catch {
// No-op. Logging must never throw into the UI.
}
},
[],
);
}

View File

@@ -20,7 +20,10 @@ import {
} from 'types/api/trace/getTraceV3';
import { COLOR_BY_FIELDS } from './constants';
import { TraceDetailEventKeys, TraceDetailEvents } from './events';
import { useTraceDetailLogEvent } from './hooks/useTraceDetailLogEvent';
import TraceStoreSync from './stores/TraceStoreSync';
import { useTraceStore } from './stores/traceStore';
import { AGGREGATIONS } from './utils/aggregations';
import { SpanDetailVariant } from './SpanDetailsPanel/constants';
import SpanDetailsPanel from './SpanDetailsPanel/SpanDetailsPanel';
@@ -56,6 +59,14 @@ function TraceDetailsV3(): JSX.Element {
const selectedSpanId = urlQuery.get('spanId') || undefined;
const { safeNavigate } = useSafeNavigate();
const logTraceEvent = useTraceDetailLogEvent('v3', traceId || '');
// Tracks which traceId the load event already fired for, so navigating
// between traces (the route component stays mounted) re-fires it once each.
const dataLoadedFiredForRef = useRef('');
const colorByField = useTraceStore((s) => s.colorByField);
const previewFieldsCount = useTraceStore((s) => s.previewFields.length);
const userPrefsReady = useTraceStore((s) => s.userPreferences !== null);
const handleSpanDetailsClose = useCallback((): void => {
urlQuery.delete('spanId');
safeNavigate({ search: urlQuery.toString() });
@@ -154,6 +165,46 @@ function TraceDetailsV3(): JSX.Element {
allSpansRef.current = allSpans;
}, [allSpans]);
useEffect(() => {
if (
!traceId ||
dataLoadedFiredForRef.current === traceId ||
!userPrefsReady
) {
return;
}
const payload = traceData?.payload;
if (!payload?.spans?.length) {
return;
}
dataLoadedFiredForRef.current = traceId;
const numServices = new Set(payload.spans.map((s) => s['service.name'])).size;
logTraceEvent(TraceDetailEvents.DataLoaded, {
[TraceDetailEventKeys.TotalSpansCount]: totalSpansCount,
[TraceDetailEventKeys.NumServices]: numServices,
[TraceDetailEventKeys.TraceDurationMs]:
payload.endTimestampMillis - payload.startTimestampMillis,
[TraceDetailEventKeys.HadErrors]: (payload.totalErrorSpansCount || 0) > 0,
[TraceDetailEventKeys.FlamegraphSampled]:
totalSpansCount > FLAMEGRAPH_SPAN_LIMIT,
[TraceDetailEventKeys.SpanPanelVariant]:
getLocalStorageKey(LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION) ||
SpanDetailVariant.DOCKED_RIGHT,
[TraceDetailEventKeys.ColorByField]: colorByField.name,
[TraceDetailEventKeys.PreviewFieldsCount]: previewFieldsCount,
[TraceDetailEventKeys.EntryPreferOldView]:
getLocalStorageKey(LOCALSTORAGE.TRACE_DETAILS_PREFER_OLD_VIEW) === 'true',
});
}, [
traceId,
userPrefsReady,
traceData,
totalSpansCount,
colorByField,
previewFieldsCount,
logTraceEvent,
]);
// Frontend mode: expand all parents by default when full data arrives
useEffect(() => {
if (isFullDataLoaded && allSpans.length > 0) {
@@ -233,6 +284,12 @@ function TraceDetailsV3(): JSX.Element {
const [activeKeys, setActiveKeys] = useState<string[]>(['flame', 'waterfall']);
const handleCollapseChange = (key: string): void => {
logTraceEvent(
key === 'flame'
? TraceDetailEvents.FlameGraphToggled
: TraceDetailEvents.WaterfallToggled,
{ [TraceDetailEventKeys.Expanded]: !activeKeys.includes(key) },
);
setActiveKeys((prev) =>
prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key],
);

File diff suppressed because one or more lines are too long

View File

@@ -24,12 +24,13 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -24,12 +24,13 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
'('=1
')'=2
'['=3

View File

@@ -1,4 +1,4 @@
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.2
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {
ATN,
@@ -38,12 +38,13 @@ export default class FilterQueryLexer extends Lexer {
public static readonly HAS = 24;
public static readonly HASANY = 25;
public static readonly HASALL = 26;
public static readonly BOOL = 27;
public static readonly NUMBER = 28;
public static readonly QUOTED_TEXT = 29;
public static readonly KEY = 30;
public static readonly WS = 31;
public static readonly FREETEXT = 32;
public static readonly SEARCH = 27;
public static readonly BOOL = 28;
public static readonly NUMBER = 29;
public static readonly QUOTED_TEXT = 30;
public static readonly KEY = 31;
public static readonly WS = 32;
public static readonly FREETEXT = 33;
public static readonly EOF = Token.EOF;
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
@@ -68,8 +69,9 @@ export default class FilterQueryLexer extends Lexer {
"AND", "OR",
"HASTOKEN",
"HAS", "HASANY",
"HASALL", "BOOL",
"NUMBER", "QUOTED_TEXT",
"HASALL", "SEARCH",
"BOOL", "NUMBER",
"QUOTED_TEXT",
"KEY", "WS",
"FREETEXT" ];
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
@@ -78,8 +80,8 @@ export default class FilterQueryLexer extends Lexer {
"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",
"BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS",
"KEY", "WS", "DIGIT", "FREETEXT",
"SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
];
@@ -100,119 +102,122 @@ export default class FilterQueryLexer extends Lexer {
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,32,320,6,-1,2,0,
public static readonly _serializedATN: number[] = [4,0,33,329,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,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,
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];
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,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];
private static __ATN: ATN;
public static get _ATN(): ATN {

View File

@@ -1,25 +1,26 @@
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.2
import {ParseTreeListener} from "antlr4";
import { QueryContext } from "./FilterQueryParser";
import { ExpressionContext } from "./FilterQueryParser";
import { OrExpressionContext } from "./FilterQueryParser";
import { AndExpressionContext } from "./FilterQueryParser";
import { UnaryExpressionContext } from "./FilterQueryParser";
import { PrimaryContext } from "./FilterQueryParser";
import { ComparisonContext } from "./FilterQueryParser";
import { InClauseContext } from "./FilterQueryParser";
import { NotInClauseContext } from "./FilterQueryParser";
import { ValueListContext } from "./FilterQueryParser";
import { FullTextContext } from "./FilterQueryParser";
import { FunctionCallContext } from "./FilterQueryParser";
import { FunctionParamListContext } from "./FilterQueryParser";
import { FunctionParamContext } from "./FilterQueryParser";
import { ArrayContext } from "./FilterQueryParser";
import { ValueContext } from "./FilterQueryParser";
import { KeyContext } from "./FilterQueryParser";
import { QueryContext } from "./FilterQueryParser.js";
import { ExpressionContext } from "./FilterQueryParser.js";
import { OrExpressionContext } from "./FilterQueryParser.js";
import { AndExpressionContext } from "./FilterQueryParser.js";
import { UnaryExpressionContext } from "./FilterQueryParser.js";
import { PrimaryContext } from "./FilterQueryParser.js";
import { ComparisonContext } from "./FilterQueryParser.js";
import { InClauseContext } from "./FilterQueryParser.js";
import { NotInClauseContext } from "./FilterQueryParser.js";
import { ValueListContext } from "./FilterQueryParser.js";
import { FullTextContext } from "./FilterQueryParser.js";
import { FunctionCallContext } from "./FilterQueryParser.js";
import { SearchCallContext } from "./FilterQueryParser.js";
import { FunctionParamListContext } from "./FilterQueryParser.js";
import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
import { ValueContext } from "./FilterQueryParser.js";
import { KeyContext } from "./FilterQueryParser.js";
/**
@@ -147,6 +148,16 @@ export default class FilterQueryListener extends ParseTreeListener {
* @param ctx the parse tree
*/
exitFunctionCall?: (ctx: FunctionCallContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.searchCall`.
* @param ctx the parse tree
*/
enterSearchCall?: (ctx: SearchCallContext) => void;
/**
* Exit a parse tree produced by `FilterQueryParser.searchCall`.
* @param ctx the parse tree
*/
exitSearchCall?: (ctx: SearchCallContext) => void;
/**
* Enter a parse tree produced by `FilterQueryParser.functionParamList`.
* @param ctx the parse tree

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,26 @@
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// Generated from FilterQuery.g4 by ANTLR 4.13.2
import {ParseTreeVisitor} from 'antlr4';
import { QueryContext } from "./FilterQueryParser";
import { ExpressionContext } from "./FilterQueryParser";
import { OrExpressionContext } from "./FilterQueryParser";
import { AndExpressionContext } from "./FilterQueryParser";
import { UnaryExpressionContext } from "./FilterQueryParser";
import { PrimaryContext } from "./FilterQueryParser";
import { ComparisonContext } from "./FilterQueryParser";
import { InClauseContext } from "./FilterQueryParser";
import { NotInClauseContext } from "./FilterQueryParser";
import { ValueListContext } from "./FilterQueryParser";
import { FullTextContext } from "./FilterQueryParser";
import { FunctionCallContext } from "./FilterQueryParser";
import { FunctionParamListContext } from "./FilterQueryParser";
import { FunctionParamContext } from "./FilterQueryParser";
import { ArrayContext } from "./FilterQueryParser";
import { ValueContext } from "./FilterQueryParser";
import { KeyContext } from "./FilterQueryParser";
import { QueryContext } from "./FilterQueryParser.js";
import { ExpressionContext } from "./FilterQueryParser.js";
import { OrExpressionContext } from "./FilterQueryParser.js";
import { AndExpressionContext } from "./FilterQueryParser.js";
import { UnaryExpressionContext } from "./FilterQueryParser.js";
import { PrimaryContext } from "./FilterQueryParser.js";
import { ComparisonContext } from "./FilterQueryParser.js";
import { InClauseContext } from "./FilterQueryParser.js";
import { NotInClauseContext } from "./FilterQueryParser.js";
import { ValueListContext } from "./FilterQueryParser.js";
import { FullTextContext } from "./FilterQueryParser.js";
import { FunctionCallContext } from "./FilterQueryParser.js";
import { SearchCallContext } from "./FilterQueryParser.js";
import { FunctionParamListContext } from "./FilterQueryParser.js";
import { FunctionParamContext } from "./FilterQueryParser.js";
import { ArrayContext } from "./FilterQueryParser.js";
import { ValueContext } from "./FilterQueryParser.js";
import { KeyContext } from "./FilterQueryParser.js";
/**
@@ -102,6 +103,12 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
* @return the visitor result
*/
visitFunctionCall?: (ctx: FunctionCallContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.searchCall`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSearchCall?: (ctx: SearchCallContext) => Result;
/**
* Visit a parse tree produced by `FilterQueryParser.functionParamList`.
* @param ctx the parse tree

View File

@@ -4,6 +4,7 @@ import { ChevronDown, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import logEvent from 'api/common/logEvent';
import { JsonView } from 'periscope/components/JsonView';
import { PrettyView } from 'periscope/components/PrettyView';
import { PrettyViewProps } from 'periscope/components/PrettyView';
@@ -12,6 +13,8 @@ import './DataViewer.styles.scss';
type ViewMode = 'pretty' | 'json';
const VIEW_MODE_CHANGED_EVENT = 'Data Viewer: View mode changed';
const VIEW_MODE_OPTIONS: { label: string; value: ViewMode }[] = [
{ label: 'Pretty', value: 'pretty' },
{ label: 'JSON', value: 'json' },
@@ -34,6 +37,20 @@ function DataViewer({
const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]);
const handleViewModeChange = (value: string): void => {
const next = value as ViewMode;
setViewMode(next);
try {
logEvent(VIEW_MODE_CHANGED_EVENT, {
viewMode: next,
path: window.location.pathname,
drawerKey,
});
} catch {
// No op
}
};
const handleCopy = (): void => {
const text = JSON.stringify(data, null, 2);
setCopy(text);
@@ -56,7 +73,7 @@ function DataViewer({
{
type: 'radio-group',
value: viewMode,
onChange: (value): void => setViewMode(value as ViewMode),
onChange: handleViewModeChange,
children: VIEW_MODE_OPTIONS.map((opt) => ({
type: 'radio',
key: opt.value,

View File

@@ -32,11 +32,12 @@ unaryExpression
;
// Primary constructs: grouped expressions, a comparison (key op value),
// a function call, or a full-text string
// a function call, a search call, or a full-text string
primary
: LPAREN orExpression RPAREN
| comparison
| functionCall
| searchCall
| fullText
| key
| value
@@ -110,6 +111,14 @@ functionCall
: (HASTOKEN | HAS | HASANY | HASALL) LPAREN functionParamList RPAREN
;
/*
* Search call: search() function
* First-class rule so search-specific semantics can be extended independently.
*/
searchCall
: SEARCH LPAREN functionParamList RPAREN
;
// Function parameters can be keys, single scalar values, or arrays
functionParamList
: functionParam ( COMMA functionParam )*
@@ -184,6 +193,7 @@ 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

View File

@@ -173,6 +173,8 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface
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 {
@@ -364,6 +366,16 @@ func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) interfa
return nil
}
// VisitSearchCall visits search() calls.
func (r *WhereClauseRewriter) VisitSearchCall(ctx *parser.SearchCallContext) interface{} {
r.rewritten.WriteString("search(")
if ctx.FunctionParamList() != nil {
ctx.FunctionParamList().Accept(r)
}
r.rewritten.WriteString(")")
return nil
}
// VisitFunctionCall visits function calls.
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) interface{} {
// Write function name

View File

@@ -392,7 +392,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
Logger: m.logger,
FieldMapper: m.fieldMapper,
ConditionBuilder: m.condBuilder,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FieldKeys: keys,
StartNs: querybuilder.ToNanoSecs(uint64(startMillis)),
EndNs: querybuilder.ToNanoSecs(uint64(endMillis)),

View File

@@ -953,7 +953,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
Logger: m.logger,
FieldMapper: m.fieldMapper,
ConditionBuilder: m.condBuilder,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FieldKeys: keys,
StartNs: querybuilder.ToNanoSecs(uint64(startMillis)),
EndNs: querybuilder.ToNanoSecs(uint64(endMillis)),

View File

@@ -501,7 +501,7 @@ func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, st
FieldMapper: s.fieldMapper,
ConditionBuilder: s.conditionBuilder,
FieldKeys: fieldKeys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels", FieldContext: telemetrytypes.FieldContextAttribute},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels", FieldContext: telemetrytypes.FieldContextAttribute},
}
opts.StartNs = querybuilder.ToNanoSecs(uint64(startMillis))

View File

@@ -74,7 +74,7 @@ func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary
events, status_message, status_code_string, kind_string, parent_span_id,
flags, is_remote, trace_state, status_code,
db_name, db_operation, http_method, http_url, http_host,
external_http_method, external_http_url, response_status_code
external_http_method, external_http_url, response_status_code, links as references
FROM %s.%s
WHERE trace_id=? AND ts_bucket_start>=? AND ts_bucket_start<=?
ORDER BY timestamp ASC, name ASC`,
@@ -130,7 +130,7 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
"events", "status_message", "status_code_string", "kind_string", "parent_span_id",
"flags", "is_remote", "trace_state", "status_code",
"db_name", "db_operation", "http_method", "http_url", "http_host",
"external_http_method", "external_http_url", "response_status_code",
"external_http_method", "external_http_url", "response_status_code", "links as references",
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
ids := make([]any, len(spanIDs))

File diff suppressed because one or more lines are too long

View File

@@ -24,12 +24,13 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -24,12 +24,13 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
'('=1
')'=2
'['=3

View File

@@ -93,6 +93,12 @@ 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) {}

View File

@@ -56,6 +56,10 @@ 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)
}

View File

@@ -4,10 +4,9 @@ package parser
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
"sync"
"unicode"
"github.com/antlr4-go/antlr/v4"
)
// Suppress unused import error
@@ -51,170 +50,174 @@ 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", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
"HASALL", "SEARCH", "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", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 32, 320, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 0, 33, 329, 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, 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,
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,
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,
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,
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,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
@@ -281,10 +284,11 @@ const (
FilterQueryLexerHAS = 24
FilterQueryLexerHASANY = 25
FilterQueryLexerHASALL = 26
FilterQueryLexerBOOL = 27
FilterQueryLexerNUMBER = 28
FilterQueryLexerQUOTED_TEXT = 29
FilterQueryLexerKEY = 30
FilterQueryLexerWS = 31
FilterQueryLexerFREETEXT = 32
FilterQueryLexerSEARCH = 27
FilterQueryLexerBOOL = 28
FilterQueryLexerNUMBER = 29
FilterQueryLexerQUOTED_TEXT = 30
FilterQueryLexerKEY = 31
FilterQueryLexerWS = 32
FilterQueryLexerFREETEXT = 33
)

View File

@@ -44,6 +44,9 @@ 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)
@@ -95,6 +98,9 @@ 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

View File

@@ -44,6 +44,9 @@ 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{}

View File

@@ -221,7 +221,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
FieldMapper: v.fieldMapper,
ConditionBuilder: v.conditionBuilder,
BodyJSONEnabled: bodyJSONEnabled,
FullTextColumn: v.fullTextColumn,
FreeTextColumn: v.fullTextColumn,
JsonKeyToKey: v.jsonKeyToKey,
StartNs: v.startNs,
EndNs: v.endNs,

View File

@@ -8,6 +8,20 @@ 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"
// FullTextSearchDefaultWarning is emitted when a search() function call is used.
FullTextSearchDefaultWarning = "Full text searches across all fields and will be slow and expensive. Consider using specific field to search e.g. <context>.<field_key>:<type>"
// FTSInternalKey is the sentinel Name on TelemetryFieldKey instances that represent
// wildcard map searches (all attribute/resource keys+values). The unconventional value
// prevents collision with any real field name a user could type.
FTSInternalKey = "_X_INTERNAL_FTS_KEY"
// SearchFunctionName is the grammar function name for full-text search.
SearchFunctionName = "search"
// FTSMaxWindowNs is the maximum allowed time range for a search() query (6 hours).
FTSMaxWindowNs = uint64(6 * 60 * 60 * 1_000_000_000)
)
var (

View File

@@ -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()",
"HASTOKEN": "hasToken()", "SEARCH": "search()",
// literals / identifiers
"NUMBER": "number",

View File

@@ -191,6 +191,9 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
} else if ctx.FunctionCall() != nil {
// Handle function calls if needed
return nil
} else if ctx.SearchCall() != nil {
// Handle search calls if needed
return nil
} else if ctx.FullText() != nil {
// Handle full text search if needed
return nil

View File

@@ -34,11 +34,12 @@ type filterExpressionVisitor struct {
errors []string
mainErrorURL string
builder *sqlbuilder.SelectBuilder
fullTextColumn *telemetrytypes.TelemetryFieldKey
freeTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
bodyJSONEnabled bool
skipResourceFilter bool
skipFullTextFilter bool
skipFreeTextFilter bool
skipFullTextSearch bool
skipFunctionCalls bool
ignoreNotFoundKeys bool
variables map[string]qbtypes.VariableItem
@@ -46,6 +47,7 @@ type filterExpressionVisitor struct {
keysWithWarnings map[string]bool
startNs uint64
endNs uint64
ftsFieldKeys []*telemetrytypes.TelemetryFieldKey
}
type FilterExprVisitorOpts struct {
@@ -55,16 +57,20 @@ type FilterExprVisitorOpts struct {
ConditionBuilder qbtypes.ConditionBuilder
FieldKeys map[string][]*telemetrytypes.TelemetryFieldKey
Builder *sqlbuilder.SelectBuilder
FullTextColumn *telemetrytypes.TelemetryFieldKey
FreeTextColumn *telemetrytypes.TelemetryFieldKey
JsonKeyToKey qbtypes.JsonKeyToFieldFunc
BodyJSONEnabled bool
SkipResourceFilter bool
SkipFullTextFilter bool
SkipFreeTextFilter bool
SkipFullTextSearch bool
SkipFunctionCalls bool
IgnoreNotFoundKeys bool
Variables map[string]qbtypes.VariableItem
StartNs uint64
EndNs uint64
// FTSFieldKeys enables search() for this query context. nil disables search()
// (traces, metrics, and non-log callers leave this nil).
FTSFieldKeys []*telemetrytypes.TelemetryFieldKey
}
// newFilterExpressionVisitor creates a new filterExpressionVisitor.
@@ -76,17 +82,19 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
conditionBuilder: opts.ConditionBuilder,
fieldKeys: opts.FieldKeys,
builder: opts.Builder,
fullTextColumn: opts.FullTextColumn,
freeTextColumn: opts.FreeTextColumn,
jsonKeyToKey: opts.JsonKeyToKey,
bodyJSONEnabled: opts.BodyJSONEnabled,
skipResourceFilter: opts.SkipResourceFilter,
skipFullTextFilter: opts.SkipFullTextFilter,
skipFreeTextFilter: opts.SkipFreeTextFilter,
skipFullTextSearch: opts.SkipFullTextSearch,
skipFunctionCalls: opts.SkipFunctionCalls,
ignoreNotFoundKeys: opts.IgnoreNotFoundKeys,
variables: opts.Variables,
keysWithWarnings: make(map[string]bool),
startNs: opts.StartNs,
endNs: opts.EndNs,
ftsFieldKeys: opts.FTSFieldKeys,
}
}
@@ -215,6 +223,8 @@ func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any {
return v.VisitFullText(t)
case *grammar.FunctionCallContext:
return v.VisitFunctionCall(t)
case *grammar.SearchCallContext:
return v.VisitSearchCall(t)
case *grammar.FunctionParamListContext:
return v.VisitFunctionParamList(t)
case *grammar.FunctionParamContext:
@@ -327,20 +337,23 @@ 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())
}
// Handle standalone key/value as a full text search term
if ctx.GetChildCount() == 1 {
if v.skipFullTextFilter {
if v.skipFreeTextFilter {
return SkipConditionLiteral
}
if v.fullTextColumn == nil {
v.errors = append(v.errors, "full text search is not supported")
if v.freeTextColumn == nil {
v.errors = append(v.errors, "free text search is not supported")
return ErrorConditionLiteral
}
child := ctx.GetChild(0)
var searchText string
if keyCtx, ok := child.(*grammar.KeyContext); ok {
@@ -360,12 +373,13 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
return ErrorConditionLiteral
}
}
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder)
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.freeTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ErrorConditionLiteral
}
if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
if v.bodyJSONEnabled && v.freeTextColumn.Name == "body" {
v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
}
@@ -699,7 +713,7 @@ func (v *filterExpressionVisitor) VisitValueList(ctx *grammar.ValueListContext)
// VisitFullText handles standalone quoted strings for full-text search.
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
if v.skipFullTextFilter {
if v.skipFreeTextFilter {
// A skipped FT term must be treated as TrueConditionLiteral, not "".
// Returning "" would silently drop this branch from an OR, incorrectly
// excluding rows that could match the FT condition on the real table.
@@ -714,24 +728,79 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
text = ctx.FREETEXT().GetText()
}
if v.fullTextColumn == nil {
v.errors = append(v.errors, "full text search is not supported")
if v.freeTextColumn == nil {
v.errors = append(v.errors, "free text search is not supported")
return ErrorConditionLiteral
}
cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.freeTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ErrorConditionLiteral
}
if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
if v.bodyJSONEnabled && v.freeTextColumn.Name == "body" {
v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
}
return cond
}
// VisitFunctionCall handles function calls like has(), hasAny(), etc.
// VisitSearchCall handles the search() function call.
func (v *filterExpressionVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
if v.skipFunctionCalls || v.skipFullTextSearch {
return SkipConditionLiteral
}
// ftsFieldKeys == nil means search() is not enabled for this signal/query type.
// Only log statement builders set FTSFieldKeys; traces/metrics leave it nil.
if len(v.ftsFieldKeys) == 0 {
v.errors = append(v.errors, "search() is only supported for log queries")
return ErrorConditionLiteral
}
paramCtxs := ctx.FunctionParamList().AllFunctionParam()
if len(paramCtxs) < 1 {
v.errors = append(v.errors, "search() requires exactly one quoted string parameter, e.g. search('error')")
return ErrorConditionLiteral
}
if len(paramCtxs) > 1 {
v.errors = append(v.errors, fmt.Sprintf("search() accepts exactly one parameter but got %d", len(paramCtxs)))
return ErrorConditionLiteral
}
paramCtx := paramCtxs[0]
var searchText string
if paramCtx.Value() != nil {
raw := v.Visit(paramCtx.Value())
searchText = fmt.Sprintf("%v", raw)
} else {
v.errors = append(v.errors, "search() parameter must be a quoted string, e.g. search('error')")
return ErrorConditionLiteral
}
if v.endNs > 0 && v.startNs > 0 && (v.endNs-v.startNs) > FTSMaxWindowNs {
v.errors = append(v.errors, "search() is restricted to a maximum of 6-hour time window")
return ErrorConditionLiteral
}
formattedText := FormatFullTextSearch(searchText)
var ftsConds []string
for _, key := range v.ftsFieldKeys {
cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, qbtypes.FilterOperatorRegexp, formattedText, v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("search() could not build condition for field %q: %s", key.Text(), err.Error()))
return ErrorConditionLiteral
}
ftsConds = append(ftsConds, cond)
}
if len(ftsConds) == 0 {
v.errors = append(v.errors, "search() failed: no searchable fields could be queried")
return ErrorConditionLiteral
}
v.warnings = append(v.warnings, FullTextSearchDefaultWarning)
return v.builder.Or(ftsConds...)
}
// VisitFunctionCall handles function calls like has(), hasAny(), hasAll(), hasToken().
func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any {
if v.skipFunctionCalls {
return SkipConditionLiteral

View File

@@ -798,7 +798,7 @@ func visitComparisonOpts(t *testing.T) (rsbOpts, sbOpts FilterExprVisitorOpts) {
ConditionBuilder: &resourceConditionBuilder{},
Variables: allVariable,
SkipResourceFilter: false,
SkipFullTextFilter: true,
SkipFreeTextFilter: true,
SkipFunctionCalls: true,
IgnoreNotFoundKeys: true,
}
@@ -808,10 +808,10 @@ func visitComparisonOpts(t *testing.T) (rsbOpts, sbOpts FilterExprVisitorOpts) {
ConditionBuilder: &conditionBuilder{},
Variables: allVariable,
SkipResourceFilter: true,
SkipFullTextFilter: false,
SkipFreeTextFilter: false,
SkipFunctionCalls: false,
IgnoreNotFoundKeys: false,
FullTextColumn: bodyCol,
FreeTextColumn: bodyCol,
}
return
}
@@ -1272,110 +1272,111 @@ func TestVisitComparison_Parens(t *testing.T) {
}
}
// TestVisitComparison_FullText covers full-text (bare string literal) expressions.
// TestVisitComparison_FreeTextSearch covers Free Text Search — bare/quoted string literals
// that route through freeTextColumn (body only). No search() involved.
// rsbOpts has SkipFullTextFilter=true → TrueConditionLiteral.
// sbOpts has SkipFullTextFilter=false, FullTextColumn=bodyCol → "body_cond".
func TestVisitComparison_FullText(t *testing.T) {
// sbOpts has SkipFullTextFilter=false, FreeTextColumn=bodyCol → "body_cond".
func TestVisitComparison_FreeTextSearch(t *testing.T) {
rsbOpts, sbOpts := visitComparisonOpts(t)
tests := []visitComparisonCase{
{
name: "standalone full-text term",
name: "standalone free-text term",
expr: "'hello'",
wantRSB: "",
wantSB: "WHERE body_cond",
},
{
// RSB: FT→true, a→true; AND propagates true.
name: "full-text AND attribute",
name: "free-text AND attribute",
expr: "'hello' AND a = 'a'",
wantRSB: "",
wantSB: "WHERE (body_cond AND a_cond)",
},
{
// RSB: FT→true stripped; x_cond survives.
name: "full-text AND resource",
name: "free-text AND resource",
expr: "'hello' AND x = 'x'",
wantRSB: "WHERE x_cond",
wantSB: "WHERE body_cond",
},
{
// RSB: NOT(FT→SkipConditionLiteral)→SkipConditionLiteral. SB: structural NOT applied.
name: "NOT full-text term",
name: "NOT free-text term",
expr: "NOT 'hello'",
wantRSB: "",
wantSB: "WHERE NOT (body_cond)",
},
{
// RSB: FT→true short-circuits OR.
name: "full-text OR resource",
name: "free-text OR resource",
expr: "'hello' OR x = 'x'",
wantRSB: "",
wantSB: "WHERE (body_cond OR x_cond)",
},
{
name: "full-text OR attribute",
name: "free-text OR attribute",
expr: "'hello' OR a = 'a'",
wantRSB: "",
wantSB: "WHERE (body_cond OR a_cond)",
},
{
name: "two full-text terms ANDed",
name: "two free-text terms ANDed",
expr: "'hello' AND 'world'",
wantRSB: "",
wantSB: "WHERE (body_cond AND body_cond)",
},
{
name: "two full-text terms ORed",
name: "two free-text terms ORed",
expr: "'hello' OR 'world'",
wantRSB: "",
wantSB: "WHERE (body_cond OR body_cond)",
},
{
name: "full-text in parentheses",
name: "free-text in parentheses",
expr: "('hello')",
wantRSB: "",
wantSB: "WHERE (body_cond)",
},
{
name: "two full-text AND attribute",
name: "two free-text AND attribute",
expr: "'hello' AND 'world' AND a = 'a'",
wantRSB: "",
wantSB: "WHERE (body_cond AND body_cond AND a_cond)",
},
{
name: "full-text OR attr OR resource all types",
name: "free-text OR attr OR resource all types",
expr: "'hello' OR a = 'a' OR x = 'x'",
wantRSB: "",
wantSB: "WHERE (body_cond OR a_cond OR x_cond)",
},
{
name: "NOT of paren full-text AND attr",
name: "NOT of paren free-text AND attr",
expr: "NOT ('hello' AND a = 'a')",
wantRSB: "",
wantSB: "WHERE NOT (((body_cond AND a_cond)))",
},
{
// RSB: NOT(FT→SkipConditionLiteral)→SkipConditionLiteral stripped from AND; x_cond survives.
name: "NOT full-text AND resource",
name: "NOT free-text AND resource",
expr: "NOT 'hello' AND x = 'x'",
wantRSB: "WHERE x_cond",
wantSB: "WHERE NOT (body_cond)",
},
{
name: "NOT full-text OR resource",
name: "NOT free-text OR resource",
expr: "NOT 'hello' OR x = 'x'",
wantRSB: "",
wantSB: "WHERE (NOT (body_cond) OR x_cond)",
},
{
// RSB: FT→true stripped; x_cond survives.
name: "full-text AND BETWEEN",
name: "free-text AND BETWEEN",
expr: "'hello' AND x BETWEEN 1 AND 3",
wantRSB: "WHERE x_cond",
wantSB: "WHERE body_cond",
},
{
name: "full-text AND EXISTS",
name: "free-text AND EXISTS",
expr: "'hello' AND x EXISTS",
wantRSB: "WHERE x_cond",
wantSB: "WHERE body_cond",
@@ -1383,21 +1384,21 @@ func TestVisitComparison_FullText(t *testing.T) {
{
// RSB: FT→true and allVariable→true; AND propagates true.
// SB: allVariable→TrueConditionLiteral stripped; body_cond survives.
name: "full-text AND allVariable",
name: "free-text AND allVariable",
expr: "'hello' AND x IN $service",
wantRSB: "",
wantSB: "WHERE body_cond",
},
{
// SB: body_cond added first; then allVariable→TrueConditionLiteral short-circuits OR.
name: "full-text OR allVariable",
name: "free-text OR allVariable",
expr: "'hello' OR x IN $service",
wantRSB: "",
wantSB: "",
},
{
// SB: body_cond
name: "full-text with sentinel value",
name: "free-text with sentinel value",
expr: SkipConditionLiteral,
wantRSB: "",
wantSB: "WHERE body_cond",
@@ -1684,6 +1685,71 @@ func TestVisitComparison_UnknownKeys(t *testing.T) {
}
}
// TestVisitComparison_FullTextSearch covers Full Text Search — the explicit search()
// function that fans out across all ftsFieldKeys. FTSFieldKeys must be set for
// search() to be enabled; invalid param counts must error.
func TestVisitComparison_FullTextSearch(t *testing.T) {
bodyKey := &telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
ftsOpts := FilterExprVisitorOpts{
Context: t.Context(),
FieldKeys: visitTestKeys,
ConditionBuilder: &conditionBuilder{},
SkipResourceFilter: false,
SkipFreeTextFilter: false,
SkipFunctionCalls: false,
IgnoreNotFoundKeys: false,
FTSFieldKeys: []*telemetrytypes.TelemetryFieldKey{bodyKey},
}
tests := []struct {
name string
expr string
opts FilterExprVisitorOpts
wantErr bool
}{
{
name: "search with quoted string - valid",
expr: "search('error')",
opts: ftsOpts,
wantErr: false,
},
{
name: "search with unquoted word - invalid, quotes required",
expr: "search(error)",
opts: ftsOpts,
wantErr: true,
},
{
name: "search(\"err1\", \"err2\") - too many params",
expr: `search("err1", "err2")`,
opts: ftsOpts,
wantErr: true,
},
{
name: "search(attributes, \"err\") - too many params",
expr: `search(attributes, "err")`,
opts: ftsOpts,
wantErr: true,
},
{
name: "search() without params - error",
expr: "search()",
opts: ftsOpts,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := PrepareWhereClause(tt.expr, tt.opts)
assert.Equal(t, tt.wantErr, err != nil, "error expectation mismatch: err=%v", err)
})
}
}
// TestVisitComparison_SkippableLiteralValues guards against two distinct collision risks
// involving SkippableConditionLiterals ("true", "__skip__", "__skip_because_of_error__"):.
func TestVisitComparison_SkippableLiteralValues(t *testing.T) {

View File

@@ -550,7 +550,7 @@ func (b *auditQueryStatementBuilder) addFilterCondition(
ConditionBuilder: b.cb,
FieldKeys: keys,
SkipResourceFilter: true,
FullTextColumn: b.fullTextColumn,
FreeTextColumn: b.fullTextColumn,
JsonKeyToKey: b.jsonKeyToKey,
Variables: variables,
StartNs: start,

View File

@@ -16,6 +16,10 @@ import (
"github.com/huandu/go-sqlbuilder"
)
var (
ErrCodeInvalidFTSOperator = errors.MustNewCode("invalid_fts_operator")
)
type conditionBuilder struct {
fm qbtypes.FieldMapper
fl flagger.Flagger
@@ -25,6 +29,74 @@ func NewConditionBuilder(fm qbtypes.FieldMapper, fl flagger.Flagger) *conditionB
return &conditionBuilder{fm: fm, fl: fl}
}
// buildFTSCondition produces the WHERE fragment for a single FTS key by iterating
// over its resolved columns and emitting the right match expression per column type:
// - Map → arrayExists(x -> match(x, ?), mapKeys/mapValues(col))
// - JSON → match(LOWER(toString(col)), LOWER(?)) — only when useJSONBody is true
// - String/LowCardinality → match(LOWER(col), LOWER(?))
//
// Evolution entries (key.Evolutions) are applied first: selectEvolutionsForColumns
// picks the active column set for the query time range, and the ColumnName from each
// entry overrides col.Name so evolved table layouts use the right physical column.
// JSON columns are then skipped when useJSONBody is false.
func buildFTSCondition(
columns []*schema.Column,
key *telemetrytypes.TelemetryFieldKey,
tsStart, tsEnd uint64,
value any,
useJSONBody bool,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
activeColumns := columns
var evolutionEntries []*telemetrytypes.EvolutionEntry
if len(key.Evolutions) > 0 {
var err error
activeColumns, evolutionEntries, err = selectEvolutionsForColumns(columns, key.Evolutions, tsStart, tsEnd)
if err != nil {
return "", err
}
}
var conditions []string
for i, col := range activeColumns {
if !useJSONBody && col.Type.GetType() == schema.ColumnTypeEnumJSON {
continue
}
colName := col.Name
if evolutionEntries != nil && evolutionEntries[i] != nil {
colName = evolutionEntries[i].ColumnName
}
switch col.Type.GetType() {
case schema.ColumnTypeEnumMap:
keysExpr := fmt.Sprintf("mapKeys(%s)", colName)
valsExpr := fmt.Sprintf("mapValues(%s)", colName)
if mc, ok := col.Type.(schema.MapColumnType); ok && mc.ValueType.GetType() != schema.ColumnTypeEnumString {
valsExpr = fmt.Sprintf("arrayMap(x -> toString(x), mapValues(%s))", colName)
}
conditions = append(conditions,
fmt.Sprintf(`arrayExists(x -> match(x, %s), %s)`, sb.Var(value), keysExpr),
fmt.Sprintf(`arrayExists(x -> match(x, %s), %s)`, sb.Var(value), valsExpr),
)
case schema.ColumnTypeEnumJSON:
conditions = append(conditions,
fmt.Sprintf(`match(LOWER(toString(%s)), LOWER(%s))`, colName, sb.Var(value)))
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumLowCardinality:
conditions = append(conditions,
fmt.Sprintf(`match(LOWER(%s), LOWER(%s))`, colName, sb.Var(value)))
}
}
if len(conditions) == 0 {
return "", errors.Newf(errors.TypeInternal, errors.CodeInternal, "no FTS conditions built for columns")
}
if len(conditions) == 1 {
return conditions[0], nil
}
return sb.Or(conditions...), nil
}
func (c *conditionBuilder) conditionFor(
ctx context.Context,
startNs, endNs uint64,
@@ -33,11 +105,22 @@ func (c *conditionBuilder) conditionFor(
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
if key.Name == querybuilder.FTSInternalKey && operator != qbtypes.FilterOperatorRegexp {
return "", errors.NewInternalf(ErrCodeInvalidFTSOperator, "only regexp operator is supported for fts")
}
columns, err := c.fm.ColumnFor(ctx, startNs, endNs, key)
if err != nil {
return "", err
}
// FTS is branched here
if key.Name == querybuilder.FTSInternalKey {
// TODO(Tushar): thread orgID here to evaluate correctly
useJSONBody := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
return buildFTSCondition(columns, key, startNs, endNs, value, useJSONBody, sb)
}
// TODO(Piyush): Update this to support multiple JSON columns based on evolutions
for _, column := range columns {
// TODO(Tushar): thread orgID here to evaluate correctly
@@ -62,7 +145,8 @@ func (c *conditionBuilder) conditionFor(
// Check if this is a body JSON search - either by FieldContext
// TODO(Tushar): thread orgID here to evaluate correctly
if key.FieldContext == telemetrytypes.FieldContextBody && !c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
if key.FieldContext == telemetrytypes.FieldContextBody &&
!c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
fieldExpression, value = GetBodyJSONKey(ctx, key, operator, value)
}
@@ -116,7 +200,6 @@ func (c *conditionBuilder) conditionFor(
return sb.ILike(fieldExpression, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorNotContains:
return sb.NotILike(fieldExpression, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorRegexp:
// Note: Escape $$ to $$$$ to avoid sqlbuilder interpreting materialized $ signs
// Only needed because we are using sprintf instead of sb.Match (not implemented in sqlbuilder)
@@ -282,6 +365,12 @@ func (c *conditionBuilder) ConditionFor(
return "", err
}
// FTS wildcard conditions are self-contained (arrayExists over full map);
// no additional EXISTS wrapper is needed.
if key.Name == querybuilder.FTSInternalKey {
return condition, nil
}
// Skip adding exists filter for intrinsic fields i.e. Table level log context fields
buildExistCondition := operator.AddDefaultExistsFilter()
switch key.FieldContext {

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -125,6 +126,21 @@ var (
Direction: qbtypes.OrderDirectionDesc,
},
}
// defaultFTSFieldKeys is set of TelemetryFieldKey instances that
// search() fans out across. Intrinsic log columns use the normal conditionFor
// path; entries with Name==FTSInternalKey are short-circuited in conditionFor
// to emit arrayExists conditions over mapKeys/mapValues without arrayConcat.
defaultFTSFieldKeys = []*telemetrytypes.TelemetryFieldKey{
{Name: LogsV2SeverityTextColumn, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: LogsV2TraceIDColumn, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: LogsV2SpanIDColumn, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: querybuilder.FTSInternalKey, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextBody, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: querybuilder.FTSInternalKey, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: querybuilder.FTSInternalKey, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber},
{Name: querybuilder.FTSInternalKey, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeBool},
{Name: querybuilder.FTSInternalKey, Signal: telemetrytypes.SignalLogs, FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString},
}
)
func bodyAliasExpression(bodyJSONEnabled bool) string {

View File

@@ -27,7 +27,7 @@ func TestLikeAndILikeWithoutWildcards_Warns(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: DefaultFullTextColumn,
FreeTextColumn: DefaultFullTextColumn,
JsonKeyToKey: GetBodyJSONKey,
}
@@ -66,7 +66,7 @@ func TestLikeAndILikeWithWildcards_NoWarn(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: DefaultFullTextColumn,
FreeTextColumn: DefaultFullTextColumn,
JsonKeyToKey: GetBodyJSONKey,
}

View File

@@ -29,7 +29,7 @@ func TestFilterExprLogsBodyJSON(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "body"},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "body"},
JsonKeyToKey: GetBodyJSONKey,
}

View File

@@ -33,7 +33,7 @@ func TestFilterExprLogs(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: DefaultFullTextColumn,
FreeTextColumn: DefaultFullTextColumn,
JsonKeyToKey: GetBodyJSONKey,
StartNs: uint64(releaseTime.Add(-5 * time.Minute).UnixNano()),
EndNs: uint64(releaseTime.Add(5 * time.Minute).UnixNano()),
@@ -100,7 +100,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} but got '<'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '<'",
},
// Single word searches with spaces
@@ -166,7 +166,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} but got '['",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '['",
},
{
category: "Special characters",
@@ -196,7 +196,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} but got ')'",
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got ')'",
},
{
category: "Special characters",
@@ -618,7 +618,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
},
{
category: "Keyword conflict",
@@ -626,7 +626,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
},
{
category: "Keyword conflict",
@@ -634,7 +634,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF",
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF",
},
{
category: "Keyword conflict",
@@ -642,7 +642,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'like'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'like'",
},
{
category: "Keyword conflict",
@@ -650,7 +650,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
},
{
category: "Keyword conflict",
@@ -658,7 +658,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
},
{
category: "Keyword conflict",
@@ -666,7 +666,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'exists'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'exists'",
},
{
category: "Keyword conflict",
@@ -674,7 +674,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'regexp'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'regexp'",
},
{
category: "Keyword conflict",
@@ -682,7 +682,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'contains'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'contains'",
},
{
category: "Keyword conflict",
@@ -2018,9 +2018,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} 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 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 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"},
@@ -2162,7 +2162,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} but got 'and'",
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
},
{
category: "Operator keywords as keys",
@@ -2170,7 +2170,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} but got 'or'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
},
{
category: "Operator keywords as keys",
@@ -2178,7 +2178,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} but got '='",
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '='",
},
{
category: "Operator keywords as keys",
@@ -2186,7 +2186,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} but got 'between'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
},
{
category: "Operator keywords as keys",
@@ -2194,7 +2194,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} but got 'in'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
},
// Using function keywords as keys
@@ -2458,7 +2458,7 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: DefaultFullTextColumn,
FreeTextColumn: DefaultFullTextColumn,
JsonKeyToKey: GetBodyJSONKey,
}

View File

@@ -27,8 +27,9 @@ type logQueryStatementBuilder struct {
aggExprRewriter qbtypes.AggExprRewriter
fl flagger.Flagger
fullTextColumn *telemetrytypes.TelemetryFieldKey
freeTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
ftsFieldKeys []*telemetrytypes.TelemetryFieldKey
}
var _ qbtypes.StatementBuilder[qbtypes.LogAggregation] = (*logQueryStatementBuilder)(nil)
@@ -39,7 +40,7 @@ func NewLogQueryStatementBuilder(
fieldMapper qbtypes.FieldMapper,
conditionBuilder qbtypes.ConditionBuilder,
aggExprRewriter qbtypes.AggExprRewriter,
fullTextColumn *telemetrytypes.TelemetryFieldKey,
freeTextColumn *telemetrytypes.TelemetryFieldKey,
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
fl flagger.Flagger,
) *logQueryStatementBuilder {
@@ -52,7 +53,7 @@ func NewLogQueryStatementBuilder(
telemetrytypes.SignalLogs,
telemetrytypes.SourceUnspecified,
metadataStore,
fullTextColumn,
freeTextColumn,
jsonKeyToKey,
fl,
)
@@ -65,8 +66,9 @@ func NewLogQueryStatementBuilder(
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
aggExprRewriter: aggExprRewriter,
fl: fl,
fullTextColumn: fullTextColumn,
freeTextColumn: freeTextColumn,
jsonKeyToKey: jsonKeyToKey,
ftsFieldKeys: defaultFTSFieldKeys,
}
}
@@ -79,7 +81,6 @@ func (b *logQueryStatementBuilder) Build(
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
start = querybuilder.ToNanoSecs(start)
end = querybuilder.ToNanoSecs(end)
// TODO(Tushar): thread orgID here to evaluate correctly
@@ -315,7 +316,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, true)
if err != nil {
return nil, err
@@ -419,7 +420,7 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
// Add FROM clause
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, true)
if err != nil {
return nil, err
@@ -576,7 +577,7 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, false)
if err != nil {
return nil, err
@@ -640,6 +641,7 @@ func (b *logQueryStatementBuilder) addFilterCondition(
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
enableFTS bool,
) (querybuilder.PreparedWhereClause, error) {
var preparedWhereClause querybuilder.PreparedWhereClause
@@ -648,22 +650,26 @@ func (b *logQueryStatementBuilder) addFilterCondition(
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
if query.Filter != nil && query.Filter.Expression != "" {
// add filter expression
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, querybuilder.FilterExprVisitorOpts{
opts := querybuilder.FilterExprVisitorOpts{
Context: ctx,
Logger: b.logger,
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
BodyJSONEnabled: bodyJSONEnabled,
FreeTextColumn: b.freeTextColumn,
SkipResourceFilter: true,
FullTextColumn: b.fullTextColumn,
JsonKeyToKey: b.jsonKeyToKey,
Variables: variables,
StartNs: start,
EndNs: end,
})
}
if enableFTS {
opts.FTSFieldKeys = b.ftsFieldKeys
}
// add filter expression
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, opts)
if err != nil {
return preparedWhereClause, err
}

View File

@@ -2,6 +2,7 @@ package telemetrylogs
import (
"context"
"strings"
"testing"
"time"
@@ -385,29 +386,13 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
expected qbtypes.Statement
expectedErr error
}{
{
name: "List with full text search",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{
Expression: "hello",
},
Limit: 10,
},
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"hello", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
{
name: "list query with mat col order by",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{
Expression: "service.name = 'cartservice' hello",
Expression: "service.name = 'cartservice'",
},
Limit: 10,
Order: []qbtypes.OrderBy{
@@ -424,8 +409,8 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "hello", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
@@ -1059,17 +1044,23 @@ func TestStmtBuilderBodyField(t *testing.T) {
}
}
func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
func TestStmtBuilderTextSearch(t *testing.T) {
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
enableUseJSONBody bool
expected qbtypes.Statement
expectedErr error
expectedErr string
// optional per-case time window (ms); zero → use default 1747947419000/1747983448000
startMs uint64
endMs uint64
}{
// ── Free Text Search ──────────────────────────────────────────────────────────
// Bare/quoted tokens route through fullTextColumn (body / body_v2.message only).
// SQL: match(LOWER(<body_col>), LOWER(?))
{
name: "fts",
name: "free_text_search",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
@@ -1077,15 +1068,16 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
Limit: 10,
},
enableUseJSONBody: true,
startMs: 1705309200000,
endMs: 1705316400000,
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE match(LOWER(body_v2.message), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Args: []any{"error", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: []string{querybuilder.BodyFullTextSearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "fts_2",
name: "free_text_search_2",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
@@ -1093,15 +1085,16 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
Limit: 10,
},
enableUseJSONBody: true,
startMs: 1705309200000,
endMs: 1705316400000,
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE match(LOWER(body_v2.message), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Args: []any{"error", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: []string{querybuilder.BodyFullTextSearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "fts_disabled",
name: "free_text_search_json_disabled",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
@@ -1109,11 +1102,80 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
Limit: 10,
},
enableUseJSONBody: false,
startMs: 1705309200000,
endMs: 1705316400000,
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"error", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: nil,
},
expectedErr: nil,
},
// ── Full Text Search ──────────────────────────────────────────────────────────
// search() fans out via ftsFieldKeys across all fields (severity_text, trace_id,
// span_id, body, attributes_*, resources_*). Uses a 2-hour window to stay under
// the 6-hour limit.
{
name: "search_fans_out_to_all_columns",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "search('error')"},
Limit: 10,
},
enableUseJSONBody: false,
startMs: 1705309200000,
endMs: 1705316400000,
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE (match(severity_text, ?) OR match(trace_id, ?) OR match(span_id, ?) OR match(LOWER(body), LOWER(?)) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_string)) OR arrayExists(x -> match(x, ?), mapValues(attributes_string))) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_number)) OR arrayExists(x -> match(x, ?), arrayMap(x -> toString(x), mapValues(attributes_number)))) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_bool)) OR arrayExists(x -> match(x, ?), arrayMap(x -> toString(x), mapValues(attributes_bool)))) OR (arrayExists(x -> match(x, ?), mapKeys(resources_string)) OR arrayExists(x -> match(x, ?), mapValues(resources_string)))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"error", "error", "error", "error", "error", "error", "error", "error", "error", "error", "error", "error", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: []string{querybuilder.FullTextSearchDefaultWarning},
},
},
{
name: "search_not_wraps_condition",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "NOT search('healthcheck')"},
Limit: 10,
},
enableUseJSONBody: false,
startMs: 1705309200000,
endMs: 1705316400000,
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE NOT ((match(severity_text, ?) OR match(trace_id, ?) OR match(span_id, ?) OR match(LOWER(body), LOWER(?)) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_string)) OR arrayExists(x -> match(x, ?), mapValues(attributes_string))) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_number)) OR arrayExists(x -> match(x, ?), arrayMap(x -> toString(x), mapValues(attributes_number)))) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_bool)) OR arrayExists(x -> match(x, ?), arrayMap(x -> toString(x), mapValues(attributes_bool)))) OR (arrayExists(x -> match(x, ?), mapKeys(resources_string)) OR arrayExists(x -> match(x, ?), mapValues(resources_string))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "healthcheck", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: []string{querybuilder.FullTextSearchDefaultWarning},
},
},
{
name: "search_combined_with_filter",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "search('error') AND severity_text = 'ERROR'"},
Limit: 10,
},
enableUseJSONBody: false,
startMs: 1705309200000,
endMs: 1705316400000,
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE ((match(severity_text, ?) OR match(trace_id, ?) OR match(span_id, ?) OR match(LOWER(body), LOWER(?)) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_string)) OR arrayExists(x -> match(x, ?), mapValues(attributes_string))) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_number)) OR arrayExists(x -> match(x, ?), arrayMap(x -> toString(x), mapValues(attributes_number)))) OR (arrayExists(x -> match(x, ?), mapKeys(attributes_bool)) OR arrayExists(x -> match(x, ?), arrayMap(x -> toString(x), mapValues(attributes_bool)))) OR (arrayExists(x -> match(x, ?), mapKeys(resources_string)) OR arrayExists(x -> match(x, ?), mapValues(resources_string)))) AND severity_text = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"error", "error", "error", "error", "error", "error", "error", "error", "error", "error", "error", "error", "ERROR", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: []string{querybuilder.FullTextSearchDefaultWarning},
},
},
{
// default window is ~10h which exceeds the 6-hour search() limit
name: "search_window_exceeds_6h",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "search('error')"},
Limit: 10,
},
enableUseJSONBody: false,
expectedErr: "maximum of 6-hour time",
},
}
@@ -1139,20 +1201,33 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
GetBodyJSONKey,
fl,
)
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErr != nil {
startMs := uint64(1747947419000)
if c.startMs != 0 {
startMs = c.startMs
}
endMs := uint64(1747983448000)
if c.endMs != 0 {
endMs = c.endMs
}
q, err := statementBuilder.Build(context.Background(), startMs, endMs, c.requestType, c.query, nil)
if c.expectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
if err != nil {
_, _, _, _, _, add := errors.Unwrapb(err)
t.Logf("error additionals: %v", add)
_, _, _, _, _, a := errors.Unwrapb(err)
found := false
for _, msg := range a {
if strings.Contains(msg, c.expectedErr) {
found = true
break
}
}
require.True(t, found, "expected additionals to contain %q, got %v", c.expectedErr, a)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
if c.expected.Query != "" {
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
}
})
}

View File

@@ -152,7 +152,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDeltaFastPath(
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
Variables: variables,
StartNs: start,
EndNs: end,
@@ -241,7 +241,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDelta(
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
Variables: variables,
StartNs: start,
EndNs: end,
@@ -311,7 +311,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
Variables: variables,
StartNs: start,
EndNs: end,

View File

@@ -274,7 +274,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
Variables: variables,
StartNs: start,
EndNs: end,

View File

@@ -153,9 +153,10 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
ConditionBuilder: b.conditionBuilder,
FieldKeys: keys,
BodyJSONEnabled: bodyJSONEnabled,
FullTextColumn: b.fullTextColumn,
FreeTextColumn: b.fullTextColumn,
JsonKeyToKey: b.jsonKeyToKey,
SkipFullTextFilter: true,
SkipFreeTextFilter: true,
SkipFullTextSearch: true,
SkipFunctionCalls: true,
// there is no need for "key" not found error for resource filtering
IgnoreNotFoundKeys: true,

View File

@@ -54,6 +54,12 @@ type Event struct {
IsError bool `json:"isError,omitempty"`
}
type OtelSpanRef struct {
TraceId string `json:"traceId,omitempty"`
SpanId string `json:"spanId,omitempty"`
RefType string `json:"refType,omitempty"`
}
// WaterfallSpan represents the span in waterfall response,
// this uses snake_case keys for response as a special case since these
// keys can be directly used to query spans and client need to know the actual fields.
@@ -74,6 +80,7 @@ type WaterfallSpan struct {
TimeUnix uint64 `json:"time_unix"`
TraceID string `json:"trace_id"`
TraceState string `json:"trace_state"`
References []OtelSpanRef `json:"references" required:"true" nullable:"false"`
// Calculated fields https://signoz.io/docs/traces-management/guides/derived-fields-spans
DBName string `json:"db_name,omitempty"`
@@ -128,6 +135,7 @@ type StorableSpan struct {
ExternalHTTPMethod string `ch:"external_http_method"`
ExternalHTTPURL string `ch:"external_http_url"`
ResponseStatusCode string `ch:"response_status_code"`
References string `ch:"references"`
}
// MinimalSpan with only the fields needed to build the parent-child tree.
@@ -285,6 +293,14 @@ func (item *StorableSpan) UnmarshalledEvents() []Event {
return events
}
func (item *StorableSpan) UnmarshalledRefs() []OtelSpanRef {
refs := []OtelSpanRef{}
if err := json.Unmarshal([]byte(item.References), &refs); err != nil {
return nil // skip malformed values
}
return refs
}
func (item *StorableSpan) ToWaterfallSpan(traceID string) *WaterfallSpan {
resources := make(map[string]string)
maps.Copy(resources, item.ResourcesString)
@@ -318,6 +334,7 @@ func (item *StorableSpan) ToWaterfallSpan(traceID string) *WaterfallSpan {
Children: make([]*WaterfallSpan, 0),
TimeUnix: uint64(item.StartTime.UnixNano()),
ServiceName: item.ServiceName,
References: item.UnmarshalledRefs(),
}
}

View File

@@ -111,6 +111,8 @@ func (v *variableReplacementVisitor) Visit(tree antlr.ParseTree) any {
return v.VisitFullText(t)
case *grammar.FunctionCallContext:
return v.VisitFunctionCall(t)
case *grammar.SearchCallContext:
return v.VisitSearchCall(t)
case *grammar.FunctionParamListContext:
return v.VisitFunctionParamList(t)
case *grammar.FunctionParamContext:
@@ -203,6 +205,8 @@ 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())
}
@@ -391,6 +395,11 @@ func (v *variableReplacementVisitor) VisitFullText(ctx *grammar.FullTextContext)
return ""
}
func (v *variableReplacementVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
params := v.Visit(ctx.FunctionParamList()).(string)
return "search(" + params + ")"
}
func (v *variableReplacementVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any {
var functionName string
if ctx.HAS() != nil {

View File

@@ -0,0 +1,273 @@
"""
Full-text search integration tests — context isolation.
Validates that FTS (bare text, "quoted", search()) correctly finds logs
when the search term lives in any one of the intended field contexts:
severity_text — LowCardinality(String), LOWER/match
trace_id — String, LOWER/match
span_id — String, LOWER/match
body — String (stringified JSON), LOWER(body)/match
attributes_string — Map(String,String), arrayExists(match, mapKeys/mapValues)
attributes_number — Map(String,Float64), arrayExists(match, mapKeys)
resources_string — Map(String,String), arrayExists(match, mapKeys/mapValues)
Each test log carries a unique token in exactly one context; all other fields
are neutral. Per-context assertions verify:
1. Exactly 1 row is returned (isolation — no cross-context bleed).
2. The returned row belongs to the expected log (service.name check).
3. The FTS warning is always present in the response.
"""
import json
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logs import Logs
from fixtures.querier import build_raw_query, get_rows, make_query_request
def test_fts_across_contexts(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
) -> None:
"""
10 logs, one unique token per log, each token in a different field context.
Every FTS form (bare / quoted / search('')) is exercised per context.
search() requires a quoted argument; unquoted args must return HTTP 400.
"""
now = datetime.now(tz=UTC)
# ── unique tokens ────────────────────────────────────────────────────────
# TOK_SEV: a severity level unused by every other fixture log (all others are INFO).
# TOK_TID / TOK_SID: valid hex-format IDs; unique so no other inserted log matches them.
# The remaining tokens are lowercase so they work for both case-insensitive
# (String/JSON) and case-sensitive (Map) match(). Each token appears in exactly one log.
TOK_SEV = "TRACE" # → severity_text (others use INFO)
TOK_TID = "0af7651916cd43dd8448eb211c80319c" # → trace_id (valid 32-char hex, unique)
TOK_SID = "6e0c63257de34c92" # → span_id (valid 16-char hex, unique)
TOK_BKEY = "xfts_bkey_004" # → body JSON — key name
TOK_BVAL = "xfts_bval_005" # → body JSON — value
TOK_ASKEY = "xfts_askey_006" # → attributes_string key
TOK_ASVAL = "xfts_asval_007" # → attributes_string value
TOK_ANKEY = "xfts_ankey_008" # → attributes_number key (string key of the map)
TOK_RKEY = "xfts_rkey_009" # → resources_string key
TOK_RVAL = "xfts_rval_010" # → resources_string value
# Neutral body — does not contain any xfts_ token.
_N = json.dumps({"message": "neutral log entry"})
# ── log fixtures ─────────────────────────────────────────────────────────
# Each log uses a distinct service.name so assertions can verify identity.
log_sev = Logs(
timestamp=now - timedelta(seconds=10),
resources={"service.name": "severity-text-svc"},
body=_N,
severity_text=TOK_SEV, # ← token lives here
)
log_tid = Logs(
timestamp=now - timedelta(seconds=9),
resources={"service.name": "trace-id-svc"},
body=_N,
severity_text="INFO",
trace_id=TOK_TID, # ← token lives here
)
log_sid = Logs(
timestamp=now - timedelta(seconds=8),
resources={"service.name": "span-id-svc"},
body=_N,
severity_text="INFO",
span_id=TOK_SID, # ← token lives here
)
log_bkey = Logs(
timestamp=now - timedelta(seconds=7),
resources={"service.name": "body-key-svc"},
body=json.dumps({TOK_BKEY: "irrelevant_val"}), # ← token is a key in the stringified body
severity_text="INFO",
)
log_bval = Logs(
timestamp=now - timedelta(seconds=6),
resources={"service.name": "body-val-svc"},
body=json.dumps({"some_key": TOK_BVAL}), # ← token is a value in the stringified body
severity_text="INFO",
)
log_askey = Logs(
timestamp=now - timedelta(seconds=5),
resources={"service.name": "attr-str-key-svc"},
attributes={TOK_ASKEY: "other_val"}, # ← token is an attr string key
body=_N,
severity_text="INFO",
)
log_asval = Logs(
timestamp=now - timedelta(seconds=4),
resources={"service.name": "attr-str-val-svc"},
attributes={"some_attr": TOK_ASVAL}, # ← token is an attr string value
body=_N,
severity_text="INFO",
)
log_ankey = Logs(
timestamp=now - timedelta(seconds=3),
resources={"service.name": "attr-num-key-svc"},
attributes={TOK_ANKEY: 42}, # ← token is an attr_number key
body=_N,
severity_text="INFO",
)
log_rkey = Logs(
timestamp=now - timedelta(seconds=2),
resources={"service.name": "resource-key-svc", TOK_RKEY: "irrelevant_val"}, # ← token is a resource key
body=_N,
severity_text="INFO",
)
log_rval = Logs(
timestamp=now - timedelta(seconds=1),
resources={"service.name": "resource-val-svc", "some_res_key": TOK_RVAL}, # ← token is a resource value
body=_N,
severity_text="INFO",
)
logs_list = [
log_sev,
log_tid,
log_sid,
log_bkey,
log_bval,
log_askey,
log_asval,
log_ankey,
log_rkey,
log_rval,
]
not_search_count = len(logs_list) - 1
insert_logs(logs_list)
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
start_ms = int((now - timedelta(seconds=20)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
# ── helpers ───────────────────────────────────────────────────────────────
def _fts(expression: str) -> requests.Response:
resp = make_query_request(
signoz=signoz,
token=token,
start_ms=start_ms,
end_ms=end_ms,
queries=[build_raw_query("A", "logs", filter_expression=expression, step_interval=60)],
request_type="raw",
)
assert resp.status_code == 200, f"FTS({expression!r}): HTTP {resp.status_code}{resp.text}"
return resp
def _only(svc: str):
"""Validate lambda: exactly 1 row for `svc`."""
return lambda r, s=svc: len(get_rows(r)) == 1 and get_rows(r)[0]["data"]["resources_string"].get("service.name") == s
# ── per-context isolation cases ───────────────────────────────────────────
# Free Text Search: bare/quoted tokens route through freeTextColumn (body only).
# Full Text Search: search() fans out across all fields via ftsFieldKeys.
cases = [
# body — Free text search (body column only)
{
"name": "fts.body/quoted",
"expression": f'"{TOK_BVAL}"',
"validate": _only("body-val-svc"),
},
# TOK_SEV lives only in severity_text — free text (body-only) must return 0 rows
{
"name": "fts.free_text/non_body_token_returns_zero",
"expression": f'"{TOK_SEV}"',
"validate": lambda r: get_rows(r) == [],
},
# ── Full Text Search (search()) ───────────────────────────────────────
# severity_text — only reachable via search(), not bare/quoted Free Text Search
{
"name": "fts.severity_text/search_quoted",
"expression": f'search("{TOK_SEV}")',
"validate": _only("severity-text-svc"),
},
# trace_id (String — only reachable via search())
{
"name": "fts.trace_id/search",
"expression": f'search("{TOK_TID}")',
"validate": _only("trace-id-svc"),
},
# span_id (String — LOWER/match, case-insensitive)
{
"name": "fts.span_id/search",
"expression": f'search("{TOK_SID}")',
"validate": _only("span-id-svc"),
},
# body key (String — LOWER(body)/match, token appears as a key in stringified JSON)
{
"name": "fts.body_key/search",
"expression": f'search("{TOK_BKEY}")',
"validate": _only("body-key-svc"),
},
# body value (String — LOWER(body)/match, token appears as a value in stringified JSON)
{
"name": "fts.body_val/search",
"expression": f'search("{TOK_BVAL}")',
"validate": _only("body-val-svc"),
},
# attributes_string key (Map — arrayExists(match, mapKeys), case-sensitive)
{
"name": "fts.attr_str_key/search",
"expression": f'search("{TOK_ASKEY}")',
"validate": _only("attr-str-key-svc"),
},
# attributes_string value (Map — arrayExists(match, mapValues), case-sensitive)
{
"name": "fts.attr_str_val/search",
"expression": f'search("{TOK_ASVAL}")',
"validate": _only("attr-str-val-svc"),
},
# attributes_number key (Map — arrayExists(match, mapKeys), key is a string)
{
"name": "fts.attr_num_key/search",
"expression": f'search("{TOK_ANKEY}")',
"validate": _only("attr-num-key-svc"),
},
# resources_string key (Map — arrayExists(match, mapKeys), case-sensitive)
{
"name": "fts.resource_key/search",
"expression": f'search("{TOK_RKEY}")',
"validate": _only("resource-key-svc"),
},
# resources_string value (Map — arrayExists(match, mapValues), case-sensitive)
{
"name": "fts.resource_val/search",
"expression": f'search("{TOK_RVAL}")',
"validate": _only("resource-val-svc"),
},
# NOT search: all logs except log_sev
{
"name": "fts.not_search",
"expression": f'NOT search("{TOK_SEV}")',
"validate": lambda r, n=not_search_count: len(get_rows(r)) == n and "severity-text-svc" not in {row["data"]["resources_string"].get("service.name") for row in get_rows(r)},
},
# no-match guard: a token that exists nowhere must return 0 rows
{"name": "fts.no_match", "expression": '"xfts_nonexistent_zzz_999"', "validate": lambda r: get_rows(r) == []},
]
for case in cases:
resp = _fts(case["expression"])
assert case["validate"](resp), f"Validation failed for '{case['name']}': {resp.json()}"
# ── unquoted search guard ────────────────────────────────────────────────
# search(TRACE) — unquoted argument — must be rejected with HTTP 400.
unquoted_resp = make_query_request(
signoz=signoz,
token=token,
start_ms=start_ms,
end_ms=end_ms,
queries=[build_raw_query("A", "logs", filter_expression=f"search({TOK_SEV})", step_interval=60)],
request_type="raw",
)
assert unquoted_resp.status_code == 400, f"Expected 400 for unquoted search(), got {unquoted_resp.status_code}: {unquoted_resp.text}"

View File

@@ -1217,7 +1217,7 @@ def test_message_searches(
"aggregation": "count()",
"validate": lambda r: len(get_rows(r)) == 2 and set(_body_messages(r)) == payment_messages,
},
# FTS — String bare keyword
# Free Text Search — String bare keyword
{
"name": "msg.fts_quoted",
"requestType": "raw",
@@ -1225,7 +1225,6 @@ def test_message_searches(
"aggregation": "count()",
"validate": lambda r: len(get_rows(r)) == 2 and all("Payment" in b.get("message", "") for b in _get_bodies(r)) and r.json().get("data", {}).get("warning") is not None,
},
# FTS — bare keyword
{
"name": "msg.fts_quoted_without_quotes",
"requestType": "raw",
@@ -1233,6 +1232,27 @@ def test_message_searches(
"aggregation": "count()",
"validate": lambda r: len(get_rows(r)) == 2 and all("Payment" in b.get("message", "") for b in _get_bodies(r)) and r.json().get("data", {}).get("warning") is not None,
},
# ── Full Text Search: search() explicit function ──────────────────────────────────────
# search("Payment") is equivalent to the bare-text '"Payment"' FTS:
# both fan out a case-insensitive match across severity_text, trace_id,
# span_id, body JSON values, attribute values, and resource values.
{
"name": "msg.search_quoted",
"requestType": "raw",
"expression": 'search("Payment")',
"aggregation": "count()",
"validate": lambda r: len(get_rows(r)) == 2 and set(_body_messages(r)) == payment_messages and r.json().get("data", {}).get("warning") is not None,
},
# NOT search("Payment") — inverted FTS: logs that do NOT have "payment"
# in any field are returned. control_log (db-service) and no_msg_log
# (metrics-service) have no "payment" anywhere → 2 results.
{
"name": "msg.not_search",
"requestType": "raw",
"expression": 'NOT search("Payment")',
"aggregation": "count()",
"validate": lambda r: len(get_rows(r)) == 2 and all("Payment" not in b.get("message", "") for b in _get_bodies(r)) and r.json().get("data", {}).get("warning") is not None,
},
# = operator via body.message — tests exact match path
{
"name": "msg.body_message_exact",

View File

@@ -0,0 +1,313 @@
"""
Text search integration tests.
Validates that Text Search (free text, full text) correctly finds logs
when the search term lives in any one of the intended field contexts:
severity_text — LowCardinality(String), LOWER/match
trace_id — String, LOWER/match
span_id — String, LOWER/match
body JSON — JSON column, LOWER(toString(col))/match
attributes_string — Map(String,String), arrayExists(match, mapKeys/mapValues)
attributes_number — Map(String,Float64), arrayExists(match, mapKeys)
resources_string — Map(String,String), arrayExists(match, mapKeys/mapValues)
Each test log carries a unique token in exactly one context; all other fields
are neutral. Per-context assertions verify:
1. Exactly 1 row is returned (isolation — no cross-context bleed).
2. The returned row belongs to the expected log (service.name check).
3. The FTS warning is always present in the response.
"""
import json
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logs import Logs
from fixtures.querier import build_raw_query, get_rows, make_query_request
def test_fts_across_contexts(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
export_json_types: Callable[[list[Logs]], None],
) -> None:
"""
10 logs, one unique token per log, each token in a different field context.
Every FTS form (bare / quoted / search('')) is exercised per context.
search() requires a quoted argument; unquoted args must return HTTP 400.
"""
now = datetime.now(tz=UTC)
# ── unique tokens ────────────────────────────────────────────────────────
# TOK_SEV: a severity level unused by every other fixture log (all others are INFO).
# TOK_TID / TOK_SID: valid hex-format IDs; unique so no other inserted log matches them.
# The remaining tokens are lowercase so they work for both case-insensitive
# (String/JSON) and case-sensitive (Map) match(). Each token appears in exactly one log.
TOK_SEV = "TRACE" # → severity_text (others use INFO)
TOK_TID = "0af7651916cd43dd8448eb211c80319c" # → trace_id (valid 32-char hex, unique)
TOK_SID = "6e0c63257de34c92" # → span_id (valid 16-char hex, unique)
TOK_BKEY = "xfts_bkey_004" # → body JSON — key name
TOK_BVAL = "xfts_bval_005" # → body JSON — value
TOK_ASKEY = "xfts_askey_006" # → attributes_string key
TOK_ASVAL = "xfts_asval_007" # → attributes_string value
TOK_ANKEY = "xfts_ankey_008" # → attributes_number key (string key of the map)
TOK_RKEY = "xfts_rkey_009" # → resources_string key
TOK_RVAL = "xfts_rval_010" # → resources_string value
TOK_BMSG = "xfts_bmsg_011" # → body_v2.message — for bare/quoted FTS (freeTextColumn path)
# Neutral body — does not contain any xfts_ token.
_N = json.dumps({"message": "neutral log entry"})
# ── log fixtures ─────────────────────────────────────────────────────────
# Each log uses a distinct service.name so assertions can verify identity.
log_sev = Logs(
timestamp=now - timedelta(seconds=10),
resources={"service.name": "severity-text-svc"},
body_v2=_N,
body_promoted="",
severity_text=TOK_SEV, # ← here
)
log_tid = Logs(
timestamp=now - timedelta(seconds=9),
resources={"service.name": "txid-svc"},
body_v2=_N,
body_promoted="",
severity_text="INFO",
trace_id=TOK_TID, # ← here
)
log_sid = Logs(
timestamp=now - timedelta(seconds=8),
resources={"service.name": "span-id-svc"},
body_v2=_N,
body_promoted="",
severity_text="INFO",
span_id=TOK_SID, # ← here
)
log_bkey = Logs(
timestamp=now - timedelta(seconds=7),
resources={"service.name": "body-key-svc"},
body_v2=json.dumps({TOK_BKEY: "irrelevant_val"}), # ← token is a JSON key
body_promoted="",
severity_text="INFO",
)
log_bval = Logs(
timestamp=now - timedelta(seconds=6),
resources={"service.name": "body-val-svc"},
body_v2=json.dumps({"some_key": TOK_BVAL}), # ← token is a JSON value
body_promoted="",
severity_text="INFO",
)
log_askey = Logs(
timestamp=now - timedelta(seconds=5),
resources={"service.name": "attr-str-key-svc"},
attributes={TOK_ASKEY: "other_val"}, # ← token is an attr string key
body_v2=_N,
body_promoted="",
severity_text="INFO",
)
log_asval = Logs(
timestamp=now - timedelta(seconds=4),
resources={"service.name": "attr-str-val-svc"},
attributes={"some_attr": TOK_ASVAL}, # ← token is an attr string value
body_v2=_N,
body_promoted="",
severity_text="INFO",
)
log_ankey = Logs(
timestamp=now - timedelta(seconds=3),
resources={"service.name": "attr-num-key-svc"},
attributes={TOK_ANKEY: 42}, # ← token is an attr_number key
body_v2=_N,
body_promoted="",
severity_text="INFO",
)
log_rkey = Logs(
timestamp=now - timedelta(seconds=2),
resources={"service.name": "resource-key-svc", TOK_RKEY: "irrelevant_val"}, # ← token is a resource key
body_v2=_N,
body_promoted="",
severity_text="INFO",
)
log_rval = Logs(
timestamp=now - timedelta(seconds=1),
resources={"service.name": "resource-val-svc", "some_res_key": TOK_RVAL}, # ← token is a resource value
body_v2=_N,
body_promoted="",
severity_text="INFO",
)
# log_bmsg: unique token in body_v2.message — the column targeted by bare/quoted FTS
log_bmsg = Logs(
timestamp=now - timedelta(milliseconds=500),
resources={"service.name": "body-msg-svc"},
body_v2=json.dumps({"message": TOK_BMSG}),
body_promoted="",
severity_text="INFO",
)
logs_list = [
log_sev,
log_tid,
log_sid,
log_bkey,
log_bval,
log_askey,
log_asval,
log_ankey,
log_rkey,
log_rval,
log_bmsg,
]
not_search_count = len(logs_list) - 1
export_json_types(logs_list)
insert_logs(logs_list)
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
start_ms = int((now - timedelta(seconds=20)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
# ── helpers ───────────────────────────────────────────────────────────────
def _fts(expression: str) -> requests.Response:
resp = make_query_request(
signoz=signoz,
token=token,
start_ms=start_ms,
end_ms=end_ms,
queries=[build_raw_query("A", "logs", filter_expression=expression, step_interval=60)],
request_type="raw",
)
assert resp.status_code == 200, f"FTS({expression!r}): HTTP {resp.status_code}{resp.text}"
return resp
def _only(svc: str):
"""Validate lambda: exactly 1 row for `svc`, FTS warning present."""
return lambda r, s=svc: len(get_rows(r)) == 1 and get_rows(r)[0]["data"]["resources_string"].get("service.name") == s and r.json().get("data", {}).get("warning") is not None
# ── per-context isolation cases ───────────────────────────────────────────
# Free Text Search: bare/quoted tokens route through freeTextColumn (body_v2.message only).
# Full Text Search: search() fans out across all fields via ftsFieldKeys.
cases = [
# body_v2.message — Free Text Search
{
"name": "fts.body_msg/bare",
"expression": TOK_BMSG,
"validate": _only("body-msg-svc"),
},
{
"name": "fts.body_msg/quoted",
"expression": f'"{TOK_BMSG}"',
"validate": _only("body-msg-svc"),
},
# TOK_SEV lives only in severity_text — free text (body_v2.message only) must return 0 rows
{
"name": "fts.free_text/non_body_token_returns_zero",
"expression": f'"{TOK_SEV}"',
"validate": lambda r: get_rows(r) == [],
},
# ── Full Text Search (search()) ───────────────────────────────────────
# severity_text — only reachable via search(), not bare/quoted Free Text Search
{
"name": "fts.severity_text/search_quoted",
"expression": f'search("{TOK_SEV}")',
"validate": _only("severity-text-svc"),
},
# trace_id (String — only reachable via search())
{
"name": "fts.trace_id/search",
"expression": f'search("{TOK_TID}")',
"validate": _only("txid-svc"),
},
# span_id (String — LOWER/match, case-insensitive)
{
"name": "fts.span_id/search",
"expression": f'search("{TOK_SID}")',
"validate": _only("span-id-svc"),
},
# body JSON key (JSON col — LOWER(toString(col))/match, full serialised JSON)
{
"name": "fts.body_key/search",
"expression": f'search("{TOK_BKEY}")',
"validate": _only("body-key-svc"),
},
# body JSON value
{
"name": "fts.body_val/search",
"expression": f'search("{TOK_BVAL}")',
"validate": _only("body-val-svc"),
},
# attributes_string key (Map — arrayExists(match, mapKeys), case-sensitive)
{
"name": "fts.attr_str_key/search",
"expression": f'search("{TOK_ASKEY}")',
"validate": _only("attr-str-key-svc"),
},
# attributes_string value (Map — arrayExists(match, mapValues), case-sensitive)
{
"name": "fts.attr_str_val/search",
"expression": f'search("{TOK_ASVAL}")',
"validate": _only("attr-str-val-svc"),
},
# attributes_number key (Map — arrayExists(match, mapKeys), key is a string)
{
"name": "fts.attr_num_key/search",
"expression": f'search("{TOK_ANKEY}")',
"validate": _only("attr-num-key-svc"),
},
# resources_string key (Map — arrayExists(match, mapKeys), case-sensitive)
{
"name": "fts.resource_key/search",
"expression": f'search("{TOK_RKEY}")',
"validate": _only("resource-key-svc"),
},
# resources_string value (Map — arrayExists(match, mapValues), case-sensitive)
{
"name": "fts.resource_val/search",
"expression": f'search("{TOK_RVAL}")',
"validate": _only("resource-val-svc"),
},
# NOT search: all logs except log_sev
{
"name": "fts.not_search",
"expression": f'NOT search("{TOK_SEV}")',
"validate": lambda r, n=not_search_count: len(get_rows(r)) == n and "severity-text-svc" not in {row["data"]["resources_string"].get("service.name") for row in get_rows(r)} and r.json().get("data", {}).get("warning") is not None,
},
# no-match guard: a token that exists nowhere must return 0 rows
{"name": "fts.no_match", "expression": '"xfts_nonexistent_zzz_999"', "validate": lambda r: get_rows(r) == []},
]
for case in cases:
resp = _fts(case["expression"])
assert case["validate"](resp), f"Validation failed for '{case['name']}': {resp.json()}"
# ── 6-hour window guard ──────────────────────────────────────────────────
# FTS over a window wider than 6 hours must be rejected with HTTP 400.
# The statement builder returns an error before ClickHouse is ever reached.
wide_resp = make_query_request(
signoz=signoz,
token=token,
start_ms=int((now - timedelta(hours=7)).timestamp() * 1000),
end_ms=int(now.timestamp() * 1000),
queries=[build_raw_query("A", "logs", filter_expression=f'search("{TOK_SEV}")', step_interval=60)],
request_type="raw",
)
assert wide_resp.status_code == 400, f"Expected 400 for FTS over >6h window, got {wide_resp.status_code}: {wide_resp.text}"
# ── unquoted search guard ────────────────────────────────────────────────
# search(TRACE) — unquoted argument — must be rejected with HTTP 400.
unquoted_resp = make_query_request(
signoz=signoz,
token=token,
start_ms=start_ms,
end_ms=end_ms,
queries=[build_raw_query("A", "logs", filter_expression=f"search({TOK_SEV})", step_interval=60)],
request_type="raw",
)
assert unquoted_resp.status_code == 400, f"Expected 400 for unquoted search(), got {unquoted_resp.status_code}: {unquoted_resp.text}"