hecks

Query

Words available inside query do ... end.

The tables on this page are generated from the language’s own aggregate-local syntax tables (lib/hecks/language/**/*.bluebook) by hecks language_run.project_reference — do not edit inside the markers. The prose between them is hand-written and survives regeneration.

Most of these run against examples/banking, whose Account and SafeDepositBox queries carry ordering, limits, and a real tenant boundary. reference_to, offset and nulls are written nowhere in the corpus, so they get a chapter of their own:

```ruby boot Hecks::Adapters::Folder.new.load_bluebooks(File.join(InMemoryDomain::ROOT, “examples/banking/bluebook”))

Hecks.hecksagon(“Banking”) do attaches “Governance” Banking::Customer.persisted_by(“Memory”) Banking::Account.persisted_by(“Memory”) Banking::SafeDepositBox.persisted_by(“Memory”) end Hecks.hecksagon(“Governance”) do Governance::RoleAssignment.persisted_by(“Memory”) Governance::RoleTransition.persisted_by(“Memory”) end


```ruby bluebook
Hecks.bluebook "QueryReference" do
  vision "The query words the corpus does not yet write."

  aggregate "Warden" do
    attribute :badge, Badge

    identified_by :badge
    value_object("Badge") { attribute :value, String }

    lifecycle :status, default: "on_duty" do
      transition "StandDown" => "off_duty", from: "on_duty"
    end

    command "Appoint" do
      sets :badge
      emits "WardenAppointed"
    end

    command "StandDown" do
      reference_to Warden
      emits "WardenStoodDown"
    end
  end

  aggregate "Sighting" do
    attribute :tag, Tag

    identified_by :tag
    reference_to Warden
    attribute :species, Species
    attribute :count,   Count, optional: true

    value_object("Tag")     { attribute :value, String }
    value_object("Species") { attribute :value, String }
    value_object("Count")   { attribute :value, Integer }
    value_object("Census") do
      attribute :species, String
      attribute :total,   Integer
    end

    command "Log" do
      attribute :warden,    Warden
      sets :tag
      sets :species
      sets :count
      emits "SightingLogged"
    end

    query "ByWarden" do
      description "Every sighting one warden logged."
      reference_to Warden, as: :warden
      where(warden: :warden)
      order_by :tag
    end

    query "Paged" do
      description "The second page of two."
      order_by :tag
      limit 2
      offset 1
    end

    query "ByCount" do
      description "Ordered by count, with the uncounted ones last."
      order_by :count
      nulls :last
    end

    # Answered by an adapter, not by the sightings on file: `returns` says in what shape.
    query "TallyBySpecies" do
      description "How many of each species the field guide counts."
      returns list_of(Census)
    end

    # A HOP — a field on the warden, not on the sighting.
    query "ByOffDutyWarden" do
      description "Sightings still filed against a warden who has stood down."
      where(:"warden/status" => "off_duty")
      order_by :tag
    end
  end

  aggregate "Lamp" do
    attribute :ref, LampRef
    attribute :lit_until, LampInstant, optional: true

    identified_by :ref
    value_object("LampRef")     { attribute :value, String }
    value_object("LampInstant") { attribute :value, Integer }

    command "Light" do
      attribute :ref, LampRef
      attribute :lit_until, LampInstant
      sets :ref
      sets :lit_until
      emits "LampLit"
    end

    # Asked with no `now`, the runtime answers it from the clock.
    query "Lit" do
      attribute :now, LampInstant
      needs :now
      where("lit_until.value": { gte: :now })
      order_by :ref
    end
  end
end

```ruby boot class RefFieldGuide def tally_by_species = [{ species: “heron”, total: 3 }, { species: “ibis”, total: 1 }] end

Hecks::Adapters.const_set(:RefFieldGuide, RefFieldGuide) unless Hecks::Adapters.const_defined?(:RefFieldGuide, false) Hecks.adapter(“RefFieldGuide”) { port “FieldGuide” }

A fixed clock, so the needs example does not depend on today’s date.

module QueryReferenceClock module_function

def now = 1_700_000_000 end Hecks::Adapters.const_set(:QueryReferenceClock, QueryReferenceClock) unless Hecks::Adapters.const_defined?(:QueryReferenceClock, false) Hecks.adapter(“QueryReferenceClock”) { port “clock” }

Hecks.hecksagon(“QueryReference”) do QueryReference::Warden.persisted_by(“Memory”) QueryReference::Sighting.persisted_by(“Memory”) QueryReference::Lamp.persisted_by(“Memory”)

QueryReference::Sighting.port “FieldGuide” do answers_query “TallyBySpecies” end end


```ruby
runtime.dispatch("Banking::Customer.Register", with: { reference: { value: "qy-1" },
                                                       name: { given: "Nancy", family: "Roman" },
                                                       email: { address: "nancy@example.com" } })
account = Banking::Account.open!(customer: "qy-1", number: { value: "qy-a1" },
                                kind: { name: "current" }, daily_limit: { cents: 50_000 })
account.credit!(amount: { cents: 9_000 }, narrative: { text: "funding" })

runtime.dispatch("QueryReference::Warden.Appoint", with: { badge: { value: "w-1" } })
runtime.dispatch("QueryReference::Warden.Appoint", with: { badge: { value: "w-2" } })
runtime.dispatch("QueryReference::Sighting.Log", with: { tag: { value: "s-1" }, warden: "w-1", species: { value: "heron" }, count: { value: 3 } })
runtime.dispatch("QueryReference::Sighting.Log", with: { tag: { value: "s-2" }, warden: "w-1", species: { value: "egret" } })
runtime.dispatch("QueryReference::Sighting.Log", with: { tag: { value: "s-3" }, warden: "w-2", species: { value: "ibis" }, count: { value: 1 } })

description

description description — fills description

| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | description |

A free-text label for the query — no rules attached, read by nothing but a human.

runtime.registry.bluebook("Banking").aggregate("Account").queries.find { |q| q.hecks_name == "Open" }.description  # => "Accounts that can transact today."

attribute

attribute name, type, default:, optional:, pattern:, matches:, admits:, one_of: — fills attributes

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | name | | positional 2 | constant | true | type | | default: | literal | false | default | | optional: | flag | false | optional | | pattern: | text | false | pattern | | matches: | symbol | false | matches | | admits: | text | false | admits | | one_of: | list | false | one_of |

Declares an argument this query accepts at ask-time, not a field on the aggregate — the :symbol a where value can resolve from (attribute :ceiling, Draft backs where(draft: { lt: :ceiling })). A :symbol naming no such attribute is refused when the bluebook builds.

Overdrawn declares attribute :floor, Money, and the caller supplies it at ask-time — it is a parameter, not a field on Account:

runtime.query("Banking::Account.Overdrawn", floor: { cents: 10_000 }).map { |row| row[:number][:value] }  # => ["qy-a1"]

Raise the floor and the same query answers differently, because the argument is the whole of what changed:

runtime.query("Banking::Account.Overdrawn", floor: { cents: 1_000 })  # => []

reference_to

reference_to type, as:, optional: — fills attributes

| argument | kind | required | fills | |—|—|—|—| | positional 1 | constant | true | type | | as: | symbol | false | name | | optional: | flag | false | optional |

Declares a query parameter that names another aggregate’s own identity, not a free string that only happens to look like one — reference_to Camper, as: :owner inside query "ByOwner" do ... end gives owner the same reference typing a real relationship deserves, usable in where(owner: :owner) exactly as any other attribute would be. Only the cross-reference shape: a query has no root of its own to act on, so there is no self-reference form here the way Command’s own reference_to has two of (see command.md) — just a plain attribute typed as a reference, as: naming it (bare, it would default to camper_id) and optional: working the same way it does anywhere else attribute accepts it.

ByWarden takes a warden’s own identity rather than a string that happens to look like one, and uses it in where the same as any other argument:

runtime.query("QueryReference::Sighting.ByWarden", warden: "w-1").map { |row| row[:tag][:value] }  # => ["s-1", "s-2"]
runtime.query("QueryReference::Sighting.ByWarden", warden: "w-2").map { |row| row[:tag][:value] }  # => ["s-3"]

where

where wheres — fills wheres

| argument | kind | required | fills | |—|—|—|—| | positional 1 | pairs | true | wheres |

Filters on eq, ne, gt, gte, lt, lte, in, and contains — a bare value is shorthand for eq. The seal requires the field to be a declared attribute or the lifecycle field, reached through any depth of dotted path as long as it lands on a scalar (landing on a value object is refused, and so is a field the aggregate never declared at all); the ordered comparators additionally require that scalar to hold a number. contains means real element membership on a list_of field and plain substring on anything else — identically on every engine, including a field whose own text carries a comma.

/ CROSSES INTO ANOTHER RECORD through a reference_to attribute, . WALKS FIELDS INSIDE THIS ONE (ADR 0025, “References”) — the operator alone says which kind of path this is. where(:"customer/status" => "suspended") on an aggregate that reference_to Customer filters on Customer’s own field, not anything the querying aggregate declares itself; where(:"customer.status" => ...) never hops, whatever customer names — it dead-ends the same way any dotted path onto a non-value-object does. The hop’s own segment name is the same one Adapters::Driving::Handle’s reference accessors answer to (account.customer in Ruby, :"customer/status" in a query — one name, both places), multi-hop chains read left to right (:"engagement/client/status"), and a hop is where-only — order_by through a hop is refused outright, since an ask is ordered by what its own rows hold and a hop answers with a candidate set, not a sort key. See the queries-and-read-models guide for the exact refusal wording, including the existential-negation case (a nil or dangling reference never satisfies a hop clause, whatever the comparator).

A bare value is eq — Open is where(status: "open"):

runtime.query("Banking::Account.Open").size  # => 1

An ordered comparator reads the caller’s argument. HighBalance is where(balance: { gte: :floor }):

runtime.query("Banking::Account.HighBalance", floor: { cents: 9_000 }).size  # => 1
runtime.query("Banking::Account.HighBalance", floor: { cents: 9_001 }).size  # => 0

A dotted path hops through a reference and reads a field on the TARGET. ByOffDutyWarden asks about the warden, not the sighting — nothing on a Sighting says anything about duty:

runtime.query("QueryReference::Sighting.ByOffDutyWarden")  # => []
runtime.dispatch_flat("QueryReference::Warden.StandDown", warden: "w-1")
runtime.query("QueryReference::Sighting.ByOffDutyWarden").map { |row| row[:tag][:value] }  # => ["s-1", "s-2"]

Nothing about the sightings changed — the hop read the warden, and the sightings filed against the warden still on duty are not in the answer:

runtime.query("QueryReference::Sighting.ByOffDutyWarden").map { |row| row[:warden] }.uniq  # => ["w-1"]

Banking carries the same shape in OpenForSuspendedCustomers — open accounts whose customer has since been suspended. It now answers nothing, ever, and that is the domain working rather than the query failing: FreezeAccountsOnSuspension freezes every open account the moment its customer is suspended, so the dangerous state this query exists to surface can no longer be reached.

Banking::Customer.find("qy-1").suspend!(standing: { value: "under-review" })
Banking::Account.find("qy-a1").status  # => "frozen"
runtime.query("Banking::Account.OpenForSuspendedCustomers")  # => []

Put back, so the sections below start where this one found things — a reference page runs top to bottom against one boot:

Banking::Customer.find("qy-1").reinstate!.status  # => "active"

order_by

order_by order_field, order_way — fills order_field

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | order_field | | positional 2 | symbol | false | order_way |

Names the field to sort by, ascending unless the second argument is :desc. The runtime always breaks ties by record identity underneath whatever you declare, so an ask never silently falls back to store order.

ByWarden orders by tag, and the rows come back in that order rather than the order they were logged:

runtime.query("QueryReference::Sighting.ByWarden", warden: "w-1").map { |row| row[:tag][:value] }  # => ["s-1", "s-2"]

HighBalance is the descending case — order_by :balance, :desc:

high = runtime.registry.bluebook("Banking").aggregate("Account").queries.find { |q| q.hecks_name == "HighBalance" }
high.order_by.direction  # => :desc

authorize

authorize policy, tenant: — fills options

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | policy | | tenant: | symbol | false | tenant |

Declares a policy name (recorded, never checked — no caller-identity or grant system exists to check it against) and, when tenant: is given, a mandatory tenant boundary that IS enforced: a caller must pass that field as an argument or the ask refuses with Unauthorized (Runtime::TenantScope), and every returned row — on every engine, Memory, Sqlite, Postgres, and the entity/sub-list path alike — is scoped to the value given, regardless of what other filters were declared.

SafeDepositBox.Rented declares authorize :vault_access, tenant: :branch_code. Two boxes, in two branches:

Banking::SafeDepositBox.rent!(customer: "qy-1", branch_code: { value: "DT" }, box_number: { value: 1 }, size: { value: "small" })
Banking::SafeDepositBox.rent!(customer: "qy-1", branch_code: { value: "UP" }, box_number: { value: 1 }, size: { value: "large" })

The tenant boundary is mandatory — an ask that does not say which branch it is standing in is refused rather than answered broadly:

runtime.query("Banking::SafeDepositBox.Rented")  # ~> Unauthorized: pass branch_code:

Name it, and every row is scoped to it. The other branch’s box is not in the answer, and no where said so:

runtime.query("Banking::SafeDepositBox.Rented", branch_code: { value: "DT" }).map { |row| row[:branch_code][:value] }  # => ["DT"]

The policy name beside it is the half that is NOT checked — nothing in this runtime knows what :vault_access would mean:

rented = runtime.registry.bluebook("Banking").aggregate("SafeDepositBox").queries.find { |q| q.hecks_name == "Rented" }
rented.authorization.policy  # => "vault_access"

inspect_query

inspect_query mode — fills options

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | false | mode |

Asks to inspect the compiled query rather than (or alongside) its rows. In practice this is a capability gate more than a feature — Ports::Query.validate! only refuses when an adapter neither implements its own inspect_query hook nor exposes a native query method under the default :sql mode. No adapter in this codebase defines the former, so today declaring it never changes what comes back; it can only ever refuse.

Nothing in the corpus declares it, and the reason is in the sentence above — against the Memory adapter it is a capability gate with nothing behind it:

runtime.registry.bluebook("Banking").aggregate("Account").queries.map(&:inspection).compact  # => []

Which is the whole of what it does today. The rows are the same rows either way — a hint nobody reads is a comment with a syntax.

Written exemption (ADR 0025 principle 4) — a real corpus use would be vacuous by construction: no adapter here implements the hook this declares, so declaring it on a real query would change nothing the query itself does and would not exercise any path this doctest above doesn’t already. The honest gap is upstream of the DSL word — an adapter that actually implements inspect_query is what would give this a real corpus use worth having.

returns

returns type — fills returns

| argument | kind | required | fills | |—|—|—|—| | positional 1 | constant | true | type |

Names the value object of the owning aggregate that shapes this query’s answer, and so says that something outside the domain answers it: returns Position for one row, returns list_of(Position) for many. A query that returns nothing is answered with the aggregate’s own rows, from its stored records. The hecksagon binds a returned query to a port with answers_query (see the DomainPort page); the shape lives here, in the bluebook, because it is the meaning of the answer. Each row an adapter answers is built as the value object, so its typed attributes, pattern:, admits: and invariants all apply, and a wrong-shaped answer is refused before it enters the domain.

Sighting.TallyBySpecies returns a list of Census rows, a value object beside it, and the FieldGuide port’s adapter answers them:

runtime.query("QueryReference::Sighting.TallyBySpecies").map { |row| [row[:species], row[:total]] }  # => [["heron", 3], ["ibis", 1]]

The declaration reads back from the aggregate:

sighting = runtime.registry.bluebook("QueryReference").aggregate("Sighting")
tally = sighting.queries.find { |query| query.hecks_name == "TallyBySpecies" }
[tally.returns, tally.returns_list?]  # => ["list_of(Census)", true]

needs

needs fact — fills needs

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | fact |

A fact the query needs from outside the records, answered by the runtime before its filter reads the arguments: the same word, and the same facts, as a command’s needs. needs :now fills the query’s own now argument with the clock port’s reading, in whole epoch seconds (UTC), and needs :today fills today with the day that reading falls in, whole days since the epoch (UTC). The caller’s own value wins, even a null, so a test or a back-fill can ask as of another time. The query must declare an attribute of that name: needs :now with no attribute :now is refused when the bluebook is built, as is a fact the runtime cannot supply or one declared twice.

Lamp.Lit asks which lamps are still lit as of now, and declares that it needs it. Two lamps are lit, one until a little before the clock reads and one until a little after:

runtime.dispatch("QueryReference::Lamp.Light", with: { ref: { value: "old" }, lit_until: { value: 1_699_999_000 } })
runtime.dispatch("QueryReference::Lamp.Light", with: { ref: { value: "new" }, lit_until: { value: 1_700_000_500 } })

Left out, now is the clock’s reading (1,700,000,000 here), so only the later lamp is lit. Named, the caller’s own time is kept, so the same query can be asked as of another moment:

runtime.query("QueryReference::Lamp.Lit").map { |row| row[:ref][:value] }  # => ["new"]
runtime.query("QueryReference::Lamp.Lit", now: { value: 1_699_999_000 }).map { |row| row[:ref][:value] }  # => ["new", "old"]

Unlike a command, a query writes no event, so nothing records the answer: the fill is for that run’s filter alone, and the query log echoes the arguments the caller offered. The IR carries "needs": [{"fact": "now"}] on a query that declares one and nothing on a query that does not.

limit

limit limit — fills limit

| argument | kind | required | fills | |—|—|—|—| | positional 1 | number | true | limit |

Caps how many rows survive after ordering runs. Declare order_by first if you mean to keep a particular slice — limit trims what ordering already sorted, it doesn’t decide which rows those are.

Paged declares limit 2 over three sightings, so only two survive:

runtime.query("QueryReference::Sighting.Paged").size  # => 2

offset

offset value — fills options

| argument | kind | required | fills | |—|—|—|—| | positional 1 | number | true | value |

Skips that many rows after ordering, for paging. Refused together with cursor on the same query — see cursor.

Paged declares limit 2 and offset 1 over three sightings, so it skips the first and takes the two after it — the order SQL’s own LIMIT n OFFSET m reads, and now what every engine here answers:

runtime.query("QueryReference::Sighting.Paged").map { |row| row[:tag][:value] }  # => ["s-2", "s-3"]

Skip-then-take is the part that matters, and it is the difference between a second page and an empty one: taking two and THEN skipping one would answer a single row here, and nothing at all at limit 10, offset 10.

cursor

cursor value — fills options

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | value |

Refused at build (QueryBuilder#seal_cursor, raises Malformed). No interpreter implements cursor pagination — declaring cursor here is always an error, not a silent no-op. Use limit/offset instead.

There is no working example to write, and that is the documentation — the word refuses where it is written, before anything can boot:

Hecks::Bluebook::DSL::QueryBuilder.build("Paged") { cursor :tag }  # ~> Malformed: no interpreter implements cursor pagination

Written exemption (ADR 0025 principle 4) — ADR 0025’s own table asked for corpus use here, with no exemption listed, but a WORD that refuses unconditionally at build has no corpus use to give: any declaration anywhere would refuse the bluebook that carried it, so “a real chapter uses cursor” and “the corpus builds” are mutually exclusive claims. S15 (ADR 0026, “the Paging sub-language”) is where this gets resolved for real — cursor/offset/nulls/limit leave the core grammar into their own attachment — landing a real corpus use here first would be work S15 immediately throws away.

nulls

nulls mode — fills options

| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | mode |

Sets how nulls sort relative to real values. This one is genuinely enforced: both the in-memory ordering path and compiled SQL read it to place nulls consistently rather than leaving it to whatever the store happens to do.

count is optional on a Sighting, so one of the three has none. ByCount declares nulls :last, and the uncounted sighting sorts after the counted ones rather than wherever the store would have put it:

runtime.query("QueryReference::Sighting.ByCount").map { |row| row[:tag][:value] }  # => ["s-3", "s-1", "s-2"]

s-2 is the one with no count, and it is last — the two real values sort among themselves first.