Compare commits

..

3 Commits

Author SHA1 Message Date
ahrefabhi
4120b53c27 fix: remove unused function call in getCurrentQueryPair 2025-07-10 22:26:47 +05:30
ahrefabhi
a7cbfe4587 fix: enhance context detection after closing parenthesis in query 2025-07-10 22:25:33 +05:30
ahrefabhi
74d480e113 fix: fixed context for query in parenthesis 2025-07-10 21:18:54 +05:30
7 changed files with 73 additions and 395 deletions

View File

@@ -1,16 +1,10 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
Having,
IBuilderQuery,
Query,
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogAggregation,
@@ -19,8 +13,6 @@ import {
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { v4 as uuid } from 'uuid';
/**
* Check if an operator requires array values (like IN, NOT IN)
@@ -28,7 +20,7 @@ import { v4 as uuid } from 'uuid';
* @returns True if the operator requires array values
*/
const isArrayOperator = (operator: string): boolean => {
const arrayOperators = ['in', 'not in', 'IN', 'NOT IN'];
const arrayOperators = ['in', 'nin', 'IN', 'NOT IN'];
return arrayOperators.includes(operator);
};
@@ -95,330 +87,6 @@ export const convertFiltersToExpression = (
};
};
function unquote(str: string): string {
if (typeof str !== 'string') return str;
const startsWithQuote = str.startsWith('"') || str.startsWith("'");
const endsWithSameQuote =
(str.endsWith('"') && str[0] === '"') ||
(str.endsWith("'") && str[0] === "'");
if (startsWithQuote && endsWithSameQuote && str.length >= 2) {
return str.slice(1, -1);
}
return str;
}
const formatValuesForFilter = (value: string | string[]): string | string[] => {
if (Array.isArray(value)) {
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
}
if (typeof value === 'string') {
return unquote(value);
}
return String(value);
};
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters,
filter: convertFiltersToExpression(filters),
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
// Map extracted query pairs to key-specific pair information for faster access
if (queryPairs.length > 0) {
queryPairsMap = new Map(
queryPairs.map((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
return [key, pair];
}),
);
}
filters.items.forEach((filter) => {
const { key, op, value } = filter;
// Skip invalid filters with no key
if (!key) return;
let shouldAddToNonExisting = true; // Flag to decide if the filter should be added to non-existing filters
const sanitizedOperator = op.trim().toUpperCase();
// Check if the operator is IN or NOT IN
if (
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].includes(
sanitizedOperator,
)
) {
const existingPair = queryPairsMap.get(
`${key.key}-${op}`.trim().toLowerCase(),
);
const formattedValue = formatValueForExpression(value, op);
// If a matching query pair exists, modify the query
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
return;
}
// Handle the different cases for IN operator
switch (sanitizedOperator) {
case OPERATORS.IN:
// If there's a NOT IN or equal operator, merge the filter
if (
queryPairsMap.has(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
)
) {
const notInPair = queryPairsMap.get(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
visitedPairs.add(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
if (notInPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notInPair.position.negationStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notInPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase())
) {
const equalsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase());
if (equalsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
equalsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
equalsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break;
case `${OPERATORS.NOT} ${OPERATORS.IN}`:
if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.NOT} ${
OPERATORS.IN
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break; // No operation needed for NOT IN case
default:
break;
}
}
if (
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
}
// Add filters that don't have an existing pair to non-existing filters
if (
shouldAddToNonExisting &&
!queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
nonExistingFilters.push(filter);
}
});
// Create new filters from non-visited query pairs
const newFilterItems: TagFilterItem[] = [];
queryPairsMap.forEach((pair, key) => {
if (!visitedPairs.has(key)) {
const operator = pair.hasNegation
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
: getOperatorValue(pair.operator.toUpperCase());
newFilterItems.push({
id: uuid(),
op: operator,
key: {
id: pair.key,
key: pair.key,
type: '',
},
value: pair.isMultiValue
? formatValuesForFilter(pair.valueList as string[]) ?? ''
: formatValuesForFilter(pair.value as string) ?? '',
});
}
});
// Merge new filter items with existing ones
if (newFilterItems.length > 0) {
updatedFilters.items = [...updatedFilters.items, ...newFilterItems];
}
// If no non-existing filters, return the modified query directly
if (nonExistingFilters.length === 0) {
return {
filters: updatedFilters,
filter: { expression: modifiedQuery },
};
}
// Convert non-existing filters to an expression and append to the modified query
const nonExistingFilterExpression = convertFiltersToExpression({
items: nonExistingFilters,
op: filters.op || 'AND',
});
if (nonExistingFilterExpression.expression) {
return {
filters: updatedFilters,
filter: {
expression: `${modifiedQuery.trim()} ${
nonExistingFilterExpression.expression
}`,
},
};
}
// Return the final result with the modified query
return {
filters: updatedFilters,
filter: { expression: modifiedQuery || '' },
};
};
/**
* Removes specified key-value pairs from a logical query expression string.
*
* This function parses the given query expression and removes any query pairs
* whose keys match those in the `keysToRemove` array. It also removes any trailing
* logical conjunctions (e.g., `AND`, `OR`) and whitespace that follow the matched pairs,
* ensuring that the resulting expression remains valid and clean.
*
* @param expression - The full query string.
* @param keysToRemove - An array of keys (case-insensitive) that should be removed from the expression.
* @returns A new expression string with the specified keys and their associated clauses removed.
*/
export const removeKeysFromExpression = (
expression: string,
keysToRemove: string[],
): string => {
if (!keysToRemove || keysToRemove.length === 0) {
return expression;
}
let updatedExpression = expression;
if (updatedExpression) {
keysToRemove.forEach((key) => {
// Extract key-value query pairs from the expression
const existingQueryPairs = extractQueryPairs(updatedExpression);
let queryPairsMap: Map<string, IQueryPair>;
if (existingQueryPairs.length > 0) {
// Build a map for quick lookup of query pairs by their lowercase trimmed keys
queryPairsMap = new Map(
existingQueryPairs.map((pair) => {
const key = pair.key.trim().toLowerCase();
return [key, pair];
}),
);
// Lookup the current query pair using the attribute key (case-insensitive)
const currentQueryPair = queryPairsMap.get(`${key}`.trim().toLowerCase());
if (currentQueryPair && currentQueryPair.isComplete) {
// Determine the start index of the query pair (fallback order: key → operator → value)
const queryPairStart =
currentQueryPair.position.keyStart ??
currentQueryPair.position.operatorStart ??
currentQueryPair.position.valueStart;
// Determine the end index of the query pair (fallback order: value → operator → key)
let queryPairEnd =
currentQueryPair.position.valueEnd ??
currentQueryPair.position.operatorEnd ??
currentQueryPair.position.keyEnd;
// Get the part of the expression that comes after the current query pair
const expressionAfterPair = `${expression.slice(queryPairEnd + 1)}`;
// Match optional spaces and an optional conjunction (AND/OR), case-insensitive
const conjunctionOrSpacesRegex = /^(\s*((AND|OR)\s+)?)/i;
const match = expressionAfterPair.match(conjunctionOrSpacesRegex);
if (match && match.length > 0) {
// If match is found, extend the queryPairEnd to include the matched part
queryPairEnd += match[0].length;
}
// Remove the full query pair (including any conjunction/whitespace) from the expression
updatedExpression = `${expression.slice(
0,
queryPairStart,
)}${expression.slice(queryPairEnd + 1)}`.trim();
}
}
});
}
return updatedExpression;
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value

View File

@@ -6,13 +6,14 @@ import './Checkbox.styles.scss';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
OPERATORS,
} from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
@@ -29,7 +30,7 @@ import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
@@ -167,11 +168,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filter: {
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
filter.attributeKey.key,
]),
},
filters: {
...item.filters,
items:
@@ -217,14 +213,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
query.filters.items = query.filters.items.filter(
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
if (isOnlyOrAll === 'Only') {
const newFilterItem: TagFilterItem = {
id: uuid(),
@@ -305,7 +293,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
}
break;
case 'not in':
case 'nin':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
@@ -384,7 +372,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (!checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue('NOT_IN'),
op: getOperatorValue(OPERATORS.NIN),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
@@ -407,7 +395,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue('NOT_IN'),
op: getOperatorValue(OPERATORS.NIN),
key: filter.attributeKey,
value,
};

View File

@@ -29,7 +29,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'In',
},
{
label: 'not in',
label: 'nin',
metricValue: '!~',
traceValue: 'NotIn',
},
@@ -49,7 +49,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'Exists',
},
{
label: 'not exists',
label: 'nexists',
metricValue: '!~',
traceValue: 'NotExists',
},
@@ -59,7 +59,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'Contains',
},
{
label: 'not contains',
label: 'ncontains',
metricValue: '!~',
traceValue: 'NotContains',
},

View File

@@ -58,31 +58,27 @@ export function getOperatorValue(op: string): string {
case 'IN':
return 'in';
case 'NOT_IN':
return 'not in';
return 'nin';
case OPERATORS.REGEX:
return 'regex';
case OPERATORS.HAS:
return 'has';
case OPERATORS.NHAS:
return 'not has';
return 'nhas';
case OPERATORS.NREGEX:
return 'not regex';
return 'nregex';
case 'LIKE':
return 'like';
case 'ILIKE':
return 'ilike';
case 'NOT_LIKE':
return 'not like';
case 'NOT_ILIKE':
return 'not ilike';
return 'nlike';
case 'EXISTS':
return 'exists';
case 'NOT_EXISTS':
return 'not exists';
return 'nexists';
case 'CONTAINS':
return 'contains';
case 'NOT_CONTAINS':
return 'not contains';
return 'ncontains';
default:
return op;
}
@@ -92,27 +88,27 @@ export function getOperatorFromValue(op: string): string {
switch (op) {
case 'in':
return 'IN';
case 'not in':
case 'nin':
return 'NOT_IN';
case 'like':
return 'LIKE';
case 'regex':
return OPERATORS.REGEX;
case 'not regex':
case 'nregex':
return OPERATORS.NREGEX;
case 'not like':
case 'nlike':
return 'NOT_LIKE';
case 'exists':
return 'EXISTS';
case 'not exists':
case 'nexists':
return 'NOT_EXISTS';
case 'contains':
return 'CONTAINS';
case 'not contains':
case 'ncontains':
return 'NOT_CONTAINS';
case 'has':
return OPERATORS.HAS;
case 'not has':
case 'nhas':
return OPERATORS.NHAS;
default:
return op;

View File

@@ -1,5 +1,4 @@
import { Select } from 'antd';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { useEffect, useState } from 'react';
@@ -120,18 +119,8 @@ function SpanScopeSelector({
return [...nonScopeFilters, ...newScopeFilter];
};
const keysToRemove = Object.values(SPAN_FILTER_CONFIG)
.map((config) => config?.key)
.filter((key): key is string => typeof key === 'string');
newQuery.builder.queryData = newQuery.builder.queryData.map((item) => ({
...item,
filter: {
expression: removeKeysFromExpression(
item.filter?.expression ?? '',
keysToRemove,
),
},
filters: {
...item.filters,
items: getUpdatedFilters(

View File

@@ -1,6 +1,6 @@
import {
convertAggregationToExpression,
convertFiltersToExpressionWithExistingQuery,
convertFiltersToExpression,
convertHavingToExpression,
} from 'components/QueryBuilderV2/utils';
import { QueryParams } from 'constants/query';
@@ -28,15 +28,13 @@ export const useGetCompositeQueryParam = (): Query | null => {
if (parsedCompositeQuery?.builder?.queryData) {
parsedCompositeQuery.builder.queryData = parsedCompositeQuery.builder.queryData.map(
(query) => {
const existingExpression = query.filter?.expression || '';
const convertedQuery = { ...query };
const convertedFilter = convertFiltersToExpressionWithExistingQuery(
query.filters,
existingExpression,
);
convertedQuery.filter = convertedFilter.filter;
convertedQuery.filters = convertedFilter.filters;
// Convert filters if needed
if (query.filters?.items?.length > 0 && !query.filter?.expression) {
const convertedFilter = convertFiltersToExpression(query.filters);
convertedQuery.filter = convertedFilter;
}
// Convert having if needed
if (query.having?.length > 0 && !query.havingExpression?.expression) {
@@ -55,6 +53,7 @@ export const useGetCompositeQueryParam = (): Query | null => {
) as any; // Type assertion to handle union type
convertedQuery.aggregations = convertedAggregation;
}
return convertedQuery;
},
);

View File

@@ -718,7 +718,6 @@ export function getQueryContextAtCursor(
isInValueBoundary ||
isInConjunctionBoundary ||
isInBracketListBoundary ||
isInParenthesisBoundary ||
isAfterClosingBracketList
) {
// Extract information from the current pair (if available)
@@ -968,6 +967,30 @@ export function getQueryContextAtCursor(
currentPair: currentPair,
};
}
if (
lastTokenContext.isInParenthesis &&
lastTokenBeforeCursor.type === FilterQueryLexer.RPAREN
) {
// If we are after a parenthesis we should enter the conjunction context.
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: adjustedCursorIndex,
stop: adjustedCursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
isInConjunction: true, // After RPARAN + space, should be conjunction context
isInParenthesis: false,
isInBracketList: false,
queryPairs: queryPairs,
currentPair: currentPair,
};
}
}
// FIXED: Consider the case where the cursor is at the end of a token
@@ -1454,6 +1477,19 @@ export function extractQueryPairs(query: string): IQueryPair[] {
}
}
function getIndexTillSpace(pair: IQueryPair, query: string): number {
const { position } = pair;
let pairEnd = position.valueEnd || position.operatorEnd || position.keyEnd;
// Start from the next index after pairEnd
pairEnd += 1;
while (pairEnd < query.length && query.charAt(pairEnd) === ' ') {
pairEnd += 1;
}
return pairEnd;
}
/**
* Gets the current query pair at the cursor position
* This is useful for getting suggestions based on the current context
@@ -1483,12 +1519,14 @@ export function getCurrentQueryPair(
position.valueEnd || position.operatorEnd || position.keyEnd;
const pairStart =
position.keyStart || position.operatorStart || position.valueStart || 0;
position.keyStart ?? (position.operatorStart || position.valueStart || 0);
// If this pair ends at or before the cursor, and it's further right than our previous best match
if (
pairEnd >= cursorIndex &&
pairStart <= cursorIndex &&
((pairEnd >= cursorIndex && pairStart <= cursorIndex) ||
(!pair.isComplete &&
pairStart <= cursorIndex &&
getIndexTillSpace(pair, query) >= cursorIndex)) &&
(!bestMatch ||
pairEnd >
(bestMatch.position.valueEnd ||