Keyspace
Hyphae's native structure engine covers the string/counter/hash/list/set/ sorted-set/stream space with full transactionality — the same commit scheduler, WAL, and CSN as SQL and search, not a separate cache tier. It is a first-class owner of keyspace data, not a Valkey process, a RESP dispatcher, a relational projection, or a disposable cache by default.
Call it "a native keyspace/data-structure engine," never "Redis-compatible." Structures share Hyphae's own transaction and TTL semantics; there is no RESP wire protocol in the native product.
Scalars auto-create; containers do not
Two categories, two different rules:
- Scalars (strings, counters) auto-create on first use.
- Containers (hash, list, set, sorted set, stream) require an explicit
createmutation before any other operation on that key.
A scalar with a TTL:
hyphae structure --data-dir "$D" set --key session:active --value note-1 \
--expires-at-micros 4102444800000000
hyphae structure --data-dir "$D" get --key session:active
hyphae structure --data-dir "$D" ttl --key session:active
The default keyspace (hyphae_internal.system.default_scalar,
object 3) is scalar-only. Container structures need
catalogued keyspaces of the matching family:
hyphae catalog --data-dir "$D" create-keyspace --id 20 --parent 2 \
--name hyphae_internal.system.counters --family counter
hyphae catalog --data-dir "$D" create-keyspace --id 21 --parent 2 \
--name hyphae_internal.system.hashes --family hash Three rules that save a debugging session:
- a container used without a prior
createfails withobject_not_found; createon a scalar family isinvalid_request— scalars auto-create, so there is nothing to create;- families are snake_case in JSON (
"sorted_set") but hyphenated in CLI flags (--family sorted-set).
Atomic batches
batch applies one typed JSON mutation array as a single
transaction — all or nothing:
hyphae structure --data-dir "$D" batch --mutations-json '[
{"operation":"create","keyspace":21,"key":"note:1","family":"hash"},
{"operation":"hash_set","keyspace":21,"key":"note:1","field":"author","value":"mario"},
{"operation":"hash_set","keyspace":21,"key":"note:1","field":"state","value":"published"},
{"operation":"counter_add","keyspace":20,"key":"visits","delta":10}
]'
Typed reads use the same envelope; optional fields such as
start_after must be present in the JSON — use
null rather than omitting them:
hyphae structure --data-dir "$D" read --request-json \
'{"operation":"hash_scan","keyspace":21,"key":"note:1","start_after":null,"limit":10}'
Available typed reads in the base command set:
string_get, counter_get, ttl,
hash_get/scan/length,
hash_field_ttl, list_range/length,
set_contains/members/cardinality,
bounded set_algebra,
sorted_set_score/rank/range/cardinality,
and stream_range. Full semantics:
structures semantics contract.
TTL and scans
Every family with a TTL uses an absolute expiry timestamp captured at
write time, evaluated against the reader's snapshot logical time — an
expired key or field reads as absent even before background cleanup runs.
Hash and set scans (HSCAN, HSCAN_REVERSE,
HSCAN_MATCH, SSCAN) take a bounded limit and an
exclusive cursor, walk the underlying B+tree directly rather than
materializing the whole collection, and skip tombstoned entries without
charging the limit. A pattern scan (HSCAN_MATCH,
KEY_SCAN_MATCH) accepts a bounded binary glob
(*, ?, [set], \
escape, at most 512 pattern bytes) and reports a physical continuation
cursor plus a stop reason of exhausted,
output_limit, or visit_limit.
Minor 6: seven Valkey-shaped mutations, six typed reads
Native protocol minor 6 adds seven Valkey-shaped mutations and six typed
reads through the same batch and read
envelopes, using Hyphae's own operation names rather than Valkey command
names. batch's response is still the commit receipt for the
whole array — the CLI does not surface each mutation's individual
outcome (whether a conditional write applied, a resulting length, a
popped member); read the structure back, or use an SDK's typed API, to
observe a specific one.
| Hyphae operation | Valkey-shaped equivalent | What it does |
|---|---|---|
string_set_conditional | SETNX / SET XX | write only if the key is absent (if_absent) or present (if_present) |
string_append | APPEND | concatenate onto the currently visible value |
string_set_range | SETRANGE | overwrite at a byte offset, zero-filling any gap |
hash_set_if_absent | HSETNX | write one hash field only if absent — the hash must already exist |
sorted_set_increment | ZINCRBY | add a delta to a member's score; a missing member starts at 0.0 |
sorted_set_pop | ZPOPMIN / ZPOPMAX | remove and return the lowest- or highest-ranked member |
set_pop | SPOP (seeded) | remove one member selected deterministically from an explicit caller seed — never hidden randomness |
Verified in sequence — SETNX, then APPEND, then SETRANGE:
# SETNX (string_set_conditional): write only if absent
hyphae structure --data-dir "$D" batch --mutations-json '[
{"operation":"string_set_conditional","keyspace":3,"key":"session:cap",
"value":"v1","expires_at_micros":null,"condition":"if_absent"}]'
# APPEND (string_append): concatenate onto the visible value
hyphae structure --data-dir "$D" batch --mutations-json '[
{"operation":"string_append","keyspace":3,"key":"session:cap","suffix":"-appended"}]'
# SETRANGE (string_set_range): overwrite at a byte offset
hyphae structure --data-dir "$D" batch --mutations-json '[
{"operation":"string_set_range","keyspace":3,"key":"session:cap","offset":0,"patch":"V2"}]'
hyphae structure --data-dir "$D" get --key session:cap
# → value "V2-appended" — SETNX wrote "v1", APPEND made it "v1-appended",
# and SETRANGE at offset 0 overwrote the first two bytes with "V2" The six typed reads surface through structure read:
sorted_set_score_range— ZRANGE_BY_SCORE / ZREVRANGE_BY_SCORE, with independently inclusive, exclusive, or unbounded score bounds and an explicit direction.hash_scan_reverse— HSCAN_REVERSE, descending exact field-byte order.hash_scan_match— HSCAN_MATCH, the bounded binary-glob scan above.key_scan_match— KEY_SCAN_MATCH, across every structure family in one keyspace.string_range— GETRANGE, with Valkey-affine signed indices.set_random_members— SRANDMEMBER, deterministic under an explicit seed.
# sorted_set_score_range: ZRANGE_BY_SCORE / ZREVRANGE_BY_SCORE
hyphae structure --data-dir "$D" read --request-json \
'{"operation":"sorted_set_score_range","keyspace":22,"key":"leaderboard",
"lower":"unbounded","upper":"unbounded","offset":0,"limit":10,"order":"ascending"}'
# → entries: bob 20.0, carol 30.0
# hash_scan_match: HSCAN_MATCH with a bounded binary glob
hyphae structure --data-dir "$D" read --request-json \
'{"operation":"hash_scan_match","keyspace":20,"key":"profile:1","pattern":"*",
"start_after":null,"output_limit":10,"visit_limit":100,"match_step_limit":100}'
# → stop: "exhausted", visited: 2 Known 3.0.0 defect: all-rejected conditional batches
A batch whose conditional mutations are all rejected —
for example string_set_conditional with if_absent
on a key that already exists, or hash_set_if_absent on a
field that already exists — stages nothing, and the empty commit is
reported as {"category":"corruption","code":"corruption"}
with exit class 9, even though the directory is healthy and unchanged. A
batch that also carries at least one applied mutation commits normally;
only the all-rejected case misreports. This is tracked as
issue #268
— if you see this exact error shape on a conditional-only batch, the
directory is not corrupt.