Files
futriix/internal/storage/document.go
T

1138 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/storage/document.go
// Назначение: Определение структуры документа, его методов для работы
// с полями, кортежами (вложенными документами) и сериализации в MessagePack.
// Документ является основной единицей хранения в СУБД futriis.
//
// ИСПРАВЛЕНО (2026): Устранён race condition в Document.
// Все методы, читающие/записывающие метаданные (ID, CreatedAt, UpdatedAt,
// DeletedAt, Version, Compressed, OriginalSize), используют sync.RWMutex.
//
// ИЗМЕНЕНО (2026): Убраны вызовы AuditFieldOperation из SetField/DeleteField.
// Аудит выполняется на уровне Collection (AuditDocumentOperation).
//
// ИЗМЕНЕНО (2026): time-travel реализован через WAL
// (см. transactions.go: GetDocumentAtTimestamp).
//
// ИСПРАВЛЕНО (2026-10, аудит):
// 1. Serialize/Deserialize больше не теряют поля документа.
// Раньше serializer.Marshal(docCopy) сериализовал только
// экспортируемые поля Document, а fieldsPtr (atomic.Value)
// в msgpack не попадал — поля молча исчезали.
// Теперь используется промежуточная структура documentWire.
// 2. Формат временных меток исправлен с "02-012006 15:04:05.000"
// на "02-01-2006 15:04:05.000".
// 3. SetField/DeleteField проверяют результат casFieldsWithBackoff.
// Раньше Version рос даже при провале CAS.
// 4. Compress/Decompress/GetCompressionRatio реализованы по-настоящему.
// Реализован Путь 1: Compress освобождает fieldsPtr и хранит
// только compressedData. Первое чтение через loadFields
// автоматически вызывает Decompress и возвращает поля.
// 5. Deserialize корректно восстанавливает Compressed из wire-данных,
// не перетирает UpdatedAt текущим временем.
// 6. Удалён локальный min — в Go 1.21+ есть встроенный.
// 7. SetNestedField поддерживает map[string]interface{} в пути —
// симметрично GetNestedField.
// 8. Добавлен метод GetCompressedData() для использования сжатого
// представления во внешнем коде (например, в будущих checkpoint-ах).
// 9. Serialize уважает флаг Compressed: если документ сжат,
// возвращает compressedData.
package storage
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"math/rand"
"strings"
"sync"
"sync/atomic"
"time"
"futriis/internal/compression"
"futriis/internal/serializer"
"github.com/google/uuid"
)
// =============================================================================
// КОНСТАНТЫ
// =============================================================================
const (
// Максимальное количество попыток CAS перед применением backoff.
maxCASRetriesBeforeBackoff = 10
// Базовая задержка backoff в наносекундах.
baseBackoffNanos = 100
// Максимальная задержка backoff в наносекундах.
maxBackoffNanos = 10000
// Максимальное количество попыток CAS всего.
maxCASTotalAttempts = 1000
)
// timeLayout — канонический формат временных меток проекта.
const timeLayout = "02-01-2006 15:04:05.000"
// =============================================================================
// FIELD VERSION — ЗАЩИТА ОТ ABA
// =============================================================================
// fieldVersionWrapper оборачивает map полей с версией для защиты от ABA.
type fieldVersionWrapper struct {
fields map[string]interface{}
version uint64
}
// =============================================================================
// DOCUMENT
// =============================================================================
// Document представляет документ в коллекции.
//
// Состояние документа может быть в одном из двух видов:
// - разжатое: fieldsPtr содержит все поля, compressedData пусто,
// Compressed == false;
// - сжатое: fieldsPtr содержит пустую map, compressedData содержит
// msgpack-сжатые данные, Compressed == true.
//
// При первом чтении (GetField/GetFields/HasField) сжатый документ
// автоматически распаковывается и снова становится разжатым.
// Это позволяет вызывать Compress() для экономии памяти, не заботясь
// о дальнейшей работе с документом — она «прозрачно» разожмётся.
type Document struct {
mu sync.RWMutex // защита метаданных и compressedData
ID string `msgpack:"_id"`
fieldsPtr atomic.Value // *fieldVersionWrapper — lock-free хранилище полей
// compressedData — сжатое представление документа.
// Заполнено только когда Compressed == true. Защищено d.mu.
compressedData []byte
CreatedAt int64 `msgpack:"created_at"`
UpdatedAt int64 `msgpack:"updated_at"`
DeletedAt int64 `msgpack:"deleted_at"`
Version uint64 `msgpack:"version"`
Compressed bool `msgpack:"compressed"`
OriginalSize int64 `msgpack:"original_size"`
}
// documentWire — структура для сериализации документа в MessagePack.
// Поля документа попадают в msgpack только через это поле.
type documentWire struct {
ID string `msgpack:"_id"`
Fields map[string]interface{} `msgpack:"fields"`
CreatedAt int64 `msgpack:"created_at"`
UpdatedAt int64 `msgpack:"updated_at"`
DeletedAt int64 `msgpack:"deleted_at"`
Version uint64 `msgpack:"version"`
Compressed bool `msgpack:"compressed"`
OriginalSize int64 `msgpack:"original_size"`
}
// =============================================================================
// TUPLE
// =============================================================================
// Tuple представляет вложенный документ (аналог кортежа).
type Tuple struct {
Fields map[string]interface{} `msgpack:"fields"`
CreatedAt int64 `msgpack:"created_at"`
UpdatedAt int64 `msgpack:"updated_at"`
mu sync.RWMutex
}
// Field представляет отдельное поле документа.
type Field struct {
Name string `msgpack:"name"`
Type FieldType `msgpack:"type"`
Value interface{} `msgpack:"value"`
UpdatedAt int64 `msgpack:"updated_at"`
}
// FieldType определяет тип поля документа.
type FieldType int
const (
TypeString FieldType = iota
TypeNumber
TypeBoolean
TypeTuple
TypeArray
TypeNull
)
// =============================================================================
// СОЗДАНИЕ ДОКУМЕНТА
// =============================================================================
// NewDocument создаёт новый документ с автоматической генерацией ID.
func NewDocument() *Document {
now := time.Now().UnixMilli()
d := &Document{
ID: uuid.New().String(),
CreatedAt: now,
UpdatedAt: now,
DeletedAt: 0,
Version: 1,
Compressed: false,
OriginalSize: 0,
}
d.fieldsPtr.Store(&fieldVersionWrapper{
fields: make(map[string]interface{}),
version: 0,
})
return d
}
// NewDocumentWithID создаёт документ с указанным ID.
func NewDocumentWithID(id string) *Document {
now := time.Now().UnixMilli()
d := &Document{
ID: id,
CreatedAt: now,
UpdatedAt: now,
DeletedAt: 0,
Version: 1,
Compressed: false,
OriginalSize: 0,
}
d.fieldsPtr.Store(&fieldVersionWrapper{
fields: make(map[string]interface{}),
version: 0,
})
return d
}
// =============================================================================
// РАБОТА С ПОЛЯМИ
// =============================================================================
// loadFieldsWrapper возвращает обёртку полей (lock-free).
// НЕ распаковывает сжатый документ — для этого используйте loadFields.
func (d *Document) loadFieldsWrapper() *fieldVersionWrapper {
val := d.fieldsPtr.Load()
if val == nil {
return &fieldVersionWrapper{
fields: make(map[string]interface{}),
version: 0,
}
}
return val.(*fieldVersionWrapper)
}
// loadFields возвращает карту полей.
//
// Если документ сжат, лениво распаковывает его. После распаковки
// документ становится разжатым (Compressed == false), и дальнейшие
// чтения идут без накладных расходов.
//
// ВНИМАНИЕ: возвращает ссылку на внутреннюю map. Не мутировать!
func (d *Document) loadFields() map[string]interface{} {
d.mu.RLock()
compressed := d.Compressed
d.mu.RUnlock()
if compressed {
if err := d.Decompress(); err != nil {
// Если распаковка упала, возвращаем пустую map —
// вызывающий увидит документ без полей. Это безопаснее,
// чем паника, хотя сигнализирует о проблеме.
return make(map[string]interface{})
}
}
wrapper := d.loadFieldsWrapper()
if wrapper == nil || wrapper.fields == nil {
return make(map[string]interface{})
}
return wrapper.fields
}
// storeFields сохраняет карту полей (lock-free).
func (d *Document) storeFields(newMap map[string]interface{}) {
oldWrapper := d.loadFieldsWrapper()
newVersion := uint64(0)
if oldWrapper != nil {
newVersion = oldWrapper.version + 1
}
d.fieldsPtr.Store(&fieldVersionWrapper{
fields: newMap,
version: newVersion,
})
}
// casFields выполняет CAS-операцию для полей с защитой от ABA.
func (d *Document) casFields(oldWrapper, newWrapper *fieldVersionWrapper) bool {
if oldWrapper == nil || newWrapper == nil {
return false
}
if newWrapper.version <= oldWrapper.version {
newWrapper.version = oldWrapper.version + 1
}
return d.fieldsPtr.CompareAndSwap(oldWrapper, newWrapper)
}
// casFieldsWithBackoff выполняет CAS с backoff для предотвращения livelock.
func (d *Document) casFieldsWithBackoff(updateFunc func(map[string]interface{}) map[string]interface{}) bool {
attempt := 0
for {
oldWrapper := d.loadFieldsWrapper()
newFields := make(map[string]interface{}, len(oldWrapper.fields))
for k, v := range oldWrapper.fields {
newFields[k] = v
}
newFields = updateFunc(newFields)
newWrapper := &fieldVersionWrapper{
fields: newFields,
version: oldWrapper.version + 1,
}
if d.casFields(oldWrapper, newWrapper) {
return true
}
attempt++
if attempt > maxCASRetriesBeforeBackoff {
// builtin min доступен в Go 1.21+; go.mod требует 1.26.
shift := min(attempt-maxCASRetriesBeforeBackoff, 10)
backoff := baseBackoffNanos * (1 << shift)
if backoff > maxBackoffNanos {
backoff = maxBackoffNanos
}
jitter := rand.Int63n(int64(backoff)/2 + 1)
totalBackoff := backoff + int(jitter)
time.Sleep(time.Duration(totalBackoff))
}
if attempt > maxCASTotalAttempts {
return false
}
}
}
// ensureDecompressed гарантирует, что документ разжат.
// Если он сжат — вызывает Decompress. Идемпотентна.
func (d *Document) ensureDecompressed() error {
d.mu.RLock()
compressed := d.Compressed
d.mu.RUnlock()
if !compressed {
return nil
}
return d.Decompress()
}
// SetField устанавливает значение поля документа.
//
// Если документ сжат — сначала распаковывает его.
// Увеличение Version/UpdatedAt выполняется только при успешном CAS.
func (d *Document) SetField(name string, value interface{}) {
if err := d.ensureDecompressed(); err != nil {
return
}
ok := d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} {
fields[name] = value
return fields
})
if !ok {
return
}
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
d.compressedData = nil
d.OriginalSize = 0
d.mu.Unlock()
}
// GetField возвращает значение поля документа.
// Если документ сжат — распаковывает его (лениво).
func (d *Document) GetField(name string) (interface{}, error) {
fields := d.loadFields()
if val, ok := fields[name]; ok {
return val, nil
}
return nil, fmt.Errorf("field not found: %s", name)
}
// DeleteField удаляет поле из документа.
//
// Если документ сжат — сначала распаковывает его.
func (d *Document) DeleteField(name string) {
if err := d.ensureDecompressed(); err != nil {
return
}
ok := d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} {
delete(fields, name)
return fields
})
if !ok {
return
}
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
d.compressedData = nil
d.OriginalSize = 0
d.mu.Unlock()
}
// HasField проверяет наличие поля в документе.
func (d *Document) HasField(name string) bool {
fields := d.loadFields()
_, ok := fields[name]
return ok
}
// GetFields возвращает копию всех полей документа.
// Если документ сжат, он будет автоматически распакован.
func (d *Document) GetFields() map[string]interface{} {
fields := d.loadFields()
copyMap := make(map[string]interface{}, len(fields))
for k, v := range fields {
copyMap[k] = v
}
return copyMap
}
// ToMap возвращает полное представление документа в виде map.
func (d *Document) ToMap() map[string]interface{} {
fields := d.GetFields()
result := make(map[string]interface{}, len(fields)+5)
for k, v := range fields {
result[k] = v
}
d.mu.RLock()
result["_id"] = d.ID
result["_created_at"] = d.CreatedAt
result["_updated_at"] = d.UpdatedAt
result["_deleted_at"] = d.DeletedAt
result["_version"] = d.Version
d.mu.RUnlock()
return result
}
// 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.
//
// Если документ сжат, возвращает compressedData без изменений —
// это позволяет не разжимать документ при сохранении в checkpoint.
// Иначе сериализует через documentWire (поля + метаданные).
func (d *Document) Serialize() ([]byte, error) {
d.mu.RLock()
if d.Compressed && len(d.compressedData) > 0 {
out := make([]byte, len(d.compressedData))
copy(out, d.compressedData)
d.mu.RUnlock()
return out, nil
}
// Документ разжат — сериализуем.
// Захватываем поля, не отпуская RLock, чтобы избежать TOCTOU
// с параллельным SetField. GetFields внутри берёт RLock повторно
// (это рекурсивный RLock, в Go RWMutex это разрешено).
fields := d.GetFields()
wire := documentWire{
ID: d.ID,
Fields: fields,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
DeletedAt: d.DeletedAt,
Version: d.Version,
Compressed: false,
OriginalSize: 0,
}
d.mu.RUnlock()
return serializer.Marshal(&wire)
}
// SerializeCompressed сериализует и сжимает документ.
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
data, err := d.Serialize()
if err != nil {
return nil, err
}
if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize {
compressed, err := compression.Compress(data, compressionConfig)
if err != nil {
return data, nil
}
if len(compressed) < len(data) {
return compressed, nil
}
return data, nil
}
return data, nil
}
// GetCompressedData возвращает копию сжатых данных документа.
// Если документ не сжат — возвращает nil.
//
// ДОБАВЛЕНО: метод для будущего использования compressedData
// в checkpoint-ах. Сейчас persistence.go хранит документы через
// json.Marshal(GetFields()), но при переходе на сжатые checkpoint-и
// этот метод позволит получить готовые сжатые байты без разжима.
func (d *Document) GetCompressedData() []byte {
d.mu.RLock()
defer d.mu.RUnlock()
if !d.Compressed || len(d.compressedData) == 0 {
return nil
}
out := make([]byte, len(d.compressedData))
copy(out, d.compressedData)
return out
}
// Deserialize десериализует документ из MessagePack.
func (d *Document) Deserialize(data []byte) error {
decompressed, err := compression.DecompressAuto(data)
if err != nil {
return fmt.Errorf("failed to decompress document: %v", err)
}
var wire documentWire
if err := serializer.Unmarshal(decompressed, &wire); err != nil {
return fmt.Errorf("failed to unmarshal document: %v", err)
}
if wire.Fields == nil {
wire.Fields = make(map[string]interface{})
}
d.mu.Lock()
d.ID = wire.ID
d.CreatedAt = wire.CreatedAt
d.UpdatedAt = wire.UpdatedAt
d.DeletedAt = wire.DeletedAt
d.Version = wire.Version
d.Compressed = false
d.OriginalSize = 0
d.compressedData = nil
d.mu.Unlock()
d.storeFields(wire.Fields)
return nil
}
// =============================================================================
// КЛОНИРОВАНИЕ
// =============================================================================
// Clone создаёт глубокую копию документа.
//
// Сжатый документ остаётся сжатым: копируется compressedData,
// а fieldsPtr инициализируется пустой map (как у оригинала).
func (d *Document) Clone() *Document {
d.mu.RLock()
compressedCopy := make([]byte, len(d.compressedData))
copy(compressedCopy, d.compressedData)
clone := &Document{
ID: d.ID,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
DeletedAt: d.DeletedAt,
Version: d.Version,
Compressed: d.Compressed,
OriginalSize: d.OriginalSize,
compressedData: compressedCopy,
}
d.mu.RUnlock()
if clone.Compressed && len(compressedCopy) > 0 {
clone.storeFields(make(map[string]interface{}))
} else {
fields := d.GetFields()
copiedFields := make(map[string]interface{}, len(fields))
for k, v := range fields {
copiedFields[k] = deepCopyValue(v)
}
clone.storeFields(copiedFields)
}
return clone
}
// =============================================================================
// ОБНОВЛЕНИЕ / УДАЛЕНИЕ / ВОССТАНОВЛЕНИЕ
// =============================================================================
// Update применяет обновление к документу.
// Если документ сжат — сначала распаковывает его.
func (d *Document) Update(updates map[string]interface{}) error {
if err := d.ensureDecompressed(); err != nil {
return err
}
success := d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} {
for k, v := range updates {
fields[k] = v
}
return fields
})
if !success {
return fmt.Errorf("failed to update document after multiple attempts (possible high contention)")
}
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
d.compressedData = nil
d.OriginalSize = 0
d.mu.Unlock()
return nil
}
// SoftDelete мягко удаляет документ.
//
// Метаданные обновляются всегда, независимо от сжатия. Поля не трогаем —
// если документ был сжат, он остаётся сжатым.
func (d *Document) SoftDelete() {
d.mu.Lock()
d.DeletedAt = time.Now().UnixMilli()
d.UpdatedAt = d.DeletedAt
d.Version++
d.mu.Unlock()
}
// 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()
d.DeletedAt = 0
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.mu.Unlock()
}
// =============================================================================
// ВРЕМЕННЫЕ МЕТКИ (СТРОКИ)
// =============================================================================
// GetDeletedAtStr возвращает человекочитаемую строку времени удаления.
func (d *Document) GetDeletedAtStr() string {
d.mu.RLock()
deletedAt := d.DeletedAt
d.mu.RUnlock()
if deletedAt == 0 {
return ""
}
return time.UnixMilli(deletedAt).Format(timeLayout)
}
// GetCreatedAtStr возвращает человекочитаемую строку времени создания.
func (d *Document) GetCreatedAtStr() string {
d.mu.RLock()
createdAt := d.CreatedAt
d.mu.RUnlock()
return time.UnixMilli(createdAt).Format(timeLayout)
}
// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления.
func (d *Document) GetUpdatedAtStr() string {
d.mu.RLock()
updatedAt := d.UpdatedAt
d.mu.RUnlock()
return time.UnixMilli(updatedAt).Format(timeLayout)
}
// =============================================================================
// СЖАТИЕ (ПУТЬ 1 — ОСВОБОЖДЕНИЕ ПАМЯТИ)
// =============================================================================
// Compress сжимает документ в памяти.
//
// После успешного сжатия:
// - compressedData содержит сжатое представление (msgpack + compression);
// - fieldsPtr заменяется на пустую map — поля освобождаются;
// - Compressed = true, OriginalSize = размер оригинальной сериализации;
// - Version не меняется.
//
// Первое чтение через GetField/GetFields/HasField лениво распакует
// документ, и он снова станет разжатым. Если хочется удержать сжатое
// состояние — не читайте поля.
func (d *Document) Compress(config *compression.Config) error {
if config == nil {
return fmt.Errorf("nil compression config")
}
if !config.Enabled {
return nil
}
d.mu.Lock()
defer d.mu.Unlock()
if d.Compressed {
return nil
}
// 1. Сериализуем во временную wire-структуру.
// loadFieldsWrapper безопасен: документ разжат, RLock не нужен —
// мы уже держим Lock.
fields := d.loadFieldsForCompressLocked()
wire := documentWire{
ID: d.ID,
Fields: fields,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
DeletedAt: d.DeletedAt,
Version: d.Version,
Compressed: false,
OriginalSize: 0,
}
raw, err := serializer.Marshal(&wire)
if err != nil {
return fmt.Errorf("failed to serialize document for compression: %v", err)
}
// 2. Проверяем минимальный размер.
if len(raw) < config.MinSize {
return nil
}
// 3. Сжимаем.
compressed, err := compression.Compress(raw, config)
if err != nil {
return fmt.Errorf("failed to compress document: %v", err)
}
// 4. Если сжатие не уменьшило размер — оставляем документ разжатым.
if len(compressed) >= len(raw) {
return nil
}
// 5. Сохраняем результат и освобождаем поля.
d.compressedData = compressed
d.OriginalSize = int64(len(raw))
d.Compressed = true
// Поля теперь в compressedData — очищаем fieldsPtr.
// storeFields не берёт d.mu (мы уже держим Lock), конфликта нет.
d.fieldsPtr.Store(&fieldVersionWrapper{
fields: make(map[string]interface{}),
version: 0,
})
return nil
}
// loadFieldsForCompressLocked возвращает копию полей для сериализации.
// Вызывается под d.mu.Lock() из Compress. Ожидает, что документ разжат.
func (d *Document) loadFieldsForCompressLocked() map[string]interface{} {
wrapper := d.loadFieldsWrapper()
if wrapper == nil || wrapper.fields == nil {
return make(map[string]interface{})
}
copyMap := make(map[string]interface{}, len(wrapper.fields))
for k, v := range wrapper.fields {
copyMap[k] = v
}
return copyMap
}
// Decompress распаковывает документ в памяти.
//
// Восстанавливает поля из compressedData и сбрасывает флаг Compressed.
// После этого fieldsPtr содержит все поля, compressedData = nil.
// Если документ не сжат — no-op.
func (d *Document) Decompress() error {
d.mu.Lock()
defer d.mu.Unlock()
if !d.Compressed {
return nil
}
if len(d.compressedData) == 0 {
// Согласованность нарушена — считаем документ несжатым,
// чтобы не зацикливаться на попытках распаковки.
d.Compressed = false
d.OriginalSize = 0
d.fieldsPtr.Store(&fieldVersionWrapper{
fields: make(map[string]interface{}),
version: 0,
})
return fmt.Errorf("compressed flag set, but compressedData is empty")
}
decompressed, err := compression.DecompressAuto(d.compressedData)
if err != nil {
return fmt.Errorf("failed to decompress document: %v", err)
}
var wire documentWire
if err := serializer.Unmarshal(decompressed, &wire); err != nil {
return fmt.Errorf("failed to unmarshal decompressed document: %v", err)
}
if wire.Fields == nil {
wire.Fields = make(map[string]interface{})
}
// Восстанавливаем поля и сбрасываем сжатое представление.
d.fieldsPtr.Store(&fieldVersionWrapper{
fields: wire.Fields,
version: 0,
})
d.compressedData = nil
d.Compressed = false
d.OriginalSize = 0
return nil
}
// GetCompressionRatio возвращает коэффициент сжатия как долю
// compressedSize/originalSize:
// - 1.0 — документ не сжат;
// - 0.5 — сжатый размер в два раза меньше оригинала.
func (d *Document) GetCompressionRatio() float64 {
d.mu.RLock()
compressed := d.Compressed
originalSize := d.OriginalSize
compressedSize := int64(len(d.compressedData))
d.mu.RUnlock()
if !compressed || originalSize == 0 {
return 1.0
}
return float64(compressedSize) / float64(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{}:
copyMap := make(map[string]interface{}, len(v))
for k, val := range v {
copyMap[k] = deepCopyValue(val)
}
return copyMap
case []interface{}:
copySlice := make([]interface{}, len(v))
for i, val := range v {
copySlice[i] = deepCopyValue(val)
}
return copySlice
default:
return v
}
}
// =============================================================================
// TUPLE — ВЛОЖЕННЫЕ ДОКУМЕНТЫ
// =============================================================================
// 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()
copyMap := make(map[string]interface{}, len(t.Fields))
for k, v := range t.Fields {
copyMap[k] = v
}
return copyMap
}
// 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")
}
var 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 устанавливает значение по точечному пути.
//
// Если документ сжат — сначала распаковывает его.
// Поддерживает map[string]interface{} в пути — симметрично GetNestedField.
func (d *Document) SetNestedField(path string, value interface{}) error {
if err := d.ensureDecompressed(); err != nil {
return err
}
parts := strings.Split(path, ".")
if len(parts) == 0 {
return fmt.Errorf("empty path")
}
if len(parts) == 1 {
d.SetField(parts[0], value)
return nil
}
var current interface{} = d
for i := 0; i < len(parts)-1; i++ {
part := parts[i]
switch v := current.(type) {
case *Document:
field, err := v.GetField(part)
if err != nil {
newTuple := NewTuple()
v.SetField(part, newTuple)
current = newTuple
continue
}
switch child := field.(type) {
case *Tuple:
current = child
case map[string]interface{}:
current = child
default:
return fmt.Errorf("field %s is not a navigable container", part)
}
case *Tuple:
val, err := v.Get(part)
if err != nil {
newTuple := NewTuple()
v.Set(part, newTuple)
current = newTuple
continue
}
switch child := val.(type) {
case *Tuple:
current = child
case map[string]interface{}:
current = child
default:
return fmt.Errorf("field %s is not a navigable container", part)
}
case map[string]interface{}:
val, ok := v[part]
if !ok {
newTuple := NewTuple()
v[part] = newTuple
current = newTuple
continue
}
switch child := val.(type) {
case *Tuple:
current = child
case map[string]interface{}:
current = child
default:
return fmt.Errorf("field %s is not a navigable container", part)
}
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)
case map[string]interface{}:
v[lastPart] = value
default:
return fmt.Errorf("cannot set field on non-document value")
}
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
d.compressedData = nil
d.OriginalSize = 0
d.mu.Unlock()
return nil
}
// =============================================================================
// ПРОЧЕЕ (утилиты)
// =============================================================================
// EncodeGzip — вспомогательная функция для отладочной сериализации.
func EncodeGzip(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
_ = w.Close()
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// DecodeGzip — вспомогательная функция, обратная EncodeGzip.
func DecodeGzip(data []byte) ([]byte, error) {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer r.Close()
out, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return out, nil
}