Files
futriis/internal/storage/collection.go
2026-04-08 21:43:35 +03:00

737 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Файл: internal/storage/collection.go
// Назначение: Реализация коллекции с индексами (первичными и вторичными).
// Индексы хранятся отдельно от документов, обеспечивают wait-free доступ.
// Исправлено: корректная работа уникальных индексов, удаление из индексов при обновлении.
package storage
import (
"fmt"
"sync"
"sync/atomic"
"time"
"strings"
"futriis/internal/serializer"
)
// Collection представляет коллекцию документов (аналог таблицы)
type Collection struct {
name string
docs sync.Map // map[string]*Document - wait-free хранилище документов
indexes sync.Map // map[string]*Index - индексы для быстрого поиска
metadata *CollectionMetadata // Метаданные коллекции
docCount atomic.Int64 // Атомарный счётчик документов
sizeBytes atomic.Int64 // Атомарный размер коллекции в байтах
mu sync.RWMutex // Для операций, изменяющих структуру коллекции
constraints *Constraints // Ограничения коллекции
acl *CollectionACL // ACL для коллекции
}
// CollectionMetadata содержит метаданные коллекции
type CollectionMetadata struct {
Name string `msgpack:"name"`
CreatedAt int64 `msgpack:"created_at"`
UpdatedAt int64 `msgpack:"updated_at"`
DocumentCount int64 `msgpack:"document_count"`
SizeBytes int64 `msgpack:"size_bytes"`
IndexCount int `msgpack:"index_count"`
Settings *CollectionSettings `msgpack:"settings"`
}
// CollectionSettings содержит настройки коллекции
type CollectionSettings struct {
MaxDocuments int `msgpack:"max_documents"` // Максимальное количество документов (0 = безлимит)
ValidateSchema bool `msgpack:"validate_schema"` // Валидировать схему документов
AutoIndexID bool `msgpack:"auto_index_id"` // Автоматически индексировать поле _id
TTLSeconds int `msgpack:"ttl_seconds"` // Время жизни документов (0 = бессрочно)
}
// Index представляет индекс для ускорения поиска (хранится отдельно от документов)
type Index struct {
Name string `msgpack:"name"`
Fields []string `msgpack:"fields"` // Поля для индексации
Unique bool `msgpack:"unique"` // Уникальный индекс
data sync.Map // map[interface{}]string - значение индекса -> ID документа
}
// Constraints представляет ограничения на коллекцию
type Constraints struct {
mu sync.RWMutex
RequiredFields map[string]bool // Обязательные поля
UniqueFields map[string]bool // Уникальные поля (дополнительно к индексам)
MinValues map[string]float64 // Минимальные значения для числовых полей
MaxValues map[string]float64 // Максимальные значения для числовых полей
PatternFields map[string]string // Regexp паттерны для полей
EnumFields map[string][]interface{} // Допустимые значения для полей
}
// CollectionACL представляет список контроля доступа для коллекции
type CollectionACL struct {
mu sync.RWMutex
ReadRoles map[string]bool // Роли, имеющие доступ на чтение
WriteRoles map[string]bool // Роли, имеющие доступ на запись
DeleteRoles map[string]bool // Роли, имеющие доступ на удаление
AdminRoles map[string]bool // Роли, имеющие полный доступ
}
// NewCollection создаёт новую коллекцию
func NewCollection(name string, settings *CollectionSettings) *Collection {
if settings == nil {
settings = &CollectionSettings{
MaxDocuments: 0,
ValidateSchema: false,
AutoIndexID: true,
TTLSeconds: 0,
}
}
now := time.Now().UnixMilli()
coll := &Collection{
name: name,
metadata: &CollectionMetadata{
Name: name,
CreatedAt: now,
UpdatedAt: now,
DocumentCount: 0,
SizeBytes: 0,
IndexCount: 0,
Settings: settings,
},
constraints: &Constraints{
RequiredFields: make(map[string]bool),
UniqueFields: make(map[string]bool),
MinValues: make(map[string]float64),
MaxValues: make(map[string]float64),
PatternFields: make(map[string]string),
EnumFields: make(map[string][]interface{}),
},
acl: &CollectionACL{
ReadRoles: make(map[string]bool),
WriteRoles: make(map[string]bool),
DeleteRoles: make(map[string]bool),
AdminRoles: make(map[string]bool),
},
}
// Автоматически создаём первичный индекс по _id
if settings.AutoIndexID {
coll.CreateIndex("_id_", []string{"_id"}, true)
}
// Запускаем фоновую задачу для удаления просроченных документов
if settings.TTLSeconds > 0 {
go coll.ttlCleanupLoop()
}
return coll
}
// Name возвращает имя коллекции
func (c *Collection) Name() string {
return c.name
}
// Insert вставляет документ в коллекцию (wait-free)
func (c *Collection) Insert(doc *Document) error {
// Проверка ограничений
if err := c.constraints.ValidateDocument(doc); err != nil {
return fmt.Errorf("constraint violation: %v", err)
}
// Проверка ACL (будет вызвано из верхнего уровня с ролью)
// Проверка на максимальное количество документов
if c.metadata.Settings.MaxDocuments > 0 {
if c.docCount.Load() >= int64(c.metadata.Settings.MaxDocuments) {
return fmt.Errorf("collection is full: max documents %d reached", c.metadata.Settings.MaxDocuments)
}
}
// Валидация схемы (если включена)
if c.metadata.Settings.ValidateSchema {
if err := c.validateDocument(doc); err != nil {
return fmt.Errorf("document validation failed: %v", err)
}
}
// Проверка уникальных индексов
if err := c.checkUniqueConstraints(doc); err != nil {
return err
}
// Сериализация для проверки (опционально)
data, err := serializer.Marshal(doc)
if err != nil {
return fmt.Errorf("failed to serialize document: %v", err)
}
// Атомарное сохранение документа
if _, loaded := c.docs.LoadOrStore(doc.ID, doc); loaded {
return fmt.Errorf("document with id %s already exists", doc.ID)
}
// Обновление индексов (wait-free)
c.updateIndexes(doc, true)
// Обновление метаданных
c.docCount.Add(1)
c.sizeBytes.Add(int64(len(data)))
c.metadata.DocumentCount = c.docCount.Load()
c.metadata.SizeBytes = c.sizeBytes.Load()
c.metadata.UpdatedAt = time.Now().UnixMilli()
return nil
}
// InsertFromMap создаёт и вставляет документ из map
func (c *Collection) InsertFromMap(fields map[string]interface{}) error {
doc := NewDocument()
for k, v := range fields {
doc.SetField(k, v)
}
return c.Insert(doc)
}
// Find находит документ по ID (с использованием первичного индекса)
func (c *Collection) Find(id string) (*Document, error) {
if val, ok := c.docs.Load(id); ok {
doc := val.(*Document)
// Проверяем, не истёк ли TTL
if c.metadata.Settings.TTLSeconds > 0 {
if time.Now().UnixMilli()-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
c.Delete(id) // Автоматически удаляем просроченный документ
return nil, fmt.Errorf("key not found")
}
}
return doc, nil
}
return nil, fmt.Errorf("key not found")
}
// FindByIndex находит документы по значению индексированного поля
// Исправлено: корректный поиск для неуникальных индексов
func (c *Collection) FindByIndex(indexName string, value interface{}) ([]*Document, error) {
idxVal, ok := c.indexes.Load(indexName)
if !ok {
return nil, fmt.Errorf("index not found: %s", indexName)
}
index := idxVal.(*Index)
docs := make([]*Document, 0)
if index.Unique {
// Уникальный индекс возвращает один документ
if docID, ok := index.data.Load(value); ok {
if doc, err := c.Find(docID.(string)); err == nil {
docs = append(docs, doc)
}
}
} else {
// Исправлено: для неуникального индекса нужно найти все документы с данным значением
index.data.Range(func(key, val interface{}) bool {
// key - значение индекса, val - ID документа
if fmt.Sprintf("%v", key) == fmt.Sprintf("%v", value) {
if doc, err := c.Find(val.(string)); err == nil {
docs = append(docs, doc)
}
}
return true
})
}
return docs, nil
}
// FindByIndexPrefix находит документы по префиксу индекса (для строковых полей)
func (c *Collection) FindByIndexPrefix(indexName string, prefix string) ([]*Document, error) {
idxVal, ok := c.indexes.Load(indexName)
if !ok {
return nil, fmt.Errorf("index not found: %s", indexName)
}
index := idxVal.(*Index)
docs := make([]*Document, 0)
index.data.Range(func(key, val interface{}) bool {
if keyStr, ok := key.(string); ok {
if strings.HasPrefix(keyStr, prefix) {
if doc, err := c.Find(val.(string)); err == nil {
docs = append(docs, doc)
}
}
}
return true
})
return docs, nil
}
// Update обновляет документ по ID
// Исправлено: корректное обновление индексов при изменении индексированных полей
func (c *Collection) Update(id string, updates map[string]interface{}) error {
val, ok := c.docs.Load(id)
if !ok {
return fmt.Errorf("key not found")
}
oldDoc := val.(*Document)
// Создаём копию для проверки уникальности
newDoc := oldDoc.Clone()
if err := newDoc.Update(updates); err != nil {
return err
}
// Проверяем ограничения
if err := c.constraints.ValidateDocument(newDoc); err != nil {
return fmt.Errorf("constraint violation: %v", err)
}
// Проверяем уникальные индексы
if err := c.checkUniqueConstraintsUpdate(oldDoc, newDoc); err != nil {
return err
}
// Исправлено: сначала удаляем старые индексы, потом добавляем новые
c.removeFromIndexes(oldDoc)
c.addToIndexes(newDoc)
// Сохраняем обновлённый документ
c.docs.Store(id, newDoc)
c.metadata.UpdatedAt = time.Now().UnixMilli()
return nil
}
// Delete удаляет документ по ID
// Исправлено: удаление из всех индексов перед удалением документа
func (c *Collection) Delete(id string) error {
val, ok := c.docs.Load(id)
if !ok {
return fmt.Errorf("key not found")
}
doc := val.(*Document)
// Удаляем из индексов
c.removeFromIndexes(doc)
// Удаляем документ
c.docs.Delete(id)
// Обновляем метаданные
c.docCount.Add(-1)
// Размер не обновляем для простоты (можно пересчитать при необходимости)
c.metadata.DocumentCount = c.docCount.Load()
c.metadata.UpdatedAt = time.Now().UnixMilli()
return nil
}
// removeFromIndexes удаляет документ из всех индексов (wait-free)
func (c *Collection) removeFromIndexes(doc *Document) {
c.indexes.Range(func(key, value interface{}) bool {
index := value.(*Index)
indexValue := c.extractIndexValue(doc, index.Fields)
index.data.Delete(indexValue)
return true
})
}
// addToIndexes добавляет документ во все индексы (wait-free)
func (c *Collection) addToIndexes(doc *Document) {
c.indexes.Range(func(key, value interface{}) bool {
index := value.(*Index)
indexValue := c.extractIndexValue(doc, index.Fields)
if index.Unique {
index.data.LoadOrStore(indexValue, doc.ID)
} else {
index.data.Store(indexValue, doc.ID)
}
return true
})
}
// CreateIndex создаёт новый индекс на коллекции (wait-free friendly)
func (c *Collection) CreateIndex(name string, fields []string, unique bool) error {
c.mu.Lock()
defer c.mu.Unlock()
if _, exists := c.indexes.Load(name); exists {
return fmt.Errorf("index %s already exists", name)
}
index := &Index{
Name: name,
Fields: fields,
Unique: unique,
}
// Строим индекс на существующих документах (wait-free)
c.docs.Range(func(key, value interface{}) bool {
doc := value.(*Document)
indexValue := c.extractIndexValue(doc, fields)
if unique {
if _, loaded := index.data.LoadOrStore(indexValue, doc.ID); loaded {
// Найден дубликат - откатываем создание индекса
c.mu.Unlock()
return false
}
} else {
index.data.Store(indexValue, doc.ID)
}
return true
})
c.indexes.Store(name, index)
c.metadata.IndexCount++
return nil
}
// DropIndex удаляет индекс
func (c *Collection) DropIndex(name string) error {
if _, exists := c.indexes.LoadAndDelete(name); !exists {
return fmt.Errorf("index not found: %s", name)
}
c.metadata.IndexCount--
return nil
}
// GetIndexes возвращает список всех индексов
func (c *Collection) GetIndexes() []string {
names := make([]string, 0)
c.indexes.Range(func(key, value interface{}) bool {
names = append(names, key.(string))
return true
})
return names
}
// extractIndexValue извлекает значение из документа для индексации
func (c *Collection) extractIndexValue(doc *Document, fields []string) interface{} {
if len(fields) == 1 {
val, _ := doc.GetField(fields[0])
return val
}
// Составной индекс - возвращаем строковое представление
parts := make([]string, 0, len(fields))
for _, field := range fields {
if val, err := doc.GetField(field); err == nil {
parts = append(parts, fmt.Sprintf("%v", val))
} else {
parts = append(parts, "NULL")
}
}
return strings.Join(parts, "|")
}
// updateIndexes обновляет индексы для документа (исправлено: используем отдельные методы)
func (c *Collection) updateIndexes(doc *Document, add bool) {
if add {
c.addToIndexes(doc)
} else {
c.removeFromIndexes(doc)
}
}
// checkUniqueConstraints проверяет уникальные индексы перед вставкой
func (c *Collection) checkUniqueConstraints(doc *Document) error {
var errs []string
c.indexes.Range(func(key, value interface{}) bool {
index := value.(*Index)
if index.Unique {
indexValue := c.extractIndexValue(doc, index.Fields)
if _, exists := index.data.Load(indexValue); exists {
errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, indexValue))
}
}
return true
})
if len(errs) > 0 {
return fmt.Errorf("%s", strings.Join(errs, "; "))
}
return nil
}
// checkUniqueConstraintsUpdate проверяет уникальность при обновлении
func (c *Collection) checkUniqueConstraintsUpdate(oldDoc, newDoc *Document) error {
var errs []string
c.indexes.Range(func(key, value interface{}) bool {
index := value.(*Index)
if index.Unique {
oldValue := c.extractIndexValue(oldDoc, index.Fields)
newValue := c.extractIndexValue(newDoc, index.Fields)
if fmt.Sprintf("%v", oldValue) != fmt.Sprintf("%v", newValue) {
if _, exists := index.data.Load(newValue); exists {
errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, newValue))
}
}
}
return true
})
if len(errs) > 0 {
return fmt.Errorf("%s", strings.Join(errs, "; "))
}
return nil
}
// validateDocument валидирует документ согласно схеме коллекции
func (c *Collection) validateDocument(doc *Document) error {
if doc.ID == "" {
return fmt.Errorf("document must have _id field")
}
return nil
}
// ttlCleanupLoop периодически удаляет просроченные документы
func (c *Collection) ttlCleanupLoop() {
ticker := time.NewTicker(time.Duration(c.metadata.Settings.TTLSeconds/2) * time.Second)
defer ticker.Stop()
for range ticker.C {
now := time.Now().UnixMilli()
toDelete := make([]string, 0)
c.docs.Range(func(key, value interface{}) bool {
doc := value.(*Document)
if now-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
toDelete = append(toDelete, doc.ID)
}
return true
})
for _, id := range toDelete {
c.Delete(id)
}
}
}
// Count возвращает количество документов в коллекции
func (c *Collection) Count() int64 {
return c.docCount.Load()
}
// Size возвращает размер коллекции в байтах
func (c *Collection) Size() int64 {
return c.sizeBytes.Load()
}
// GetAllDocuments возвращает все документы коллекции
func (c *Collection) GetAllDocuments() []*Document {
docs := make([]*Document, 0, c.docCount.Load())
c.docs.Range(func(key, value interface{}) bool {
docs = append(docs, value.(*Document))
return true
})
return docs
}
// FindByFilter находит документы по произвольному фильтру
func (c *Collection) FindByFilter(filter func(*Document) bool) []*Document {
results := make([]*Document, 0)
c.docs.Range(func(key, value interface{}) bool {
doc := value.(*Document)
if filter(doc) {
results = append(results, doc)
}
return true
})
return results
}
// GetMetadata возвращает метаданные коллекции
func (c *Collection) GetMetadata() *CollectionMetadata {
c.mu.RLock()
defer c.mu.RUnlock()
return c.metadata
}
// Drop удаляет все документы из коллекции
func (c *Collection) Drop() error {
c.mu.Lock()
defer c.mu.Unlock()
c.docs = sync.Map{}
c.indexes = sync.Map{}
if c.metadata.Settings.AutoIndexID {
c.CreateIndex("_id_", []string{"_id"}, true)
}
c.docCount.Store(0)
c.sizeBytes.Store(0)
c.metadata.DocumentCount = 0
c.metadata.SizeBytes = 0
c.metadata.UpdatedAt = time.Now().UnixMilli()
return nil
}
// ========== Constraints Methods ==========
// AddRequiredField добавляет обязательное поле
func (c *Collection) AddRequiredField(field string) {
c.constraints.mu.Lock()
defer c.constraints.mu.Unlock()
c.constraints.RequiredFields[field] = true
}
// AddUniqueConstraint добавляет ограничение уникальности
func (c *Collection) AddUniqueConstraint(field string) {
c.constraints.mu.Lock()
defer c.constraints.mu.Unlock()
c.constraints.UniqueFields[field] = true
// Также создаём уникальный индекс
c.CreateIndex("unique_"+field, []string{field}, true)
}
// AddMinConstraint добавляет минимальное значение
func (c *Collection) AddMinConstraint(field string, min float64) {
c.constraints.mu.Lock()
defer c.constraints.mu.Unlock()
c.constraints.MinValues[field] = min
}
// AddMaxConstraint добавляет максимальное значение
func (c *Collection) AddMaxConstraint(field string, max float64) {
c.constraints.mu.Lock()
defer c.constraints.mu.Unlock()
c.constraints.MaxValues[field] = max
}
// AddPatternConstraint добавляет regexp паттерн
func (c *Collection) AddPatternConstraint(field string, pattern string) {
c.constraints.mu.Lock()
defer c.constraints.mu.Unlock()
c.constraints.PatternFields[field] = pattern
}
// AddEnumConstraint добавляет допустимые значения
func (c *Collection) AddEnumConstraint(field string, values []interface{}) {
c.constraints.mu.Lock()
defer c.constraints.mu.Unlock()
c.constraints.EnumFields[field] = values
}
// ValidateDocument проверяет документ на соответствие ограничениям
func (cons *Constraints) ValidateDocument(doc *Document) error {
cons.mu.RLock()
defer cons.mu.RUnlock()
// Проверка обязательных полей
for field := range cons.RequiredFields {
if !doc.HasField(field) {
return fmt.Errorf("required field '%s' is missing", field)
}
}
// Проверка уникальности (дополнительно к индексам)
// (основная проверка в индексах)
// Проверка числовых ограничений
for field, minVal := range cons.MinValues {
if val, err := doc.GetField(field); err == nil {
if numVal, ok := toFloat64(val); ok {
if numVal < minVal {
return fmt.Errorf("field '%s' value %v is less than minimum %v", field, numVal, minVal)
}
}
}
}
for field, maxVal := range cons.MaxValues {
if val, err := doc.GetField(field); err == nil {
if numVal, ok := toFloat64(val); ok {
if numVal > maxVal {
return fmt.Errorf("field '%s' value %v exceeds maximum %v", field, numVal, maxVal)
}
}
}
}
// Проверка enum
for field, allowedValues := range cons.EnumFields {
if val, err := doc.GetField(field); err == nil {
found := false
for _, allowed := range allowedValues {
if fmt.Sprintf("%v", val) == fmt.Sprintf("%v", allowed) {
found = true
break
}
}
if !found {
return fmt.Errorf("field '%s' value '%v' not in allowed list", field, val)
}
}
}
return nil
}
// ========== ACL Methods ==========
// SetACL устанавливает ACL для коллекции
func (c *Collection) SetACL(role string, canRead, canWrite, canDelete, isAdmin bool) {
c.acl.mu.Lock()
defer c.acl.mu.Unlock()
if canRead {
c.acl.ReadRoles[role] = true
}
if canWrite {
c.acl.WriteRoles[role] = true
}
if canDelete {
c.acl.DeleteRoles[role] = true
}
if isAdmin {
c.acl.AdminRoles[role] = true
}
}
// CheckPermission проверяет наличие разрешения у роли
func (c *Collection) CheckPermission(role, operation string) bool {
c.acl.mu.RLock()
defer c.acl.mu.RUnlock()
// Администратор имеет все права
if c.acl.AdminRoles[role] {
return true
}
switch operation {
case "read":
return c.acl.ReadRoles[role]
case "write":
return c.acl.WriteRoles[role]
case "delete":
return c.acl.DeleteRoles[role]
default:
return false
}
}
// toFloat64 конвертирует interface{} в float64
func toFloat64(val interface{}) (float64, bool) {
switch v := val.(type) {
case int:
return float64(v), true
case int64:
return float64(v), true
case float64:
return v, true
case float32:
return float64(v), true
default:
return 0, false
}
}