Using comparators
Comparators describe a single column condition in a where / values filter. Anywhere the Tables SDK accepts a filter dict — select, select_one, select_df, update, delete — you can replace a plain value with a comparator to express ranges, lists, pattern matches, null checks, and more.
The rule of thumb:
- a plain value in a filter dict means
column = value(it's equivalent to the comparatoris_eq), Nonein a filter dict meanscolumn IS NULL(it's equivalent to the comparatoris_null()),- everything else uses an explicit comparator like
is_gt(...),is_in([...]),is_ilike(...), etc.
Full list of comparators
| Function | SQL | Description |
|---|---|---|
is_eq(value) | ... = value | Equal to (same as passing value directly) |
is_neq(value) | ... <> value | Not equal to |
is_gt(value) | ... > value | Greater than |
is_gte(value) | ... >= value | Greater than or equal |
is_lt(value) | ... < value | Less than |
is_lte(value) | ... <= value | Less than or equal |
is_between(value1, value2) | ... BETWEEN value1 AND value2 | Inclusive range |
is_in(values) | ... IN (...) | Value is in the given list |
is_not_in(values) | ... NOT IN (...) | Value is not in the given list |
is_like(pattern) | ... LIKE pattern | Pattern match (case-sensitive) |
is_not_like(pattern) | ... NOT LIKE pattern | Negated pattern match |
is_ilike(pattern) | ... ILIKE pattern | Pattern match (case-insensitive) |
is_not_ilike(pattern) | ... NOT ILIKE pattern | Negated case-insensitive pattern match |
is_null() | ... IS NULL | Column is NULL (same as passing None) |
is_not_null() | ... IS NOT NULL | Column is not NULL |
All comparators are imported from abstra.tables:
from abstra.tables import (
is_eq, is_neq,
is_gt, is_gte, is_lt, is_lte,
is_between,
is_in, is_not_in,
is_like, is_not_like,
is_ilike, is_not_ilike,
is_null, is_not_null,
)
Equality and inequality
is_eq is the explicit form of the default behavior and we recommend relying in the default behavior instead of explicitly using this comparator. Use is_neq when you want the opposite behavior.
from abstra.tables import select, is_eq, is_neq
# These two calls are equivalent:
select("users", where={"status": "active"})
select("users", where={"status": is_eq("active")})
# Everyone whose status is NOT "active":
select("users", where={"status": is_neq("active")})
Ranges (is_gt, is_gte, is_lt, is_lte, is_between)
Compare numeric, date, or datetime columns. The example below shows is_gt in select, is_gte in update, and is_lt in delete:
from datetime import date, timedelta
from abstra.tables import select, update, delete, is_gt, is_gte, is_lt
# SELECT — orders above 1000
big_orders = select(
"orders",
where={"total_cents": is_gt(100_000)},
)
# UPDATE — mark all VIPs (>= 10k lifetime spend)
update(
"users",
set={"tier": "vip"},
where={"lifetime_spend_cents": is_gte(1_000_000)},
)
# DELETE — drop logs older than 30 days
delete(
"logs",
{"created_at": is_lt((date.today() - timedelta(days=30)).isoformat())},
)
is_between covers an inclusive range (BETWEEN low AND high):
from abstra.tables import select, is_between
# Users aged 18 to 25 inclusive
select("users", where={"age": is_between(18, 25)})
Lists (is_in, is_not_in)
Use a list of values for IN / NOT IN checks:
from abstra.tables import select, delete, is_in, is_not_in
# Select users in any of these regions
select(
"users",
where={"region": is_in(["NA", "EU", "LATAM"])},
)
# Delete logs that aren't from the prod environment
delete(
"logs",
{"env": is_not_in(["production"])},
)
Null checks (is_null, is_not_null)
None is treated as IS NULL, so is_null() is rarely needed, use it when you want to be explicit. is_not_null() has no shortcut and is the standard way to express IS NOT NULL:
from abstra.tables import select, delete, is_not_null
# These two are equivalent:
select("users", where={"deleted_at": None})
# from abstra.tables import is_null
# select("users", where={"deleted_at": is_null()})
# Users that have already verified their email:
select("users", where={"verified_at": is_not_null()})
# Cleanup: drop unverified pending invites
delete("invites", {"accepted_at": None, "status": "pending"})
Pattern matching (is_like, is_ilike, and their negations)
is_like is case-sensitive and is_ilike is case-insensitive. Both take a SQL pattern string with two wildcards:
%matches any sequence of characters, including the empty string. So"abc%"matches anything starting with"abc"("abc","abcdef", …),"%@gmail.com"matches anything ending with"@gmail.com", and"%silva%"matches anything that contains"silva"anywhere._matches exactly one character. So"a_c"matches"abc","axc","a9c".
Any character that isn't % or _ is matched literally. To match a literal % or _ in the column value, prefix it with \ (e.g. "50\%" to match the string "50%").
from abstra.tables import select, update, delete, is_like, is_ilike, is_not_ilike
# SELECT — case-sensitive prefix match
select(
"products",
where={"sku": is_like("APX-%")},
)
# SELECT — case-insensitive contains
select(
"users",
where={"name": is_ilike("%silva%")},
)
# UPDATE — flag suspicious emails
update(
"users",
set={"flagged": True},
where={"email": is_ilike("%@tempmail.%")},
)
# DELETE — drop test accounts (case-insensitive), but keep anything
# that doesn't look like a test prefix
delete(
"users",
{"email": is_ilike("test+%@%")},
)
# SELECT — everything that is NOT a noreply address
select(
"messages",
where={"from_email": is_not_ilike("noreply@%")},
)
Combining comparators in one filter
All keys in a single filter dict are combined with AND. Use this to layer comparators:
from datetime import date, timedelta
from abstra.tables import select, is_gte, is_in, is_ilike, is_not_null
active_premium_brazilians = select(
"users",
where={
"status": "active",
"tier": is_in(["premium", "vip"]),
"country": "BR",
"email": is_ilike("%@%.com.br"),
"last_login_at": is_gte((date.today() - timedelta(days=30)).isoformat()),
"deleted_at": None, # IS NULL
"verified_at": is_not_null(), # IS NOT NULL
},
)
For conditions that can't be expressed as a flat AND of column checks (joins, OR across columns, subqueries), drop down to run_sql.