BountyHub Docs
Dataset Structured Filters
Filter project dataset queries with the typed FilterNode AST: operators, AND/OR/NOT groups, value types, time ranges, history, and the UI filter builder.
Dataset structured filters
Datasets support two kinds of filtering:
- SQL — put a
WHEREclause in your query. This is the most flexible and runs inside DuckDB. - Structured filters — a typed filter tree sent alongside the query. The service compiles it into a parameterized SQL predicate, so values are bound, never interpolated.
The Hub's Filters panel builds structured filters for you. The same AST is
available in QueryDatasetRequest.filter (and ExportDatasetRequest.filter),
where values are bound as SQL parameters. Project-wide queries filter with SQL
WHERE clauses instead.
#UI filter builder
The Query tab starts with no filters. Use Add filter, choose a column and an operator, enter a value, then press Run.
- Match all / any combines the conditions with AND or OR.
- Operators are limited to the selected column kind (see below).
inandnot inaccept comma-separated values.- Booleans use a true/false selector; timestamps use a date-time input.
- is empty / is not empty are
IS NULL/IS NOT NULLand need no value. - Filters are not applied live; they take effect when the query runs.
#Operators
| Operator | Enum | Values | Compiled predicate |
|---|---|---|---|
| Is | FILTER_OP_EQ | 1 | "column" = ? |
| Is not | FILTER_OP_NE | 1 | "column" <> ? |
> | FILTER_OP_GT | 1 | "column" > ? |
>= | FILTER_OP_GTE | 1 | "column" >= ? |
< | FILTER_OP_LT | 1 | "column" < ? |
<= | FILTER_OP_LTE | 1 | "column" <= ? |
| Contains | FILTER_OP_CONTAINS | 1 | strpos("column", ?) > 0 |
| Starts with | FILTER_OP_STARTS_WITH | 1 | starts_with("column", ?) |
| Ends with | FILTER_OP_ENDS_WITH | 1 | ends_with("column", ?) |
| In | FILTER_OP_IN | 1+ | "column" IN (?, …) |
| Not in | FILTER_OP_NOT_IN | 1+ | "column" NOT IN (?, …) |
| Is empty | FILTER_OP_IS_NULL | 0 | "column" IS NULL |
| Is not empty | FILTER_OP_IS_NOT_NULL | 0 | "column" IS NOT NULL |
Text operators are case-sensitive and literal: contains uses strpos, so
% and _ have no wildcard meaning. Use SQL ILIKE if you need
case-insensitive matching.
#Groups and negation
- An empty AND group compiles to
TRUE; an empty OR group compiles toFALSE. notwraps any node:NOT (<predicate>).- Column names must be valid identifiers. They are quoted, and all values are parameterized.
#Where filters run
This distinction matters when your SQL aggregates or renames columns.
#Single-dataset queries and exports
QueryDataset and ExportDataset apply the structured filter outside your
SELECT:
So the filter sees your query's output columns:
- With the default
SELECT * FROM dataset, every schema column plus__ingested_atand__deletedis available. - If you project
SELECT host, count(*) AS n ..., onlyhostandnexist. Referencingseverityfails; filter on the output name or move the predicate into the SQLWHERE.
#Project-wide queries
QueryDatasets does not use the structured filter builder. Filter inside the
SQL with a WHERE clause:
The per-relation filters field on QueryDatasetsRequest is reserved and is
not supported by the current execution path.
#Views
ExecuteDatasetView has no filter or time-range fields. Put predicates in the
view SQL.
#Time ranges and history
| Field | Applies to | Notes |
|---|---|---|
ingestedAtFrom | __ingested_at >= value | Same “outside your SELECT” rule as filters. |
ingestedAtTo | __ingested_at <= value | Must not be before ingestedAtFrom. |
includeHistory | Switches the relation to raw observations | Filters and time bounds apply to history too. |
The default SELECT * FROM dataset includes __ingested_at. If you project a
subset and set a time range, include __ingested_at in the projection, or
filter inside SQL.
The Hub's date-time inputs use the browser's local time zone. API clients send RFC 3339 timestamps.
#API examples
#AND group
#OR group with a negation
integerValue is an int64 and therefore a JSON string. Timestamps are RFC 3339
strings inside timestampValue.
#Value types
| Column kind | Value field | Example |
|---|---|---|
| Text | textValue | {"textValue":"api.example.com"} |
| Integer | integerValue (string) | {"integerValue":"200"} |
| Number | numberValue | {"numberValue":9.8} |
| Boolean | booleanValue | {"booleanValue":true} |
| Timestamp | timestampValue | {"timestampValue":"2026-01-01T00:00:00Z"} |
Values must match the column kind. The UI builds the correct value type from the schema; API clients should do the same. A mismatch surfaces as a DuckDB binder or conversion error.
SQL NULL comparisons never match, so is = null is not a useful filter — use
is empty (IS NULL).
#Errors
| Error | Cause |
|---|---|
Invalid dataset filter | Unsupported operator, wrong number of values, or an empty value (except null checks). |
DuckDB could not prepare the query | The column does not exist in the filtered scope, or a value type does not match. |
Invalid filter for dataset '<name>' | A project-wide RelationFilter failed to compile against that relation. |
#Related
- SQL reference — write the query the filter wraps.
- Overview — current state vs history.
- API and CLI — request shapes.