/** * High-level storage interface and implementation for Implexus. * * Provides entity persistence operations built on top of the Dbm layer. */ namespace Implexus.Storage { /** * Configuration for a Category/Index entity. */ public class CategoryConfig : Object { /** * The type label for entities in this category. */ public string type_label { get; set; } /** * The expression used to generate the index. */ public string expression { get; set; } /** * Creates a new CategoryConfig. */ public CategoryConfig(string type_label, string expression) { this.type_label = type_label; this.expression = expression; } } /** * Configuration for a Catalogue entity. */ public class CatalogueConfig : Object { /** * The type label for documents to catalogue. */ public string type_label { get; set; } /** * The expression used to extract the grouping key. */ public string expression { get; set; } /** * Creates a new CatalogueConfig. */ public CatalogueConfig(string type_label, string expression) { this.type_label = type_label; this.expression = expression; } } /** * Interface for high-level entity storage operations. */ public interface Storage : Object { /** * Stores entity metadata. * * @param path The entity path * @param type The entity type * @param type_label Optional type label for Categories */ public abstract void store_entity_metadata(Core.EntityPath path, Core.EntityType type, string? type_label = null) throws StorageError; /** * Gets the entity type for a path. * * @param path The entity path * @return The entity type, or null if not found */ public abstract Core.EntityType? get_entity_type(Core.EntityPath path) throws StorageError; /** * Gets the entity type label for a path. * * @param path The entity path * @return The type label, or null if not found */ public abstract string? get_entity_type_label(Core.EntityPath path) throws StorageError; /** * Checks if an entity exists at the given path. * * @param path The entity path * @return True if the entity exists */ public abstract bool entity_exists(Core.EntityPath path); /** * Deletes an entity and all its data. * * @param path The entity path */ public abstract void delete_entity(Core.EntityPath path) throws StorageError; /** * Stores document properties for an entity. * * @param path The entity path * @param properties The properties to store */ public abstract void store_properties(Core.EntityPath path, Invercargill.Properties properties) throws StorageError; /** * Loads document properties for an entity. * * @param path The entity path * @return The properties, or null if not found */ public abstract Invercargill.Properties? load_properties(Core.EntityPath path) throws StorageError; /** * Adds a child name to a category. * * @param parent The parent category path * @param child_name The name of the child */ public abstract void add_child(Core.EntityPath parent, string child_name) throws StorageError; /** * Removes a child name from a category. * * @param parent The parent category path * @param child_name The name of the child */ public abstract void remove_child(Core.EntityPath parent, string child_name) throws StorageError; /** * Gets the children of a category. * * @param parent The parent category path * @return Enumerable of child names */ public abstract Invercargill.Enumerable get_children(Core.EntityPath parent) throws StorageError; /** * Checks if a category has a specific child. * * @param parent The parent category path * @param child_name The name of the child * @return True if the child exists */ public abstract bool has_child(Core.EntityPath parent, string child_name) throws StorageError; /** * Stores category configuration. * * @param path The category path * @param type_label The type label for entities * @param expression The index expression */ public abstract void store_category_config(Core.EntityPath path, string type_label, string expression) throws StorageError; /** * Gets category configuration. * * @param path The category path * @return The configuration, or null if not found */ public abstract CategoryConfig? get_category_config(Core.EntityPath path) throws StorageError; /** * Stores catalogue configuration. * * @param path The catalogue path * @param type_label The type label for documents to catalogue * @param expression The expression to extract the grouping key */ public abstract void store_catalogue_config(Core.EntityPath path, string type_label, string expression) throws StorageError; /** * Gets catalogue configuration. * * @param path The catalogue path * @return The configuration, or null if not found */ public abstract CatalogueConfig? get_catalogue_config(Core.EntityPath path) throws StorageError; } /** * Basic implementation of Storage using Dbm. */ public class BasicStorage : Object, Storage { /** * The underlying Dbm backend. * * This is exposed to allow IndexManager to share the same storage. */ public Dbm dbm { get { return _dbm; } } private Dbm _dbm; // Key prefixes for different data types private const string ENTITY_PREFIX = "entity:"; private const string PROPS_PREFIX = "props:"; private const string CHILDREN_PREFIX = "children:"; private const string CONFIG_PREFIX = "config:"; /** * Creates a new BasicStorage with the given Dbm backend. * * @param dbm The Dbm backend to use */ public BasicStorage(Dbm dbm) { _dbm = dbm; } /** * Creates a new BasicStorage with a file-based Dbm. * * @param data_dir Directory to store data in */ public BasicStorage.with_directory(string data_dir) { _dbm = new FilesystemDbm(data_dir); } /** * {@inheritDoc} */ public void store_entity_metadata(Core.EntityPath path, Core.EntityType type, string? type_label = null) throws StorageError { string key = ENTITY_PREFIX + path.to_string(); var writer = new ElementWriter(); // Use write_element to include type code, so read_element can read it back writer.write_element(new Invercargill.NativeElement((int64) type)); writer.write_element(new Invercargill.NativeElement(type_label ?? "")); _dbm.set(key, writer.to_binary_data()); } /** * {@inheritDoc} */ public Core.EntityType? get_entity_type(Core.EntityPath path) throws StorageError { string key = ENTITY_PREFIX + path.to_string(); var data = _dbm.get(key); if (data == null) { return null; } var reader = new ElementReader((!) data); try { var element = reader.read_element(); if (element.is_null()) { return null; } int64? type_val = element.as(); return (Core.EntityType) (type_val == null ? 0 : (!) type_val); } catch (Invercargill.ElementError e) { throw new StorageError.CORRUPT_DATA("Failed to read entity type: %s".printf(e.message)); } } /** * {@inheritDoc} */ public string? get_entity_type_label(Core.EntityPath path) throws StorageError { string key = ENTITY_PREFIX + path.to_string(); var data = _dbm.get(key); if (data == null) { return null; } var reader = new ElementReader((!) data); try { reader.read_element(); // Skip type var label_element = reader.read_element(); if (label_element.is_null()) { return null; } string label = label_element.as(); return label == "" ? null : label; } catch (Invercargill.ElementError e) { throw new StorageError.CORRUPT_DATA("Failed to read entity type label: %s".printf(e.message)); } } /** * {@inheritDoc} */ public bool entity_exists(Core.EntityPath path) { string key = ENTITY_PREFIX + path.to_string(); return _dbm.has_key(key); } /** * {@inheritDoc} */ public void delete_entity(Core.EntityPath path) throws StorageError { string path_str = path.to_string(); // Delete entity metadata _dbm.delete(ENTITY_PREFIX + path_str); // Delete properties _dbm.delete(PROPS_PREFIX + path_str); // Delete children (for categories) _dbm.delete(CHILDREN_PREFIX + path_str); // Delete category config (for categories) _dbm.delete(CONFIG_PREFIX + path_str); } /** * {@inheritDoc} */ public void store_properties(Core.EntityPath path, Invercargill.Properties properties) throws StorageError { string key = PROPS_PREFIX + path.to_string(); var writer = new ElementWriter(); // Use write_element to include type code, so read_element can read it back writer.write_element(new Invercargill.NativeElement(properties)); _dbm.set(key, writer.to_binary_data()); } /** * {@inheritDoc} */ public Invercargill.Properties? load_properties(Core.EntityPath path) throws StorageError { string key = PROPS_PREFIX + path.to_string(); var data = _dbm.get(key); if (data == null) { return null; } var reader = new ElementReader((!) data); try { var element = reader.read_element(); if (element.is_null()) { return null; } return element.as(); } catch (Invercargill.ElementError e) { throw new StorageError.CORRUPT_DATA("Failed to read properties: %s".printf(e.message)); } } /** * {@inheritDoc} */ public void add_child(Core.EntityPath parent, string child_name) throws StorageError { var children = load_children_set(parent); try { children.set(child_name, true); } catch (Invercargill.IndexError e) { throw new StorageError.IO_ERROR("Failed to add child: %s".printf(e.message)); } save_children_set(parent, children); } /** * {@inheritDoc} */ public void remove_child(Core.EntityPath parent, string child_name) throws StorageError { var children = load_children_set(parent); try { children.remove(child_name); } catch (Invercargill.IndexError e) { // Child not in set, that's fine } save_children_set(parent, children); } /** * {@inheritDoc} */ public Invercargill.Enumerable get_children(Core.EntityPath parent) throws StorageError { var children = load_children_set(parent); return children.keys; } /** * {@inheritDoc} */ public bool has_child(Core.EntityPath parent, string child_name) throws StorageError { var children = load_children_set(parent); return children.has(child_name); } /** * Loads the children set for a category. */ private Invercargill.DataStructures.Dictionary load_children_set(Core.EntityPath parent) throws StorageError { string key = CHILDREN_PREFIX + parent.to_string(); var data = _dbm.get(key); var result = new Invercargill.DataStructures.Dictionary(); if (data == null) { return result; } var reader = new ElementReader((!) data); try { var element = reader.read_element(); if (element.is_null()) { return result; } // The children set is stored as an array of strings var array = element.as>(); foreach (var child_element in array) { if (!child_element.is_null()) { string child_name = child_element.as(); try { result.set(child_name, true); } catch (Invercargill.IndexError e) { // Skip } } } } catch (Invercargill.ElementError e) { throw new StorageError.CORRUPT_DATA("Failed to read children: %s".printf(e.message)); } return result; } /** * Saves the children set for a category. */ private void save_children_set(Core.EntityPath parent, Invercargill.DataStructures.Dictionary children) throws StorageError { string key = CHILDREN_PREFIX + parent.to_string(); uint count = children.count(); if (count == 0) { _dbm.delete(key); return; } // Store as array of strings var array = new Invercargill.DataStructures.Vector(); foreach (var child_name in children.keys) { var element = new Invercargill.NativeElement(child_name); array.add(element); } var writer = new ElementWriter(); // Use write_element to include type code, so read_element can read it back writer.write_element(new Invercargill.NativeElement>(array)); _dbm.set(key, writer.to_binary_data()); } /** * {@inheritDoc} */ public void store_category_config(Core.EntityPath path, string type_label, string expression) throws StorageError { string key = CONFIG_PREFIX + path.to_string(); var writer = new ElementWriter(); // Use write_element to include type code, so read_element can read it back writer.write_element(new Invercargill.NativeElement(type_label)); writer.write_element(new Invercargill.NativeElement(expression)); _dbm.set(key, writer.to_binary_data()); } /** * {@inheritDoc} */ public CategoryConfig? get_category_config(Core.EntityPath path) throws StorageError { string key = CONFIG_PREFIX + path.to_string(); var data = _dbm.get(key); if (data == null) { return null; } var reader = new ElementReader((!) data); try { var label_element = reader.read_element(); var expr_element = reader.read_element(); if (label_element.is_null() || expr_element.is_null()) { return null; } string type_label = label_element.as(); string expression = expr_element.as(); return new CategoryConfig(type_label, expression); } catch (Invercargill.ElementError e) { throw new StorageError.CORRUPT_DATA("Failed to read category config: %s".printf(e.message)); } } // Key prefix for catalogue configuration private const string CAT_CONFIG_PREFIX = "catcfg:"; /** * {@inheritDoc} */ public void store_catalogue_config(Core.EntityPath path, string type_label, string expression) throws StorageError { string key = CAT_CONFIG_PREFIX + path.to_string(); var writer = new ElementWriter(); writer.write_element(new Invercargill.NativeElement(type_label)); writer.write_element(new Invercargill.NativeElement(expression)); _dbm.set(key, writer.to_binary_data()); } /** * {@inheritDoc} */ public CatalogueConfig? get_catalogue_config(Core.EntityPath path) throws StorageError { string key = CAT_CONFIG_PREFIX + path.to_string(); var data = _dbm.get(key); if (data == null) { return null; } var reader = new ElementReader((!) data); try { var label_element = reader.read_element(); var expr_element = reader.read_element(); if (label_element.is_null() || expr_element.is_null()) { return null; } string type_label = label_element.as(); string expression = expr_element.as(); return new CatalogueConfig(type_label, expression); } catch (Invercargill.ElementError e) { throw new StorageError.CORRUPT_DATA("Failed to read catalogue config: %s".printf(e.message)); } } } }