queries — Named Reads

Named, compiled SQL queries served by the query service. No arbitrary SQL — each query is validated against the projection schema at compile time.

queries:
  my_concert_history:
    from: user_concert_stats
    input:
      user_id: { type: string, required: true }
      limit:   { type: integer, required: false }
    fields:
      - user_concert_stats.*
    where:
      user_id:
        eq: input.user_id
    order_by:
      last_checkin_at: desc
    limit: "input.limit ?: 50"

Query fields

FieldRequiredDescription
fromyesSource projection table name
inputnoNamed query parameters with types
fieldsnoColumns to return (default: all)
wherenoFilter conditions
order_bynoSort columns and direction
limitnoStatic integer or "input.param ?: default"
joinsnoJoin other projections (max 5)
group_bynoFor aggregate queries
aggregatenoAggregate functions
distinctnotrue for SELECT DISTINCT
coalesce_zeronoList of columns to replace NULL with 0
windownoAllowed time windows for gte_window

where operators

OperatorDescription
eqEquals
neqNot equals
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal
inValue in list
ninValue not in list
likeSQL LIKE pattern
ilikeCase-insensitive LIKE
containsText search (TEXT/STRING columns only)
is_nullColumn is NULL
is_not_nullColumn is not NULL
gte_windowTime-window filter (requires window.allowed)

Joins

Maximum 5 joins per query. type is inner (default), left, right, or full.

Single equality (shorthand):

joins:
  show_directory:
    type: left
    on:
      show_experience_detail.show_id: show_directory.show_id
    fields:
      - show_directory.title
      - show_directory.starts_at

Composite ON (multi-column):

joins:
  parent_rows:
    type: inner
    on:
      pairs:
        - { left: child_rows.tenant_id, right: parent_rows.tenant_id }
        - { left: child_rows.parent_id, right: parent_rows.parent_id }

Up to 16 equalities per join.

Aggregate queries

⚠️

count under fields: is silently dropped. Always use group_by + aggregate:.

queries:
  unread_notification_count:
    from: user_notifications
    input:
      user_id: { type: string, required: true }
    where:
      user_id: { eq: input.user_id }
      read:    { eq: false }
    group_by:
      - user_id                    # single group = scalar result
    aggregate:
      unread_count:
        count: notification_id
    limit: 1

Aggregate functions: count, sum, avg, min, max.

Window queries

For bucketed projections, declare allowed windows:

queries:
  hourly_checkins:
    from: hourly_checkin_counts
    input:
      venue_id: { type: string, required: true }
      window:   { type: string, required: true }
    where:
      venue_id:     { eq: input.venue_id }
      bucket_start: { gte_window: input.window }
    window:
      allowed: ["1h", "24h", "7d"]

Misc query options

# Replace NULL with 0 in result rows
coalesce_zero: [shows_attended, venues_visited]
 
# SELECT DISTINCT (requires explicit fields, cannot combine with group_by/aggregate)
distinct: true