TessariDBDocs
  • 0.34.2-betalatest
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

Spaces

A key-value store inside the database, in the same transaction as the records — not a cache beside it, though it does what one is run for.

A space is a table whose records hold a single value rather than a document. It is declared like anything else:

DEFINE NAMESPACE prod;
USE NAMESPACE prod;
DEFINE DATABASE orders;
USE DATABASE orders;
DEFINE SPACE sessions;

Four statements work on it — SET, GET, DEL and KEYS — and they are the ones you expect:

SET sessions:'abc' = 42;
GET sessions:'abc';

42. The key is a record id, so sessions:'abc' is an address in exactly the sense users:1 is — which is why nothing new had to be invented to name one.

A write replaces

SET sessions:'abc' = { user: 1, live: true };
SET sessions:'abc' = 'replaced';
GET sessions:'abc';

'replaced'. There is no merge, no patch and no partial update. A key holds one value and a write is the whole of it — which is the property that makes a key-value read a single lookup rather than a reconstruction.

Two kinds of nothing

SET sessions:'here' = NULL;
GET sessions:'here';

NULL — the key is there and holds nothing.

GET sessions:'gone';

NONE — there is no such key. The distinction is the same one the record language draws, and it is load-bearing for a session store: the token was revoked and the token was never issued are different answers, and a store that collapsed them would make the two indistinguishable at exactly the moment somebody is trying to work out which happened.

Removing a key gives you the second one:

DEL sessions:'abc';
GET sessions:'abc';

Listing keys

SET sessions:'a' = 1;
SET sessions:'f' = 2;
SET sessions:'m' = 3;
SET sessions:'z' = 4;
KEYS FROM sessions;

Bounded, with the end excluded:

KEYS FROM sessions RANGE 'a'..'m';

…and included when the range says so:

KEYS FROM sessions RANGE 'a'..='m';

Text keys order lexicographically, so a range means what the byte order says. 'here' — written above — sorts between 'f' and 'm', which is worth seeing once on purpose rather than discovering in a count that is one short.

A bound has to be something a key could be:

KEYS FROM sessions RANGE 1.5..2.5;

InvalidKeyBound.

Keys that share a beginning are listed by it, and a long space is read a page at a time:

SET sessions:'user:42:a' = 1;
SET sessions:'user:42:b' = 2;
SET sessions:'user:7:a' = 3;
KEYS FROM sessions PREFIX 'user:42:';

The two keys of user 42, and nothing else. The walk seeks to where the prefix begins and stops where it ends, so a prefix over three keys in a space of a million reads a handful of entries rather than the space.

KEYS FROM sessions LIMIT 2;
KEYS FROM sessions AFTER 'f' LIMIT 2;

AFTER starts past the key it names — the last key of the previous page — so a space is paged with no key repeated and none dropped, however many were written in between. An empty prefix is refused, and an expired key is never listed.

What else a space does

A space is also where the things a cache is usually run for belong: a key that expires, a counter that never loses an increment, a lock that frees itself, and a space that holds at most so many keys. Each of them is a write in the same transaction as everything else below.

A space of text is searchable

A value that is not an object answers the route value with itself (from 0.22.0-beta), so a space of text is declared, indexed and searched like a field:

DEFINE ANALYZER english FILTERS lowercase, ascii, stemmer;
DEFINE SPACE phrases;
DEFINE FIELD value ON phrases TYPE string ANALYZER english;
DEFINE INDEX by_value ON phrases FIELDS value SEARCH;
SET phrases:'a' = 'the quick foxes';
SELECT id FROM phrases WHERE value MATCHES 'fox';

One key, phrases:'a'. A value that is an object keeps its own value field: only something with no fields at all answers the route with itself.

The reason this is not a cache

A key-value read stands where a value stands, so it composes with everything else in the language:

DEFINE COLLECTION users;
DEFINE INDEX by_email ON users FIELDS email UNIQUE;
CREATE users:1 = { name: 'ada', email: 'ada@example.com' };
DEFINE SPACE emails;
SET emails:'primary' = 'ada@example.com';
SELECT * FROM users WHERE email = GET emails:'primary';

One row. And a record may hold the results of both:

DEFINE COLLECTION audit;
CREATE audit:1 = {
  actor:  GET emails:'primary',
  target: (SELECT * FROM users:1),
};
SELECT * FROM audit:1;

Most of all, a space and a table are in the same database, so they are in the same transaction:

BEGIN;
DEFINE SPACE tokens;
SET tokens:'abc' = users:1;
CREATE users = { name: 'hopper' };
COMMIT;

The session token and the user it refers to either both exist or neither does. That is the thing a separate key-value cache beside a database cannot give you at any price — the two systems can be made fast and can be made consistent-ish, but they cannot be made to commit together.

Spaces — TessariDB