1998 lines
58 KiB
Go
1998 lines
58 KiB
Go
/*
|
||||
|
|
* 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 (mvccManager, mvccEnabled,
|
|||
|
|
// CreateVersion, GetVersionAt, GetAllVersions, GetLatestVersion, GetMVCCStats).
|
|||
|
|
// Time-travel теперь реализован через WAL (см. transactions.go:
|
|||
|
|
// GetDocumentAtTimestamp, GetDocumentVersionsSince).
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО (2026): Добавлено AOF-логирование в Insert, Update, Delete,
|
|||
|
|
// PermanentDelete, RestoreDeleted (см. aof.go: LogAOFInsert, LogAOFUpdate,
|
|||
|
|
// LogAOFDelete, LogAOFRestore).
|
|||
|
|
|
|||
|
|
package storage
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"sync"
|
|||
|
|
"sync/atomic"
|
|||
|
|
"time"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"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 // Атомарный счётчик документов
|
|||
|
|
sizeBytes atomic.Int64 // Атомарный размер коллекции в байтах
|
|||
|
|
mu sync.RWMutex // Для операций, изменяющих структуру коллекции
|
|||
|
|
constraints *Constraints // Ограничения коллекции (lock-free)
|
|||
|
|
acl *CollectionACL // ACL для коллекции (lock-free)
|
|||
|
|
triggers sync.Map // map[string]*Trigger - триггеры коллекции
|
|||
|
|
|
|||
|
|
// ИСПРАВЛЕНО: Блокировка для согласованности индексов при SoftDelete
|
|||
|
|
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"` // Максимальное количество документов (0 = безлимит)
|
|||
|
|
ValidateSchema bool `msgpack:"validate_schema"` // Валидировать схему документов
|
|||
|
|
AutoIndexID bool `msgpack:"auto_index_id"` // Автоматически индексировать поле _id
|
|||
|
|
TTLSeconds int `msgpack:"ttl_seconds"` // Время жизни документов (0 = бессрочно)
|
|||
|
|
SoftDelete bool `msgpack:"soft_delete"` // Мягкое удаление документов
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Index представляет индекс для ускорения поиска.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: поле data теперь *SkipList (lock-free skip list) вместо sync.Map.
|
|||
|
|
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 документа (lock-free skip list)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Constraints представляет ограничения на коллекцию (lock-free версия).
|
|||
|
|
type Constraints struct {
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
|
|||
|
|
// Атомарные указатели для lock-free доступа к данным ограничений
|
|||
|
|
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"` // ADD, REMOVE, MODIFY
|
|||
|
|
ConstraintType string `msgpack:"constraint_type"` // required, unique, min, max, enum, regex
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
// Атомарные указатели для lock-free доступа к ACL
|
|||
|
|
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"` // GRANT, REVOKE, SET
|
|||
|
|
Role string `msgpack:"role"`
|
|||
|
|
Permission string `msgpack:"permission"` // read, write, delete, admin
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadRequiredFields загружает карту обязательных полей (lock-free).
|
|||
|
|
func (cons *Constraints) loadRequiredFields() map[string]bool {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.requiredFieldsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeRequiredFields сохраняет карту обязательных полей (lock-free).
|
|||
|
|
func (cons *Constraints) storeRequiredFields(newMap map[string]bool) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.requiredFieldsPtr.Store(newMap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadUniqueFields загружает карту уникальных полей (lock-free).
|
|||
|
|
func (cons *Constraints) loadUniqueFields() map[string]bool {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.uniqueFieldsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeUniqueFields сохраняет карту уникальных полей (lock-free).
|
|||
|
|
func (cons *Constraints) storeUniqueFields(newMap map[string]bool) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.uniqueFieldsPtr.Store(newMap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadMinValues загружает карту минимальных значений (lock-free).
|
|||
|
|
func (cons *Constraints) loadMinValues() map[string]float64 {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.minValuesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]float64)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]float64)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeMinValues сохраняет карту минимальных значений (lock-free).
|
|||
|
|
func (cons *Constraints) storeMinValues(newMap map[string]float64) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.minValuesPtr.Store(newMap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadMaxValues загружает карту максимальных значений (lock-free).
|
|||
|
|
func (cons *Constraints) loadMaxValues() map[string]float64 {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.maxValuesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]float64)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]float64)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeMaxValues сохраняет карту максимальных значений (lock-free).
|
|||
|
|
func (cons *Constraints) storeMaxValues(newMap map[string]float64) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.maxValuesPtr.Store(newMap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadPatternFields загружает карту regex паттернов (lock-free).
|
|||
|
|
func (cons *Constraints) loadPatternFields() map[string]string {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.patternFieldsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]string)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]string)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storePatternFields сохраняет карту regex паттернов (lock-free).
|
|||
|
|
func (cons *Constraints) storePatternFields(newMap map[string]string) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.patternFieldsPtr.Store(newMap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadEnumFields загружает карту enum ограничений (lock-free).
|
|||
|
|
func (cons *Constraints) loadEnumFields() map[string][]interface{} {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.enumFieldsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string][]interface{})
|
|||
|
|
}
|
|||
|
|
original := val.(map[string][]interface{})
|
|||
|
|
result := make(map[string][]interface{})
|
|||
|
|
for k, v := range original {
|
|||
|
|
copied := make([]interface{}, len(v))
|
|||
|
|
copy(copied, v)
|
|||
|
|
result[k] = copied
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeEnumFields сохраняет карту enum ограничений (lock-free).
|
|||
|
|
func (cons *Constraints) storeEnumFields(newMap map[string][]interface{}) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.enumFieldsPtr.Store(newMap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadConstraintHistory загружает историю изменений (lock-free).
|
|||
|
|
func (cons *Constraints) loadConstraintHistory() []ConstraintChange {
|
|||
|
|
cons.mu.RLock()
|
|||
|
|
defer cons.mu.RUnlock()
|
|||
|
|
val := cons.constraintHistory.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make([]ConstraintChange, 0)
|
|||
|
|
}
|
|||
|
|
return val.([]ConstraintChange)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeConstraintHistory сохраняет историю изменений (lock-free).
|
|||
|
|
func (cons *Constraints) storeConstraintHistory(history []ConstraintChange) {
|
|||
|
|
cons.mu.Lock()
|
|||
|
|
defer cons.mu.Unlock()
|
|||
|
|
cons.constraintHistory.Store(history)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewCollectionACL создаёт новый экземпляр CollectionACL с lock-free реализацией.
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadReadRoles загружает роли чтения (lock-free).
|
|||
|
|
func (acl *CollectionACL) loadReadRoles() map[string]bool {
|
|||
|
|
acl.mu.RLock()
|
|||
|
|
defer acl.mu.RUnlock()
|
|||
|
|
val := acl.readRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeReadRoles сохраняет роли чтения (lock-free).
|
|||
|
|
func (acl *CollectionACL) storeReadRoles(roles map[string]bool) {
|
|||
|
|
acl.mu.Lock()
|
|||
|
|
defer acl.mu.Unlock()
|
|||
|
|
acl.readRolesPtr.Store(roles)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadWriteRoles загружает роли записи (lock-free).
|
|||
|
|
func (acl *CollectionACL) loadWriteRoles() map[string]bool {
|
|||
|
|
acl.mu.RLock()
|
|||
|
|
defer acl.mu.RUnlock()
|
|||
|
|
val := acl.writeRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeWriteRoles сохраняет роли записи (lock-free).
|
|||
|
|
func (acl *CollectionACL) storeWriteRoles(roles map[string]bool) {
|
|||
|
|
acl.mu.Lock()
|
|||
|
|
defer acl.mu.Unlock()
|
|||
|
|
acl.writeRolesPtr.Store(roles)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadDeleteRoles загружает роли удаления (lock-free).
|
|||
|
|
func (acl *CollectionACL) loadDeleteRoles() map[string]bool {
|
|||
|
|
acl.mu.RLock()
|
|||
|
|
defer acl.mu.RUnlock()
|
|||
|
|
val := acl.deleteRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeDeleteRoles сохраняет роли удаления (lock-free).
|
|||
|
|
func (acl *CollectionACL) storeDeleteRoles(roles map[string]bool) {
|
|||
|
|
acl.mu.Lock()
|
|||
|
|
defer acl.mu.Unlock()
|
|||
|
|
acl.deleteRolesPtr.Store(roles)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadAdminRoles загружает административные роли (lock-free).
|
|||
|
|
func (acl *CollectionACL) loadAdminRoles() map[string]bool {
|
|||
|
|
acl.mu.RLock()
|
|||
|
|
defer acl.mu.RUnlock()
|
|||
|
|
val := acl.adminRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeAdminRoles сохраняет административные роли (lock-free).
|
|||
|
|
func (acl *CollectionACL) storeAdminRoles(roles map[string]bool) {
|
|||
|
|
acl.mu.Lock()
|
|||
|
|
defer acl.mu.Unlock()
|
|||
|
|
acl.adminRolesPtr.Store(roles)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadACLHistory загружает историю ACL (lock-free).
|
|||
|
|
func (acl *CollectionACL) loadACLHistory() []ACLChange {
|
|||
|
|
acl.mu.RLock()
|
|||
|
|
defer acl.mu.RUnlock()
|
|||
|
|
val := acl.aclHistory.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make([]ACLChange, 0)
|
|||
|
|
}
|
|||
|
|
return val.([]ACLChange)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// storeACLHistory сохраняет историю ACL (lock-free).
|
|||
|
|
func (acl *CollectionACL) storeACLHistory(history []ACLChange) {
|
|||
|
|
acl.mu.Lock()
|
|||
|
|
defer acl.mu.Unlock()
|
|||
|
|
acl.aclHistory.Store(history)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewCollection создаёт новую коллекцию.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: удалены поля mvccEnabled, mvccManager, MVCCEnabled,
|
|||
|
|
// MaxVersions из CollectionSettings.
|
|||
|
|
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.Enabled = true
|
|||
|
|
trigger.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
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.Enabled = false
|
|||
|
|
trigger.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
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 вставляет документ в коллекцию.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: удалена работа с MVCC. Добавлено AOF-логирование.
|
|||
|
|
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.docCount.Load() >= int64(c.metadata.Settings.MaxDocuments) {
|
|||
|
|
return fmt.Errorf("collection is full: max documents %d reached", c.metadata.Settings.MaxDocuments)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if c.metadata.Settings.ValidateSchema {
|
|||
|
|
if err := c.validateDocument(doc); err != nil {
|
|||
|
|
return fmt.Errorf("document validation failed: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := c.checkUniqueConstraints(doc); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc.CreatedAt = time.Now().UnixMilli()
|
|||
|
|
doc.UpdatedAt = doc.CreatedAt
|
|||
|
|
doc.DeletedAt = 0
|
|||
|
|
|
|||
|
|
data, err := serializer.Marshal(doc)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("failed to serialize document: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if _, loaded := c.docs.LoadOrStore(doc.ID, doc); loaded {
|
|||
|
|
return fmt.Errorf("document with id %s already exists", doc.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.updateIndexes(doc, true)
|
|||
|
|
|
|||
|
|
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())
|
|||
|
|
|
|||
|
|
// AOF-логирование (для мутаций вне batch)
|
|||
|
|
LogAOFInsert(c.dbName, c.name, doc)
|
|||
|
|
|
|||
|
|
c.executeTriggers("AFTER_INSERT", doc.ID, doc.GetFields())
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InsertFromMap создаёт и вставляет документ из map.
|
|||
|
|
func (c *Collection) InsertFromMap(fields map[string]interface{}) error {
|
|||
|
|
doc := NewDocument()
|
|||
|
|
for k, v := range fields {
|
|||
|
|
doc.SetField(k, v)
|
|||
|
|
}
|
|||
|
|
return c.Insert(doc)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Find находит документ по ID (с использованием первичного индекса).
|
|||
|
|
func (c *Collection) Find(id string) (*Document, error) {
|
|||
|
|
if val, ok := c.docs.Load(id); ok {
|
|||
|
|
doc := val.(*Document)
|
|||
|
|
|
|||
|
|
if doc.IsDeleted() && c.metadata.Settings.SoftDelete {
|
|||
|
|
return nil, fmt.Errorf("document deleted at %s", doc.GetDeletedAtStr())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if c.metadata.Settings.TTLSeconds > 0 {
|
|||
|
|
if time.Now().UnixMilli()-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
|
|||
|
|
if c.metadata.Settings.SoftDelete {
|
|||
|
|
doc.SoftDelete()
|
|||
|
|
c.docs.Store(id, doc)
|
|||
|
|
} else {
|
|||
|
|
c.Delete(id)
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("key not found")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return doc, nil
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("key not found")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FindIncludingDeleted находит документ даже если он мягко удалён.
|
|||
|
|
func (c *Collection) FindIncludingDeleted(id string) (*Document, error) {
|
|||
|
|
if val, ok := c.docs.Load(id); ok {
|
|||
|
|
return val.(*Document), nil
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("key not found")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// compareValues сравнивает два значения с учётом типа.
|
|||
|
|
func compareValues(a, b interface{}) bool {
|
|||
|
|
if a == nil && b == nil {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
if a == nil || b == nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if a == b {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
aStr := fmt.Sprintf("%v", a)
|
|||
|
|
bStr := fmt.Sprintf("%v", b)
|
|||
|
|
return aStr == bStr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FindByIndex находит документы по значению индексированного поля.
|
|||
|
|
func (c *Collection) FindByIndex(indexName string, value interface{}) ([]*Document, error) {
|
|||
|
|
idxVal, ok := c.indexes.Load(indexName)
|
|||
|
|
if !ok {
|
|||
|
|
return nil, fmt.Errorf("index not found: %s", indexName)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
index := idxVal.(*Index)
|
|||
|
|
docs := make([]*Document, 0)
|
|||
|
|
|
|||
|
|
if index.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.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: удалена работа с MVCC. Добавлено AOF-логирование.
|
|||
|
|
func (c *Collection) Update(id string, updates map[string]interface{}) error {
|
|||
|
|
val, ok := c.docs.Load(id)
|
|||
|
|
if !ok {
|
|||
|
|
return fmt.Errorf("key not found")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
oldDoc := val.(*Document)
|
|||
|
|
|
|||
|
|
if oldDoc.IsDeleted() && c.metadata.Settings.SoftDelete {
|
|||
|
|
return fmt.Errorf("cannot update deleted document")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
newDoc := oldDoc.Clone()
|
|||
|
|
if err := newDoc.Update(updates); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
newDoc.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
if err := c.constraints.ValidateDocument(newDoc); err != nil {
|
|||
|
|
return fmt.Errorf("constraint violation: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := c.checkUniqueConstraintsUpdate(oldDoc, newDoc); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.executeTriggers("BEFORE_UPDATE", id, newDoc.GetFields())
|
|||
|
|
|
|||
|
|
c.indexMu.Lock()
|
|||
|
|
c.removeFromIndexes(oldDoc)
|
|||
|
|
c.addToIndexes(newDoc)
|
|||
|
|
c.indexMu.Unlock()
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
// AOF-логирование
|
|||
|
|
LogAOFUpdate(c.dbName, c.name, id, updates)
|
|||
|
|
|
|||
|
|
c.executeTriggers("AFTER_UPDATE", id, newDoc.GetFields())
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Delete удаляет документ по ID.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: удалена работа с MVCC. Добавлено AOF-логирование.
|
|||
|
|
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())
|
|||
|
|
|
|||
|
|
if c.metadata.Settings.SoftDelete {
|
|||
|
|
c.indexMu.Lock()
|
|||
|
|
|
|||
|
|
doc.SoftDelete()
|
|||
|
|
c.docs.Store(id, doc)
|
|||
|
|
c.removeFromIndexes(doc)
|
|||
|
|
|
|||
|
|
c.indexMu.Unlock()
|
|||
|
|
|
|||
|
|
AuditDocumentOperation("SOFT_DELETE", c.dbName, c.name, id, map[string]interface{}{
|
|||
|
|
"deleted_at": doc.DeletedAt,
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
c.indexMu.Lock()
|
|||
|
|
|
|||
|
|
c.removeFromIndexes(doc)
|
|||
|
|
c.docs.Delete(id)
|
|||
|
|
|
|||
|
|
c.indexMu.Unlock()
|
|||
|
|
|
|||
|
|
AuditDocumentOperation("DELETE", c.dbName, c.name, id, nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AOF-логирование
|
|||
|
|
LogAOFDelete(c.dbName, c.name, id)
|
|||
|
|
|
|||
|
|
c.docCount.Add(-1)
|
|||
|
|
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 выполняет физическое удаление мягко удалённого документа.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: удалена работа с MVCC. Добавлено AOF-логирование.
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
// AOF-логирование (физическое удаление логируем как delete)
|
|||
|
|
LogAOFDelete(c.dbName, c.name, id)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RestoreDeleted восстанавливает мягко удалённый документ.
|
|||
|
|
//
|
|||
|
|
// ИЗМЕНЕНО: удалена работа с MVCC. Добавлено AOF-логирование.
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
// AOF-логирование
|
|||
|
|
LogAOFRestore(c.dbName, c.name, id)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// removeFromIndexes удаляет документ из всех индексов.
|
|||
|
|
func (c *Collection) removeFromIndexes(doc *Document) {
|
|||
|
|
c.indexes.Range(func(key, value interface{}) bool {
|
|||
|
|
index := value.(*Index)
|
|||
|
|
if index.data == nil {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
indexValue := c.extractIndexValue(doc, index.Fields)
|
|||
|
|
index.data.Delete(indexValue)
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// addToIndexes добавляет документ во все индексы.
|
|||
|
|
func (c *Collection) addToIndexes(doc *Document) {
|
|||
|
|
c.indexes.Range(func(key, value interface{}) bool {
|
|||
|
|
index := value.(*Index)
|
|||
|
|
if index.data == nil {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
indexValue := c.extractIndexValue(doc, index.Fields)
|
|||
|
|
index.data.Insert(indexValue, doc.ID)
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateIndex создаёт новый индекс на коллекции.
|
|||
|
|
func (c *Collection) CreateIndex(name string, fields []string, unique bool) error {
|
|||
|
|
c.mu.Lock()
|
|||
|
|
defer c.mu.Unlock()
|
|||
|
|
|
|||
|
|
if _, exists := c.indexes.Load(name); exists {
|
|||
|
|
return fmt.Errorf("index %s already exists", name)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
now := time.Now().UnixMilli()
|
|||
|
|
index := &Index{
|
|||
|
|
Name: name,
|
|||
|
|
Fields: fields,
|
|||
|
|
Unique: unique,
|
|||
|
|
CreatedAt: now,
|
|||
|
|
UpdatedAt: now,
|
|||
|
|
data: NewSkipList(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var buildErr error
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
doc := value.(*Document)
|
|||
|
|
if doc.IsDeleted() && c.metadata.Settings.SoftDelete {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
indexValue := c.extractIndexValue(doc, fields)
|
|||
|
|
if unique {
|
|||
|
|
if _, exists := index.data.Search(indexValue); exists {
|
|||
|
|
buildErr = fmt.Errorf("duplicate key for index %s: %v", name, indexValue)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
index.data.Insert(indexValue, doc.ID)
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if buildErr != nil {
|
|||
|
|
return buildErr
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.indexes.Store(name, index)
|
|||
|
|
c.metadata.IndexCount++
|
|||
|
|
|
|||
|
|
AuditIndexOperation("CREATE_INDEX", c.dbName, c.name, name, fields, unique)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DropIndex удаляет индекс.
|
|||
|
|
func (c *Collection) DropIndex(name string) error {
|
|||
|
|
if _, exists := c.indexes.LoadAndDelete(name); !exists {
|
|||
|
|
return fmt.Errorf("index not found: %s", name)
|
|||
|
|
}
|
|||
|
|
c.metadata.IndexCount--
|
|||
|
|
|
|||
|
|
AuditIndexOperation("DROP_INDEX", c.dbName, c.name, name, nil, false)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetIndexes возвращает список всех индексов.
|
|||
|
|
func (c *Collection) GetIndexes() []string {
|
|||
|
|
names := make([]string, 0)
|
|||
|
|
c.indexes.Range(func(key, value interface{}) bool {
|
|||
|
|
names = append(names, key.(string))
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return names
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetIndexesInfo возвращает подробную информацию об индексах.
|
|||
|
|
func (c *Collection) GetIndexesInfo() []map[string]interface{} {
|
|||
|
|
indexes := make([]map[string]interface{}, 0)
|
|||
|
|
c.indexes.Range(func(key, value interface{}) bool {
|
|||
|
|
idx := value.(*Index)
|
|||
|
|
indexes = append(indexes, map[string]interface{}{
|
|||
|
|
"name": idx.Name,
|
|||
|
|
"fields": idx.Fields,
|
|||
|
|
"unique": idx.Unique,
|
|||
|
|
"created_at": idx.CreatedAt,
|
|||
|
|
"updated_at": idx.UpdatedAt,
|
|||
|
|
})
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return indexes
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// extractIndexValue извлекает значение из документа для индексации.
|
|||
|
|
func (c *Collection) extractIndexValue(doc *Document, fields []string) interface{} {
|
|||
|
|
if len(fields) == 1 {
|
|||
|
|
val, _ := doc.GetField(fields[0])
|
|||
|
|
return val
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
parts := make([]string, 0, len(fields))
|
|||
|
|
for _, field := range fields {
|
|||
|
|
if val, err := doc.GetField(field); err == nil {
|
|||
|
|
parts = append(parts, fmt.Sprintf("%v", val))
|
|||
|
|
} else {
|
|||
|
|
parts = append(parts, "NULL")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return strings.Join(parts, "|")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// updateIndexes обновляет индексы для документа.
|
|||
|
|
func (c *Collection) updateIndexes(doc *Document, add bool) {
|
|||
|
|
if add {
|
|||
|
|
c.addToIndexes(doc)
|
|||
|
|
} else {
|
|||
|
|
c.removeFromIndexes(doc)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// checkUniqueConstraints проверяет уникальные индексы перед вставкой.
|
|||
|
|
func (c *Collection) checkUniqueConstraints(doc *Document) error {
|
|||
|
|
var errs []string
|
|||
|
|
|
|||
|
|
c.indexes.Range(func(key, value interface{}) bool {
|
|||
|
|
index := value.(*Index)
|
|||
|
|
if index.Unique && 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 проверяет уникальность при обновлении.
|
|||
|
|
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() {
|
|||
|
|
ticker := time.NewTicker(time.Duration(c.metadata.Settings.TTLSeconds/2) * time.Second)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for range ticker.C {
|
|||
|
|
now := time.Now().UnixMilli()
|
|||
|
|
toDelete := make([]string, 0)
|
|||
|
|
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
doc := value.(*Document)
|
|||
|
|
if !doc.IsDeleted() && now-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
|
|||
|
|
toDelete = append(toDelete, doc.ID)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
for _, id := range toDelete {
|
|||
|
|
c.Delete(id)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Count возвращает количество активных документов в коллекции.
|
|||
|
|
func (c *Collection) Count() int64 {
|
|||
|
|
if c.metadata.Settings.SoftDelete {
|
|||
|
|
count := int64(0)
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
doc := value.(*Document)
|
|||
|
|
if !doc.IsDeleted() {
|
|||
|
|
count++
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return count
|
|||
|
|
}
|
|||
|
|
return c.docCount.Load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CountAll возвращает общее количество документов (включая мягко удалённые).
|
|||
|
|
func (c *Collection) CountAll() int64 {
|
|||
|
|
return c.docCount.Load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CountDeleted возвращает количество мягко удалённых документов.
|
|||
|
|
func (c *Collection) CountDeleted() int64 {
|
|||
|
|
if !c.metadata.Settings.SoftDelete {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
count := int64(0)
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
doc := value.(*Document)
|
|||
|
|
if doc.IsDeleted() {
|
|||
|
|
count++
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return count
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Size возвращает размер коллекции в байтах.
|
|||
|
|
func (c *Collection) Size() int64 {
|
|||
|
|
return c.sizeBytes.Load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetAllDocuments возвращает все активные документы коллекции.
|
|||
|
|
func (c *Collection) GetAllDocuments() []*Document {
|
|||
|
|
docs := make([]*Document, 0, c.docCount.Load())
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
doc := value.(*Document)
|
|||
|
|
if !c.metadata.Settings.SoftDelete || !doc.IsDeleted() {
|
|||
|
|
docs = append(docs, doc)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return docs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetAllDocumentsIncludingDeleted возвращает все документы (включая мягко удалённые).
|
|||
|
|
func (c *Collection) GetAllDocumentsIncludingDeleted() []*Document {
|
|||
|
|
docs := make([]*Document, 0, c.docCount.Load())
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
docs = append(docs, value.(*Document))
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return docs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FindByFilter находит документы по произвольному фильтру.
|
|||
|
|
func (c *Collection) FindByFilter(filter func(*Document) bool) []*Document {
|
|||
|
|
results := make([]*Document, 0)
|
|||
|
|
c.docs.Range(func(key, value interface{}) bool {
|
|||
|
|
doc := value.(*Document)
|
|||
|
|
if (!c.metadata.Settings.SoftDelete || !doc.IsDeleted()) && filter(doc) {
|
|||
|
|
results = append(results, doc)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
return results
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetMetadata возвращает метаданные коллекции.
|
|||
|
|
func (c *Collection) GetMetadata() *CollectionMetadata {
|
|||
|
|
c.mu.RLock()
|
|||
|
|
defer c.mu.RUnlock()
|
|||
|
|
return c.metadata
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetTimestamps возвращает временные метки коллекции.
|
|||
|
|
func (c *Collection) GetTimestamps() map[string]int64 {
|
|||
|
|
c.mu.RLock()
|
|||
|
|
defer c.mu.RUnlock()
|
|||
|
|
return map[string]int64{
|
|||
|
|
"created_at": c.metadata.CreatedAt,
|
|||
|
|
"updated_at": c.metadata.UpdatedAt,
|
|||
|
|
"deleted_at": c.metadata.DeletedAt,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Drop удаляет все документы из коллекции.
|
|||
|
|
func (c *Collection) Drop() error {
|
|||
|
|
c.mu.Lock()
|
|||
|
|
defer c.mu.Unlock()
|
|||
|
|
|
|||
|
|
c.docs = sync.Map{}
|
|||
|
|
c.indexes = sync.Map{}
|
|||
|
|
c.triggers = sync.Map{}
|
|||
|
|
|
|||
|
|
if c.metadata.Settings.AutoIndexID {
|
|||
|
|
c.CreateIndex("_id_", []string{"_id"}, true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.docCount.Store(0)
|
|||
|
|
c.sizeBytes.Store(0)
|
|||
|
|
c.metadata.DocumentCount = 0
|
|||
|
|
c.metadata.SizeBytes = 0
|
|||
|
|
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
|||
|
|
|
|||
|
|
AuditCollectionOperation("DROP", c.dbName, c.name, nil)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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 добавляет regexp паттерн.
|
|||
|
|
func (c *Collection) AddRegexConstraint(field string, pattern string) {
|
|||
|
|
c.constraints.mu.Lock()
|
|||
|
|
oldMap := c.constraints.loadPatternFieldsUnsafe()
|
|||
|
|
oldVal := oldMap[field]
|
|||
|
|
|
|||
|
|
newMap := make(map[string]string)
|
|||
|
|
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{})
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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 удаляет regexp паттерн.
|
|||
|
|
func (c *Collection) RemoveRegexConstraint(field string) {
|
|||
|
|
c.constraints.mu.Lock()
|
|||
|
|
oldMap := c.constraints.loadPatternFieldsUnsafe()
|
|||
|
|
oldVal := oldMap[field]
|
|||
|
|
|
|||
|
|
newMap := make(map[string]string)
|
|||
|
|
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{})
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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{})
|
|||
|
|
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)
|
|||
|
|
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 проверяет документ на соответствие ограничениям.
|
|||
|
|
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 != "" && !strings.Contains(strVal, pattern) {
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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)
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// ВНУТРЕННИЕ МЕТОДЫ БЕЗ БЛОКИРОВКИ
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// 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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadPatternFieldsUnsafe загружает карту regex паттернов без блокировки.
|
|||
|
|
func (cons *Constraints) loadPatternFieldsUnsafe() map[string]string {
|
|||
|
|
val := cons.patternFieldsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]string)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]string)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadEnumFieldsUnsafe загружает карту enum ограничений без блокировки.
|
|||
|
|
func (cons *Constraints) loadEnumFieldsUnsafe() map[string][]interface{} {
|
|||
|
|
val := cons.enumFieldsPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string][]interface{})
|
|||
|
|
}
|
|||
|
|
return val.(map[string][]interface{})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadReadRolesUnsafe загружает роли чтения без блокировки.
|
|||
|
|
func (acl *CollectionACL) loadReadRolesUnsafe() map[string]bool {
|
|||
|
|
val := acl.readRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadWriteRolesUnsafe загружает роли записи без блокировки.
|
|||
|
|
func (acl *CollectionACL) loadWriteRolesUnsafe() map[string]bool {
|
|||
|
|
val := acl.writeRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadDeleteRolesUnsafe загружает роли удаления без блокировки.
|
|||
|
|
func (acl *CollectionACL) loadDeleteRolesUnsafe() map[string]bool {
|
|||
|
|
val := acl.deleteRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadAdminRolesUnsafe загружает административные роли без блокировки.
|
|||
|
|
func (acl *CollectionACL) loadAdminRolesUnsafe() map[string]bool {
|
|||
|
|
val := acl.adminRolesPtr.Load()
|
|||
|
|
if val == nil {
|
|||
|
|
return make(map[string]bool)
|
|||
|
|
}
|
|||
|
|
return val.(map[string]bool)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// toFloat64 конвертирует interface{} в float64.
|
|||
|
|
func toFloat64(val interface{}) (float64, bool) {
|
|||
|
|
switch v := val.(type) {
|
|||
|
|
case int:
|
|||
|
|
return float64(v), true
|
|||
|
|
case int64:
|
|||
|
|
return float64(v), true
|
|||
|
|
case float64:
|
|||
|
|
return v, true
|
|||
|
|
case float32:
|
|||
|
|
return float64(v), true
|
|||
|
|
default:
|
|||
|
|
return 0, false
|
|||
|
|
}
|
|||
|
|
}
|