before refactor index store to complex file-line pattern
This commit is contained in:
212
pkg/linedb/index.go
Normal file
212
pkg/linedb/index.go
Normal file
@@ -0,0 +1,212 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user