diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d62a43d..deaeb62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v5.0.2 + +### Changes + +* When using the `@file` operator in queries, the values are matched exactly with the [SQL + `IN` operator](https://sqlite.org/src/doc/tip/src/in-operator.md). + * The `@like` operator no longer has any effect when use in conjunction with `@file` + ## v5.0.1 ### Changes diff --git a/digiarch/__version__.py b/digiarch/__version__.py index 2fe5fde1..3a223dde 100644 --- a/digiarch/__version__.py +++ b/digiarch/__version__.py @@ -1 +1 @@ -__version__ = "5.0.1" +__version__ = "5.0.2" diff --git a/digiarch/commands/log.py b/digiarch/commands/log.py index 0084c372..cd981b78 100644 --- a/digiarch/commands/log.py +++ b/digiarch/commands/log.py @@ -39,7 +39,7 @@ def cmd_log(ctx: Context, runs_only: bool, order: str, limit: int): query: TQuery = [] if runs_only: - query = [("operation", "%:start", True), ("operation", "%:end", True)] + query = [("operation", "%:start", "like"), ("operation", "%:end", "like")] with open_database(ctx, get_avid(ctx)) as database: events: list[Event] = list(query_table(database.log, query, [("time", order)], limit)) diff --git a/digiarch/query.py b/digiarch/query.py index 09be05b9..c2d8a154 100644 --- a/digiarch/query.py +++ b/digiarch/query.py @@ -16,11 +16,11 @@ M = TypeVar("M", bound=BaseModel) FC = TypeVar("FC", bound=Callable[..., Any]) -TQuery = list[tuple[str, str | bool | Type[Ellipsis] | None, bool]] +TQuery = list[tuple[str, str | bool | Type[Ellipsis] | list[str] | None, str]] # field name, value(s), operation token_quotes = re_compile(r'(? tuple[str, list[str]]: @@ -34,21 +34,32 @@ def query_to_where(query: TQuery) -> tuple[str, list[str]]: for field, values in query_fields.items(): where_field: list[str] = [] - for value, like in values: - if value is None: - where_field.append(f"{field} is null") - elif value is Ellipsis: - where_field.append(f"{field} is not null") - elif value is True: - where_field.append(f"{field} is true") - elif value is False: - where_field.append(f"{field} is false") - elif like: - where_field.append(f"{field} like ?") - parameters.append(value) - else: - where_field.append(f"{field} = ?") - parameters.append(value) + for value, op in values: + match (value, op): + case None, "is": + where_field.append(f"{field} is null") + case None, "is not": + where_field.append(f"{field} is not null") + case True, "is": + where_field.append(f"{field} is true") + case True, "is not": + where_field.append(f"{field} is false") + case False, "is": + where_field.append(f"{field} is false") + case False, "is not": + where_field.append(f"{field} is true") + case _, "in" if isinstance(value, list): + where_field.append(f"{field} in ({','.join(['?'] * len(value))})") + parameters.extend(value) + case _, "in" if isinstance(value, str): + where_field.append(f"instr({field}, ?) != 0") + parameters.append(value) + case _, "=": + where_field.append(f"{field} = ?") + parameters.append(value) + case _, "like": + where_field.append(f"{field} like ?") + parameters.append(value) where.append(f"({' or '.join(where_field)})") @@ -66,13 +77,13 @@ def tokenize_query(query_string: str, default_field: str, allowed_fields: list[s for token in tokens: if token == "@null": - query_tokens.append((field, None, False)) + query_tokens.append((field, None, "is")) elif token == "@notnull": - query_tokens.append((field, ..., True)) + query_tokens.append((field, None, "is not")) elif token == "@true": - query_tokens.append((field, True, False)) + query_tokens.append((field, True, "is")) elif token == "@false": - query_tokens.append((field, False, False)) + query_tokens.append((field, True, "is not")) elif token == "@like": like = True elif token == "@file": @@ -84,15 +95,15 @@ def tokenize_query(query_string: str, default_field: str, allowed_fields: list[s from_file = False elif from_file: with open(token) as fh: - query_tokens.extend([(field, line_, like) for line in fh.readlines() if (line_ := line.strip())]) + query_tokens.append((field, [line for l in fh.readlines() if (line := l.rstrip("\r\n"))], "in")) else: - query_tokens.append((field, token, like)) + query_tokens.append((field, token, "like" if like else "=")) return query_tokens def argument_query(required: bool, default: str, allowed_fields: list[str] | None = None) -> Callable[[FC], FC]: - def callback(ctx: Context, param: Parameter, value: str | None) -> list[tuple[str, str, bool]]: + def callback(ctx: Context, param: Parameter, value: str | None) -> TQuery: if not (value := value or "").strip() and required: raise MissingParameter(None, ctx, param) if not value: diff --git a/pyproject.toml b/pyproject.toml index 76e0cd01..d81a914d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "digiarch" -version = "5.0.1" +version = "5.0.2" description = "Tools for the Digital Archive Project at Aarhus Stadsarkiv" authors = ["Aarhus Stadsarkiv "] license = "GPL-3.0" @@ -101,6 +101,7 @@ ignore = [ "DTZ006", # datetime.datetime.fromtimestamp() called without a tz argument "E501", # line to long, to many false positives, gets handled by black "E712", # comparison to True/False, we ignore because we use sqlalchemy + "E741", # Ambiguous variable name: `l` "FBT001", # boolean arguement in function definition "INP001", # implicit namespace without __init__ (throws errors in tests) "ISC001", # check for implicit concatanation of str on one line, not compatabil with black.