# Storage Layer Redesign
## Document Status
- **Created**: 2026-03-13
- **Status**: Draft
- **Author**: Collaborative design session
## Problems with Current Architecture
### 1. Confusing Separation of Concerns
The current design splits storage operations between `Storage` and `IndexManager` without clear boundaries:
- `Storage` handles entity metadata, properties, children, AND configs
- `IndexManager` handles type indices, category members, catalogue groups, AND n-grams
- It's unclear which class should handle what
### 2. Awkward Coupling in EmbeddedEngine
```vala
// EmbeddedEngine must cast to BasicStorage to get Dbm for IndexManager
var basic_storage = (_storage as Storage.BasicStorage);
if (basic_storage != null) {
_index_manager = new Storage.IndexManager(basic_storage.dbm);
}
```
This is a code smell indicating the abstraction is broken.
### 3. Unclear Value of Storage Interface
The `Storage` interface has only one implementation (`BasicStorage`). It's unclear what alternative implementations would look like or why an interface is needed.
### 4. Naming Confusion
- `Storage.add_child()` / `get_children()` - Structural child names
- `IndexManager.add_to_category()` / `get_category_members()` - Indexed member documents
- Both deal with "children" but mean different things
### 5. Key Prefix Sprawl
Both classes define key prefixes independently, making it hard to see the overall storage schema.
## Proposed Architecture
### Design Principles
1. **One class per prefix** - Each key prefix gets its own focused class
2. **Entity facades compose prefix stores** - High-level APIs for each entity type
3. **Engine holds facade references** - Clean dependency graph
4. **All stores are concrete classes** - No unnecessary interfaces
### Architecture Overview
```mermaid
graph TB
subgraph Low-Level Prefix Stores
EMS[EntityMetadataStorage
entity: prefix]
PS[PropertiesStorage
props: prefix]
CS[ChildrenStorage
children: prefix]
CCS[CategoryConfigStorage
config: prefix]
CLCS[CatalogueConfigStorage
catcfg: prefix]
TIS[TypeIndexStorage
typeidx: prefix]
CATIS[CategoryIndexStorage
cat: prefix]
CLIS[CatalogueIndexStorage
catl: prefix]
TXIS[TextIndexStorage
idx: prefix]
end
subgraph High-Level Entity Facades
ES[EntityStore
metadata + type index]
DS[DocumentStore
properties]
CNS[ContainerStore
children]
CAS[CategoryStore
config + index + children]
CLS[CatalogueStore
config + index]
IDS[IndexStore
config + text index]
end
subgraph Engine
E[EmbeddedEngine]
end
EMS --> ES
TIS --> ES
PS --> DS
CS --> CNS
CCS --> CAS
CATIS --> CAS
CS --> CAS
CLCS --> CLS
CLIS --> CLS
CCS --> IDS
TXIS --> IDS
E --> ES
E --> DS
E --> CNS
E --> CAS
E --> CLS
E --> IDS
```
## Low-Level Prefix Stores
Each prefix store handles exactly one key prefix and provides type-safe operations.
**Naming Convention:** Prefix stores use the `Storage` suffix (e.g., `EntityMetadataStorage`).
### EntityMetadataStorage
**Prefix:** `entity:`
**Key Format:** `entity:`
**Value:** Serialized `(EntityType type, string? type_label)`
```vala
public class EntityMetadataStorage : Object {
public EntityMetadataStorage(Dbm dbm);
public void store_metadata(EntityPath path, EntityType type, string? type_label) throws StorageError;
public EntityType? get_type(EntityPath path) throws StorageError;
public string? get_type_label(EntityPath path) throws StorageError;
public bool exists(EntityPath path);
public void delete(EntityPath path) throws StorageError;
}
```
### PropertiesStorage
**Prefix:** `props:`
**Key Format:** `props:`
**Value:** Serialized `Properties` dictionary
```vala
public class PropertiesStorage : Object {
public PropertiesStorage(Dbm dbm);
public void store(EntityPath path, Properties properties) throws StorageError;
public Properties? load(EntityPath path) throws StorageError;
public void delete(EntityPath path) throws StorageError;
}
```
### ChildrenStorage
**Prefix:** `children:`
**Key Format:** `children:`
**Value:** Serialized array of child names
```vala
public class ChildrenStorage : Object {
public ChildrenStorage(Dbm dbm);
public void add_child(EntityPath parent, string child_name) throws StorageError;
public void remove_child(EntityPath parent, string child_name) throws StorageError;
public bool has_child(EntityPath parent, string child_name) throws StorageError;
public Enumerable get_children(EntityPath parent) throws StorageError;
public void delete(EntityPath parent) throws StorageError;
}
```
### CategoryConfigStorage
**Prefix:** `config:`
**Key Format:** `config:`
**Value:** Serialized `(string type_label, string expression)`
```vala
public class CategoryConfigStorage : Object {
public CategoryConfigStorage(Dbm dbm);
public void store(EntityPath path, string type_label, string expression) throws StorageError;
public CategoryConfig? load(EntityPath path) throws StorageError;
public void delete(EntityPath path) throws StorageError;
}
public class CategoryConfig : Object {
public string type_label { get; construct set; }
public string expression { get; construct set; }
}
```
### CatalogueConfigStorage
**Prefix:** `catcfg:`
**Key Format:** `catcfg:`
**Value:** Serialized `(string type_label, string expression)`
```vala
public class CatalogueConfigStorage : Object {
public CatalogueConfigStorage(Dbm dbm);
public void store(EntityPath path, string type_label, string expression) throws StorageError;
public CatalogueConfig? load(EntityPath path) throws StorageError;
public void delete(EntityPath path) throws StorageError;
}
public class CatalogueConfig : Object {
public string type_label { get; construct set; }
public string expression { get; construct set; }
}
```
### TypeIndexStorage
**Prefix:** `typeidx:`
**Key Format:** `typeidx:`
**Value:** Serialized array of document paths
```vala
public class TypeIndexStorage : Object {
public TypeIndexStorage(Dbm dbm);
public void add_document(string type_label, string doc_path) throws StorageError;
public void remove_document(string type_label, string doc_path) throws StorageError;
public Enumerable get_documents(string type_label);
}
```
### CategoryIndexStorage
**Prefix:** `cat:`
**Key Format:** `cat::members`
**Value:** Serialized array of document paths
```vala
public class CategoryIndexStorage : Object {
public CategoryIndexStorage(Dbm dbm);
// Single member operations
public void add_member(string category_path, string doc_path) throws StorageError;
public void remove_member(string category_path, string doc_path) throws StorageError;
// Batch member operations
public void add_members(string category_path, Enumerable doc_paths) throws StorageError;
public void remove_members(string category_path, Enumerable doc_paths) throws StorageError;
public void set_members(string category_path, Enumerable doc_paths) throws StorageError;
// Query and lifecycle
public Enumerable get_members(string category_path);
public void clear(string category_path) throws StorageError;
}
```
### CatalogueIndexStorage
**Prefix:** `catl:`
**Key Formats:**
- `catl::keys` - List of group keys
- `catl::group:` - Document paths in group
```vala
public class CatalogueIndexStorage : Object {
public CatalogueIndexStorage(Dbm dbm);
// Group operations
public void add_to_group(string catalogue_path, string key, string doc_path) throws StorageError;
public void remove_from_group(string catalogue_path, string key, string doc_path) throws StorageError;
public Enumerable get_group_members(string catalogue_path, string key);
// Key operations
public void add_key(string catalogue_path, string key) throws StorageError;
public void remove_key(string catalogue_path, string key) throws StorageError;
public Enumerable get_keys(string catalogue_path);
// Clear all
public void clear(string catalogue_path) throws StorageError;
}
```
### TextIndexStorage
**Prefix:** `idx:`
**Key Formats:**
- `idx::tri:` - Document paths containing trigram
- `idx::bi:` - Trigrams containing bigram
- `idx::uni:` - Bigrams starting with unigram
- `idx::doc:` - Cached document content
```vala
public class TextIndexStorage : Object {
public TextIndexStorage(Dbm dbm);
// Trigram index
public void add_trigram(string index_path, string trigram, string doc_path) throws StorageError;
public void remove_trigram(string index_path, string trigram, string doc_path) throws StorageError;
public Enumerable get_documents_for_trigram(string index_path, string trigram);
// Bigram reverse index
public void add_bigram_mapping(string index_path, string bigram, string trigram) throws StorageError;
public Enumerable get_trigrams_for_bigram(string index_path, string bigram);
// Unigram reverse index
public void add_unigram_mapping(string index_path, string unigram, string bigram) throws StorageError;
public Enumerable get_bigrams_for_unigram(string index_path, string unigram);
// Document content cache
public void store_document_content(string index_path, string doc_path, string content) throws StorageError;
public string? get_document_content(string index_path, string doc_path);
public void remove_document_content(string index_path, string doc_path) throws StorageError;
// Clear all
public void clear(string index_path) throws StorageError;
}
```
## High-Level Entity Facades
Facades compose prefix stores to provide entity-specific APIs.
**Naming Convention:** Entity facades use the `Store` suffix (e.g., `EntityStore`).
### EntityStore
**Composition:** `EntityMetadataStorage` + `TypeIndexStorage`
```vala
public class EntityStore : Object {
public EntityStore(Dbm dbm);
// Metadata operations
public void store_metadata(EntityPath path, EntityType type, string? type_label) throws StorageError;
public EntityType? get_type(EntityPath path) throws StorageError;
public string? get_type_label(EntityPath path) throws StorageError;
public bool exists(EntityPath path);
public void delete(EntityPath path) throws StorageError;
// Type index operations
public void register_document_type(string type_label, string doc_path) throws StorageError;
public void unregister_document_type(string type_label, string doc_path) throws StorageError;
public Enumerable get_documents_by_type(string type_label);
}
```
### DocumentStore
**Composition:** `PropertiesStorage`
```vala
public class DocumentStore : Object {
public DocumentStore(Dbm dbm);
public void store_properties(EntityPath path, Properties properties) throws StorageError;
public Properties? load_properties(EntityPath path) throws StorageError;
public void delete(EntityPath path) throws StorageError;
}
```
### ContainerStore
**Composition:** `ChildrenStorage`
```vala
public class ContainerStore : Object {
public ContainerStore(Dbm dbm);
public void add_child(EntityPath parent, string child_name) throws StorageError;
public void remove_child(EntityPath parent, string child_name) throws StorageError;
public bool has_child(EntityPath parent, string child_name) throws StorageError;
public Enumerable get_children(EntityPath parent) throws StorageError;
}
```
### CategoryStore
**Composition:** `CategoryConfigStorage` + `CategoryIndexStorage` + `ChildrenStorage`
```vala
public class CategoryStore : Object {
public CategoryStore(Dbm dbm);
// Configuration
public void store_config(EntityPath path, string type_label, string expression) throws StorageError;
public CategoryConfig? load_config(EntityPath path) throws StorageError;
// Single member operations
public void add_member(EntityPath category_path, string doc_path) throws StorageError;
public void remove_member(EntityPath category_path, string doc_path) throws StorageError;
// Batch member operations
public void add_members(EntityPath category_path, Enumerable doc_paths) throws StorageError;
public void remove_members(EntityPath category_path, Enumerable doc_paths) throws StorageError;
public void set_members(EntityPath category_path, Enumerable doc_paths) throws StorageError;
public Enumerable get_members(EntityPath category_path);
// Structural children (for when entities are created inside category)
public void add_child(EntityPath parent, string child_name) throws StorageError;
public void remove_child(EntityPath parent, string child_name) throws StorageError;
public Enumerable get_children(EntityPath parent) throws StorageError;
// Lifecycle
public void delete(EntityPath path) throws StorageError;
}
```
### CatalogueStore
**Composition:** `CatalogueConfigStorage` + `CatalogueIndexStorage`
```vala
public class CatalogueStore : Object {
public CatalogueStore(Dbm dbm);
// Configuration
public void store_config(EntityPath path, string type_label, string expression) throws StorageError;
public CatalogueConfig? load_config(EntityPath path) throws StorageError;
// Group operations
public void add_to_group(EntityPath catalogue_path, string key, string doc_path) throws StorageError;
public void remove_from_group(EntityPath catalogue_path, string key, string doc_path) throws StorageError;
public Enumerable get_group_members(EntityPath catalogue_path, string key);
public Enumerable get_group_keys(EntityPath catalogue_path);
// Lifecycle
public void delete(EntityPath path) throws StorageError;
}
```
### IndexStore
**Composition:** `CategoryConfigStorage` + `TextIndexStorage`
```vala
public class IndexStore : Object {
public IndexStore(Dbm dbm);
// Configuration (reuses CategoryConfigStorage with config: prefix)
public void store_config(EntityPath path, string type_label, string expression) throws StorageError;
public CategoryConfig? load_config(EntityPath path) throws StorageError;
// Trigram index
public void add_trigram(EntityPath index_path, string trigram, string doc_path) throws StorageError;
public void remove_trigram(EntityPath index_path, string trigram, string doc_path) throws StorageError;
public Enumerable get_documents_for_trigram(EntityPath index_path, string trigram);
// Reverse indices
public void add_bigram_mapping(EntityPath index_path, string bigram, string trigram) throws StorageError;
public Enumerable get_trigrams_for_bigram(EntityPath index_path, string bigram);
public void add_unigram_mapping(EntityPath index_path, string unigram, string bigram) throws StorageError;
public Enumerable get_bigrams_for_unigram(EntityPath index_path, string unigram);
// Content cache
public void store_document_content(EntityPath index_path, string doc_path, string content) throws StorageError;
public string? get_document_content(EntityPath index_path, string doc_path);
public void remove_document_content(EntityPath index_path, string doc_path) throws StorageError;
// Lifecycle
public void delete(EntityPath path) throws StorageError;
}
```
## Engine Integration
The `EmbeddedEngine` holds references to all entity facades:
```vala
public class EmbeddedEngine : Object, Core.Engine {
private EntityStore _entity_store;
private DocumentStore _document_store;
private ContainerStore _container_store;
private CategoryStore _category_store;
private CatalogueStore _catalogue_store;
private IndexStore _index_store;
public EmbeddedEngine.with_path(string storage_path) {
var dbm = new FilesystemDbm(storage_path);
_entity_store = new EntityStore(dbm);
_document_store = new DocumentStore(dbm);
_container_store = new ContainerStore(dbm);
_category_store = new CategoryStore(dbm);
_catalogue_store = new CatalogueStore(dbm);
_index_store = new IndexStore(dbm);
// ... rest of initialization
}
// Public access for entity classes
public EntityStore entity_store { get { return _entity_store; } }
public DocumentStore document_store { get { return _document_store; } }
public ContainerStore container_store { get { return _container_store; } }
public CategoryStore category_store { get { return _category_store; } }
public CatalogueStore catalogue_store { get { return _catalogue_store; } }
public IndexStore index_store { get { return _index_store; } }
}
```
## Key Schema Summary
| Prefix | Storage Class | Description |
|--------|---------------|-------------|
| `entity:` | EntityMetadataStorage | Entity type and type_label |
| `props:` | PropertiesStorage | Document properties |
| `children:` | ChildrenStorage | Structural child names |
| `config:` | CategoryConfigStorage | Category/Index configuration |
| `catcfg:` | CatalogueConfigStorage | Catalogue configuration |
| `typeidx:` | TypeIndexStorage | Global type → documents index |
| `cat:` | CategoryIndexStorage | Category members index |
| `catl:` | CatalogueIndexStorage | Catalogue groups and keys |
| `idx:` | TextIndexStorage | N-gram indices and content cache |
## Implementation Strategy
Since this is a greenfields project, we can implement directly without backward compatibility concerns.
### Phase 1: Create Prefix Storage Classes
1. Create `EntityMetadataStorage` in `src/Storage/EntityMetadataStorage.vala`
2. Create `PropertiesStorage` in `src/Storage/PropertiesStorage.vala`
3. Create `ChildrenStorage` in `src/Storage/ChildrenStorage.vala`
4. Create `CategoryConfigStorage` in `src/Storage/CategoryConfigStorage.vala`
5. Create `CatalogueConfigStorage` in `src/Storage/CatalogueConfigStorage.vala`
6. Create `TypeIndexStorage` in `src/Storage/TypeIndexStorage.vala`
7. Create `CategoryIndexStorage` in `src/Storage/CategoryIndexStorage.vala`
8. Create `CatalogueIndexStorage` in `src/Storage/CatalogueIndexStorage.vala`
9. Create `TextIndexStorage` in `src/Storage/TextIndexStorage.vala`
### Phase 2: Create Entity Facade Classes
1. Create `EntityStore` in `src/Storage/EntityStore.vala`
2. Create `DocumentStore` in `src/Storage/DocumentStore.vala`
3. Create `ContainerStore` in `src/Storage/ContainerStore.vala`
4. Create `CategoryStore` in `src/Storage/CategoryStore.vala`
5. Create `CatalogueStore` in `src/Storage/CatalogueStore.vala`
6. Create `IndexStore` in `src/Storage/IndexStore.vala`
### Phase 3: Update EmbeddedEngine
1. Add `with_write_transaction()` helper method
2. Add facade references for all stores
3. Remove old `Storage` and `IndexManager` references
### Phase 4: Update Entity Classes
1. Update `Container.vala` to use `ContainerStore` and `EntityStore`
2. Update `Document.vala` to use `DocumentStore` and `EntityStore`
3. Update `Category.vala` to use `CategoryStore` and `EntityStore`
4. Update `Catalogue.vala` to use `CatalogueStore` and `EntityStore`
5. Update `Index.vala` to use `IndexStore` and `EntityStore`
### Phase 5: Remove Old Classes
1. Delete `src/Storage/Storage.vala`
2. Delete `src/Storage/IndexManager.vala`
### Phase 6: Update Tests
1. Update `tests/Storage/StorageTest.vala` to test new storage classes
2. Add tests for each prefix storage class
3. Add tests for each entity facade class
## Benefits of New Design
1. **Clear Separation**: Each prefix store has one responsibility
2. **Composable**: Facades compose stores as needed
3. **Testable**: Small, focused classes are easier to test
4. **Discoverable**: `engine.category_store.add_member()` is self-documenting
5. **Flexible**: Can use low-level stores directly if needed
6. **No Broken Abstractions**: No casting to concrete types
7. **Clear Key Schema**: All prefixes documented in one place
## Transaction Model
The new architecture uses a **transaction-per-write-request** model:
### Behavior
| Operation Type | Transaction Behavior |
|----------------|---------------------|
| **Write operations** | Automatically wrapped in a transaction |
| **Read operations** | No transaction (no overhead) |
| **Hooks** | Run within the same transaction as the triggering write |
### Benefits
1. **Atomicity where needed**: Create document + add to container = one transaction
2. **Hooks are atomic**: Update document + category membership updates = one transaction
3. **No explicit transaction management**: Caller doesn't need to think about transactions
4. **Consistent behavior**: Same model works for both EmbeddedEngine and RemoteEngine
5. **No read overhead**: Reads don't pay transaction cost
### Implementation
#### EmbeddedEngine
Write operations on entities wrap their work in a transaction:
```vala
// In Container.create_child()
public Document create_child(string name) throws EntityError {
Document? doc = null;
_engine.with_write_transaction(() => {
// Create entity metadata
_engine.entity_store.store_metadata(path.child(name), EntityType.DOCUMENT, type_label);
// Add to container's children
_engine.container_store.add_child(path, name);
// Create document entity
doc = new Document(_engine, path.child(name));
// Hooks run within same transaction
_engine.hooks.run_after_create(doc);
});
return doc;
}
```
The Engine provides a helper method:
```vala
public class EmbeddedEngine : Object, Core.Engine {
private Dbm _dbm;
public void with_write_transaction(WriteTransactionDelegate delegate) throws Error {
_dbm.with_transaction(() => delegate());
}
}
```
#### RemoteEngine (Server Side)
The server wraps each write request in a transaction:
```vala
// In ClientHandler
void handle_create_document(Message request) {
_engine.with_write_transaction(() => {
// Process the entire request in one transaction
var path = request.get_path();
var type_label = request.get_type_label();
_engine.entity_store.store_metadata(path, EntityType.DOCUMENT, type_label);
_engine.container_store.add_child(path.parent, path.name);
// Hooks run within same transaction
var doc = new Document(_engine, path);
_engine.hooks.run_after_create(doc);
});
}
```
### Examples
#### Creating a Document
```vala
// Single transaction covers:
// 1. Create entity metadata
// 2. Add to container's children list
// 3. Store initial properties
// 4. Run after_create hooks (e.g., add to categories)
var doc = container.create_child("my-doc");
```
#### Updating a Document
```vala
// Single transaction covers:
// 1. Update properties
// 2. Run after_update hooks (e.g., update category memberships, update indices)
doc.set_property("status", "active");
```
#### Deleting a Document
```vala
// Single transaction covers:
// 1. Remove from container's children list
// 2. Delete entity metadata
// 3. Delete properties
// 4. Run after_delete hooks (e.g., remove from categories, remove from indices)
doc.delete();
```
## Decisions Made
1. **Caching**: Deferred - no caching layer needed in the initial implementation. Can be added later if performance profiling indicates it's needed.
2. **Batch Operations**: Added `add_members()` and `remove_members()` batch methods to `CategoryIndexStorage` and `CategoryStore` for efficient bulk updates during reindexing operations.
3. **Transaction Model**: Transaction-per-write-request model selected. Write operations are automatically wrapped in transactions, reads have no transaction overhead, and hooks run within the same transaction as the triggering write.