package linedb import ( "fmt" "strconv" "sync" ) // IndexPosition — позиция записи в индексе (партиция + номер строки). // Partition "default" — для обычных коллекций без партиционирования. type IndexPosition struct { Partition string // имя партиции ("default" для обычных коллекций) LineIndex int } // DefaultPartition — значение партиции для непартиционированных коллекций. const DefaultPartition = "default" // IndexStore — интерфейс хранилища индексов. // Позволяет подключать разные реализации: в памяти, memcached и др. // Индекс привязан к логической коллекции; для партиционированных хранит (partition, lineIndex). type IndexStore interface { // Lookup ищет позиции записей по полю и значению. // value — строковое представление (см. valueToIndexKey). // Возвращает (partition, lineIndex) — для непартиционированных partition = DefaultPartition. Lookup(collection, field, value string) ([]IndexPosition, error) // IndexRecord добавляет одну запись в индекс. // partition — имя партиции (DefaultPartition для обычных коллекций). IndexRecord(collection, partition string, fields []string, record map[string]any, lineIndex int) // UnindexRecord удаляет одну запись из индекса. UnindexRecord(collection, partition string, fields []string, record map[string]any, lineIndex int) // Rebuild перестраивает вклад в индекс для одной партиции (или всей коллекции, если одна партиция). // lineIndexes[i] — 0-based номер физической строки в файле для records[i] (после пустых/удалённых строк). // Если lineIndexes == nil или len != len(records), используются плотные индексы 0..len-1 (устаревшее поведение). Rebuild(collection, partition string, fields []string, records []any, lineIndexes []int) 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 -> []IndexPosition index map[string]map[string][]IndexPosition } // NewInMemoryIndexStore создаёт новый in-memory индекс. func NewInMemoryIndexStore() *InMemoryIndexStore { return &InMemoryIndexStore{ index: make(map[string]map[string][]IndexPosition), } } // indexKey формирует ключ карты для collection:field. func (s *InMemoryIndexStore) indexKey(collection, field string) string { return collection + ":" + field } // IndexRecord добавляет запись в индекс. func (s *InMemoryIndexStore) IndexRecord(collection, partition string, fields []string, record map[string]any, lineIndex int) { if len(fields) == 0 || record == nil { return } if partition == "" { partition = DefaultPartition } 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][]IndexPosition) } s.index[key][val] = append(s.index[key][val], IndexPosition{Partition: partition, LineIndex: lineIndex}) } } // UnindexRecord удаляет запись из индекса (по partition и lineIndex). func (s *InMemoryIndexStore) UnindexRecord(collection, partition string, fields []string, record map[string]any, lineIndex int) { if len(fields) == 0 || record == nil { return } if partition == "" { partition = DefaultPartition } 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 } positions := bucket[val] newPos := make([]IndexPosition, 0, len(positions)) for _, p := range positions { if p.Partition != partition || p.LineIndex != lineIndex { newPos = append(newPos, p) } } if len(newPos) == 0 { delete(bucket, val) } else { bucket[val] = newPos } } } // Lookup возвращает позиции (partition, lineIndex) по полю и значению. func (s *InMemoryIndexStore) Lookup(collection, field, value string) ([]IndexPosition, error) { s.mu.RLock() defer s.mu.RUnlock() key := s.indexKey(collection, field) bucket := s.index[key] if bucket == nil { return nil, nil } positions := bucket[value] if len(positions) == 0 { return nil, nil } out := make([]IndexPosition, len(positions)) copy(out, positions) return out, nil } // Rebuild перестраивает вклад партиции в индекс. func (s *InMemoryIndexStore) Rebuild(collection, partition string, fields []string, records []any, lineIndexes []int) error { if partition == "" { partition = DefaultPartition } s.mu.Lock() defer s.mu.Unlock() // Удаляем старые позиции этой партиции из индекса for _, field := range fields { key := s.indexKey(collection, field) bucket := s.index[key] if bucket == nil { continue } for val, positions := range bucket { newPos := make([]IndexPosition, 0, len(positions)) for _, p := range positions { if p.Partition != partition { newPos = append(newPos, p) } } if len(newPos) == 0 { delete(bucket, val) } else { bucket[val] = newPos } } } // Добавляем новые позиции for i, rec := range records { lineIdx := i if lineIndexes != nil && i < len(lineIndexes) { lineIdx = lineIndexes[i] } 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][]IndexPosition) } s.index[key][val] = append(s.index[key][val], IndexPosition{Partition: partition, LineIndex: lineIdx}) } } 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][]IndexPosition. 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][]IndexPosition, len(bucket)) for val, positions := range bucket { pos2 := make([]IndexPosition, len(positions)) copy(pos2, positions) cp[val] = pos2 } out[k] = cp } return out }