diff --git a/tutor/database.py b/tutor/database.py index 868921a..5263bd7 100644 --- a/tutor/database.py +++ b/tutor/database.py @@ -102,38 +102,56 @@ async def advanced_search( params = {} sets = [] - logging.debug("Performing search for: %s", search) + logger.debug("Performing search for: %s", search) for i, criterion in enumerate(search.criteria): param = f"param_{i}" if isinstance(criterion, tutor.search.Name): constraints.append(f"cards.name LIKE :{param}") - params[param] = f"%{criterion.name}%" + params[param] = f"%{criterion.text}%" if isinstance(criterion, tutor.search.Type): constraints.append(f"cards.type_line LIKE :{param}") - params[param] = f"%{criterion.name}%" - if isinstance(criterion, tutor.search.IsExpansion): + params[param] = f"%{criterion.text}%" + if isinstance(criterion, tutor.search.Expansion): constraints.append(f"cards.set_code LIKE :{param}") params[param] = criterion.set_code - if isinstance(criterion, tutor.search.InExpansion): - sets.append(criterion.set_code) - if isinstance(criterion, tutor.search.IsColor): - constraints.append(f"cards.color_identity LIKE :{param}") - params[param] = tutor.models.Color.to_string(criterion.colors) - if isinstance(criterion, tutor.search.IsRarity): - constraints.append(f"cards.rarity LIKE :{param}") - params[param] = str(criterion.rarity) + if isinstance(criterion, tutor.search.Color): + if criterion.operator == tutor.search.Operator.matches: + constraints.append(f"cards.color_identity LIKE :{param}") + params[param] = tutor.models.Color.to_string(criterion.colors) + if criterion.operator == tutor.search.Operator.lte: + constraints.append( + "({})".format( + " OR ".join( + [ + f"cards.color_identity LIKE :{param}_{color}" + for color in criterion.colors + ] + ) + ) + ) + params.update( + {f"{param}_{color}": str(color) for color in criterion.colors} + ) + params[param] = tutor.models.Color.to_string(criterion.colors) + if criterion.operator == tutor.search.Operator.gte: + constraints.append(f"cards.color_identity LIKE :{param}") + params[param] = "%{}%".format( + "%".join(tutor.models.Color.to_string(criterion.colors)) + ) + if isinstance(criterion, tutor.search.Rarity): + if criterion.operator == tutor.search.Operator.matches: + constraints.append(f"cards.rarity LIKE :{param}") + params[param] = str(criterion.rarity) + if criterion.operator == tutor.search.Operator.lte: + constraints.append(f"rarities.rarity_ord <= :{param}") + params[param] = criterion.rarity.value + if criterion.operator == tutor.search.Operator.gte: + constraints.append(f"rarities.rarity_ord >= :{param}") + params[param] = criterion.rarity.value if isinstance(criterion, tutor.search.Oracle): constraints.append(f"cards.oracle_text LIKE :{param}") params[param] = f"%{criterion.text}%" - if sets: - set_params = {f"set_{i}": set_code for i, set_code in enumerate(sets)} - constraints.append( - "cards.set_code IN ({})".format( - ", ".join([f":{key}" for key in set_params.keys()]) - ) - ) - params.update(set_params) if in_collection is not None: if in_collection: joins.append("JOIN copies ON (cards.scryfall_id = copies.scryfall_id)") @@ -157,6 +175,7 @@ async def advanced_search( f"LIMIT {offset},{limit}", ] ) + logger.debug("Query: %s", (query, params)) cursor = await db.execute(query, params) rows = await cursor.fetchall() return [ diff --git a/tutor/search.py b/tutor/search.py index c19978f..854bb87 100644 --- a/tutor/search.py +++ b/tutor/search.py @@ -1,4 +1,5 @@ import dataclasses +import enum import functools import typing @@ -7,38 +8,35 @@ import parsy import tutor.models +class Operator(enum.Enum): + matches = ":" + gte = ">=" + lte = "<=" + + +@dataclasses.dataclass class Criterion: - ... + operator: Operator @dataclasses.dataclass -class IsExpansion(Criterion): +class Expansion(Criterion): set_code: parsy.string @dataclasses.dataclass -class InExpansion(Criterion): - set_code: parsy.string - - -@dataclasses.dataclass -class IsColor(Criterion): +class Color(Criterion): colors: typing.Set[tutor.models.Color] @dataclasses.dataclass -class IsRarity(Criterion): +class Rarity(Criterion): rarity: tutor.models.Rarity -@dataclasses.dataclass -class Name(Criterion): - name: parsy.string - - @dataclasses.dataclass class Type(Criterion): - name: parsy.string + text: parsy.string @dataclasses.dataclass @@ -46,6 +44,12 @@ class Oracle(Criterion): text: parsy.string +@dataclasses.dataclass +class Name(Criterion): + operator: Operator = Operator.matches + text: parsy.string = "" + + @dataclasses.dataclass class Search: criteria: typing.List[Criterion] @@ -63,7 +67,10 @@ W, U, B, G, R = ( tutor.models.Color.Green, tutor.models.Color.Red, ) -matches = parsy.string(":") + +matches = parsy.string(":").map(Operator) +gte = parsy.string(">=").map(Operator) +lte = parsy.string("<=").map(Operator) color = ( ustring("w").result(W) @@ -112,38 +119,56 @@ wedge = ( | lstring("temur").result({U, R, G}) ) -colors = single_color | guild | shard | wedge | multicolor +any_color = single_color | guild | shard | wedge | multicolor + +colors = parsy.seq( + _keyword=lstring_from("c", "color", "colors"), + operator=matches | gte | lte, + colors=any_color, +).combine_dict(Color) expansion_string = ( parsy.regex(r"[a-zA-Z0-9]+").map(lambda s: s.upper()).desc("expansion set code") ) -is_expansion = lstring_from("e", "expansion", "s", "set") >> matches >> expansion_string.map(IsExpansion) +expansion = parsy.seq( + _keyword=lstring_from("e", "expansion", "s", "set"), + operator=matches, + set_code=expansion_string, +).combine_dict(Expansion) -in_expansion = lstring("in") >> matches >> expansion_string.map(InExpansion) +# in_expansion = lstring("in") >> matches >> expansion_string.map(InExpansion) -rarity = ( +any_rarity = ( lstring_from("c", "common").result(tutor.models.Rarity.Common) | lstring_from("u", "uncommon").result(tutor.models.Rarity.Uncommon) | lstring_from("r", "rare").result(tutor.models.Rarity.Rare) | lstring_from("m", "mythic").result(tutor.models.Rarity.Mythic) ) -is_rarity = lstring_from("r", "rarity") >> matches >> rarity.map(IsRarity) - -is_color = lstring_from("c", "color", "colors") >> matches >> colors.map(IsColor) +rarity = parsy.seq( + _keyword=lstring_from("r", "rarity"), + operator=matches | gte | lte, + rarity=any_rarity, +).combine_dict(Rarity) string_literal = parsy.regex(r'"[^"]*"').map(lambda s: s[1:-1]) | parsy.regex(r"[^\s]+") -has_type = lstring_from("t", "type") >> matches >> string_literal.map(Type) +type_line = parsy.seq( + _keyword=lstring_from("t", "type"), + operator=matches, + text=string_literal, +).combine_dict(Type) -has_oracle = lstring_from("o", "oracle") >> matches >> string_literal.map(Oracle) +oracle = parsy.seq( + _keyword=lstring_from("o", "oracle"), + operator=matches, + text=string_literal, +).combine_dict(Oracle) name = string_literal.map(Name) -criterion = ( - is_expansion | in_expansion | is_rarity | is_color | has_type | has_oracle | name -) +criterion = colors | expansion | rarity | type_line | oracle | name padding = parsy.regex(r"\s*") search = padding >> (criterion << padding).many().map(Search)