using System.Collections.Concurrent; using System.Text; namespace ZB.MOM.WW.CBDD.Core.Storage; public sealed partial class StorageEngine { private readonly ConcurrentDictionary _dictionaryCache = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _dictionaryReverseCache = new(); private uint _dictionaryRootPageId; private ushort _nextDictionaryId; // Lock for dictionary modifications (simple lock for now, could be RW lock) private readonly object _dictionaryLock = new(); private void InitializeDictionary() { // 1. Read File Header (Page 0) to get Dictionary Root var headerBuffer = new byte[PageSize]; ReadPage(0, null, headerBuffer); var header = PageHeader.ReadFrom(headerBuffer); if (header.DictionaryRootPageId == 0) { // Initialize new Dictionary lock (_dictionaryLock) { // Double check ReadPage(0, null, headerBuffer); header = PageHeader.ReadFrom(headerBuffer); if (header.DictionaryRootPageId == 0) { _dictionaryRootPageId = AllocatePage(); // Init Dictionary Page var pageBuffer = new byte[PageSize]; DictionaryPage.Initialize(pageBuffer, _dictionaryRootPageId); WritePageImmediate(_dictionaryRootPageId, pageBuffer); // Update Header header.DictionaryRootPageId = _dictionaryRootPageId; header.WriteTo(headerBuffer); WritePageImmediate(0, headerBuffer); // Init Next ID _nextDictionaryId = DictionaryPage.ReservedValuesEnd + 1; } else { _dictionaryRootPageId = header.DictionaryRootPageId; } } } else { _dictionaryRootPageId = header.DictionaryRootPageId; // Warm cache ushort maxId = DictionaryPage.ReservedValuesEnd; foreach (var (key, val) in DictionaryPage.FindAllGlobal(this, _dictionaryRootPageId)) { var lowerKey = key.ToLowerInvariant(); _dictionaryCache[lowerKey] = val; _dictionaryReverseCache[val] = lowerKey; if (val > maxId) maxId = val; } _nextDictionaryId = (ushort)(maxId + 1); } // Pre-register internal keys used for Schema persistence RegisterKeys(new[] { "_id", "t", "_v", "f", "n", "b", "s", "a" }); // Pre-register common array indices to avoid mapping during high-frequency writes var indices = new List(101); for (int i = 0; i <= 100; i++) indices.Add(i.ToString()); RegisterKeys(indices); } /// /// Gets the key-to-id dictionary cache. /// /// The key-to-id map. public ConcurrentDictionary GetKeyMap() => _dictionaryCache; /// /// Gets the id-to-key dictionary cache. /// /// The id-to-key map. public ConcurrentDictionary GetKeyReverseMap() => _dictionaryReverseCache; /// /// Gets the ID for a dictionary key, creating it if it doesn't exist. /// Thread-safe. /// /// The dictionary key. /// The dictionary identifier for the key. public ushort GetOrAddDictionaryEntry(string key) { key = key.ToLowerInvariant(); if (_dictionaryCache.TryGetValue(key, out var id)) { return id; } lock (_dictionaryLock) { // Double checked locking if (_dictionaryCache.TryGetValue(key, out id)) { return id; } // Try to find in storage (in case cache is incomplete or another process?) // Note: FindAllGlobal loaded everything, so strict cache miss means it's not in DB. // BUT if we support concurrent writers (multiple processed), we should re-check DB. // Current CBDD seems to be single-process exclusive lock (FileShare.None). // So in-memory cache is authoritative after load. // Generate New ID ushort nextId = _nextDictionaryId; if (nextId == 0) nextId = DictionaryPage.ReservedValuesEnd + 1; // Should be init, but safety // Insert into Page // usage of default(ulong) or null transaction? // Dictionary updates should ideally be transactional or immediate? // "Immediate" for now to simplify, as dictionary is cross-collection. // If we use transaction, we need to pass it in. For now, immediate write. // We need to support "Insert Global" which handles overflow. // DictionaryPage.Insert only handles single page. // We need logic here to traverse chain and find space. if (InsertDictionaryEntryGlobal(key, nextId)) { _dictionaryCache[key] = nextId; _dictionaryReverseCache[nextId] = key; _nextDictionaryId++; return nextId; } else { throw new InvalidOperationException("Failed to insert dictionary entry (Storage Full?)"); } } } /// /// Gets the dictionary key for an identifier. /// /// The dictionary identifier. /// The dictionary key if found; otherwise, . public string? GetDictionaryKey(ushort id) { if (_dictionaryReverseCache.TryGetValue(id, out var key)) return key; return null; } private bool InsertDictionaryEntryGlobal(string key, ushort value) { var pageId = _dictionaryRootPageId; var pageBuffer = new byte[PageSize]; while (true) { ReadPage(pageId, null, pageBuffer); // Try Insert if (DictionaryPage.Insert(pageBuffer, key, value)) { // Success - Write Back WritePageImmediate(pageId, pageBuffer); return true; } // Page Full - Check Next Page var header = PageHeader.ReadFrom(pageBuffer); if (header.NextPageId != 0) { pageId = header.NextPageId; continue; } // No Next Page - Allocate New var newPageId = AllocatePage(); var newPageBuffer = new byte[PageSize]; DictionaryPage.Initialize(newPageBuffer, newPageId); // Should likely insert into NEW page immediately to save I/O? // Or just link and loop? // Let's Insert into new page logic here to avoid re-reading. if (!DictionaryPage.Insert(newPageBuffer, key, value)) return false; // Should not happen on empty page unless key is huge > page // Write New Page WritePageImmediate(newPageId, newPageBuffer); // Update Previous Page Link header.NextPageId = newPageId; header.WriteTo(pageBuffer); WritePageImmediate(pageId, pageBuffer); return true; } } /// /// Registers a set of keys in the global dictionary. /// Ensures all keys are assigned an ID and persisted. /// /// The keys to register. public void RegisterKeys(IEnumerable keys) { foreach (var key in keys) { GetOrAddDictionaryEntry(key.ToLowerInvariant()); } } }