Filters

pgContext accepts Qdrant-style JSON filters and converts them into a typed AST before any SQL planning happens. The typed renderer builds SQL predicate plans for registered PostgreSQL columns and JSONB paths, then binds predicate values through SPI parameters during search, count, facet, and payload mutation execution. Malformed filters are rejected before they reach SQL rendering.

Boolean filters use must, should, and must_not arrays:

{
  "must": [
    { "key": "tenant_id", "match": "acme" },
    { "key": "price", "range": { "gte": 10, "lt": 20 } }
  ],
  "should": [
    { "key": "metadata.topic", "match": { "value": "billing" } }
  ],
  "must_not": [
    { "key": "archived", "match": true }
  ]
}

The parser currently accepts scalar match values, match.value, match.any, match.except, range bounds (gt, gte, lt, lte), is_null, and is_empty.

Unknown object fields are rejected. Empty filters are rejected. Filter depth, condition count, field-key length, and dotted path depth are bounded by pgContext policy defaults before later planning phases resolve fields or render SQL predicates.

Before a filter can be rendered as SQL, every field key must resolve against registered filterable fields. Ordinary column fields resolve to validated SQL column identifiers. JSONB fields resolve to a validated JSONB column identifier and structural path segments. Unknown keys are rejected instead of being treated as raw SQL identifiers or JSON path text.

Rendered predicate plans contain SQL text with positional placeholders plus an ordered parameter list. Predicate values and JSONB path segments are represented as parameters instead of being interpolated into SQL text. Each parameter also records its SQL type intent, so the PostgreSQL adapter can bind JSONB paths as text[], JSONB comparison values as jsonb, and ordinary column values through the surrounding SQL type context.

Use pgcontext.register_jsonb_path to expose a JSONB path as a filter and facet field. Missing path values behave like SQL NULL for facets and are omitted from counts.

Registered filter fields also define the payload mutation surface. Qdrant-style set_payload, delete_payload, and clear_payload can update only registered ordinary columns and JSONB paths, and they require source-table UPDATE privilege. This prevents arbitrary payload keys from silently mutating unregistered source-table columns.

Field Semantics

Ordinary columns and JSONB paths deliberately follow PostgreSQL null and type rules instead of inventing a separate document-store truth table.

  • Missing ordinary columns cannot be referenced. Registering a missing column or filtering on an unregistered field fails with SQLSTATE 42703 or 22023.
  • SQL NULL ordinary-column values do not match scalar equality, match.any, match.except, or range predicates. Use is_null when null membership is the intended predicate.
  • Missing JSONB paths are treated like SQL NULL: they do not match scalar or range predicates, and facets omit them from counts.
  • JSONB null at a registered path is also treated as null for filter and facet purposes.
  • Empty arrays and empty JSON objects are values, not missing fields. Use is_empty for empty-container checks; scalar equality and range predicates do not treat them as strings.
  • Wrong-type comparisons do not coerce silently. Numeric range predicates apply to JSON numbers and numeric-compatible ordinary columns. Strings that look numeric remain strings unless PostgreSQL casts the registered ordinary column through its declared SQL type.
  • Mixed numeric/string comparisons are intentionally not normalized across representations. Register the field with the representation clients should query, and keep payload writers consistent.
  • Unknown object fields inside the filter JSON are rejected during parsing. Unknown filter keys are rejected during field resolution, before SQL is rendered.