Files
futriix/internal/storage/collection.go
T

1785 lines
60 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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-10): Удалены вызовы LogAOFInsert/Update/Delete/Restore.
// AOF удалён, единственный источник истины — WAL.
//
// ИСПРАВЛЕНО (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())
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)
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)
}
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)
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)
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
}
}