From 94a5bc41262d6ec1db9ce1dfd9c1e2700d7344c4 Mon Sep 17 00:00:00 2001 From: Alexander Dao Date: Fri, 3 Oct 2025 10:31:48 -0400 Subject: [PATCH] Feat: Add TRUE and FALSE as search terms --- .../core/library/alchemy/visitors.py | 9 ++++++++- src/tagstudio/core/query_lang/ast.py | 12 ++++++++++++ src/tagstudio/core/query_lang/parser.py | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/tagstudio/core/library/alchemy/visitors.py b/src/tagstudio/core/library/alchemy/visitors.py index c24c8ed7a..0bb62b57a 100644 --- a/src/tagstudio/core/library/alchemy/visitors.py +++ b/src/tagstudio/core/library/alchemy/visitors.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, override import structlog -from sqlalchemy import ColumnElement, and_, distinct, func, or_, select +from sqlalchemy import ColumnElement, and_, distinct, false, func, or_, select, true from sqlalchemy.orm import Session from sqlalchemy.sql.operators import ilike_op @@ -18,6 +18,7 @@ AST, ANDList, BaseVisitor, + Boolean, Constraint, ConstraintType, Not, @@ -116,6 +117,12 @@ def visit_constraint(self, node: Constraint) -> ColumnElement[bool]: # type: ig def visit_property(self, node: Property) -> ColumnElement[bool]: # type: ignore raise NotImplementedError("This should never be reached!") + def visit_boolean(self, node: Boolean) -> ColumnElement[bool]: + if node.value: + return true() + else: + return false() + @override def visit_not(self, node: Not) -> ColumnElement[bool]: # type: ignore return ~self.visit(node.child) diff --git a/src/tagstudio/core/query_lang/ast.py b/src/tagstudio/core/query_lang/ast.py index 0323bf26d..125815be7 100644 --- a/src/tagstudio/core/query_lang/ast.py +++ b/src/tagstudio/core/query_lang/ast.py @@ -94,6 +94,12 @@ def __init__(self, child: AST) -> None: super().__init__() self.child = child +class Boolean(AST): + value: bool + + def __init__(self, value: bool) -> None: + super().__init__() + self.value = value T = TypeVar("T") @@ -110,6 +116,8 @@ def visit(self, node: AST) -> T: return self.visit_property(node) elif isinstance(node, Not): return self.visit_not(node) + elif isinstance(node, Boolean): + return self.visit_boolean(node) raise Exception(f"Unknown Node Type of {node}") # pragma: nocover @abstractmethod @@ -131,3 +139,7 @@ def visit_property(self, node: Property) -> T: @abstractmethod def visit_not(self, node: Not) -> T: raise NotImplementedError() # pragma: nocover + + @abstractmethod + def visit_boolean(self, node: Boolean) -> T: + raise NotImplementedError() # pragma: nocover diff --git a/src/tagstudio/core/query_lang/parser.py b/src/tagstudio/core/query_lang/parser.py index ff17465d7..01aca3c4c 100644 --- a/src/tagstudio/core/query_lang/parser.py +++ b/src/tagstudio/core/query_lang/parser.py @@ -6,6 +6,7 @@ from tagstudio.core.query_lang.ast import ( AST, ANDList, + Boolean, Constraint, ConstraintType, Not, @@ -81,6 +82,12 @@ def __term(self) -> AST: if isinstance(term, Not): # instead of Not(Not(child)) return child return term.child return Not(term) + if self.__is_next_true(): + self.__eat(TokenType.ULITERAL) + return Boolean(value = True) + if self.__is_next_false(): + self.__eat(TokenType.ULITERAL) + return Boolean(value = False) if self.next_token.type == TokenType.RBRACKETO: self.__eat(TokenType.RBRACKETO) out = self.__or_list() @@ -92,6 +99,18 @@ def __term(self) -> AST: def __is_next_not(self) -> bool: return self.next_token.type == TokenType.ULITERAL and self.next_token.value.upper() == "NOT" # pyright: ignore + def __is_next_true(self) -> bool: + return ( + self.next_token.type == TokenType.ULITERAL + and self.next_token.value.upper() == "TRUE" # pyright: ignore + ) + + def __is_next_false(self) -> bool: + return ( + self.next_token.type == TokenType.ULITERAL + and self.next_token.value.upper() == "TRUE" # pyright: ignore + ) + def __constraint(self) -> Constraint: if self.next_token.type == TokenType.CONSTRAINTTYPE: constraint = self.__eat(TokenType.CONSTRAINTTYPE).value