Pre-1.0 hoardDB is pre-1.0. Expect breaking changes.
Store Types
hoardDB has six stores, and you pick the one that matches how you read. In ring
mode (hoarddb -ring) a bucket declares fields, and a field can be a store of any
of the six types, so every document keeps its own queues, maps and files:
create bucket User { Name string, Photos lifoStore, Purchases fifoStore, Friends hashStore,
Recipes btreeStore, Books heapStore, Data blobStore }
Quick reference
The word you write in HQL is the store’s name followed by Store: fifoStore,
lifoStore, hashStore, btreeStore, heapStore and
blobStore. The first column is what goes after the field name.
| Write | What it is | Reads back | Use it for | Verbs that run in ring mode |
|---|---|---|---|---|
fifoStore | a queue (FIFO) | the oldest element first | queues, logs, event streams | push pop peek list count claim get |
lifoStore | a stack (LIFO) | the newest element first | recent items, undo, a feed | push pop peek list count claim get |
hashStore | a map | an element by its key | profiles, settings, sessions | put get take list count |
btreeStore | an ordered map | elements by key, in key order | leaderboards, sorted data, time series | put get take list count |
heapStore | a priority queue | elements by priority | task queues, alerts, scheduling | pop peek list count get |
blobStore | large values and files | the bytes you stored | files, images, binary assets | count |
Two stores do not run every verb yet: a heapStore has no push and a
blobStore has no put in ring mode
(Limitations). help types in a ring
session always lists the verbs that run on the build you have.
Spell the word exactly. fifo, FIFO, heap and queue are names of the
structures, not HQL words, and FifoStore is refused for its capital letter:
[h]oardDB [app]> create bucket Z { A heap }
CLI error: PARSE_ERROR: "heap" is not a type: write one of string, int32, int, double, decimal, number, bool, datetime, bsontimestamp, binary, uuid, objectid, object, array, regex, null, any, or a store: fifoStore, lifoStore, hashStore, btreeStore, heapStore, blobStore ('A string') (character 21). ...
[h]oardDB [app]> create bucket Z { A FifoStore }
CLI error: PARSE_ERROR: "FifoStore" is not a type word: type words are spelled exactly, so write "fifoStore" ('A fifoStore') (character 21). ...
[h]oardDB [app]> create bucket Z { A lifoStore }
OK created bucket app.Z (1 fields)
Fields: A lifoStore
Getting started walks through each store, and Data types covers the scalar fields that sit beside them.
Buckets that have a type
The rest of this page has not been rewritten for ring mode yet: it describes buckets that are backed by one store type, which ring mode does not have, so read it as a description of how the six structures behave and not as commands that run.
Each bucket is backed by one store type, and a hash or btree bucket can also give each of its documents stores of its own: a field of a bucket can itself be a store of any of the six types, Purchases fifoStore on User being that user’s own queue. The store type determines the data structure and access semantics. Store fields or separate buckets says which to use.
Use each store’s own verbs. push and pop for a FIFO, LIFO or heap bucket; insert and get for a hash or btree bucket; put and get for a blob bucket. The server is permissive in one direction: it also accepts some of another type’s write verbs, and the next section says exactly which, and what each does. It refuses a verb a type has no meaning for, such as pop on a hash bucket: a hash has no order to take from, so the refusal says so and names the verbs that work (delete("<key>") to remove one, get("<key>") or find() to read). pop removes what it returns, so it is a write, not a read.
What each store type accepts
| Verb | FIFO | LIFO | Hash | B+Tree | Heap | Blob |
|---|---|---|---|---|---|---|
insert (write one) | pushes | pushes | yes | yes | pushes at priority 0 | stores the document as the payload |
| insert many (over the protocol; the shell inserts one at a time) | yes | yes | yes | yes | yes | refused |
push | yes | yes | stores under its _id_ | stores under its _id_ | yes | refused |
pop, peek | yes | yes | refused | refused | yes | refused |
claim, ack, release, extend, dead, requeue, discard | yes | yes | refused | refused | yes | refused |
get, delete | refused | refused | yes | yes | refused | yes |
exists (over the protocol; the shell has no exists) | refused | refused | yes | yes | refused | yes |
find, distinct | yes | yes | yes | yes | yes | refused |
search | full scan | full scan | index, else full scan | full scan | full scan | refused |
count, bucket stats | yes | yes | yes | yes | yes | yes |
A refused verb answers in the same call with a message that names the bucket type; nothing is written. These are the surprising ones, each with a command to try:
claim/ackare the lossless way to consume a FIFO, LIFO or heap bucket.claimtakes entries under a lease,ackremoves them,releaseputs them back,extendmoves the deadline, anddead/requeue/discardmanage entries that died. A hash, btree or blob bucket has no order to take from and refuses them. A store field ispop-only in v1.0.inserton a FIFO, LIFO or heap bucket pushes. The key (or the document’s_id_) is kept only as the element’s_id_, you read the element back withpoporpeekand not by key, and on a heap it goes in at priority 0, so every such element ties. Usepushwith apriorityon a heap.db.Jobs.insert({_id_: "j1", Task: "resize"}); -- Jobs is a fifo bucket: this is a push db.Jobs.peek(); -- the next element, _id_ "j1"inserton a blob bucket stores the document itself as the blob. The document is encoded as BSON and kept as the blob’s bytes, with no metadata. It is not the way to store a file:puttakes the content (and atype) and records its size and metadata. A blob written withinsertreads back as those BSON bytes: in the shell,db.Photos.get("note")reports them asbinary (40 B), anddb.Photos.get({_id_: "note", out: "note.bson"})saves them to a file.db.Photos.insert({_id_: "note", Caption: "Sunset"}); -- the document becomes the payload db.Photos.put({_id_: "sunset", data: "hello photo", type: "text/plain"}); -- the file verbpushon a hash or B+Tree bucket is a keyed insert. The document is stored under its_id_, or under a UUID the server mints when it has none, exactly asinsertwould.popon the same bucket is still refused, because a hash has no order to take from.db.Profiles.push({_id_: "jane", Name: "Jane"}); -- Profiles is a hash bucket db.Profiles.get("jane"); -- foundgetandexistson a blob bucket answer, butgetis the payload verb in the shell. Over the protocol,GETreturns the stored document when the blob’s bytes are BSON, or a small document{_id_, blob_id, size, ...metadata}when they are not; the content itself is read with the blob read verb, which is whatdb.Photos.get("sunset")sends.searchon a bucket with no index scans it. A hash bucket answers from an index when one serves the filter (Indexes); a FIFO, LIFO, B+Tree or heap bucket has no indexes, and a hash bucket whose indexes do not serve the filter has none to use, so the server walks every document and says so in the answer (index_used: "none — full scan"). It costs a read of the whole bucket, the same asfind. A blob bucket refusessearch.db.Leaderboard.search({User: "jane"}); -- Leaderboard is a B+Tree bucket: a full scan
Two answers that used to be wrong are now refusals. Over the protocol, exists on a FIFO, LIFO or heap bucket, or with an empty key, is an INVALID_ARGUMENT error (it used to answer exists: false, which means “looked, not there”), and find on a blob bucket answers found: false with a message pointing at get and info. An insert-many on a FIFO or LIFO bucket now reports the count and the ids it pushed (it used to say 0).
Size limits
One document is limited: 24 MiB of encoded bytes by default, which an operator can lower to 4 KiB or raise to at most 30 MiB (HOARDDB_MAX_DOCUMENT_BYTES, see Configuration). A larger document is refused with the limit in the message. Put a list that grows in a nested store field, whose elements are stored separately, and a large value in a blob bucket or behind a pointer document (Large file stores). A blob has its own, smaller cap, described under Blob below. What a bucket and a document are, and every limit, is on the Limits page.
FIFO (First In, First Out)
Append-only queue. Items are pushed to the tail and popped from the head. O(1) push and pop.
When to Use
- Message queues
- Event streams and log ingestion
- Comment threads (chronological)
- Time-series data
- Job queues (FIFO processing order)
Operations
-- Push: append to tail
db.Comments.push({Author: "Bob", Text: "Nice photo!"});
-- Pop: remove and return from head (oldest first)
db.Comments.pop();
-- Returns: {Author: "Bob", Text: "Nice photo!"}
-- Pop the next 3, oldest first, in one write
db.Comments.pop({limit: 3});
-- Peek: look at head without removing
db.Comments.peek();
-- Peek the next 3, oldest first, without removing them
db.Comments.peek({limit: 3});
-- Length: number of items
db.Comments.length();
How It Works
Each pushed item gets the next position in the queue, and the queue keeps a head position and a tail position. A push writes the item at the tail; a pop reads the item at the head and removes it, moving the head on. peek() returns one item, the one a pop would return, and peek({limit: N}) the next N in the order N pops would return them, without removing any. A peek reads only what it returns (at most 1000 items), so its cost grows with N. pop({limit: N}) removes and returns the next N in one write (N is 1 to 100); a pop’s answer is the only copy of what it removed, so if your program can lose the answer, pop fewer at a time.
- Push: one write at the tail, in order
- Pop: one read at the head and a delete, with no scan of the items already consumed
Leases and dead entries
Every FIFO, LIFO and heap bucket also answers claim, ack, release,
extend, dead, requeue and discard. claim takes entries under a lease
and leaves them in the store, so a lost answer is a redelivery rather than a
loss; ack removes the entries whose work finished; release puts a lease’s
entries back at once; extend moves the deadline; dead lists entries that
were claimed 5 times without an acknowledgement (or a heap element whose _id_
was pushed again), and requeue/discard retry or drop them. A hash, btree or
blob bucket has no order to take from and refuses them. A store field is
pop-only in v1.0.
The lease_id that ack, release and extend name is the one claim
printed on its last line.
Where It Is Stored
A FIFO bucket is a range of keys in the node’s one store, like every other bucket: it has no directory, files or process of its own. See Where the data lives.
LIFO (Last In, First Out)
Stack. Items are pushed and popped from the same end. O(1) push and pop. It is stored like a FIFO bucket but pops from the tail instead of the head.
When to Use
- Undo stacks
- Session state (latest-first access)
- Recent items (most recent first)
- Browser history
- Photo feeds (newest first)
Operations
-- Push: append to tail (same as FIFO)
db.Sessions.push({IP: "192.0.2.1", UA: "Chrome"});
-- Pop: remove and return from tail (newest first)
db.Sessions.pop();
-- Returns: {IP: "192.0.2.1", UA: "Chrome"}
-- Pop the newest 3, newest first, in one write
db.Sessions.pop({limit: 3});
-- Peek: look at tail without removing
db.Sessions.peek();
-- Peek the newest 3, newest first, without removing them
db.Sessions.peek({limit: 3});
-- Length
db.Sessions.length();
FIFO vs LIFO
| Operation | FIFO | LIFO |
|---|---|---|
| Write | Append to tail | Append to tail (identical) |
| Read | Forward from head | Backward from tail |
| Pop | Removes the oldest item | Removes the newest item |
The API is identical — the store type determines behavior internally.
Hash (Key-Value)
Key-value table. O(1) average-case lookup, insert, and delete.
When to Use
- User profiles and accounts
- Configuration storage
- Session data
- Any exact key-value lookup
- Metadata storage
Operations
-- Insert
db.User.insert({_id_: "jane_doe", Name: "Jane", Email: "jane@example.com"});
-- Find by key
db.User.find({_id_: "jane_doe"});
-- Returns: {Name: "Jane", Email: "jane@example.com"}
-- Find with filter
db.User.find({Email: "jane@example.com"});
-- Update
db.User.update({_id_: "jane_doe"}, {$set: {Name: "Janet"}});
-- Delete
db.User.delete({_id_: "jane_doe"});
-- Count
db.User.count();
How It Works
Each document is stored under its bucket’s key range, keyed by its _id_, as a BSON-encoded value. A lookup by _id_ is one read; a filter on another field scans the bucket unless the field is indexed.
Placement
In this build spread is opt-in. A hash bucket created with no
placementis recordedwholefor now, until the spread collection work completes; nameplacement: spreadto get a spread bucket. The examples on this page run on either placement; a spread bucket differs in the ways listed below.
A hash bucket records a placement, spread or whole, when you create it. Leave it out and the bucket is whole in this build; name the one you want:
create bucket User { placement: spread };
create bucket Audit { placement: whole };
describe bucket User shows which. Only hash buckets have a placement; a FIFO, LIFO, heap, B+Tree or blob bucket is always whole, and placement: spread on one is refused.
whole keeps the bucket on the nodes that hold it, every document on every copy. Any copy answers a read, and at a replication factor of 2 or more the bucket stays readable while one node is down.
spread divides the bucket’s documents over the cluster. Each document belongs to one of 256 parts, chosen by a hash of its _id_, and each part is an ordinary bucket on the node the ring names for it, at the bucket’s replication factor. A read or write by key — get, insert, upsert, update, delete, a store-field verb, or a keyed BATCH item — reaches the document through any node, which sends it to the document’s part. find, count, distinct, search, describe and dump cover every part and answer as a whole bucket of the same documents does: find returns the same page in the same order, with or without a sort; count and describe the collection’s totals; distinct and search the same values and documents. A spread bucket’s name is at most 250 bytes (Limits), and a dump of it holds the collection, never a part (Backup and Restore).
Every operation on a spread bucket is answered through one node, its router, the node that owns the bucket’s name. While that node is down the bucket cannot be read or written at any replication factor, until it returns or remove-node hands the bucket’s name to a node that holds a copy of it; a whole bucket at factor 2 or more stays readable from its copies (Limitations).
A spread bucket refuses a unique field other than _id_ and an update by filter (multi from a driver). No part can see another part’s values, so neither can be checked. The refusals print the create that works, for example create bucket User in app { placement: whole, Name string, Email string unique }; update by _id_ one document at a time, or create the bucket placement: whole.
A spread bucket’s declaration is fixed when it is created. alter bucket and declaring new store fields are refused. migrate moves its parts with the rebalance rather than as one bucket. A bucket’s placement is fixed too. To change a whole bucket’s placement or declaration, dump it, drop it, create it with what you want and restore it (help placement prints the sequence; a dump does not carry blob payloads, so copy those out first). A spread bucket can be dropped: drop bucket removes every part and then the bucket’s name. From its first step the bucket refuses every other operation, and if a part’s node does not answer the drop is refused partway; run it again once that node returns or is removed. Its placement and declaration are fixed at create, so choose them before you load data; dump it first to keep a copy of the documents.
Where It Is Stored
In the node’s one store, as a key range of the bucket. See Where the data lives.
B+Tree (Sorted Key-Value)
Sorted key-value store. Keys are stored in sorted order, enabling efficient range queries and ordered iteration.
When to Use
- Leaderboards and rankings
- Alphabetical or numerical listings
- Time-range queries on indexed fields
- Any data requiring ordered iteration
Operations
-- Insert: the key is the sort order. Keys are strings, so write a score
-- to one width with leading zeros ("0092", not "92") to sort numerically.
db.Leaderboard.insert({_id_: "0100", User: "jane"});
db.Leaderboard.insert({_id_: "0085", User: "bob"});
db.Leaderboard.insert({_id_: "0092", User: "alice"});
-- Get by exact key
db.Leaderboard.get({_id_: "0092"});
-- Range query: the bounds apply to the key, written at the top level
db.Leaderboard.range({$gte: "0090"});
-- Returns the entries with key >= "0090", in key order: 0092 then 0100
-- First (lowest key)
db.Leaderboard.first();
-- Last (highest key)
db.Leaderboard.last();
-- Count
db.Leaderboard.count();
How It Works
Keys are stored in an order-preserving form, so the store’s own key order is the bucket’s order. Every key you write through the CLI or the wire is a string, even one that looks like a number, so a range over 1, 2, 10 puts 10 before 2. Write numeric keys with leading zeros to one width (0002, 0010) to get numeric order. A range, first or last lists each key exactly as you wrote it, so a key you put is the key you get back (the CLI shows a key holding a control character, such as a NUL or an escape code, in quotes). A range’s bounds are inclusive, and a low bound above the high bound is an empty range (the CLI prints (no entries in range)), not an error. HQL: B-tree ranges has the full range syntax.
Where It Is Stored
In the node’s one store, as a key range of the bucket. See Where the data lives.
Heap (Priority Queue)
Priority queue. The highest-priority item is always the next one out, and peek shows it without removing it.
When to Use
- Task queues (most important first)
- Alert prioritization
- Job scheduling
- Any “most important first” access pattern
Operations
-- Push with priority
db.Alerts.push({priority: 10, Msg: "Disk full", Host: "node-1"});
db.Alerts.push({priority: 5, Msg: "High CPU", Host: "node-2"});
db.Alerts.push({priority: 8, Msg: "Network latency", Host: "node-3"});
-- Pop: remove and return highest priority
db.Alerts.pop();
-- Returns: {Msg: "Disk full", Host: "node-1"}
-- Pop the next 2 by priority, highest first, in one write
db.Alerts.pop({limit: 2});
-- Peek: see highest priority without removing
db.Alerts.peek();
-- Returns: {Msg: "Network latency", Host: "node-3"}
-- Peek the next 2 by priority, highest first, without removing them
db.Alerts.peek({limit: 2});
-- Length
db.Alerts.length();
On a heap, priority orders the element and is not stored in the document,
so pop() and peek() return the document without it. Keep a copy in another
field if you need to read it back. A push with no priority is accepted at
priority 0. On every other bucket type, priority is an ordinary field and is
stored like any other.
How It Works
Each element is stored under a key that starts with its inverted priority, so the store’s own key order puts the highest priority first. peek() reads the first key, peek({limit: N}) the first N keys in order, and pop reads the first and deletes it (pop({limit: N}) reads and deletes the first N in one write). Entries of equal priority come out in _id_ order, which is push order for ids the server generates; peek and pop always agree on it. There is no in-memory index to rebuild: opening a heap bucket costs nothing, however many elements it holds.
Complexity
| Operation | Cost |
|---|---|
| Push | one ordered write, O(log n) |
| Pop | a read of the first key and a delete, O(log n) |
| Peek | a read of the first key |
| Length | a stored count, maintained as items are pushed and popped |
Where It Is Stored
In the node’s one store, as a key range of the bucket. See Where the data lives.
Blob (Binary Storage)
Raw binary data storage. Content under 64 KiB is stored inline with its metadata in the node’s shared store; larger content is a file in one payload tree shared by the whole node, not a directory per bucket.
When to Use
- File storage (images, PDFs, audio, video)
- Binary assets alongside metadata
- Any data where raw bytes are the primary content
Operations
-- Put: store a blob under a key. The content is the data string and the
-- rest is metadata. Leave the key out and the server mints a UUID.
db.Photos.put({_id_: "sunset", data: "hello photo", type: "text/plain"});
-- Get: retrieve the content and its metadata
db.Photos.get("sunset");
-- Info: metadata only (no data transfer)
db.Photos.info("sunset");
-- Get, written straight to a file instead of printed (there is no chunked
-- streaming read yet — this still moves the whole blob in one round trip):
db.Photos.get({_id_: "sunset", out: "sunset.txt"});
-- Delete
db.Photos.delete("sunset");
How It Works
- Content under 64 KiB is stored inline, as part of the blob’s own metadata, in the node’s shared store; only content at or above that boundary becomes a file
- Blob files, when written, are raw binary on disk — no encoding, no transformation
- A SHA-256 checksum is stored with the blob’s metadata and checked on every read, so corrupted content is refused rather than returned
- A blob is capped at 16 MiB by default; an operator can raise the cap to at most 31.9 MiB (
HOARDDB_MAX_BLOB_BYTES, see Configuration). A larger blob is refused with the limit in the message. For larger files, keep the file on your own storage and store a pointer document, see Large file stores - There is no streaming API:
putandgetmove a blob’s full content in one round trip, buffered in memory on both ends.get({..., out: "path"})writes the content straight to a file instead of printing it, but does not avoid loading it first - References in BSON documents store the blob’s key — the
_id_you pass toput(), or the UUID the server mints for you when you omit one — not the blob data
On-Disk Layout
A blob bucket writes to two node-wide places — never a directory per bucket:
<data_dir>/
├── store/ # every database and bucket: documents, indexes, queue
│ # entries and blob metadata (name, type, size, checksum,
│ # and inline content under 64 KiB), keyed by bucket
└── blobs/ # blob payloads at or above 64 KiB, fanned out by hash —
# or HOARDDB_BLOB_DIR, if it is set, instead of <data_dir>/blobs
There is no Photos/ directory and no per-bucket index file: every bucket in the database shares the one store, and every blob bucket on the node shares the one payload tree.
Referencing Blobs
BSON documents store a blob’s key — the same string passed to get() and delete() — not the data itself:
{
"_id_": "0192a3b4-c5d6-7890-abcd-ef1234567895",
"Caption": "Sunset over the hills",
"Photos": [
"0192a3b4-c5d6-7890-abcd-ef12345678a1",
"0192a3b4-c5d6-7890-abcd-ef12345678a2"
]
}
To retrieve the actual image, call db.Photos.get({_id_: "<key>"}) with the key stored in Photos.
Where the data lives
Every bucket, of every store type, and every store field on every document, is a range of keys in the one store of the node that holds it. A bucket has no directory, no files and no process of its own, so creating another costs its catalog entry and the data you put in it, and a node can hold thousands of buckets. The only data kept outside that store is the content of blobs at or above 64 KiB, which is a file in one payload tree shared by the whole node. Limits gives the numbers.
<data_dir>/
├── store/ # every database's every bucket and document store:
│ # documents, indexes, queue entries, heap elements and
│ # blob metadata, keyed by bucket, not by directory
└── blobs/ # blob payloads at or above 64 KiB — or
# HOARDDB_BLOB_DIR, if set, instead of <data_dir>/blobs
Every store type is equally durable: a node answers a write only once it is on disk, so an acknowledged write survives a crash of the process, a kernel crash and a power cut. A node in a cluster with other members syncs its write-ahead log before it applies the write; any other node, a single node included, syncs its store before it answers, one sync shared by the writes that arrive together. Storage durability has the detail, and the one setting that trades this away for benchmarks.
L1 vs L2 Bucket Decision Guide
A bucket declares its fields, and a field can itself be a store of any of the six types, on each document. Choosing a store type is therefore two choices: which store the bucket is, and which stores each of its documents carries. HQL: store fields shows the syntax.
Levels
- L1 — a bucket. Like a SQL table or a document-database collection. It has a key of its own, so you can look any of its documents up directly.
- L2 — a store field of one document:
Comments fifoStoreonPostgives every post its own comments queue,user.Friendsis that user’s own hash. It is addressed through the document,db.Post["p1"].Comments, and it is deleted with the document. - L3 — a store on the elements of an L2 field
(
Comments fifoStore { Flags fifoStore }). Keep it shallow; L1 and L2 cover most designs. A scalar inside those braces is noted as not enforced (and the note names the store field it sits in); auniqueinside them is refused, because an index covers the bucket’s own fields only. Put the unique field on the bucket, or give the elements their own bucket.
Decision Rules
| Data | Where it goes | Reason |
|---|---|---|
| User’s first name | a field of the User document | Fixed size, read with the document |
| User’s friends | Friends hashStore on User | Belongs to one user, looked up by friend id |
| User’s posts | a Post bucket | A post has an identity of its own: you list, index and look up posts across users |
| Post’s comments | Comments fifoStore on Post | Belongs to one post, read in order |
| Post’s photos | Photos blobStore on Post | Belongs to one post |
| Post’s caption | a field of the Post document | Fixed size, read with the document |
The Critical Rule
If you will look a thing up, list it or index it on its own, across its owners, it is a bucket. If it only ever exists as part of one document, it is a store field of that document.
If one document’s store field is read so often that its node is the bottleneck,
raise the bucket’s replication factor (alter bucket <name> { replication_factor: 3 })
so more nodes hold, and answer reads of, the copy. Do not add a level of nesting:
a store field lives inside its document on the same nodes, so nesting spreads
nothing.
A store field is reached through its document’s key, so it has no index and
find and search do not see it. A Post you want to search by Author
belongs in a bucket with indices: ["Author"]; the comments under one post
belong in that post. Many small stores are what the design is for: one queue
per post, one hash per user.
L1 User ← bucket
└── L2 Friends (hashStore) ← one per user: db.User["jane"].Friends
L1 Post ← its own bucket, one document per post
├── L2 Comments (fifoStore) ← one per post: db.Post["p1"].Comments
└── L2 Photos (blobStore) ← one per post: db.Post["p1"].Photos
L2 → L1 Migration
When an L2 bucket has grown too large to live inside its parent, the application can migrate:
- Create a new L1 bucket (e.g.,
Post_Comments_{postId}) - Pop all entries from the L2 and push to the new L1
- Update the parent’s reference to point to the new L1
- Delete the empty L2
Example: PhotoJournal Schema
Two entities, User and Post, each a bucket, and the parts that belong to
one document are store fields of it:
create database PhotoJournal;
create bucket User {
placement: whole,
Email string unique,
Friends hashStore
};
create bucket Post {
Caption string,
Photos blobStore,
Comments fifoStore
};
User’s documents carry FirstName, LastName and Email, and Post’s a
Caption and the Author’s user id. Email string unique is a unique index
on Email, the same as indices: [{field: "Email", unique: true}], and it needs
placement: whole (see Placement); it is the
one scalar keyword that is enforced, and any other (Caption string) is
accepted and noted, not enforced. See indexes.md.
Each user has a Friends hash of their own, and each post has its own
Photos and Comments:
[h]oardDB [PhotoJournal]> db.User.insert({_id_: "jane", FirstName: "Jane", Email: "jane@example.com"})
OK id=jane
[h]oardDB [PhotoJournal]> db.User.insert({_id_: "sam", FirstName: "Sam", Email: "sam@example.com"})
OK id=sam
[h]oardDB [PhotoJournal]> db.User["jane"].Friends.put("sam", {since: 2021})
OK
[h]oardDB [PhotoJournal]> db.User["jane"].Friends.get("sam")
{"_id_": "01a101bb-59fb-7b53-86f4-2605a706ec60", "since": 2021}
[h]oardDB [PhotoJournal]> db.Post.insert({_id_: "p1", Author: "jane", Caption: "Sunset over the hills"})
OK id=p1
[h]oardDB [PhotoJournal]> db.Post["p1"].Comments.push({Author: "sam", Text: "Lovely light"})
OK
[h]oardDB [PhotoJournal]> db.Post["p1"].Comments.push({Author: "jane", Text: "Thanks"})
OK
[h]oardDB [PhotoJournal]> db.Post["p1"].Comments.pop()
{"Author": "sam", "Text": "Lovely light", "_id_": "01a101bb-59fc-73c2-88cf-20253a89985f"}
[h]oardDB [PhotoJournal]> db.Post["p1"].Comments.len()
1
[h]oardDB [PhotoJournal]> db.User.search({Email: "jane@example.com"})
Found 1 results (via Email_1):
{"Email": "jane@example.com", "FirstName": "Jane", "_id_": "jane"}
describe bucket Post lists the stores, with their element counts summed over
every post:
[h]oardDB [PhotoJournal]> describe bucket Post
BUCKET PhotoJournal.Post
---
Type: hash
Entries: 1
Logical size: 355 B (before compression and compaction)
Encrypted: no (this node's store)
Replication factor: 1 (inherited)
Indexes: (none — declare them when you create the bucket)
Stores: (elements summed over every document)
Photos blobStore 1 element(s)
Comments fifoStore 1 element(s)
Photos blobStore is declared the same way. Its put takes the content from
the data string and the rest as metadata, and get prints it or writes it
to a file with {out: "file"}:
[h]oardDB [PhotoJournal]> db.Post["p1"].Photos.put("ph1", {data: "hello photo", name: "b.txt"})
OK
[h]oardDB [PhotoJournal]> db.Post["p1"].Photos.get("ph1")
Key: ph1
Blob: 01a101e0-dcc1-7459-a116-4f4b62f44237
Size: 11 B
Metadata: {"name": "b.txt"}
Data:
hello photo
See HQL: store fields. A photo of your own is a file, so the post can also keep the key of a blob bucket’s entry instead, as in Referencing Blobs.
Referential design: Post is its own bucket, so a post points at its author
with the author’s user id, and the application resolves it with a get on
User. Store type is chosen per field as well as per bucket: a document’s
queue, table or heap is declared on it, not kept in a separate bucket that
points back. Keep a separate bucket only for something that is its own entity,
such as Post, or a dedicated heap of alerts that you read across all users.
Where it is stored: both buckets, and every Friends, Photos and Comments store on
their documents, live in the node’s one shared store, as described in
Where the data lives. User’s Email index is entries kept
alongside User’s own documents, not a file of its own.
Source: docs/user/store-types.md in the repository.