249 lines
7.8 KiB
Go
249 lines
7.8 KiB
Go
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 перестраивает вклад в индекс для одной партиции (или всей коллекции, если одна партиция).
|
|
// partition — имя партиции; records — записи, позиция в срезе = lineIndex.
|
|
Rebuild(collection, partition 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 -> []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) 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 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][]IndexPosition)
|
|
}
|
|
s.index[key][val] = append(s.index[key][val], IndexPosition{Partition: partition, LineIndex: 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][]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
|
|
}
|