From 38dcdedf53235bdd9da8df8e6ff78c1898bc6b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=A1?= =?UTF-8?q?=D0=B0=D1=84=D1=80=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Fri, 12 Jun 2026 22:53:49 +0000 Subject: [PATCH] Upload files to "internal/storage" --- internal/storage/collection.go | 1653 ++++++++++++++++++++++++++++++++ internal/storage/document.go | 613 ++++++++++++ 2 files changed, 2266 insertions(+) create mode 100644 internal/storage/collection.go create mode 100644 internal/storage/document.go diff --git a/internal/storage/collection.go b/internal/storage/collection.go new file mode 100644 index 0000000..f588995 --- /dev/null +++ b/internal/storage/collection.go @@ -0,0 +1,1653 @@ +/* + * 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 переведены на атомарные операции. + +package storage + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + "strings" + + "futriis/internal/serializer" +) + +// Collection представляет коллекцию документов (аналог таблицы) +type Collection struct { + dbName string // имя базы данных (добавлено) + name string // имя коллекции + docs sync.Map // map[string]*Document - wait-free хранилище документов + indexes sync.Map // map[string]*Index - индексы для быстрого поиска (lock-free) + metadata *CollectionMetadata // Метаданные коллекции + docCount atomic.Int64 // Атомарный счётчик документов + sizeBytes atomic.Int64 // Атомарный размер коллекции в байтах + mu sync.RWMutex // Для операций, изменяющих структуру коллекции + constraints *Constraints // Ограничения коллекции (lock-free) + acl *CollectionACL // ACL для коллекции (lock-free) +} + +// 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 представляет индекс для ускорения поиска (хранится отдельно от документов) +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 sync.Map // map[interface{}]string - значение индекса -> ID документа (lock-free) +} + +// Constraints представляет ограничения на коллекцию (lock-free версия) +type Constraints struct { + // Атомарные указатели для 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 { + // Атомарные указатели для 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(), + } + + // Инициализация атомарных указателей с пустыми map + 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 { + 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.requiredFieldsPtr.Store(newMap) +} + +// loadUniqueFields загружает карту уникальных полей (lock-free) +func (cons *Constraints) loadUniqueFields() map[string]bool { + 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.uniqueFieldsPtr.Store(newMap) +} + +// loadMinValues загружает карту минимальных значений (lock-free) +func (cons *Constraints) loadMinValues() map[string]float64 { + 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.minValuesPtr.Store(newMap) +} + +// loadMaxValues загружает карту максимальных значений (lock-free) +func (cons *Constraints) loadMaxValues() map[string]float64 { + 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.maxValuesPtr.Store(newMap) +} + +// loadPatternFields загружает карту regex паттернов (lock-free) +func (cons *Constraints) loadPatternFields() map[string]string { + 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.patternFieldsPtr.Store(newMap) +} + +// loadEnumFields загружает карту enum ограничений (lock-free) +func (cons *Constraints) loadEnumFields() map[string][]interface{} { + 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.enumFieldsPtr.Store(newMap) +} + +// loadConstraintHistory загружает историю изменений (lock-free) +func (cons *Constraints) loadConstraintHistory() []ConstraintChange { + val := cons.constraintHistory.Load() + if val == nil { + return make([]ConstraintChange, 0) + } + return val.([]ConstraintChange) +} + +// storeConstraintHistory сохраняет историю изменений (lock-free) +func (cons *Constraints) storeConstraintHistory(history []ConstraintChange) { + 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 { + 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.readRolesPtr.Store(roles) +} + +// loadWriteRoles загружает роли записи (lock-free) +func (acl *CollectionACL) loadWriteRoles() map[string]bool { + 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.writeRolesPtr.Store(roles) +} + +// loadDeleteRoles загружает роли удаления (lock-free) +func (acl *CollectionACL) loadDeleteRoles() map[string]bool { + 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.deleteRolesPtr.Store(roles) +} + +// loadAdminRoles загружает административные роли (lock-free) +func (acl *CollectionACL) loadAdminRoles() map[string]bool { + 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.adminRolesPtr.Store(roles) +} + +// loadACLHistory загружает историю ACL (lock-free) +func (acl *CollectionACL) loadACLHistory() []ACLChange { + 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.aclHistory.Store(history) +} + +// CheckPermission проверяет наличие разрешения у роли (lock-free) +func (acl *CollectionACL) CheckPermission(role, operation string) bool { + // Администратор имеет все права (lock-free чтение) + adminRoles := acl.loadAdminRoles() + if adminRoles[role] { + return true + } + + switch operation { + case "read": + readRoles := acl.loadReadRoles() + return readRoles[role] + case "write": + writeRoles := acl.loadWriteRoles() + return writeRoles[role] + case "delete": + deleteRoles := acl.loadDeleteRoles() + return deleteRoles[role] + default: + return false + } +} + +// 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(), + } + + // Автоматически создаём первичный индекс по _id + if settings.AutoIndexID { + coll.CreateIndex("_id_", []string{"_id"}, true) + } + + // Запускаем фоновую задачу для удаления просроченных документов + if settings.TTLSeconds > 0 { + go coll.ttlCleanupLoop() + } + + // Аудит создания коллекции + AuditCollectionOperation("CREATE", dbName, name, settings) + + return coll +} + +// Name возвращает имя коллекции +func (c *Collection) Name() string { + return c.name +} + +// DBName возвращает имя базы данных +func (c *Collection) DBName() string { + return c.dbName +} + +// Insert вставляет документ в коллекцию (wait-free) +func (c *Collection) Insert(doc *Document) error { + // Проверка ограничений + if err := c.constraints.ValidateDocument(doc); err != nil { + return fmt.Errorf("constraint violation: %v", err) + } + + // Проверка на максимальное количество документов + 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) + } + + // Обновление индексов (wait-free) + c.updateIndexes(doc, true) + + // Обновление метаданных + c.docCount.Add(1) + c.sizeBytes.Add(int64(len(data))) + c.metadata.DocumentCount = c.docCount.Load() + c.metadata.SizeBytes = c.sizeBytes.Load() + c.metadata.UpdatedAt = time.Now().UnixMilli() + + // Аудит вставки документа + AuditDocumentOperation("INSERT", c.dbName, c.name, 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()) + } + + // Проверяем, не истёк ли TTL + 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.Unique { + // Уникальный индекс возвращает один документ + if docID, ok := index.data.Load(value); ok { + if doc, err := c.Find(docID.(string)); err == nil { + docs = append(docs, doc) + } + } + } else { + // Для неуникального индекса используем Range с правильным сравнением + index.data.Range(func(key, val interface{}) bool { + if compareValues(key, value) { + if doc, err := c.Find(val.(string)); err == nil { + docs = append(docs, doc) + } + } + return true + }) + } + + return docs, nil +} + +// FindByIndexPrefix находит документы по префиксу индекса (для строковых полей) +func (c *Collection) FindByIndexPrefix(indexName string, prefix string) ([]*Document, error) { + idxVal, ok := c.indexes.Load(indexName) + if !ok { + return nil, fmt.Errorf("index not found: %s", indexName) + } + + index := idxVal.(*Index) + docs := make([]*Document, 0) + + index.data.Range(func(key, val interface{}) bool { + if keyStr, ok := key.(string); ok { + if strings.HasPrefix(keyStr, prefix) { + if doc, err := c.Find(val.(string)); err == nil { + docs = append(docs, doc) + } + } + } + return true + }) + + return docs, nil +} + +// Update обновляет документ по ID +func (c *Collection) Update(id string, updates map[string]interface{}) error { + val, ok := c.docs.Load(id) + if !ok { + return fmt.Errorf("key not found") + } + + oldDoc := val.(*Document) + + // Проверяем, не удалён ли документ мягко + 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.removeFromIndexes(oldDoc) + c.addToIndexes(newDoc) + + // Сохраняем обновлённый документ + c.docs.Store(id, newDoc) + + c.metadata.UpdatedAt = time.Now().UnixMilli() + + // Аудит обновления документа + AuditDocumentOperation("UPDATE", c.dbName, c.name, id, updates) + + return nil +} + +// Delete удаляет документ по ID +func (c *Collection) Delete(id string) error { + val, ok := c.docs.Load(id) + if !ok { + return fmt.Errorf("key not found") + } + + doc := val.(*Document) + + 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) + + // Аудит физического удаления + AuditDocumentOperation("DELETE", c.dbName, c.name, id, nil) + } + + // Обновляем метаданные + c.docCount.Add(-1) + c.metadata.DocumentCount = c.docCount.Load() + c.metadata.UpdatedAt = time.Now().UnixMilli() + + 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.removeFromIndexes(doc) + + // Удаляем документ + c.docs.Delete(id) + + // Обновляем метаданные + c.docCount.Add(-1) + c.metadata.DocumentCount = c.docCount.Load() + c.metadata.UpdatedAt = time.Now().UnixMilli() + + // Аудит физического удаления + 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") + } + + doc.Restore() + + // Восстанавливаем индексы + c.addToIndexes(doc) + + c.docs.Store(id, doc) + c.metadata.UpdatedAt = time.Now().UnixMilli() + + // Аудит восстановления + AuditDocumentOperation("RESTORE", c.dbName, c.name, id, nil) + + return nil +} + +// removeFromIndexes удаляет документ из всех индексов (wait-free) +func (c *Collection) removeFromIndexes(doc *Document) { + c.indexes.Range(func(key, value interface{}) bool { + index := value.(*Index) + indexValue := c.extractIndexValue(doc, index.Fields) + index.data.Delete(indexValue) + return true + }) +} + +// addToIndexes добавляет документ во все индексы (wait-free) +func (c *Collection) addToIndexes(doc *Document) { + c.indexes.Range(func(key, value interface{}) bool { + index := value.(*Index) + indexValue := c.extractIndexValue(doc, index.Fields) + if index.Unique { + index.data.LoadOrStore(indexValue, doc.ID) + } else { + index.data.Store(indexValue, doc.ID) + } + return true + }) +} + +// CreateIndex создаёт новый индекс на коллекции +func (c *Collection) CreateIndex(name string, fields []string, unique bool) error { + c.mu.Lock() + defer c.mu.Unlock() + + if _, exists := c.indexes.Load(name); exists { + return fmt.Errorf("index %s already exists", name) + } + + now := time.Now().UnixMilli() + index := &Index{ + Name: name, + Fields: fields, + Unique: unique, + CreatedAt: now, + UpdatedAt: now, + } + + // Строим индекс на существующих документах (wait-free) + 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 _, loaded := index.data.LoadOrStore(indexValue, doc.ID); loaded { + // Найден дубликат - откатываем создание индекса + c.mu.Unlock() + return false + } + } else { + index.data.Store(indexValue, doc.ID) + } + return true + }) + + c.indexes.Store(name, index) + c.metadata.IndexCount++ + + // Аудит создания индекса + AuditCollectionOperation("CREATE_INDEX", c.dbName, c.name, map[string]interface{}{ + "index_name": name, + "fields": fields, + "unique": 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-- + + // Аудит удаления индекса + AuditCollectionOperation("DROP_INDEX", c.dbName, c.name, map[string]interface{}{ + "index_name": name, + }) + + return nil +} + +// GetIndexes возвращает список всех индексов +func (c *Collection) GetIndexes() []string { + names := make([]string, 0) + c.indexes.Range(func(key, value interface{}) bool { + names = append(names, key.(string)) + return true + }) + return names +} + +// GetIndexesInfo возвращает подробную информацию об индексах (для API) +func (c *Collection) GetIndexesInfo() []map[string]interface{} { + indexes := make([]map[string]interface{}, 0) + c.indexes.Range(func(key, value interface{}) bool { + idx := value.(*Index) + indexes = append(indexes, map[string]interface{}{ + "name": idx.Name, + "fields": idx.Fields, + "unique": idx.Unique, + "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 { + indexValue := c.extractIndexValue(doc, index.Fields) + if _, exists := index.data.Load(indexValue); exists { + errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, indexValue)) + } + } + return true + }) + + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "; ")) + } + return nil +} + +// checkUniqueConstraintsUpdate проверяет уникальность при обновлении +func (c *Collection) checkUniqueConstraintsUpdate(oldDoc, newDoc *Document) error { + var errs []string + + c.indexes.Range(func(key, value interface{}) bool { + index := value.(*Index) + if index.Unique { + oldValue := c.extractIndexValue(oldDoc, index.Fields) + newValue := c.extractIndexValue(newDoc, index.Fields) + + if fmt.Sprintf("%v", oldValue) != fmt.Sprintf("%v", newValue) { + if _, exists := index.data.Load(newValue); exists { + errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, newValue)) + } + } + } + return true + }) + + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "; ")) + } + return nil +} + +// validateDocument валидирует документ согласно схеме коллекции +func (c *Collection) validateDocument(doc *Document) error { + if doc.ID == "" { + return fmt.Errorf("document must have _id field") + } + return nil +} + +// ttlCleanupLoop периодически удаляет просроченные документы +func (c *Collection) ttlCleanupLoop() { + ticker := time.NewTicker(time.Duration(c.metadata.Settings.TTLSeconds/2) * time.Second) + defer ticker.Stop() + + for range ticker.C { + now := time.Now().UnixMilli() + toDelete := make([]string, 0) + + c.docs.Range(func(key, value interface{}) bool { + doc := value.(*Document) + if !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{} + + 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 логирует изменение ограничения (lock-free) +func (c *Collection) logConstraintChange(action, constraintType, field string, oldVal, newVal interface{}) { + nowMs, nowStr := GetCurrentTimestamp() + change := ConstraintChange{ + Timestamp: nowMs, + TimestampStr: nowStr, + Action: action, + ConstraintType: constraintType, + Field: field, + OldValue: oldVal, + NewValue: newVal, + } + + // Lock-free обновление истории + history := c.constraints.loadConstraintHistory() + newHistory := make([]ConstraintChange, len(history)+1) + copy(newHistory, history) + newHistory[len(history)] = change + c.constraints.storeConstraintHistory(newHistory) + c.constraints.updatedAt.Store(nowMs) + + // Аудит изменения ограничения + 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 (lock-free) +func (c *Collection) logACLChange(action, role, permission string, granted bool) { + nowMs, nowStr := GetCurrentTimestamp() + change := ACLChange{ + Timestamp: nowMs, + TimestampStr: nowStr, + Action: action, + Role: role, + Permission: permission, + Granted: granted, + } + + // Lock-free обновление истории + history := c.acl.loadACLHistory() + newHistory := make([]ACLChange, len(history)+1) + copy(newHistory, history) + newHistory[len(history)] = change + c.acl.storeACLHistory(newHistory) + c.acl.updatedAt.Store(nowMs) + + // Аудит изменения ACL + LogAudit("ACL_"+action, "ACL", fmt.Sprintf("%s.%s", c.dbName, c.name), map[string]interface{}{ + "role": role, + "permission": permission, + "granted": granted, + }) +} + +// ========== Constraints Methods (lock-free) ========== + +// AddRequiredField добавляет обязательное поле (lock-free) +func (c *Collection) AddRequiredField(field string) { + oldMap := c.constraints.loadRequiredFields() + oldVal := oldMap[field] + + newMap := make(map[string]bool) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = true + + c.constraints.storeRequiredFields(newMap) + c.logConstraintChange("ADD", "required", field, oldVal, true) +} + +// AddUniqueConstraint добавляет ограничение уникальности (lock-free) +func (c *Collection) AddUniqueConstraint(field string) { + oldMap := c.constraints.loadUniqueFields() + oldVal := oldMap[field] + + newMap := make(map[string]bool) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = true + + c.constraints.storeUniqueFields(newMap) + c.logConstraintChange("ADD", "unique", field, oldVal, true) + // Также создаём уникальный индекс + c.CreateIndex("unique_"+field, []string{field}, true) +} + +// AddMinConstraint добавляет минимальное значение (lock-free) +func (c *Collection) AddMinConstraint(field string, min float64) { + oldMap := c.constraints.loadMinValues() + oldVal := oldMap[field] + + newMap := make(map[string]float64) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = min + + c.constraints.storeMinValues(newMap) + c.logConstraintChange("ADD", "min", field, oldVal, min) +} + +// AddMaxConstraint добавляет максимальное значение (lock-free) +func (c *Collection) AddMaxConstraint(field string, max float64) { + oldMap := c.constraints.loadMaxValues() + oldVal := oldMap[field] + + newMap := make(map[string]float64) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = max + + c.constraints.storeMaxValues(newMap) + c.logConstraintChange("ADD", "max", field, oldVal, max) +} + +// AddRegexConstraint добавляет regexp паттерн (lock-free) +func (c *Collection) AddRegexConstraint(field string, pattern string) { + oldMap := c.constraints.loadPatternFields() + oldVal := oldMap[field] + + newMap := make(map[string]string) + for k, v := range oldMap { + newMap[k] = v + } + newMap[field] = pattern + + c.constraints.storePatternFields(newMap) + c.logConstraintChange("ADD", "regex", field, oldVal, pattern) +} + +// AddEnumConstraint добавляет допустимые значения (lock-free) +func (c *Collection) AddEnumConstraint(field string, values []interface{}) { + oldMap := c.constraints.loadEnumFields() + 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.storeEnumFields(newMap) + c.logConstraintChange("ADD", "enum", field, oldVal, values) +} + +// RemoveRequiredField удаляет обязательное поле (lock-free) +func (c *Collection) RemoveRequiredField(field string) { + oldMap := c.constraints.loadRequiredFields() + oldVal := oldMap[field] + + newMap := make(map[string]bool) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + + c.constraints.storeRequiredFields(newMap) + c.logConstraintChange("REMOVE", "required", field, oldVal, nil) +} + +// RemoveUniqueConstraint удаляет ограничение уникальности (lock-free) +func (c *Collection) RemoveUniqueConstraint(field string) { + oldMap := c.constraints.loadUniqueFields() + oldVal := oldMap[field] + + newMap := make(map[string]bool) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + + c.constraints.storeUniqueFields(newMap) + c.logConstraintChange("REMOVE", "unique", field, oldVal, nil) + // Также удаляем индекс + c.DropIndex("unique_" + field) +} + +// RemoveMinConstraint удаляет минимальное значение (lock-free) +func (c *Collection) RemoveMinConstraint(field string) { + oldMap := c.constraints.loadMinValues() + oldVal := oldMap[field] + + newMap := make(map[string]float64) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + + c.constraints.storeMinValues(newMap) + c.logConstraintChange("REMOVE", "min", field, oldVal, nil) +} + +// RemoveMaxConstraint удаляет максимальное значение (lock-free) +func (c *Collection) RemoveMaxConstraint(field string) { + oldMap := c.constraints.loadMaxValues() + oldVal := oldMap[field] + + newMap := make(map[string]float64) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + + c.constraints.storeMaxValues(newMap) + c.logConstraintChange("REMOVE", "max", field, oldVal, nil) +} + +// RemoveRegexConstraint удаляет regexp паттерн (lock-free) +func (c *Collection) RemoveRegexConstraint(field string) { + oldMap := c.constraints.loadPatternFields() + oldVal := oldMap[field] + + newMap := make(map[string]string) + for k, v := range oldMap { + if k != field { + newMap[k] = v + } + } + + c.constraints.storePatternFields(newMap) + c.logConstraintChange("REMOVE", "regex", field, oldVal, nil) +} + +// RemoveEnumConstraint удаляет допустимые значения (lock-free) +func (c *Collection) RemoveEnumConstraint(field string) { + oldMap := c.constraints.loadEnumFields() + 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.storeEnumFields(newMap) + c.logConstraintChange("REMOVE", "enum", field, oldVal, nil) +} + +// GetRequiredFields возвращает список обязательных полей (lock-free) +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 возвращает список уникальных полей (lock-free) +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 возвращает карту минимальных значений (lock-free) +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 возвращает карту максимальных значений (lock-free) +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 ограничений (lock-free) +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 паттернов (lock-free) +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 возвращает все ограничения коллекции (для API) +func (c *Collection) GetConstraints() map[string]interface{} { + return map[string]interface{}{ + "required_fields": c.GetRequiredFields(), + "unique_fields": c.GetUniqueConstraints(), + "min_values": c.GetMinConstraints(), + "max_values": c.GetMaxConstraints(), + "enum_values": c.GetEnumConstraints(), + "regex_patterns": c.GetRegexConstraints(), + } +} + +// GetConstraintTimestamps возвращает временные метки ограничений (lock-free) +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 возвращает историю изменений ограничений (lock-free) +func (c *Collection) GetConstraintHistory() []ConstraintChange { + history := c.constraints.loadConstraintHistory() + result := make([]ConstraintChange, len(history)) + copy(result, history) + return result +} + +// ValidateDocument проверяет документ на соответствие ограничениям (lock-free) +func (cons *Constraints) ValidateDocument(doc *Document) error { + // Проверка обязательных полей (lock-free чтение) + requiredFields := cons.loadRequiredFields() + for field := range requiredFields { + if !doc.HasField(field) { + return fmt.Errorf("required field '%s' is missing", field) + } + } + + // Проверка числовых ограничений (lock-free чтение) + 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) + } + } + } + } + + // Проверка regex паттернов (lock-free чтение) + 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) + } + } + } + } + + // Проверка enum (lock-free чтение) + 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 Methods (lock-free) ========== + +// SetACL устанавливает ACL для коллекции (lock-free) +func (c *Collection) SetACL(role string, canRead, canWrite, canDelete, isAdmin bool) { + c.acl.updatedAt.Store(time.Now().UnixMilli()) + + if canRead { + oldRoles := c.acl.loadReadRoles() + if !oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.storeReadRoles(newRoles) + c.logACLChange("GRANT", role, "read", true) + } + } + if canWrite { + oldRoles := c.acl.loadWriteRoles() + if !oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.storeWriteRoles(newRoles) + c.logACLChange("GRANT", role, "write", true) + } + } + if canDelete { + oldRoles := c.acl.loadDeleteRoles() + if !oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.storeDeleteRoles(newRoles) + c.logACLChange("GRANT", role, "delete", true) + } + } + if isAdmin { + oldRoles := c.acl.loadAdminRoles() + if !oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + newRoles[k] = v + } + newRoles[role] = true + c.acl.storeAdminRoles(newRoles) + c.logACLChange("GRANT", role, "admin", true) + } + } +} + +// RevokeACL отзывает разрешения у роли (lock-free) +func (c *Collection) RevokeACL(role string, permission string) { + c.acl.updatedAt.Store(time.Now().UnixMilli()) + + switch permission { + case "read": + oldRoles := c.acl.loadReadRoles() + if oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.storeReadRoles(newRoles) + c.logACLChange("REVOKE", role, "read", false) + } + case "write": + oldRoles := c.acl.loadWriteRoles() + if oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.storeWriteRoles(newRoles) + c.logACLChange("REVOKE", role, "write", false) + } + case "delete": + oldRoles := c.acl.loadDeleteRoles() + if oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.storeDeleteRoles(newRoles) + c.logACLChange("REVOKE", role, "delete", false) + } + case "admin": + oldRoles := c.acl.loadAdminRoles() + if oldRoles[role] { + newRoles := make(map[string]bool) + for k, v := range oldRoles { + if k != role { + newRoles[k] = v + } + } + c.acl.storeAdminRoles(newRoles) + c.logACLChange("REVOKE", role, "admin", false) + } + } +} + +// CheckPermission проверяет наличие разрешения у роли (lock-free) +func (c *Collection) CheckPermission(role, operation string) bool { + return c.acl.CheckPermission(role, operation) +} + +// GetACLTimestamps возвращает временные метки ACL (lock-free) +func (c *Collection) GetACLTimestamps() map[string]interface{} { + return map[string]interface{}{ + "created_at": c.acl.createdAt, + "updated_at": c.acl.updatedAt.Load(), + } +} + +// GetACLHistory возвращает историю изменений ACL (lock-free) +func (c *Collection) GetACLHistory() []ACLChange { + history := c.acl.loadACLHistory() + result := make([]ACLChange, len(history)) + copy(result, history) + return result +} + +// GetACLUpdatedAt возвращает время последнего обновления ACL (lock-free) +func (c *Collection) GetACLUpdatedAt() int64 { + return c.acl.updatedAt.Load() +} + +// GetACLCreatedAt возвращает время создания ACL (lock-free) +func (c *Collection) GetACLCreatedAt() int64 { + return c.acl.createdAt +} + +// 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 + } +} diff --git a/internal/storage/document.go b/internal/storage/document.go new file mode 100644 index 0000000..74e614a --- /dev/null +++ b/internal/storage/document.go @@ -0,0 +1,613 @@ +/* + * 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/document.go +// Назначение: Определение структуры документа, его методов для работы +// с полями, кортежами (вложенными документами) и сериализации в MessagePack. +// Документ является основной единицей хранения в СУБД futriis. +// Lock-free: Document.fields переведён на atomic.Value для wait-free доступа. + +package storage + +import ( + "fmt" + "strings" + "sync" + "sync/atomic" + "time" + + "futriis/internal/compression" + "futriis/internal/serializer" + "github.com/google/uuid" +) + +// Document представляет документ в коллекции (аналог строки в реляционной СУБД) +type Document struct { + ID string `msgpack:"_id"` // Уникальный идентификатор документа + fieldsPtr atomic.Value // map[string]interface{} - lock-free хранилище полей + CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды) + UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления + DeletedAt int64 `msgpack:"deleted_at"` // Время удаления (Unix миллисекунды, 0 = не удалён) + Version uint64 `msgpack:"version"` // Версия документа (для оптимистичных блокировок) + Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ + OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия +} + +// Tuple представляет вложенный документ (аналог кортежа в реляционной СУБД) +type Tuple struct { + Fields map[string]interface{} `msgpack:"fields"` + CreatedAt int64 `msgpack:"created_at"` // Время создания кортежа + UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления кортежа + mu sync.RWMutex +} + +// Field представляет отдельное поле документа (аналог колонки) +type Field struct { + Name string `msgpack:"name"` + Type FieldType `msgpack:"type"` + Value interface{} `msgpack:"value"` + UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления поля +} + +// FieldType определяет тип поля документа +type FieldType int + +const ( + TypeString FieldType = iota + TypeNumber + TypeBoolean + TypeTuple // Вложенный документ + TypeArray + TypeNull +) + +// NewDocument создаёт новый документ с автоматической генерацией ID +func NewDocument() *Document { + now := time.Now().UnixMilli() + d := &Document{ + ID: uuid.New().String(), + CreatedAt: now, + UpdatedAt: now, + DeletedAt: 0, + Version: 1, + Compressed: false, + OriginalSize: 0, + } + d.fieldsPtr.Store(make(map[string]interface{})) + return d +} + +// NewDocumentWithID создаёт документ с указанным ID +func NewDocumentWithID(id string) *Document { + now := time.Now().UnixMilli() + d := &Document{ + ID: id, + CreatedAt: now, + UpdatedAt: now, + DeletedAt: 0, + Version: 1, + Compressed: false, + OriginalSize: 0, + } + d.fieldsPtr.Store(make(map[string]interface{})) + return d +} + +// loadFields загружает карту полей (lock-free) +func (d *Document) loadFields() map[string]interface{} { + val := d.fieldsPtr.Load() + if val == nil { + return make(map[string]interface{}) + } + return val.(map[string]interface{}) +} + +// storeFields сохраняет карту полей (lock-free) +func (d *Document) storeFields(newMap map[string]interface{}) { + d.fieldsPtr.Store(newMap) +} + +// SetField устанавливает значение поля документа (lock-free) +func (d *Document) SetField(name string, value interface{}) { + for { + oldFields := d.loadFields() + newFields := make(map[string]interface{}) + for k, v := range oldFields { + newFields[k] = v + } + newFields[name] = value + + if d.compareAndSwapFields(oldFields, newFields) { + d.UpdatedAt = time.Now().UnixMilli() + d.Version++ + d.Compressed = false + + // Аудит изменения поля + AuditFieldOperation("UPDATE", "", "", d.ID, name, value) + return + } + } +} + +// compareAndSwapFields выполняет CAS операцию для полей (lock-free) +func (d *Document) compareAndSwapFields(old, new map[string]interface{}) bool { + return d.fieldsPtr.CompareAndSwap(old, new) +} + +// GetField возвращает значение поля документа (lock-free) +func (d *Document) GetField(name string) (interface{}, error) { + fields := d.loadFields() + if val, ok := fields[name]; ok { + return val, nil + } + return nil, fmt.Errorf("field not found: %s", name) +} + +// DeleteField удаляет поле из документа (lock-free) +func (d *Document) DeleteField(name string) { + for { + oldFields := d.loadFields() + if _, exists := oldFields[name]; !exists { + return + } + + newFields := make(map[string]interface{}) + for k, v := range oldFields { + if k != name { + newFields[k] = v + } + } + + if d.compareAndSwapFields(oldFields, newFields) { + d.UpdatedAt = time.Now().UnixMilli() + d.Version++ + d.Compressed = false + + // Аудит удаления поля + AuditFieldOperation("DELETE", "", "", d.ID, name, nil) + return + } + } +} + +// HasField проверяет наличие поля в документе (lock-free) +func (d *Document) HasField(name string) bool { + fields := d.loadFields() + _, ok := fields[name] + return ok +} + +// GetFields возвращает копию всех полей документа (lock-free) +func (d *Document) GetFields() map[string]interface{} { + fields := d.loadFields() + copy := make(map[string]interface{}) + for k, v := range fields { + copy[k] = v + } + return copy +} + +// SetTuple устанавливает вложенный документ (кортеж) в поле +func (d *Document) SetTuple(fieldName string, tuple *Tuple) { + d.SetField(fieldName, tuple) +} + +// GetTuple возвращает вложенный документ из поля +func (d *Document) GetTuple(fieldName string) (*Tuple, error) { + val, err := d.GetField(fieldName) + if err != nil { + return nil, err + } + + if tuple, ok := val.(*Tuple); ok { + return tuple, nil + } + return nil, fmt.Errorf("field %s is not a tuple", fieldName) +} + +// Serialize сериализует документ в MessagePack с поддержкой сжатия +func (d *Document) Serialize() ([]byte, error) { + // Создаём копию для сериализации + fields := d.GetFields() + docCopy := &Document{ + ID: d.ID, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + DeletedAt: d.DeletedAt, + Version: d.Version, + Compressed: d.Compressed, + OriginalSize: d.OriginalSize, + } + docCopy.fieldsPtr.Store(fields) + + data, err := serializer.Marshal(docCopy) + if err != nil { + return nil, err + } + + return data, nil +} + +// SerializeCompressed сериализует и сжимает документ +func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) { + data, err := d.Serialize() + if err != nil { + return nil, err + } + + // Проверяем, нужно ли сжимать + if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize { + compressed, err := compression.Compress(data, compressionConfig) + if err != nil { + return data, nil + } + return compressed, nil + } + + return data, nil +} + +// Deserialize десериализует документ из MessagePack (автоматически определяет сжатие) +func (d *Document) Deserialize(data []byte) error { + // Пытаемся определить, сжаты ли данные + decompressed, err := compression.DecompressAuto(data) + if err == nil && len(decompressed) < len(data) { + var doc Document + if err := serializer.Unmarshal(decompressed, &doc); err != nil { + return err + } + d.ID = doc.ID + d.fieldsPtr.Store(doc.loadFields()) + d.CreatedAt = doc.CreatedAt + d.UpdatedAt = doc.UpdatedAt + d.DeletedAt = doc.DeletedAt + d.Version = doc.Version + d.Compressed = true + d.OriginalSize = int64(len(decompressed)) + } else { + var doc Document + if err := serializer.Unmarshal(data, &doc); err != nil { + return err + } + d.ID = doc.ID + d.fieldsPtr.Store(doc.loadFields()) + d.CreatedAt = doc.CreatedAt + d.UpdatedAt = doc.UpdatedAt + d.DeletedAt = doc.DeletedAt + d.Version = doc.Version + d.Compressed = false + d.OriginalSize = 0 + } + + d.UpdatedAt = time.Now().UnixMilli() + return nil +} + +// Clone создаёт глубокую копию документа (lock-free) +func (d *Document) Clone() *Document { + fields := d.GetFields() + clone := &Document{ + ID: d.ID, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + DeletedAt: d.DeletedAt, + Version: d.Version, + Compressed: d.Compressed, + OriginalSize: d.OriginalSize, + } + + // Глубокое копирование полей + copiedFields := make(map[string]interface{}) + for k, v := range fields { + copiedFields[k] = deepCopyValue(v) + } + clone.fieldsPtr.Store(copiedFields) + + return clone +} + +// Update применяет обновление к документу (атомарно, lock-free) +func (d *Document) Update(updates map[string]interface{}) error { + for { + oldFields := d.loadFields() + newFields := make(map[string]interface{}) + for k, v := range oldFields { + newFields[k] = v + } + for k, v := range updates { + newFields[k] = v + } + + if d.compareAndSwapFields(oldFields, newFields) { + d.UpdatedAt = time.Now().UnixMilli() + d.Version++ + d.Compressed = false + return nil + } + } +} + +// SoftDelete мягко удаляет документ (устанавливает метку времени удаления) +func (d *Document) SoftDelete() { + d.DeletedAt = time.Now().UnixMilli() + d.UpdatedAt = d.DeletedAt + d.Version++ +} + +// IsDeleted проверяет, удалён ли документ (мягкое удаление) +func (d *Document) IsDeleted() bool { + return d.DeletedAt > 0 +} + +// Restore восстанавливает мягко удалённый документ +func (d *Document) Restore() { + d.DeletedAt = 0 + d.UpdatedAt = time.Now().UnixMilli() + d.Version++ +} + +// GetDeletedAtStr возвращает человекочитаемую строку времени удаления +func (d *Document) GetDeletedAtStr() string { + if d.DeletedAt == 0 { + return "" + } + return time.UnixMilli(d.DeletedAt).Format("2006-01-02 15:04:05.000") +} + +// GetCreatedAtStr возвращает человекочитаемую строку времени создания +func (d *Document) GetCreatedAtStr() string { + return time.UnixMilli(d.CreatedAt).Format("2006-01-02 15:04:05.000") +} + +// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления +func (d *Document) GetUpdatedAtStr() string { + return time.UnixMilli(d.UpdatedAt).Format("2006-01-02 15:04:05.000") +} + +// Compress сжимает документ в памяти +func (d *Document) Compress(config *compression.Config) error { + if d.Compressed { + return nil + } + + fields := d.loadFields() + originalSize := len(fields) + if originalSize < config.MinSize { + return nil + } + + d.Compressed = true + d.OriginalSize = int64(originalSize) + + return nil +} + +// Decompress распаковывает документ в памяти +func (d *Document) Decompress() error { + if !d.Compressed { + return nil + } + + d.Compressed = false + d.OriginalSize = 0 + + return nil +} + +// GetCompressionRatio возвращает коэффициент сжатия +func (d *Document) GetCompressionRatio() float64 { + if !d.Compressed || d.OriginalSize == 0 { + return 1.0 + } + + fields := d.loadFields() + currentSize := len(fields) + return float64(currentSize) / float64(d.OriginalSize) +} + +// GetMetadata возвращает метаданные документа (временные метки) +func (d *Document) GetMetadata() map[string]int64 { + return map[string]int64{ + "created_at": d.CreatedAt, + "updated_at": d.UpdatedAt, + "deleted_at": d.DeletedAt, + "version": int64(d.Version), + } +} + +// deepCopyValue выполняет глубокое копирование значения +func deepCopyValue(val interface{}) interface{} { + switch v := val.(type) { + case *Tuple: + return v.Clone() + case map[string]interface{}: + copy := make(map[string]interface{}) + for k, val := range v { + copy[k] = deepCopyValue(val) + } + return copy + case []interface{}: + copy := make([]interface{}, len(v)) + for i, val := range v { + copy[i] = deepCopyValue(val) + } + return copy + default: + return v + } +} + +// NewTuple создаёт новый вложенный документ (кортеж) +func NewTuple() *Tuple { + now := time.Now().UnixMilli() + return &Tuple{ + Fields: make(map[string]interface{}), + CreatedAt: now, + UpdatedAt: now, + } +} + +// Set устанавливает поле во вложенном документе +func (t *Tuple) Set(name string, value interface{}) { + t.mu.Lock() + defer t.mu.Unlock() + t.Fields[name] = value + t.UpdatedAt = time.Now().UnixMilli() +} + +// Get возвращает поле из вложенного документа +func (t *Tuple) Get(name string) (interface{}, error) { + t.mu.RLock() + defer t.mu.RUnlock() + + if val, ok := t.Fields[name]; ok { + return val, nil + } + return nil, fmt.Errorf("tuple field not found: %s", name) +} + +// Clone создаёт копию кортежа +func (t *Tuple) Clone() *Tuple { + t.mu.RLock() + defer t.mu.RUnlock() + + clone := NewTuple() + for k, v := range t.Fields { + clone.Fields[k] = deepCopyValue(v) + } + clone.CreatedAt = t.CreatedAt + clone.UpdatedAt = t.UpdatedAt + return clone +} + +// ToMap конвертирует кортеж в map +func (t *Tuple) ToMap() map[string]interface{} { + t.mu.RLock() + defer t.mu.RUnlock() + + copy := make(map[string]interface{}) + for k, v := range t.Fields { + copy[k] = v + } + return copy +} + +// GetTupleMetadata возвращает метаданные кортежа +func (t *Tuple) GetTupleMetadata() map[string]int64 { + t.mu.RLock() + defer t.mu.RUnlock() + + return map[string]int64{ + "created_at": t.CreatedAt, + "updated_at": t.UpdatedAt, + } +} + +// GetNestedField получает значение по точечному пути (например, "user.address.city") +func (d *Document) GetNestedField(path string) (interface{}, error) { + parts := strings.Split(path, ".") + if len(parts) == 0 { + return nil, fmt.Errorf("empty path") + } + + current := interface{}(d) + for _, part := range parts { + switch v := current.(type) { + case *Document: + val, err := v.GetField(part) + if err != nil { + return nil, err + } + current = val + case *Tuple: + val, err := v.Get(part) + if err != nil { + return nil, err + } + current = val + case map[string]interface{}: + if val, ok := v[part]; ok { + current = val + } else { + return nil, fmt.Errorf("field not found: %s", part) + } + default: + return nil, fmt.Errorf("cannot navigate into non-document value at %s", part) + } + } + + return current, nil +} + +// SetNestedField устанавливает значение по точечному пути +func (d *Document) SetNestedField(path string, value interface{}) error { + parts := strings.Split(path, ".") + if len(parts) == 0 { + return fmt.Errorf("empty path") + } + + if len(parts) == 1 { + d.SetField(parts[0], value) + return nil + } + + // Для простоты реализации используем подход с чтением-модификацией-записью + // В production коде потребуется более сложная lock-free структура + + // Сначала проверяем путь + var current interface{} = d + for i := 0; i < len(parts)-1; i++ { + part := parts[i] + + switch v := current.(type) { + case *Document: + if !v.HasField(part) { + newTuple := NewTuple() + v.SetField(part, newTuple) + current = newTuple + } else { + field, _ := v.GetField(part) + if tuple, ok := field.(*Tuple); ok { + current = tuple + } else { + return fmt.Errorf("field %s is not a tuple", part) + } + } + case *Tuple: + if val, err := v.Get(part); err == nil { + if tuple, ok := val.(*Tuple); ok { + current = tuple + } else { + return fmt.Errorf("field %s is not a tuple", part) + } + } else { + newTuple := NewTuple() + v.Set(part, newTuple) + current = newTuple + } + default: + return fmt.Errorf("cannot set nested field on non-document value") + } + } + + lastPart := parts[len(parts)-1] + switch v := current.(type) { + case *Document: + v.SetField(lastPart, value) + case *Tuple: + v.Set(lastPart, value) + default: + return fmt.Errorf("cannot set field on non-document value") + } + + d.UpdatedAt = time.Now().UnixMilli() + d.Compressed = false + return nil +}