On this page
Query grammar
A query selects events by type and tags. The same grammar serves query in a read and failIfEventsMatch in an Append
Condition. It follows the DCB specification.
Key words in capitals follow RFC 2119.
Shape
A query is either the string "*", which matches every event, or an array of query items:
[
{
"types": ["user-created", "user-updated"],
"identifiers": [ { "name": "userId", "value": "123" } ],
"metadata": [ { "name": "tenantId", "value": "acme" } ]
},
{ "types": ["some-other-event"] }
]Matching
- The items are combined with OR: an event matches the query if it matches any item.
- Within one item, the three keys are combined with AND:
types: OR, the event’s type is one of the listed types;identifiers: AND, the event carries every listed identifier;metadata: AND, the event carries every listed metadata entry.
- A key left out doesn’t filter.
- Identifiers and metadata are separate: a
userIdinmetadatadoesn’t match auserIdasked for inidentifiers. - Values are compared exactly, byte for byte: case counts, and no Unicode normalization happens.
- An identifier with several values on the event (see Events) matches if any one of them is the value asked for.
Rules
- Every array (the query itself,
types,identifiers,metadata) MUST be non-empty when present. An empty array gets400, instead of meaning something special. To leave an axis open, leave the key out. To match every event, use"*". - An item MUST name at least one of
types,identifiers, ormetadata. An empty item ({}) gets400: it poses no constraint. - There is no negation (“not X”). A negation describes an unlimited set of events, so nothing could guarantee that no future event breaks a condition built on it.
- A query carries at most 100 items, and an item at most 100 values across
types,identifiers, andmetadatacombined. A larger query gets400. These limits are fixed, not configuration, and they’re counted after duplicate items are dropped. - Two items that are exact duplicates (same types, identifiers, and metadata, in any order) are kept once, silently. A client that merges several queries into one doesn’t have to deduplicate them first.
Why the size limits. They keep the SQL a query turns into well inside SQLite’s limits on expression depth and bound
parameters. A query of about 1,000 terms would otherwise fail inside SQLite, instead of getting a clear 400.
Shared test cases
A client library matches its own pending events against queries, in memory (see Client libraries). Its matcher MUST follow this grammar exactly, or a command could miss one of its own pending events without any error.
The repository publishes shared test cases for that,
testdata/query-cases.json: each case
is a query, an event, and whether it matches. A test in internal/store checks every case against the SQL the server
runs; a client library replays them against its matcher.