737 lines
26 KiB
Go
737 lines
26 KiB
Go
|
|
// Файл: 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
|
|||
|
|
}
|
|||
|
|
}
|