Skip to content

Query Builder

SpecStar includes a high-level query builder for searching indexed resource fields and resource metadata.

Use it when you want expressive filtering logic in Python or when you want your HTTP qb expressions to mirror the same mental model.

If you need a complete method-by-method lookup, see the Query Builder reference.


When to use it

The query builder is useful for:

  • filtering lists by field values
  • combining multiple conditions with AND, OR, and NOT
  • building reusable search logic in services or tests
  • keeping search code readable instead of hand-writing JSON filter payloads

Basic Python usage

from specstar import QB

query = (
    QB["status"].eq("active")
    .filter(QB["priority"] >= 3)
    .exclude(QB["archived"].eq(True))
    .sort("-created_time")
    .page(1, 20)
)

results = manager.search_resources(query)

QB[...] returns a field-aware builder object, so comparison operators and helper methods can be chained naturally.


HTTP usage

The same ideas can be passed to the API through the qb query parameter:

GET /tasks?qb=(QB["status"] == "active") & (QB["priority"] >= 3)
GET /tasks?qb=QB["owner"].one_of(["alice", "bob"])
GET /tasks?qb=QB.created_time().last_n_days(7)

List endpoints are paginated by default. The startup default comes from the SPECSTAR_DEFAULT_QUERY_LIMIT environment variable, and you can still pass a different limit per request.

The server parses the expression with a safe AST parser.


Common patterns

Simple comparisons

QB["age"] > 18
QB["status"].eq("active")
QB["score"].between(80, 100)

String matching

QB["name"].contains("ali")
QB["email"].ends_with("@example.com")
QB["title"].icontains("urgent")
QB["code"].regex(r"^[A-Z]{3}")

List membership

QB["status"].in_(["draft", "review"])
QB["role"].not_in(["guest"])
QB["owner"].one_of(["alice", "bob"])

Null and value checks

QB["deleted_at"].is_null()
QB["email"].is_not_null()
QB["nickname"].is_blank()
QB["profile"].has_value()

Date helpers

QB.created_time().today()
QB.updated_time().this_week()
QB.created_time().last_n_days(30)

Sorting and pagination

QB["status"].eq("active").sort("-created_time", "+name")
QB["status"].eq("active").limit(10).offset(20)
QB["status"].eq("active").page(2, 10)
QB["status"].eq("active").first()

Metadata accessors

Use metadata helper methods when the filter targets resource metadata instead of indexed data.

QB.resource_id().starts_with("task-")
QB.current_revision_id().eq("rev-123")
QB.created_by().eq("admin")
QB.is_deleted().is_false()
QB.total_revision_count() > 3

All built-in metadata accessors are filterable and sortable, including resource_id, current_revision_id, created_time, updated_time, created_by, updated_by, is_deleted, schema_version, and total_revision_count.

Revision mirror fields

SpecStar stores a denormalized snapshot of the current revision's key attributes directly in ResourceMeta. These can be filtered and sorted without any extra revision reads:

QB.rev_status().eq("draft")                     # only resources with a draft current revision
QB.rev_status().eq("stable")                    # only stable
QB.rev_created_by().one_of(["alice", "bob"])    # current revision created by alice or bob
QB.rev_updated_by().ne("guest")                 # current revision not last touched by guest
QB.rev_created_time().last_n_days(7)            # current revision created in the past week
QB.rev_updated_time().this_month()              # current revision updated this month

These fields are kept in sync by SpecStar on every create(), update(), modify(), and switch() call.


Low-level alternative

If you need fully explicit structured queries, you can still build ResourceMetaSearchQuery objects manually:

from specstar.types import (
    DataSearchCondition,
    DataSearchOperator,
    ResourceMetaSearchQuery,
)

query = ResourceMetaSearchQuery(
    conditions=[
        DataSearchCondition(
            field_path="status",
            operator=DataSearchOperator.equals,
            value="open",
        ),
        DataSearchCondition(
            field_path="priority",
            operator=DataSearchOperator.greater_than_or_equal,
            value=3,
        ),
    ],
    limit=20,
)

results = manager.search_resources(query)

This is useful for generated clients or integrations that prefer explicit JSON-like structures.


Important limitations

  • queries only work reliably on metadata fields and indexed fields
  • if qb is used in HTTP requests, do not combine it with data_conditions, conditions, sorts, time-range / user filter params (created_time_start, created_time_end, updated_time_start, updated_time_end, created_bys, updated_bys), or revision filter params (rev_statuses, rev_created_bys, rev_updated_bys, rev_created_time_start, rev_created_time_end, rev_updated_time_start, rev_updated_time_end); conflicting requests return HTTP 422
  • is_deleted is the one exception: it may be combined with qb. The server ANDs it into the QB conditions automatically. Swagger always sends is_deleted=false by default, so QB expressions work in Swagger out of the box.
  • invalid or unsupported QB expressions return HTTP 400
  • URL limit and offset override pagination values defined inside the QB expression
  • for metadata filtering in QB mode (time ranges, creator filters, revision filters, etc.), include them directly in the expression — for example QB.created_time().last_n_days(7), QB.created_by().eq("alice"), or QB.rev_status().eq("draft")

QB error responses at a glance

Situation HTTP What to do
malformed or unsupported QB expression 400 fix the expression itself
qb combined with JSON conditions, sorts, or time-range/user filter params 422 choose either QB mode or individual query parameters

For the shared route-level error mapping, see the HTTP error reference page.


Good practices

  • index fields that you plan to search frequently
  • start with small filters and expand only when needed
  • use QB.all() and QB.any() for nested grouped logic
  • prefer QB for readability and JSON conditions for machine-generated requests