2PC released
This commit is contained in:
184
internal/storage/audit.go
Normal file
184
internal/storage/audit.go
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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/audit.go
|
||||
// Назначение: Аудит всех операций создания, изменения, удаления данных
|
||||
// с записью временной метки с точностью до миллисекунды
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditEntry представляет запись аудита
|
||||
type AuditEntry struct {
|
||||
ID string `msgpack:"id"`
|
||||
Timestamp int64 `msgpack:"timestamp"` // Unix миллисекунды
|
||||
TimestampStr string `msgpack:"timestamp_str"` // Человекочитаемая строка
|
||||
Operation string `msgpack:"operation"` // CREATE, UPDATE, DELETE, START, COMMIT, ABORT, CLUSTER, SOFT_DELETE, RESTORE, PERMANENT_DELETE
|
||||
DataType string `msgpack:"data_type"` // DATABASE, COLLECTION, DOCUMENT, FIELD, TUPLE, SESSION, TRANSACTION, CLUSTER, INDEX
|
||||
Name string `msgpack:"name"` // Имя объекта
|
||||
Details map[string]interface{} `msgpack:"details"` // Детали операции
|
||||
}
|
||||
|
||||
// AuditLogger управляет аудитом
|
||||
type AuditLogger struct {
|
||||
entries []AuditEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var globalAuditLogger = &AuditLogger{
|
||||
entries: make([]AuditEntry, 0),
|
||||
}
|
||||
|
||||
// GetCurrentTimestamp возвращает текущую временную метку с миллисекундами
|
||||
func GetCurrentTimestamp() (int64, string) {
|
||||
now := time.Now()
|
||||
timestampMs := now.UnixMilli()
|
||||
timestampStr := now.Format("2006-01-02 15:04:05.000")
|
||||
return timestampMs, timestampStr
|
||||
}
|
||||
|
||||
// LogAudit записывает событие в аудит
|
||||
func LogAudit(operation, dataType, name string, details map[string]interface{}) {
|
||||
timestampMs, timestampStr := GetCurrentTimestamp()
|
||||
|
||||
// Если details не содержат timestamp, добавляем его
|
||||
if details == nil {
|
||||
details = make(map[string]interface{})
|
||||
}
|
||||
if _, ok := details["audit_timestamp"]; !ok {
|
||||
details["audit_timestamp"] = timestampMs
|
||||
details["audit_timestamp_str"] = timestampStr
|
||||
}
|
||||
|
||||
entry := AuditEntry{
|
||||
ID: fmt.Sprintf("%d", timestampMs),
|
||||
Timestamp: timestampMs,
|
||||
TimestampStr: timestampStr,
|
||||
Operation: operation,
|
||||
DataType: dataType,
|
||||
Name: name,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
globalAuditLogger.mu.Lock()
|
||||
globalAuditLogger.entries = append(globalAuditLogger.entries, entry)
|
||||
globalAuditLogger.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetAuditLog возвращает копию лога аудита
|
||||
func GetAuditLog() []AuditEntry {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
|
||||
result := make([]AuditEntry, len(globalAuditLogger.entries))
|
||||
copy(result, globalAuditLogger.entries)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAuditLogFiltered возвращает отфильтрованный лог аудита
|
||||
func GetAuditLogFiltered(dataType, operation string, fromTime, toTime int64) []AuditEntry {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
|
||||
result := make([]AuditEntry, 0)
|
||||
for _, entry := range globalAuditLogger.entries {
|
||||
if dataType != "" && entry.DataType != dataType {
|
||||
continue
|
||||
}
|
||||
if operation != "" && entry.Operation != operation {
|
||||
continue
|
||||
}
|
||||
if fromTime > 0 && entry.Timestamp < fromTime {
|
||||
continue
|
||||
}
|
||||
if toTime > 0 && entry.Timestamp > toTime {
|
||||
continue
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearAuditLog очищает лог аудита (только для отладки)
|
||||
func ClearAuditLog() {
|
||||
globalAuditLogger.mu.Lock()
|
||||
defer globalAuditLogger.mu.Unlock()
|
||||
globalAuditLogger.entries = make([]AuditEntry, 0)
|
||||
}
|
||||
|
||||
// GetAuditLogSize возвращает количество записей в логе аудита
|
||||
func GetAuditLogSize() int {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
return len(globalAuditLogger.entries)
|
||||
}
|
||||
|
||||
// AuditDatabaseOperation логирует операцию с базой данных
|
||||
func AuditDatabaseOperation(operation, dbName string) {
|
||||
LogAudit(operation, "DATABASE", dbName, map[string]interface{}{
|
||||
"database": dbName,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditCollectionOperation логирует операцию с коллекцией
|
||||
func AuditCollectionOperation(operation, dbName, collName string, settings interface{}) {
|
||||
LogAudit(operation, "COLLECTION", fmt.Sprintf("%s.%s", dbName, collName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditDocumentOperation логирует операцию с документом
|
||||
func AuditDocumentOperation(operation, dbName, collName, docID string, fields map[string]interface{}) {
|
||||
LogAudit(operation, "DOCUMENT", fmt.Sprintf("%s.%s.%s", dbName, collName, docID), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"fields": fields,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditFieldOperation логирует операцию с полем
|
||||
func AuditFieldOperation(operation, dbName, collName, docID, fieldName string, value interface{}) {
|
||||
LogAudit(operation, "FIELD", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, docID, fieldName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"field": fieldName,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditTupleOperation логирует операцию с кортежем
|
||||
func AuditTupleOperation(operation, dbName, collName, docID, tuplePath string) {
|
||||
LogAudit(operation, "TUPLE", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, docID, tuplePath), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"tuple_path": tuplePath,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditIndexOperation логирует операцию с индексом
|
||||
func AuditIndexOperation(operation, dbName, collName, indexName string, fields []string, unique bool) {
|
||||
LogAudit(operation, "INDEX", fmt.Sprintf("%s.%s.%s", dbName, collName, indexName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"index_name": indexName,
|
||||
"fields": fields,
|
||||
"unique": unique,
|
||||
})
|
||||
}
|
||||
1354
internal/storage/collection.go
Normal file
1354
internal/storage/collection.go
Normal file
File diff suppressed because it is too large
Load Diff
584
internal/storage/document.go
Normal file
584
internal/storage/document.go
Normal file
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
* 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.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"futriis/internal/compression"
|
||||
"futriis/internal/serializer"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Document представляет документ в коллекции (аналог строки в реляционной СУБД)
|
||||
type Document struct {
|
||||
ID string `msgpack:"_id"` // Уникальный идентификатор документа
|
||||
Fields map[string]interface{} `msgpack:"fields"` // Поля документа (аналог колонок)
|
||||
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"` // Оригинальный размер до сжатия
|
||||
mu sync.RWMutex `msgpack:"-"` // Блокировка для wait-free операций
|
||||
}
|
||||
|
||||
// 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()
|
||||
return &Document{
|
||||
ID: uuid.New().String(),
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: 0,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDocumentWithID создаёт документ с указанным ID
|
||||
func NewDocumentWithID(id string) *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Document{
|
||||
ID: id,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: 0,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SetField устанавливает значение поля документа (wait-free)
|
||||
func (d *Document) SetField(name string, value interface{}) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.Fields[name] = value
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // При изменении документа снимаем флаг сжатия
|
||||
|
||||
// Аудит изменения поля
|
||||
AuditFieldOperation("UPDATE", "", "", d.ID, name, value)
|
||||
}
|
||||
|
||||
// GetField возвращает значение поля документа
|
||||
func (d *Document) GetField(name string) (interface{}, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if val, ok := d.Fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field not found: %s", name)
|
||||
}
|
||||
|
||||
// DeleteField удаляет поле из документа
|
||||
func (d *Document) DeleteField(name string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
delete(d.Fields, name)
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
|
||||
// Аудит удаления поля
|
||||
AuditFieldOperation("DELETE", "", "", d.ID, name, nil)
|
||||
}
|
||||
|
||||
// HasField проверяет наличие поля в документе
|
||||
func (d *Document) HasField(name string) bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
_, ok := d.Fields[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetFields возвращает копию всех полей документа
|
||||
func (d *Document) GetFields() map[string]interface{} {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range d.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) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
data, err := serializer.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SerializeCompressed сериализует и сжимает документ
|
||||
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
// Сериализуем документ
|
||||
data, err := serializer.Marshal(d)
|
||||
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 {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Пытаемся определить, сжаты ли данные
|
||||
// Для этого пробуем распаковать, если не получается - данные несжатые
|
||||
decompressed, err := compression.DecompressAuto(data)
|
||||
if err == nil && len(decompressed) < len(data) {
|
||||
// Данные были сжаты, используем распакованную версию
|
||||
if err := serializer.Unmarshal(decompressed, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(len(decompressed))
|
||||
} else {
|
||||
// Данные не сжаты или не удалось распаковать
|
||||
if err := serializer.Unmarshal(data, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
}
|
||||
|
||||
// Обновляем временные метки при десериализации
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone создаёт глубокую копию документа
|
||||
func (d *Document) Clone() *Document {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
clone := &Document{
|
||||
ID: d.ID,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
DeletedAt: d.DeletedAt,
|
||||
Version: d.Version,
|
||||
Compressed: d.Compressed,
|
||||
OriginalSize: d.OriginalSize,
|
||||
}
|
||||
|
||||
// Глубокое копирование полей
|
||||
for k, v := range d.Fields {
|
||||
clone.Fields[k] = deepCopyValue(v)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Update применяет обновление к документу (атомарно)
|
||||
func (d *Document) Update(updates map[string]interface{}) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
for k, v := range updates {
|
||||
d.Fields[k] = v
|
||||
}
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // После обновления документ больше не сжат
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SoftDelete мягко удаляет документ (устанавливает метку времени удаления)
|
||||
func (d *Document) SoftDelete() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.DeletedAt = time.Now().UnixMilli()
|
||||
d.UpdatedAt = d.DeletedAt
|
||||
d.Version++
|
||||
}
|
||||
|
||||
// IsDeleted проверяет, удалён ли документ (мягкое удаление)
|
||||
func (d *Document) IsDeleted() bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return d.DeletedAt > 0
|
||||
}
|
||||
|
||||
// Restore восстанавливает мягко удалённый документ
|
||||
func (d *Document) Restore() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.DeletedAt = 0
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
}
|
||||
|
||||
// GetDeletedAtStr возвращает человекочитаемую строку времени удаления
|
||||
func (d *Document) GetDeletedAtStr() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
if d.DeletedAt == 0 {
|
||||
return ""
|
||||
}
|
||||
return time.UnixMilli(d.DeletedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetCreatedAtStr возвращает человекочитаемую строку времени создания
|
||||
func (d *Document) GetCreatedAtStr() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return time.UnixMilli(d.CreatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления
|
||||
func (d *Document) GetUpdatedAtStr() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return time.UnixMilli(d.UpdatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// Compress сжимает документ в памяти
|
||||
func (d *Document) Compress(config *compression.Config) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Сохраняем текущее состояние
|
||||
originalSize := len(d.Fields)
|
||||
if originalSize < config.MinSize {
|
||||
return nil // Не сжимаем маленькие документы
|
||||
}
|
||||
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(originalSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает документ в памяти
|
||||
func (d *Document) Decompress() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func (d *Document) GetCompressionRatio() float64 {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if !d.Compressed || d.OriginalSize == 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
currentSize := len(d.Fields)
|
||||
return float64(currentSize) / float64(d.OriginalSize)
|
||||
}
|
||||
|
||||
// GetMetadata возвращает метаданные документа (временные метки)
|
||||
func (d *Document) GetMetadata() map[string]int64 {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Иначе нужно создать промежуточные структуры
|
||||
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
|
||||
}
|
||||
797
internal/storage/engine.go
Normal file
797
internal/storage/engine.go
Normal file
@@ -0,0 +1,797 @@
|
||||
/*
|
||||
* 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/engine.go
|
||||
// Назначение: In-memory движок хранения документов с поддержкой коллекций,
|
||||
// слайсов (аналог БД), тапплов (аналог таблиц), полей и кортежей.
|
||||
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// Storage представляет основное хранилище баз данных
|
||||
type Storage struct {
|
||||
databases sync.Map // map[string]*Database
|
||||
pageSize int64
|
||||
logger *log.Logger
|
||||
totalDocs atomic.Int64
|
||||
createdAt int64
|
||||
}
|
||||
|
||||
// Database представляет базу данных (аналог слайса в реляционных СУБД)
|
||||
type Database struct {
|
||||
name string
|
||||
collections sync.Map // map[string]*Collection
|
||||
createdAt int64
|
||||
updatedAt int64
|
||||
deletedAt int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ExportMetadata содержит метаданные экспорта (добавлено)
|
||||
type ExportMetadata struct {
|
||||
DatabaseName string `msgpack:"database_name"`
|
||||
ExportTime int64 `msgpack:"export_time"`
|
||||
ExportTimeStr string `msgpack:"export_time_str"`
|
||||
Version string `msgpack:"version"`
|
||||
CollectionsCount int `msgpack:"collections_count"`
|
||||
DocumentsCount int64 `msgpack:"documents_count"`
|
||||
ExportedBy string `msgpack:"exported_by,omitempty"`
|
||||
Additional map[string]interface{} `msgpack:"additional,omitempty"`
|
||||
}
|
||||
|
||||
// ImportMetadata содержит метаданные импорта (добавлено)
|
||||
type ImportMetadata struct {
|
||||
DatabaseName string `msgpack:"database_name"`
|
||||
ImportTime int64 `msgpack:"import_time"`
|
||||
ImportTimeStr string `msgpack:"import_time_str"`
|
||||
SourceFile string `msgpack:"source_file"`
|
||||
SourceExportTime int64 `msgpack:"source_export_time,omitempty"`
|
||||
CollectionsCount int `msgpack:"collections_count"`
|
||||
DocumentsCount int64 `msgpack:"documents_count"`
|
||||
SkippedDocuments int64 `msgpack:"skipped_documents"`
|
||||
FailedDocuments int64 `msgpack:"failed_documents"`
|
||||
ImportedBy string `msgpack:"imported_by,omitempty"`
|
||||
}
|
||||
|
||||
// NewStorage создаёт новый экземпляр хранилища
|
||||
func NewStorage(pageSizeMB int, logger *log.Logger) *Storage {
|
||||
return &Storage{
|
||||
pageSize: int64(pageSizeMB) * 1024 * 1024,
|
||||
logger: logger,
|
||||
createdAt: time.Now().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDatabase создаёт новую базу данных
|
||||
func (s *Storage) CreateDatabase(name string) error {
|
||||
now := time.Now().UnixMilli()
|
||||
db := &Database{
|
||||
name: name,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: 0,
|
||||
}
|
||||
|
||||
if _, exists := s.databases.LoadOrStore(name, db); exists {
|
||||
return fmt.Errorf("database already exists")
|
||||
}
|
||||
AuditDatabaseOperation("CREATE", name)
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Database created: " + name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatabase возвращает базу данных по имени
|
||||
func (s *Storage) GetDatabase(name string) (*Database, error) {
|
||||
if val, ok := s.databases.Load(name); ok {
|
||||
return val.(*Database), nil
|
||||
}
|
||||
return nil, fmt.Errorf("database not found")
|
||||
}
|
||||
|
||||
// DropDatabase удаляет базу данных
|
||||
func (s *Storage) DropDatabase(name string) error {
|
||||
if val, ok := s.databases.Load(name); ok {
|
||||
db := val.(*Database)
|
||||
db.mu.Lock()
|
||||
db.deletedAt = time.Now().UnixMilli()
|
||||
db.mu.Unlock()
|
||||
}
|
||||
|
||||
if _, ok := s.databases.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("database not found")
|
||||
}
|
||||
AuditDatabaseOperation("DROP", name)
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Database dropped: " + name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDatabases возвращает список всех баз данных
|
||||
func (s *Storage) ListDatabases() []string {
|
||||
databases := make([]string, 0)
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
databases = append(databases, key.(string))
|
||||
return true
|
||||
})
|
||||
return databases
|
||||
}
|
||||
|
||||
// Name возвращает имя базы данных
|
||||
func (db *Database) Name() string {
|
||||
return db.name
|
||||
}
|
||||
|
||||
// GetCreatedAt возвращает время создания базы данных
|
||||
func (db *Database) GetCreatedAt() int64 {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return db.createdAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt возвращает время последнего обновления базы данных
|
||||
func (db *Database) GetUpdatedAt() int64 {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return db.updatedAt
|
||||
}
|
||||
|
||||
// GetDeletedAt возвращает время удаления базы данных (0 = не удалена)
|
||||
func (db *Database) GetDeletedAt() int64 {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return db.deletedAt
|
||||
}
|
||||
|
||||
// UpdateTimestamp обновляет временную метку базы данных
|
||||
func (db *Database) UpdateTimestamp() {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
db.updatedAt = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// CreateCollection создаёт новую коллекцию в базе данных
|
||||
func (db *Database) CreateCollection(name string) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, nil)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
db.UpdateTimestamp()
|
||||
AuditCollectionOperation("CREATE", db.name, name, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCollectionWithSettings создаёт коллекцию с настройками
|
||||
func (db *Database) CreateCollectionWithSettings(name string, settings *CollectionSettings) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, settings)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
db.UpdateTimestamp()
|
||||
AuditCollectionOperation("CREATE", db.name, name, settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollection возвращает коллекцию по имени
|
||||
func (db *Database) GetCollection(name string) (*Collection, error) {
|
||||
if val, ok := db.collections.Load(name); ok {
|
||||
return val.(*Collection), nil
|
||||
}
|
||||
return nil, fmt.Errorf("collection not found")
|
||||
}
|
||||
|
||||
// DropCollection удаляет коллекцию
|
||||
func (db *Database) DropCollection(name string) error {
|
||||
if _, ok := db.collections.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("collection not found")
|
||||
}
|
||||
db.UpdateTimestamp()
|
||||
AuditCollectionOperation("DROP", db.name, name, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCollections возвращает список всех коллекций в базе данных
|
||||
func (db *Database) ListCollections() []string {
|
||||
collections := make([]string, 0)
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
collections = append(collections, key.(string))
|
||||
return true
|
||||
})
|
||||
return collections
|
||||
}
|
||||
|
||||
// GetTotalDocuments возвращает общее количество документов во всех коллекциях
|
||||
func (s *Storage) GetTotalDocuments() int64 {
|
||||
return s.totalDocs.Load()
|
||||
}
|
||||
|
||||
// GetPageSize возвращает размер страницы памяти
|
||||
func (s *Storage) GetPageSize() int64 {
|
||||
return s.pageSize
|
||||
}
|
||||
|
||||
// GetCreatedAt возвращает время создания хранилища
|
||||
func (s *Storage) GetCreatedAt() int64 {
|
||||
return s.createdAt
|
||||
}
|
||||
|
||||
// SerializeDatabaseWithMetadata сериализует базу данных с метаданными экспорта (модифицировано)
|
||||
func (db *Database) SerializeDatabaseWithMetadata(exportedBy string) ([]byte, *ExportMetadata, error) {
|
||||
dbData := make(map[string]interface{})
|
||||
|
||||
totalDocuments := int64(0)
|
||||
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
coll := value.(*Collection)
|
||||
collData := make(map[string]interface{})
|
||||
|
||||
docs := coll.GetAllDocuments()
|
||||
totalDocuments += int64(len(docs))
|
||||
|
||||
collDocs := make([]*Document, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
collDocs = append(collDocs, doc)
|
||||
}
|
||||
collData["documents"] = collDocs
|
||||
collData["metadata"] = coll.GetMetadata()
|
||||
collData["timestamps"] = coll.GetTimestamps()
|
||||
|
||||
// Добавляем информацию об индексах при экспорте
|
||||
collData["indexes"] = coll.GetIndexesInfo()
|
||||
|
||||
// Добавляем ограничения при экспорте
|
||||
collData["constraints"] = coll.GetConstraints()
|
||||
collData["constraint_timestamps"] = coll.GetConstraintTimestamps()
|
||||
|
||||
// Добавляем ACL при экспорте
|
||||
collData["acl_timestamps"] = coll.GetACLTimestamps()
|
||||
|
||||
dbData[key.(string)] = collData
|
||||
return true
|
||||
})
|
||||
|
||||
nowMs, nowStr := GetCurrentTimestamp()
|
||||
metadata := &ExportMetadata{
|
||||
DatabaseName: db.name,
|
||||
ExportTime: nowMs,
|
||||
ExportTimeStr: nowStr,
|
||||
Version: "1.0",
|
||||
CollectionsCount: len(db.ListCollections()),
|
||||
DocumentsCount: totalDocuments,
|
||||
ExportedBy: exportedBy,
|
||||
Additional: map[string]interface{}{
|
||||
"storage_created_at": time.UnixMilli(db.createdAt).Format("2006-01-02 15:04:05.000"),
|
||||
},
|
||||
}
|
||||
|
||||
// Добавляем метаданные базы данных
|
||||
dbData["_metadata"] = map[string]interface{}{
|
||||
"name": db.name,
|
||||
"created_at": db.GetCreatedAt(),
|
||||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"updated_at": db.GetUpdatedAt(),
|
||||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"export_metadata": metadata,
|
||||
}
|
||||
|
||||
data, err := serializer.Marshal(dbData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return data, metadata, nil
|
||||
}
|
||||
|
||||
// SerializeDatabase сериализует всю базу данных в MessagePack (сохранён для совместимости)
|
||||
func (db *Database) SerializeDatabase() ([]byte, error) {
|
||||
data, _, err := db.SerializeDatabaseWithMetadata("")
|
||||
return data, err
|
||||
}
|
||||
|
||||
// DeserializeDatabaseWithMetadata десериализует базу данных с сохранением метаданных импорта (модифицировано)
|
||||
func (db *Database) DeserializeDatabaseWithMetadata(data []byte, sourceFile string, importedBy string) (*ImportMetadata, error) {
|
||||
var dbData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &dbData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sourceExportTime int64
|
||||
var collectionsCount int
|
||||
var documentsCount int64
|
||||
|
||||
// Извлекаем метаданные
|
||||
if metaRaw, ok := dbData["_metadata"]; ok {
|
||||
if meta, ok := metaRaw.(map[string]interface{}); ok {
|
||||
if createdAt, ok := meta["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
db.createdAt = v
|
||||
}
|
||||
}
|
||||
if exportMetaRaw, ok := meta["export_metadata"]; ok {
|
||||
if exportMeta, ok := exportMetaRaw.(map[string]interface{}); ok {
|
||||
if sourceTime, ok := exportMeta["export_time"]; ok {
|
||||
if v, ok := sourceTime.(int64); ok {
|
||||
sourceExportTime = v
|
||||
} else if v, ok := sourceTime.(float64); ok {
|
||||
sourceExportTime = int64(v)
|
||||
}
|
||||
}
|
||||
if collCount, ok := exportMeta["collections_count"]; ok {
|
||||
if v, ok := collCount.(int); ok {
|
||||
collectionsCount = v
|
||||
} else if v, ok := collCount.(int64); ok {
|
||||
collectionsCount = int(v)
|
||||
} else if v, ok := collCount.(float64); ok {
|
||||
collectionsCount = int(v)
|
||||
}
|
||||
}
|
||||
if docCount, ok := exportMeta["documents_count"]; ok {
|
||||
if v, ok := docCount.(int64); ok {
|
||||
documentsCount = v
|
||||
} else if v, ok := docCount.(float64); ok {
|
||||
documentsCount = int64(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(dbData, "_metadata")
|
||||
}
|
||||
|
||||
for collName, collDataRaw := range dbData {
|
||||
collData, ok := collDataRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
settings := &CollectionSettings{
|
||||
MaxDocuments: 0,
|
||||
ValidateSchema: false,
|
||||
AutoIndexID: true,
|
||||
TTLSeconds: 0,
|
||||
SoftDelete: false,
|
||||
}
|
||||
|
||||
if metaRaw, ok := collData["metadata"]; ok {
|
||||
if meta, ok := metaRaw.(map[string]interface{}); ok {
|
||||
if settingsRaw, ok := meta["settings"]; ok {
|
||||
if settingsMap, ok := settingsRaw.(map[string]interface{}); ok {
|
||||
if maxDocs, ok := settingsMap["max_documents"]; ok {
|
||||
if v, ok := maxDocs.(int); ok {
|
||||
settings.MaxDocuments = v
|
||||
} else if v, ok := maxDocs.(int64); ok {
|
||||
settings.MaxDocuments = int(v)
|
||||
} else if v, ok := maxDocs.(float64); ok {
|
||||
settings.MaxDocuments = int(v)
|
||||
}
|
||||
}
|
||||
if validateSchema, ok := settingsMap["validate_schema"]; ok {
|
||||
if v, ok := validateSchema.(bool); ok {
|
||||
settings.ValidateSchema = v
|
||||
}
|
||||
}
|
||||
if autoIndexID, ok := settingsMap["auto_index_id"]; ok {
|
||||
if v, ok := autoIndexID.(bool); ok {
|
||||
settings.AutoIndexID = v
|
||||
}
|
||||
}
|
||||
if ttlSeconds, ok := settingsMap["ttl_seconds"]; ok {
|
||||
if v, ok := ttlSeconds.(int); ok {
|
||||
settings.TTLSeconds = v
|
||||
} else if v, ok := ttlSeconds.(int64); ok {
|
||||
settings.TTLSeconds = int(v)
|
||||
} else if v, ok := ttlSeconds.(float64); ok {
|
||||
settings.TTLSeconds = int(v)
|
||||
}
|
||||
}
|
||||
if softDelete, ok := settingsMap["soft_delete"]; ok {
|
||||
if v, ok := softDelete.(bool); ok {
|
||||
settings.SoftDelete = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if meta, ok := metaRaw.(*CollectionMetadata); ok {
|
||||
if meta.Settings != nil {
|
||||
settings = meta.Settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coll := NewCollection(db.name, collName, settings)
|
||||
|
||||
// Восстанавливаем временные метки коллекции если они есть
|
||||
if timestampsRaw, ok := collData["timestamps"]; ok {
|
||||
if timestamps, ok := timestampsRaw.(map[string]interface{}); ok {
|
||||
if createdAt, ok := timestamps["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
coll.metadata.CreatedAt = v
|
||||
} else if v, ok := createdAt.(float64); ok {
|
||||
coll.metadata.CreatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if updatedAt, ok := timestamps["updated_at"]; ok {
|
||||
if v, ok := updatedAt.(int64); ok {
|
||||
coll.metadata.UpdatedAt = v
|
||||
} else if v, ok := updatedAt.(float64); ok {
|
||||
coll.metadata.UpdatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Восстанавливаем индексы
|
||||
if indexesRaw, ok := collData["indexes"]; ok {
|
||||
if indexesList, ok := indexesRaw.([]interface{}); ok {
|
||||
for _, idxRaw := range indexesList {
|
||||
if idxMap, ok := idxRaw.(map[string]interface{}); ok {
|
||||
idxName, _ := idxMap["name"].(string)
|
||||
idxFieldsRaw, _ := idxMap["fields"].([]interface{})
|
||||
idxFields := make([]string, len(idxFieldsRaw))
|
||||
for i, f := range idxFieldsRaw {
|
||||
idxFields[i] = fmt.Sprintf("%v", f)
|
||||
}
|
||||
idxUnique, _ := idxMap["unique"].(bool)
|
||||
if idxName != "" && idxName != "_id_" {
|
||||
coll.CreateIndex(idxName, idxFields, idxUnique)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if docsRaw, ok := collData["documents"]; ok {
|
||||
if docs, ok := docsRaw.([]interface{}); ok {
|
||||
for _, docRaw := range docs {
|
||||
if doc, ok := docRaw.(*Document); ok {
|
||||
coll.Insert(doc)
|
||||
} else if docMap, ok := docRaw.(map[string]interface{}); ok {
|
||||
doc := NewDocument()
|
||||
if id, ok := docMap["ID"].(string); ok {
|
||||
doc.ID = id
|
||||
} else if id, ok := docMap["id"].(string); ok {
|
||||
doc.ID = id
|
||||
}
|
||||
if fields, ok := docMap["fields"]; ok {
|
||||
if fieldsMap, ok := fields.(map[string]interface{}); ok {
|
||||
for k, v := range fieldsMap {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if createdAt, ok := docMap["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
doc.CreatedAt = v
|
||||
} else if v, ok := createdAt.(float64); ok {
|
||||
doc.CreatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if updatedAt, ok := docMap["updated_at"]; ok {
|
||||
if v, ok := updatedAt.(int64); ok {
|
||||
doc.UpdatedAt = v
|
||||
} else if v, ok := updatedAt.(float64); ok {
|
||||
doc.UpdatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if deletedAt, ok := docMap["deleted_at"]; ok {
|
||||
if v, ok := deletedAt.(int64); ok {
|
||||
doc.DeletedAt = v
|
||||
} else if v, ok := deletedAt.(float64); ok {
|
||||
doc.DeletedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if version, ok := docMap["version"]; ok {
|
||||
if v, ok := version.(uint64); ok {
|
||||
doc.Version = v
|
||||
} else if v, ok := version.(int64); ok {
|
||||
doc.Version = uint64(v)
|
||||
} else if v, ok := version.(float64); ok {
|
||||
doc.Version = uint64(v)
|
||||
}
|
||||
}
|
||||
coll.Insert(doc)
|
||||
}
|
||||
}
|
||||
} else if docs, ok := docsRaw.([]*Document); ok {
|
||||
for _, doc := range docs {
|
||||
coll.Insert(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.collections.Store(collName, coll)
|
||||
AuditCollectionOperation("RESTORE", db.name, collName, settings)
|
||||
}
|
||||
|
||||
nowMs, nowStr := GetCurrentTimestamp()
|
||||
importMetadata := &ImportMetadata{
|
||||
DatabaseName: db.name,
|
||||
ImportTime: nowMs,
|
||||
ImportTimeStr: nowStr,
|
||||
SourceFile: sourceFile,
|
||||
SourceExportTime: sourceExportTime,
|
||||
CollectionsCount: collectionsCount,
|
||||
DocumentsCount: documentsCount,
|
||||
SkippedDocuments: 0,
|
||||
FailedDocuments: 0,
|
||||
ImportedBy: importedBy,
|
||||
}
|
||||
|
||||
db.UpdateTimestamp()
|
||||
|
||||
// Аудит импорта
|
||||
LogAudit("IMPORT", "DATABASE", db.name, map[string]interface{}{
|
||||
"source_file": sourceFile,
|
||||
"collections_count": collectionsCount,
|
||||
"documents_count": documentsCount,
|
||||
"source_export_time": sourceExportTime,
|
||||
"imported_by": importedBy,
|
||||
})
|
||||
|
||||
return importMetadata, nil
|
||||
}
|
||||
|
||||
// DeserializeDatabase десериализует базу данных из MessagePack (сохранён для совместимости)
|
||||
func (db *Database) DeserializeDatabase(data []byte) error {
|
||||
_, err := db.DeserializeDatabaseWithMetadata(data, "", "")
|
||||
return err
|
||||
}
|
||||
|
||||
// ExportDatabaseWithMetadata экспортирует базу данных с метаданными (добавлено)
|
||||
func (s *Storage) ExportDatabaseWithMetadata(dbName, filePath, exportedBy string) (*ExportMetadata, error) {
|
||||
if !s.ExistsDatabase(dbName) {
|
||||
return nil, fmt.Errorf("database '%s' not found", dbName)
|
||||
}
|
||||
|
||||
db, err := s.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, metadata, err := db.SerializeDatabaseWithMetadata(exportedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write export file: %v", err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Database '%s' exported to %s at %s", dbName, filePath, metadata.ExportTimeStr))
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// ImportDatabaseWithMetadata импортирует базу данных с метаданными (добавлено)
|
||||
func (s *Storage) ImportDatabaseWithMetadata(dbName, filePath, importedBy string) (*ImportMetadata, error) {
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("import file '%s' not found", filePath)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read import file: %v", err)
|
||||
}
|
||||
|
||||
if !s.ExistsDatabase(dbName) {
|
||||
if err := s.CreateDatabase(dbName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := s.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata, err := db.DeserializeDatabaseWithMetadata(data, filePath, importedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Database '%s' imported from %s at %s", dbName, filePath, metadata.ImportTimeStr))
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// GetDatabaseNames возвращает имена всех баз данных
|
||||
func (s *Storage) GetDatabaseNames() []string {
|
||||
return s.ListDatabases()
|
||||
}
|
||||
|
||||
// ExistsDatabase проверяет существование базы данных
|
||||
func (s *Storage) ExistsDatabase(name string) bool {
|
||||
_, ok := s.databases.Load(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetDatabaseCount возвращает количество баз данных
|
||||
func (s *Storage) GetDatabaseCount() int {
|
||||
count := 0
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
// ========== Дополнительные методы для управления хранилищем ==========
|
||||
|
||||
// Backup создаёт резервную копию всех данных
|
||||
func (s *Storage) Backup(backupPath string) error {
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Starting backup to %s", backupPath))
|
||||
}
|
||||
|
||||
backup := make(map[string]interface{})
|
||||
backup["created_at"] = s.createdAt
|
||||
backup["backup_time"] = time.Now().UnixMilli()
|
||||
backup["backup_time_str"] = time.Now().Format("2006-01-02 15:04:05.000")
|
||||
|
||||
databases := make(map[string][]byte)
|
||||
databasesMetadata := make(map[string]interface{})
|
||||
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
dbName := key.(string)
|
||||
db := value.(*Database)
|
||||
|
||||
dbData, err := db.SerializeDatabase()
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to serialize database %s: %v", dbName, err))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
databases[dbName] = dbData
|
||||
databasesMetadata[dbName] = map[string]interface{}{
|
||||
"created_at": db.GetCreatedAt(),
|
||||
"updated_at": db.GetUpdatedAt(),
|
||||
"collections": len(db.ListCollections()),
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
backup["databases"] = databases
|
||||
backup["databases_metadata"] = databasesMetadata
|
||||
backup["total_databases"] = s.GetDatabaseCount()
|
||||
|
||||
data, err := serializer.Marshal(backup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal backup: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(backupPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write backup: %v", err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Backup completed: %s", backupPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore восстанавливает данные из резервной копии
|
||||
func (s *Storage) Restore(backupPath string) error {
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Starting restore from %s", backupPath))
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read backup file: %v", err)
|
||||
}
|
||||
|
||||
var backup map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &backup); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal backup: %v", err)
|
||||
}
|
||||
|
||||
// Восстанавливаем временную метку хранилища
|
||||
if createdAt, ok := backup["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
s.createdAt = v
|
||||
}
|
||||
}
|
||||
|
||||
databasesRaw, ok := backup["databases"]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid backup format: missing databases")
|
||||
}
|
||||
|
||||
databases, ok := databasesRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid backup format: databases is not a map")
|
||||
}
|
||||
|
||||
for dbName, dbDataRaw := range databases {
|
||||
dbData, ok := dbDataRaw.([]byte)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !s.ExistsDatabase(dbName) {
|
||||
if err := s.CreateDatabase(dbName); err != nil {
|
||||
return fmt.Errorf("failed to create database %s: %v", dbName, err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := s.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database %s: %v", dbName, err)
|
||||
}
|
||||
|
||||
if err := db.DeserializeDatabase(dbData); err != nil {
|
||||
return fmt.Errorf("failed to restore database %s: %v", dbName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Restore completed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику хранилища
|
||||
func (s *Storage) GetStats() map[string]interface{} {
|
||||
stats := map[string]interface{}{
|
||||
"total_databases": s.GetDatabaseCount(),
|
||||
"total_documents": s.GetTotalDocuments(),
|
||||
"page_size_bytes": s.pageSize,
|
||||
"created_at": s.createdAt,
|
||||
"created_at_str": time.UnixMilli(s.createdAt).Format("2006-01-02 15:04:05.000"),
|
||||
}
|
||||
|
||||
databases := make([]map[string]interface{}, 0)
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
db := value.(*Database)
|
||||
dbStats := map[string]interface{}{
|
||||
"name": db.name,
|
||||
"collections": len(db.ListCollections()),
|
||||
"created_at": db.GetCreatedAt(),
|
||||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"updated_at": db.GetUpdatedAt(),
|
||||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"deleted_at": db.GetDeletedAt(),
|
||||
}
|
||||
|
||||
totalDocs := int64(0)
|
||||
totalSize := int64(0)
|
||||
db.collections.Range(func(k, v interface{}) bool {
|
||||
coll := v.(*Collection)
|
||||
totalDocs += coll.Count()
|
||||
totalSize += coll.Size()
|
||||
return true
|
||||
})
|
||||
|
||||
dbStats["documents"] = totalDocs
|
||||
dbStats["size_bytes"] = totalSize
|
||||
databases = append(databases, dbStats)
|
||||
return true
|
||||
})
|
||||
|
||||
stats["databases"] = databases
|
||||
return stats
|
||||
}
|
||||
83
internal/storage/fsync.go
Normal file
83
internal/storage/fsync.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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/fsync.go
|
||||
// Назначение: Реальная синхронизация с диском (fsync) для WAL
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RealFsync выполняет реальный fsync на файле
|
||||
func RealFsync(file *os.File) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Вызываем системный fsync
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
// RealFsyncWithRetry выполняет fsync с повторными попытками
|
||||
func RealFsyncWithRetry(file *os.File, maxRetries int, retryDelay time.Duration) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if err := RealFsync(file); err != nil {
|
||||
lastErr = err
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("fsync failed after %d attempts: %v", maxRetries, lastErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// FsyncDir синхронизирует директорию (для гарантии, что создание файла записано на диск)
|
||||
func FsyncDir(dirPath string) error {
|
||||
dir, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// На Windows нет прямой поддержки fsync для директорий
|
||||
return nil
|
||||
}
|
||||
|
||||
return dir.Sync()
|
||||
}
|
||||
|
||||
// FsyncDataOnly выполняет fsync только для данных (не метаданных)
|
||||
func FsyncDataOnly(file *os.File) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fd := int(file.Fd())
|
||||
_, _, err := syscall.Syscall(syscall.SYS_FSYNC, uintptr(fd), 0, 0)
|
||||
if err != 0 {
|
||||
return fmt.Errorf("fsync failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
466
internal/storage/persistence.go
Normal file
466
internal/storage/persistence.go
Normal file
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* 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/persistence.go
|
||||
// Назначение: Персистентное хранение данных на диске с поддержкой checkpoint и recovery
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PersistenceConfig конфигурация персистентного хранения
|
||||
type PersistenceConfig struct {
|
||||
DataDir string `json:"data_dir"`
|
||||
CheckpointInterval time.Duration `json:"checkpoint_interval"`
|
||||
MaxCheckpoints int `json:"max_checkpoints"`
|
||||
CompressEnabled bool `json:"compress_enabled"`
|
||||
SyncWrites bool `json:"sync_writes"`
|
||||
}
|
||||
|
||||
// DefaultPersistenceConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultPersistenceConfig() *PersistenceConfig {
|
||||
return &PersistenceConfig{
|
||||
DataDir: "futriis_data",
|
||||
CheckpointInterval: 5 * time.Minute,
|
||||
MaxCheckpoints: 10,
|
||||
CompressEnabled: true,
|
||||
SyncWrites: true,
|
||||
}
|
||||
}
|
||||
|
||||
// DatabaseSnapshot представляет снапшот базы данных
|
||||
type DatabaseSnapshot struct {
|
||||
Name string `json:"name"`
|
||||
Collections map[string]interface{} `json:"collections"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Version uint64 `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
// PersistenceManager управляет персистентным хранением
|
||||
type PersistenceManager struct {
|
||||
config *PersistenceConfig
|
||||
storage *Storage
|
||||
logger LoggerInterface
|
||||
mu sync.RWMutex
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
lastCheckpoint int64
|
||||
checkpointID atomic.Uint64
|
||||
}
|
||||
|
||||
// NewPersistenceManager создаёт новый менеджер персистентности
|
||||
func NewPersistenceManager(config *PersistenceConfig, storage *Storage, logger LoggerInterface) *PersistenceManager {
|
||||
if config == nil {
|
||||
config = DefaultPersistenceConfig()
|
||||
}
|
||||
|
||||
pm := &PersistenceManager{
|
||||
config: config,
|
||||
storage: storage,
|
||||
logger: logger,
|
||||
stopChan: make(chan struct{}),
|
||||
lastCheckpoint: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
// Создаём директорию для данных
|
||||
if err := os.MkdirAll(config.DataDir, 0755); err != nil {
|
||||
if logger != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create data directory: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// Start запускает фоновое сохранение чекпоинтов
|
||||
func (pm *PersistenceManager) Start() {
|
||||
pm.wg.Add(1)
|
||||
go pm.checkpointLoop()
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Persistence manager started, data dir: %s", pm.config.DataDir))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает менеджер
|
||||
func (pm *PersistenceManager) Stop() {
|
||||
close(pm.stopChan)
|
||||
pm.wg.Wait()
|
||||
|
||||
// Финальное сохранение
|
||||
pm.SaveAll()
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info("Persistence manager stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// checkpointLoop периодически создаёт чекпоинты
|
||||
func (pm *PersistenceManager) checkpointLoop() {
|
||||
defer pm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(pm.config.CheckpointInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pm.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := pm.SaveAll(); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to save checkpoint: %v", err))
|
||||
}
|
||||
} else {
|
||||
pm.cleanupOldCheckpoints()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SaveDatabase сохраняет базу данных на диск
|
||||
func (pm *PersistenceManager) SaveDatabase(dbName string) error {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database not found: %s", dbName)
|
||||
}
|
||||
|
||||
// Собираем данные всех коллекций
|
||||
snapshot := &DatabaseSnapshot{
|
||||
Name: dbName,
|
||||
Collections: make(map[string]interface{}),
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Version: pm.checkpointID.Add(1),
|
||||
}
|
||||
|
||||
for _, collName := range db.ListCollections() {
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
docs := coll.GetAllDocuments()
|
||||
collData := make([]map[string]interface{}, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
collData = append(collData, map[string]interface{}{
|
||||
"_id": doc.ID,
|
||||
"fields": doc.GetFields(),
|
||||
"created_at": doc.CreatedAt,
|
||||
"updated_at": doc.UpdatedAt,
|
||||
"version": doc.Version,
|
||||
})
|
||||
}
|
||||
snapshot.Collections[collName] = collData
|
||||
}
|
||||
|
||||
// Сериализуем и сохраняем
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Опционально сжимаем
|
||||
if pm.config.CompressEnabled {
|
||||
data, err = pm.compress(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем в файл
|
||||
filename := pm.getSnapshotFilename(dbName, snapshot.Version)
|
||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Синхронизируем с диском если нужно
|
||||
if pm.config.SyncWrites {
|
||||
if f, err := os.OpenFile(filename, os.O_RDWR, 0644); err == nil {
|
||||
RealFsync(f)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Saved database %s checkpoint %d (%d bytes)", dbName, snapshot.Version, len(data)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAll сохраняет все базы данных
|
||||
func (pm *PersistenceManager) SaveAll() error {
|
||||
databases := pm.storage.ListDatabases()
|
||||
|
||||
var lastErr error
|
||||
for _, dbName := range databases {
|
||||
if err := pm.SaveDatabase(dbName); err != nil {
|
||||
lastErr = err
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to save database %s: %v", dbName, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// LoadDatabase загружает базу данных с диска
|
||||
func (pm *PersistenceManager) LoadDatabase(dbName string) error {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
// Находим последний чекпоинт
|
||||
snapshot, err := pm.findLatestSnapshot(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if snapshot == nil {
|
||||
return fmt.Errorf("no snapshot found for database: %s", dbName)
|
||||
}
|
||||
|
||||
// Восстанавливаем базу данных
|
||||
if !pm.storage.ExistsDatabase(dbName) {
|
||||
if err := pm.storage.CreateDatabase(dbName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Восстанавливаем коллекции
|
||||
for collName, collDataRaw := range snapshot.Collections {
|
||||
collData, ok := collDataRaw.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.GetCollection(collName); err != nil {
|
||||
if err := db.CreateCollection(collName); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, docRaw := range collData {
|
||||
docMap, ok := docRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
docID, ok := docMap["_id"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
doc := NewDocumentWithID(docID)
|
||||
|
||||
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if createdAt, ok := docMap["created_at"].(float64); ok {
|
||||
doc.CreatedAt = int64(createdAt)
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"].(float64); ok {
|
||||
doc.UpdatedAt = int64(updatedAt)
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"].(float64); ok {
|
||||
doc.Version = uint64(version)
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to restore document %s: %v", docID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Loaded database %s from snapshot (version %d)", dbName, snapshot.Version))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAll загружает все базы данных с диска
|
||||
func (pm *PersistenceManager) LoadAll() error {
|
||||
files, err := filepath.Glob(filepath.Join(pm.config.DataDir, "snapshot_*.json*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
databases := make(map[string]bool)
|
||||
for _, file := range files {
|
||||
base := filepath.Base(file)
|
||||
// Извлекаем имя базы данных из имени файла
|
||||
// Формат: snapshot_{dbname}_{version}.json.gz
|
||||
parts := strings.Split(base, "_")
|
||||
if len(parts) >= 2 {
|
||||
dbName := parts[1]
|
||||
databases[dbName] = true
|
||||
}
|
||||
}
|
||||
|
||||
for dbName := range databases {
|
||||
if err := pm.LoadDatabase(dbName); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to load database %s: %v", dbName, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSnapshotFilename возвращает имя файла для снапшота
|
||||
func (pm *PersistenceManager) getSnapshotFilename(dbName string, version uint64) string {
|
||||
filename := fmt.Sprintf("snapshot_%s_%d.json", dbName, version)
|
||||
if pm.config.CompressEnabled {
|
||||
filename += ".gz"
|
||||
}
|
||||
return filepath.Join(pm.config.DataDir, filename)
|
||||
}
|
||||
|
||||
// findLatestSnapshot находит последний снапшот базы данных
|
||||
func (pm *PersistenceManager) findLatestSnapshot(dbName string) (*DatabaseSnapshot, error) {
|
||||
pattern := filepath.Join(pm.config.DataDir, fmt.Sprintf("snapshot_%s_*.json*", dbName))
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Сортируем по времени создания (новейшие первыми)
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
infoI, _ := os.Stat(files[i])
|
||||
infoJ, _ := os.Stat(files[j])
|
||||
if infoI == nil || infoJ == nil {
|
||||
return false
|
||||
}
|
||||
return infoI.ModTime().After(infoJ.ModTime())
|
||||
})
|
||||
|
||||
// Читаем последний снапшот
|
||||
data, err := os.ReadFile(files[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pm.config.CompressEnabled && strings.HasSuffix(files[0], ".gz") {
|
||||
data, err = pm.decompress(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot DatabaseSnapshot
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
// compress сжимает данные с помощью gzip
|
||||
func (pm *PersistenceManager) compress(data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return nil, fmt.Errorf("failed to write compressed data: %v", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, fmt.Errorf("failed to close gzip writer: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// decompress разжимает данные с помощью gzip
|
||||
func (pm *PersistenceManager) decompress(data []byte) ([]byte, error) {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.ReadFrom(reader); err != nil {
|
||||
return nil, fmt.Errorf("failed to decompress data: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// cleanupOldCheckpoints удаляет старые чекпоинты
|
||||
func (pm *PersistenceManager) cleanupOldCheckpoints() {
|
||||
pattern := filepath.Join(pm.config.DataDir, "snapshot_*.json*")
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(files) <= pm.config.MaxCheckpoints {
|
||||
return
|
||||
}
|
||||
|
||||
// Сортируем по времени модификации
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
infoI, _ := os.Stat(files[i])
|
||||
infoJ, _ := os.Stat(files[j])
|
||||
if infoI == nil || infoJ == nil {
|
||||
return false
|
||||
}
|
||||
return infoI.ModTime().Before(infoJ.ModTime())
|
||||
})
|
||||
|
||||
// Удаляем старые
|
||||
toDelete := files[:len(files)-pm.config.MaxCheckpoints]
|
||||
for _, f := range toDelete {
|
||||
os.Remove(f)
|
||||
if pm.logger != nil {
|
||||
pm.logger.Debug(fmt.Sprintf("Removed old checkpoint: %s", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
2241
internal/storage/transaction.go
Normal file
2241
internal/storage/transaction.go
Normal file
File diff suppressed because it is too large
Load Diff
816
internal/storage/trigger.go
Normal file
816
internal/storage/trigger.go
Normal file
@@ -0,0 +1,816 @@
|
||||
/*
|
||||
* 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/trigger.go
|
||||
// Назначение: Реализация триггеров, похожих на MongoDB trigger syntax.
|
||||
// Поддерживает события: INSERT, UPDATE, DELETE, REPLACE.
|
||||
// Триггеры могут выполняться до или после события.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
)
|
||||
|
||||
// TriggerEvent определяет тип события для триггера
|
||||
type TriggerEvent string
|
||||
|
||||
const (
|
||||
TriggerBeforeInsert TriggerEvent = "BEFORE_INSERT"
|
||||
TriggerAfterInsert TriggerEvent = "AFTER_INSERT"
|
||||
TriggerBeforeUpdate TriggerEvent = "BEFORE_UPDATE"
|
||||
TriggerAfterUpdate TriggerEvent = "AFTER_UPDATE"
|
||||
TriggerBeforeDelete TriggerEvent = "BEFORE_DELETE"
|
||||
TriggerAfterDelete TriggerEvent = "AFTER_DELETE"
|
||||
TriggerBeforeReplace TriggerEvent = "BEFORE_REPLACE"
|
||||
TriggerAfterReplace TriggerEvent = "AFTER_REPLACE"
|
||||
)
|
||||
|
||||
// TriggerAction определяет действие триггера
|
||||
type TriggerAction string
|
||||
|
||||
const (
|
||||
ActionAbort TriggerAction = "abort" // Прервать операцию
|
||||
ActionSkip TriggerAction = "skip" // Пропустить операцию
|
||||
ActionModify TriggerAction = "modify" // Модифицировать документ
|
||||
ActionLog TriggerAction = "log" // Записать в лог
|
||||
ActionNotify TriggerAction = "notify" // Отправить уведомление
|
||||
ActionCustom TriggerAction = "custom" // Пользовательское действие
|
||||
)
|
||||
|
||||
// Trigger представляет триггер на коллекции
|
||||
type Trigger struct {
|
||||
Name string `msgpack:"name"`
|
||||
Collection string `msgpack:"collection"`
|
||||
Event TriggerEvent `msgpack:"event"`
|
||||
Action TriggerAction `msgpack:"action"`
|
||||
Condition *TriggerCondition `msgpack:"condition"`
|
||||
Operations []TriggerOperation `msgpack:"operations"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
UpdatedAt int64 `msgpack:"updated_at"`
|
||||
Enabled bool `msgpack:"enabled"`
|
||||
Description string `msgpack:"description"`
|
||||
mu sync.RWMutex `msgpack:"-"`
|
||||
}
|
||||
|
||||
// TriggerCondition определяет условие выполнения триггера
|
||||
type TriggerCondition struct {
|
||||
Field string `msgpack:"field"` // Поле для проверки
|
||||
Operator string `msgpack:"operator"` // Оператор: eq, ne, gt, lt, gte, lte, in, nin, exists, regex
|
||||
Value interface{} `msgpack:"value"` // Значение для сравнения
|
||||
Match string `msgpack:"match"` // Паттерн для regex
|
||||
}
|
||||
|
||||
// TriggerOperation определяет операцию, выполняемую триггером
|
||||
type TriggerOperation struct {
|
||||
Type string `msgpack:"type"` // set, unset, inc, mul, rename, currentDate
|
||||
Field string `msgpack:"field"` // Поле для операции
|
||||
Value interface{} `msgpack:"value"` // Значение для операции
|
||||
Params map[string]interface{} `msgpack:"params"`
|
||||
}
|
||||
|
||||
// TriggerExecution содержит контекст выполнения триггера
|
||||
type TriggerExecution struct {
|
||||
TriggerName string
|
||||
Event TriggerEvent
|
||||
Collection string
|
||||
Database string
|
||||
DocumentID string
|
||||
OldDocument *Document
|
||||
NewDocument *Document
|
||||
Operation string
|
||||
Timestamp time.Time
|
||||
TimestampMs int64 `msgpack:"timestamp_ms"` // Unix миллисекунды (добавлено)
|
||||
TimestampStr string `msgpack:"timestamp_str"` // Человекочитаемая строка (добавлено)
|
||||
User string
|
||||
Role string
|
||||
CustomData map[string]interface{}
|
||||
ActionResult string `msgpack:"action_result"` // Результат выполнения (добавлено)
|
||||
DurationMs int64 `msgpack:"duration_ms"` // Длительность выполнения (добавлено)
|
||||
}
|
||||
|
||||
// TriggerManager управляет триггерами в СУБД
|
||||
type TriggerManager struct {
|
||||
triggers sync.Map // map[string]*Trigger (ключ: collection|event|name)
|
||||
logger *log.Logger
|
||||
mu sync.RWMutex
|
||||
auditLog []*TriggerExecution
|
||||
maxLogSize int
|
||||
}
|
||||
|
||||
var (
|
||||
globalTriggerManager *TriggerManager
|
||||
triggerManagerOnce sync.Once
|
||||
)
|
||||
|
||||
// GetTriggerManager возвращает глобальный менеджер триггеров
|
||||
func GetTriggerManager() *TriggerManager {
|
||||
triggerManagerOnce.Do(func() {
|
||||
globalTriggerManager = &TriggerManager{
|
||||
maxLogSize: 10000,
|
||||
auditLog: make([]*TriggerExecution, 0),
|
||||
}
|
||||
})
|
||||
return globalTriggerManager
|
||||
}
|
||||
|
||||
// InitTriggerManager инициализирует менеджер триггеров с логгером
|
||||
func InitTriggerManager(logger *log.Logger) {
|
||||
tm := GetTriggerManager()
|
||||
tm.logger = logger
|
||||
if logger != nil {
|
||||
logger.Info("Trigger manager initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTrigger создаёт новый триггер (синтаксис MongoDB-like)
|
||||
// Пример: db.collection.createTrigger("triggerName", "BEFORE_INSERT", {
|
||||
// condition: { field: "status", operator: "eq", value: "active" },
|
||||
// action: "modify",
|
||||
// operations: [
|
||||
// { type: "set", field: "updated_at", value: "$$NOW" }
|
||||
// ]
|
||||
// })
|
||||
func (tm *TriggerManager) CreateTrigger(database, collection, name string, event TriggerEvent, config map[string]interface{}) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
key := tm.getTriggerKey(collection, event, name)
|
||||
if _, exists := tm.triggers.Load(key); exists {
|
||||
return fmt.Errorf("trigger '%s' already exists on %s for event %s", name, collection, event)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
trigger := &Trigger{
|
||||
Name: name,
|
||||
Collection: collection,
|
||||
Event: event,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Operations: make([]TriggerOperation, 0),
|
||||
}
|
||||
|
||||
// Парсим конфигурацию триггера
|
||||
if action, ok := config["action"].(string); ok {
|
||||
switch strings.ToLower(action) {
|
||||
case "abort":
|
||||
trigger.Action = ActionAbort
|
||||
case "skip":
|
||||
trigger.Action = ActionSkip
|
||||
case "modify":
|
||||
trigger.Action = ActionModify
|
||||
case "log":
|
||||
trigger.Action = ActionLog
|
||||
case "notify":
|
||||
trigger.Action = ActionNotify
|
||||
default:
|
||||
trigger.Action = ActionCustom
|
||||
}
|
||||
}
|
||||
|
||||
// Парсим условие
|
||||
if cond, ok := config["condition"].(map[string]interface{}); ok {
|
||||
trigger.Condition = &TriggerCondition{}
|
||||
if field, ok := cond["field"].(string); ok {
|
||||
trigger.Condition.Field = field
|
||||
}
|
||||
if operator, ok := cond["operator"].(string); ok {
|
||||
trigger.Condition.Operator = operator
|
||||
}
|
||||
if value, ok := cond["value"]; ok {
|
||||
trigger.Condition.Value = value
|
||||
}
|
||||
if match, ok := cond["match"].(string); ok {
|
||||
trigger.Condition.Match = match
|
||||
}
|
||||
}
|
||||
|
||||
// Парсим операции
|
||||
if ops, ok := config["operations"].([]interface{}); ok {
|
||||
for _, opRaw := range ops {
|
||||
opMap, ok := opRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
operation := TriggerOperation{}
|
||||
if opType, ok := opMap["type"].(string); ok {
|
||||
operation.Type = opType
|
||||
}
|
||||
if field, ok := opMap["field"].(string); ok {
|
||||
operation.Field = field
|
||||
}
|
||||
if value, ok := opMap["value"]; ok {
|
||||
operation.Value = value
|
||||
}
|
||||
if params, ok := opMap["params"].(map[string]interface{}); ok {
|
||||
operation.Params = params
|
||||
}
|
||||
trigger.Operations = append(trigger.Operations, operation)
|
||||
}
|
||||
}
|
||||
|
||||
if desc, ok := config["description"].(string); ok {
|
||||
trigger.Description = desc
|
||||
}
|
||||
|
||||
tm.triggers.Store(key, trigger)
|
||||
|
||||
// Аудит создания триггера
|
||||
LogAudit("CREATE", "TRIGGER", fmt.Sprintf("%s.%s.%s", database, collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
"action": trigger.Action,
|
||||
"description": trigger.Description,
|
||||
})
|
||||
|
||||
if tm.logger != nil {
|
||||
tm.logger.Info(fmt.Sprintf("Trigger '%s' created on %s.%s for event %s", name, database, collection, event))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropTrigger удаляет триггер
|
||||
func (tm *TriggerManager) DropTrigger(collection, event, name string) error {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
if _, exists := tm.triggers.LoadAndDelete(key); !exists {
|
||||
return fmt.Errorf("trigger '%s' not found on %s for event %s", name, collection, event)
|
||||
}
|
||||
|
||||
// Аудит удаления триггера
|
||||
LogAudit("DROP", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
})
|
||||
|
||||
if tm.logger != nil {
|
||||
tm.logger.Info(fmt.Sprintf("Trigger '%s' dropped from %s for event %s", name, collection, event))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTrigger возвращает триггер по имени
|
||||
func (tm *TriggerManager) GetTrigger(collection, event, name string) (*Trigger, error) {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
if val, ok := tm.triggers.Load(key); ok {
|
||||
return val.(*Trigger), nil
|
||||
}
|
||||
return nil, fmt.Errorf("trigger not found: %s", name)
|
||||
}
|
||||
|
||||
// ListTriggers возвращает список всех триггеров для коллекции
|
||||
func (tm *TriggerManager) ListTriggers(collection string) []*Trigger {
|
||||
triggers := make([]*Trigger, 0)
|
||||
tm.triggers.Range(func(key, value interface{}) bool {
|
||||
trigger := value.(*Trigger)
|
||||
if collection == "" || trigger.Collection == collection {
|
||||
triggers = append(triggers, trigger)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return triggers
|
||||
}
|
||||
|
||||
// ListTriggersByEvent возвращает триггеры для конкретного события
|
||||
func (tm *TriggerManager) ListTriggersByEvent(collection string, event TriggerEvent) []*Trigger {
|
||||
triggers := make([]*Trigger, 0)
|
||||
tm.triggers.Range(func(key, value interface{}) bool {
|
||||
trigger := value.(*Trigger)
|
||||
if trigger.Collection == collection && trigger.Event == event && trigger.Enabled {
|
||||
triggers = append(triggers, trigger)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return triggers
|
||||
}
|
||||
|
||||
// EnableTrigger включает триггер
|
||||
func (tm *TriggerManager) EnableTrigger(collection, event, name string) error {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
val, ok := tm.triggers.Load(key)
|
||||
if !ok {
|
||||
return fmt.Errorf("trigger not found: %s", name)
|
||||
}
|
||||
trigger := val.(*Trigger)
|
||||
trigger.mu.Lock()
|
||||
trigger.Enabled = true
|
||||
trigger.UpdatedAt = time.Now().UnixMilli()
|
||||
trigger.mu.Unlock()
|
||||
tm.triggers.Store(key, trigger)
|
||||
|
||||
// Аудит включения триггера
|
||||
LogAudit("ENABLE", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableTrigger выключает триггер
|
||||
func (tm *TriggerManager) DisableTrigger(collection, event, name string) error {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
val, ok := tm.triggers.Load(key)
|
||||
if !ok {
|
||||
return fmt.Errorf("trigger not found: %s", name)
|
||||
}
|
||||
trigger := val.(*Trigger)
|
||||
trigger.mu.Lock()
|
||||
trigger.Enabled = false
|
||||
trigger.UpdatedAt = time.Now().UnixMilli()
|
||||
trigger.mu.Unlock()
|
||||
tm.triggers.Store(key, trigger)
|
||||
|
||||
// Аудит выключения триггера
|
||||
LogAudit("DISABLE", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteTriggers выполняет все триггеры для данного события
|
||||
// Возвращает: modifiedDocument, shouldAbort, error
|
||||
func (tm *TriggerManager) ExecuteTriggers(execCtx *TriggerExecution) (*Document, bool, error) {
|
||||
triggers := tm.ListTriggersByEvent(execCtx.Collection, execCtx.Event)
|
||||
|
||||
if len(triggers) == 0 {
|
||||
return execCtx.NewDocument, false, nil
|
||||
}
|
||||
|
||||
currentDoc := execCtx.NewDocument
|
||||
if currentDoc == nil && execCtx.OldDocument != nil {
|
||||
currentDoc = execCtx.OldDocument.Clone()
|
||||
}
|
||||
|
||||
for _, trigger := range triggers {
|
||||
if !trigger.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// Проверяем условие
|
||||
if trigger.Condition != nil {
|
||||
if !tm.evaluateCondition(execCtx, trigger.Condition) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Выполняем действие триггера
|
||||
switch trigger.Action {
|
||||
case ActionAbort:
|
||||
tm.logExecution(execCtx, trigger, "aborted", startTime)
|
||||
return currentDoc, true, fmt.Errorf("operation aborted by trigger: %s", trigger.Name)
|
||||
|
||||
case ActionSkip:
|
||||
tm.logExecution(execCtx, trigger, "skipped", startTime)
|
||||
return currentDoc, true, nil
|
||||
|
||||
case ActionModify:
|
||||
if currentDoc != nil {
|
||||
currentDoc = tm.applyOperations(currentDoc, trigger.Operations, execCtx)
|
||||
}
|
||||
tm.logExecution(execCtx, trigger, "modified", startTime)
|
||||
|
||||
case ActionLog:
|
||||
tm.logExecution(execCtx, trigger, "logged", startTime)
|
||||
if tm.logger != nil {
|
||||
tm.logger.Info(fmt.Sprintf("Trigger %s executed on %s.%s (event: %s, doc: %s)",
|
||||
trigger.Name, execCtx.Database, execCtx.Collection, execCtx.Event, execCtx.DocumentID))
|
||||
}
|
||||
|
||||
case ActionNotify:
|
||||
tm.logExecution(execCtx, trigger, "notified", startTime)
|
||||
// Здесь можно отправить уведомление через WebSocket или другой канал
|
||||
}
|
||||
}
|
||||
|
||||
return currentDoc, false, nil
|
||||
}
|
||||
|
||||
// evaluateCondition проверяет условие триггера
|
||||
func (tm *TriggerManager) evaluateCondition(execCtx *TriggerExecution, cond *TriggerCondition) bool {
|
||||
var docToCheck *Document
|
||||
if execCtx.NewDocument != nil {
|
||||
docToCheck = execCtx.NewDocument
|
||||
} else if execCtx.OldDocument != nil {
|
||||
docToCheck = execCtx.OldDocument
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
fieldValue, err := docToCheck.GetField(cond.Field)
|
||||
if err != nil {
|
||||
// Поле не существует
|
||||
if cond.Operator == "exists" {
|
||||
if existsVal, ok := cond.Value.(bool); ok && !existsVal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
switch cond.Operator {
|
||||
case "eq":
|
||||
return fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", cond.Value)
|
||||
case "ne":
|
||||
return fmt.Sprintf("%v", fieldValue) != fmt.Sprintf("%v", cond.Value)
|
||||
case "gt":
|
||||
return compareNumbers(fieldValue, cond.Value) > 0
|
||||
case "lt":
|
||||
return compareNumbers(fieldValue, cond.Value) < 0
|
||||
case "gte":
|
||||
return compareNumbers(fieldValue, cond.Value) >= 0
|
||||
case "lte":
|
||||
return compareNumbers(fieldValue, cond.Value) <= 0
|
||||
case "in":
|
||||
if arr, ok := cond.Value.([]interface{}); ok {
|
||||
for _, v := range arr {
|
||||
if fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
case "nin":
|
||||
if arr, ok := cond.Value.([]interface{}); ok {
|
||||
for _, v := range arr {
|
||||
if fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
case "exists":
|
||||
if existsVal, ok := cond.Value.(bool); ok {
|
||||
return existsVal
|
||||
}
|
||||
return true
|
||||
case "regex":
|
||||
if pattern, ok := cond.Value.(string); ok {
|
||||
matched, _ := regexp.MatchString(pattern, fmt.Sprintf("%v", fieldValue))
|
||||
return matched
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// applyOperations применяет операции к документу
|
||||
func (tm *TriggerManager) applyOperations(doc *Document, ops []TriggerOperation, execCtx *TriggerExecution) *Document {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := doc.Clone()
|
||||
|
||||
for _, op := range ops {
|
||||
switch op.Type {
|
||||
case "set":
|
||||
value := tm.resolveValue(op.Value, execCtx)
|
||||
result.SetField(op.Field, value)
|
||||
|
||||
case "unset":
|
||||
result.DeleteField(op.Field)
|
||||
|
||||
case "inc":
|
||||
if incVal, ok := toFloat64(op.Value); ok {
|
||||
if current, err := result.GetField(op.Field); err == nil {
|
||||
if currVal, ok := toFloat64(current); ok {
|
||||
result.SetField(op.Field, currVal+incVal)
|
||||
}
|
||||
} else {
|
||||
result.SetField(op.Field, incVal)
|
||||
}
|
||||
}
|
||||
|
||||
case "mul":
|
||||
if mulVal, ok := toFloat64(op.Value); ok {
|
||||
if current, err := result.GetField(op.Field); err == nil {
|
||||
if currVal, ok := toFloat64(current); ok {
|
||||
result.SetField(op.Field, currVal*mulVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "rename":
|
||||
if newName, ok := op.Value.(string); ok {
|
||||
if val, err := result.GetField(op.Field); err == nil {
|
||||
result.SetField(newName, val)
|
||||
result.DeleteField(op.Field)
|
||||
}
|
||||
}
|
||||
|
||||
case "currentDate":
|
||||
result.SetField(op.Field, time.Now().UnixMilli())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// resolveValue разрешает специальные значения типа $$NOW, $$USER
|
||||
func (tm *TriggerManager) resolveValue(value interface{}, execCtx *TriggerExecution) interface{} {
|
||||
if strVal, ok := value.(string); ok {
|
||||
switch strVal {
|
||||
case "$$NOW":
|
||||
return time.Now().UnixMilli()
|
||||
case "$$USER":
|
||||
if execCtx.User != "" {
|
||||
return execCtx.User
|
||||
}
|
||||
return "anonymous"
|
||||
case "$$ROLE":
|
||||
if execCtx.Role != "" {
|
||||
return execCtx.Role
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// logExecution логирует выполнение триггера (модифицировано с добавлением временных меток)
|
||||
func (tm *TriggerManager) logExecution(execCtx *TriggerExecution, trigger *Trigger, result string, startTime time.Time) {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
duration := time.Since(startTime)
|
||||
now := time.Now()
|
||||
nowMs := now.UnixMilli()
|
||||
nowStr := now.Format("2006-01-02 15:04:05.000")
|
||||
|
||||
execCtx.TriggerName = trigger.Name
|
||||
execCtx.Timestamp = now
|
||||
execCtx.TimestampMs = nowMs
|
||||
execCtx.TimestampStr = nowStr
|
||||
execCtx.ActionResult = result
|
||||
execCtx.DurationMs = duration.Milliseconds()
|
||||
|
||||
if len(tm.auditLog) >= tm.maxLogSize {
|
||||
tm.auditLog = tm.auditLog[1:]
|
||||
}
|
||||
tm.auditLog = append(tm.auditLog, execCtx)
|
||||
|
||||
// Аудит выполнения триггера
|
||||
LogAudit("TRIGGER_EXECUTE", "TRIGGER", fmt.Sprintf("%s.%s", trigger.Collection, trigger.Name), map[string]interface{}{
|
||||
"event": execCtx.Event,
|
||||
"result": result,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
"document_id": execCtx.DocumentID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetTriggerExecutionLog возвращает лог выполнения триггеров
|
||||
func (tm *TriggerManager) GetTriggerExecutionLog() []*TriggerExecution {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
result := make([]*TriggerExecution, len(tm.auditLog))
|
||||
copy(result, tm.auditLog)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTriggerExecutionLogFiltered возвращает отфильтрованный лог выполнения триггеров (добавлено)
|
||||
func (tm *TriggerManager) GetTriggerExecutionLogFiltered(triggerName, collection string, fromTime, toTime int64) []*TriggerExecution {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
result := make([]*TriggerExecution, 0)
|
||||
for _, entry := range tm.auditLog {
|
||||
if triggerName != "" && entry.TriggerName != triggerName {
|
||||
continue
|
||||
}
|
||||
if collection != "" && entry.Collection != collection {
|
||||
continue
|
||||
}
|
||||
if fromTime > 0 && entry.TimestampMs < fromTime {
|
||||
continue
|
||||
}
|
||||
if toTime > 0 && entry.TimestampMs > toTime {
|
||||
continue
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearTriggerLog очищает лог выполнения триггеров (добавлено)
|
||||
func (tm *TriggerManager) ClearTriggerLog() {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
tm.auditLog = make([]*TriggerExecution, 0)
|
||||
LogAudit("CLEAR", "TRIGGER_LOG", "all", nil)
|
||||
}
|
||||
|
||||
// GetTriggerExecutionStats возвращает статистику выполнения триггеров (добавлено)
|
||||
func (tm *TriggerManager) GetTriggerExecutionStats() map[string]interface{} {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
triggerStats := make(map[string]map[string]interface{})
|
||||
for _, entry := range tm.auditLog {
|
||||
if _, ok := triggerStats[entry.TriggerName]; !ok {
|
||||
triggerStats[entry.TriggerName] = map[string]interface{}{
|
||||
"total_executions": 0,
|
||||
"aborted": 0,
|
||||
"skipped": 0,
|
||||
"modified": 0,
|
||||
"logged": 0,
|
||||
"notified": 0,
|
||||
"total_duration_ms": int64(0),
|
||||
"avg_duration_ms": float64(0),
|
||||
}
|
||||
}
|
||||
|
||||
stats := triggerStats[entry.TriggerName]
|
||||
stats["total_executions"] = stats["total_executions"].(int) + 1
|
||||
stats["total_duration_ms"] = stats["total_duration_ms"].(int64) + entry.DurationMs
|
||||
|
||||
switch entry.ActionResult {
|
||||
case "aborted":
|
||||
stats["aborted"] = stats["aborted"].(int) + 1
|
||||
case "skipped":
|
||||
stats["skipped"] = stats["skipped"].(int) + 1
|
||||
case "modified":
|
||||
stats["modified"] = stats["modified"].(int) + 1
|
||||
case "logged":
|
||||
stats["logged"] = stats["logged"].(int) + 1
|
||||
case "notified":
|
||||
stats["notified"] = stats["notified"].(int) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Вычисляем среднюю длительность
|
||||
for _, stats := range triggerStats {
|
||||
if totalExec, ok := stats["total_executions"].(int); ok && totalExec > 0 {
|
||||
if totalDuration, ok := stats["total_duration_ms"].(int64); ok {
|
||||
stats["avg_duration_ms"] = float64(totalDuration) / float64(totalExec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_trigger_executions": len(tm.auditLog),
|
||||
"triggers_stats": triggerStats,
|
||||
"log_size": len(tm.auditLog),
|
||||
"max_log_size": tm.maxLogSize,
|
||||
}
|
||||
}
|
||||
|
||||
// getTriggerKey возвращает ключ для хранения триггера
|
||||
func (tm *TriggerManager) getTriggerKey(collection string, event TriggerEvent, name string) string {
|
||||
return fmt.Sprintf("%s|%s|%s", collection, event, name)
|
||||
}
|
||||
|
||||
// compareNumbers сравнивает два числа
|
||||
func compareNumbers(a, b interface{}) int {
|
||||
aVal, aOk := toFloat64(a)
|
||||
bVal, bOk := toFloat64(b)
|
||||
|
||||
if aOk && bOk {
|
||||
if aVal < bVal {
|
||||
return -1
|
||||
}
|
||||
if aVal > bVal {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// MongoDBLikeTriggerConfig создаёт конфигурацию триггера в стиле MongoDB
|
||||
// Пример использования:
|
||||
// config := MongoDBLikeTriggerConfig().
|
||||
// On("BEFORE_INSERT").
|
||||
// Condition("status", "eq", "active").
|
||||
// Set("updated_at", "$$NOW").
|
||||
// Build()
|
||||
func MongoDBLikeTriggerConfig() *TriggerConfigBuilder {
|
||||
return &TriggerConfigBuilder{
|
||||
config: make(map[string]interface{}),
|
||||
ops: make([]interface{}, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerConfigBuilder строитель конфигурации триггера
|
||||
type TriggerConfigBuilder struct {
|
||||
config map[string]interface{}
|
||||
ops []interface{}
|
||||
}
|
||||
|
||||
// On устанавливает событие триггера
|
||||
func (b *TriggerConfigBuilder) On(event string) *TriggerConfigBuilder {
|
||||
b.config["event"] = event
|
||||
return b
|
||||
}
|
||||
|
||||
// Condition добавляет условие
|
||||
func (b *TriggerConfigBuilder) Condition(field, operator string, value interface{}) *TriggerConfigBuilder {
|
||||
b.config["condition"] = map[string]interface{}{
|
||||
"field": field,
|
||||
"operator": operator,
|
||||
"value": value,
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ConditionRegex добавляет regex условие
|
||||
func (b *TriggerConfigBuilder) ConditionRegex(field, pattern string) *TriggerConfigBuilder {
|
||||
b.config["condition"] = map[string]interface{}{
|
||||
"field": field,
|
||||
"operator": "regex",
|
||||
"value": pattern,
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Set добавляет операцию установки поля
|
||||
func (b *TriggerConfigBuilder) Set(field string, value interface{}) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "set",
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Unset добавляет операцию удаления поля
|
||||
func (b *TriggerConfigBuilder) Unset(field string) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "unset",
|
||||
"field": field,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Inc добавляет операцию инкремента
|
||||
func (b *TriggerConfigBuilder) Inc(field string, value float64) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "inc",
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Mul добавляет операцию умножения
|
||||
func (b *TriggerConfigBuilder) Mul(field string, value float64) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "mul",
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Rename добавляет операцию переименования поля
|
||||
func (b *TriggerConfigBuilder) Rename(oldName, newName string) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "rename",
|
||||
"field": oldName,
|
||||
"value": newName,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// CurrentDate добавляет операцию установки текущей даты
|
||||
func (b *TriggerConfigBuilder) CurrentDate(field string) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "currentDate",
|
||||
"field": field,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Action устанавливает действие триггера
|
||||
func (b *TriggerConfigBuilder) Action(action string) *TriggerConfigBuilder {
|
||||
b.config["action"] = action
|
||||
return b
|
||||
}
|
||||
|
||||
// Description устанавливает описание триггера
|
||||
func (b *TriggerConfigBuilder) Description(desc string) *TriggerConfigBuilder {
|
||||
b.config["description"] = desc
|
||||
return b
|
||||
}
|
||||
|
||||
// Build собирает конфигурацию
|
||||
func (b *TriggerConfigBuilder) Build() map[string]interface{} {
|
||||
b.config["operations"] = b.ops
|
||||
return b.config
|
||||
}
|
||||
Reference in New Issue
Block a user