package linedb import ( "fmt" "strconv" "sync" ) // IndexStore — интерфейс хранилища индексов. // Позволяет подключать разные реализации: в памяти, memcached и др. type IndexStore interface { // Lookup ищет позиции записей по полю и значению. // value — строковое представление (см. valueToIndexKey). // Возвращает срез индексов строк (0-based) или nil, nil, // если индекс не используется или записей нет. Lookup(collection, field, value string) ([]int, error) // IndexRecord добавляет одну запись в индекс по номеру строки (для точечных Update). IndexRecord(collection string, fields []string, record map[string]any, lineIndex int) // UnindexRecord удаляет одну запись из индекса по номеру строки. UnindexRecord(collection string, fields []string, record map[string]any, lineIndex int) // Rebuild полностью перестраивает индекс коллекции. // records — полный список записей, каждая позиция в срезе соответствует номеру строки в файле. Rebuild(collection string, fields []string, records []any) error // Clear очищает индекс коллекции. Clear(collection string) error } // valueToIndexKey преобразует значение фильтра в ключ для индекса. func valueToIndexKey(v any) string { if v == nil { return "" } switch t := v.(type) { case string: return t case int: return strconv.Itoa(t) case int64: return strconv.FormatInt(t, 10) case float64: return strconv.FormatFloat(t, 'g', -1, 64) case bool: if t { return "true" } return "false" default: return fmt.Sprintf("%v", v) } } // getFieldValue извлекает значение поля из записи и превращает в ключ индекса. func getFieldValue(record map[string]any, field string) string { v, ok := record[field] if !ok { return "" } return valueToIndexKey(v) } // InMemoryIndexStore — реализация IndexStore в памяти (по умолчанию). type InMemoryIndexStore struct { mu sync.RWMutex // index: collection:field -> value -> список индексов строк (0-based) index map[string]map[string][]int } // NewInMemoryIndexStore создаёт новый in-memory индекс. func NewInMemoryIndexStore() *InMemoryIndexStore { return &InMemoryIndexStore{ index: make(map[string]map[string][]int), } } // indexKey формирует ключ карты для collection:field. func (s *InMemoryIndexStore) indexKey(collection, field string) string { return collection + ":" + field } // IndexRecord добавляет запись в индекс. func (s *InMemoryIndexStore) IndexRecord(collection string, fields []string, record map[string]any, lineIndex int) { if len(fields) == 0 || record == nil { return } s.mu.Lock() defer s.mu.Unlock() for _, field := range fields { val := getFieldValue(record, field) key := s.indexKey(collection, field) if s.index[key] == nil { s.index[key] = make(map[string][]int) } s.index[key][val] = append(s.index[key][val], lineIndex) } } // UnindexRecord удаляет запись из индекса. func (s *InMemoryIndexStore) UnindexRecord(collection string, fields []string, record map[string]any, lineIndex int) { if len(fields) == 0 || record == nil { return } s.mu.Lock() defer s.mu.Unlock() for _, field := range fields { val := getFieldValue(record, field) key := s.indexKey(collection, field) bucket := s.index[key] if bucket == nil { continue } idxs := bucket[val] newIdxs := make([]int, 0, len(idxs)) for _, i := range idxs { if i != lineIndex { newIdxs = append(newIdxs, i) } } if len(newIdxs) == 0 { delete(bucket, val) } else { bucket[val] = newIdxs } } } // Lookup возвращает индексы строк по полю и значению. func (s *InMemoryIndexStore) Lookup(collection, field, value string) ([]int, error) { s.mu.RLock() defer s.mu.RUnlock() key := s.indexKey(collection, field) bucket := s.index[key] if bucket == nil { return nil, nil } indexes := bucket[value] if len(indexes) == 0 { return nil, nil } // Возвращаем копию, чтобы вызывающий код не модифицировал внутренний срез out := make([]int, len(indexes)) copy(out, indexes) return out, nil } // Rebuild полностью перестраивает индекс коллекции. func (s *InMemoryIndexStore) Rebuild(collection string, fields []string, records []any) error { s.mu.Lock() defer s.mu.Unlock() // Очищаем старые записи по этой коллекции for _, field := range fields { key := s.indexKey(collection, field) delete(s.index, key) } for idx, rec := range records { recMap, ok := rec.(map[string]any) if !ok { continue } for _, field := range fields { val := getFieldValue(recMap, field) key := s.indexKey(collection, field) if s.index[key] == nil { s.index[key] = make(map[string][]int) } s.index[key][val] = append(s.index[key][val], idx) } } return nil } // Clear очищает индекс коллекции. func (s *InMemoryIndexStore) Clear(collection string) error { s.mu.Lock() defer s.mu.Unlock() prefix := collection + ":" for k := range s.index { if len(k) > len(prefix) && k[:len(prefix)] == prefix { delete(s.index, k) } } return nil } // GetSnapshotForTest возвращает копию индекса для тестов. Доступ только при accessKey == "give_me_cache". // Ключ — "collection:field", значение — map[string][]int (значение поля -> номера строк). func (s *InMemoryIndexStore) GetSnapshotForTest(accessKey string) map[string]any { const testAccessKey = "give_me_cache" if accessKey != testAccessKey { return map[string]any{} } s.mu.RLock() defer s.mu.RUnlock() out := make(map[string]any, len(s.index)) for k, bucket := range s.index { cp := make(map[string][]int, len(bucket)) for val, idxs := range bucket { idxs2 := make([]int, len(idxs)) copy(idxs2, idxs) cp[val] = idxs2 } out[k] = cp } return out }