Aster Web Data / Connect your data
TigerBeetlePreview
How Aster Web Data publishes TigerBeetle accounts and transfers, and which reads and writes the ledger can answer.
If you keep balances in a TigerBeetle ledger, Aster Web Data publishes its accounts and transfers as two OData sets, so a screen can list an account's transfers or create a transfer with an ordinary request. The service runs no SQL here: each read or write becomes a request to the cluster, and the cluster's own rules apply. Records are immutable once created, so they can be read and created, but not changed or deleted.
TigerBeetle has no table scan, which shapes every read you design. A read must name an ID, an account, or a field the cluster can query by; a broad list, a count and offset paging are refused with a message that names the alternatives.
Connecting
The catalog is fixed, so a service stored on the ledger declares no data sources:
services:
ledger:
datasources: []
databases:
ledger:
kind: tigerbeetle
options:
cluster_id: '0' # your cluster's ID
addresses: 127.0.0.1:3000 # the replica addresses, comma-separated
| Key | Meaning |
|---|---|
options.cluster_id |
Required. The cluster ID as unsigned decimal text, with no sign and no leading zero, up to 128 bits. |
options.addresses |
Required. The replica addresses, separated by commas. |
options.timeout_ms |
The longest a call waits for the cluster, 5,000 ms by default. The request's statement timeout applies when it is shorter. |
A URL, the parts form, schema, TLS settings and read_only are refused, and setup takes only existing, because there are no tables to create and both sets accept inserts. pool is accepted and not used: one client serves every call. A service on the ledger also refuses transactional server rules and request-context rules that need a check after the write. The configuration reference lists the shared keys, such as required and retry.
The service uses the official TigerBeetle client, version 0.17.9, and the tests run against a cluster of the same version.
Start-up check
There are no tables to check, and opening the client makes no call to the cluster. The first health check, a lookup of account ID 0 bounded by timeout_ms, is the first call that reaches it. A failed check makes Accounts and Transfers answer 503 ConnectionLost until the ledger is back, while the service's other sets keep serving; A database goes down describes the rest. $metadata is the same whether or not the cluster is reachable.
Accounts and transfers
The two sets use TigerBeetle's own field names.
| Field | Storage in TigerBeetle | OData type |
|---|---|---|
id, the debit and credit account IDs, amount, pending_id, user_data_128 |
128-bit unsigned | Edm.String holding decimal text |
The four balance counters of an account (debits_pending, debits_posted, credits_pending, credits_posted) |
128-bit unsigned | Decimal text |
user_data_64 |
64-bit unsigned | Decimal text, because a signed 64-bit type cannot hold every value |
user_data_32, ledger, timeout |
32-bit | Edm.Int64 |
code |
16-bit | Edm.Int32 |
timestamp |
64-bit, in nanoseconds | Edm.Int64 |
Each flag (linked, pending, closed, and the others) |
A bit | Edm.Boolean |
Identifiers and amounts travel as decimal text, so no value passes through floating point. Every property is required, so there are no nulls. When a record is created, an omitted writable value is zero or false, and id must be supplied. The balance counters, timestamp and linked are set by the cluster. Accounts have no navigations, so $expand does not apply.
Where each operation runs
The service answers these request shapes with a native query to the cluster:
| Request shape | Native request |
|---|---|
id eq or in, up to 8,189 distinct IDs |
A lookup of accounts or transfers by ID. The result is complete, so any further filter is exact. |
A transfer filtered by debit_account_id eq or credit_account_id eq |
A query for the transfers of an account, with the debit or credit flag. |
The debit and credit filters for the same account, joined with or |
One query for the transfers of that account, with both flags. |
A non-zero eq on ledger, code, user_data_128, user_data_64 or user_data_32 |
A query of accounts or transfers by that field. |
timestamp with eq, gt, ge, lt or le, beside one of the filters above |
A query with inclusive bounds on the timestamp. |
$orderby=timestamp, ascending or descending |
The native reversed flag. Timestamps are unique, so no ID tie-breaker is needed. |
| Server-driven paging | A bounded native limit, plus one record to check for a next page. The cursor in @odata.nextLink is a timestamp. |
The default page is 1,000 records, and a response holds at most 8,189. On a bounded result such as an ID lookup, the service evaluates any other filter itself, and sums 128-bit values and orders them numerically. String functions on these fields are refused.
These are refused with a 400 that names the alternatives: a read with no ID, account ID or query filter, or filtered only by timestamp; $count=true and /$count; $skip above 0; ordering by a field other than timestamp; and a query that would fill the 8,189-record batch without paging, which is refused rather than returned cut short.
Errors
| Case | Response |
|---|---|
| A record the cluster rejects as malformed, such as a zero value that must be non-zero, a reserved field, or mutually exclusive flags | 400, with the native name in error.code |
| A conflict with ledger state, such as a missing account, an exceeded balance, a closed account, or an overflow of a balance counter | 409, with the native name in error.code |
| A repeated create with an ID that exists | 409, with exists |
| An amount or identifier outside its native width | 400, before anything is sent |
| A request larger than the cluster's negotiated limit | 400 |
| An unknown result code from a newer cluster | 500 unknown_native_result. It is never read as success. |
| The client was evicted or closed | 503 ConnectionLost |
| A call that ran out of time | 503 ConnectionLost, with Retry-After |
Writes and transactions
- Creating records. A
POSTcreates one account or transfer, and the response is the record as the cluster stored it. Two-phase transfers use thependingflag, thenpost_pending_transferorvoid_pending_transfer, withpending_idnaming the first transfer. - No updates or deletes.
PATCH,PUTandDELETEreturn 405, with anAllowheader that lists the allowed methods, and$metadatareports that updates and deletes are not supported. A ledger set takes noconcurrencysetting, because no write decides over a stored record that another writer could change. - Atomic groups. A
$batchatomicity group is sent as one native linked chain of 1 to 8,189 inserts, all of accounts or all of transfers, with literal values. The service checks every member before it sends anything, and refuses a group that mixes the two sets, contains a call or a lock, or spans two databases. If the chain fails, the failing member reports the cluster's result code and the others report that they were rolled back. - Transactions. Atomic groups are the only grouping; isolation levels do not apply.
- Timeouts. A call is bounded by the request's statement timeout and by
options.timeout_ms, whichever is shorter. A call that runs out of time returns 503ConnectionLost. The cluster may or may not have applied a write in that call, so check for the record before retrying the write.
Scaffold
There is nothing to scaffold. The scaffold command stops with a message on a TigerBeetle database, because the service publishes Accounts and Transfers itself, for each service whose database: names the ledger. A configuration can declare the ledger beside other databases, as Several databases in one service describes.