Compare commits

..

1 Commits

Author SHA1 Message Date
Gaurav Tewari
3765ca3d42 chore: migrate dropdown 2026-05-20 17:39:33 +05:30
16 changed files with 229 additions and 610 deletions

View File

@@ -1,46 +1,30 @@
import { useState } from 'react';
import { Ellipsis } from '@signozhq/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Button } from 'antd';
import './DropDown.styles.scss';
type DropDownItemClick = (info: { key: string; keyPath: string[] }) => void;
function DropDown({
element,
onDropDownItemClick,
}: {
element: JSX.Element[];
onDropDownItemClick?: MenuProps['onClick'];
onDropDownItemClick?: DropDownItemClick;
}): JSX.Element {
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
key: index,
}),
);
const [isDdOpen, setDdOpen] = useState<boolean>(false);
const items: MenuItem[] = element.map((e, index) => ({
key: String(index),
label: e,
onClick: onDropDownItemClick,
}));
return (
<Dropdown
menu={{
items,
onMouseEnter: (): void => setDdOpen(true),
onMouseLeave: (): void => setDdOpen(false),
onClick: (item): void => onDropDownItemClick?.(item),
}}
open={isDdOpen}
>
<Button
type="link"
className={`dropdown-button`}
onClick={(e): void => {
e.preventDefault();
setDdOpen(true);
}}
>
<DropdownMenuSimple menu={{ items }}>
<Button type="link" className="dropdown-button">
<Ellipsis className="dropdown-icon" size={16} />
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -1,15 +1,7 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import {
Button,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Select,
Space,
} from 'antd';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -241,9 +233,9 @@ function ExplorerCard({
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<DropdownMenuSimple menu={moreOptionMenu}>
<Ellipsis size="md" />
</Dropdown>
</DropdownMenuSimple>
)}
</Space>
</OffSetCol>

View File

@@ -4,13 +4,13 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { Button, Flex, Switch } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from '@signozhq/icons';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
@@ -85,8 +85,9 @@ function DynamicColumnTable({
);
};
const items: MenuProps['items'] =
const items: MenuItem[] =
dynamicColumns?.map((column, index) => ({
key: String(index),
label: (
<div className="dynamicColumnsTable-items">
<div>{column.title?.toString()}</div>
@@ -96,8 +97,6 @@ function DynamicColumnTable({
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
@@ -126,18 +125,14 @@ function DynamicColumnTable({
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<DropdownMenuSimple menu={{ items }}>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
</DropdownMenuSimple>
)}
</Flex>

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { CloudDownload } from '@signozhq/icons';
import { Button, Dropdown, MenuProps, Flex } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Button, Flex } from 'antd';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
@@ -67,7 +68,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
};
return (
<Dropdown menu={menu} trigger={['click']}>
<DropdownMenuSimple menu={menu}>
<Button
className="download-button"
loading={isLoading || isDownloading}
@@ -79,7 +80,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
Download
</Flex>
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -15,7 +15,8 @@ import {
X,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Button, Input, Tooltip } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
@@ -128,7 +129,7 @@ function WidgetHeader({
],
);
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
if (isTWidgetOptions(key)) {
const functionToCall = keyMethodMapping[key];
@@ -221,8 +222,10 @@ function WidgetHeader({
const menu = useMemo(
() => ({
items: updatedMenuList,
onClick: onMenuItemSelectHandler,
items: updatedMenuList.map((item) => ({
...item,
onClick: onMenuItemSelectHandler,
})),
}),
[updatedMenuList, onMenuItemSelectHandler],
);
@@ -321,7 +324,7 @@ function WidgetHeader({
/>
)}
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<DropdownMenuSimple menu={menu} side="bottom" align="end">
<Button
data-testid="widget-header-options"
className={`widget-header-more-options ${
@@ -329,7 +332,7 @@ function WidgetHeader({
}`}
icon={<EllipsisVertical size="md" />}
/>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</>

View File

@@ -1,9 +1,9 @@
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
import type { MenuItem as DropdownMenuItem } from '@signozhq/ui/dropdown-menu';
import { MenuItemKeys } from './contants';
import { MenuItem } from './types';
export const generateMenuList = (actions: MenuItem[]): MenuItemType[] =>
export const generateMenuList = (actions: MenuItem[]): DropdownMenuItem[] =>
actions
.filter((action: MenuItem) => action.isVisible)
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({

View File

@@ -12,12 +12,11 @@ import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Modal,
Popover,
Skeleton,
@@ -553,7 +552,7 @@ function DashboardsList(): JSX.Element {
];
const getCreateDashboardItems = useMemo(() => {
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
{
label: (
<div
@@ -711,11 +710,11 @@ function DashboardsList(): JSX.Element {
{createNewDashboard && (
<section className="actions">
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="text"
@@ -727,7 +726,7 @@ function DashboardsList(): JSX.Element {
>
New Dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
<Button
type="text"
className="learn-more"
@@ -756,11 +755,11 @@ function DashboardsList(): JSX.Element {
onChange={handleSearch}
/>
{createNewDashboard && (
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="primary"
@@ -773,7 +772,7 @@ function DashboardsList(): JSX.Element {
>
New dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
)}
</div>

View File

@@ -7,7 +7,12 @@ import {
DropResult,
} from 'react-beautiful-dnd';
import { Color } from '@signozhq/design-tokens';
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Button, Divider, Input, Tooltip } from 'antd';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import { FieldDataType } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -159,34 +164,12 @@ function ExplorerColumnsRenderer({
debouncedSetQuerySearchText(e.target.value);
};
const items: MenuProps['items'] = [
{
key: 'search',
label: (
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
value={searchText}
onChange={handleSearchChange}
prefix={<Search size={16} style={{ padding: '6px' }} />}
/>
),
},
{
key: 'columns',
label: (
<ExplorerAttributeColumns
isLoading={isLoading}
data={data}
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
),
},
];
const handleOpenChange = (nextOpen: boolean): void => {
setOpen(nextOpen);
if (nextOpen) {
setSearchText('');
}
};
const removeSelectedLogField = (name: string): void => {
if (
@@ -238,13 +221,6 @@ function ExplorerColumnsRenderer({
}
};
const toggleDropdown = (): void => {
setOpen(!open);
if (!open) {
setSearchText('');
}
};
const isDarkMode = useIsDarkMode();
return (
@@ -327,25 +303,38 @@ function ExplorerColumnsRenderer({
</Droppable>
</DragDropContext>
<div>
<Dropdown
menu={{ items }}
arrow
placement="top"
open={open}
overlayClassName="explorer-columns-dropdown"
>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
onClick={toggleDropdown}
/>
</Dropdown>
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
value={searchText}
onChange={handleSearchChange}
prefix={<Search size={16} style={{ padding: '6px' }} />}
/>
<ExplorerAttributeColumns
isLoading={isLoading}
data={data}
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)}

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { ChevronDown } from '@signozhq/icons';
import { Button, ColorPicker, Dropdown, MenuProps, Space } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Button, ColorPicker, Space } from 'antd';
import type { Color } from 'antd/es/color-picker';
import useDebounce from 'hooks/useDebounce';
@@ -26,7 +27,7 @@ function ColorSelector({
setColorFromPicker(hex);
};
const items: MenuProps['items'] = [
const items: MenuItem[] = [
{
key: 'Red',
label: <CustomColor color="Red" />,
@@ -62,7 +63,7 @@ function ColorSelector({
];
return (
<Dropdown menu={{ items }} trigger={['click']}>
<DropdownMenuSimple menu={{ items }}>
<Button
onClick={(e): void => e.preventDefault()}
className="color-selector-button"
@@ -72,7 +73,7 @@ function ColorSelector({
<ChevronDown size="md" />
</Space>
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -1,5 +1,6 @@
import {
MouseEvent,
ReactNode,
useCallback,
useEffect,
useMemo,
@@ -25,7 +26,14 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Dropdown, MenuProps, Modal, Tooltip } from 'antd';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { Button, MenuProps, Modal, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import { Logout } from 'api/utils';
import updateUserPreference from 'api/v1/user/preferences/name/update';
@@ -1182,46 +1190,95 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
{isAIAssistantEnabled && renderNavItems([aiAssistantMenuItem], false)}
<div className="nav-dropdown-item">
<Dropdown
menu={{
items: helpSupportDropdownMenuItems,
onClick: handleHelpSupportMenuItemClick,
}}
placement="topLeft"
overlayClassName="nav-dropdown-overlay help-support-dropdown"
trigger={['click']}
onOpenChange={(open): void => setIsDropdownOpen(open)}
>
<div className="nav-item">
<div className="nav-item-data" data-testid="help-support-nav-item">
<div className="nav-item-icon">{helpSupportMenuItem.icon}</div>
<DropdownMenu onOpenChange={(open): void => setIsDropdownOpen(open)}>
<DropdownMenuTrigger asChild>
<div className="nav-item">
<div className="nav-item-data" data-testid="help-support-nav-item">
<div className="nav-item-icon">{helpSupportMenuItem.icon}</div>
<div className="nav-item-label">{helpSupportMenuItem.label}</div>
<div className="nav-item-label">{helpSupportMenuItem.label}</div>
</div>
</div>
</div>
</Dropdown>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="nav-dropdown-overlay help-support-dropdown"
>
{helpSupportDropdownMenuItems.map((item, idx) => {
if ('type' in item) {
// eslint-disable-next-line react/no-array-index-key
return <DropdownMenuSeparator key={`help-sep-${idx}`} />;
}
return (
<DropdownMenuItem
key={String(item.key)}
leftIcon={item.icon}
onClick={(e): void =>
handleHelpSupportMenuItemClick({
...item,
key: String(item.key),
domEvent: e.nativeEvent,
} as unknown as SidebarItem)
}
>
{item.label}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="nav-dropdown-item">
<Dropdown
menu={{
items: userSettingsDropdownMenuItems,
onClick: handleSettingsMenuItemClick,
}}
placement="topLeft"
overlayClassName="nav-dropdown-overlay settings-dropdown"
trigger={['click']}
onOpenChange={(open): void => setIsDropdownOpen(open)}
>
<div className={cx('nav-item', isSettingsPage && 'active')}>
<div className="nav-item-active-marker" />
<div className="nav-item-data" data-testid="settings-nav-item">
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
<DropdownMenu onOpenChange={(open): void => setIsDropdownOpen(open)}>
<DropdownMenuTrigger asChild>
<div className={cx('nav-item', isSettingsPage && 'active')}>
<div className="nav-item-active-marker" />
<div className="nav-item-data" data-testid="settings-nav-item">
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
<div className="nav-item-label">{userSettingsMenuItem.label}</div>
<div className="nav-item-label">{userSettingsMenuItem.label}</div>
</div>
</div>
</div>
</Dropdown>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="nav-dropdown-overlay settings-dropdown"
>
{(userSettingsDropdownMenuItems ?? []).map((item, idx) => {
if (!item) {
return null;
}
if ('type' in item && item.type === 'divider') {
// eslint-disable-next-line react/no-array-index-key
return <DropdownMenuSeparator key={`settings-sep-${idx}`} />;
}
const settingsItem = item as {
key?: string | number;
label?: ReactNode;
icon?: ReactNode;
disabled?: boolean;
};
return (
<DropdownMenuItem
key={String(settingsItem.key)}
leftIcon={settingsItem.icon}
disabled={settingsItem.disabled}
onClick={(e): void =>
handleSettingsMenuItemClick({
key: String(settingsItem.key),
domEvent: e.nativeEvent,
} as unknown as SidebarItem)
}
>
{settingsItem.label}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd';
import { Divider, Switch, Tooltip } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
import {
@@ -11,7 +12,6 @@ import {
} from 'pages/AlertDetails/hooks';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { useAlertRule } from 'providers/Alert';
import { CSSProperties } from 'styled-components';
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
import { AlertDef } from 'types/api/alerts/def';
@@ -20,16 +20,6 @@ import RenameModal from './RenameModal';
import './ActionButtons.styles.scss';
const menuItemStyle: CSSProperties = {
fontSize: '14px',
letterSpacing: '0.14px',
};
const menuItemStyleV2: CSSProperties = {
fontSize: '13px',
letterSpacing: '0.13px',
};
function AlertActionButtons({
ruleId,
alertDetails,
@@ -68,9 +58,7 @@ function AlertActionButtons({
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
const finalMenuItemStyle = isV2Alert ? menuItemStyleV2 : menuItemStyle;
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
...(!isV2Alert
? [
{
@@ -78,7 +66,6 @@ function AlertActionButtons({
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: handleRename,
style: finalMenuItemStyle,
},
]
: []),
@@ -87,17 +74,13 @@ function AlertActionButtons({
label: 'Duplicate',
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
onClick: handleAlertDuplicate,
style: finalMenuItemStyle,
},
{
key: 'delete-rule',
label: 'Delete',
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
onClick: handleAlertDelete,
style: {
...finalMenuItemStyle,
color: Color.BG_CHERRY_400,
},
danger: true,
},
];
@@ -138,7 +121,7 @@ function AlertActionButtons({
<Divider type="vertical" />
<Dropdown trigger={['click']} menu={{ items: menuItems }}>
<DropdownMenuSimple menu={{ items: menuItems }}>
<Tooltip title="More options">
<Ellipsis
size={16}
@@ -147,7 +130,7 @@ function AlertActionButtons({
className="dropdown-icon"
/>
</Tooltip>
</Dropdown>
</DropdownMenuSimple>
</div>
<RenameModal

View File

@@ -1,14 +1,6 @@
import { useMemo, useState } from 'react';
import {
Button,
Divider,
Dropdown,
Form,
MenuProps,
Space,
Switch,
Tooltip,
} from 'antd';
import { Button, Divider, Form, Space, Switch, Tooltip } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import cx from 'classnames';
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
import { QueryParams } from 'constants/query';
@@ -44,16 +36,22 @@ function FunnelStep({
const [isAddDetailsModalOpen, setIsAddDetailsModalOpen] =
useState<boolean>(false);
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
(option) => ({
key: option.value,
label: option.key,
style:
option.value === stepData.latency_pointer
? { backgroundColor: 'var(--bg-slate-100)' }
: {},
}),
);
const latencyPointerItems: MenuItem[] = [
{
type: 'radio-group',
value: stepData.latency_pointer,
onChange: (value): void =>
onStepChange(index, {
latency_pointer: value as FunnelStepData['latency_pointer'],
}),
children: LatencyPointers.map((option) => ({
type: 'radio',
key: option.value,
label: option.key,
value: option.value,
})),
},
];
const updatedCurrentQuery = useMemo(
() => ({
@@ -212,17 +210,18 @@ function FunnelStep({
</div>
<div className="latency-pointer">
<div className="latency-pointer__label">Latency pointer</div>
<Dropdown
menu={{
items: latencyPointerItems,
onClick: ({ key }): void =>
onStepChange(index, {
latency_pointer: key as FunnelStepData['latency_pointer'],
}),
}}
trigger={['click']}
disabled={!hasEditPermission}
>
{hasEditPermission ? (
<DropdownMenuSimple menu={{ items: latencyPointerItems }}>
<Space>
{
LatencyPointers.find(
(option) => option.value === stepData.latency_pointer,
)?.key
}
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</DropdownMenuSimple>
) : (
<Space>
{
LatencyPointers.find(
@@ -231,7 +230,7 @@ function FunnelStep({
}
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</Dropdown>
)}
</div>
</div>
</Form>

View File

@@ -70,31 +70,12 @@ func (b *traceOperatorCTEBuilder) build(ctx context.Context, requestType qbtypes
selectFromCTE := rootCTEName
if b.operator.ReturnSpansFrom != "" {
sourceQueryCTE := b.queryToCTEName[b.operator.ReturnSpansFrom]
if sourceQueryCTE == "" {
selectFromCTE = b.queryToCTEName[b.operator.ReturnSpansFrom]
if selectFromCTE == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
"returnSpansFrom references query '%s' which has no corresponding CTE",
b.operator.ReturnSpansFrom)
}
filteredCTEName := fmt.Sprintf("__return_from_%s", b.operator.ReturnSpansFrom)
// rootCTEName holds one row per matching *span*, not per *trace*, so it can
// contain many rows for the same trace_id. DISTINCT de-duplicates that set
// before ClickHouse builds the hash table for the IN check, keeping memory
// usage proportional to the number of distinct traces rather than spans.
matchingTracedSB := sqlbuilder.NewSelectBuilder()
matchingTracedSB.Select("DISTINCT trace_id")
matchingTracedSB.From(rootCTEName)
matchedTracesSQL, matchedTracesArgs := matchingTracedSB.BuildWithFlavor(sqlbuilder.ClickHouse)
filteredSB := sqlbuilder.NewSelectBuilder()
filteredSB.Select("*")
filteredSB.From(sourceQueryCTE)
filteredSB.Where(fmt.Sprintf("trace_id IN (%s)", matchedTracesSQL))
filteredSQL, filteredArgs := filteredSB.BuildWithFlavor(sqlbuilder.ClickHouse, matchedTracesArgs...)
b.addCTE(filteredCTEName, filteredSQL, filteredArgs, []string{sourceQueryCTE, rootCTEName})
selectFromCTE = filteredCTEName
}
finalStmt, err := b.buildFinalQuery(ctx, selectFromCTE, requestType)

View File

@@ -385,82 +385,6 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
expectedErr: nil,
},
{
name: "returnSpansFrom B: A -> B return B spans filtered by operator",
requestType: qbtypes.RequestTypeRaw,
operator: qbtypes.QueryBuilderTraceOperator{
Expression: "A -> B",
ReturnSpansFrom: "B",
Limit: 10,
},
compositeQuery: &qbtypes.CompositeQuery{
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "A",
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "service.name = 'gateway'"},
},
},
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "B",
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "service.name = 'database'"},
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
},
{
name: "returnSpansFrom C: (A -> B) && C return C spans filtered by operator",
requestType: qbtypes.RequestTypeRaw,
operator: qbtypes.QueryBuilderTraceOperator{
Expression: "(A -> B) && C",
ReturnSpansFrom: "C",
Limit: 10,
},
compositeQuery: &qbtypes.CompositeQuery{
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "A",
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "service.name = 'gateway'"},
},
},
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "B",
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "service.name = 'database'"},
},
},
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "C",
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "service.name = 'auth'"},
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
},
}
fm := NewFieldMapper()

View File

@@ -72,7 +72,6 @@ class TraceOperatorQuery:
return_spans_from: str | None = None
limit: int | None = None
order: list[OrderBy] | None = None
select_fields: list[TelemetryFieldKey] | None = None
def to_dict(self) -> dict:
spec: dict[str, Any] = {
@@ -85,8 +84,6 @@ class TraceOperatorQuery:
spec["limit"] = self.limit
if self.order:
spec["order"] = [o.to_dict() if hasattr(o, "to_dict") else o for o in self.order]
if self.select_fields:
spec["selectFields"] = [f.to_dict() for f in self.select_fields]
return {"type": "builder_trace_operator", "spec": spec}

View File

@@ -1,286 +0,0 @@
"""
Integration tests for TraceOperatorQuery (builder_trace_operator) through the
/api/v5/query_range endpoint.
Covers:
1. Order-by variants for trace operator (A -> B, A => B) with returnSpansFrom="A".
Guards against the NOT_FOUND_COLUMN_IN_BLOCK regression where ordering by a
column absent from an outer SELECT caused a query failure.
2. Expression operators (=>, ->, &&, ||, A NOT B) with and without returnSpansFrom.
returnSpansFrom semantics
--------------------------
returnSpansFrom="" (default)
The final rows come from the expression's root CTE. Only spans that
directly satisfy the structural predicate are returned.
returnSpansFrom="A"
The expression is still evaluated in full (the structural relationship
must hold), but the final rows are drawn from the A sub-query CTE,
filtered to traces that appeared in the expression result. Concretely:
the query returns every A span whose trace_id belongs to a trace that
matched the expression.
"""
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.querier import OrderBy, TelemetryFieldKey, TraceOperatorQuery, make_query_request
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _chain_trace(now: datetime, *spans: tuple) -> list[Traces]:
"""
Build a single trace as a linear chain.
Each span tuple is (name, service, op_type, duration_s[, extra_attrs]).
The first span is the root; each subsequent span is a child of the previous.
"""
trace_id = TraceIdGenerator.trace_id()
ids = [TraceIdGenerator.span_id() for _ in spans]
result = []
for i, s in enumerate(spans):
name, service, op_type, duration_s = s[0], s[1], s[2], s[3]
extra = s[4] if len(s) > 4 else {}
result.append(
Traces(
timestamp=now - timedelta(seconds=10 - i),
duration=timedelta(seconds=duration_s),
trace_id=trace_id,
span_id=ids[i],
parent_span_id="" if i == 0 else ids[i - 1],
name=name,
kind=TracesKind.SPAN_KIND_SERVER if i == 0 else TracesKind.SPAN_KIND_INTERNAL,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={"service.name": service},
attributes={"operation.type": op_type, **extra},
)
)
return result
def _builder_query(name: str, filter_expr: str, limit: int = 100) -> dict:
return {
"type": "builder_query",
"spec": {
"name": name,
"signal": "traces",
"filter": {"expression": filter_expr},
"limit": limit,
},
}
# ---------------------------------------------------------------------------
# Order-by test
# ---------------------------------------------------------------------------
def test_trace_operator_query_order_by(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[list[Traces]], None],
) -> None:
"""
Verifies order-by behaviour for three sub-cases, all inserted once:
field_not_in_select
Order by an attribute absent from selectFields.
Guards against the NOT_FOUND_COLUMN_IN_BLOCK ClickHouse regression.
core_span_field
Order by duration_nano with no explicit selectFields.
non_core_field_in_select
Order by an attribute that IS in selectFields.
"""
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
insert_traces(
[
# field_not_in_select — two 3-level chains; differ only by http.method
*_chain_trace(
now,
("fnis-gp", "svc-a", "fnis-grandparent", 5, {"http.method": "POST"}),
("fnis-mid", "svc-a", "fnis-middle", 3),
("fnis-gc", "svc-a", "fnis-grandchild", 1),
),
*_chain_trace(
now,
("fnis-gp", "svc-b", "fnis-grandparent", 5, {"http.method": "GET"}),
("fnis-mid", "svc-b", "fnis-middle", 3),
("fnis-gc", "svc-b", "fnis-grandchild", 1),
),
# core_span_field — two parent→child chains; differ by duration
*_chain_trace(now, ("csf-parent-long", "svc-long", "csf-parent", 5), ("csf-child-long", "svc-long", "csf-child", 1)),
*_chain_trace(now, ("csf-parent-short", "svc-short", "csf-parent", 1), ("csf-child-short", "svc-short", "csf-child", 1)),
# non_core_field_in_select — two parent→child chains; differ by http.method
*_chain_trace(now, ("ncis-parent-post", "svc-post", "ncis-parent", 3, {"http.method": "POST"}), ("ncis-child-post", "svc-post", "ncis-child", 1)),
*_chain_trace(now, ("ncis-parent-get", "svc-get", "ncis-parent", 3, {"http.method": "GET"}), ("ncis-child-get", "svc-get", "ncis-child", 1)),
# noise
*_chain_trace(now, ("noise-span", "svc-noise", "noise-op", 1)),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
def check_order(case_id, filter_a, filter_b, expression, select_fields, order, expected_rows):
resp = make_query_request(
signoz,
token,
start_ms=start_ms,
end_ms=end_ms,
request_type="raw",
queries=[
_builder_query("A", filter_a),
_builder_query("B", filter_b),
TraceOperatorQuery(name="C", expression=expression, return_spans_from="A", limit=100, select_fields=select_fields, order=order).to_dict(),
],
)
assert resp.status_code == HTTPStatus.OK, f"[{case_id}] {resp.text}"
assert resp.json()["status"] == "success"
rows = resp.json()["data"]["data"]["results"][0].get("rows") or []
assert len(rows) == len(expected_rows), f"[{case_id}] expected {len(expected_rows)} rows, got {len(rows)}"
for i, (row, expected) in enumerate(zip(rows, expected_rows)):
for key, value in expected.items():
assert row["data"].get(key) == value, f"[{case_id}] row {i}: {key}={value!r} expected, got {row['data'].get(key)!r}"
# POST > GET in DESC; order key is absent from selectFields
check_order(
"field_not_in_select",
"operation.type = 'fnis-grandparent'",
"operation.type = 'fnis-grandchild'",
"A -> B",
[TelemetryFieldKey(name="service.name", field_data_type="string", field_context="resource")],
[OrderBy(key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"), direction="desc")],
[{"service.name": "svc-a"}, {"service.name": "svc-b"}],
)
# 5 s parent before 1 s parent in DESC
check_order(
"core_span_field",
"operation.type = 'csf-parent'",
"operation.type = 'csf-child'",
"A => B",
None,
[OrderBy(key=TelemetryFieldKey(name="duration_nano", field_context="span"), direction="desc")],
[{"name": "csf-parent-long"}, {"name": "csf-parent-short"}],
)
# POST > GET in DESC; order key is in selectFields so it appears in each row
check_order(
"non_core_field_in_select",
"operation.type = 'ncis-parent'",
"operation.type = 'ncis-child'",
"A => B",
[TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute")],
[OrderBy(key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"), direction="desc")],
[{"http.method": "POST"}, {"http.method": "GET"}],
)
# ---------------------------------------------------------------------------
# Expression × returnSpansFrom test
# ---------------------------------------------------------------------------
def test_trace_operator_expressions(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[list[Traces]], None],
) -> None:
"""
Covers all five operators × two returnSpansFrom settings in a single pass.
All test spans are inserted once; each operator uses a unique op_type prefix
so queries never interfere with each other.
For each operator:
default (returnSpansFrom="") — only spans satisfying the structural predicate
return_A (returnSpansFrom="A") — A spans from traces where the predicate held
Unary NOT A is skipped: its root CTE reads from all_spans (unbounded by any
filter), making row counts non-deterministic across a shared ClickHouse session.
"""
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
insert_traces(
[
# A => B: trace 1 matches (dc-root directly parents dc-leaf); trace 2 does not
*_chain_trace(now, ("dc-root", "svc-dc-a", "dc-root", 5), ("dc-leaf", "svc-dc-a", "dc-leaf", 2)),
*_chain_trace(now, ("dc-root-only", "svc-dc-b", "dc-root", 2)),
# A -> B: trace 1 matches (id-gp is an indirect ancestor of id-gc); trace 2 does not
*_chain_trace(
now,
("id-gp", "svc-id-a", "id-gp", 5),
("id-mid", "svc-id-a", "id-mid", 3),
("id-gc", "svc-id-a", "id-gc", 1),
),
*_chain_trace(now, ("id-gp-only", "svc-id-b", "id-gp", 2)),
# A && B: trace 1 matches (contains both A and B); trace 2 does not (no B)
*_chain_trace(now, ("and-root", "svc-and-a", "and-root", 5), ("and-leaf", "svc-and-a", "and-leaf", 2)),
*_chain_trace(now, ("and-root-only", "svc-and-b", "and-root", 2)),
# A || B: trace 1 has A only, trace 2 has B only (both match A || B)
*_chain_trace(now, ("or-a-span", "svc-or-a", "or-a", 5)),
*_chain_trace(now, ("or-b-span", "svc-or-b", "or-b", 2)),
# A NOT B: trace 1 has A + B child (does NOT match); trace 2 has A only (matches)
*_chain_trace(now, ("not-root-with-child", "svc-not-a", "not-root", 5), ("not-child", "svc-not-a", "not-child", 2)),
*_chain_trace(now, ("not-root-no-child", "svc-not-b", "not-root", 2)),
# noise — must not surface in any query below
*_chain_trace(now, ("noise-span", "svc-noise", "noise-op", 1)),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
def check(case_id, filter_a, filter_b, expression, return_spans_from, expected_names):
resp = make_query_request(
signoz,
token,
start_ms=start_ms,
end_ms=end_ms,
request_type="raw",
queries=[
_builder_query("A", filter_a),
_builder_query("B", filter_b),
TraceOperatorQuery(name="C", expression=expression, return_spans_from=return_spans_from, limit=100).to_dict(),
],
)
assert resp.status_code == HTTPStatus.OK, f"[{case_id}] {resp.text}"
rows = resp.json()["data"]["data"]["results"][0].get("rows") or []
actual = {r["data"]["name"] for r in rows}
assert actual == expected_names, f"[{case_id}] expected {expected_names!r}, got {actual!r}"
# ── A => B (direct child) ────────────────────────────────────────────────
check("direct_child_default", "operation.type = 'dc-root'", "operation.type = 'dc-leaf'", "A => B", "", {"dc-root"})
check("direct_child_return_A", "operation.type = 'dc-root'", "operation.type = 'dc-leaf'", "A => B", "A", {"dc-root"})
# ── A -> B (indirect descendant) ─────────────────────────────────────────
check("indirect_descendant_default", "operation.type = 'id-gp'", "operation.type = 'id-gc'", "A -> B", "", {"id-gp"})
check("indirect_descendant_return_A", "operation.type = 'id-gp'", "operation.type = 'id-gc'", "A -> B", "A", {"id-gp"})
# ── A && B ────────────────────────────────────────────────────────────────
check("and_default", "operation.type = 'and-root'", "operation.type = 'and-leaf'", "A && B", "", {"and-root"})
check("and_return_A", "operation.type = 'and-root'", "operation.type = 'and-leaf'", "A && B", "A", {"and-root"})
# ── A || B ────────────────────────────────────────────────────────────────
# default returns UNION of A and B; return_A returns only A spans from matching traces
check("or_default", "operation.type = 'or-a'", "operation.type = 'or-b'", "A || B", "", {"or-a-span", "or-b-span"})
check("or_return_A", "operation.type = 'or-a'", "operation.type = 'or-b'", "A || B", "A", {"or-a-span"})
# ── A NOT B (binary not) ──────────────────────────────────────────────────
check("not_binary_default", "operation.type = 'not-root'", "operation.type = 'not-child'", "A NOT B", "", {"not-root-no-child"})
check("not_binary_return_A", "operation.type = 'not-root'", "operation.type = 'not-child'", "A NOT B", "A", {"not-root-no-child"})