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.