Files
futriix/internal/storage/collection.go
T

1785 lines
60 KiB
Go
Raw Normal View History

2026-10-04 21:02:26 +00:00
/*
* 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-06 21:42:15 +00:00
// ИЗМЕНЕНО (2026-10): Удалены вызовы LogAOFInsert/Update/Delete/Restore.
// AOF удалён, единственный источник истины — WAL.
2026-10-04 21:02:26 +00:00
//
// ИСПРАВЛЕНО (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
}
2026-10-06 21:42:15 +00:00
}