diff --git a/internal/storage/collection.go b/internal/storage/collection.go deleted file mode 100644 index cddb2c2..0000000 --- a/internal/storage/collection.go +++ /dev/null @@ -1,780 +0,0 @@ -/* - * Copyright 2026 Safronov Grigorii - * - * Licensed under the CDDL, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * https://opensource.org/licenses/CDDL-1.0 - */ - -// Файл: internal/storage/collection.go -// Назначение: Реализация коллекции с индексами (первичными и вторичными). -// Индексы хранятся отдельно от документов, обеспечивают wait-free доступ. -// Исправлено: корректная работа уникальных индексов, удаление из индексов при обновлении. - -package storage - -import ( - "fmt" - "sync" - "sync/atomic" - "time" - "strings" - - "futriis/internal/serializer" -) - -// Collection представляет коллекцию документов (аналог таблицы) -type Collection struct { - dbName string // имя базы данных (добавлено) - 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(dbName, name string, settings *CollectionSettings) *Collection { - if settings == nil { - settings = &CollectionSettings{ - MaxDocuments: 0, - ValidateSchema: false, - AutoIndexID: true, - TTLSeconds: 0, - } - } - - now := time.Now().UnixMilli() - coll := &Collection{ - dbName: dbName, - 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 -} - -// DBName возвращает имя базы данных (добавлено) -func (c *Collection) DBName() string { - return c.dbName -} - -// 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") -} - -// compareValues сравнивает два значения с учётом типа -func compareValues(a, b interface{}) bool { - if a == nil && b == nil { - return true - } - if a == nil || b == nil { - return false - } - - // Пробуем прямое сравнение - if a == b { - return true - } - - // Пробуем сравнение через строковое представление для разных типов - aStr := fmt.Sprintf("%v", a) - bStr := fmt.Sprintf("%v", b) - return aStr == bStr -} - -// 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 { - // Для неуникального индекса используем Range с правильным сравнением - index.data.Range(func(key, val interface{}) bool { - if compareValues(key, 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 создаёт новый индекс на коллекции -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 -} - -// GetIndexesInfo возвращает подробную информацию об индексах (для API) -func (c *Collection) GetIndexesInfo() []map[string]interface{} { - indexes := make([]map[string]interface{}, 0) - c.indexes.Range(func(key, value interface{}) bool { - idx := value.(*Index) - indexes = append(indexes, map[string]interface{}{ - "name": idx.Name, - "fields": idx.Fields, - "unique": idx.Unique, - }) - return true - }) - return indexes -} - -// 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 - } -}