Storage Package
This package provides a unified storage abstraction layer for zqk, supporting both file-based and graph-based storage backends through a common interface.
Architecture Overview
The storage package follows a provider pattern with pluggable backends:
ObjectStorageProvider (Interface)
├── FileObjectStorage (File-based backend)
└── GraphObjectStorage (Graph-based backend)
Backend Selection
Storage backends are selected automatically via StorageFactory:
- Checks if graph backend is enabled (
ZQK_GRAPH_ENABLED=true) - Attempts to connect to graph database
- Falls back to file backend if graph is unavailable or disabled
- Only one backend is active at a time
Intentional differences (file vs graph)
| Aspect | File backend | Graph backend |
|---|---|---|
| Storage | YAML files in .zqk/process/{kind}/; bucketing; optional CAS |
Nodes and edges in MemGraph/Neo4j; kind-specific labels |
| Validation | Full spec + lifecycle + reference + blocking-issue detection | Spec + lifecycle; graph enforces relationships |
| Query | List/filter/sort over files; full-text search | List/filter/sort; Cypher; vector similarity; traversal (GetRelated, GetPath, GetNeighbors) |
| Use when | Default; no graph DB; traceable YAML on disk | Graph DB enabled; relationship-heavy queries; vector search |
Shared behavior: same ObjectStorageProvider interface, permission checks, metadata (created_at, updated_at, namespace_id), and error types.
Core Components
ObjectStorageProvider Interface
The ObjectStorageProvider interface defines the contract for all storage backends:
- CRUD Operations:
Create,Read,Update,Delete,Move - Query Operations:
List,Query,Count,Exists,Aggregate - Search:
Search(full-text search with fuzzy matching) - Bulk Operations:
BulkCreate,BulkUpdate,BulkGet,BulkDelete - Graph Operations:
GetRelated,GetPath,GetNeighbors(relationship traversal) - Transactions:
BeginTransaction(atomic multi-object operations)
Shared Utilities
Common functionality extracted to storage_common.go:
- Permission Checking:
CheckPermission,CheckPermissionWithKindSpecialCases - Metadata Management:
EnsureObjectMetadata(sets created_at, updated_at, namespace_id) - Built-in Detection:
IsBuiltIn(identifies system objects)
File Storage Backend
FileObjectStorage implements file-based storage:
- Location: Objects stored as YAML files in
.zqk/process/{kind}/directories - Bucketing: Supports configurable bucketing strategies (date-based, hash-based, etc.)
- Content-Addressable Storage (CAS): Optional CAS for large files
- Hash Registry: Tracks file hashes for integrity checking
- Validation: Comprehensive validation with blocking issue detection
- Transactions: File-based transaction support
Key Files:
object_storage_file.go- Core infrastructureobject_storage_file_crud.go- Create, Read, Update, Delete operationsobject_storage_file_list.go- List, query, filtering, sortingobject_storage_file_validation.go- Validation logicobject_storage_file_helpers.go- Helper functions
Graph Storage Backend
GraphObjectStorage implements graph-based storage:
- Database: Uses MemGraph/Neo4j via
pkg/graph/provider - Labels: Objects stored as nodes with kind-specific labels
- Relationships: Reference fields create graph edges
- Cypher Queries: Supports custom Cypher queries
- Vector Search: Supports vector similarity search
- Validation: Simplified validation (graph handles relationships)
Key Files:
object_storage_graph.go- Core infrastructureobject_storage_graph_crud.go- Create, Read, Update, Delete operationsobject_storage_graph_list.go- List, query, filteringobject_storage_graph_traversal.go- Graph traversal operationsobject_storage_graph_validation.go- Validation logic
Common Patterns
Permission Checking
All operations enforce permissions via SecurityContext:
// Check permission before operation
if err := CheckPermission(secCtx, "write", kind); err != nil {
return err
}
Metadata Management
Objects automatically get metadata fields set:
// Ensures created_at, updated_at, created_by, updated_by, namespace_id
EnsureObjectMetadata(obj, secCtx, isCreate, idValidator)
Validation
Both backends validate objects before persistence:
- Spec Validation: Validates against object specs
- Lifecycle Validation: Validates state transitions
- Reference Validation: Ensures referenced objects exist
- File Backend: Additional blocking issue detection
Error Handling
Consistent error types:
ErrObjectNotFound- Object doesn't existErrObjectExists- Object already existsErrVersionConflict- Optimistic locking conflictErrPermissionDenied- Permission denied
Advanced Features
Transactions
Atomic multi-object operations:
tx, _ := storage.BeginTransaction(ctx)
tx.Create(ctx, secCtx, obj1)
tx.Update(ctx, secCtx, id2, updates)
tx.Commit(ctx) // or tx.Rollback(ctx)
Bulk Operations
Efficient batch processing:
result, _ := storage.BulkCreate(ctx, secCtx, objects)
// result.SuccessCount, result.FailureCount, result.Errors
Filtering and Sorting
Rich query capabilities:
filter := ListFilter{
Kind: "backlog_item",
Filters: map[string]any{
"status": "planned",
"priority": map[string]any{"$gt": 5},
},
SortBy: "created_at",
SortAsc: false,
Limit: 100,
}
Graph Traversal
Relationship queries (graph backend):
// Get all related objects (depth 2)
related, _ := storage.GetRelated(ctx, secCtx, id, "", 2)
// Find path between objects
path, _ := storage.GetPath(ctx, secCtx, fromID, toID)
// Get immediate neighbors
neighbors, _ := storage.GetNeighbors(ctx, secCtx, id, "both")
File Organization
The package is organized into focused modules:
Core Infrastructure
object_storage_interface.go- Interface definitionsstorage_factory.go- Backend selectionstorage_common.go- Shared utilitiesstorage_config.go- Configuration
File Backend
object_storage_file*.go- File storage implementationhash_registry*.go- Hash trackingcontent_addressable_storage*.go- CAS implementationbucketing_strategy*.go- Bucketing strategies
Graph Backend
object_storage_graph*.go- Graph storage implementation
Caches and invalidation
- List cache (
list_cache.go): Process-wide cache forList()results (keyed by project root + filter + limit). It must be invalidated whenever the object ID cache is updated or invalidated, or whenever storage is modified (create/update/delete), so list results stay consistent with the ID cache and on-disk state. UseInvalidateListCache()for a full clear orInvalidateListCacheForKind(kind)when only one kind changed. Object ID cache invalidation (e.g. incmd/zqk/system/check_cache.goandroot.goafter create/update/delete) typically triggers list cache invalidation.
Supporting Systems
operation_executor*.go- Async operation processingwaitgroup_manager.go- WaitGroup lifecycle managementvalue_comparison.go- Shared comparison utilitiesaudit_*.go- Audit event handlingmetrics_*.go- Metrics collection
Usage Example
// Create storage factory (auto-selects backend)
factory, _ := storage.NewStorageFactory(ctx, projectRoot)
storage := factory.GetStorage()
// Create an object
obj := map[string]any{
"id": "BLI-001",
"kind": "backlog_item",
"title": "Implement feature X",
"status": "planned",
}
err := storage.Create(ctx, secCtx, obj)
// List objects with filtering
filter := storage.ListFilter{
Kind: "backlog_item",
Filters: map[string]any{"status": "planned"},
SortBy: "created_at",
Limit: 10,
}
result, _ := storage.List(ctx, secCtx, storageCtx, filter)
Running tests
- Faster feedback:
go test -short ./pkg/storage/...skips heavy tests:TestAllKindsCRUD,TestComprehensiveFilteringSortingGrouping, comprehensive CAS tests, deadlock/concurrent/integration tests. Use for quick iteration. - Full suite:
go test ./pkg/storage/...runs all tests; expect several minutes. Increase per-package parallelism with-parallel N(e.g.-parallel 8) if you have CPU headroom. - Single test:
go test -run TestFoo ./pkg/storage/...to run one test or pattern. - Quick + parallel:
go test -short -parallel 8 ./pkg/storage/...for faster short runs.
Related Documentation
- Storage Architecture - System architecture
- Graph Package - Graph database provider
Notes
- Both backends implement the same interface for consistency
- File backend has more complex validation (blocking issues, partial data detection)
- Graph backend has simpler validation (appropriate for graph database)
- Shared utilities ensure consistent permission and metadata handling
- All operations are permission-aware and context-aware