Documentation
¶
Overview ¶
Full document enumeration in a single pass.
All scans data records (_r=2) across the heap and sparse regions, extracting label and content by byte scanning. Unlike the List+Get pattern, it never follows index pointers — content is read directly from the data record, avoiding per-document seek overhead.
The scan uses the same sorted-region awareness as Search: only the heap and sparse regions are visited, skipping the index section entirely. Records retired by Set or Delete have their type byte patched from 2 to 3 (history), so the type check at TypePos naturally excludes them.
Database teardown.
Public entry points for the two common Repair modes.
Compression for inline history snapshots.
Each record's _h field stores the document content at the time of write. The content is Zstd-compressed for size, then Ascii85-encoded to produce a printable string that can be embedded directly in a JSON value without escaping. This avoids the 33% overhead of base64 while remaining newline-free (critical for the line-delimited format).
Core type definitions and structural helpers.
Concurrency is managed in three layers, each serving a different scope:
Atomic state machine (db.state): gates whether new operations are allowed at all. Transitions: StateAll → StateRead → StateNone → StateClosed. Checked at the top of every public method.
sync.RWMutex (db.mu): coordinates in-process readers and writers. Readers hold RLock; writers and Repair hold Lock.
OS file lock (db.lock): coordinates across processes via flock(2) or LockFileEx. See internal/flock for lifetime management.
When an operation starts, it waits (via db.cond) until the state allows it, then acquires the appropriate lock level at layers 2 and 3.
Soft deletion — the record is converted to history so its compressed snapshot survives for version retrieval, but it no longer appears in lookups or listings because its index is erased.
Package folio provides an append-only document database backed by a single JSONL file. Documents are stored as newline-delimited JSON records with automatic versioning — every update preserves the previous content as a compressed history snapshot.
The file is divided into a heap, index, and sparse region. The heap co-locates data and history records sorted by ID then timestamp, so all versions of a document are contiguous. Indexes follow the heap in sorted order. The sparse region collects new writes and is scanned linearly. Compaction merges the sparse region back into the heap. This design keeps all state on disk without requiring in-memory indexes, though an optional bloom filter can accelerate negative lookups in the sparse region.
Concurrency gating for the three-layer lock protocol.
blockWrite and blockRead acquire all three concurrency layers (state check → OS flock → RWMutex) before allowing an operation to proceed. On return the caller holds db.mu (Lock or RLock) and db.lock; both must be released in the defer of the calling method.
Document retrieval using the two-region lookup strategy.
Lookups check the sorted index section first (binary search, O(log n)), then fall back to the sparse region (linear scan) for records written since the last compaction. The optional bloom filter can skip the sparse scan entirely when an ID is definitively absent.
Hash algorithms for deriving the _id field from a document label.
_id is always 16 hex characters (64 bits). This fixed width is what allows scanm to extract IDs at a known byte offset without parsing. The algorithm is stored in the header so all records in a file use the same one; Rehash can migrate between algorithms in place because the output width is identical across all three.
xxHash3 is the default because it has the best throughput for short strings (document labels) and excellent distribution. FNV-1a exists as a stdlib-only fallback for environments that cannot use cgo or external dependencies. Blake2b is offered for users who want cryptographic-quality distribution to minimise collision probability, at the cost of ~10x slower hashing — relevant only for very large databases where birthday-bound collisions on 64-bit hashes become a concern.
The header occupies the first 128 bytes of the database file. It stores section boundaries, document count, and a dirty flag for crash recovery. The fixed size allows the dirty flag to be toggled with a single-byte write at a known offset, avoiding a full header rewrite on every mutation.
Version history retrieval from compressed _h snapshots.
Both current Records (_r=2) and retired History records (_r=3) carry a compressed snapshot in _h. History collects all of them for a given label, decompresses each, and yields them in chronological write order.
After compaction, all versions of a document are contiguous in the heap (sorted by ID then timestamp). History uses group() to binary-search the heap for the ID group, then linearly scans the sparse region for any records appended since the last compaction.
Because results must be sorted by file offset (the ground truth for write order), all versions are collected and sorted before yielding. The iterator API provides consistency with Search, MatchLabel, and List even though this method buffers internally.
Label enumeration across the entire file.
Memory-mapped file I/O for Unix platforms.
mmapFile maps a file read-only with MAP_SHARED so that in-place patches made through the writer fd are visible immediately (both fds share the kernel page cache). Only appends that extend the file beyond the mapped region require a remap.
Database creation and opening.
Low-level read primitives for the newline-delimited record format.
Every record is a single JSON line terminated by '\n'. These functions read individual lines and find record boundaries via io.ReaderAt so that concurrent readers do not interfere with each other's offsets.
All read functions accept a source (an io.ReaderAt with a known size) rather than *os.File directly. When memory-mapping is enabled the source wraps the mmap'd region; otherwise it wraps the read-only fd.
Record format and type definitions.
Every line in the database file is a JSON object beginning with {"_r":N where N identifies the record type. This fixed prefix allows type detection and ID extraction at known byte offsets without JSON parsing — critical for binary search and compaction where millions of records may be scanned.
Three types coexist in the file:
- Index (_r=1): maps a label's hash to the byte offset of its data record.
- Record (_r=2): the current content of a document.
- History (_r=3): a previous version with compressed content in _h.
On update, the old Record is retyped to History (byte patch from 2→3) and its _d field is blanked. This preserves the compressed snapshot in _h for version retrieval while making the record invisible to data scans.
In-place hash algorithm migration.
All three algorithms produce a 16 hex character (8 byte) _id, and the _id field sits at a fixed byte offset in every record. This means Rehash can overwrite each _id in place without moving or resizing any records — no temp file, no rewrite, just a linear scan with targeted byte patches.
The dirty flag is set before any patches begin and cleared after the header is updated. A crash mid-rehash leaves the flag set, so the next Open triggers automatic Repair — which rebuilds all IDs from labels, restoring consistency regardless of how many patches completed.
Label renaming with in-place patching when possible.
When old and new labels have the same byte length, Rename patches _id and _l directly in the data record and index record — no new version is created and no history entry is added. When lengths differ, it falls back to appending a new record+index and blanking the old ones (equivalent to Set+Delete but under a single lock hold).
History records are not patched in either path: they retain the old ID and become unreachable via History(newLabel). This matches the behaviour callers would get from the manual Get+Set+Delete approach.
Repair rebuilds the database file with all records in sorted order.
Over time, appends accumulate in the sparse region and lookups degrade toward linear scans. Repair reads every record, sorts by ID, and writes a new file with a contiguous heap (data + history sorted by ID then timestamp) followed by sorted indexes — restoring O(log n) binary search. It also serves as crash recovery: on Open, if a .tmp file or dirty flag is found, Repair is run automatically to restore consistency.
A temporary file (.tmp) is used instead of rewriting in place because in-place rewrite risks total data loss on crash: if the process dies mid-rewrite, both the old and new data are gone. Writing to a temp file, syncing, then atomically renaming means the original file is intact until the rename succeeds. A crash during the write phase at worst orphans the .tmp file, which is cleaned up on next Open.
The operation proceeds in two phases to minimise the time readers are blocked:
- Phase 1 (read lock): scan the old file and write the new .tmp file. Concurrent readers continue using the old file.
- Phase 2 (write lock): swap file handles from the old file to the new one. This is a brief exclusive lock for the atomic rename.
When called for crash recovery (BlockReaders=true), a write lock is held for the entire operation since the file may be inconsistent.
Scan strategies for the two-region file layout.
After compaction the file contains a sorted heap (data + history interleaved by ID then timestamp) followed by sorted indexes. New writes are appended after the indexes into a sparse (unsorted) region. Lookups therefore need two strategies:
- scan: binary search over a sorted section. O(log n) seeks. Pass recordType=0 to match any type (type-agnostic).
- sparse: linear scan over the unsorted region. O(n) but bounded to records written since the last compaction.
scanm is a compaction-only variant that extracts metadata at fixed byte positions without JSON parsing, since compaction must touch every record but only needs ID, type, timestamp, and label.
Search over document content and labels.
Search scans data records (_r=2) and matches against the _d field. After compaction the file has three sections: heap (data + history), index, and sparse. Search only needs the heap and sparse regions — the index section contains no _d fields. The scan skips the index section entirely by reading [HeaderSize..heapEnd) then [sparseStart..EOF). Before any compaction both boundaries are zero, so the first range is empty and the second covers the whole file — identical to a full scan.
Literal patterns (no regex metacharacters) take a fast path: the query is JSON-escaped and matched with bytes.Contains, avoiding both regex overhead and the need to unescape record content. Patterns containing regex metacharacters fall back to regexp.Match with optional decode.
The literal path works by escaping the search term into the same JSON representation used on disk (via json.Marshal), then matching raw bytes directly. This avoids per-record unescape overhead entirely. However, it assumes the on-disk encoding matches what json.Marshal produces. If Decode is set, the caller explicitly wants unescape-then-match semantics (e.g. to handle non-standard encodings like \u0041 for 'A'), so the literal path is bypassed to guarantee equivalent results.
Case-insensitive literal search uses bytes.ToLower on both needle and content. This allocates a copy of the _d slice per record. A zero-alloc alternative (sliding bytes.EqualFold) would trade O(n) for O(n*m) but eliminate GC pressure. We keep ToLower for now because search terms are typically short and the allocation is bounded to the _d field, not the full record line. Revisit if profiling shows GC pressure from search.
MatchLabel scans index records (_r=1) and matches against _l. It scans only the index section and sparse region, skipping the heap entirely.
Both stream through the file line-by-line to avoid loading it into memory. Callers consume results lazily via range and can break early to stop the scan without reading the rest of the file.
Document creation and update using append-then-blank.
A Set always appends a new Record+Index pair at the tail. If an older version exists, it is then patched in place: its type byte is changed from Record (2) to History (3), its _d content is blanked with spaces, and its index line is overwritten with spaces. The compressed snapshot in _h is preserved so History can still retrieve the old content. This approach avoids rewriting the file on every update while keeping the latest version immediately accessible via the newest index.
Batch amortises lock acquisition across multiple documents. All inputs are validated before any writes begin — if validation fails, no documents are written.
Write primitives for the append-only file.
New records are always appended at db.tail (the current end of file). The dirty flag is set on the first write of a session so that an unclean shutdown can be detected on next Open and trigger automatic repair. It is cleared during Close once all data has been flushed.
Example ¶
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
// Open or create a database
db, err := folio.Open(filepath.Join(dir, "myapp.folio"), folio.Config{})
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Store a document
db.Set("readme", "# My App\n\nWelcome to my application.")
// Retrieve it
content, _ := db.Get("readme")
fmt.Println(content)
}
Output: # My App Welcome to my application.
Index ¶
- Constants
- Variables
- type CompactOptions
- type Config
- type DB
- func (db *DB) All() iter.Seq2[Document, error]
- func (db *DB) Batch(docs ...Document) error
- func (db *DB) Close() error
- func (db *DB) Compact() error
- func (db *DB) Count() int
- func (db *DB) Delete(label string) error
- func (db *DB) Exists(label string) (bool, error)
- func (db *DB) Get(label string) (string, error)
- func (db *DB) History(label string) iter.Seq2[Version, error]
- func (db *DB) List() iter.Seq2[string, error]
- func (db *DB) MatchLabel(pattern string) iter.Seq2[Match, error]
- func (db *DB) Purge() error
- func (db *DB) Rehash(newAlg int) error
- func (db *DB) Rename(old, new string) error
- func (db *DB) Repair(opts *CompactOptions) error
- func (db *DB) Search(pattern string, opts SearchOptions) iter.Seq2[Match, error]
- func (db *DB) Set(label, content string) error
- type Document
- type Entry
- type Header
- type Index
- type Match
- type Record
- type Result
- type SearchOptions
- type Version
Examples ¶
Constants ¶
const ( StateAll = 0 // reads and writes permitted StateRead = 1 // reads only (Compact in progress, Phase 1) StateNone = 2 // nothing permitted (Rehash / crash recovery) StateClosed = 3 // terminal )
State machine values. Transitions are monotonic during shutdown (All→Closed) but cycle during maintenance (All→Read→All for Compact, All→None→All for Rehash). blockRead/blockWrite wait on db.cond until the state allows their operation.
const ( AlgXXHash3 = 1 // default — fastest, good distribution AlgFNV1a = 2 // stdlib only, no external dependencies AlgBlake2b = 3 // cryptographic quality distribution )
const ( TypeIndex = 1 TypeRecord = 2 TypeHistory = 3 )
Record type markers. These appear as the first value in every JSON line ({"_r":N) and are used for byte-level type checks during scan.
const ( TypePos = 6 // {"_r":N — type digit position IDStart = 15 // first byte of the 16-char hex ID IDEnd = 31 // one past the last byte of the ID TSStart = 39 // first byte of the 13-digit timestamp TSEnd = 52 // one past the last byte of the timestamp MinRecordSize = 52 // shortest valid line (must reach TSEnd) )
Fixed byte positions within a record line. Every record starts with {"_r":N,"_id":"...","_ts":N and these fields are always serialised in the same order, so type, ID, and timestamp can be read at known offsets without JSON parsing.
const HeaderSize = 128
HeaderSize is fixed so the dirty flag can be patched at a known byte offset without rewriting the whole header.
const MaxLabelSize = 256 // bytes
const MaxRecordSize = 16 * 1024 * 1024 // 16MB, bounds scanner buffer allocation
Variables ¶
var ( ErrNotFound = errors.New("document not found") ErrExists = errors.New("document already exists") ErrLabelTooLong = errors.New("label exceeds maximum size") ErrInvalidLabel = errors.New("label contains invalid characters") ErrEmptyContent = errors.New("content cannot be empty") ErrClosed = errors.New("database is closed") ErrInvalidPattern = errors.New("invalid regex pattern") ErrCorruptHeader = errors.New("corrupt header") ErrCorruptRecord = errors.New("corrupt record") ErrCorruptIndex = errors.New("corrupt index") ErrDecompress = errors.New("decompression failed") )
Sentinel errors for programmatic handling. Callers can use errors.Is to distinguish recoverable conditions (ErrNotFound) from corruption (ErrCorruptHeader, ErrCorruptRecord, ErrCorruptIndex, ErrDecompress).
Functions ¶
This section is empty.
Types ¶
type CompactOptions ¶
type Config ¶
type Config struct {
HashAlgorithm int // 1=xxHash3 (default), 2=FNV1a, 3=Blake2b
ReadBuffer int // scanner buffer (default 64KB)
MaxRecordSize int // largest allowed record (default 16MB)
SyncWrites bool // fsync after every write (durability vs throughput)
BloomFilter bool // maintain bloom filter over the sparse region
AutoCompact int // compact every N writes; persisted to header, 0 = leave stored value unchanged
MMap bool // memory-map the file for reads (unix only)
Index bool // maintain in-memory index for O(1) lookups
}
Config tunes the memory/disk trade-off. Zero values use safe defaults.
Example ¶
package main
import (
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
// Custom configuration
cfg := folio.Config{
HashAlgorithm: folio.AlgXXHash3, // Default, fastest
SyncWrites: true, // fsync after each write
ReadBuffer: 128 * 1024, // 128KB read buffer
MaxRecordSize: 32 * 1024 * 1024, // 32MB max record
}
db, _ := folio.Open(filepath.Join(dir, "custom.folio"), cfg)
defer db.Close()
}
Output:
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is an open database handle. Two separate file descriptors are held so that concurrent reads never contend with writes. ReadAt on the O_RDONLY fd is position-independent and safe for concurrent readers; a single O_RDWR fd would have reads and appends fighting over the shared file position. Splitting eliminates that contention entirely.
func Open ¶
Open opens or creates a database at the given path. If a previous session crashed (dirty flag set, or .tmp file left behind), an automatic Repair is attempted under an exclusive lock to restore consistency before returning.
func (*DB) All ¶ added in v0.1.2
All yields every current document as a label–content pair. It scans data records directly, avoiding the N+1 cost of List followed by Get for each label. Callers consume results lazily via range and can break early to stop the scan.
func (*DB) Batch ¶ added in v0.1.2
Batch creates or updates multiple documents under a single lock hold. All inputs are validated before any writes begin. Documents are processed in slice order.
func (*DB) Close ¶
Close flushes state, clears the dirty flag if set, and releases all file handles. Any blocked operations wake up and receive ErrClosed.
func (*DB) Compact ¶
Compact merges the sparse region back into sorted order, restoring binary search performance. All history is preserved.
Example ¶
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
// After many writes, compact reorganises for faster reads
for i := range 100 {
db.Set("counter", fmt.Sprintf("%d", i))
}
// Compact sorts data for binary search (preserves history)
db.Compact()
// Purge removes history, keeping only current versions
db.Purge()
}
Output:
func (*DB) Count ¶ added in v0.1.2
Count returns the current document count. This is a best-guess value maintained incrementally by Set and Delete. It is corrected to an accurate count during Compact or Repair.
func (*DB) Delete ¶
Delete soft-removes a document. The record's compressed history snapshot is preserved; only Purge permanently removes it.
Example ¶
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
db.Set("temp", "Temporary data")
// Delete removes from active documents but preserves history
db.Delete("temp")
_, err := db.Get("temp")
fmt.Println(err == folio.ErrNotFound)
}
Output: true
func (*DB) Exists ¶
Exists performs the same two-region lookup as Get but returns as soon as a matching index is found, without reading the data record.
func (*DB) Get ¶
Get returns the current content of a document identified by label. The lookup follows the index (not the data records directly) because the index is smaller and faster to binary search, then a single seek to the data record's offset retrieves the content.
Example ¶
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
db.Set("greeting", "Hello, World!")
content, err := db.Get("greeting")
if err == folio.ErrNotFound {
fmt.Println("Document not found")
return
}
fmt.Println(content)
}
Output: Hello, World!
func (*DB) History ¶
History yields every version of a document in chronological order. It searches the heap via binary search (O(log n) + group size), then scans the sparse region for records appended since the last compaction.
Example ¶
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
// Create multiple versions
db.Set("doc", "Version 1")
db.Set("doc", "Version 2")
db.Set("doc", "Version 3")
// Retrieve all versions (oldest first)
i := 0
for v, err := range db.History("doc") {
if err != nil {
log.Fatal(err)
}
i++
fmt.Printf("v%d: %s\n", i, v.Data)
}
}
Output: v1: Version 1 v2: Version 2 v3: Version 3
func (*DB) List ¶
List yields labels for all current documents. It scans the entire file (both sorted and sparse regions) for index records because a document may only exist in the sparse region if it was created since the last compaction. Labels are deduplicated but not sorted. Callers consume results lazily via range and can break early to stop the scan.
Example ¶
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
db.Set("apple", "A fruit")
db.Set("banana", "Another fruit")
db.Set("carrot", "A vegetable")
count := 0
for _, err := range db.List() {
if err != nil {
log.Fatal(err)
}
count++
}
fmt.Printf("Documents: %d\n", count)
}
Output: Documents: 3
func (*DB) MatchLabel ¶
MatchLabel matches a regex against the _l field of index records. Only index lines (_r=1) are checked, so the scan skips data records entirely using the type byte at TypePos. Results are yielded lazily.
func (*DB) Purge ¶
Purge does the same as Compact but also drops history records, permanently removing all previous versions of every document.
func (*DB) Rehash ¶
Rehash migrates all records to a new hash algorithm. Blocks all readers and writers because every _id in the file is being rewritten.
func (*DB) Rename ¶ added in v0.1.2
Rename changes a document's label. Returns ErrNotFound if old does not exist, or ErrExists if new already exists.
func (*DB) Repair ¶
func (db *DB) Repair(opts *CompactOptions) error
Repair rebuilds the file. See the package comment for phase details.
func (*DB) Search ¶
Search matches a pattern against the _d field of current data records. Results are yielded lazily; break from the range loop to stop early.
Example ¶
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
db.Set("readme", "# Welcome\n\nThis is the README file.")
db.Set("changelog", "# Changelog\n\n## v1.0\n- Initial release")
// Search file content with regex
count := 0
for _, err := range db.Search("README", folio.SearchOptions{}) {
if err != nil {
log.Fatal(err)
}
count++
}
fmt.Printf("Matches: %d\n", count)
}
Output:
func (*DB) Set ¶
Set creates or updates a document. See the package comment for the append-then-blank strategy.
Example ¶
package main
import (
"log"
"os"
"path/filepath"
"github.com/jpl-au/folio"
)
func main() {
dir, _ := os.MkdirTemp("", "folio-example")
defer os.RemoveAll(dir)
db, _ := folio.Open(filepath.Join(dir, "example.folio"), folio.Config{})
defer db.Close()
// Create a new document
err := db.Set("config", "theme: dark\nlanguage: en")
if err != nil {
log.Fatal(err)
}
// Update overwrites the previous version (history preserved)
db.Set("config", "theme: light\nlanguage: en")
}
Output:
type Entry ¶
type Entry struct {
ID string
TS int64
Type int
SrcOff int64 // position in the source file
DstOff int64 // position in the compaction output (set during write)
Length int
Label string // populated only for index entries
}
Entry holds lightweight metadata extracted by scanm. Full JSON parsing is skipped — fields are read at fixed byte positions. DstOff is zero until compaction fills it with the record's new position in the output.
type Header ¶
type Header struct {
Version int `json:"_v"` // Format version: 1 = current
Error int `json:"_e"` // Dirty flag: 1 = unclean shutdown detected
Algorithm int `json:"_alg"` // Hash algorithm used to derive _id from label
Timestamp int64 `json:"_ts"` // Unix ms when this header was last written
State [6]uint64 `json:"_s"` // Section boundaries, counts, compaction state
}
Header describes the file layout. The State array holds section boundaries and counters that divide the file into contiguous regions:
[0..128) Header (this struct, space-padded, newline-terminated) [128..State[stHeap]) Heap: data + history sorted by ID then timestamp [State[stHeap]..State[stIndex]) Sorted index records [State[stIndex]..EOF) Sparse region (unsorted appends since last compaction)
Within each ID group in the heap, records are sorted oldest-first. History records (_r=3) precede the current data record (_r=2). A zero offset means that section is empty or not yet established.
type Index ¶
type Index struct {
Type int `json:"_r"`
ID string `json:"_id"`
Timestamp int64 `json:"_ts"`
Offset int64 `json:"_o"` // byte position of the corresponding Record
Label string `json:"_l"`
}
Index maps a label's hashed ID to the byte offset of its data Record. During lookup, the index is found first (by binary or sparse scan on ID), then the data record is read at the offset the index points to.
type Match ¶
Match is a single search result: a label and the byte offset of the matching record in the file.
type Record ¶
type Record struct {
Type int `json:"_r"`
ID string `json:"_id"` // 16 hex chars, hash of Label
Timestamp int64 `json:"_ts"` // unix ms
Label string `json:"_l"`
Data string `json:"_d"` // current content (blank for history)
History string `json:"_h"` // zstd+ascii85 compressed snapshot
}
Record is a data or history line. When a document is updated, the old Record has its Type patched from 2→3 (becoming history) and _d blanked, so the compressed _h snapshot is the only way to recover prior content.
type Result ¶
Result carries a record's position and raw bytes from a scan. Callers use Offset to read or overwrite the record and Data for parsing.
type SearchOptions ¶
type SearchOptions struct {
CaseSensitive bool
Decode bool // unescape JSON string escapes in _d before matching; bypasses literal fast path
}
SearchOptions configures Search behaviour. Callers control result count by breaking out of the range loop — no Limit field is needed.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
bloom
Package bloom provides a probabilistic set membership filter.
|
Package bloom provides a probabilistic set membership filter. |
|
flock
Package flock provides OS-level file locking for cross-process coordination.
|
Package flock provides OS-level file locking for cross-process coordination. |