Assertions

v3.0.0

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 count and sum of the group’s committed and reserved rows.
  • An entry holds a row’s key and its SUM value. 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

WriteChecked?
INSERTYes, before the row’s key is allocated
UPDATE that raises the summed columnYes, by the size of the increase
UPDATE that moves a row to another groupYes, against the destination group
UPDATE that changes neitherNo
DELETENo: 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 must ROLLBACK. 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 TABLE and ALTER TABLE on 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 ASSERTION leaves 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
FieldMeaning
enforcing1 when the assertion is checked. 0 only for an assertion whose state could not be rebuilt at startup.
checksWrites checked
violationsWrites refused
reservedReservations made
abortedReservations 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 on DELETE and on decreasing UPDATEs.

    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.

  • SUM over anything but an int64 column.

  • Bounds that are not non-negative integer literals, and !=.

  • COUNT(*) <= 0 and COUNT(*) < 1, because a group exists only while it holds a row.

  • Deferred checking (DEFERRABLE, INITIALLY ...) and NOT VALID. A check always runs at statement time.

  • One table per assertion. No conditions across tables, no WHERE-filtered groups.

  • Not transactional. CREATE ASSERTION and DROP ASSERTION are not undone by ROLLBACK.

See also