truegrain.filters

Constructors for structured filters.

A filter is never a SQL fragment. That is what keeps an ungoverned predicate inexpressible, and it is the reason these are small functions rather than a string builder.

Using them instead of hand-written dictionaries catches a misspelled operator at the call site rather than as a refusal from the server.

 1"""Constructors for structured filters.
 2
 3A filter is never a SQL fragment. That is what keeps an ungoverned predicate
 4inexpressible, and it is the reason these are small functions rather than a
 5string builder.
 6
 7Using them instead of hand-written dictionaries catches a misspelled operator at
 8the call site rather than as a refusal from the server.
 9"""
10
11from __future__ import annotations
12
13from typing import Any
14
15Filter = dict[str, Any]
16
17
18def _filter(dimension: str, op: str, *values: Any) -> Filter:
19    out: Filter = {"dimension": dimension, "op": op}
20    if values:
21        out["values"] = list(values)
22    return out
23
24
25def eq(dimension: str, value: Any) -> Filter:
26    """`dimension = value`."""
27    return _filter(dimension, "eq", value)
28
29
30def ne(dimension: str, value: Any) -> Filter:
31    """`dimension <> value`."""
32    return _filter(dimension, "ne", value)
33
34
35def in_(dimension: str, *values: Any) -> Filter:
36    """`dimension IN (...)`. Named with a trailing underscore; `in` is a keyword."""
37    if not values:
38        raise ValueError("in_ needs at least one value")
39    return _filter(dimension, "in", *values)
40
41
42def not_in(dimension: str, *values: Any) -> Filter:
43    """`dimension NOT IN (...)`."""
44    if not values:
45        raise ValueError("not_in needs at least one value")
46    return _filter(dimension, "not_in", *values)
47
48
49def gt(dimension: str, value: Any) -> Filter:
50    """`dimension > value`."""
51    return _filter(dimension, "gt", value)
52
53
54def gte(dimension: str, value: Any) -> Filter:
55    """`dimension >= value`."""
56    return _filter(dimension, "gte", value)
57
58
59def lt(dimension: str, value: Any) -> Filter:
60    """`dimension < value`."""
61    return _filter(dimension, "lt", value)
62
63
64def lte(dimension: str, value: Any) -> Filter:
65    """`dimension <= value`."""
66    return _filter(dimension, "lte", value)
67
68
69def between(dimension: str, low: Any, high: Any) -> Filter:
70    """`dimension BETWEEN low AND high`.
71
72    The engine refuses a reversed range rather than matching no rows, because an
73    empty result that looks like a real answer is worse than an error.
74    """
75    return _filter(dimension, "between", low, high)
76
77
78def is_null(dimension: str) -> Filter:
79    """`dimension IS NULL`."""
80    return _filter(dimension, "is_null")
81
82
83def is_not_null(dimension: str) -> Filter:
84    """`dimension IS NOT NULL`."""
85    return _filter(dimension, "is_not_null")
Filter = dict[str, typing.Any]
def eq(dimension: str, value: Any) -> dict[str, typing.Any]:
26def eq(dimension: str, value: Any) -> Filter:
27    """`dimension = value`."""
28    return _filter(dimension, "eq", value)

dimension = value.

def ne(dimension: str, value: Any) -> dict[str, typing.Any]:
31def ne(dimension: str, value: Any) -> Filter:
32    """`dimension <> value`."""
33    return _filter(dimension, "ne", value)

dimension <> value.

def in_(dimension: str, *values: Any) -> dict[str, typing.Any]:
36def in_(dimension: str, *values: Any) -> Filter:
37    """`dimension IN (...)`. Named with a trailing underscore; `in` is a keyword."""
38    if not values:
39        raise ValueError("in_ needs at least one value")
40    return _filter(dimension, "in", *values)

dimension IN (...). Named with a trailing underscore; in is a keyword.

def not_in(dimension: str, *values: Any) -> dict[str, typing.Any]:
43def not_in(dimension: str, *values: Any) -> Filter:
44    """`dimension NOT IN (...)`."""
45    if not values:
46        raise ValueError("not_in needs at least one value")
47    return _filter(dimension, "not_in", *values)

dimension NOT IN (...).

def gt(dimension: str, value: Any) -> dict[str, typing.Any]:
50def gt(dimension: str, value: Any) -> Filter:
51    """`dimension > value`."""
52    return _filter(dimension, "gt", value)

dimension > value.

def gte(dimension: str, value: Any) -> dict[str, typing.Any]:
55def gte(dimension: str, value: Any) -> Filter:
56    """`dimension >= value`."""
57    return _filter(dimension, "gte", value)

dimension >= value.

def lt(dimension: str, value: Any) -> dict[str, typing.Any]:
60def lt(dimension: str, value: Any) -> Filter:
61    """`dimension < value`."""
62    return _filter(dimension, "lt", value)

dimension < value.

def lte(dimension: str, value: Any) -> dict[str, typing.Any]:
65def lte(dimension: str, value: Any) -> Filter:
66    """`dimension <= value`."""
67    return _filter(dimension, "lte", value)

dimension <= value.

def between(dimension: str, low: Any, high: Any) -> dict[str, typing.Any]:
70def between(dimension: str, low: Any, high: Any) -> Filter:
71    """`dimension BETWEEN low AND high`.
72
73    The engine refuses a reversed range rather than matching no rows, because an
74    empty result that looks like a real answer is worse than an error.
75    """
76    return _filter(dimension, "between", low, high)

dimension BETWEEN low AND high.

The engine refuses a reversed range rather than matching no rows, because an empty result that looks like a real answer is worse than an error.

def is_null(dimension: str) -> dict[str, typing.Any]:
79def is_null(dimension: str) -> Filter:
80    """`dimension IS NULL`."""
81    return _filter(dimension, "is_null")

dimension IS NULL.

def is_not_null(dimension: str) -> dict[str, typing.Any]:
84def is_not_null(dimension: str) -> Filter:
85    """`dimension IS NOT NULL`."""
86    return _filter(dimension, "is_not_null")

dimension IS NOT NULL.