Skip to content

Qirava DMS docs

Maintenance: TTL sweep

Time-to-live (TTL) lets rows expire automatically. A table declares a TTL field holding a unix-seconds expiry; the sweeper deletes every row whose expiry is <= now. The sweep runs as a normal QQL DELETE per TTL table, so it honors the same durability and (importantly) is index-served when you declare a Filter index on the TTL field — turning the sweep into a range scan instead of a full scan. The sweeper is a public maintenance function the worker scheduler drives on a schedule. This page covers declaring TTL, how the sweep selects rows, and running it.

Declaring a TTL field#

A table sets its TTL field to the field that holds each row's expiry as a unix-seconds integer. In code this is TableDef::new(...).with_ttl("<field>"). A row is expired when <field> <= now. Declaring a Filter index on that field makes the sweep index-served (a range scan over expired keys), avoiding a full table scan.

rust
TableDef::new("sessions", TableSchema::Schemaless)
    .with_ttl("exp")
    .with_index(IndexDef::new("exp_idx", IndexKind::Filter, vec!["exp".into()]))

How the sweep selects rows#

sweep_expired(now) gathers every table that declared a TTL field and, for each, runs the equivalent of DELETE WHERE <ttl_field> <= now. The <= range is served by the Filter index when present (an ordered range scan), else by a scan. It returns the total number of rows reclaimed across all TTL tables. Because it uses the normal DELETE path, expirations are WAL-logged and indexes are maintained.

Request

-- sessions has TTL field `exp` + Filter index on `exp`
INSERT INTO sessions { id: 1, exp: 100 }          -- expired
INSERT INTO sessions { id: 2, exp: 5000000000 }   -- future
INSERT INTO sessions { id: 3, exp: 200 }          -- expired
-- sweep at now = 1000
SELECT id FROM sessions   -- after the sweep

Response

// sweep_expired(1000) reclaims 2 rows (id 1 and 3)
// SELECT id FROM sessions -> { "error": null, "data": [ { "id": 2 } ], "root": { "count": 1 } }
// a second sweep at now = 1000 reclaims 0

Running the sweep on a schedule#

The sweeper is exposed as a public function (db.ttl_sweep) so the worker scheduler can run it like any other function, on a schedule. It runs through the executor, so it composes with the same concurrency and durability as normal writes.

Request

-- t has TTL field `exp`
INSERT INTO t { id: 1, exp: 1 }   -- far past
-- scheduler invokes db.ttl_sweep
SELECT id FROM t

Response

// after the swept run -> { "error": null, "data": [], "root": { "count": 0 } }