The current design splits storage operations between Storage and IndexManager without clear boundaries:
Storage handles entity metadata, properties, children, AND configsIndexManager handles type indices, category members, catalogue groups, AND n-grams// 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.
The Storage interface has only one implementation (BasicStorage). It's unclear what alternative implementations would look like or why an interface is needed.
Storage.add_child() / get_children() - Structural child namesIndexManager.add_to_category() / get_category_members() - Indexed member documentsBoth classes define key prefixes independently, making it hard to see the overall storage schema.
graph TB
subgraph Low-Level Prefix Stores
EMS[EntityMetadataStorage<br/>entity: prefix]
PS[PropertiesStorage<br/>props: prefix]
CS[ChildrenStorage<br/>children: prefix]
CCS[CategoryConfigStorage<br/>config: prefix]
CLCS[CatalogueConfigStorage<br/>catcfg: prefix]
TIS[TypeIndexStorage<br/>typeidx: prefix]
CATIS[CategoryIndexStorage<br/>cat: prefix]
CLIS[CatalogueIndexStorage<br/>catl: prefix]
TXIS[TextIndexStorage<br/>idx: prefix]
end
subgraph High-Level Entity Facades
ES[EntityStore<br/>metadata + type index]
DS[DocumentStore<br/>properties]
CNS[ContainerStore<br/>children]
CAS[CategoryStore<br/>config + index + children]
CLS[CatalogueStore<br/>config + index]
IDS[IndexStore<br/>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
Each prefix store handles exactly one key prefix and provides type-safe operations.
Naming Convention: Prefix stores use the Storage suffix (e.g., EntityMetadataStorage).
Prefix: entity:
Key Format: entity:<path>
Value: Serialized (EntityType type, string? type_label)
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;
}
Prefix: props:
Key Format: props:<path>
Value: Serialized Properties dictionary
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;
}
Prefix: children:
Key Format: children:<parent_path>
Value: Serialized array of child names
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<string> get_children(EntityPath parent) throws StorageError;
public void delete(EntityPath parent) throws StorageError;
}
Prefix: config:
Key Format: config:<path>
Value: Serialized (string type_label, string expression)
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; }
}
Prefix: catcfg:
Key Format: catcfg:<path>
Value: Serialized (string type_label, string expression)
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; }
}
Prefix: typeidx:
Key Format: typeidx:<type_label>
Value: Serialized array of document paths
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<string> get_documents(string type_label);
}
Prefix: cat:
Key Format: cat:<category_path>:members
Value: Serialized array of document paths
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<string> doc_paths) throws StorageError;
public void remove_members(string category_path, Enumerable<string> doc_paths) throws StorageError;
public void set_members(string category_path, Enumerable<string> doc_paths) throws StorageError;
// Query and lifecycle
public Enumerable<string> get_members(string category_path);
public void clear(string category_path) throws StorageError;
}
Prefix: catl:
Key Formats:
catl:<catalogue_path>:keys - List of group keyscatl:<catalogue_path>:group:<key> - Document paths in group
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<string> 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<string> get_keys(string catalogue_path);
// Clear all
public void clear(string catalogue_path) throws StorageError;
}
Prefix: idx:
Key Formats:
idx:<index_path>:tri:<trigram> - Document paths containing trigramidx:<index_path>:bi:<bigram> - Trigrams containing bigramidx:<index_path>:uni:<unigram> - Bigrams starting with unigramidx:<index_path>:doc:<doc_path> - Cached document content
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<string> 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<string> 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<string> 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;
}
Facades compose prefix stores to provide entity-specific APIs.
Naming Convention: Entity facades use the Store suffix (e.g., EntityStore).
Composition: EntityMetadataStorage + TypeIndexStorage
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<string> get_documents_by_type(string type_label);
}
Composition: PropertiesStorage
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;
}
Composition: ChildrenStorage
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<string> get_children(EntityPath parent) throws StorageError;
}
Composition: CategoryConfigStorage + CategoryIndexStorage + ChildrenStorage
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<string> doc_paths) throws StorageError;
public void remove_members(EntityPath category_path, Enumerable<string> doc_paths) throws StorageError;
public void set_members(EntityPath category_path, Enumerable<string> doc_paths) throws StorageError;
public Enumerable<string> 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<string> get_children(EntityPath parent) throws StorageError;
// Lifecycle
public void delete(EntityPath path) throws StorageError;
}
Composition: CatalogueConfigStorage + CatalogueIndexStorage
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<string> get_group_members(EntityPath catalogue_path, string key);
public Enumerable<string> get_group_keys(EntityPath catalogue_path);
// Lifecycle
public void delete(EntityPath path) throws StorageError;
}
Composition: CategoryConfigStorage + TextIndexStorage
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<string> 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<string> 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<string> 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;
}
The EmbeddedEngine holds references to all entity facades:
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; } }
}
| 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 |
Since this is a greenfields project, we can implement directly without backward compatibility concerns.
EntityMetadataStorage in src/Storage/EntityMetadataStorage.valaPropertiesStorage in src/Storage/PropertiesStorage.valaChildrenStorage in src/Storage/ChildrenStorage.valaCategoryConfigStorage in src/Storage/CategoryConfigStorage.valaCatalogueConfigStorage in src/Storage/CatalogueConfigStorage.valaTypeIndexStorage in src/Storage/TypeIndexStorage.valaCategoryIndexStorage in src/Storage/CategoryIndexStorage.valaCatalogueIndexStorage in src/Storage/CatalogueIndexStorage.valaTextIndexStorage in src/Storage/TextIndexStorage.valaEntityStore in src/Storage/EntityStore.valaDocumentStore in src/Storage/DocumentStore.valaContainerStore in src/Storage/ContainerStore.valaCategoryStore in src/Storage/CategoryStore.valaCatalogueStore in src/Storage/CatalogueStore.valaIndexStore in src/Storage/IndexStore.valawith_write_transaction() helper methodStorage and IndexManager referencesContainer.vala to use ContainerStore and EntityStoreDocument.vala to use DocumentStore and EntityStoreCategory.vala to use CategoryStore and EntityStoreCatalogue.vala to use CatalogueStore and EntityStoreIndex.vala to use IndexStore and EntityStoresrc/Storage/Storage.valasrc/Storage/IndexManager.valatests/Storage/StorageTest.vala to test new storage classesengine.category_store.add_member() is self-documentingThe new architecture uses a transaction-per-write-request model:
| 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 |
Write operations on entities wrap their work in a transaction:
// 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:
public class EmbeddedEngine : Object, Core.Engine {
private Dbm _dbm;
public void with_write_transaction(WriteTransactionDelegate delegate) throws Error {
_dbm.with_transaction(() => delegate());
}
}
The server wraps each write request in a transaction:
// 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);
});
}
// 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");
// Single transaction covers:
// 1. Update properties
// 2. Run after_update hooks (e.g., update category memberships, update indices)
doc.set_property("status", "active");
// 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();
Caching: Deferred - no caching layer needed in the initial implementation. Can be added later if performance profiling indicates it's needed.
Batch Operations: Added add_members() and remove_members() batch methods to CategoryIndexStorage and CategoryStore for efficient bulk updates during reindexing operations.
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.