feat: add support for negation context in query processing

This commit is contained in:
ahrefabhi 2025-06-27 11:44:04 +05:30
parent 6f08aff58b
commit c783c5c7ee
2 changed files with 176 additions and 14 deletions

View File

@ -248,9 +248,9 @@ export function findKeyOperatorValueTriplet(
FilterQueryLexer.GT,
FilterQueryLexer.GE,
FilterQueryLexer.LIKE,
FilterQueryLexer.NOT_LIKE,
// FilterQueryLexer.NOT_LIKE,
FilterQueryLexer.ILIKE,
FilterQueryLexer.NOT_ILIKE,
// FilterQueryLexer.NOT_ILIKE,
FilterQueryLexer.BETWEEN,
FilterQueryLexer.EXISTS,
FilterQueryLexer.REGEXP,
@ -375,6 +375,7 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: true, // Default to key context when input is empty
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -388,6 +389,7 @@ export function getQueryContextAtCursor(
stop: cursorIndex,
currentToken: '',
isInValue: false,
isInNegation: false,
isInKey: false,
isInOperator: false,
isInFunction: false,
@ -418,6 +420,8 @@ export function getQueryContextAtCursor(
const isInKey = currentToken.type === FilterQueryLexer.KEY;
const isInNegation = currentToken.type === FilterQueryLexer.NOT;
const isInOperator = [
FilterQueryLexer.EQUALS,
FilterQueryLexer.NOT_EQUALS,
@ -427,9 +431,9 @@ export function getQueryContextAtCursor(
FilterQueryLexer.GT,
FilterQueryLexer.GE,
FilterQueryLexer.LIKE,
FilterQueryLexer.NOT_LIKE,
// FilterQueryLexer.NOT_LIKE,
FilterQueryLexer.ILIKE,
FilterQueryLexer.NOT_ILIKE,
// FilterQueryLexer.NOT_ILIKE,
FilterQueryLexer.BETWEEN,
FilterQueryLexer.EXISTS,
FilterQueryLexer.REGEXP,
@ -442,7 +446,7 @@ export function getQueryContextAtCursor(
FilterQueryLexer.HAS,
FilterQueryLexer.HASANY,
FilterQueryLexer.HASALL,
FilterQueryLexer.HASNONE,
// FilterQueryLexer.HASNONE,
].includes(currentToken.type);
// Get the context-related tokens (key, operator, value)
@ -473,6 +477,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: true,
isInFunction: false,
isInConjunction: false,
@ -491,6 +496,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: true,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -509,6 +515,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: true,
@ -526,6 +533,7 @@ export function getQueryContextAtCursor(
stop: currentToken.stop,
currentToken: currentToken.text,
isInValue: false,
isInNegation: false,
isInKey: true,
isInOperator: false,
isInFunction: false,
@ -546,6 +554,7 @@ export function getQueryContextAtCursor(
stop: currentToken.stop,
currentToken: currentToken.text,
isInValue: false,
isInNegation: false,
isInKey: true,
isInOperator: false,
isInFunction: false,
@ -567,6 +576,7 @@ export function getQueryContextAtCursor(
stop: currentToken.stop,
currentToken: currentToken.text,
isInValue: false,
isInNegation: false,
isInKey: false,
isInOperator: false,
isInFunction: false,
@ -585,6 +595,7 @@ export function getQueryContextAtCursor(
stop: currentToken.stop,
currentToken: currentToken.text,
isInValue: true,
isInNegation: false,
isInKey: false,
isInOperator: false,
isInFunction: false,
@ -612,6 +623,25 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: false,
isInKey: true,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
isInParenthesis: false,
...relationTokens, // Include related tokens
};
}
if (isInNegation && nextToken.type === FilterQueryLexer.NOT) {
return {
tokenType: currentToken.type,
text: currentToken.text,
start: currentToken.start,
stop: currentToken.stop,
currentToken: currentToken.text,
isInValue: false,
isInKey: false,
isInNegation: true,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -636,6 +666,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: true,
isInFunction: false,
isInConjunction: false,
@ -660,6 +691,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: true,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -684,6 +716,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: true,
@ -703,6 +736,25 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: true,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
isInParenthesis: false,
...relationTokens, // Include related tokens
};
}
if (nextToken.type === FilterQueryLexer.NOT) {
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInValue: false,
isInKey: false,
isInNegation: true,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -729,6 +781,7 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: true,
isInFunction: false,
isInConjunction: false,
@ -750,6 +803,7 @@ export function getQueryContextAtCursor(
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInNegation: false,
isInValue: true,
isInKey: false,
isInOperator: false,
@ -769,6 +823,7 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: true,
@ -794,6 +849,7 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -812,6 +868,7 @@ export function getQueryContextAtCursor(
currentToken: currentToken.text,
isInValue,
isInKey,
isInNegation,
isInOperator,
isInFunction,
isInConjunction,
@ -828,6 +885,7 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -836,6 +894,16 @@ export function getQueryContextAtCursor(
}
}
export const negationQueryOperatorSuggestions = [
{ label: 'LIKE', type: 'operator', info: 'Like' },
{ label: 'ILIKE', type: 'operator', info: 'Case insensitive like' },
{ label: 'EXISTS', type: 'operator', info: 'Exists' },
{ label: 'BETWEEN', type: 'operator', info: 'Between' },
{ label: 'IN', type: 'operator', info: 'In' },
{ label: 'REGEXP', type: 'operator', info: 'Regular expression' },
{ label: 'CONTAINS', type: 'operator', info: 'Contains' },
];
export const queryOperatorSuggestions = [
{ label: '=', type: 'operator', info: 'Equal to' },
{ label: '!=', type: 'operator', info: 'Not equal to' },
@ -843,14 +911,6 @@ export const queryOperatorSuggestions = [
{ label: '<', type: 'operator', info: 'Less than' },
{ label: '>=', type: 'operator', info: 'Greater than or equal to' },
{ label: '<=', type: 'operator', info: 'Less than or equal to' },
{ label: 'LIKE', type: 'operator', info: 'Like' },
{ label: 'ILIKE', type: 'operator', info: 'Case insensitive like' },
{ label: 'BETWEEN', type: 'operator', info: 'Between' },
{ label: 'EXISTS', type: 'operator', info: 'Exists' },
{ label: 'NOT_EXISTS', type: 'operator', info: 'Not Exists' },
{ label: 'REGEXP', type: 'operator', info: 'Regular expression' },
{ label: 'CONTAINS', type: 'operator', info: 'Contains' },
{ label: 'IN', type: 'operator', info: 'In' },
{ label: 'NOT', type: 'operator', info: 'Not' },
{ label: 'NOT_LIKE', type: 'operator', info: 'Not like' },
...negationQueryOperatorSuggestions,
];

View File

@ -51,6 +51,7 @@ function normalizeSpaces(query: string): string {
export function createContext(
token: Token,
isInKey: boolean,
isInNegation: boolean,
isInOperator: boolean,
isInValue: boolean,
keyToken?: string,
@ -66,6 +67,7 @@ export function createContext(
stop: token.stop,
currentToken: token.text || '',
isInKey,
isInNegation,
isInOperator,
isInValue,
isInFunction: false,
@ -85,6 +87,7 @@ function determineTokenContext(
query: string,
): {
isInKey: boolean;
isInNegation: boolean;
isInOperator: boolean;
isInValue: boolean;
isInFunction: boolean;
@ -92,6 +95,7 @@ function determineTokenContext(
isInParenthesis: boolean;
} {
let isInKey: boolean = false;
let isInNegation: boolean = false;
let isInOperator: boolean = false;
let isInValue: boolean = false;
let isInFunction: boolean = false;
@ -126,6 +130,9 @@ function determineTokenContext(
}
}
// Negation context
isInNegation = tokenType === FilterQueryLexer.NOT;
// Function context
isInFunction = isFunctionToken(tokenType);
@ -137,6 +144,7 @@ function determineTokenContext(
return {
isInKey,
isInNegation,
isInOperator,
isInValue,
isInFunction,
@ -156,6 +164,7 @@ function determineContextBoundaries(
operatorContext: { start: number; end: number } | null;
valueContext: { start: number; end: number } | null;
conjunctionContext: { start: number; end: number } | null;
negationContext: { start: number; end: number } | null;
bracketContext: { start: number; end: number; isForList: boolean } | null;
} {
// Find the current query pair based on cursor position
@ -343,6 +352,12 @@ function determineContextBoundaries(
if (currentPair) {
const { position } = currentPair;
// Negation context: from negationStart to negationEnd
const negationContext = {
start: position.negationStart ?? 0,
end: position.negationEnd ?? 0,
};
// Key context: from keyStart to keyEnd
const keyContext = {
start: position.keyStart,
@ -412,6 +427,7 @@ function determineContextBoundaries(
return {
keyContext,
negationContext,
operatorContext,
valueContext,
conjunctionContext,
@ -433,6 +449,18 @@ function determineContextBoundaries(
if (tokenAtCursor.type === FilterQueryLexer.KEY) {
return {
keyContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
negationContext: null,
operatorContext: null,
valueContext: null,
conjunctionContext: null,
bracketContext,
};
}
if (tokenAtCursor.type === FilterQueryLexer.NOT) {
return {
keyContext: null,
negationContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
operatorContext: null,
valueContext: null,
conjunctionContext: null,
@ -443,6 +471,7 @@ function determineContextBoundaries(
if (isOperatorToken(tokenAtCursor.type)) {
return {
keyContext: null,
negationContext: null,
operatorContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
valueContext: null,
conjunctionContext: null,
@ -453,6 +482,7 @@ function determineContextBoundaries(
if (isValueToken(tokenAtCursor.type)) {
return {
keyContext: null,
negationContext: null,
operatorContext: null,
valueContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
conjunctionContext: null,
@ -463,6 +493,7 @@ function determineContextBoundaries(
if (isConjunctionToken(tokenAtCursor.type)) {
return {
keyContext: null,
negationContext: null,
operatorContext: null,
valueContext: null,
conjunctionContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
@ -474,6 +505,7 @@ function determineContextBoundaries(
// If no current pair, return null for all contexts except possibly bracket context
return {
keyContext: null,
negationContext: null,
operatorContext: null,
valueContext: null,
conjunctionContext: null,
@ -515,6 +547,7 @@ export function getQueryContextAtCursor(
stop: cursorIndex,
currentToken: '',
isInKey: true,
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -660,6 +693,12 @@ export function getQueryContextAtCursor(
adjustedCursorIndex <= contextBoundaries.keyContext.end) ||
adjustedCursorIndex === contextBoundaries.keyContext.end + 1);
const isInNegationBoundary =
contextBoundaries.negationContext &&
((adjustedCursorIndex >= contextBoundaries.negationContext.start &&
adjustedCursorIndex <= contextBoundaries.negationContext.end) ||
adjustedCursorIndex === contextBoundaries.negationContext.end + 1);
const isInOperatorBoundary =
contextBoundaries.operatorContext &&
((adjustedCursorIndex >= contextBoundaries.operatorContext.start &&
@ -703,6 +742,7 @@ export function getQueryContextAtCursor(
// If cursor is within a specific context boundary, this takes precedence
if (
isInKeyBoundary ||
isInNegationBoundary ||
isInOperatorBoundary ||
isInValueBoundary ||
isInConjunctionBoundary ||
@ -736,6 +776,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: '',
isInKey: isInKeyBoundary || false,
isInNegation: isInNegationBoundary || false,
isInOperator: isInOperatorBoundary || false,
isInValue: finalIsInValue || false,
isInConjunction: finalIsInConjunction || false,
@ -824,6 +865,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: '',
isInKey: true, // Default to key context when input is empty
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -853,6 +895,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInKey: false,
isInNegation: false,
isInOperator: true, // After key + space, should be operator context
isInValue: false,
isInFunction: false,
@ -865,6 +908,27 @@ export function getQueryContextAtCursor(
};
}
if (lastTokenContext.isInNegation) {
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: adjustedCursorIndex,
stop: adjustedCursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInKey: false,
isInNegation: false,
isInOperator: true, // After key + space + NOT, should be operator context
isInValue: false,
isInFunction: false,
isInConjunction: false,
isInParenthesis: false,
isInBracketList: false,
keyToken: lastTokenBeforeCursor.text,
queryPairs: queryPairs,
currentPair: currentPair,
};
}
if (lastTokenContext.isInOperator) {
// If we just typed an operator and then a space, we move to value context
const keyFromPair = currentPair?.key || '';
@ -876,6 +940,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInValue: !isNonValueToken, // After operator + space, should be value context
isInFunction: false,
@ -900,6 +965,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -923,6 +989,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInKey: true, // After conjunction + space, should be key context
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -1016,6 +1083,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: previousToken.text,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInValue: true, // Always in value context after operator
isInFunction: false,
@ -1038,6 +1106,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: previousToken.text,
isInKey: false,
isInNegation: false,
isInOperator: true, // After key, progress to operator context
isInValue: false,
isInFunction: false,
@ -1058,6 +1127,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: previousToken.text,
isInKey: false,
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -1080,6 +1150,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: previousToken.text,
isInKey: true, // After conjunction, progress back to key context
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -1100,6 +1171,7 @@ export function getQueryContextAtCursor(
stop: adjustedCursorIndex,
currentToken: '',
isInKey: true,
isInNegation: false,
isInOperator: false,
isInValue: false,
isInFunction: false,
@ -1119,6 +1191,7 @@ export function getQueryContextAtCursor(
currentToken: '',
isInValue: false,
isInKey: true, // Default to key context on error
isInNegation: false,
isInOperator: false,
isInFunction: false,
isInConjunction: false,
@ -1185,6 +1258,7 @@ export function extractQueryPairs(query: string): IQueryPair[] {
key: currentPair.key,
operator: currentPair.operator || '',
value: currentPair.value,
hasNegation: currentPair.hasNegation || false,
position: {
keyStart: currentPair.position?.keyStart || 0,
keyEnd: currentPair.position?.keyEnd || 0,
@ -1192,6 +1266,8 @@ export function extractQueryPairs(query: string): IQueryPair[] {
operatorEnd: currentPair.position?.operatorEnd || 0,
valueStart: currentPair.position?.valueStart,
valueEnd: currentPair.position?.valueEnd,
negationStart: currentPair.position?.negationStart || 0,
negationEnd: currentPair.position?.negationEnd || 0,
},
isComplete: !!(
currentPair.key &&
@ -1212,6 +1288,22 @@ export function extractQueryPairs(query: string): IQueryPair[] {
},
};
}
// If NOT token comes set hasNegation to true
else if (token.type === FilterQueryLexer.NOT && currentPair) {
currentPair.hasNegation = true;
currentPair.position = {
keyStart: currentPair.position?.keyStart || 0,
keyEnd: currentPair.position?.keyEnd || 0,
operatorStart: currentPair.position?.operatorStart || 0,
operatorEnd: currentPair.position?.operatorEnd || 0,
valueStart: currentPair.position?.valueStart,
valueEnd: currentPair.position?.valueEnd,
negationStart: token.start,
negationEnd: token.stop,
};
}
// If token is an operator and we have a key, add the operator
else if (
isOperatorToken(token.type) &&
@ -1228,6 +1320,8 @@ export function extractQueryPairs(query: string): IQueryPair[] {
operatorEnd: token.stop,
valueStart: currentPair.position?.valueStart,
valueEnd: currentPair.position?.valueEnd,
negationStart: currentPair.position?.negationStart || 0,
negationEnd: currentPair.position?.negationEnd || 0,
};
}
// If token is a value and we have a key and operator, add the value
@ -1247,6 +1341,8 @@ export function extractQueryPairs(query: string): IQueryPair[] {
operatorEnd: currentPair.position?.operatorEnd || 0,
valueStart: token.start,
valueEnd: token.stop,
negationStart: currentPair.position?.negationStart || 0,
negationEnd: currentPair.position?.negationEnd || 0,
};
}
// If token is a conjunction (AND/OR), finalize the current pair
@ -1255,6 +1351,7 @@ export function extractQueryPairs(query: string): IQueryPair[] {
key: currentPair.key,
operator: currentPair.operator || '',
value: currentPair.value,
hasNegation: currentPair.hasNegation || false,
position: {
keyStart: currentPair.position?.keyStart || 0,
keyEnd: currentPair.position?.keyEnd || 0,
@ -1262,6 +1359,8 @@ export function extractQueryPairs(query: string): IQueryPair[] {
operatorEnd: currentPair.position?.operatorEnd || 0,
valueStart: currentPair.position?.valueStart,
valueEnd: currentPair.position?.valueEnd,
negationStart: currentPair.position?.negationStart || 0,
negationEnd: currentPair.position?.negationEnd || 0,
},
isComplete: !!(
currentPair.key &&
@ -1281,6 +1380,7 @@ export function extractQueryPairs(query: string): IQueryPair[] {
key: currentPair.key,
operator: currentPair.operator || '',
value: currentPair.value,
hasNegation: currentPair.hasNegation || false,
position: {
keyStart: currentPair.position?.keyStart || 0,
keyEnd: currentPair.position?.keyEnd || 0,
@ -1288,6 +1388,8 @@ export function extractQueryPairs(query: string): IQueryPair[] {
operatorEnd: currentPair.position?.operatorEnd || 0,
valueStart: currentPair.position?.valueStart,
valueEnd: currentPair.position?.valueEnd,
negationStart: currentPair.position?.negationStart || 0,
negationEnd: currentPair.position?.negationEnd || 0,
},
isComplete: !!(
currentPair.key &&