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 create mutation 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 create fails with object_not_found;
  • create on a scalar family is invalid_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 operationValkey-shaped equivalentWhat it does
string_set_conditionalSETNX / SET XXwrite only if the key is absent (if_absent) or present (if_present)
string_appendAPPENDconcatenate onto the currently visible value
string_set_rangeSETRANGEoverwrite at a byte offset, zero-filling any gap
hash_set_if_absentHSETNXwrite one hash field only if absent — the hash must already exist
sorted_set_incrementZINCRBYadd a delta to a member's score; a missing member starts at 0.0
sorted_set_popZPOPMIN / ZPOPMAXremove and return the lowest- or highest-ranked member
set_popSPOP (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.