From 3d4b6499039105278fa1538feaebe5cffd15f320 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Thu, 28 Nov 2024 21:31:27 +0100 Subject: [PATCH] implement NOT --- .../src/core/library/alchemy/visitors.py | 17 ++++++++-- tagstudio/src/core/query_lang/ast.py | 18 +++++++++-- tagstudio/src/core/query_lang/parser.py | 32 +++++++++++-------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/tagstudio/src/core/library/alchemy/visitors.py b/tagstudio/src/core/library/alchemy/visitors.py index 46c653e9..2b649b41 100644 --- a/tagstudio/src/core/library/alchemy/visitors.py +++ b/tagstudio/src/core/library/alchemy/visitors.py @@ -1,10 +1,10 @@ -from sqlalchemy import and_, or_ +from sqlalchemy import and_, or_, select from sqlalchemy.sql.expression import ColumnExpressionArgument from src.core.media_types import MediaCategories from src.core.query_lang import BaseVisitor -from src.core.query_lang.ast import ANDList, Constraint, ConstraintType, ORList, Property +from src.core.query_lang.ast import ANDList, Constraint, ConstraintType, Not, ORList, Property -from .models import Entry, Tag, TagAlias +from .models import Entry, Tag, TagAlias, TagBoxField class SQLBoolExpressionBuilder(BaseVisitor): @@ -38,7 +38,18 @@ class SQLBoolExpressionBuilder(BaseVisitor): elif node.type == ConstraintType.FileType: return Entry.suffix.ilike(node.value) + # raise exception if Constraint stays unhandled raise NotImplementedError("This type of constraint is not implemented yet") def visit_property(self, node: Property) -> None: return + + def visit_not(self, node: Not) -> ColumnExpressionArgument: + return ~Entry.id.in_( + # TODO TSQLANG there is technically code duplication from Library.search_library here + select(Entry.id) + .outerjoin(Entry.tag_box_fields) + .outerjoin(TagBoxField.tags) + .outerjoin(TagAlias) + .where(self.visit(node.child)) + ) diff --git a/tagstudio/src/core/query_lang/ast.py b/tagstudio/src/core/query_lang/ast.py index 47376652..820a79c5 100644 --- a/tagstudio/src/core/query_lang/ast.py +++ b/tagstudio/src/core/query_lang/ast.py @@ -35,9 +35,9 @@ class AST: class ANDList(AST): - terms: list[Union["ORList", "Constraint"]] + terms: list[Union["ORList", "Constraint", "Not"]] - def __init__(self, terms: list[Union["ORList", "Constraint"]]) -> None: + def __init__(self, terms: list[Union["ORList", "Constraint", "Not"]]) -> None: super().__init__() for term in terms: term.parent = self @@ -78,6 +78,14 @@ class Property(AST): self.value = value +class Not(AST): + child: AST + + def __init__(self, child: AST) -> None: + super().__init__() + self.child = child + + T = TypeVar("T") @@ -91,6 +99,8 @@ class BaseVisitor(ABC, Generic[T]): return self.visit_constraint(node) elif isinstance(node, Property): return self.visit_property(node) + elif isinstance(node, Not): + return self.visit_not(node) raise Exception(f"Unknown Node Type of {node}") @abstractmethod @@ -108,3 +118,7 @@ class BaseVisitor(ABC, Generic[T]): @abstractmethod def visit_property(self, node: Property) -> T: raise NotImplementedError() + + @abstractmethod + def visit_not(self, node: Not) -> T: + raise NotImplementedError() diff --git a/tagstudio/src/core/query_lang/parser.py b/tagstudio/src/core/query_lang/parser.py index 5631da5b..6f0735c5 100644 --- a/tagstudio/src/core/query_lang/parser.py +++ b/tagstudio/src/core/query_lang/parser.py @@ -1,6 +1,6 @@ from typing import Union -from src.core.query_lang.ast import AST, ANDList, Constraint, ORList, Property +from src.core.query_lang.ast import AST, ANDList, Constraint, Not, ORList, Property from src.core.query_lang.tokenizer import ConstraintType, Token, Tokenizer, TokenType from src.core.query_lang.util import ParsingError @@ -25,6 +25,18 @@ class Parser: raise ParsingError(self.next_token.start, self.next_token.end, "Syntax Error") return out + def __or_list(self) -> ORList: + terms = [self.__and_list()] + + while self.__is_next_or(): + self.__eat(TokenType.ULITERAL) + terms.append(self.__and_list()) + + return ORList(terms) + + def __is_next_or(self) -> bool: + return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "OR" + def __and_list(self) -> ANDList: elements = [self.__term()] while self.next_token.type != TokenType.EOF and not self.__is_next_or(): @@ -42,19 +54,10 @@ class Parser: def __is_next_and(self) -> bool: return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "AND" - def __or_list(self) -> ORList: - terms = [self.__and_list()] - - while self.__is_next_or(): + def __term(self) -> Union[ORList, Constraint, Not]: + if self.__is_next_not(): self.__eat(TokenType.ULITERAL) - terms.append(self.__and_list()) - - return ORList(terms) - - def __is_next_or(self) -> bool: - return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "OR" - - def __term(self) -> Union["ORList", "Constraint"]: + return Not(self.__term()) if self.next_token.type == TokenType.RBRACKETO: self.__eat(TokenType.RBRACKETO) out = self.__or_list() @@ -63,6 +66,9 @@ class Parser: else: return self.__constraint() + def __is_next_not(self) -> bool: + return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "NOT" + def __constraint(self) -> Constraint: if self.next_token.type == TokenType.CONSTRAINTTYPE: self.last_constraint_type = self.__eat(TokenType.CONSTRAINTTYPE).value