/* * 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"` DeletedAt int64 `msgpack:"deleted_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 = бессрочно) SoftDelete bool `msgpack:"soft_delete"` // Мягкое удаление документов } // Index представляет индекс для ускорения поиска (хранится отдельно от документов) type Index struct { Name string `msgpack:"name"` Fields []string `msgpack:"fields"` // Поля для индексации Unique bool `msgpack:"unique"` // Уникальный индекс CreatedAt int64 `msgpack:"created_at"` // Время создания индекса 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 // Роли, имеющие полный доступ UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления ACL } // NewCollection создаёт новую коллекцию func NewCollection(dbName, name string, settings *CollectionSettings) *Collection { if settings == nil { settings = &CollectionSettings{ MaxDocuments: 0, ValidateSchema: false, AutoIndexID: true, TTLSeconds: 0, SoftDelete: false, } } now := time.Now().UnixMilli() coll := &Collection{ dbName: dbName, name: name, metadata: &CollectionMetadata{ Name: name, CreatedAt: now, UpdatedAt: now, DeletedAt: 0, 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), UpdatedAt: now, }, } // Автоматически создаём первичный индекс по _id if settings.AutoIndexID { coll.CreateIndex("_id_", []string{"_id"}, true) } // Запускаем фоновую задачу для удаления просроченных документов if settings.TTLSeconds > 0 { go coll.ttlCleanupLoop() } // Аудит создания коллекции AuditCollectionOperation("CREATE", dbName, name, settings) 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 } // Обновляем временные метки документа doc.CreatedAt = time.Now().UnixMilli() doc.UpdatedAt = doc.CreatedAt doc.DeletedAt = 0 // Сериализация для проверки (опционально) 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() // Аудит вставки документа AuditDocumentOperation("INSERT", c.dbName, c.name, doc.ID, doc.GetFields()) 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) // Проверяем мягкое удаление if doc.IsDeleted() && c.metadata.Settings.SoftDelete { return nil, fmt.Errorf("document deleted at %s", doc.GetDeletedAtStr()) } // Проверяем, не истёк ли TTL if c.metadata.Settings.TTLSeconds > 0 { if time.Now().UnixMilli()-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) { if c.metadata.Settings.SoftDelete { doc.SoftDelete() c.docs.Store(id, doc) } else { c.Delete(id) } return nil, fmt.Errorf("key not found") } } return doc, nil } return nil, fmt.Errorf("key not found") } // FindIncludingDeleted находит документ даже если он мягко удалён func (c *Collection) FindIncludingDeleted(id string) (*Document, error) { if val, ok := c.docs.Load(id); ok { return val.(*Document), 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) // Проверяем, не удалён ли документ мягко if oldDoc.IsDeleted() && c.metadata.Settings.SoftDelete { return fmt.Errorf("cannot update deleted document") } // Создаём копию для проверки уникальности newDoc := oldDoc.Clone() if err := newDoc.Update(updates); err != nil { return err } // Обновляем временную метку newDoc.UpdatedAt = time.Now().UnixMilli() // Проверяем ограничения 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() // Аудит обновления документа AuditDocumentOperation("UPDATE", c.dbName, c.name, id, updates) 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) if c.metadata.Settings.SoftDelete { // Мягкое удаление doc.SoftDelete() c.docs.Store(id, doc) // Удаляем из индексов при мягком удалении c.removeFromIndexes(doc) // Аудит мягкого удаления AuditDocumentOperation("SOFT_DELETE", c.dbName, c.name, id, map[string]interface{}{ "deleted_at": doc.DeletedAt, }) } else { // Физическое удаление // Удаляем из индексов c.removeFromIndexes(doc) // Удаляем документ c.docs.Delete(id) // Аудит физического удаления AuditDocumentOperation("DELETE", c.dbName, c.name, id, nil) } // Обновляем метаданные c.docCount.Add(-1) c.metadata.DocumentCount = c.docCount.Load() c.metadata.UpdatedAt = time.Now().UnixMilli() return nil } // PermanentDelete выполняет физическое удаление мягко удалённого документа func (c *Collection) PermanentDelete(id string) error { val, ok := c.docs.Load(id) if !ok { return fmt.Errorf("key not found") } doc := val.(*Document) if !doc.IsDeleted() { return fmt.Errorf("document is not soft deleted, use Delete() instead") } // Удаляем из индексов c.removeFromIndexes(doc) // Удаляем документ c.docs.Delete(id) // Обновляем метаданные c.docCount.Add(-1) c.metadata.DocumentCount = c.docCount.Load() c.metadata.UpdatedAt = time.Now().UnixMilli() // Аудит физического удаления AuditDocumentOperation("PERMANENT_DELETE", c.dbName, c.name, id, nil) return nil } // RestoreDeleted восстанавливает мягко удалённый документ func (c *Collection) RestoreDeleted(id string) error { val, ok := c.docs.Load(id) if !ok { return fmt.Errorf("key not found") } doc := val.(*Document) if !doc.IsDeleted() { return fmt.Errorf("document is not deleted") } doc.Restore() // Восстанавливаем индексы c.addToIndexes(doc) c.docs.Store(id, doc) c.metadata.UpdatedAt = time.Now().UnixMilli() // Аудит восстановления AuditDocumentOperation("RESTORE", c.dbName, c.name, id, nil) 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) } now := time.Now().UnixMilli() index := &Index{ Name: name, Fields: fields, Unique: unique, CreatedAt: now, } // Строим индекс на существующих документах (wait-free) c.docs.Range(func(key, value interface{}) bool { doc := value.(*Document) // Пропускаем мягко удалённые документы if doc.IsDeleted() && c.metadata.Settings.SoftDelete { return true } 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++ // Аудит создания индекса AuditCollectionOperation("CREATE_INDEX", c.dbName, c.name, map[string]interface{}{ "index_name": name, "fields": fields, "unique": unique, }) 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-- // Аудит удаления индекса AuditCollectionOperation("DROP_INDEX", c.dbName, c.name, map[string]interface{}{ "index_name": name, }) 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, "created_at": idx.CreatedAt, }) 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 !doc.IsDeleted() && 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 { if c.metadata.Settings.SoftDelete { // Подсчитываем только не удалённые документы count := int64(0) c.docs.Range(func(key, value interface{}) bool { doc := value.(*Document) if !doc.IsDeleted() { count++ } return true }) return count } return c.docCount.Load() } // CountAll возвращает общее количество документов (включая мягко удалённые) func (c *Collection) CountAll() int64 { return c.docCount.Load() } // CountDeleted возвращает количество мягко удалённых документов func (c *Collection) CountDeleted() int64 { if !c.metadata.Settings.SoftDelete { return 0 } count := int64(0) c.docs.Range(func(key, value interface{}) bool { doc := value.(*Document) if doc.IsDeleted() { count++ } return true }) return count } // 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 { doc := value.(*Document) if !c.metadata.Settings.SoftDelete || !doc.IsDeleted() { docs = append(docs, doc) } return true }) return docs } // GetAllDocumentsIncludingDeleted возвращает все документы (включая мягко удалённые) func (c *Collection) GetAllDocumentsIncludingDeleted() []*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 (!c.metadata.Settings.SoftDelete || !doc.IsDeleted()) && 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 } // GetTimestamps возвращает временные метки коллекции func (c *Collection) GetTimestamps() map[string]int64 { c.mu.RLock() defer c.mu.RUnlock() return map[string]int64{ "created_at": c.metadata.CreatedAt, "updated_at": c.metadata.UpdatedAt, "deleted_at": c.metadata.DeletedAt, } } // 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() // Аудит удаления коллекции AuditCollectionOperation("DROP", c.dbName, c.name, nil) 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 } // AddRegexConstraint добавляет regexp паттерн func (c *Collection) AddRegexConstraint(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 } // RemoveRequiredField удаляет обязательное поле func (c *Collection) RemoveRequiredField(field string) { c.constraints.mu.Lock() defer c.constraints.mu.Unlock() delete(c.constraints.RequiredFields, field) } // RemoveUniqueConstraint удаляет ограничение уникальности func (c *Collection) RemoveUniqueConstraint(field string) { c.constraints.mu.Lock() defer c.constraints.mu.Unlock() delete(c.constraints.UniqueFields, field) // Также удаляем индекс c.DropIndex("unique_" + field) } // RemoveMinConstraint удаляет минимальное значение func (c *Collection) RemoveMinConstraint(field string) { c.constraints.mu.Lock() defer c.constraints.mu.Unlock() delete(c.constraints.MinValues, field) } // RemoveMaxConstraint удаляет максимальное значение func (c *Collection) RemoveMaxConstraint(field string) { c.constraints.mu.Lock() defer c.constraints.mu.Unlock() delete(c.constraints.MaxValues, field) } // RemoveRegexConstraint удаляет regexp паттерн func (c *Collection) RemoveRegexConstraint(field string) { c.constraints.mu.Lock() defer c.constraints.mu.Unlock() delete(c.constraints.PatternFields, field) } // RemoveEnumConstraint удаляет допустимые значения func (c *Collection) RemoveEnumConstraint(field string) { c.constraints.mu.Lock() defer c.constraints.mu.Unlock() delete(c.constraints.EnumFields, field) } // GetRequiredFields возвращает список обязательных полей func (c *Collection) GetRequiredFields() []string { c.constraints.mu.RLock() defer c.constraints.mu.RUnlock() fields := make([]string, 0, len(c.constraints.RequiredFields)) for field := range c.constraints.RequiredFields { fields = append(fields, field) } return fields } // GetUniqueConstraints возвращает список уникальных полей func (c *Collection) GetUniqueConstraints() []string { c.constraints.mu.RLock() defer c.constraints.mu.RUnlock() fields := make([]string, 0, len(c.constraints.UniqueFields)) for field := range c.constraints.UniqueFields { fields = append(fields, field) } return fields } // GetMinConstraints возвращает карту минимальных значений func (c *Collection) GetMinConstraints() map[string]float64 { c.constraints.mu.RLock() defer c.constraints.mu.RUnlock() result := make(map[string]float64) for field, val := range c.constraints.MinValues { result[field] = val } return result } // GetMaxConstraints возвращает карту максимальных значений func (c *Collection) GetMaxConstraints() map[string]float64 { c.constraints.mu.RLock() defer c.constraints.mu.RUnlock() result := make(map[string]float64) for field, val := range c.constraints.MaxValues { result[field] = val } return result } // GetEnumConstraints возвращает карту enum ограничений func (c *Collection) GetEnumConstraints() map[string][]interface{} { c.constraints.mu.RLock() defer c.constraints.mu.RUnlock() result := make(map[string][]interface{}) for field, vals := range c.constraints.EnumFields { copied := make([]interface{}, len(vals)) copy(copied, vals) result[field] = copied } return result } // GetRegexConstraints возвращает карту regex паттернов func (c *Collection) GetRegexConstraints() map[string]string { c.constraints.mu.RLock() defer c.constraints.mu.RUnlock() result := make(map[string]string) for field, pattern := range c.constraints.PatternFields { result[field] = pattern } return result } // GetConstraints возвращает все ограничения коллекции (для API) func (c *Collection) GetConstraints() map[string]interface{} { return map[string]interface{}{ "required_fields": c.GetRequiredFields(), "unique_fields": c.GetUniqueConstraints(), "min_values": c.GetMinConstraints(), "max_values": c.GetMaxConstraints(), "enum_values": c.GetEnumConstraints(), "regex_patterns": c.GetRegexConstraints(), } } // 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) } } } } // Проверка regex паттернов for field, pattern := range cons.PatternFields { if val, err := doc.GetField(field); err == nil { if strVal, ok := val.(string); ok { // Простая проверка regex (можно заменить на regexp.MustCompile) if pattern != "" && !strings.Contains(strVal, pattern) { return fmt.Errorf("field '%s' value '%s' does not match pattern '%s'", field, strVal, pattern) } } } } // Проверка 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() c.acl.UpdatedAt = time.Now().UnixMilli() 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 } } // GetACLUpdatedAt возвращает время последнего обновления ACL func (c *Collection) GetACLUpdatedAt() int64 { c.acl.mu.RLock() defer c.acl.mu.RUnlock() return c.acl.UpdatedAt } // 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 } }