Assertions
An assertion is an upper bound on COUNT(*) or SUM(column) per group of rows, checked in constant time before every write and kept exact across cores and crashes.
In this article
An assertion is a rule about a whole group of rows, such as “at most 4 tickets on one flight” or “a passenger’s checked baggage may not sum past 46 kg”. KDBS checks it before each write and refuses a write that would break it. SQL-92 defined CREATE ASSERTION for arbitrary conditions, which are expensive to check and hard to keep correct under concurrent writes. KDBS supports a narrow class instead, upper bounds on COUNT(*) and SUM(column) per group, because that class can be checked in constant time and kept exact while many sessions write at once.
How it works
Declaring a bound
CREATE ASSERTION seat_limit ON tickets
GROUP BY (flight_id) CHECK COUNT(*) <= 4;
CREATE ASSERTION baggage_cap ON tickets
GROUP BY (passenger_id) CHECK SUM(baggage_kg) <= 46;
A group is one distinct combination of values in the GROUP BY columns. The bound applies to every group separately. < N is enforced as <= N - 1.
The Bound Cabin: a running total per group
Each assertion has its own Bound Cabin: a stored structure with one header per group and one 32-byte entry per row.
Bound Cabin for baggage_cap
┌─────────────────────────────────────────────────────┐
│ group directory: hash(group key) → header │
│ │
│ header (passenger_id=1): count=2 sum=46 ──┐ │
│ header (passenger_id=2): count=1 sum=15 │ │
│ ▼ │
│ entries: pk=1 value=23, pk=9 value=23, ... │
└─────────────────────────────────────────────────────┘
(illustration)
- A group header holds the running
countandsumof the group’s committed and reserved rows. - An entry holds a row’s key and its
SUMvalue. Entries are used to check and repair the headers; they are not read on the write path. - A check reads only the group header. Its cost does not depend on how many rows the group has.
The Bound Cabin covers every row of the table, is never evicted from memory, and is written to the write-ahead log like table data.
Which writes are checked
| Write | Checked? |
|---|---|
INSERT | Yes, before the row’s key is allocated |
UPDATE that raises the summed column | Yes, by the size of the increase |
UPDATE that moves a row to another group | Yes, against the destination group |
UPDATE that changes neither | No |
DELETE | No: removing a row can never break an upper bound |
DELETE still lowers the group’s running total, so the room it frees is available at once.
Reservations
When a write passes its check, its amount is reserved in the group header immediately, before the transaction commits. Every later check counts committed rows plus reservations. At commit the reservation becomes committed; at rollback it is removed.
- If a check fails only because of a reservation held by a transaction that is still running, the write waits for that transaction to commit or roll back, then checks again. Two transactions waiting on each other’s reservations are refused as a deadlock.
- On an instance with several cores, all cores reserve into the same registry. Two sessions on two cores that both try to take the last place in a group get one success and one violation.
Creating an assertion
CREATE ASSERTION scans the whole table inside the statement and builds the Bound Cabin. While it builds, it waits for every open write to the table to finish, and new writes wait for it. If existing data already breaks the bound, the statement fails and builds nothing:
kdbs> CREATE ASSERTION one_per_passenger ON tickets GROUP BY (passenger_id) CHECK COUNT(*) <= 1
ERR ASSERTION_VIOLATION retryable=0 assertion "one_per_passenger" group (passenger_id=1): COUNT(*) would exceed bound 1
The assertion is published in one step: it is either enforcing or does not exist.
Examples
The sample tickets table starts with three tickets on flight 3. A fourth fits the bound of 4; a fifth does not:
kdbs> INSERT INTO tickets VALUES (3, 4, '43A', 0, 0, NULL)
INSERTED oid=4019 id=10 page=139 slot=9
kdbs> INSERT INTO tickets VALUES (3, 5, '43B', 0, 0, NULL)
ERR ASSERTION_VIOLATION retryable=0 assertion "seat_limit" group (flight_id=3): COUNT(*) would exceed bound 4
Passenger 1 already has 46 kg of checked baggage on two tickets, so the sum is exactly at its bound. An UPDATE that raises one of the weights is refused by baggage_cap, and so is a new ticket that carries baggage:
kdbs> UPDATE tickets SET baggage_kg = 30 WHERE id = 1
ERR ASSERTION_VIOLATION retryable=0 assertion "baggage_cap" group (passenger_id=1): SUM(baggage_kg) would exceed bound 46
kdbs> INSERT INTO tickets VALUES (2, 1, '05A', 5, 0, NULL)
ERR ASSERTION_VIOLATION retryable=0 assertion "baggage_cap" group (passenger_id=1): SUM(baggage_kg) would exceed bound 46
Deleting a row frees room in its group immediately. Flight 1 is full; after one of its tickets is deleted, a new ticket fits:
kdbs> DELETE FROM tickets WHERE id = 4
DELETED 1
kdbs> INSERT INTO tickets VALUES (1, 5, '16A', 0, 0, NULL)
INSERTED oid=4019 id=11 page=139 slot=10
Rows written earlier in the same open transaction count too. Flight 4 has no tickets, so four fit and the fifth is refused:
kdbs> BEGIN
BEGIN
kdbs> INSERT INTO tickets VALUES (4, 2, '20A', 0, 0, NULL)
INSERTED oid=4019 id=12 page=139 slot=11
kdbs> INSERT INTO tickets VALUES (4, 3, '20B', 0, 0, NULL)
INSERTED oid=4019 id=13 page=139 slot=12
kdbs> INSERT INTO tickets VALUES (4, 4, '20C', 0, 0, NULL)
INSERTED oid=4019 id=14 page=139 slot=13
kdbs> INSERT INTO tickets VALUES (4, 5, '20D', 0, 0, NULL)
INSERTED oid=4019 id=15 page=139 slot=14
kdbs> INSERT INTO tickets VALUES (4, 6, '20E', 0, 0, NULL)
ERR ASSERTION_VIOLATION retryable=0 assertion "seat_limit" group (flight_id=4): COUNT(*) would exceed bound 4
kdbs> ROLLBACK
ROLLBACK
What it guarantees
- No group is ever over its bound, counting committed rows and rows reserved by running transactions.
- No false admission. A check can refuse a write that would have fit if a concurrent transaction later rolled back; it never admits a write that breaks the bound. In the first case the write waits for that transaction instead of failing.
- The check is constant time per checked write.
- Enforcement continues across a crash. The Bound Cabin is logged and replayed at startup, so an assertion enforces as soon as the server is up again, with no rebuild scan.
- One rule per instance. The bound holds across all cores.
What it costs
-
Write cost. Each checked write adds one 32-byte entry and one small log record.
-
A violation fails the transaction. Inside
BEGIN, a violation poisons the transaction like any failed statement, and the client mustROLLBACK. In autocommit the statement is undone completely, reservations included. -
Creating an assertion blocks writers of the table while it scans.
-
Schema changes are restricted.
DROP TABLEandALTER TABLEon a table with an assertion are refused; drop the assertion first.kdbs> DROP TABLE tickets ERR assertion 'seat_limit' is declared on this relation; DROP ASSERTION first -
Space is not returned on drop.
DROP ASSERTIONleaves the Bound Cabin’s pages allocated until the next restart.
How to observe it
SHOW ASSERTIONS lists every assertion with its declaration and counters:
kdbs> SHOW ASSERTIONS
assertions=2
assertion_id=1 name=seat_limit rel=tickets enforcing=1 checks=9 violations=0 reserved=9 aborted=0 def=CREATE ASSERTION seat_limit ON tickets GROUP BY (flight_id) CHECK COUNT(*) <= 4
assertion_id=2 name=baggage_cap rel=tickets enforcing=1 checks=9 violations=0 reserved=9 aborted=0 def=CREATE ASSERTION baggage_cap ON tickets GROUP BY (passenger_id) CHECK SUM(baggage_kg) <= 46
| Field | Meaning |
|---|---|
enforcing | 1 when the assertion is checked. 0 only for an assertion whose state could not be rebuilt at startup. |
checks | Writes checked |
violations | Writes refused |
reserved | Reservations made |
aborted | Reservations removed by a rollback |
SHOW META reports recovery_assertions_enforcing and recovery_assertions_unrecovered for the last startup.
Limitations
The grammar is the supported class. Everything else is refused with the byte position of the refused part:
-
Lower bounds and equality (
>,>=,=). Enforcing them would need checks onDELETEand on decreasingUPDATEs.kdbs> CREATE ASSERTION eq ON tickets GROUP BY (passenger_id) CHECK COUNT(*) = 1 ERR NOT_IMPLEMENTED retryable=0 equality assertions (=) are not supported (byte 70); enforcing = means enforcing a lower bound, which v1 excludes, and reading it as <= would enforce something other than what was written (docs/spec/assertion.md AS11) -
Other aggregates:
MIN,MAX,AVG,COUNT(<column>),DISTINCT. -
SUMover anything but anint64column. -
Bounds that are not non-negative integer literals, and
!=. -
COUNT(*) <= 0andCOUNT(*) < 1, because a group exists only while it holds a row. -
Deferred checking (
DEFERRABLE,INITIALLY ...) andNOT VALID. A check always runs at statement time. -
One table per assertion. No conditions across tables, no
WHERE-filtered groups. -
Not transactional.
CREATE ASSERTIONandDROP ASSERTIONare not undone byROLLBACK.