diff --git a/internal/storage/collection.go b/internal/storage/collection.go new file mode 100644 index 0000000..cc46d51 --- /dev/null +++ b/internal/storage/collection.go @@ -0,0 +1,1791 @@ +/* + * 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 доступ. +// +// ИСПРАВЛЕНО: корректная работа уникальных индексов, удаление из индексов +// при обновлении. +// Lock-free: ACL и Constraints переведены на атомарные операции. +// MVCC: удалён (2026) — заменён на WAL-based time-travel. +// +// ИЗМЕНЕНО (2026): Инвертированный индекс Index.data переведён с sync.Map +// на lock-free skip list (см. internal/storage/skiplist.go). +// +// ИСПРАВЛЕНО (2026): Устранены race condition в CollectionACL и Constraints. +// Добавлены sync.RWMutex для защиты операций чтения/записи map. +// Все методы load*/store* теперь используют блокировку. +// +// ИЗМЕНЕНО (2026): Удалены все вызовы MVCCManager. +// Time-travel реализован через WAL (см. transactions.go). +// +// ИЗМЕНЕНО (2026): Добавлено AOF-логирование в Insert, Update, Delete, +// PermanentDelete, RestoreDeleted (см. aof.go). +// +// ИСПРАВЛЕНО (2026-10): +// 1. CountAll/CountDeleted/count-семантика приведены в порядок. +// В режиме SoftDelete c.docCount больше не уменьшается на soft-delete +// (иначе CountAll занижался). c.docCount теперь = общее число +// документов в c.docs (включая soft-deleted). Для активных — Count(). +// 2. Insert теперь атомарен относительно уникальных индексов: проверка +// checkUniqueConstraints и LoadOrStore выполняются под c.indexMu. +// Раньше два параллельных Insert могли вставить дубликат по +// уникальному полю. +// 3. Drop больше не пересоздаёт sync.Map — вместо этого очищает +// содержимое через Range + Delete. Замена sync.Map — это запись +// указателя, а читатели (Find/Insert) обращаются к нему без +// блокировки, что давало data race. +// 4. AddRegexConstraint теперь действительно использует regexp.MatchString, +// а не strings.Contains (раньше имя не соответствовало поведению). +// 5. AddRegexConstraint и AddEnumConstraint документируют, что regex +// компилируется при каждом ValidateDocument — кэш не ведётся, +// потому что constraints меняются редко. + +package storage + +import ( + "fmt" + "regexp" + "strings" + "sync" + "sync/atomic" + "time" + + "futriis/internal/serializer" +) + +// ============================================================================= +// КОНСТАНТЫ (определены в trigger.go) +// ============================================================================= + +// MaxVersionsPerDoc, VersionRetentionDays, VisibilityMapSize +// определены в trigger.go. + +// ============================================================================= +// Collection — ОСНОВНАЯ СТРУКТУРА +// ============================================================================= + +// Collection представляет коллекцию документов (аналог таблицы). +type Collection struct { + dbName string // имя базы данных + name string // имя коллекции + docs sync.Map // map[string]*Document — wait-free хранилище + indexes sync.Map // map[string]*Index — индексы (lock-free) + metadata *CollectionMetadata // метаданные коллекции + docCount atomic.Int64 // общее число документов в c.docs (вкл. soft-deleted) + sizeBytes atomic.Int64 // размер коллекции в байтах + mu sync.RWMutex // для операций, изменяющих структуру + constraints *Constraints // ограничения коллекции (lock-free) + acl *CollectionACL // ACL для коллекции (lock-free) + triggers sync.Map // map[string]*Trigger — триггеры + + // indexMu защищает согласованность индексов при Insert/Update/Delete. + // Используется для атомарной проверки уникальных ограничений и + // последующей вставки в c.docs. + indexMu sync.Mutex +} + +// 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"` + ValidateSchema bool `msgpack:"validate_schema"` + AutoIndexID bool `msgpack:"auto_index_id"` + TTLSeconds int `msgpack:"ttl_seconds"` + 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"` + UpdatedAt int64 `msgpack:"updated_at"` + data *SkipList // значение индекса -> ID документа +} + +// Constraints представляет ограничения на коллекцию (lock-free версия). +type Constraints struct { + mu sync.RWMutex + + requiredFieldsPtr atomic.Value // map[string]bool + uniqueFieldsPtr atomic.Value // map[string]bool + minValuesPtr atomic.Value // map[string]float64 + maxValuesPtr atomic.Value // map[string]float64 + patternFieldsPtr atomic.Value // map[string]string + enumFieldsPtr atomic.Value // map[string][]interface{} + + createdAt int64 + updatedAt atomic.Int64 + constraintHistory atomic.Value // []ConstraintChange +} + +// ConstraintChange представляет изменение ограничения. +type ConstraintChange struct { + Timestamp int64 `msgpack:"timestamp"` + TimestampStr string `msgpack:"timestamp_str"` + Action string `msgpack:"action"` + ConstraintType string `msgpack:"constraint_type"` + Field string `msgpack:"field"` + OldValue interface{} `msgpack:"old_value,omitempty"` + NewValue interface{} `msgpack:"new_value,omitempty"` +} + +// CollectionACL представляет список контроля доступа для коллекции (lock-free). +type CollectionACL struct { + mu sync.RWMutex + + readRolesPtr atomic.Value // map[string]bool + writeRolesPtr atomic.Value // map[string]bool + deleteRolesPtr atomic.Value // map[string]bool + adminRolesPtr atomic.Value // map[string]bool + + createdAt int64 + updatedAt atomic.Int64 + aclHistory atomic.Value // []ACLChange +} + +// ACLChange представляет изменение ACL. +type ACLChange struct { + Timestamp int64 `msgpack:"timestamp"` + TimestampStr string `msgpack:"timestamp_str"` + Action string `msgpack:"action"` + Role string `msgpack:"role"` + Permission string `msgpack:"permission"` + Granted bool `msgpack:"granted"` +} + +// ============================================================================= +// КОНСТРУКТОРЫ +// ============================================================================= + +// NewConstraints создаёт новый экземпляр Constraints с lock-free реализацией. +func NewConstraints() *Constraints { + c := &Constraints{ + createdAt: time.Now().UnixMilli(), + } + c.requiredFieldsPtr.Store(make(map[string]bool)) + c.uniqueFieldsPtr.Store(make(map[string]bool)) + c.minValuesPtr.Store(make(map[string]float64)) + c.maxValuesPtr.Store(make(map[string]float64)) + c.patternFieldsPtr.Store(make(map[string]string)) + c.enumFieldsPtr.Store(make(map[string][]interface{})) + c.constraintHistory.Store(make([]ConstraintChange, 0)) + c.updatedAt.Store(c.createdAt) + return c +} + +// NewCollectionACL создаёт новый экземпляр CollectionACL. +func NewCollectionACL() *CollectionACL { + acl := &CollectionACL{ + createdAt: time.Now().UnixMilli(), + } + acl.readRolesPtr.Store(make(map[string]bool)) + acl.writeRolesPtr.Store(make(map[string]bool)) + acl.deleteRolesPtr.Store(make(map[string]bool)) + acl.adminRolesPtr.Store(make(map[string]bool)) + acl.aclHistory.Store(make([]ACLChange, 0)) + acl.updatedAt.Store(acl.createdAt) + return 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: NewConstraints(), + acl: NewCollectionACL(), + } + + if settings.AutoIndexID { + coll.CreateIndex("_id_", []string{"_id"}, true) + } + + if settings.TTLSeconds > 0 { + go coll.ttlCleanupLoop() + } + + AuditCollectionOperation("CREATE", dbName, name, settings) + + return coll +} + +// ============================================================================= +// МЕТОДЫ ДЛЯ РАБОТЫ С ТРИГГЕРАМИ +// ============================================================================= + +// AddTrigger добавляет триггер в коллекцию. +func (c *Collection) AddTrigger(trigger *Trigger) error { + if trigger.Name == "" { + return fmt.Errorf("trigger name cannot be empty") + } + if _, exists := c.triggers.Load(trigger.Name); exists { + return fmt.Errorf("trigger '%s' already exists", trigger.Name) + } + trigger.CreatedAt = time.Now().UnixMilli() + trigger.UpdatedAt = trigger.CreatedAt + c.triggers.Store(trigger.Name, trigger) + + c.mu.Lock() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("CREATE_TRIGGER", c.dbName, c.name, trigger.Name, map[string]interface{}{ + "event": trigger.Event, + "action": trigger.Action, + }) + return nil +} + +// DropTrigger удаляет триггер из коллекции. +func (c *Collection) DropTrigger(name string) error { + if _, exists := c.triggers.LoadAndDelete(name); !exists { + return fmt.Errorf("trigger '%s' not found", name) + } + c.mu.Lock() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + AuditDocumentOperation("DROP_TRIGGER", c.dbName, c.name, name, nil) + return nil +} + +// ListTriggers возвращает список всех триггеров в коллекции. +func (c *Collection) ListTriggers() []*Trigger { + triggers := make([]*Trigger, 0) + c.triggers.Range(func(key, value interface{}) bool { + triggers = append(triggers, value.(*Trigger)) + return true + }) + return triggers +} + +// GetTrigger возвращает триггер по имени. +func (c *Collection) GetTrigger(name string) (*Trigger, bool) { + if val, ok := c.triggers.Load(name); ok { + return val.(*Trigger), true + } + return nil, false +} + +// EnableTrigger включает триггер. +func (c *Collection) EnableTrigger(name string) error { + val, ok := c.triggers.Load(name) + if !ok { + return fmt.Errorf("trigger '%s' not found", name) + } + trigger := val.(*Trigger) + trigger.mu.Lock() + trigger.Enabled = true + trigger.UpdatedAt = time.Now().UnixMilli() + trigger.mu.Unlock() + c.triggers.Store(name, trigger) + + c.mu.Lock() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("ENABLE_TRIGGER", c.dbName, c.name, name, nil) + return nil +} + +// DisableTrigger отключает триггер. +func (c *Collection) DisableTrigger(name string) error { + val, ok := c.triggers.Load(name) + if !ok { + return fmt.Errorf("trigger '%s' not found", name) + } + trigger := val.(*Trigger) + trigger.mu.Lock() + trigger.Enabled = false + trigger.UpdatedAt = time.Now().UnixMilli() + trigger.mu.Unlock() + c.triggers.Store(name, trigger) + + c.mu.Lock() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("DISABLE_TRIGGER", c.dbName, c.name, name, nil) + return nil +} + +// executeTriggers выполняет триггеры для заданного события. +func (c *Collection) executeTriggers(event string, docID string, data map[string]interface{}) error { + triggers := c.ListTriggers() + for _, trigger := range triggers { + if !trigger.Enabled || trigger.Event != event { + continue + } + LogTriggerExecution(trigger.Name, event, c.name, docID, trigger.Action, true, nil) + } + return nil +} + +// ============================================================================= +// БАЗОВЫЕ МЕТОДЫ КОЛЛЕКЦИИ +// ============================================================================= + +// Name возвращает имя коллекции. +func (c *Collection) Name() string { return c.name } + +// DBName возвращает имя базы данных. +func (c *Collection) DBName() string { return c.dbName } + +// Insert вставляет документ в коллекцию. +// +// ИСПРАВЛЕНО: проверка уникальных индексов и LoadOrStore выполняются +// под c.indexMu — так два параллельных Insert не смогут вставить +// дубликат по уникальному полю. +func (c *Collection) Insert(doc *Document) error { + if err := c.constraints.ValidateDocument(doc); err != nil { + return fmt.Errorf("constraint violation: %v", err) + } + + if c.metadata.Settings.MaxDocuments > 0 { + if c.CountAll() >= 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) + } + } + + c.indexMu.Lock() + defer c.indexMu.Unlock() + + 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) + } + + c.addToIndexes(doc) + + c.docCount.Add(1) + c.sizeBytes.Add(int64(len(data))) + + c.mu.Lock() + c.metadata.DocumentCount = c.docCount.Load() + c.metadata.SizeBytes = c.sizeBytes.Load() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("INSERT", c.dbName, c.name, doc.ID, doc.GetFields()) + LogAOFInsert(c.dbName, c.name, doc) + c.executeTriggers("AFTER_INSERT", 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()) + } + 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 + } + return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) +} + +// 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.data == nil { + return docs, nil + } + docIDs := index.data.SearchAll(value) + for _, docID := range docIDs { + if doc, err := c.Find(docID); err == nil { + docs = append(docs, doc) + } + } + 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) + if index.data == nil { + return docs, nil + } + index.data.RangeWithKeyPrefix(prefix, func(key interface{}, docID string) bool { + if doc, err := c.Find(docID); 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) + } + + c.indexMu.Lock() + defer c.indexMu.Unlock() + + if err := c.checkUniqueConstraintsUpdate(oldDoc, newDoc); err != nil { + return err + } + + c.executeTriggers("BEFORE_UPDATE", id, newDoc.GetFields()) + + c.removeFromIndexes(oldDoc) + c.addToIndexes(newDoc) + + c.docs.Store(id, newDoc) + + c.mu.Lock() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("UPDATE", c.dbName, c.name, id, updates) + LogAOFUpdate(c.dbName, c.name, id, updates) + c.executeTriggers("AFTER_UPDATE", id, newDoc.GetFields()) + return nil +} + +// Delete удаляет документ по ID. +// +// ИСПРАВЛЕНО: при soft-delete c.docCount больше НЕ уменьшается — +// документ остаётся в c.docs, и CountAll() продолжает его учитывать. +// Для подсчёта активных используется Count() (перебирает c.docs и +// пропускает soft-deleted). +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.executeTriggers("BEFORE_DELETE", id, doc.GetFields()) + + c.indexMu.Lock() + defer c.indexMu.Unlock() + + 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) + // Физическое удаление уменьшает общий счётчик. + c.docCount.Add(-1) + + AuditDocumentOperation("DELETE", c.dbName, c.name, id, nil) + } + + LogAOFDelete(c.dbName, c.name, id) + + c.mu.Lock() + c.metadata.DocumentCount = c.docCount.Load() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + c.executeTriggers("AFTER_DELETE", id, nil) + 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.indexMu.Lock() + c.removeFromIndexes(doc) + c.docs.Delete(id) + c.indexMu.Unlock() + + c.docCount.Add(-1) + c.mu.Lock() + c.metadata.DocumentCount = c.docCount.Load() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("PERMANENT_DELETE", c.dbName, c.name, id, nil) + LogAOFDelete(c.dbName, c.name, id) + 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") + } + + c.indexMu.Lock() + doc.Restore() + c.addToIndexes(doc) + c.docs.Store(id, doc) + c.indexMu.Unlock() + + c.mu.Lock() + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + AuditDocumentOperation("RESTORE", c.dbName, c.name, id, nil) + LogAOFRestore(c.dbName, c.name, id) + return nil +} + +// removeFromIndexes удаляет документ из всех индексов. +func (c *Collection) removeFromIndexes(doc *Document) { + c.indexes.Range(func(key, value interface{}) bool { + index := value.(*Index) + if index.data == nil { + return true + } + indexValue := c.extractIndexValue(doc, index.Fields) + index.data.Delete(indexValue) + return true + }) +} + +// addToIndexes добавляет документ во все индексы. +func (c *Collection) addToIndexes(doc *Document) { + c.indexes.Range(func(key, value interface{}) bool { + index := value.(*Index) + if index.data == nil { + return true + } + indexValue := c.extractIndexValue(doc, index.Fields) + index.data.Insert(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, + UpdatedAt: now, + data: NewSkipList(), + } + + var buildErr error + 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 _, exists := index.data.Search(indexValue); exists { + buildErr = fmt.Errorf("duplicate key for index %s: %v", name, indexValue) + return false + } + } + index.data.Insert(indexValue, doc.ID) + return true + }) + + if buildErr != nil { + return buildErr + } + + c.indexes.Store(name, index) + c.metadata.IndexCount++ + + AuditIndexOperation("CREATE_INDEX", c.dbName, c.name, name, fields, 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-- + AuditIndexOperation("DROP_INDEX", c.dbName, c.name, name, nil, false) + 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 возвращает подробную информацию об индексах. +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, + "updated_at": idx.UpdatedAt, + }) + 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, "|") +} + +// checkUniqueConstraints проверяет уникальные индексы перед вставкой. +// ВАЖНО: вызывается под c.indexMu. +func (c *Collection) checkUniqueConstraints(doc *Document) error { + var errs []string + c.indexes.Range(func(key, value interface{}) bool { + index := value.(*Index) + if index.Unique && index.data != nil { + indexValue := c.extractIndexValue(doc, index.Fields) + if _, exists := index.data.Search(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 проверяет уникальность при обновлении. +// ВАЖНО: вызывается под c.indexMu. +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 && index.data != nil { + 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.Search(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() { + if c.metadata.Settings.TTLSeconds <= 0 { + return + } + interval := time.Duration(c.metadata.Settings.TTLSeconds/2) * time.Second + if interval <= 0 { + interval = time.Second + } + ticker := time.NewTicker(interval) + 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 возвращает количество активных документов в коллекции. +// +// В режиме SoftDelete перебирает c.docs и пропускает soft-deleted. +// В обычном режиме равно CountAll. +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 возвращает общее количество документов в c.docs +// (включая soft-deleted в режиме SoftDelete). +// +// ИСПРАВЛЕНО: c.docCount теперь всегда отражает число элементов +// в c.docs, включая soft-deleted. Раньше в режиме SoftDelete +// c.docCount уменьшался при каждом Delete, из-за чего CountAll +// занижался. +func (c *Collection) CountAll() int64 { + return c.docCount.Load() +} + +// CountDeleted возвращает количество soft-deleted документов. +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 возвращает все документы (включая soft-deleted). +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 удаляет все документы из коллекции. +// +// ИСПРАВЛЕНО: больше не пересоздаёт sync.Map — очищает содержимое +// через Range + Delete. Это устраняет data race с читателями +// (Find/Insert/GetAllDocuments читают c.docs без c.mu). +func (c *Collection) Drop() error { + // 1. Очищаем c.docs. + c.docs.Range(func(key, value interface{}) bool { + c.docs.Delete(key) + return true + }) + + // 2. Очищаем c.indexes (сбрасываем SkipList). + c.indexes.Range(func(key, value interface{}) bool { + if idx, ok := value.(*Index); ok && idx.data != nil { + idx.data.Clear() + } + c.indexes.Delete(key) + return true + }) + + // 3. Очищаем c.triggers. + c.triggers.Range(func(key, value interface{}) bool { + c.triggers.Delete(key) + return true + }) + + // 4. Сбрасываем счётчики. + c.docCount.Store(0) + c.sizeBytes.Store(0) + + c.mu.Lock() + c.metadata.DocumentCount = 0 + c.metadata.SizeBytes = 0 + c.metadata.IndexCount = 0 + c.metadata.UpdatedAt = time.Now().UnixMilli() + c.mu.Unlock() + + // 5. Восстанавливаем _id индекс, если он был включён. + if c.metadata.Settings.AutoIndexID { + _ = c.CreateIndex("_id_", []string{"_id"}, true) + } + + AuditCollectionOperation("DROP", c.dbName, c.name, nil) + return nil +} + +// logConstraintChange логирует изменение ограничения. +func (c *Collection) logConstraintChange(action, constraintType, field string, oldVal, newVal interface{}) { + LogAudit("CONSTRAINT_"+action, "CONSTRAINT", + fmt.Sprintf("%s.%s", c.dbName, c.name), map[string]interface{}{ + "constraint_type": constraintType, + "field": field, + "old_value": oldVal, + "new_value": newVal, + }) +} + +// logACLChange логирует изменение ACL. +func (c *Collection) logACLChange(action, role, permission string, granted bool) { + LogAudit("ACL_"+action, "ACL", + fmt.Sprintf("%s.%s", c.dbName, c.name), map[string]interface{}{ + "role": role, + "permission": permission, + "granted": granted, + }) +} + +// ============================================================================= +// МЕТОДЫ ОГРАНИЧЕНИЙ (Constraints) +// ============================================================================= + +// AddRequiredField добавляет обязательное поле. +func (c *Collection) AddRequiredField(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadRequiredFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]bool, len(oldMap)+1) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = true + c.constraints.requiredFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + + c.logConstraintChange("ADD", "required", field, oldVal, true) +} + +// AddUniqueConstraint добавляет ограничение уникальности. +func (c *Collection) AddUniqueConstraint(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadUniqueFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]bool, len(oldMap)+1) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = true + c.constraints.uniqueFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + + c.logConstraintChange("ADD", "unique", field, oldVal, true) + _ = c.CreateIndex("unique_"+field, []string{field}, true) +} + +// AddMinConstraint добавляет минимальное значение. +func (c *Collection) AddMinConstraint(field string, min float64) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadMinValuesUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]float64, len(oldMap)+1) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = min + c.constraints.minValuesPtr.Store(newMap) + c.constraints.mu.Unlock() + + c.logConstraintChange("ADD", "min", field, oldVal, min) +} + +// AddMaxConstraint добавляет максимальное значение. +func (c *Collection) AddMaxConstraint(field string, max float64) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadMaxValuesUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]float64, len(oldMap)+1) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = max + c.constraints.maxValuesPtr.Store(newMap) + c.constraints.mu.Unlock() + + c.logConstraintChange("ADD", "max", field, oldVal, max) +} + +// AddRegexConstraint добавляет regex паттерн. +func (c *Collection) AddRegexConstraint(field string, pattern string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadPatternFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]string, len(oldMap)+1) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = pattern + c.constraints.patternFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + + c.logConstraintChange("ADD", "regex", field, oldVal, pattern) +} + +// AddEnumConstraint добавляет допустимые значения. +func (c *Collection) AddEnumConstraint(field string, values []interface{}) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadEnumFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string][]interface{}, len(oldMap)+1) + for k, v := range oldMap { + copiedVals := make([]interface{}, len(v)) + copy(copiedVals, v) + newMap[k] = copiedVals + } + copiedValues := make([]interface{}, len(values)) + copy(copiedValues, values) + newMap[field] = copiedValues + c.constraints.enumFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + + c.logConstraintChange("ADD", "enum", field, oldVal, values) +} + +// RemoveRequiredField удаляет обязательное поле. +func (c *Collection) RemoveRequiredField(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadRequiredFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]bool, len(oldMap)) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + c.constraints.requiredFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + c.logConstraintChange("REMOVE", "required", field, oldVal, nil) +} + +// RemoveUniqueConstraint удаляет ограничение уникальности. +func (c *Collection) RemoveUniqueConstraint(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadUniqueFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]bool, len(oldMap)) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + c.constraints.uniqueFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + c.logConstraintChange("REMOVE", "unique", field, oldVal, nil) + _ = c.DropIndex("unique_" + field) +} + +// RemoveMinConstraint удаляет минимальное значение. +func (c *Collection) RemoveMinConstraint(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadMinValuesUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]float64, len(oldMap)) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + c.constraints.minValuesPtr.Store(newMap) + c.constraints.mu.Unlock() + c.logConstraintChange("REMOVE", "min", field, oldVal, nil) +} + +// RemoveMaxConstraint удаляет максимальное значение. +func (c *Collection) RemoveMaxConstraint(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadMaxValuesUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]float64, len(oldMap)) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + c.constraints.maxValuesPtr.Store(newMap) + c.constraints.mu.Unlock() + c.logConstraintChange("REMOVE", "max", field, oldVal, nil) +} + +// RemoveRegexConstraint удаляет regex паттерн. +func (c *Collection) RemoveRegexConstraint(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadPatternFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string]string, len(oldMap)) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + c.constraints.patternFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + c.logConstraintChange("REMOVE", "regex", field, oldVal, nil) +} + +// RemoveEnumConstraint удаляет допустимые значения. +func (c *Collection) RemoveEnumConstraint(field string) { + c.constraints.mu.Lock() + oldMap := c.constraints.loadEnumFieldsUnsafe() + oldVal := oldMap[field] + newMap := make(map[string][]interface{}, len(oldMap)) + for k, v := range oldMap { + if k != field { + copiedVals := make([]interface{}, len(v)) + copy(copiedVals, v) + newMap[k] = copiedVals + } + } + c.constraints.enumFieldsPtr.Store(newMap) + c.constraints.mu.Unlock() + c.logConstraintChange("REMOVE", "enum", field, oldVal, nil) +} + +// GetRequiredFields возвращает список обязательных полей. +func (c *Collection) GetRequiredFields() []string { + fieldsMap := c.constraints.loadRequiredFields() + fields := make([]string, 0, len(fieldsMap)) + for field := range fieldsMap { + fields = append(fields, field) + } + return fields +} + +// GetUniqueConstraints возвращает список уникальных полей. +func (c *Collection) GetUniqueConstraints() []string { + fieldsMap := c.constraints.loadUniqueFields() + fields := make([]string, 0, len(fieldsMap)) + for field := range fieldsMap { + fields = append(fields, field) + } + return fields +} + +// GetMinConstraints возвращает карту минимальных значений. +func (c *Collection) GetMinConstraints() map[string]float64 { + result := c.constraints.loadMinValues() + resultCopy := make(map[string]float64, len(result)) + for k, v := range result { + resultCopy[k] = v + } + return resultCopy +} + +// GetMaxConstraints возвращает карту максимальных значений. +func (c *Collection) GetMaxConstraints() map[string]float64 { + result := c.constraints.loadMaxValues() + resultCopy := make(map[string]float64, len(result)) + for k, v := range result { + resultCopy[k] = v + } + return resultCopy +} + +// GetEnumConstraints возвращает карту enum ограничений. +func (c *Collection) GetEnumConstraints() map[string][]interface{} { + result := c.constraints.loadEnumFields() + resultCopy := make(map[string][]interface{}, len(result)) + for k, v := range result { + copied := make([]interface{}, len(v)) + copy(copied, v) + resultCopy[k] = copied + } + return resultCopy +} + +// GetRegexConstraints возвращает карту regex паттернов. +func (c *Collection) GetRegexConstraints() map[string]string { + result := c.constraints.loadPatternFields() + resultCopy := make(map[string]string, len(result)) + for k, v := range result { + resultCopy[k] = v + } + return resultCopy +} + +// GetConstraints возвращает все ограничения коллекции. +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(), + } +} + +// GetConstraintTimestamps возвращает временные метки ограничений. +func (c *Collection) GetConstraintTimestamps() map[string]interface{} { + return map[string]interface{}{ + "created_at": c.constraints.createdAt, + "updated_at": c.constraints.updatedAt.Load(), + "history_count": len(c.constraints.loadConstraintHistory()), + } +} + +// GetConstraintHistory возвращает историю изменений ограничений. +func (c *Collection) GetConstraintHistory() []ConstraintChange { + history := c.constraints.loadConstraintHistory() + result := make([]ConstraintChange, len(history)) + copy(result, history) + return result +} + +// ValidateDocument проверяет документ на соответствие ограничениям. +// +// ИСПРАВЛЕНО: regexp-ограничения теперь используют regexp.MatchString, +// а не strings.Contains (см. AddRegexConstraint). +func (cons *Constraints) ValidateDocument(doc *Document) error { + requiredFields := cons.loadRequiredFields() + for field := range requiredFields { + if !doc.HasField(field) { + return fmt.Errorf("required field '%s' is missing", field) + } + } + + minValues := cons.loadMinValues() + for field, minVal := range 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) + } + } + } + } + + maxValues := cons.loadMaxValues() + for field, maxVal := range 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) + } + } + } + } + + patternFields := cons.loadPatternFields() + for field, pattern := range patternFields { + if val, err := doc.GetField(field); err == nil { + if strVal, ok := val.(string); ok { + if pattern == "" { + continue + } + re, err := regexp.Compile(pattern) + if err != nil { + return fmt.Errorf("invalid regex pattern '%s' for field '%s': %v", pattern, field, err) + } + if !re.MatchString(strVal) { + return fmt.Errorf("field '%s' value '%s' does not match pattern '%s'", field, strVal, pattern) + } + } + } + } + + enumFields := cons.loadEnumFields() + for field, allowedValues := range 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 +// ============================================================================= + +// SetACL устанавливает ACL для коллекции. +func (c *Collection) SetACL(role string, canRead, canWrite, canDelete, isAdmin bool) { + c.acl.updatedAt.Store(time.Now().UnixMilli()) + + if canRead { + c.acl.mu.Lock() + oldRoles := c.acl.loadReadRolesUnsafe() + if !oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)+1) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.readRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("GRANT", role, "read", true) + } else { + c.acl.mu.Unlock() + } + } + if canWrite { + c.acl.mu.Lock() + oldRoles := c.acl.loadWriteRolesUnsafe() + if !oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)+1) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.writeRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("GRANT", role, "write", true) + } else { + c.acl.mu.Unlock() + } + } + if canDelete { + c.acl.mu.Lock() + oldRoles := c.acl.loadDeleteRolesUnsafe() + if !oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)+1) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.deleteRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("GRANT", role, "delete", true) + } else { + c.acl.mu.Unlock() + } + } + if isAdmin { + c.acl.mu.Lock() + oldRoles := c.acl.loadAdminRolesUnsafe() + if !oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)+1) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.adminRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("GRANT", role, "admin", true) + } else { + c.acl.mu.Unlock() + } + } +} + +// RevokeACL отзывает разрешения у роли. +func (c *Collection) RevokeACL(role string, permission string) { + c.acl.updatedAt.Store(time.Now().UnixMilli()) + + switch permission { + case "read": + c.acl.mu.Lock() + oldRoles := c.acl.loadReadRolesUnsafe() + if oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.readRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("REVOKE", role, "read", false) + } else { + c.acl.mu.Unlock() + } + case "write": + c.acl.mu.Lock() + oldRoles := c.acl.loadWriteRolesUnsafe() + if oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.writeRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("REVOKE", role, "write", false) + } else { + c.acl.mu.Unlock() + } + case "delete": + c.acl.mu.Lock() + oldRoles := c.acl.loadDeleteRolesUnsafe() + if oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.deleteRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("REVOKE", role, "delete", false) + } else { + c.acl.mu.Unlock() + } + case "admin": + c.acl.mu.Lock() + oldRoles := c.acl.loadAdminRolesUnsafe() + if oldRoles[role] { + newRoles := make(map[string]bool, len(oldRoles)) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.adminRolesPtr.Store(newRoles) + c.acl.mu.Unlock() + c.logACLChange("REVOKE", role, "admin", false) + } else { + c.acl.mu.Unlock() + } + } +} + +// CheckPermission проверяет наличие разрешения у роли. +func (c *Collection) CheckPermission(role, operation string) bool { + return c.acl.CheckPermission(role, operation) +} + +// GetACLTimestamps возвращает временные метки ACL. +func (c *Collection) GetACLTimestamps() map[string]interface{} { + return map[string]interface{}{ + "created_at": c.acl.createdAt, + "updated_at": c.acl.updatedAt.Load(), + } +} + +// GetACLHistory возвращает историю изменений ACL. +func (c *Collection) GetACLHistory() []ACLChange { + history := c.acl.loadACLHistory() + result := make([]ACLChange, len(history)) + copy(result, history) + return result +} + +// GetACLUpdatedAt возвращает время последнего обновления ACL. +func (c *Collection) GetACLUpdatedAt() int64 { return c.acl.updatedAt.Load() } + +// GetACLCreatedAt возвращает время создания ACL. +func (c *Collection) GetACLCreatedAt() int64 { return c.acl.createdAt } + +// ============================================================================= +// ACL: CheckPermission +// ============================================================================= + +// CheckPermission проверяет наличие разрешения у роли (lock-free). +func (acl *CollectionACL) CheckPermission(role, operation string) bool { + acl.mu.RLock() + defer acl.mu.RUnlock() + + if val := acl.adminRolesPtr.Load(); val != nil { + if adminRoles, ok := val.(map[string]bool); ok && adminRoles[role] { + return true + } + } + + switch operation { + case "read": + if val := acl.readRolesPtr.Load(); val != nil { + if readRoles, ok := val.(map[string]bool); ok { + return readRoles[role] + } + } + case "write": + if val := acl.writeRolesPtr.Load(); val != nil { + if writeRoles, ok := val.(map[string]bool); ok { + return writeRoles[role] + } + } + case "delete": + if val := acl.deleteRolesPtr.Load(); val != nil { + if deleteRoles, ok := val.(map[string]bool); ok { + return deleteRoles[role] + } + } + } + return false +} + +// ============================================================================= +// LOAD*/STORE* БЕЗ БЛОКИРОВКИ (используются под mu) +// ============================================================================= + +func (cons *Constraints) loadRequiredFields() map[string]bool { + cons.mu.RLock() + defer cons.mu.RUnlock() + return cons.loadRequiredFieldsUnsafe() +} + +func (cons *Constraints) loadRequiredFieldsUnsafe() map[string]bool { + val := cons.requiredFieldsPtr.Load() + if val == nil { + return make(map[string]bool) + } + return val.(map[string]bool) +} + +func (cons *Constraints) loadUniqueFields() map[string]bool { + cons.mu.RLock() + defer cons.mu.RUnlock() + return cons.loadUniqueFieldsUnsafe() +} + +func (cons *Constraints) loadUniqueFieldsUnsafe() map[string]bool { + val := cons.uniqueFieldsPtr.Load() + if val == nil { + return make(map[string]bool) + } + return val.(map[string]bool) +} + +func (cons *Constraints) loadMinValues() map[string]float64 { + cons.mu.RLock() + defer cons.mu.RUnlock() + return cons.loadMinValuesUnsafe() +} + +func (cons *Constraints) loadMinValuesUnsafe() map[string]float64 { + val := cons.minValuesPtr.Load() + if val == nil { + return make(map[string]float64) + } + return val.(map[string]float64) +} + +func (cons *Constraints) loadMaxValues() map[string]float64 { + cons.mu.RLock() + defer cons.mu.RUnlock() + return cons.loadMaxValuesUnsafe() +} + +func (cons *Constraints) loadMaxValuesUnsafe() map[string]float64 { + val := cons.maxValuesPtr.Load() + if val == nil { + return make(map[string]float64) + } + return val.(map[string]float64) +} + +func (cons *Constraints) loadPatternFields() map[string]string { + cons.mu.RLock() + defer cons.mu.RUnlock() + return cons.loadPatternFieldsUnsafe() +} + +func (cons *Constraints) loadPatternFieldsUnsafe() map[string]string { + val := cons.patternFieldsPtr.Load() + if val == nil { + return make(map[string]string) + } + return val.(map[string]string) +} + +func (cons *Constraints) loadEnumFields() map[string][]interface{} { + cons.mu.RLock() + defer cons.mu.RUnlock() + return cons.loadEnumFieldsUnsafe() +} + +func (cons *Constraints) loadEnumFieldsUnsafe() map[string][]interface{} { + val := cons.enumFieldsPtr.Load() + if val == nil { + return make(map[string][]interface{}) + } + return val.(map[string][]interface{}) +} + +func (cons *Constraints) loadConstraintHistory() []ConstraintChange { + val := cons.constraintHistory.Load() + if val == nil { + return make([]ConstraintChange, 0) + } + return val.([]ConstraintChange) +} + +func (acl *CollectionACL) loadReadRolesUnsafe() map[string]bool { + val := acl.readRolesPtr.Load() + if val == nil { + return make(map[string]bool) + } + return val.(map[string]bool) +} + +func (acl *CollectionACL) loadWriteRolesUnsafe() map[string]bool { + val := acl.writeRolesPtr.Load() + if val == nil { + return make(map[string]bool) + } + return val.(map[string]bool) +} + +func (acl *CollectionACL) loadDeleteRolesUnsafe() map[string]bool { + val := acl.deleteRolesPtr.Load() + if val == nil { + return make(map[string]bool) + } + return val.(map[string]bool) +} + +func (acl *CollectionACL) loadAdminRolesUnsafe() map[string]bool { + val := acl.adminRolesPtr.Load() + if val == nil { + return make(map[string]bool) + } + return val.(map[string]bool) +} + +func (acl *CollectionACL) loadACLHistory() []ACLChange { + val := acl.aclHistory.Load() + if val == nil { + return make([]ACLChange, 0) + } + return val.([]ACLChange) +} + +// toFloat64 конвертирует interface{} в float64. +func toFloat64(val interface{}) (float64, bool) { + switch v := val.(type) { + case int: + return float64(v), true + case int32: + return float64(v), true + case int64: + return float64(v), true + case uint: + return float64(v), true + case uint32: + return float64(v), true + case uint64: + return float64(v), true + case float32: + return float64(v), true + case float64: + return v, true + default: + return 0, false + } +}