この資料は英語です
chobo のリファレンスのうち、言語・形式・出力先の三つは英語だけで書いています。日本語で読めるのは、このサイトの日本語のページと、診断コードの一覧です。
The chobo language
A book (.book) holds three things: the units things are counted in, the accounts they are kept
in with the bounds each account keeps, and the kinds of transfer that move them from account to
account. This page is the whole language. formats.md has the JSON chobo reads and
writes, targets.md what chobo build writes for PostgreSQL, TigerBeetle and the
MySQL family, and codes.md every diagnostic.
A book
The inventory example (examples/inventory/inventory.book):
book inventory v1
description "Stock per SKU. A delivery adds to it, an order holds what it takes for 30 minutes, shipping posts the hold, cancelling voids it, and a return puts the goods back on the shelves"
unit pcs
account stock(sku: string) : pcs
description "what is on the shelves"
at least 0 refused as out_of_stock
account suppliers : pcs outside
account customers : pcs outside
transfer receive(delivery: string, sku: string, qty: pcs)
key delivery, sku
move qty from suppliers to stock(sku)
transfer reserve(order: string, sku: string, qty: pcs)
description "posted when the order ships, voided when it is cancelled"
key order, sku
pending expires after 30 minutes
move qty from stock(sku) to customers
transfer take_back(return_id: string, sku: string, qty: pcs)
description "what a customer sent back, on the shelves again"
key return_id, sku
move qty from customers to stock(sku)
The first line names the book and its version. Indentation makes the blocks, and # starts a
comment that runs to the end of the line. The keywords are English; the names (of the book, the
units, the accounts, the transfers, their parameters and the reasons of refusal) can be in any
language, and the examples have a Japanese twin beside each book (inventory.ja.book).
Units
An amount is a whole number of the smallest step of its unit: one piece, one cent. scale says
how many decimal places the literals of the book (bounds and amounts) are written with, and
chobo doc shows: in USD scale 2, 12.50 is 1250 cents. An amount is from 0 to 2⁶³ − 1
(what a bigint of PostgreSQL or MySQL holds), and a bound from −(2⁶³ − 1) to 2⁶³ − 1.
The account a move takes from, the account it puts into, and its amount are in one unit (E010). chobo converts nothing: an exchange is two moves, one in each unit, each through an account outside the book, with both amounts worked out by the caller.
A unit of money may end with whether its amounts are with tax or without: incl_tax or
excl_tax. Nothing in the book changes with it; it is for amounts handed over from a rule or a
workflow, which are to say the same. The book hands its units to ritsu's other languages as the
units rulec writes:
| Unit | As the other languages see it |
|---|---|
a currency's name (円, JPY, an ISO 4217 code, 銭, USDc) with scale 0 |
that money: money[円], or with its tax money[円, incl_tax] |
a currency's name other than 円 and JPY, with scale 2 |
its hundredth: unit USD scale 2 is money[USDc] |
a unit of mass, length, area, volume or time (g, kg, L, h) with scale 0 |
that unit: mass[kg] |
anything else (pcs, seats, pt, yen with scale 2, and the units of temperature and of sound, ℃, ℉ and dB, which do not add up) |
a count with its name and nothing else, the same only as itself |
Only the money of the first two rows takes a tax (E014); the hundredth of a yen is 銭.
Accounts
account points(member: string) : pt
at least 0 refused as not_enough_points
at most 1000000 refused as points_cap
account issued : pt outside
- Parameters. An account kind is split by its parameters, which are strings: one account for
each member, each SKU, each day. An account (
points("m-1")) is there from its first use, with a balance of 0. - The balance is what came in, less what went out. An account outside the book (
outside) stands for the world the book trades with, a supplier, the buyers, a bank: it has no bounds, and its balance goes below 0 as things come from it. - Bounds. An account of the book (one that is not
outside) hasat least,at mostor both (E020), each with the reason a transfer that would break it is refused with (refused as, E023). That name is what a caller gets back, and the name of the error a dandori task declares. An upper bound is 0 or more (E022); a lower bound may be above 0 (keep 3 on the shelf) or below it (credit down to −50000). - A bound is a constant of the account kind. A limit that differs from account to account (a member's credit line) is an account of its own, which a transfer fills.
A bound is the only condition a book can write, and that is on purpose: a bound looks at one account, so the write that changes the account can check it, with nothing else to read in between. A rule over two accounts, a refund never exceeds the sale, becomes the bound of an account of its own: what is left to refund on each order, which the sale fills and a refund takes from, and which never goes below 0 (examples/refunds).
Transfers
transfer sale(order: string, shop: string, to_shop: USD, fee: USD)
description "to_shop and fee add up to what the buyer paid; the caller works out the fee"
key order
move to_shop from buyers to shop_balance(shop)
move fee from buyers to fees
- Parameters are strings or amounts of a unit.
keynames the parameters that make a call once only (E030): the same key with the same arguments does nothing the second time, and with other arguments is refused. An amount cannot be part of the key (E031).pendingmakes the transfer hold first:pending expires after <n> seconds|minutes|hours|days(from 1 second to 2³² − 1 seconds, E041) orpending never expires(E040). Without it, the transfer moves at once.move <amount> from <account> to <account>: the amount is a parameter or a literal, and the accounts take the transfer's parameters (or string literals) as their arguments, by position. A transfer has one move or more (E013), made in the order written, all or none.
A transfer computes nothing: an amount is given, not worked out from a balance or another amount. A fee, a rate or a rounding belongs to the caller, or to a rulec rule the caller asks.
Operations and what they answer
| Operation | On | What it does |
|---|---|---|
do |
a transfer that moves at once | makes the moves |
hold |
a transfer that holds first | holds what the moves would move |
post |
the same | makes the hold final: all of it, or the amounts given (the rest goes back) |
void |
the same | lets the hold go |
Each answers one of:
done: done now.done_before: the same call, with the same key and the same contents, was done before; nothing happens.refused, with a reason: nothing happens.
A refusal is an answer of the business (there is no stock), not a failure, so it comes back as a result, not an exception. A failure is something else: a database that cannot be reached, arguments of the wrong type, an amount out of range, or a balance that would leave the range of a 64-bit integer. A call that failed may be retried with the same arguments; its key keeps it from moving twice.
Two reads go with them: the balance of an account (what is posted, held going out, and held
coming in), and the status of a hold (held, posted, voided or expired, or none).
How a bound is checked
The moves are made one at a time, in the order written, and each is checked against the balances the moves before it left:
- a move of
aout of an account withat least Lgoes through only whenposted − held_out − a ≥ L: what is held going out counts, what is held coming in does not; - a move of
ainto an account withat most Ugoes through only whenposted + held_in + a ≤ U: what is held coming in counts, what is held going out does not.
A hold is checked the same way when it is made, and adds to held_out and held_in; posting it
moves what it holds from held to posted, and voiding or expiry lets it go. Neither is checked
again: the hold was checked for the worst case when it was made, so a post never breaks a bound.
When a move is refused, the moves before it are undone, and the answer is the reason of the first bound refused. Moves in the order written can refuse what the moves together would allow: a transfer that takes from an account before putting into it is refused when the account is short at that point (W103), and in a hold, what one move holds coming in cannot be taken by another (W104).
Keys
- Keys are kept apart by transfer kind and operation:
postandvoidtake the key of the hold. - The contents compared are every argument for
doandhold, the amount of each move forpost(all of it counts as the amount held), and nothing forvoid. - The same key with the same contents answers
done_before; with other contents,key_conflict. - A key a bound refused stays refused: the next call with it answers
already_refused, whatever it carries, even once the balance is enough. To try again, use another key (with the attempt in it). - A key refused for another reason (
same_account,no_such_hold, the state of a hold) is not spent. - A hold's key cannot be used again after the hold has ended: holding again with it answers
done_before, and holds nothing.
The life of a hold
| The hold | post |
void |
it expires |
|---|---|---|---|
| held | posted (refused with expired from its expiry on) |
voided (likewise) | expired |
| posted | done_before with the same amounts, key_conflict with others |
already_posted |
— |
| voided | already_voided |
done_before |
— |
| expired | expired |
expired |
— |
| no hold with the key | no_such_hold |
no_such_hold |
— |
A post with amounts posts each move for what its amount parameter says now, and lets the rest
go; a move with a literal amount is posted in full. A post of more than a move holds is refused
with over_hold. A hold expires when its expiry comes, outside the caller's hands; one that
never expires is ended by the caller alone. chobo api gives the life of a hold as a state
machine, in the shape dandori reads a rulec rule's in, for a workflow's checker to follow
(dandori does not read it yet).
The reasons chobo gives
| Reason | When | Does it spend the key |
|---|---|---|
a bound's own (out_of_stock) |
a move would break the bound | yes: already_refused from then on |
key_conflict |
the same key with other contents | no: the call before stands |
already_refused |
a key a bound refused, again | — |
same_account |
a move whose two accounts are the same (the same arguments) | no |
no_such_hold |
a post or a void of a hold that is not there | no |
already_posted |
a void of a posted hold | no |
already_voided |
a post of a voided hold | no |
expired |
a post or a void of an expired hold | no |
over_hold |
a post of more than the hold holds | no |
chobo check lists, for each operation of each transfer, the reasons it can be refused with,
each found by a short list of operations that the reference interpreter runs to it; --format
json, chobo api and chobo doc show those operations.
Scenarios
A scenario is a list of steps: operations, pass <duration> (the clock of the reference
interpreter moves on, and the holds whose expiry it reaches expire), and together (the
operations of two callers or more at the same time, eight at most). chobo run runs one, and
for together gives every way it can come out, trying every order the callers' operations can
interleave in. chobo scenarios writes them for a book: each bound just before, at and past
it, each key used twice, every way a hold ends, a refusal partway through the moves, and two
callers after the last of something. formats.md has their JSON.
Versions
A book's version (v1) is not part of what an account or a transfer is: a new version goes on
with the balances and holds the database has. So chobo check --diff-base <revision> compares
the book with the one at that git revision, and refuses what the data could not follow: a change
to an account kind's unit, scale, parameters, bounds or whether it is outside (E050), and to a
transfer's parameters, key, hold or moves (E051); an account kind or a transfer that is gone, or
a book with another name, is a warning (W107). To change a bound, declare a new account kind and
write a transfer that moves the balance.
Names and keywords
A name starts with a letter or _, and goes on with letters, digits and _. Names are compared
as they are written, without Unicode normalization; a name with a combining mark (a dakuten or
an accent written as a character of its own) is refused (E001), to be written precomposed. ℃ and
℉, which are signs and not letters, are a unit's name where one goes, unit ℃ and the unit after
the : of an account or a parameter, as the other languages read them; anywhere else, in a name
too, they are E001.
| Where | Keywords |
|---|---|
| at the start of a line | book description unit account transfer key pending move |
| within a line | scale incl_tax excl_tax outside at least at most refused as expires after never expires from to |
| as a type | string |
after expires after <n>, and only there |
second seconds minute minutes hour hours day days |
No name may be a word of the first three rows. The words of durations are keywords only after
expires after <n>, so a parameter can be called day.
Diagnostics
Every diagnostic has a code, the line it is about, and, when it only shows when the book is
called, the operations that get there, which the checker ran in the reference interpreter before
saying so. codes.md has all 32 codes, as chobo explain --all prints them, and
chobo explain <code> prints one.