From a23e297ec9a66a8613719e4bdf61f4dafc202d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=A1?= =?UTF-8?q?=D0=B0=D1=84=D1=80=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Sat, 2 May 2026 19:07:56 +0000 Subject: [PATCH] Upload files to "internal/storage" --- internal/storage/collection.go | 922 +++++++++++++++++++++++++++++++++ 1 file changed, 922 insertions(+) create mode 100644 internal/storage/collection.go diff --git a/internal/storage/collection.go b/internal/storage/collection.go new file mode 100644 index 0000000..96dca03 --- /dev/null +++ b/internal/storage/collection.go @@ -0,0 +1,922 @@ +/* + * 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 +} + +// 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() + + 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 + } +}