Skip to content

Commit

Permalink
refactor: use IS NOT NULL if word is a wildcard (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
ninoseki committed Mar 28, 2024
1 parent 06d0e7d commit 4ef47cc
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 21 deletions.
9 changes: 7 additions & 2 deletions sqlmodel_filters/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Word,
)
from luqum.visitor import TreeVisitor
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.sql._typing import _ColumnExpressionArgument
from sqlmodel import SQLModel, and_, not_, or_, select
from sqlmodel.sql.expression import Select, SelectOfScalar
Expand All @@ -34,7 +35,7 @@ def __init__(self, node: Item, *, model: type[ModelType], name: str):
self.model = model

@property
def field(self):
def field(self) -> InstrumentedAttribute:
try:
return getattr(self.model, self.name)
except AttributeError as e:
Expand All @@ -50,7 +51,11 @@ def _phrase_expression(self, phrase: Phrase):
def _word_expression(self, word: Word):
casted = cast_by_annotation(word.value, self.annotation)
if isinstance(casted, str):
yield self.field.like(str(LikeWord(casted)))
l_word = LikeWord(casted)
if l_word.is_wildcard:
yield self.field.isnot(None)
else:
yield self.field.like(str(LikeWord(casted)))
else:
yield self.field == casted

Expand Down
47 changes: 28 additions & 19 deletions sqlmodel_filters/components.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
# Lucene:
# - ?: a single character wildcard search
# - *: multiple character wildcard search
# SQL LIKE:
# - _: Represents a single character
# - %: Represents zero or more characters
WILDCARD_TABLE = {
"?": "_",
"*": "%",
}


def replace_wildcards(s: str, *, table: dict[str, str] = WILDCARD_TABLE):
for k, v in table.items():
from types import MappingProxyType


def replace_wildcards(s: str, *, mapping: MappingProxyType[str, str]):
for k, v in mapping.items():
s = s.replace(k, v)

return s


class LikeWord:
def __init__(self, value: str, *, table: dict[str, str] = WILDCARD_TABLE):
# Lucene:
# - ?: a single character wildcard search
# - *: multiple character wildcard search
# SQL LIKE:
# - _: Represents a single character
# - %: Represents zero or more characters
WILDCARD_MAPPING = MappingProxyType({"?": "_", "*": "%"})

def __init__(self, value: str):
self.value = value
self.table = table

@property
def is_wildcard(self) -> bool:
return self.value == "*"

@property
def wildcards(self):
return self.WILDCARD_MAPPING.keys()

@property
def has_wildcard(self) -> bool:
return any(wildcard in self.value for wildcard in self.wildcards)

def __str__(self):
wildcards = self.table.keys()
if any(wildcard in self.value for wildcard in wildcards):
return replace_wildcards(self.value)
if self.has_wildcard:
return replace_wildcards(self.value, mapping=self.WILDCARD_MAPPING)

return f"%{self.value}%"
14 changes: 14 additions & 0 deletions tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ def test_equal(builder: SelectBuilder, session: Session):
assert heros[0].name == "Spider-Boy"


def test_is_not_null(builder: SelectBuilder):
statement = builder(parse("name:*"))

assert normalize_multiline_string(
str(compile_with_literal_binds(statement)) # type: ignore
) == normalize_multiline_string(
"""
SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.created_at
FROM hero
WHERE hero.name IS NOT NULL
"""
)


@pytest.mark.parametrize(
("q", "expected"),
[
Expand Down

0 comments on commit 4ef47cc

Please sign in to comment.