Compare commits

..

12 Commits

Author SHA1 Message Date
Gaurav Tewari
120b194e1a Merge branch 'main' into feat/add-ripple-effect 2026-06-02 10:22:55 +05:30
Gaurav Tewari
da268fa694 Merge remote-tracking branch 'origin/main' into feat/add-ripple-effect
# Conflicts:
#	frontend/src/container/MySettings/index.tsx
2026-05-29 17:00:53 +05:30
Gaurav Tewari
5d07fcb181 fix: formatting 2026-05-29 15:56:02 +05:30
Gaurav Tewari
b02e17a0d4 chore: minor comments 2026-05-27 14:04:05 +05:30
Gaurav Tewari
637e194d1c chore: more changes 2026-05-27 13:12:31 +05:30
Gaurav Tewari
9eb8d6466c Merge remote-tracking branch 'origin' into feat/add-ripple-effect 2026-05-27 12:38:31 +05:30
Gaurav Tewari
0aec7a9fe8 fix: tests 2026-05-25 23:22:50 +05:30
Gaurav Tewari
bebe4ebb89 fix: update theme 2026-05-25 23:17:50 +05:30
Gaurav Tewari
2209708caa chore: more fixes 2026-05-25 22:53:36 +05:30
Gaurav Tewari
1f813ce21f refactor: update effect 2026-05-25 22:38:01 +05:30
Gaurav Tewari
f082821ac2 feat: add light and dark mode in command pallet 2026-05-25 13:30:44 +05:30
Gaurav Tewari
3c5bd81421 chore: add ripple effect 2026-05-23 22:15:00 +05:30
86 changed files with 1627 additions and 2674 deletions

View File

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

View File

@@ -7768,21 +7768,6 @@ 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;
};
@@ -7877,10 +7862,6 @@ export interface SpantypesWaterfallSpanDTO {
* @type string
*/
parent_span_id?: string;
/**
* @type array
*/
references: SpantypesOtelSpanRefDTO[];
/**
* @type object,null
*/

View File

@@ -4,7 +4,6 @@ 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';
@@ -110,7 +109,7 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<TooltipSimple title="Noz">
<Button
variant="solid"
color="secondary"

View File

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

View File

@@ -14,7 +14,9 @@ export function ShiftHoldOverlayController({
const actions = createShortcutActions({
navigate: noop,
handleThemeChange: noop,
// Overlay is read-only — actions never fire — so we only need a no-op
// that satisfies the typed signature.
handleThemeChange: (): void => undefined,
});
const visible = useShiftHoldOverlay({

View File

@@ -116,17 +116,35 @@ jest.mock('hooks/useNotifications', (): unknown => ({
}));
// mock theme hook
jest.mock('hooks/useDarkMode', (): unknown => ({
useThemeMode: (): {
//
// We spread jest.requireActual so additions to hooks/useDarkMode (new hooks,
// re-exports, contexts) keep working in this test without needing the mock to
// re-enumerate every export. We only override the hooks the palette actually
// calls.
jest.mock('hooks/useDarkMode', (): unknown => {
const actual = jest.requireActual('hooks/useDarkMode');
const useThemeModeMock = (): {
setAutoSwitch: jest.Mock;
setTheme: jest.Mock;
toggleTheme: jest.Mock;
theme: string;
autoSwitch: boolean;
} => ({
setAutoSwitch: jest.fn(),
setTheme: jest.fn(),
toggleTheme: jest.fn(),
theme: 'dark',
}),
}));
autoSwitch: false,
});
return {
...actual,
__esModule: true,
default: useThemeModeMock,
useThemeMode: useThemeModeMock,
useIsDarkMode: (): boolean => true,
useSystemTheme: (): 'dark' | 'light' => 'dark',
};
});
// mock updateUserPreference API and react-query mutation
jest.mock('api/v1/user/preferences/name/update', (): jest.Mock => jest.fn());

View File

@@ -21,6 +21,8 @@ import {
useAIAssistantStore,
} from 'container/AIAssistant/store/useAIAssistantStore';
import { useThemeMode } from 'hooks/useDarkMode';
import { ThemeMode } from 'hooks/useDarkMode/constant';
import { useThemeSelection } from 'hooks/useDarkMode/useThemeSelection';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import history from 'lib/history';
import { ROLES as UserRole } from 'types/roles';
@@ -48,7 +50,8 @@ export function CmdKPalette({
}): JSX.Element | null {
const { open, setOpen } = useCmdK();
const { setAutoSwitch, setTheme, theme } = useThemeMode();
const { theme } = useThemeMode();
const selectTheme = useThemeSelection();
const location = useLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const startNewConversation = useAIAssistantStore(
@@ -81,14 +84,12 @@ export function CmdKPalette({
useEffect(cmdKEffect, [setOpen]);
function handleThemeChange(value: string): void {
function handleThemeChange(value: ThemeMode): void {
logEvent('Account Settings: Theme Changed', { theme: value });
if (value === 'auto') {
setAutoSwitch(true);
} else {
setAutoSwitch(false);
setTheme(value);
}
// Close the palette inside the same flushSync batch as the theme change
// so its dismissal is part of the captured "new" frame of the wipe;
// otherwise the dialog would be visible in both snapshots and flicker.
selectTheme(value, () => setOpen(false));
}
function onClickHandler(key: string): void {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import ROUTES from 'constants/routes';
import { GlobalShortcutsName } from 'constants/shortcuts/globalShortcuts';
import { THEME_MODE } from 'hooks/useDarkMode/constant';
import { THEME_MODE, ThemeMode } from 'hooks/useDarkMode/constant';
import {
BarChart,
BellDot,
@@ -34,7 +34,7 @@ export type CmdAction = {
type ActionDeps = {
navigate: (path: string) => void;
handleThemeChange: (mode: string) => void;
handleThemeChange: (mode: ThemeMode) => void;
/**
* Provided only when the AI Assistant feature is available for the current
* tenant. When present, the palette surfaces an "Open AI Assistant" entry

View File

@@ -5,7 +5,6 @@ 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';
@@ -43,15 +42,16 @@ export default function AIAssistantTrigger(): JSX.Element | null {
}
return (
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<TooltipSimple title="Noz">
<Button
variant="solid"
color="primary"
className={`${styles.trigger} noz-wave`}
onClick={handleOpen}
aria-label="Open Noz"
prefix={<Noz size={24} />}
/>
>
<Noz size={24} />
</Button>
</TooltipSimple>
);
}

View File

@@ -12,6 +12,8 @@ import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
const toggleThemeFunction = jest.fn();
const setThemeFunction = jest.fn();
const setAutoSwitchFunction = jest.fn();
const logEventFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
@@ -56,9 +58,11 @@ jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: jest.fn(() => true),
useSystemTheme: jest.fn(() => 'dark'),
default: jest.fn(() => ({
theme: 'dark',
setTheme: setThemeFunction,
toggleTheme: toggleThemeFunction,
autoSwitch: false,
setAutoSwitch: jest.fn(),
setAutoSwitch: setAutoSwitchFunction,
})),
}));
@@ -134,7 +138,8 @@ describe('MySettings Flows', () => {
fireEvent.click(lightOption);
await waitFor(() => {
expect(toggleThemeFunction).toHaveBeenCalled();
expect(setAutoSwitchFunction).toHaveBeenCalledWith(false);
expect(setThemeFunction).toHaveBeenCalledWith('light');
expect(logEventFunction).toHaveBeenCalledWith(
'Account Settings: Theme Changed',
{
@@ -142,6 +147,10 @@ describe('MySettings Flows', () => {
},
);
});
// Lock in that the new selectTheme flow does not call toggleTheme;
// otherwise we'd double-flip on top of the explicit setTheme call.
expect(toggleThemeFunction).not.toHaveBeenCalled();
});
});

View File

@@ -9,6 +9,8 @@ import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
import { USER_PREFERENCES } from 'constants/userPreferences';
import useThemeMode, { useIsDarkMode, useSystemTheme } from 'hooks/useDarkMode';
import { THEME_MODE, ThemeMode } from 'hooks/useDarkMode/constant';
import { useThemeSelection } from 'hooks/useDarkMode/useThemeSelection';
import { useNotifications } from 'hooks/useNotifications';
import { MonitorCog, Moon, Sun } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
@@ -24,9 +26,10 @@ import './MySettings.styles.scss';
function MySettings(): JSX.Element {
const isDarkMode = useIsDarkMode();
const { userPreferences, updateUserPreferenceInContext } = useAppContext();
const { toggleTheme, autoSwitch, setAutoSwitch } = useThemeMode();
const { autoSwitch } = useThemeMode();
const systemTheme = useSystemTheme();
const { notifications } = useNotifications();
const selectTheme = useThemeSelection();
const [sideNavPinned, setSideNavPinned] = useState(false);
@@ -59,7 +62,7 @@ function MySettings(): JSX.Element {
<Moon data-testid="dark-theme-icon" size={12} /> Dark{' '}
</div>
),
value: 'dark',
value: THEME_MODE.DARK,
},
{
label: (
@@ -68,7 +71,7 @@ function MySettings(): JSX.Element {
<Badge color="robin">Beta</Badge>
</div>
),
value: 'light',
value: THEME_MODE.LIGHT,
},
{
label: (
@@ -76,46 +79,31 @@ function MySettings(): JSX.Element {
<MonitorCog size={12} data-testid="auto-theme-icon" /> System{' '}
</div>
),
value: 'auto',
value: THEME_MODE.SYSTEM,
},
];
const [theme, setTheme] = useState(() => {
if (autoSwitch) {
return 'auto';
return THEME_MODE.SYSTEM;
}
return isDarkMode ? 'dark' : 'light';
return isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT;
});
const handleThemeChange = (value: string): void => {
logEvent('Account Settings: Theme Changed', {
theme: value,
});
setTheme(value);
if (value === 'auto') {
setAutoSwitch(true);
} else {
setAutoSwitch(false);
// Only toggle if the current theme is different from the target
const targetIsDark = value === 'dark';
if (targetIsDark !== isDarkMode) {
toggleTheme();
}
}
// ToggleGroupSimple items above are all THEME_MODE values, so narrowing
// the string here is safe.
const mode = value as ThemeMode;
logEvent('Account Settings: Theme Changed', { theme: mode });
selectTheme(mode);
};
useEffect(() => {
if (autoSwitch) {
setTheme('auto');
setTheme(THEME_MODE.SYSTEM);
return;
}
if (isDarkMode) {
setTheme('dark');
} else {
setTheme('light');
}
setTheme(isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT);
}, [autoSwitch, isDarkMode]);
const handleSideNavPinnedChange = (checked: boolean): void => {

View File

@@ -5,6 +5,7 @@ import { Pin, PinOff } from '@signozhq/icons';
import { SidebarItem } from '../sideNav.types';
import './NavItem.styles.scss';
import './NavItem.styles.scss';
export default function NavItem({
@@ -26,7 +27,7 @@ export default function NavItem({
showIcon?: boolean;
dataTestId?: string;
}): JSX.Element {
const { label, icon, isBeta, isNew, isEarlyAccess, tooltip } = item;
const { label, icon, isBeta, isNew, isEarlyAccess } = item;
const handleTogglePinClick = (
event: React.MouseEvent<SVGSVGElement, MouseEvent>,
@@ -35,7 +36,7 @@ export default function NavItem({
onTogglePin?.(item);
};
const navItem = (
return (
<div
className={cx(
'nav-item',
@@ -106,15 +107,6 @@ 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,7 +45,6 @@ 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,
@@ -98,7 +97,6 @@ export const aiAssistantMenuItem = {
icon: <Noz size={16} />,
itemKey: 'ai-assistant',
isEarlyAccess: true,
tooltip: NOZ_TOOLTIP_TITLE,
};
export const shortcutMenuItem = {

View File

@@ -15,8 +15,6 @@ 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

@@ -0,0 +1,164 @@
import { act, renderHook } from '@testing-library/react';
import { useThemeSelection } from '../useThemeSelection';
const setThemeMock = jest.fn();
const setAutoSwitchMock = jest.fn();
let themeValue = 'dark';
let systemThemeValue: 'dark' | 'light' = 'light';
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
default: (): {
theme: string;
setTheme: jest.Mock;
setAutoSwitch: jest.Mock;
toggleTheme: jest.Mock;
autoSwitch: boolean;
} => ({
theme: themeValue,
setTheme: setThemeMock,
setAutoSwitch: setAutoSwitchMock,
toggleTheme: jest.fn(),
autoSwitch: false,
}),
useThemeMode: (): {
theme: string;
setTheme: jest.Mock;
setAutoSwitch: jest.Mock;
toggleTheme: jest.Mock;
autoSwitch: boolean;
} => ({
theme: themeValue,
setTheme: setThemeMock,
setAutoSwitch: setAutoSwitchMock,
toggleTheme: jest.fn(),
autoSwitch: false,
}),
useSystemTheme: (): 'dark' | 'light' => systemThemeValue,
useIsDarkMode: (): boolean => themeValue === 'dark',
}));
const canAnimateMock = jest.fn();
const runTransitionMock = jest.fn();
jest.mock('utils/themeTransition', () => ({
__esModule: true,
canAnimateThemeTransition: (): boolean => canAnimateMock(),
runThemeTransition: (cb: () => void): void => runTransitionMock(cb),
}));
describe('useThemeSelection', () => {
beforeEach(() => {
jest.clearAllMocks();
themeValue = 'dark';
systemThemeValue = 'light';
canAnimateMock.mockReturnValue(false);
// Default behaviour: invoke the applyChange callback synchronously.
runTransitionMock.mockImplementation((cb: () => void) => cb());
});
it('applies an explicit light theme without auto-switch', () => {
themeValue = 'dark';
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('light'));
expect(setAutoSwitchMock).toHaveBeenCalledWith(false);
expect(setThemeMock).toHaveBeenCalledWith('light');
});
it('applies an explicit dark theme without auto-switch', () => {
themeValue = 'light';
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('dark'));
expect(setAutoSwitchMock).toHaveBeenCalledWith(false);
expect(setThemeMock).toHaveBeenCalledWith('dark');
});
it('SYSTEM with a light system preference resolves to setTheme("light") + auto on', () => {
themeValue = 'dark';
systemThemeValue = 'light';
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('auto'));
expect(setAutoSwitchMock).toHaveBeenCalledWith(true);
// Explicit resolved value is what keeps the wipe snapshot accurate;
// see the comment in useThemeSelection for the failure mode.
expect(setThemeMock).toHaveBeenCalledWith('light');
});
it('SYSTEM with a dark system preference resolves to setTheme("dark") + auto on', () => {
themeValue = 'light';
systemThemeValue = 'dark';
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('auto'));
expect(setAutoSwitchMock).toHaveBeenCalledWith(true);
expect(setThemeMock).toHaveBeenCalledWith('dark');
});
it('invokes onApplied inside the same batch, after the state mutations', () => {
themeValue = 'dark';
const onApplied = jest.fn();
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('light', onApplied));
expect(onApplied).toHaveBeenCalledTimes(1);
expect(setThemeMock.mock.invocationCallOrder[0]).toBeLessThan(
onApplied.mock.invocationCallOrder[0],
);
});
it('routes through runThemeTransition when the dark↔light state actually flips', () => {
themeValue = 'dark';
canAnimateMock.mockReturnValue(true);
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('light'));
expect(runTransitionMock).toHaveBeenCalledTimes(1);
expect(setThemeMock).toHaveBeenCalledWith('light');
});
it('skips runThemeTransition when no dark↔light flip happens', () => {
themeValue = 'dark';
canAnimateMock.mockReturnValue(true);
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('dark'));
expect(runTransitionMock).not.toHaveBeenCalled();
// applyChange still ran inline.
expect(setThemeMock).toHaveBeenCalledWith('dark');
});
it('skips runThemeTransition when SYSTEM resolves to the currently-rendered theme', () => {
themeValue = 'dark';
systemThemeValue = 'dark';
canAnimateMock.mockReturnValue(true);
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('auto'));
expect(runTransitionMock).not.toHaveBeenCalled();
expect(setAutoSwitchMock).toHaveBeenCalledWith(true);
expect(setThemeMock).toHaveBeenCalledWith('dark');
});
it('skips runThemeTransition when capability check is false even if the theme flips', () => {
themeValue = 'dark';
canAnimateMock.mockReturnValue(false);
const { result } = renderHook(() => useThemeSelection());
act(() => result.current('light'));
expect(runTransitionMock).not.toHaveBeenCalled();
expect(setThemeMock).toHaveBeenCalledWith('light');
});
});

View File

@@ -2,4 +2,6 @@ export const THEME_MODE = {
LIGHT: 'light',
DARK: 'dark',
SYSTEM: 'auto',
};
} as const;
export type ThemeMode = (typeof THEME_MODE)[keyof typeof THEME_MODE];

View File

@@ -18,7 +18,13 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { THEME_MODE } from './constant';
export const ThemeContext = createContext({
export const ThemeContext = createContext<{
theme: string;
toggleTheme: () => void;
autoSwitch: boolean;
setAutoSwitch: Dispatch<SetStateAction<boolean>>;
setTheme: Dispatch<SetStateAction<string>>;
}>({
theme: THEME_MODE.DARK,
toggleTheme: (): void => {},
autoSwitch: false,

View File

@@ -0,0 +1,67 @@
import { useCallback } from 'react';
import {
canAnimateThemeTransition,
runThemeTransition,
} from 'utils/themeTransition';
import useThemeMode, { useSystemTheme } from './index';
import { THEME_MODE, ThemeMode } from './constant';
type SelectTheme = (value: ThemeMode, onApplied?: () => void) => void;
// Centralises the "apply a theme selection" flow used by MySettings and the
// command palette: figures out whether the visible (dark↔light) theme is
// actually flipping, applies the state change, and — when capable — wraps the
// change in a left→right view-transition wipe.
//
// `value` is one of THEME_MODE.{LIGHT,DARK,SYSTEM}; `onApplied` runs inside the
// same flushSync batch as the theme change (useful for, e.g., closing the
// command palette so its dismissal is part of the captured "new" snapshot).
export function useThemeSelection(): SelectTheme {
const { theme, setTheme, setAutoSwitch } = useThemeMode();
const systemTheme = useSystemTheme();
return useCallback<SelectTheme>(
(value, onApplied) => {
const currentIsDark = theme === THEME_MODE.DARK;
// When switching to SYSTEM, the visible theme flips iff the OS preference
// differs from what we're currently rendering. For explicit LIGHT/DARK,
// resolvedTargetIsDark is just (value === DARK).
const resolvedTargetIsDark =
value === THEME_MODE.SYSTEM
? systemTheme === THEME_MODE.DARK
: value === THEME_MODE.DARK;
const isSystem = value === THEME_MODE.SYSTEM;
// Always push the resolved LIGHT/DARK through setTheme synchronously so
// the View Transition snapshot reflects the new theme. If we relied on
// ThemeProvider's effect (setAutoSwitch → re-render → effect →
// setThemeState), the flip wouldn't be guaranteed to run inside this
// flushSync batch and the wipe would capture old → old, then snap.
const resolvedTheme = resolvedTargetIsDark
? THEME_MODE.DARK
: THEME_MODE.LIGHT;
// runThemeTransition needs a zero-arg callback, so this closure is
// unavoidable. It allocates once per selection — cheap enough that
// micro-optimising it would just obscure the flow.
const apply = (): void => {
setAutoSwitch(isSystem);
setTheme(resolvedTheme);
onApplied?.();
};
const willFlipDarkMode = resolvedTargetIsDark !== currentIsDark;
if (!willFlipDarkMode || !canAnimateThemeTransition()) {
apply();
return;
}
runThemeTransition(apply);
},
[theme, systemTheme, setTheme, setAutoSwitch],
);
}
export default useThemeSelection;

View File

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

View File

@@ -31,12 +31,7 @@ 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,
@@ -91,16 +86,6 @@ 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);
@@ -391,7 +376,7 @@ function SpanDetailsContent({
<div className={styles.tabsSection}>
{/* Step 9: ContentTabs */}
<TabsRoot defaultValue="overview" onValueChange={handleTabChange}>
<TabsRoot defaultValue="overview">
<TabsList variant="secondary">
<TabsTrigger value="overview" variant="secondary">
<Bookmark size={14} /> Overview

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import {
@@ -29,8 +29,6 @@ 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';
@@ -92,35 +90,11 @@ 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, 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],
);
}, [traceID]);
const handlePreviousBtnClick = useCallback((): void => {
if (hasInAppHistory()) {
@@ -193,7 +167,7 @@ function TraceDetailsHeader({
size="icon"
color="secondary"
aria-label="Analytics"
onClick={handleToggleAnalytics}
onClick={(): void => setIsAnalyticsOpen((prev) => !prev)}
>
<ChartPie size={14} />
</Button>
@@ -271,7 +245,6 @@ function TraceDetailsHeader({
<AnalyticsPanel
isOpen={isAnalyticsOpen}
onClose={(): void => setIsAnalyticsOpen(false)}
onTabChange={handleAnalyticsTabChange}
/>
</div>
);

View File

@@ -1,38 +0,0 @@
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

@@ -1,88 +0,0 @@
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

@@ -1,39 +0,0 @@
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,10 +20,7 @@ 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';
@@ -59,14 +56,6 @@ 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() });
@@ -165,46 +154,6 @@ 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) {
@@ -284,12 +233,6 @@ 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,13 +24,12 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,4 +1,4 @@
// Generated from FilterQuery.g4 by ANTLR 4.13.2
// Generated from FilterQuery.g4 by ANTLR 4.13.1
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {
ATN,
@@ -38,13 +38,12 @@ export default class FilterQueryLexer extends Lexer {
public static readonly HAS = 24;
public static readonly HASANY = 25;
public static readonly HASALL = 26;
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 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 EOF = Token.EOF;
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
@@ -69,9 +68,8 @@ export default class FilterQueryLexer extends Lexer {
"AND", "OR",
"HASTOKEN",
"HAS", "HASANY",
"HASALL", "SEARCH",
"BOOL", "NUMBER",
"QUOTED_TEXT",
"HASALL", "BOOL",
"NUMBER", "QUOTED_TEXT",
"KEY", "WS",
"FREETEXT" ];
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
@@ -80,8 +78,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",
"SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS",
"KEY", "WS", "DIGIT", "FREETEXT",
];
@@ -102,122 +100,119 @@ export default class FilterQueryLexer extends Lexer {
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,33,329,6,-1,2,0,
public static readonly _serializedATN: number[] = [4,0,32,320,6,-1,2,0,
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,1,0,
1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,91,8,5,1,6,1,6,1,6,
1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,
1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,
14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,134,8,15,1,16,1,16,1,16,1,16,
1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,151,8,17,1,
18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,
1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,
24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,
1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,210,
8,27,1,28,1,28,1,29,3,29,215,8,29,1,29,4,29,218,8,29,11,29,12,29,219,1,
29,1,29,5,29,224,8,29,10,29,12,29,227,9,29,3,29,229,8,29,1,29,1,29,3,29,
233,8,29,1,29,4,29,236,8,29,11,29,12,29,237,3,29,240,8,29,1,29,3,29,243,
8,29,1,29,1,29,4,29,247,8,29,11,29,12,29,248,1,29,1,29,3,29,253,8,29,1,
29,4,29,256,8,29,11,29,12,29,257,3,29,260,8,29,3,29,262,8,29,1,30,1,30,
1,30,1,30,5,30,268,8,30,10,30,12,30,271,9,30,1,30,1,30,1,30,1,30,1,30,5,
30,278,8,30,10,30,12,30,281,9,30,1,30,3,30,284,8,30,1,31,1,31,5,31,288,
8,31,10,31,12,31,291,9,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,
1,34,1,34,1,34,1,34,1,34,4,34,307,8,34,11,34,12,34,308,5,34,311,8,34,10,
34,12,34,314,9,34,1,35,4,35,317,8,35,11,35,12,35,318,1,35,1,35,1,36,1,36,
1,37,4,37,326,8,37,11,37,12,37,327,0,0,38,1,1,3,2,5,3,7,4,9,5,11,6,13,7,
15,8,17,9,19,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];
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];
private static __ATN: ATN;
public static get _ATN(): ATN {

View File

@@ -1,26 +1,25 @@
// Generated from FilterQuery.g4 by ANTLR 4.13.2
// Generated from FilterQuery.g4 by ANTLR 4.13.1
import {ParseTreeListener} from "antlr4";
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";
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";
/**
@@ -148,16 +147,6 @@ 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,26 +1,25 @@
// Generated from FilterQuery.g4 by ANTLR 4.13.2
// Generated from FilterQuery.g4 by ANTLR 4.13.1
import {ParseTreeVisitor} from 'antlr4';
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";
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";
/**
@@ -103,12 +102,6 @@ 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,7 +4,6 @@ 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';
@@ -13,8 +12,6 @@ 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' },
@@ -37,20 +34,6 @@ 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);
@@ -73,7 +56,7 @@ function DataViewer({
{
type: 'radio-group',
value: viewMode,
onChange: handleViewModeChange,
onChange: (value): void => setViewMode(value as ViewMode),
children: VIEW_MODE_OPTIONS.map((opt) => ({
type: 'radio',
key: opt.value,

View File

@@ -827,3 +827,22 @@ body.ai-assistant-panel-open {
--input-focus-outline-width: 0;
--radius-2: 4px;
}
// Scoped to .theme-wipe-active (toggled on <html> in runThemeTransition) so
// these overrides don't leak into any unrelated view transitions added later.
// We disable the default UA crossfade so the JS-driven clip-path wipe is the
// only visible effect, and stack the new snapshot above the old.
html.theme-wipe-active {
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none;
}
&::view-transition-old(root) {
z-index: 1;
}
&::view-transition-new(root) {
z-index: 2;
}
}

View File

@@ -0,0 +1,193 @@
import {
canAnimateThemeTransition,
runThemeTransition,
THEME_WIPE_ACTIVE_CLASS,
} from '../themeTransition';
type StartVT = (cb: () => void) => {
ready: Promise<void>;
finished: Promise<void>;
};
const installStartViewTransition = (impl?: StartVT): jest.Mock => {
const defaultImpl: StartVT = (cb) => {
cb();
return { ready: Promise.resolve(), finished: Promise.resolve() };
};
const fn = jest.fn(impl ?? defaultImpl);
Object.defineProperty(document, 'startViewTransition', {
configurable: true,
writable: true,
value: fn,
});
return fn;
};
const removeStartViewTransition = (): void => {
Object.defineProperty(document, 'startViewTransition', {
configurable: true,
writable: true,
value: undefined,
});
};
const setReducedMotion = (matches: boolean): void => {
(window.matchMedia as jest.Mock) = jest
.fn()
.mockImplementation((query: string) => ({
matches: query === '(prefers-reduced-motion: reduce)' ? matches : false,
media: query,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
};
describe('canAnimateThemeTransition', () => {
afterEach(() => {
removeStartViewTransition();
});
it('returns false when document.startViewTransition is unavailable', () => {
removeStartViewTransition();
setReducedMotion(false);
expect(canAnimateThemeTransition()).toBe(false);
});
it('returns false when prefers-reduced-motion is reduce', () => {
installStartViewTransition();
setReducedMotion(true);
expect(canAnimateThemeTransition()).toBe(false);
});
it('returns true when API is supported and motion is allowed', () => {
installStartViewTransition();
setReducedMotion(false);
expect(canAnimateThemeTransition()).toBe(true);
});
});
describe('runThemeTransition', () => {
afterEach(() => {
removeStartViewTransition();
document.documentElement.classList.remove(THEME_WIPE_ACTIVE_CLASS);
});
it('falls back to running applyChange directly when API is missing', () => {
removeStartViewTransition();
const applyChange = jest.fn();
runThemeTransition(applyChange);
expect(applyChange).toHaveBeenCalledTimes(1);
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(false);
});
it('invokes startViewTransition and runs applyChange inside its callback', () => {
const startVT = installStartViewTransition();
const applyChange = jest.fn();
runThemeTransition(applyChange);
expect(startVT).toHaveBeenCalledTimes(1);
expect(applyChange).toHaveBeenCalledTimes(1);
});
it('toggles the wipe-active class on <html> for the lifetime of the transition', async () => {
let resolveFinished: () => void = (): void => {};
installStartViewTransition((cb) => {
cb();
return {
ready: Promise.resolve(),
finished: new Promise<void>((resolve) => {
resolveFinished = resolve;
}),
};
});
runThemeTransition(() => undefined);
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(true);
resolveFinished();
await Promise.resolve();
await Promise.resolve();
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(false);
});
it('keeps the wipe-active class through overlapping transitions', async () => {
let resolveA: () => void = (): void => {};
let resolveB: () => void = (): void => {};
let callIndex = 0;
installStartViewTransition((cb) => {
cb();
callIndex += 1;
if (callIndex === 1) {
return {
ready: Promise.resolve(),
finished: new Promise<void>((resolve) => {
resolveA = resolve;
}),
};
}
return {
ready: Promise.resolve(),
finished: new Promise<void>((resolve) => {
resolveB = resolve;
}),
};
});
runThemeTransition(() => undefined);
runThemeTransition(() => undefined);
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(true);
// First transition finishes — class must stay because B is still in flight.
resolveA();
await Promise.resolve();
await Promise.resolve();
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(true);
resolveB();
await Promise.resolve();
await Promise.resolve();
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(false);
});
it('falls back to applyChange and releases the class when startViewTransition throws before its callback runs', () => {
installStartViewTransition(() => {
throw new Error('boom');
});
const applyChange = jest.fn();
runThemeTransition(applyChange);
expect(applyChange).toHaveBeenCalledTimes(1);
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(false);
});
it('does not double-invoke applyChange when startViewTransition throws after its callback runs', () => {
installStartViewTransition((cb) => {
cb();
throw new Error('post-cb');
});
const applyChange = jest.fn();
runThemeTransition(applyChange);
expect(applyChange).toHaveBeenCalledTimes(1);
expect(
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
).toBe(false);
});
});

View File

@@ -0,0 +1,115 @@
import { flushSync } from 'react-dom';
const WIPE_DURATION_MS = 400;
const WIPE_EASING = 'ease-out';
// Toggled on <html> for the duration of the wipe so the CSS overrides
// (animation: none on ::view-transition-{old,new}(root)) don't leak into
// any future, unrelated view transitions in the app.
export const THEME_WIPE_ACTIVE_CLASS = 'theme-wipe-active';
type ViewTransition = {
ready: Promise<void>;
finished: Promise<void>;
};
type DocumentWithVT = Document & {
startViewTransition?: (callback: () => void) => ViewTransition;
};
// Rapid theme switches cancel the in-flight transition and immediately start a
// new one; if we removed the class on the first transition's settled promise,
// we'd strip the CSS override mid-way through the next wipe and the user
// would briefly see the UA crossfade. Refcount so the class only comes off
// once every transition we started has settled.
let wipeActiveRefCount = 0;
const acquireWipeClass = (root: HTMLElement): void => {
wipeActiveRefCount += 1;
root.classList.add(THEME_WIPE_ACTIVE_CLASS);
};
const releaseWipeClass = (root: HTMLElement): void => {
wipeActiveRefCount = Math.max(0, wipeActiveRefCount - 1);
if (wipeActiveRefCount === 0) {
root.classList.remove(THEME_WIPE_ACTIVE_CLASS);
}
};
// Identity of the transition we most recently started. Used to skip the
// .animate() call on a stale transition whose .ready resolved after a newer
// transition has already taken over the ::view-transition-new pseudo-element.
let currentTransition: ViewTransition | null = null;
export function canAnimateThemeTransition(): boolean {
const doc = document as DocumentWithVT;
if (typeof doc.startViewTransition !== 'function') {
return false;
}
return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Runs `applyChange` inside a View Transition and wipes the new theme in from
// left to right via a polygon clip-path on ::view-transition-new(root).
// Callers should gate on canAnimateThemeTransition() first; this is a safe
// no-animation fallback otherwise.
export function runThemeTransition(applyChange: () => void): void {
const doc = document as DocumentWithVT;
if (!doc.startViewTransition) {
applyChange();
return;
}
const root = document.documentElement;
acquireWipeClass(root);
// Some Chromium versions throw if startViewTransition is called while
// another transition is in setup. Track whether the callback ran so we
// don't double-apply if the throw happens mid-callback.
let applied = false;
let transition: ViewTransition;
try {
transition = doc.startViewTransition(() => {
applied = true;
flushSync(applyChange);
});
} catch {
releaseWipeClass(root);
if (!applied) {
applyChange();
}
return;
}
currentTransition = transition;
const from = 'polygon(0 0, 0 0, 0 100%, 0 100%)';
const to = 'polygon(0 0, 100% 0, 100% 100%, 0 100%)';
transition.ready
.then(() => {
// If a newer transition has superseded this one between
// startViewTransition() and `ready` resolving, the browser has
// already cancelled our pseudo-element. Calling .animate() on it now
// would race with the newer transition's own animation.
if (currentTransition !== transition) {
return;
}
root.animate(
{ clipPath: [from, to] },
{
duration: WIPE_DURATION_MS,
easing: WIPE_EASING,
pseudoElement: '::view-transition-new(root)',
},
);
})
.catch(() => {
// Transition cancelled — applyChange has already run.
});
const cleanup = (): void => {
if (currentTransition === transition) {
currentTransition = null;
}
releaseWipeClass(root);
};
transition.finished.then(cleanup).catch(cleanup);
}

View File

@@ -32,12 +32,11 @@ unaryExpression
;
// Primary constructs: grouped expressions, a comparison (key op value),
// a function call, a search call, or a full-text string
// a function call, or a full-text string
primary
: LPAREN orExpression RPAREN
| comparison
| functionCall
| searchCall
| fullText
| key
| value
@@ -111,14 +110,6 @@ 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 )*
@@ -193,7 +184,6 @@ HASTOKEN : [Hh][Aa][Ss][Tt][Oo][Kk][Ee][Nn];
HAS : [Hh][Aa][Ss] ;
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
// Potential boolean constants
BOOL

View File

@@ -173,8 +173,6 @@ 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 {
@@ -366,16 +364,6 @@ 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,
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FullTextColumn: &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,
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FieldKeys: keys,
StartNs: querybuilder.ToNanoSecs(uint64(startMillis)),
EndNs: querybuilder.ToNanoSecs(uint64(endMillis)),

View File

@@ -104,4 +104,3 @@ func (c *conditionBuilder) ConditionFor(
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
}

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,
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels", FieldContext: telemetrytypes.FieldContextAttribute},
FullTextColumn: &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, links as references
external_http_method, external_http_url, response_status_code
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", "links as references",
"external_http_method", "external_http_url", "response_status_code",
)
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,13 +24,12 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -93,12 +93,6 @@ func (s *BaseFilterQueryListener) EnterFunctionCall(ctx *FunctionCallContext) {}
// ExitFunctionCall is called when production functionCall is exited.
func (s *BaseFilterQueryListener) ExitFunctionCall(ctx *FunctionCallContext) {}
// EnterSearchCall is called when production searchCall is entered.
func (s *BaseFilterQueryListener) EnterSearchCall(ctx *SearchCallContext) {}
// ExitSearchCall is called when production searchCall is exited.
func (s *BaseFilterQueryListener) ExitSearchCall(ctx *SearchCallContext) {}
// EnterFunctionParamList is called when production functionParamList is entered.
func (s *BaseFilterQueryListener) EnterFunctionParamList(ctx *FunctionParamListContext) {}

View File

@@ -56,10 +56,6 @@ func (v *BaseFilterQueryVisitor) VisitFunctionCall(ctx *FunctionCallContext) int
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitSearchCall(ctx *SearchCallContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitFunctionParamList(ctx *FunctionParamListContext) interface{} {
return v.VisitChildren(ctx)
}

View File

@@ -4,9 +4,10 @@ package parser
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
"sync"
"unicode"
"github.com/antlr4-go/antlr/v4"
)
// Suppress unused import error
@@ -50,174 +51,170 @@ func filterquerylexerLexerInit() {
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
"HASALL", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
}
staticData.RuleNames = []string{
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"HASALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 33, 329, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 0, 32, 320, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1,
13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15,
1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 134, 8, 15, 1, 16, 1, 16, 1, 16, 1,
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
1, 17, 3, 17, 151, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1,
19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22,
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1,
24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 210,
8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 215, 8, 29, 1, 29, 4, 29, 218, 8, 29,
11, 29, 12, 29, 219, 1, 29, 1, 29, 5, 29, 224, 8, 29, 10, 29, 12, 29, 227,
9, 29, 3, 29, 229, 8, 29, 1, 29, 1, 29, 3, 29, 233, 8, 29, 1, 29, 4, 29,
236, 8, 29, 11, 29, 12, 29, 237, 3, 29, 240, 8, 29, 1, 29, 3, 29, 243,
8, 29, 1, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 1, 29, 1,
29, 3, 29, 253, 8, 29, 1, 29, 4, 29, 256, 8, 29, 11, 29, 12, 29, 257, 3,
29, 260, 8, 29, 3, 29, 262, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 268,
8, 30, 10, 30, 12, 30, 271, 9, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 5,
30, 278, 8, 30, 10, 30, 12, 30, 281, 9, 30, 1, 30, 3, 30, 284, 8, 30, 1,
31, 1, 31, 5, 31, 288, 8, 31, 10, 31, 12, 31, 291, 9, 31, 1, 32, 1, 32,
1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1,
34, 1, 34, 4, 34, 307, 8, 34, 11, 34, 12, 34, 308, 5, 34, 311, 8, 34, 10,
34, 12, 34, 314, 9, 34, 1, 35, 4, 35, 317, 8, 35, 11, 35, 12, 35, 318,
1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 4, 37, 326, 8, 37, 11, 37, 12, 37, 327,
0, 0, 38, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
7, 36, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5,
1, 5, 1, 5, 3, 5, 89, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1,
8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1,
12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14,
1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1,
15, 1, 15, 3, 15, 132, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16,
1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 149,
8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1,
20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22,
1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1,
24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25,
1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 201,
8, 26, 1, 27, 1, 27, 1, 28, 3, 28, 206, 8, 28, 1, 28, 4, 28, 209, 8, 28,
11, 28, 12, 28, 210, 1, 28, 1, 28, 5, 28, 215, 8, 28, 10, 28, 12, 28, 218,
9, 28, 3, 28, 220, 8, 28, 1, 28, 1, 28, 3, 28, 224, 8, 28, 1, 28, 4, 28,
227, 8, 28, 11, 28, 12, 28, 228, 3, 28, 231, 8, 28, 1, 28, 3, 28, 234,
8, 28, 1, 28, 1, 28, 4, 28, 238, 8, 28, 11, 28, 12, 28, 239, 1, 28, 1,
28, 3, 28, 244, 8, 28, 1, 28, 4, 28, 247, 8, 28, 11, 28, 12, 28, 248, 3,
28, 251, 8, 28, 3, 28, 253, 8, 28, 1, 29, 1, 29, 1, 29, 1, 29, 5, 29, 259,
8, 29, 10, 29, 12, 29, 262, 9, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 5,
29, 269, 8, 29, 10, 29, 12, 29, 272, 9, 29, 1, 29, 3, 29, 275, 8, 29, 1,
30, 1, 30, 5, 30, 279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 31, 1, 31,
1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1,
33, 1, 33, 4, 33, 298, 8, 33, 11, 33, 12, 33, 299, 5, 33, 302, 8, 33, 10,
33, 12, 33, 305, 9, 33, 1, 34, 4, 34, 308, 8, 34, 11, 34, 12, 34, 309,
1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 4, 36, 317, 8, 36, 11, 36, 12, 36, 318,
0, 0, 37, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37,
19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55,
28, 57, 0, 59, 29, 61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0,
75, 33, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0,
75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84,
84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88,
88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71,
71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79,
111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104,
104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102,
102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92,
4, 0, 35, 36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64,
90, 95, 95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57,
8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 353,
0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0,
0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0,
0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0,
0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1,
0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39,
1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0,
47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0,
0, 55, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0,
0, 0, 71, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0,
0, 0, 5, 81, 1, 0, 0, 0, 7, 83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90,
1, 0, 0, 0, 13, 92, 1, 0, 0, 0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0,
19, 100, 1, 0, 0, 0, 21, 103, 1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108,
1, 0, 0, 0, 27, 113, 1, 0, 0, 0, 29, 119, 1, 0, 0, 0, 31, 127, 1, 0, 0,
0, 33, 135, 1, 0, 0, 0, 35, 142, 1, 0, 0, 0, 37, 152, 1, 0, 0, 0, 39, 155,
1, 0, 0, 0, 41, 159, 1, 0, 0, 0, 43, 163, 1, 0, 0, 0, 45, 166, 1, 0, 0,
0, 47, 175, 1, 0, 0, 0, 49, 179, 1, 0, 0, 0, 51, 186, 1, 0, 0, 0, 53, 193,
1, 0, 0, 0, 55, 209, 1, 0, 0, 0, 57, 211, 1, 0, 0, 0, 59, 261, 1, 0, 0,
0, 61, 283, 1, 0, 0, 0, 63, 285, 1, 0, 0, 0, 65, 292, 1, 0, 0, 0, 67, 295,
1, 0, 0, 0, 69, 299, 1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 322, 1, 0, 0,
0, 75, 325, 1, 0, 0, 0, 77, 78, 5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80,
5, 41, 0, 0, 80, 4, 1, 0, 0, 0, 81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0,
83, 84, 5, 93, 0, 0, 84, 8, 1, 0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1,
0, 0, 0, 87, 91, 5, 61, 0, 0, 88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0,
90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5,
33, 0, 0, 93, 94, 5, 61, 0, 0, 94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0,
96, 97, 5, 62, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1,
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0,
0, 103, 104, 5, 62, 0, 0, 104, 22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
107, 5, 61, 0, 0, 107, 24, 1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110,
7, 1, 0, 0, 110, 111, 7, 2, 0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1, 0, 0,
116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 28, 1, 0, 0, 0, 119, 120,
7, 4, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 5, 0, 0, 122, 123, 7, 6,
0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 3, 0, 0, 125, 126, 7, 7, 0, 0,
126, 30, 1, 0, 0, 0, 127, 128, 7, 3, 0, 0, 128, 129, 7, 8, 0, 0, 129, 130,
7, 1, 0, 0, 130, 131, 7, 9, 0, 0, 131, 133, 7, 5, 0, 0, 132, 134, 7, 9,
0, 0, 133, 132, 1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 32, 1, 0, 0, 0,
135, 136, 7, 10, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 11, 0, 0, 138,
139, 7, 3, 0, 0, 139, 140, 7, 8, 0, 0, 140, 141, 7, 12, 0, 0, 141, 34,
1, 0, 0, 0, 142, 143, 7, 13, 0, 0, 143, 144, 7, 14, 0, 0, 144, 145, 7,
7, 0, 0, 145, 146, 7, 5, 0, 0, 146, 147, 7, 15, 0, 0, 147, 148, 7, 1, 0,
0, 148, 150, 7, 7, 0, 0, 149, 151, 7, 9, 0, 0, 150, 149, 1, 0, 0, 0, 150,
151, 1, 0, 0, 0, 151, 36, 1, 0, 0, 0, 152, 153, 7, 1, 0, 0, 153, 154, 7,
7, 0, 0, 154, 38, 1, 0, 0, 0, 155, 156, 7, 7, 0, 0, 156, 157, 7, 14, 0,
0, 157, 158, 7, 5, 0, 0, 158, 40, 1, 0, 0, 0, 159, 160, 7, 15, 0, 0, 160,
161, 7, 7, 0, 0, 161, 162, 7, 16, 0, 0, 162, 42, 1, 0, 0, 0, 163, 164,
7, 14, 0, 0, 164, 165, 7, 10, 0, 0, 165, 44, 1, 0, 0, 0, 166, 167, 7, 17,
0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 5, 0, 0,
170, 171, 7, 14, 0, 0, 171, 172, 7, 2, 0, 0, 172, 173, 7, 3, 0, 0, 173,
174, 7, 7, 0, 0, 174, 46, 1, 0, 0, 0, 175, 176, 7, 17, 0, 0, 176, 177,
7, 15, 0, 0, 177, 178, 7, 9, 0, 0, 178, 48, 1, 0, 0, 0, 179, 180, 7, 17,
0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 9, 0, 0, 182, 183, 7, 15, 0,
0, 183, 184, 7, 7, 0, 0, 184, 185, 7, 18, 0, 0, 185, 50, 1, 0, 0, 0, 186,
187, 7, 17, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 9, 0, 0, 189, 190,
7, 15, 0, 0, 190, 191, 7, 0, 0, 0, 191, 192, 7, 0, 0, 0, 192, 52, 1, 0,
0, 0, 193, 194, 7, 9, 0, 0, 194, 195, 7, 3, 0, 0, 195, 196, 7, 15, 0, 0,
196, 197, 7, 10, 0, 0, 197, 198, 7, 13, 0, 0, 198, 199, 7, 17, 0, 0, 199,
54, 1, 0, 0, 0, 200, 201, 7, 5, 0, 0, 201, 202, 7, 10, 0, 0, 202, 203,
7, 19, 0, 0, 203, 210, 7, 3, 0, 0, 204, 205, 7, 20, 0, 0, 205, 206, 7,
15, 0, 0, 206, 207, 7, 0, 0, 0, 207, 208, 7, 9, 0, 0, 208, 210, 7, 3, 0,
0, 209, 200, 1, 0, 0, 0, 209, 204, 1, 0, 0, 0, 210, 56, 1, 0, 0, 0, 211,
212, 7, 21, 0, 0, 212, 58, 1, 0, 0, 0, 213, 215, 3, 57, 28, 0, 214, 213,
1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 217, 1, 0, 0, 0, 216, 218, 3, 73,
36, 0, 217, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 217, 1, 0, 0, 0,
219, 220, 1, 0, 0, 0, 220, 228, 1, 0, 0, 0, 221, 225, 5, 46, 0, 0, 222,
224, 3, 73, 36, 0, 223, 222, 1, 0, 0, 0, 224, 227, 1, 0, 0, 0, 225, 223,
1, 0, 0, 0, 225, 226, 1, 0, 0, 0, 226, 229, 1, 0, 0, 0, 227, 225, 1, 0,
0, 0, 228, 221, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0,
230, 232, 7, 3, 0, 0, 231, 233, 3, 57, 28, 0, 232, 231, 1, 0, 0, 0, 232,
233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 73, 36, 0, 235, 234,
1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0,
0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0,
240, 262, 1, 0, 0, 0, 241, 243, 3, 57, 28, 0, 242, 241, 1, 0, 0, 0, 242,
243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 5, 46, 0, 0, 245, 247,
3, 73, 36, 0, 246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1,
0, 0, 0, 248, 249, 1, 0, 0, 0, 249, 259, 1, 0, 0, 0, 250, 252, 7, 3, 0,
0, 251, 253, 3, 57, 28, 0, 252, 251, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0,
253, 255, 1, 0, 0, 0, 254, 256, 3, 73, 36, 0, 255, 254, 1, 0, 0, 0, 256,
257, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 260,
1, 0, 0, 0, 259, 250, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0,
0, 0, 261, 214, 1, 0, 0, 0, 261, 242, 1, 0, 0, 0, 262, 60, 1, 0, 0, 0,
263, 269, 5, 34, 0, 0, 264, 268, 8, 22, 0, 0, 265, 266, 5, 92, 0, 0, 266,
268, 9, 0, 0, 0, 267, 264, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 271,
1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 272, 1, 0,
0, 0, 271, 269, 1, 0, 0, 0, 272, 284, 5, 34, 0, 0, 273, 279, 5, 39, 0,
0, 274, 278, 8, 23, 0, 0, 275, 276, 5, 92, 0, 0, 276, 278, 9, 0, 0, 0,
277, 274, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 1, 0, 0, 0, 279,
277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 282, 1, 0, 0, 0, 281, 279,
1, 0, 0, 0, 282, 284, 5, 39, 0, 0, 283, 263, 1, 0, 0, 0, 283, 273, 1, 0,
0, 0, 284, 62, 1, 0, 0, 0, 285, 289, 7, 24, 0, 0, 286, 288, 7, 25, 0, 0,
287, 286, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289,
290, 1, 0, 0, 0, 290, 64, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 293, 5,
91, 0, 0, 293, 294, 5, 93, 0, 0, 294, 66, 1, 0, 0, 0, 295, 296, 5, 91,
0, 0, 296, 297, 5, 42, 0, 0, 297, 298, 5, 93, 0, 0, 298, 68, 1, 0, 0, 0,
299, 312, 3, 63, 31, 0, 300, 301, 5, 46, 0, 0, 301, 311, 3, 63, 31, 0,
302, 311, 3, 65, 32, 0, 303, 311, 3, 67, 33, 0, 304, 306, 5, 46, 0, 0,
305, 307, 3, 73, 36, 0, 306, 305, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308,
306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 300,
1, 0, 0, 0, 310, 302, 1, 0, 0, 0, 310, 303, 1, 0, 0, 0, 310, 304, 1, 0,
0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0,
313, 70, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 317, 7, 26, 0, 0, 316,
315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0, 0, 318, 319,
1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 321, 6, 35, 0, 0, 321, 72, 1, 0,
0, 0, 322, 323, 7, 27, 0, 0, 323, 74, 1, 0, 0, 0, 324, 326, 8, 28, 0, 0,
325, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 327,
328, 1, 0, 0, 0, 328, 76, 1, 0, 0, 0, 29, 0, 90, 133, 150, 209, 214, 219,
225, 228, 232, 237, 239, 242, 248, 252, 257, 259, 261, 267, 269, 277, 279,
283, 289, 308, 310, 312, 318, 327, 1, 6, 0, 0,
0, 57, 28, 59, 29, 61, 0, 63, 0, 65, 0, 67, 30, 69, 31, 71, 0, 73, 32,
1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75,
107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84, 116,
116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88, 120,
120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103,
103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111, 111,
2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2,
0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 2,
0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0, 35,
36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64, 90, 95,
95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0,
9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 344, 0,
1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0,
9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0,
0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0,
0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0,
0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1,
0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47,
1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0,
57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0,
0, 73, 1, 0, 0, 0, 1, 75, 1, 0, 0, 0, 3, 77, 1, 0, 0, 0, 5, 79, 1, 0, 0,
0, 7, 81, 1, 0, 0, 0, 9, 83, 1, 0, 0, 0, 11, 88, 1, 0, 0, 0, 13, 90, 1,
0, 0, 0, 15, 93, 1, 0, 0, 0, 17, 96, 1, 0, 0, 0, 19, 98, 1, 0, 0, 0, 21,
101, 1, 0, 0, 0, 23, 103, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 111, 1,
0, 0, 0, 29, 117, 1, 0, 0, 0, 31, 125, 1, 0, 0, 0, 33, 133, 1, 0, 0, 0,
35, 140, 1, 0, 0, 0, 37, 150, 1, 0, 0, 0, 39, 153, 1, 0, 0, 0, 41, 157,
1, 0, 0, 0, 43, 161, 1, 0, 0, 0, 45, 164, 1, 0, 0, 0, 47, 173, 1, 0, 0,
0, 49, 177, 1, 0, 0, 0, 51, 184, 1, 0, 0, 0, 53, 200, 1, 0, 0, 0, 55, 202,
1, 0, 0, 0, 57, 252, 1, 0, 0, 0, 59, 274, 1, 0, 0, 0, 61, 276, 1, 0, 0,
0, 63, 283, 1, 0, 0, 0, 65, 286, 1, 0, 0, 0, 67, 290, 1, 0, 0, 0, 69, 307,
1, 0, 0, 0, 71, 313, 1, 0, 0, 0, 73, 316, 1, 0, 0, 0, 75, 76, 5, 40, 0,
0, 76, 2, 1, 0, 0, 0, 77, 78, 5, 41, 0, 0, 78, 4, 1, 0, 0, 0, 79, 80, 5,
91, 0, 0, 80, 6, 1, 0, 0, 0, 81, 82, 5, 93, 0, 0, 82, 8, 1, 0, 0, 0, 83,
84, 5, 44, 0, 0, 84, 10, 1, 0, 0, 0, 85, 89, 5, 61, 0, 0, 86, 87, 5, 61,
0, 0, 87, 89, 5, 61, 0, 0, 88, 85, 1, 0, 0, 0, 88, 86, 1, 0, 0, 0, 89,
12, 1, 0, 0, 0, 90, 91, 5, 33, 0, 0, 91, 92, 5, 61, 0, 0, 92, 14, 1, 0,
0, 0, 93, 94, 5, 60, 0, 0, 94, 95, 5, 62, 0, 0, 95, 16, 1, 0, 0, 0, 96,
97, 5, 60, 0, 0, 97, 18, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 100, 5, 61,
0, 0, 100, 20, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 22, 1, 0, 0, 0,
103, 104, 5, 62, 0, 0, 104, 105, 5, 61, 0, 0, 105, 24, 1, 0, 0, 0, 106,
107, 7, 0, 0, 0, 107, 108, 7, 1, 0, 0, 108, 109, 7, 2, 0, 0, 109, 110,
7, 3, 0, 0, 110, 26, 1, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 2, 0, 0, 115, 116, 7, 3, 0, 0,
116, 28, 1, 0, 0, 0, 117, 118, 7, 4, 0, 0, 118, 119, 7, 3, 0, 0, 119, 120,
7, 5, 0, 0, 120, 121, 7, 6, 0, 0, 121, 122, 7, 3, 0, 0, 122, 123, 7, 3,
0, 0, 123, 124, 7, 7, 0, 0, 124, 30, 1, 0, 0, 0, 125, 126, 7, 3, 0, 0,
126, 127, 7, 8, 0, 0, 127, 128, 7, 1, 0, 0, 128, 129, 7, 9, 0, 0, 129,
131, 7, 5, 0, 0, 130, 132, 7, 9, 0, 0, 131, 130, 1, 0, 0, 0, 131, 132,
1, 0, 0, 0, 132, 32, 1, 0, 0, 0, 133, 134, 7, 10, 0, 0, 134, 135, 7, 3,
0, 0, 135, 136, 7, 11, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 8, 0, 0,
138, 139, 7, 12, 0, 0, 139, 34, 1, 0, 0, 0, 140, 141, 7, 13, 0, 0, 141,
142, 7, 14, 0, 0, 142, 143, 7, 7, 0, 0, 143, 144, 7, 5, 0, 0, 144, 145,
7, 15, 0, 0, 145, 146, 7, 1, 0, 0, 146, 148, 7, 7, 0, 0, 147, 149, 7, 9,
0, 0, 148, 147, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 36, 1, 0, 0, 0,
150, 151, 7, 1, 0, 0, 151, 152, 7, 7, 0, 0, 152, 38, 1, 0, 0, 0, 153, 154,
7, 7, 0, 0, 154, 155, 7, 14, 0, 0, 155, 156, 7, 5, 0, 0, 156, 40, 1, 0,
0, 0, 157, 158, 7, 15, 0, 0, 158, 159, 7, 7, 0, 0, 159, 160, 7, 16, 0,
0, 160, 42, 1, 0, 0, 0, 161, 162, 7, 14, 0, 0, 162, 163, 7, 10, 0, 0, 163,
44, 1, 0, 0, 0, 164, 165, 7, 17, 0, 0, 165, 166, 7, 15, 0, 0, 166, 167,
7, 9, 0, 0, 167, 168, 7, 5, 0, 0, 168, 169, 7, 14, 0, 0, 169, 170, 7, 2,
0, 0, 170, 171, 7, 3, 0, 0, 171, 172, 7, 7, 0, 0, 172, 46, 1, 0, 0, 0,
173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176, 7, 9, 0, 0, 176,
48, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15, 0, 0, 179, 180,
7, 9, 0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 7, 0, 0, 182, 183, 7, 18,
0, 0, 183, 50, 1, 0, 0, 0, 184, 185, 7, 17, 0, 0, 185, 186, 7, 15, 0, 0,
186, 187, 7, 9, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 0, 0, 0, 189,
190, 7, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 5, 0, 0, 192, 193, 7,
10, 0, 0, 193, 194, 7, 19, 0, 0, 194, 201, 7, 3, 0, 0, 195, 196, 7, 20,
0, 0, 196, 197, 7, 15, 0, 0, 197, 198, 7, 0, 0, 0, 198, 199, 7, 9, 0, 0,
199, 201, 7, 3, 0, 0, 200, 191, 1, 0, 0, 0, 200, 195, 1, 0, 0, 0, 201,
54, 1, 0, 0, 0, 202, 203, 7, 21, 0, 0, 203, 56, 1, 0, 0, 0, 204, 206, 3,
55, 27, 0, 205, 204, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 208, 1, 0,
0, 0, 207, 209, 3, 71, 35, 0, 208, 207, 1, 0, 0, 0, 209, 210, 1, 0, 0,
0, 210, 208, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 219, 1, 0, 0, 0, 212,
216, 5, 46, 0, 0, 213, 215, 3, 71, 35, 0, 214, 213, 1, 0, 0, 0, 215, 218,
1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 220, 1, 0,
0, 0, 218, 216, 1, 0, 0, 0, 219, 212, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0,
220, 230, 1, 0, 0, 0, 221, 223, 7, 3, 0, 0, 222, 224, 3, 55, 27, 0, 223,
222, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226, 1, 0, 0, 0, 225, 227,
3, 71, 35, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 226, 1,
0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 231, 1, 0, 0, 0, 230, 221, 1, 0, 0,
0, 230, 231, 1, 0, 0, 0, 231, 253, 1, 0, 0, 0, 232, 234, 3, 55, 27, 0,
233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235,
237, 5, 46, 0, 0, 236, 238, 3, 71, 35, 0, 237, 236, 1, 0, 0, 0, 238, 239,
1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 250, 1, 0,
0, 0, 241, 243, 7, 3, 0, 0, 242, 244, 3, 55, 27, 0, 243, 242, 1, 0, 0,
0, 243, 244, 1, 0, 0, 0, 244, 246, 1, 0, 0, 0, 245, 247, 3, 71, 35, 0,
246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1, 0, 0, 0, 248,
249, 1, 0, 0, 0, 249, 251, 1, 0, 0, 0, 250, 241, 1, 0, 0, 0, 250, 251,
1, 0, 0, 0, 251, 253, 1, 0, 0, 0, 252, 205, 1, 0, 0, 0, 252, 233, 1, 0,
0, 0, 253, 58, 1, 0, 0, 0, 254, 260, 5, 34, 0, 0, 255, 259, 8, 22, 0, 0,
256, 257, 5, 92, 0, 0, 257, 259, 9, 0, 0, 0, 258, 255, 1, 0, 0, 0, 258,
256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261,
1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 275, 5, 34,
0, 0, 264, 270, 5, 39, 0, 0, 265, 269, 8, 23, 0, 0, 266, 267, 5, 92, 0,
0, 267, 269, 9, 0, 0, 0, 268, 265, 1, 0, 0, 0, 268, 266, 1, 0, 0, 0, 269,
272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273,
1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 39, 0, 0, 274, 254, 1, 0,
0, 0, 274, 264, 1, 0, 0, 0, 275, 60, 1, 0, 0, 0, 276, 280, 7, 24, 0, 0,
277, 279, 7, 25, 0, 0, 278, 277, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0, 280,
278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 62, 1, 0, 0, 0, 282, 280, 1,
0, 0, 0, 283, 284, 5, 91, 0, 0, 284, 285, 5, 93, 0, 0, 285, 64, 1, 0, 0,
0, 286, 287, 5, 91, 0, 0, 287, 288, 5, 42, 0, 0, 288, 289, 5, 93, 0, 0,
289, 66, 1, 0, 0, 0, 290, 303, 3, 61, 30, 0, 291, 292, 5, 46, 0, 0, 292,
302, 3, 61, 30, 0, 293, 302, 3, 63, 31, 0, 294, 302, 3, 65, 32, 0, 295,
297, 5, 46, 0, 0, 296, 298, 3, 71, 35, 0, 297, 296, 1, 0, 0, 0, 298, 299,
1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0,
0, 0, 301, 291, 1, 0, 0, 0, 301, 293, 1, 0, 0, 0, 301, 294, 1, 0, 0, 0,
301, 295, 1, 0, 0, 0, 302, 305, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 303,
304, 1, 0, 0, 0, 304, 68, 1, 0, 0, 0, 305, 303, 1, 0, 0, 0, 306, 308, 7,
26, 0, 0, 307, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 307, 1, 0, 0,
0, 309, 310, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 6, 34, 0, 0, 312,
70, 1, 0, 0, 0, 313, 314, 7, 27, 0, 0, 314, 72, 1, 0, 0, 0, 315, 317, 8,
28, 0, 0, 316, 315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0,
0, 318, 319, 1, 0, 0, 0, 319, 74, 1, 0, 0, 0, 29, 0, 88, 131, 148, 200,
205, 210, 216, 219, 223, 228, 230, 233, 239, 243, 248, 250, 252, 258, 260,
268, 270, 274, 280, 299, 301, 303, 309, 318, 1, 6, 0, 0,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
@@ -284,11 +281,10 @@ const (
FilterQueryLexerHAS = 24
FilterQueryLexerHASANY = 25
FilterQueryLexerHASALL = 26
FilterQueryLexerSEARCH = 27
FilterQueryLexerBOOL = 28
FilterQueryLexerNUMBER = 29
FilterQueryLexerQUOTED_TEXT = 30
FilterQueryLexerKEY = 31
FilterQueryLexerWS = 32
FilterQueryLexerFREETEXT = 33
FilterQueryLexerBOOL = 27
FilterQueryLexerNUMBER = 28
FilterQueryLexerQUOTED_TEXT = 29
FilterQueryLexerKEY = 30
FilterQueryLexerWS = 31
FilterQueryLexerFREETEXT = 32
)

View File

@@ -44,9 +44,6 @@ type FilterQueryListener interface {
// EnterFunctionCall is called when entering the functionCall production.
EnterFunctionCall(c *FunctionCallContext)
// EnterSearchCall is called when entering the searchCall production.
EnterSearchCall(c *SearchCallContext)
// EnterFunctionParamList is called when entering the functionParamList production.
EnterFunctionParamList(c *FunctionParamListContext)
@@ -98,9 +95,6 @@ type FilterQueryListener interface {
// ExitFunctionCall is called when exiting the functionCall production.
ExitFunctionCall(c *FunctionCallContext)
// ExitSearchCall is called when exiting the searchCall production.
ExitSearchCall(c *SearchCallContext)
// ExitFunctionParamList is called when exiting the functionParamList production.
ExitFunctionParamList(c *FunctionParamListContext)

File diff suppressed because it is too large Load Diff

View File

@@ -44,9 +44,6 @@ type FilterQueryVisitor interface {
// Visit a parse tree produced by FilterQueryParser#functionCall.
VisitFunctionCall(ctx *FunctionCallContext) interface{}
// Visit a parse tree produced by FilterQueryParser#searchCall.
VisitSearchCall(ctx *SearchCallContext) interface{}
// Visit a parse tree produced by FilterQueryParser#functionParamList.
VisitFunctionParamList(ctx *FunctionParamListContext) interface{}

View File

@@ -120,7 +120,6 @@ func newProvider(
logAggExprRewriter,
telemetrylogs.DefaultFullTextColumn,
telemetrylogs.GetBodyJSONKey,
logConditionBuilder.ConditionForSearch,
flagger,
)

View File

@@ -88,7 +88,6 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
logAggExprRewriter,
telemetrylogs.DefaultFullTextColumn,
telemetrylogs.GetBodyJSONKey,
logConditionBuilder.ConditionForSearch,
fl,
)

View File

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

View File

@@ -8,12 +8,6 @@ const (
// BodyFullTextSearchDefaultWarning is emitted when a full-text search or "body" searches are hit
// with New JSON Body enhancements.
BodyFullTextSearchDefaultWarning = "Full text searches default to `body.message:string`. Use `body.<key>` to search a different field inside body"
// 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>"
// 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()", "SEARCH": "search()",
"HASTOKEN": "hasToken()",
// literals / identifiers
"NUMBER": "number",

View File

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

View File

@@ -34,12 +34,11 @@ type filterExpressionVisitor struct {
errors []string
mainErrorURL string
builder *sqlbuilder.SelectBuilder
freeTextColumn *telemetrytypes.TelemetryFieldKey
fullTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
bodyJSONEnabled bool
skipResourceFilter bool
skipFreeTextFilter bool
skipFullTextSearch bool
skipFullTextFilter bool
skipFunctionCalls bool
ignoreNotFoundKeys bool
variables map[string]qbtypes.VariableItem
@@ -47,7 +46,6 @@ type filterExpressionVisitor struct {
keysWithWarnings map[string]bool
startNs uint64
endNs uint64
ftsCondition qbtypes.FTSConditionFunc
}
type FilterExprVisitorOpts struct {
@@ -57,13 +55,11 @@ type FilterExprVisitorOpts struct {
ConditionBuilder qbtypes.ConditionBuilder
FieldKeys map[string][]*telemetrytypes.TelemetryFieldKey
Builder *sqlbuilder.SelectBuilder
FreeTextColumn *telemetrytypes.TelemetryFieldKey
FullTextColumn *telemetrytypes.TelemetryFieldKey
JsonKeyToKey qbtypes.JsonKeyToFieldFunc
FTSCondition qbtypes.FTSConditionFunc
BodyJSONEnabled bool
SkipResourceFilter bool
SkipFreeTextFilter bool
SkipFullTextSearch bool
SkipFullTextFilter bool
SkipFunctionCalls bool
IgnoreNotFoundKeys bool
Variables map[string]qbtypes.VariableItem
@@ -80,13 +76,11 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
conditionBuilder: opts.ConditionBuilder,
fieldKeys: opts.FieldKeys,
builder: opts.Builder,
freeTextColumn: opts.FreeTextColumn,
fullTextColumn: opts.FullTextColumn,
jsonKeyToKey: opts.JsonKeyToKey,
ftsCondition: opts.FTSCondition,
bodyJSONEnabled: opts.BodyJSONEnabled,
skipResourceFilter: opts.SkipResourceFilter,
skipFreeTextFilter: opts.SkipFreeTextFilter,
skipFullTextSearch: opts.SkipFullTextSearch,
skipFullTextFilter: opts.SkipFullTextFilter,
skipFunctionCalls: opts.SkipFunctionCalls,
ignoreNotFoundKeys: opts.IgnoreNotFoundKeys,
variables: opts.Variables,
@@ -221,8 +215,6 @@ 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:
@@ -335,23 +327,20 @@ 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.skipFreeTextFilter {
if v.skipFullTextFilter {
return SkipConditionLiteral
}
if v.freeTextColumn == nil {
v.errors = append(v.errors, "free text search is not supported")
if v.fullTextColumn == nil {
v.errors = append(v.errors, "full text search is not supported")
return ErrorConditionLiteral
}
child := ctx.GetChild(0)
var searchText string
if keyCtx, ok := child.(*grammar.KeyContext); ok {
@@ -371,13 +360,12 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
return ErrorConditionLiteral
}
}
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.freeTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder)
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.fullTextColumn, 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.freeTextColumn.Name == "body" {
if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
}
@@ -711,7 +699,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.skipFreeTextFilter {
if v.skipFullTextFilter {
// 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.
@@ -726,71 +714,24 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
text = ctx.FREETEXT().GetText()
}
if v.freeTextColumn == nil {
v.errors = append(v.errors, "free text search is not supported")
if v.fullTextColumn == nil {
v.errors = append(v.errors, "full text search is not supported")
return ErrorConditionLiteral
}
cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.freeTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.fullTextColumn, 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.freeTextColumn.Name == "body" {
if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
}
return cond
}
// VisitSearchCall handles the search() function call.
func (v *filterExpressionVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
if v.skipFunctionCalls || v.skipFullTextSearch {
return SkipConditionLiteral
}
// ftsCondition nil means search() is not enabled for this signal.
// Only log statement builders set these; traces/metrics leave them nil.
if v.ftsCondition == nil {
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)
cond, err := v.ftsCondition(v.context, formattedText, v.builder)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("search() could not build condition: %s", err.Error()))
return ErrorConditionLiteral
}
v.warnings = append(v.warnings, FullTextSearchDefaultWarning)
return cond
}
// VisitFunctionCall handles function calls like has(), hasAny(), hasAll(), hasToken().
// VisitFunctionCall handles function calls like has(), hasAny(), etc.
func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any {
if v.skipFunctionCalls {
return SkipConditionLiteral

View File

@@ -755,6 +755,7 @@ func (b *conditionBuilder) ConditionFor(
_ any,
_ *sqlbuilder.SelectBuilder,
) (string, error) {
return fmt.Sprintf("%s_cond", key.Name), nil
}
@@ -797,7 +798,7 @@ func visitComparisonOpts(t *testing.T) (rsbOpts, sbOpts FilterExprVisitorOpts) {
ConditionBuilder: &resourceConditionBuilder{},
Variables: allVariable,
SkipResourceFilter: false,
SkipFreeTextFilter: true,
SkipFullTextFilter: true,
SkipFunctionCalls: true,
IgnoreNotFoundKeys: true,
}
@@ -807,10 +808,10 @@ func visitComparisonOpts(t *testing.T) (rsbOpts, sbOpts FilterExprVisitorOpts) {
ConditionBuilder: &conditionBuilder{},
Variables: allVariable,
SkipResourceFilter: true,
SkipFreeTextFilter: false,
SkipFullTextFilter: false,
SkipFunctionCalls: false,
IgnoreNotFoundKeys: false,
FreeTextColumn: bodyCol,
FullTextColumn: bodyCol,
}
return
}
@@ -1271,111 +1272,110 @@ func TestVisitComparison_Parens(t *testing.T) {
}
}
// TestVisitComparison_FreeTextSearch covers Free Text Search — bare/quoted string literals
// that route through freeTextColumn (body only). No search() involved.
// TestVisitComparison_FullText covers full-text (bare string literal) expressions.
// rsbOpts has SkipFullTextFilter=true → TrueConditionLiteral.
// sbOpts has SkipFullTextFilter=false, FreeTextColumn=bodyCol → "body_cond".
func TestVisitComparison_FreeTextSearch(t *testing.T) {
// sbOpts has SkipFullTextFilter=false, FullTextColumn=bodyCol → "body_cond".
func TestVisitComparison_FullText(t *testing.T) {
rsbOpts, sbOpts := visitComparisonOpts(t)
tests := []visitComparisonCase{
{
name: "standalone free-text term",
name: "standalone full-text term",
expr: "'hello'",
wantRSB: "",
wantSB: "WHERE body_cond",
},
{
// RSB: FT→true, a→true; AND propagates true.
name: "free-text AND attribute",
name: "full-text AND attribute",
expr: "'hello' AND a = 'a'",
wantRSB: "",
wantSB: "WHERE (body_cond AND a_cond)",
},
{
// RSB: FT→true stripped; x_cond survives.
name: "free-text AND resource",
name: "full-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 free-text term",
name: "NOT full-text term",
expr: "NOT 'hello'",
wantRSB: "",
wantSB: "WHERE NOT (body_cond)",
},
{
// RSB: FT→true short-circuits OR.
name: "free-text OR resource",
name: "full-text OR resource",
expr: "'hello' OR x = 'x'",
wantRSB: "",
wantSB: "WHERE (body_cond OR x_cond)",
},
{
name: "free-text OR attribute",
name: "full-text OR attribute",
expr: "'hello' OR a = 'a'",
wantRSB: "",
wantSB: "WHERE (body_cond OR a_cond)",
},
{
name: "two free-text terms ANDed",
name: "two full-text terms ANDed",
expr: "'hello' AND 'world'",
wantRSB: "",
wantSB: "WHERE (body_cond AND body_cond)",
},
{
name: "two free-text terms ORed",
name: "two full-text terms ORed",
expr: "'hello' OR 'world'",
wantRSB: "",
wantSB: "WHERE (body_cond OR body_cond)",
},
{
name: "free-text in parentheses",
name: "full-text in parentheses",
expr: "('hello')",
wantRSB: "",
wantSB: "WHERE (body_cond)",
},
{
name: "two free-text AND attribute",
name: "two full-text AND attribute",
expr: "'hello' AND 'world' AND a = 'a'",
wantRSB: "",
wantSB: "WHERE (body_cond AND body_cond AND a_cond)",
},
{
name: "free-text OR attr OR resource all types",
name: "full-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 free-text AND attr",
name: "NOT of paren full-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 free-text AND resource",
name: "NOT full-text AND resource",
expr: "NOT 'hello' AND x = 'x'",
wantRSB: "WHERE x_cond",
wantSB: "WHERE NOT (body_cond)",
},
{
name: "NOT free-text OR resource",
name: "NOT full-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: "free-text AND BETWEEN",
name: "full-text AND BETWEEN",
expr: "'hello' AND x BETWEEN 1 AND 3",
wantRSB: "WHERE x_cond",
wantSB: "WHERE body_cond",
},
{
name: "free-text AND EXISTS",
name: "full-text AND EXISTS",
expr: "'hello' AND x EXISTS",
wantRSB: "WHERE x_cond",
wantSB: "WHERE body_cond",
@@ -1383,21 +1383,21 @@ func TestVisitComparison_FreeTextSearch(t *testing.T) {
{
// RSB: FT→true and allVariable→true; AND propagates true.
// SB: allVariable→TrueConditionLiteral stripped; body_cond survives.
name: "free-text AND allVariable",
name: "full-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: "free-text OR allVariable",
name: "full-text OR allVariable",
expr: "'hello' OR x IN $service",
wantRSB: "",
wantSB: "",
},
{
// SB: body_cond
name: "free-text with sentinel value",
name: "full-text with sentinel value",
expr: SkipConditionLiteral,
wantRSB: "",
wantSB: "WHERE body_cond",
@@ -1684,68 +1684,6 @@ func TestVisitComparison_UnknownKeys(t *testing.T) {
}
}
// TestVisitComparison_FullTextSearch covers Full Text Search — the explicit search()
// function that fans out across all FTSSet columns. FTSSet must be set for
// search() to be enabled; invalid param counts must error.
func TestVisitComparison_FullTextSearch(t *testing.T) {
ftsOpts := FilterExprVisitorOpts{
Context: t.Context(),
FieldKeys: visitTestKeys,
ConditionBuilder: &conditionBuilder{},
SkipResourceFilter: false,
SkipFreeTextFilter: false,
SkipFunctionCalls: false,
IgnoreNotFoundKeys: false,
FTSCondition: func(_ context.Context, _ any, _ *sqlbuilder.SelectBuilder) (string, error) {
return fmt.Sprintf("fts_cond"), nil
},
}
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

@@ -198,4 +198,3 @@ func (c *conditionBuilder) ConditionFor(
return condition, nil
}

View File

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

View File

@@ -25,50 +25,6 @@ func NewConditionBuilder(fm qbtypes.FieldMapper, fl flagger.Flagger) *conditionB
return &conditionBuilder{fm: fm, fl: fl}
}
// ConditionForSearch builds the search condition for all FTS columns belonging
// to fieldContext. JSON columns are skipped when useJSONBody is disabled.
// Returns ("", nil) when no searchable columns exist for the context.
func (c *conditionBuilder) ConditionForSearch(
ctx context.Context,
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
contexts := []telemetrytypes.FieldContext{
telemetrytypes.FieldContextLog,
telemetrytypes.FieldContextBody,
telemetrytypes.FieldContextAttribute,
telemetrytypes.FieldContextResource,
}
useJSONBody := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
var conditions []string
for _, fieldContext := range contexts {
for _, col := range ftsColumns(ctx, fieldContext, useJSONBody) {
switch col.Type.GetType() {
case schema.ColumnTypeEnumMap:
keysExpr := fmt.Sprintf("mapKeys(%s)", col.Name)
valsExpr := fmt.Sprintf("mapValues(%s)", col.Name)
if mc, ok := col.Type.(schema.MapColumnType); ok && mc.ValueType.GetType() != schema.ColumnTypeEnumString {
valsExpr = fmt.Sprintf("arrayMap(x -> toString(x), mapValues(%s))", col.Name)
}
conditions = append(conditions, sb.Or(
fmt.Sprintf(`arrayExists(x -> match(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))`, col.Name, sb.Var(value)))
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumLowCardinality:
conditions = append(conditions, fmt.Sprintf(`match(LOWER(%s), LOWER(%s))`, col.Name, sb.Var(value)))
default:
return "", errors.Newf(errors.TypeInternal, errors.CodeInternal, "FTS unsupported column type %s", col.Type)
}
}
}
return sb.Or(conditions...), nil
}
func (c *conditionBuilder) conditionFor(
ctx context.Context,
startNs, endNs uint64,
@@ -106,8 +62,7 @@ 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)
}
@@ -161,6 +116,7 @@ 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)

View File

@@ -34,7 +34,6 @@ const (
LogsV2AttributesNumberColumn = "attributes_number"
LogsV2AttributesBoolColumn = "attributes_bool"
LogsV2ResourcesStringColumn = "resources_string"
LogsV2ResourceJSONColumn = "resource"
LogsV2ScopeStringColumn = "scope_string"
BodyV2ColumnPrefix = constants.BodyV2ColumnPrefix

View File

@@ -543,35 +543,3 @@ func (m *fieldMapper) buildArrayMap(currentNode *telemetrytypes.JSONAccessNode,
return fmt.Sprintf("arrayMap(%s->%s, %s)", currentNode.Alias(), nestedExpr, arrayExpr), nil
}
// GetColumns returns the physical columns for the given field context that
// search() fans out across. For FieldContextBody the active column set depends
// on the useJSONBody feature flag.
func ftsColumns(ctx context.Context, fieldContext telemetrytypes.FieldContext, useJSONBody bool) []*schema.Column {
switch fieldContext {
case telemetrytypes.FieldContextLog:
return []*schema.Column{
logsV2Columns[LogsV2SeverityTextColumn],
logsV2Columns[LogsV2TraceIDColumn],
logsV2Columns[LogsV2SpanIDColumn],
}
case telemetrytypes.FieldContextBody:
if useJSONBody {
return []*schema.Column{logsV2Columns[LogsV2BodyV2Column]}
}
return []*schema.Column{logsV2Columns[LogsV2BodyColumn]}
case telemetrytypes.FieldContextAttribute:
return []*schema.Column{
logsV2Columns[LogsV2AttributesStringColumn],
logsV2Columns[LogsV2AttributesNumberColumn],
logsV2Columns[LogsV2AttributesBoolColumn],
}
case telemetrytypes.FieldContextResource:
return []*schema.Column{
logsV2Columns[LogsV2ResourcesStringColumn],
logsV2Columns[LogsV2ResourceJSONColumn],
}
default:
return nil
}
}

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ func TestFilterExprLogs(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FreeTextColumn: DefaultFullTextColumn,
FullTextColumn: 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, search()} but got '<'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '<'",
},
// Single word searches with spaces
@@ -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, search()} but got '['",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} 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, search()} but got ')'",
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} 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, search()} but got 'and'",
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} 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, search()} but got 'or'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got EOF",
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} 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, search()} but got 'like'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'like'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got 'between'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got 'in'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got 'exists'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'exists'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got 'regexp'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'regexp'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got 'contains'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'contains'",
},
{
category: "Keyword conflict",
@@ -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, search()} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF"},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF"},
{category: "Only functions", query: "has", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
{category: "Only functions", query: "hasAny", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
@@ -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, search()} but got 'and'",
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
},
{
category: "Operator keywords as keys",
@@ -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, search()} but got 'or'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
},
{
category: "Operator keywords as keys",
@@ -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, search()} but got '='",
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '='",
},
{
category: "Operator keywords as keys",
@@ -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, search()} but got 'between'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
},
{
category: "Operator keywords as keys",
@@ -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, search()} but got 'in'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
},
// Using function keywords as keys
@@ -2458,7 +2458,7 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FreeTextColumn: DefaultFullTextColumn,
FullTextColumn: DefaultFullTextColumn,
JsonKeyToKey: GetBodyJSONKey,
}

View File

@@ -1204,7 +1204,6 @@ func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) (*logQueryStat
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)

View File

@@ -27,9 +27,8 @@ type logQueryStatementBuilder struct {
aggExprRewriter qbtypes.AggExprRewriter
fl flagger.Flagger
freeTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
ftsConditionFunc qbtypes.FTSConditionFunc
fullTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
}
var _ qbtypes.StatementBuilder[qbtypes.LogAggregation] = (*logQueryStatementBuilder)(nil)
@@ -40,9 +39,8 @@ func NewLogQueryStatementBuilder(
fieldMapper qbtypes.FieldMapper,
conditionBuilder qbtypes.ConditionBuilder,
aggExprRewriter qbtypes.AggExprRewriter,
freeTextColumn *telemetrytypes.TelemetryFieldKey,
fullTextColumn *telemetrytypes.TelemetryFieldKey,
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
ftsConditionFunc qbtypes.FTSConditionFunc,
fl flagger.Flagger,
) *logQueryStatementBuilder {
logsSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrylogs")
@@ -54,7 +52,7 @@ func NewLogQueryStatementBuilder(
telemetrytypes.SignalLogs,
telemetrytypes.SourceUnspecified,
metadataStore,
freeTextColumn,
fullTextColumn,
jsonKeyToKey,
fl,
)
@@ -67,9 +65,8 @@ func NewLogQueryStatementBuilder(
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
aggExprRewriter: aggExprRewriter,
fl: fl,
freeTextColumn: freeTextColumn,
jsonKeyToKey: jsonKeyToKey,
ftsConditionFunc: ftsConditionFunc,
fullTextColumn: fullTextColumn,
jsonKeyToKey: jsonKeyToKey,
}
}
@@ -82,6 +79,7 @@ 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
@@ -317,7 +315,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, true)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -421,7 +419,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, true)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -578,7 +576,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, false)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -642,7 +640,6 @@ 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
@@ -651,26 +648,22 @@ func (b *logQueryStatementBuilder) addFilterCondition(
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
if query.Filter != nil && query.Filter.Expression != "" {
opts := querybuilder.FilterExprVisitorOpts{
// add filter expression
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, 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.FTSCondition = b.ftsConditionFunc
}
})
// add filter expression
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, opts)
if err != nil {
return preparedWhereClause, err
}

View File

@@ -2,7 +2,6 @@ package telemetrylogs
import (
"context"
"strings"
"testing"
"time"
@@ -212,7 +211,6 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -354,7 +352,6 @@ func TestStatementBuilderListQuery(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -388,13 +385,29 @@ 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'",
Expression: "service.name = 'cartservice' hello",
},
Limit: 10,
Order: []qbtypes.OrderBy{
@@ -411,8 +424,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 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},
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},
},
expectedErr: nil,
},
@@ -485,7 +498,6 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -562,7 +574,6 @@ func TestStatementBuilderTimeSeriesBodyGroupBy(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -658,7 +669,6 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -883,7 +893,6 @@ func TestAdjustKey(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -1029,7 +1038,6 @@ func TestStmtBuilderBodyField(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
@@ -1051,23 +1059,17 @@ func TestStmtBuilderBodyField(t *testing.T) {
}
}
func TestStmtBuilderTextSearch(t *testing.T) {
func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
enableUseJSONBody bool
expected qbtypes.Statement
expectedErr string
// optional per-case time window (ms); zero → use default 1747947419000/1747983448000
startMs uint64
endMs uint64
expectedErr error
}{
// ── Free Text Search ──────────────────────────────────────────────────────────
// Bare/quoted tokens route through fullTextColumn (body / body_v2.message only).
// SQL: match(LOWER(<body_col>), LOWER(?))
{
name: "free_text_search",
name: "fts",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
@@ -1075,16 +1077,15 @@ func TestStmtBuilderTextSearch(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", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Args: []any{"error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{querybuilder.BodyFullTextSearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "free_text_search_2",
name: "fts_2",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
@@ -1092,16 +1093,15 @@ func TestStmtBuilderTextSearch(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", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Args: []any{"error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{querybuilder.BodyFullTextSearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "free_text_search_json_disabled",
name: "fts_disabled",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
@@ -1109,80 +1109,11 @@ func TestStmtBuilderTextSearch(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", "1705309200000000000", uint64(1705307400), "1705316400000000000", uint64(1705316400), 10},
Warnings: nil,
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},
},
},
// ── Full Text Search ──────────────────────────────────────────────────────────
// search() fans out via ftsSupportedContexts (log, body, attribute, resource).
// Each context returns one OR-combined condition from ConditionForContext.
// 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(LOWER(severity_text), LOWER(?)) OR match(LOWER(trace_id), LOWER(?)) OR match(LOWER(span_id), LOWER(?))) 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(LOWER(severity_text), LOWER(?)) OR match(LOWER(trace_id), LOWER(?)) OR match(LOWER(span_id), LOWER(?))) 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(LOWER(severity_text), LOWER(?)) OR match(LOWER(trace_id), LOWER(?)) OR match(LOWER(span_id), LOWER(?))) 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",
expectedErr: nil,
},
}
@@ -1206,36 +1137,22 @@ func TestStmtBuilderTextSearch(t *testing.T) {
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
cb.ConditionForSearch,
fl,
)
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 != "" {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErr != nil {
require.Error(t, err)
_, _, _, _, _, 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)
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
require.NoError(t, err)
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)
if err != nil {
_, _, _, _, _, add := errors.Unwrapb(err)
t.Logf("error additionals: %v", add)
}
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)
}
})
}

View File

@@ -135,4 +135,3 @@ func (c *conditionBuilder) ConditionFor(
return fmt.Sprintf(expr, columns[0].Name, sb.Var(key.Name), cond), nil
}

View File

@@ -152,7 +152,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDeltaFastPath(
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FullTextColumn: &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,
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FullTextColumn: &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,
FreeTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels"},
Variables: variables,
StartNs: start,
EndNs: end,

View File

@@ -157,4 +157,3 @@ func (c *conditionBuilder) ConditionFor(
return condition, nil
}

View File

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

View File

@@ -206,4 +206,3 @@ func (b *defaultConditionBuilder) ConditionFor(
}
return "", qbtypes.ErrUnsupportedOperator
}

View File

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

View File

@@ -265,7 +265,6 @@ func (c *conditionBuilder) ConditionFor(
return condition, nil
}
func (c *conditionBuilder) isSpanScopeField(name string) bool {
keyName := strings.ToLower(name)
return keyName == SpanSearchScopeRoot || keyName == SpanSearchScopeEntryPoint

View File

@@ -19,11 +19,6 @@ var (
type JsonKeyToFieldFunc func(context.Context, *telemetrytypes.TelemetryFieldKey, FilterOperator, any) (string, any)
// FTSConditionFunc builds the search() SQL condition for all FTS columns belonging
// to fieldContext. Returns ("", nil) when no columns are searchable for the
// context (e.g. body JSON when useJSONBody is off). Pass nil to disable search().
type FTSConditionFunc func(ctx context.Context, value any, sb *sqlbuilder.SelectBuilder) (string, error)
// FieldMapper maps the telemetry field key to the table field name.
type FieldMapper interface {
// FieldFor returns the field name for the given key.

View File

@@ -54,12 +54,6 @@ 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.
@@ -80,7 +74,6 @@ 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"`
@@ -135,7 +128,6 @@ 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.
@@ -293,14 +285,6 @@ 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)
@@ -334,7 +318,6 @@ 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,8 +111,6 @@ 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:
@@ -205,8 +203,6 @@ func (v *variableReplacementVisitor) VisitPrimary(ctx *grammar.PrimaryContext) a
return v.Visit(ctx.Comparison())
} else if ctx.FunctionCall() != nil {
return v.Visit(ctx.FunctionCall())
} else if ctx.SearchCall() != nil {
return v.Visit(ctx.SearchCall())
} else if ctx.FullText() != nil {
return v.Visit(ctx.FullText())
}
@@ -395,11 +391,6 @@ 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

@@ -1,273 +0,0 @@
"""
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,
},
# Free Text Search — String bare keyword
# FTS — String bare keyword
{
"name": "msg.fts_quoted",
"requestType": "raw",
@@ -1225,6 +1225,7 @@ 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",
@@ -1232,27 +1233,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,
},
# ── 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

@@ -1,313 +0,0 @@
"""
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}"