Skip to main content

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 comparator is_eq),
  • None in a filter dict means column IS NULL (it's equivalent to the comparator is_null()),
  • everything else uses an explicit comparator like is_gt(...), is_in([...]), is_ilike(...), etc.

Full list of comparators

FunctionSQLDescription
is_eq(value)... = valueEqual to (same as passing value directly)
is_neq(value)... <> valueNot equal to
is_gt(value)... > valueGreater than
is_gte(value)... >= valueGreater than or equal
is_lt(value)... < valueLess than
is_lte(value)... <= valueLess than or equal
is_between(value1, value2)... BETWEEN value1 AND value2Inclusive 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 patternPattern match (case-sensitive)
is_not_like(pattern)... NOT LIKE patternNegated pattern match
is_ilike(pattern)... ILIKE patternPattern match (case-insensitive)
is_not_ilike(pattern)... NOT ILIKE patternNegated case-insensitive pattern match
is_null()... IS NULLColumn is NULL (same as passing None)
is_not_null()... IS NOT NULLColumn 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.