init elowdb go-port commit

This commit is contained in:
41 changed files with 7273 additions and 0 deletions

200
pkg/linedb/cache.go Normal file
View File

@@ -0,0 +1,200 @@
package linedb
import (
"sync"
"time"
)
// CacheEntry представляет запись в кэше
type CacheEntry struct {
Data any
Timestamp time.Time
TTL time.Duration
}
// RecordCache представляет кэш записей
type RecordCache struct {
cache map[string]*CacheEntry
mutex sync.RWMutex
maxSize int
ttl time.Duration
stopChan chan struct{}
}
// NewRecordCache создает новый кэш
func NewRecordCache(maxSize int, ttl time.Duration) *RecordCache {
cache := &RecordCache{
cache: make(map[string]*CacheEntry),
maxSize: maxSize,
ttl: ttl,
stopChan: make(chan struct{}),
}
// Запускаем очистку устаревших записей только если TTL > 0
if ttl > 0 {
go cache.cleanupLoop()
}
return cache
}
// Set устанавливает значение в кэш
func (c *RecordCache) Set(key string, value any) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Проверяем размер кэша
if len(c.cache) >= c.maxSize {
c.evictOldest()
}
c.cache[key] = &CacheEntry{
Data: value,
Timestamp: time.Now(),
TTL: c.ttl,
}
}
// Get получает значение из кэша
func (c *RecordCache) Get(key string) (any, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
entry, exists := c.cache[key]
if !exists {
return nil, false
}
// Проверяем TTL
if c.ttl > 0 && time.Since(entry.Timestamp) > c.ttl {
delete(c.cache, key)
return nil, false
}
return entry.Data, true
}
// Has проверяет наличие ключа в кэше
func (c *RecordCache) Has(key string) bool {
_, exists := c.Get(key)
return exists
}
// Delete удаляет ключ из кэша
func (c *RecordCache) Delete(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.cache, key)
}
// Clear очищает кэш
func (c *RecordCache) Clear() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.cache = make(map[string]*CacheEntry)
}
// Stop останавливает кэш
func (c *RecordCache) Stop() {
close(c.stopChan)
}
// Size возвращает размер кэша
func (c *RecordCache) Size() int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.cache)
}
// GetFlatCacheMap возвращает плоскую карту кэша
func (c *RecordCache) GetFlatCacheMap() map[string]*CacheEntry {
c.mutex.RLock()
defer c.mutex.RUnlock()
result := make(map[string]*CacheEntry)
for key, entry := range c.cache {
result[key] = entry
}
return result
}
// evictOldest удаляет самую старую запись из кэша
func (c *RecordCache) evictOldest() {
var oldestKey string
var oldestTime time.Time
for key, entry := range c.cache {
if oldestKey == "" || entry.Timestamp.Before(oldestTime) {
oldestKey = key
oldestTime = entry.Timestamp
}
}
if oldestKey != "" {
delete(c.cache, oldestKey)
}
}
// cleanupLoop запускает цикл очистки устаревших записей
func (c *RecordCache) cleanupLoop() {
// Используем более безопасный интервал
interval := c.ttl / 4
if interval < time.Second {
interval = time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.cleanup()
case <-c.stopChan:
return
}
}
}
// cleanup очищает устаревшие записи
func (c *RecordCache) cleanup() {
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
for key, entry := range c.cache {
if c.ttl > 0 && now.Sub(entry.Timestamp) > c.ttl {
delete(c.cache, key)
}
}
}
// SetByRecord устанавливает запись в кэш по записи
func (c *RecordCache) SetByRecord(record any, collectionName string) {
key := toString(record) + ":" + collectionName
c.Set(key, record)
}
// UpdateCacheAfterInsert обновляет кэш после вставки
func (c *RecordCache) UpdateCacheAfterInsert(record any, collectionName string) {
c.SetByRecord(record, collectionName)
}
// toString преобразует значение в строку
func toString(value any) string {
switch v := value.(type) {
case string:
return v
case int:
return string(rune(v))
case float64:
return string(rune(int(v)))
case map[string]any:
if id, exists := v["id"]; exists {
return toString(id)
}
return "unknown"
default:
return "unknown"
}
}

484
pkg/linedb/jsonl_file.go Normal file
View File

@@ -0,0 +1,484 @@
package linedb
import (
"bufio"
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
goccyjson "github.com/goccy/go-json"
)
// Функции сериализации по умолчанию (используют go-json)
func defaultJSONMarshal(v any) ([]byte, error) {
return goccyjson.Marshal(v)
}
func defaultJSONUnmarshal(data []byte, v any) error {
return goccyjson.Unmarshal(data, v)
}
// JSONLFile представляет адаптер для работы с JSONL файлами
type JSONLFile struct {
filename string
cypherKey string
allocSize int
collectionName string
hashFilename string
options JSONLFileOptions
initialized bool
inTransaction bool
transaction *Transaction
mutex sync.RWMutex
selectCache map[string]any
events map[string][]func(any)
// Функции сериализации
jsonMarshal func(any) ([]byte, error)
jsonUnmarshal func([]byte, any) error
}
// NewJSONLFile создает новый экземпляр JSONLFile
func NewJSONLFile(filename string, cypherKey string, options JSONLFileOptions) *JSONLFile {
hash := sha256.Sum256([]byte(filename))
hashFilename := fmt.Sprintf("%x", hash)
collectionName := options.CollectionName
if collectionName == "" {
collectionName = hashFilename
}
allocSize := options.AllocSize
if allocSize == 0 {
allocSize = 256
}
// Определяем функции сериализации
jsonMarshal := defaultJSONMarshal
jsonUnmarshal := defaultJSONUnmarshal
// Используем пользовательские функции если они предоставлены
if options.JSONMarshal != nil {
jsonMarshal = options.JSONMarshal
}
if options.JSONUnmarshal != nil {
jsonUnmarshal = options.JSONUnmarshal
}
return &JSONLFile{
filename: filename,
cypherKey: cypherKey,
allocSize: allocSize,
collectionName: collectionName,
hashFilename: hashFilename,
options: options,
selectCache: make(map[string]any),
events: make(map[string][]func(any)),
jsonMarshal: jsonMarshal,
jsonUnmarshal: jsonUnmarshal,
}
}
// Init инициализирует файл
func (j *JSONLFile) Init(force bool, options LineDbAdapterOptions) error {
j.mutex.Lock()
defer j.mutex.Unlock()
if j.initialized && !force {
return nil
}
// Создаем директорию если не существует
dir := filepath.Dir(j.filename)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Создаем файл если не существует
if _, err := os.Stat(j.filename); os.IsNotExist(err) {
file, err := os.Create(j.filename)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
file.Close()
}
j.initialized = true
return nil
}
// Read читает все записи из файла
func (j *JSONLFile) Read(options LineDbAdapterOptions) ([]any, error) {
j.mutex.RLock()
defer j.mutex.RUnlock()
if !j.initialized {
return nil, fmt.Errorf("file not initialized")
}
file, err := os.Open(j.filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var records []any
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// Расшифровываем если нужно
if j.cypherKey != "" {
decoded, err := base64.StdEncoding.DecodeString(line)
if err != nil {
if j.options.SkipInvalidLines {
continue
}
return nil, fmt.Errorf("failed to decode base64: %w", err)
}
line = string(decoded)
}
var record any
if err := j.jsonUnmarshal([]byte(line), &record); err != nil {
if j.options.SkipInvalidLines {
continue
}
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
records = append(records, record)
}
return records, scanner.Err()
}
// Write записывает данные в файл
func (j *JSONLFile) Write(data any, options LineDbAdapterOptions) error {
j.mutex.Lock()
defer j.mutex.Unlock()
if !j.initialized {
return fmt.Errorf("file not initialized")
}
records, ok := data.([]any)
if !ok {
records = []any{data}
}
file, err := os.OpenFile(j.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open file for writing: %w", err)
}
defer file.Close()
for _, record := range records {
jsonData, err := j.jsonMarshal(record)
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
line := string(jsonData)
// Шифруем если нужно
if j.cypherKey != "" {
line = base64.StdEncoding.EncodeToString([]byte(line))
}
// Дополняем до allocSize
if len(line) < j.allocSize {
line += strings.Repeat(" ", j.allocSize-len(line)-1)
}
if _, err := file.WriteString(line + "\n"); err != nil {
return fmt.Errorf("failed to write line: %w", err)
}
}
return nil
}
// Insert вставляет новые записи
func (j *JSONLFile) Insert(data any, options LineDbAdapterOptions) ([]any, error) {
records, ok := data.([]any)
if !ok {
records = []any{data}
}
// Генерируем ID если нужно
for i, record := range records {
if recordMap, ok := record.(map[string]any); ok {
if recordMap["id"] == nil || recordMap["id"] == "" {
recordMap["id"] = time.Now().UnixNano()
records[i] = recordMap
}
}
}
if err := j.Write(records, options); err != nil {
return nil, err
}
return records, nil
}
// ReadByFilter читает записи по фильтру
func (j *JSONLFile) ReadByFilter(filter any, options LineDbAdapterOptions) ([]any, error) {
allRecords, err := j.Read(options)
if err != nil {
return nil, err
}
var filteredRecords []any
for _, record := range allRecords {
if j.matchesFilter(record, filter, options.StrictCompare) {
filteredRecords = append(filteredRecords, record)
}
}
return filteredRecords, nil
}
// Update обновляет записи
func (j *JSONLFile) Update(data any, filter any, options LineDbAdapterOptions) ([]any, error) {
// Читаем все записи
allRecords, err := j.Read(options)
if err != nil {
return nil, err
}
// Фильтруем записи для обновления
var recordsToUpdate []any
updateData, ok := data.(map[string]any)
if !ok {
return nil, fmt.Errorf("update data must be a map")
}
for _, record := range allRecords {
if j.matchesFilter(record, filter, options.StrictCompare) {
// Обновляем запись
if recordMap, ok := record.(map[string]any); ok {
for key, value := range updateData {
recordMap[key] = value
}
recordsToUpdate = append(recordsToUpdate, recordMap)
}
}
}
// Перезаписываем файл
if err := j.rewriteFile(allRecords); err != nil {
return nil, err
}
return recordsToUpdate, nil
}
// Delete удаляет записи
func (j *JSONLFile) Delete(data any, options LineDbAdapterOptions) ([]any, error) {
// Читаем все записи
allRecords, err := j.Read(options)
if err != nil {
return nil, err
}
var remainingRecords []any
var deletedRecords []any
for _, record := range allRecords {
if j.matchesFilter(record, data, options.StrictCompare) {
deletedRecords = append(deletedRecords, record)
} else {
remainingRecords = append(remainingRecords, record)
}
}
// Перезаписываем файл
if err := j.rewriteFile(remainingRecords); err != nil {
return nil, err
}
return deletedRecords, nil
}
// GetFilename возвращает имя файла
func (j *JSONLFile) GetFilename() string {
return j.filename
}
// GetCollectionName возвращает имя коллекции
func (j *JSONLFile) GetCollectionName() string {
return j.collectionName
}
// GetOptions возвращает опции
func (j *JSONLFile) GetOptions() JSONLFileOptions {
return j.options
}
// GetEncryptKey возвращает ключ шифрования
func (j *JSONLFile) GetEncryptKey() string {
return j.cypherKey
}
// matchesFilter проверяет соответствие записи фильтру
func (j *JSONLFile) matchesFilter(record any, filter any, strictCompare bool) bool {
if filter == nil {
return true
}
switch f := filter.(type) {
case string:
// Простая проверка по строке
recordStr := fmt.Sprintf("%v", record)
if strictCompare {
return recordStr == f
}
return strings.Contains(strings.ToLower(recordStr), strings.ToLower(f))
case map[string]any:
// Проверка по полям
if recordMap, ok := record.(map[string]any); ok {
for key, filterValue := range f {
recordValue, exists := recordMap[key]
if !exists {
return false
}
if !j.valuesMatch(recordValue, filterValue, strictCompare) {
return false
}
}
return true
}
case func(any) bool:
// Функция фильтрации
return f(record)
}
return false
}
// valuesMatch сравнивает значения
func (j *JSONLFile) valuesMatch(a, b any, strictCompare bool) bool {
if strictCompare {
return a == b
}
// Для строк - нечувствительное к регистру сравнение
if aStr, ok := a.(string); ok {
if bStr, ok := b.(string); ok {
return strings.Contains(strings.ToLower(aStr), strings.ToLower(bStr))
}
}
return a == b
}
// rewriteFile перезаписывает файл новыми данными
func (j *JSONLFile) rewriteFile(records []any) error {
// Создаем временный файл
tempFile := j.filename + ".tmp"
file, err := os.Create(tempFile)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer file.Close()
// Записываем данные во временный файл
for _, record := range records {
jsonData, err := j.jsonMarshal(record)
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
line := string(jsonData)
// Шифруем если нужно
if j.cypherKey != "" {
line = base64.StdEncoding.EncodeToString([]byte(line))
}
// Дополняем до allocSize
if len(line) < j.allocSize {
line += strings.Repeat(" ", j.allocSize-len(line)-1)
}
if _, err := file.WriteString(line + "\n"); err != nil {
return fmt.Errorf("failed to write line: %w", err)
}
}
// Заменяем оригинальный файл
if err := os.Rename(tempFile, j.filename); err != nil {
return fmt.Errorf("failed to rename temp file: %w", err)
}
return nil
}
// Destroy очищает ресурсы
func (j *JSONLFile) Destroy() {
j.mutex.Lock()
defer j.mutex.Unlock()
j.initialized = false
j.selectCache = nil
j.events = nil
}
// WithTransaction выполняет операцию в транзакции
func (j *JSONLFile) WithTransaction(callback func(*JSONLFile, LineDbAdapterOptions) error, transactionOptions TransactionOptions, methodsOptions LineDbAdapterOptions) error {
// Создаем транзакцию
tx := NewTransaction("write", generateTransactionID(), transactionOptions.Timeout, transactionOptions.Rollback)
// Создаем резервную копию если нужно
if transactionOptions.Rollback {
if err := tx.CreateBackup(j.filename); err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
}
j.transaction = tx
j.inTransaction = true
defer func() {
j.inTransaction = false
j.transaction = nil
}()
// Выполняем callback
if err := callback(j, methodsOptions); err != nil {
// Откатываем изменения если нужно
if transactionOptions.Rollback {
if restoreErr := tx.RestoreFromBackup(j.filename); restoreErr != nil {
return fmt.Errorf("failed to restore from backup: %w", restoreErr)
}
}
return err
}
// Очищаем резервную копию
if err := tx.CleanupBackup(); err != nil {
return fmt.Errorf("failed to cleanup backup: %w", err)
}
return nil
}
// generateTransactionID генерирует ID транзакции
func generateTransactionID() string {
return fmt.Sprintf("tx_%d", time.Now().UnixNano())
}

View File

@@ -0,0 +1,75 @@
package linedb
import (
"sync"
)
// LastIDManager управляет последними ID для коллекций
type LastIDManager struct {
lastIDs map[string]int
mutex sync.RWMutex
}
var lastIDManagerInstance *LastIDManager
var lastIDManagerOnce sync.Once
// GetInstance возвращает единственный экземпляр LastIDManager
func GetLastIDManagerInstance() *LastIDManager {
lastIDManagerOnce.Do(func() {
lastIDManagerInstance = &LastIDManager{
lastIDs: make(map[string]int),
}
})
return lastIDManagerInstance
}
// GetLastID получает последний ID для коллекции
func (l *LastIDManager) GetLastID(filename string) int {
l.mutex.RLock()
defer l.mutex.RUnlock()
baseFileName := l.getBaseFileName(filename)
return l.lastIDs[baseFileName]
}
// SetLastID устанавливает последний ID для коллекции
func (l *LastIDManager) SetLastID(filename string, id int) {
l.mutex.Lock()
defer l.mutex.Unlock()
baseFileName := l.getBaseFileName(filename)
currentID := l.lastIDs[baseFileName]
if currentID < id {
l.lastIDs[baseFileName] = id
}
}
// IncrementLastID увеличивает последний ID для коллекции
func (l *LastIDManager) IncrementLastID(filename string) int {
l.mutex.Lock()
defer l.mutex.Unlock()
baseFileName := l.getBaseFileName(filename)
currentID := l.lastIDs[baseFileName]
newID := currentID + 1
l.lastIDs[baseFileName] = newID
return newID
}
// getBaseFileName извлекает базовое имя файла
func (l *LastIDManager) getBaseFileName(filename string) string {
if idx := l.findPartitionSeparator(filename); idx != -1 {
return filename[:idx]
}
return filename
}
// findPartitionSeparator находит разделитель партиции
func (l *LastIDManager) findPartitionSeparator(filename string) int {
for i, char := range filename {
if char == '_' {
return i
}
}
return -1
}

962
pkg/linedb/line_db.go Normal file
View File

@@ -0,0 +1,962 @@
package linedb
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
// LineDb представляет основную базу данных
// Соответствует TypeScript классу LineDb
type LineDb struct {
adapters map[string]*JSONLFile
collections map[string]string
partitionFunctions map[string]func(any) string
mutex sync.RWMutex
cacheSize int
cacheExternal *RecordCache
nextIDFn func(any, string) (any, error)
lastIDManager *LastIDManager
// inTransaction bool
cacheTTL time.Duration
constructorOptions *LineDbOptions
initOptions *LineDbInitOptions
}
// NewLineDb создает новый экземпляр LineDb
func NewLineDb(options *LineDbOptions, adapters ...*JSONLFile) *LineDb {
if options == nil {
options = &LineDbOptions{}
}
db := &LineDb{
adapters: make(map[string]*JSONLFile),
collections: make(map[string]string),
partitionFunctions: make(map[string]func(any) string),
cacheSize: options.CacheSize,
cacheTTL: options.CacheTTL,
lastIDManager: GetLastIDManagerInstance(),
constructorOptions: options,
}
// Инициализируем кэш если нужно
if db.cacheSize > 0 && db.cacheTTL > 0 {
db.cacheExternal = NewRecordCache(db.cacheSize, db.cacheTTL)
}
// Добавляем готовые адаптеры
for _, adapter := range adapters {
collectionName := adapter.GetCollectionName()
db.adapters[collectionName] = adapter
db.collections[collectionName] = adapter.GetFilename()
}
return db
}
// Init инициализирует базу данных
func (db *LineDb) Init(force bool, initOptions *LineDbInitOptions) error {
db.mutex.Lock()
defer db.mutex.Unlock()
if initOptions == nil {
return fmt.Errorf("no init options provided")
}
// Устанавливаем опции
db.initOptions = initOptions
db.cacheSize = initOptions.CacheSize
db.cacheTTL = initOptions.CacheTTL
// Инициализируем кэш если нужно
if db.cacheSize > 0 && db.cacheTTL > 0 {
db.cacheExternal = NewRecordCache(db.cacheSize, db.cacheTTL)
}
// Создаем папку базы данных
dbFolder := initOptions.DBFolder
if dbFolder == "" {
dbFolder = "linedb"
}
if err := os.MkdirAll(dbFolder, 0755); err != nil {
return fmt.Errorf("failed to create database folder: %w", err)
}
// Сохраняем функции партиционирования
for _, partition := range initOptions.Partitions {
if partition.PartIDFn != nil {
db.partitionFunctions[partition.CollectionName] = partition.PartIDFn
}
}
// Создаем адаптеры для коллекций
for i, adapterOptions := range initOptions.Collections {
collectionName := adapterOptions.CollectionName
if collectionName == "" {
collectionName = fmt.Sprintf("collection_%d", i+1)
}
// Создаем путь к файлу
filename := filepath.Join(dbFolder, collectionName+".jsonl")
// Создаем адаптер
adapter := NewJSONLFile(filename, adapterOptions.EncryptKeyForLineDb, adapterOptions)
// Инициализируем адаптер
if err := adapter.Init(force, LineDbAdapterOptions{}); err != nil {
return fmt.Errorf("failed to init adapter for collection %s: %w", collectionName, err)
}
// Добавляем в карту адаптеров
db.adapters[collectionName] = adapter
db.collections[collectionName] = filename
}
return nil
}
// Read читает все записи из коллекции
func (db *LineDb) Read(collectionName string, options LineDbAdapterOptions) ([]any, error) {
db.mutex.RLock()
defer db.mutex.RUnlock()
if collectionName == "" {
collectionName = db.getFirstCollection()
}
adapter, exists := db.adapters[collectionName]
if !exists {
return nil, fmt.Errorf("collection %s not found", collectionName)
}
return adapter.Read(options)
}
// Insert вставляет новые записи в коллекцию
func (db *LineDb) Insert(data any, collectionName string, options LineDbAdapterOptions) error {
// Блокируем только если не в транзакции
if !options.InTransaction {
db.mutex.Lock()
defer db.mutex.Unlock()
}
if collectionName == "" {
collectionName = db.getFirstCollection()
}
// Проверяем debug tag
if options.DebugTag == "error" {
return fmt.Errorf("test error")
}
// Обрабатываем данные
dataArray := db.normalizeDataArray(data)
resultDataArray := make([]any, 0, len(dataArray))
for _, item := range dataArray {
itemMap, ok := item.(map[string]any)
if !ok {
return fmt.Errorf("invalid data format")
}
// Генерируем ID если отсутствует
if itemMap["id"] == nil || db.isInvalidID(itemMap["id"]) {
newID, err := db.NextID(item, collectionName)
if err != nil {
return fmt.Errorf("failed to generate ID: %w", err)
}
// Проверяем уникальность ID
done := false
count := 0
for !done && count < 10000 {
// Проверяем, что ID не существует в результатах
exists := false
for _, resultItem := range resultDataArray {
if resultMap, ok := resultItem.(map[string]any); ok {
if resultMap["id"] == newID {
exists = true
break
}
}
}
if !exists {
done = true
} else {
newID, err = db.NextID(item, collectionName)
if err != nil {
return fmt.Errorf("failed to generate unique ID: %w", err)
}
}
count++
}
if count >= 10000 {
return fmt.Errorf("can not generate new id for 10 000 iterations")
}
itemMap["id"] = newID
resultDataArray = append(resultDataArray, itemMap)
} else {
// Проверяем существование записи если не пропускаем проверку
if !options.SkipCheckExistingForWrite {
filter := map[string]any{"id": itemMap["id"]}
for key, partitionAdapter := range db.adapters {
if strings.Contains(key, collectionName) {
exists, err := partitionAdapter.ReadByFilter(filter, LineDbAdapterOptions{InTransaction: true})
if err != nil {
return fmt.Errorf("failed to check existing record: %w", err)
}
if len(exists) > 0 {
return fmt.Errorf("record with id %v already exists in collection %s", collectionName, itemMap["id"])
}
}
}
}
resultDataArray = append(resultDataArray, itemMap)
}
}
// Записываем данные с флагом транзакции
writeOptions := LineDbAdapterOptions{InTransaction: true, InternalCall: true}
if err := db.Write(resultDataArray, collectionName, writeOptions); err != nil {
return fmt.Errorf("failed to write data: %w", err)
}
// Обновляем кэш
if db.cacheExternal != nil {
for _, item := range resultDataArray {
db.cacheExternal.UpdateCacheAfterInsert(item, collectionName)
}
}
return nil
}
// Write записывает данные в коллекцию
func (db *LineDb) Write(data any, collectionName string, options LineDbAdapterOptions) error {
// Блокируем только если не в транзакции
if !options.InTransaction {
db.mutex.Lock()
defer db.mutex.Unlock()
}
if collectionName == "" {
collectionName = db.getFirstCollection()
}
// Проверяем партиционирование
if db.isCollectionPartitioned(collectionName) {
dataArray := db.normalizeDataArray(data)
for _, item := range dataArray {
adapter, err := db.getPartitionAdapter(item, collectionName)
if err != nil {
return fmt.Errorf("failed to get partition adapter: %w", err)
}
if err := adapter.Write(item, options); err != nil {
return fmt.Errorf("failed to write to partition: %w", err)
}
}
return nil
}
// Обычная запись
adapter, exists := db.adapters[collectionName]
if !exists {
return fmt.Errorf("collection %s not found", collectionName)
}
return adapter.Write(data, options)
}
// Update обновляет записи в коллекции
func (db *LineDb) Update(data any, collectionName string, filter any, options LineDbAdapterOptions) ([]any, error) {
// Блокируем только если не в транзакции
if !options.InTransaction {
db.mutex.Lock()
defer db.mutex.Unlock()
}
if collectionName == "" {
collectionName = db.getFirstCollection()
}
// Проверяем конфликт ID
if dataMap, ok := data.(map[string]any); ok {
if filterMap, ok := filter.(map[string]any); ok {
if dataMap["id"] != nil && filterMap["id"] != nil {
if !db.compareIDs(dataMap["id"], filterMap["id"]) {
return nil, fmt.Errorf("you can not update record id with filter by another id. Use delete and insert instead")
}
}
}
}
// Проверяем партиционирование
if db.isCollectionPartitioned(collectionName) {
return db.updatePartitioned(data, collectionName, filter, options)
}
// Обычное обновление
adapter, exists := db.adapters[collectionName]
if !exists {
return nil, fmt.Errorf("collection %s not found", collectionName)
}
return adapter.Update(data, filter, options)
}
// Delete удаляет записи из коллекции
func (db *LineDb) Delete(data any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
// Блокируем только если не в транзакции
if !options.InTransaction {
db.mutex.Lock()
defer db.mutex.Unlock()
}
if collectionName == "" {
collectionName = db.getFirstCollection()
}
// Проверяем партиционирование
if db.isCollectionPartitioned(collectionName) {
return db.deletePartitioned(data, collectionName, options)
}
// Обычное удаление
adapter, exists := db.adapters[collectionName]
if !exists {
return nil, fmt.Errorf("collection %s not found", collectionName)
}
return adapter.Delete(data, options)
}
// Select выполняет выборку с поддержкой цепочки
func (db *LineDb) Select(filter any, collectionName string, options LineDbAdapterOptions) (any, error) {
result, err := db.ReadByFilter(filter, collectionName, options)
if err != nil {
return nil, err
}
if options.ReturnChain {
return NewCollectionChain(result), nil
}
return result, nil
}
// ReadByFilter читает записи по фильтру
func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
db.mutex.RLock()
defer db.mutex.RUnlock()
if collectionName == "" {
collectionName = db.getFirstCollection()
}
// Проверяем кэш
if db.cacheExternal != nil && !options.InTransaction {
if cached, exists := db.cacheExternal.Get(db.generateCacheKey(filter, collectionName)); exists {
if cachedArray, ok := cached.([]any); ok {
return cachedArray, nil
}
}
}
// Проверяем партиционирование
if db.isCollectionPartitioned(collectionName) {
return db.readByFilterPartitioned(filter, collectionName, options)
}
// Обычная фильтрация
adapter, exists := db.adapters[collectionName]
if !exists {
return nil, fmt.Errorf("collection %s not found", collectionName)
}
result, err := adapter.ReadByFilter(filter, options)
if err != nil {
return nil, err
}
// Обновляем кэш
if db.cacheExternal != nil && !options.InTransaction {
db.cacheExternal.Set(db.generateCacheKey(filter, collectionName), result)
}
return result, nil
}
// NextID генерирует следующий ID
func (db *LineDb) NextID(data any, collectionName string) (any, error) {
if db.nextIDFn != nil {
return db.nextIDFn(data, collectionName)
}
// Используем LastIDManager по умолчанию
lastID := db.lastIDManager.GetLastID(collectionName)
newID := lastID + 1
db.lastIDManager.SetLastID(collectionName, newID)
return newID, nil
}
// LastSequenceID возвращает последний последовательный ID
func (db *LineDb) LastSequenceID(collectionName string) int {
if collectionName == "" {
collectionName = db.getFirstCollection()
}
return db.lastIDManager.GetLastID(collectionName)
}
// ClearCache очищает кэш
func (db *LineDb) ClearCache(collectionName string, options LineDbAdapterOptions) error {
// Блокируем только если не в транзакции
if !options.InTransaction {
db.mutex.Lock()
defer db.mutex.Unlock()
}
if db.cacheExternal != nil {
if collectionName == "" {
db.cacheExternal.Clear()
} else {
// Очищаем только записи для конкретной коллекции
// Это упрощенная реализация
db.cacheExternal.Clear()
}
}
return nil
}
// Close закрывает базу данных
func (db *LineDb) Close() {
db.mutex.Lock()
defer db.mutex.Unlock()
// Закрываем все адаптеры
for _, adapter := range db.adapters {
adapter.Destroy()
}
// Останавливаем и очищаем кэш
if db.cacheExternal != nil {
db.cacheExternal.Stop()
db.cacheExternal.Clear()
}
// Очищаем карты
db.adapters = make(map[string]*JSONLFile)
db.collections = make(map[string]string)
db.partitionFunctions = make(map[string]func(any) string)
}
// Вспомогательные методы
func (db *LineDb) getFirstCollection() string {
for name := range db.adapters {
return name
}
return ""
}
func (db *LineDb) getBaseCollectionName(collectionName string) string {
if idx := strings.Index(collectionName, "_"); idx != -1 {
return collectionName[:idx]
}
return collectionName
}
func (db *LineDb) isCollectionPartitioned(collectionName string) bool {
_, exists := db.partitionFunctions[collectionName]
return exists
}
func (db *LineDb) getPartitionFiles(collectionName string) ([]string, error) {
baseName := db.getBaseCollectionName(collectionName)
var files []string
for name, filename := range db.collections {
if strings.HasPrefix(name, baseName+"_") {
files = append(files, filename)
}
}
return files, nil
}
func (db *LineDb) getPartitionAdapter(data any, collectionName string) (*JSONLFile, error) {
partitionFn, exists := db.partitionFunctions[collectionName]
if !exists {
return nil, fmt.Errorf("partition function not found for collection %s", collectionName)
}
partitionID := partitionFn(data)
partitionName := fmt.Sprintf("%s_%s", collectionName, partitionID)
adapter, exists := db.adapters[partitionName]
if !exists {
// Создаем новый адаптер для партиции
filename := filepath.Join(db.initOptions.DBFolder, partitionName+".jsonl")
adapter = NewJSONLFile(filename, "", JSONLFileOptions{CollectionName: partitionName})
if err := adapter.Init(false, LineDbAdapterOptions{}); err != nil {
return nil, fmt.Errorf("failed to init partition adapter: %w", err)
}
db.adapters[partitionName] = adapter
db.collections[partitionName] = filename
}
return adapter, nil
}
func (db *LineDb) GetMaxID(records []any) int {
maxID := 0
for _, record := range records {
if recordMap, ok := record.(map[string]any); ok {
if id, ok := recordMap["id"]; ok {
if idInt, ok := id.(int); ok && idInt > maxID {
maxID = idInt
}
}
}
}
return maxID
}
func (db *LineDb) matchesFilter(record any, filter any, strictCompare bool) bool {
if recordMap, ok := record.(map[string]any); ok {
if filterMap, ok := filter.(map[string]any); ok {
for key, filterValue := range filterMap {
if recordValue, exists := recordMap[key]; exists {
if !db.valuesMatch(recordValue, filterValue, strictCompare) {
return false
}
} else if strictCompare {
return false
}
}
return true
}
}
return false
}
func (db *LineDb) valuesMatch(a, b any, strictCompare bool) bool {
if strictCompare {
return a == b
}
// Нестрогое сравнение
if a == b {
return true
}
// Сравнение строк
if aStr, ok := a.(string); ok {
if bStr, ok := b.(string); ok {
return strings.EqualFold(aStr, bStr)
}
}
// Сравнение чисел
if aNum, ok := db.toNumber(a); ok {
if bNum, ok := db.toNumber(b); ok {
return aNum == bNum
}
}
return false
}
func (db *LineDb) toNumber(value any) (float64, bool) {
switch v := value.(type) {
case int:
return float64(v), true
case int64:
return float64(v), true
case float64:
return v, true
case string:
if f, err := strconv.ParseFloat(v, 64); err == nil {
return f, true
}
}
return 0, false
}
func (db *LineDb) normalizeDataArray(data any) []any {
switch v := data.(type) {
case []any:
return v
case any:
return []any{v}
default:
return []any{data}
}
}
func (db *LineDb) isInvalidID(id any) bool {
if id == nil {
return true
}
if idNum, ok := id.(int); ok {
return idNum <= -1
}
return false
}
func (db *LineDb) compareIDs(a, b any) bool {
return a == b
}
func (db *LineDb) generateCacheKey(filter any, collectionName string) string {
// Упрощенная реализация генерации ключа кэша
return fmt.Sprintf("%s:%v", collectionName, filter)
}
func (db *LineDb) updatePartitioned(data any, collectionName string, filter any, options LineDbAdapterOptions) ([]any, error) {
// Получаем все партиции
partitionFiles, err := db.getPartitionFiles(collectionName)
if err != nil {
return nil, err
}
var allResults []any
for _, filename := range partitionFiles {
// Находим адаптер по имени файла
var adapter *JSONLFile
for name, adapterFile := range db.collections {
if adapterFile == filename {
adapter = db.adapters[name]
break
}
}
if adapter != nil {
results, err := adapter.Update(data, filter, options)
if err != nil {
return nil, err
}
allResults = append(allResults, results...)
}
}
return allResults, nil
}
func (db *LineDb) deletePartitioned(data any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
// Получаем все партиции
partitionFiles, err := db.getPartitionFiles(collectionName)
if err != nil {
return nil, err
}
var allResults []any
for _, filename := range partitionFiles {
// Находим адаптер по имени файла
var adapter *JSONLFile
for name, adapterFile := range db.collections {
if adapterFile == filename {
adapter = db.adapters[name]
break
}
}
if adapter != nil {
results, err := adapter.Delete(data, options)
if err != nil {
return nil, err
}
allResults = append(allResults, results...)
}
}
return allResults, nil
}
func (db *LineDb) readByFilterPartitioned(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
// Получаем все партиции
partitionFiles, err := db.getPartitionFiles(collectionName)
if err != nil {
return nil, err
}
var allResults []any
for _, filename := range partitionFiles {
// Находим адаптер по имени файла
var adapter *JSONLFile
for name, adapterFile := range db.collections {
if adapterFile == filename {
adapter = db.adapters[name]
break
}
}
if adapter != nil {
results, err := adapter.ReadByFilter(filter, options)
if err != nil {
return nil, err
}
allResults = append(allResults, results...)
}
}
return allResults, nil
}
// Добавляем недостающие методы
// SelectWithPagination выполняет выборку с пагинацией
func (db *LineDb) SelectWithPagination(filter any, page, limit int, collectionName string, options LineDbAdapterOptions) (*PaginatedResult, error) {
if page < 1 {
page = 1
}
if limit < 1 {
limit = 20
}
// Получаем все данные
allData, err := db.ReadByFilter(filter, collectionName, options)
if err != nil {
return nil, err
}
total := len(allData)
pages := (total + limit - 1) / limit // Округление вверх
// Вычисляем индексы для пагинации
start := (page - 1) * limit
end := start + limit
if end > total {
end = total
}
var data []any
if start < total {
data = allData[start:end]
}
return &PaginatedResult{
Data: data,
Total: total,
Limit: limit,
Pages: pages,
Page: page,
}, nil
}
// Join выполняет операцию JOIN между коллекциями
func (db *LineDb) Join(leftCollection, rightCollection any, options JoinOptions) (*CollectionChain, error) {
// Получаем данные левой коллекции
var leftData []any
switch v := leftCollection.(type) {
case string:
data, err := db.Read(v, LineDbAdapterOptions{InTransaction: options.InTransaction})
if err != nil {
return nil, err
}
leftData = data
case []any:
leftData = v
default:
return nil, fmt.Errorf("invalid left collection type")
}
// Получаем данные правой коллекции
var rightData []any
switch v := rightCollection.(type) {
case string:
data, err := db.Read(v, LineDbAdapterOptions{InTransaction: options.InTransaction})
if err != nil {
return nil, err
}
rightData = data
case []any:
rightData = v
default:
return nil, fmt.Errorf("invalid right collection type")
}
// Применяем фильтры
if options.LeftFilter != nil {
leftData = db.applyFilter(leftData, options.LeftFilter, options.StrictCompare)
}
if options.RightFilter != nil {
rightData = db.applyFilter(rightData, options.RightFilter, options.StrictCompare)
}
// Выполняем JOIN
var result []any
switch options.Type {
case JoinTypeInner:
result = db.innerJoin(leftData, rightData, options)
case JoinTypeLeft:
result = db.leftJoin(leftData, rightData, options)
case JoinTypeRight:
result = db.rightJoin(leftData, rightData, options)
case JoinTypeFull:
result = db.fullJoin(leftData, rightData, options)
default:
return nil, fmt.Errorf("unsupported join type: %s", options.Type)
}
return NewCollectionChain(result), nil
}
func (db *LineDb) applyFilter(data []any, filter map[string]any, strictCompare bool) []any {
var result []any
for _, item := range data {
if db.matchesFilter(item, filter, strictCompare) {
result = append(result, item)
}
}
return result
}
func (db *LineDb) innerJoin(leftData, rightData []any, options JoinOptions) []any {
var result []any
for _, left := range leftData {
for _, right := range rightData {
if db.matchJoinFields(left, right, options.LeftFields, options.RightFields, options.StrictCompare) {
result = append(result, JoinResult{Left: left, Right: right})
if options.OnlyOneFromRight {
break
}
}
}
}
return result
}
func (db *LineDb) leftJoin(leftData, rightData []any, options JoinOptions) []any {
var result []any
for _, left := range leftData {
matched := false
for _, right := range rightData {
if db.matchJoinFields(left, right, options.LeftFields, options.RightFields, options.StrictCompare) {
result = append(result, JoinResult{Left: left, Right: right})
matched = true
if options.OnlyOneFromRight {
break
}
}
}
if !matched {
result = append(result, JoinResult{Left: left, Right: nil})
}
}
return result
}
func (db *LineDb) rightJoin(leftData, rightData []any, options JoinOptions) []any {
var result []any
for _, right := range rightData {
matched := false
for _, left := range leftData {
if db.matchJoinFields(left, right, options.LeftFields, options.RightFields, options.StrictCompare) {
result = append(result, JoinResult{Left: left, Right: right})
matched = true
if options.OnlyOneFromRight {
break
}
}
}
if !matched {
result = append(result, JoinResult{Left: nil, Right: right})
}
}
return result
}
func (db *LineDb) fullJoin(leftData, rightData []any, options JoinOptions) []any {
// Объединяем LEFT и RIGHT JOIN
leftResult := db.leftJoin(leftData, rightData, options)
rightResult := db.rightJoin(leftData, rightData, options)
// Удаляем дубликаты
seen := make(map[string]bool)
var result []any
for _, item := range append(leftResult, rightResult...) {
key := db.generateJoinKey(item)
if !seen[key] {
seen[key] = true
result = append(result, item)
}
}
return result
}
func (db *LineDb) matchJoinFields(left, right any, leftFields, rightFields []string, strictCompare bool) bool {
if len(leftFields) != len(rightFields) {
return false
}
leftMap, leftOk := left.(map[string]any)
rightMap, rightOk := right.(map[string]any)
if !leftOk || !rightOk {
return false
}
for i, leftField := range leftFields {
rightField := rightFields[i]
leftValue := leftMap[leftField]
rightValue := rightMap[rightField]
if !db.valuesMatch(leftValue, rightValue, strictCompare) {
return false
}
}
return true
}
func (db *LineDb) generateJoinKey(item any) string {
// Упрощенная реализация генерации ключа для JOIN
if joinResult, ok := item.(JoinResult); ok {
return fmt.Sprintf("%v:%v", joinResult.Left, joinResult.Right)
}
return fmt.Sprintf("%v", item)
}
// Getter методы для совместимости с TypeScript версией
func (db *LineDb) GetActualCacheSize() int {
if db.cacheExternal != nil {
return db.cacheExternal.Size()
}
return 0
}
func (db *LineDb) GetLimitCacheSize() int {
return db.cacheSize
}
func (db *LineDb) GetCacheMap() map[string]*CacheEntry {
if db.cacheExternal != nil {
return db.cacheExternal.GetFlatCacheMap()
}
return make(map[string]*CacheEntry)
}
func (db *LineDb) GetFirstCollection() string {
return db.getFirstCollection()
}

147
pkg/linedb/transaction.go Normal file
View File

@@ -0,0 +1,147 @@
package linedb
import (
"fmt"
"os"
"time"
)
// Transaction представляет транзакцию
type Transaction struct {
transactionMode string
transactionID string
timeoutMs int
timeoutID *time.Timer
rollback bool
backupFile string
doNotDeleteBackupFile bool
// mutex sync.RWMutex
active bool
}
// NewTransaction создает новую транзакцию
func NewTransaction(mode string, id string, timeout int, rollback bool) *Transaction {
tx := &Transaction{
transactionMode: mode,
transactionID: id,
timeoutMs: timeout,
rollback: rollback,
active: true,
}
if timeout > 0 {
tx.timeoutID = time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() {
tx.active = false
})
}
return tx
}
// ClearTimeout очищает таймаут транзакции
func (t *Transaction) ClearTimeout() {
if t.timeoutID != nil {
t.timeoutID.Stop()
}
}
// IsActive проверяет, является ли транзакция активной
func (t *Transaction) IsActive() bool {
return t.active
}
// IsReadMode проверяет, является ли транзакция режимом чтения
func (t *Transaction) IsReadMode() bool {
return t.transactionMode == "read"
}
// IsWriteMode проверяет, является ли транзакция режимом записи
func (t *Transaction) IsWriteMode() bool {
return t.transactionMode == "write"
}
// ShouldRollback проверяет, требуется ли откат транзакции при ошибке
func (t *Transaction) ShouldRollback() bool {
return t.rollback
}
// ShouldKeepBackup проверяет, нужно ли сохранять резервную копию
func (t *Transaction) ShouldKeepBackup() bool {
return !t.doNotDeleteBackupFile
}
// GetBackupFile получает путь к файлу резервной копии
func (t *Transaction) GetBackupFile() string {
return t.backupFile
}
// SetBackupFile устанавливает путь к файлу резервной копии
func (t *Transaction) SetBackupFile(path string) {
t.backupFile = path
}
// CreateBackup создает резервную копию файла
func (t *Transaction) CreateBackup(filename string) error {
if filename == "" {
return fmt.Errorf("filename is required")
}
// Создаем резервную копию
backupFile := filename + ".backup"
// Копируем файл
src, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open source file: %w", err)
}
defer src.Close()
dst, err := os.Create(backupFile)
if err != nil {
return fmt.Errorf("failed to create backup file: %w", err)
}
defer dst.Close()
_, err = dst.ReadFrom(src)
if err != nil {
return fmt.Errorf("failed to copy file: %w", err)
}
t.backupFile = backupFile
return nil
}
// RestoreFromBackup восстанавливает из резервной копии
func (t *Transaction) RestoreFromBackup(filename string) error {
if t.backupFile == "" {
return fmt.Errorf("no backup file available")
}
// Копируем резервную копию обратно
src, err := os.Open(t.backupFile)
if err != nil {
return fmt.Errorf("failed to open backup file: %w", err)
}
defer src.Close()
dst, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to create target file: %w", err)
}
defer dst.Close()
_, err = dst.ReadFrom(src)
if err != nil {
return fmt.Errorf("failed to copy backup: %w", err)
}
return nil
}
// CleanupBackup удаляет резервную копию
func (t *Transaction) CleanupBackup() error {
if t.backupFile != "" && !t.doNotDeleteBackupFile {
return os.Remove(t.backupFile)
}
return nil
}

280
pkg/linedb/types.go Normal file
View File

@@ -0,0 +1,280 @@
package linedb
import (
"time"
)
// LineDbAdapter представляет базовый интерфейс для записей в базе данных
// Соответствует TypeScript интерфейсу LineDbAdapter
type LineDbAdapter interface {
GetID() any
SetID(id any)
GetTimestamp() *time.Time
SetTimestamp(timestamp *time.Time)
}
// BaseRecord представляет базовую структуру записи
type BaseRecord struct {
ID any `json:"id"`
Timestamp *time.Time `json:"timestamp,omitempty"`
}
func (r *BaseRecord) GetID() any {
return r.ID
}
func (r *BaseRecord) SetID(id any) {
r.ID = id
}
func (r *BaseRecord) GetTimestamp() *time.Time {
return r.Timestamp
}
func (r *BaseRecord) SetTimestamp(timestamp *time.Time) {
r.Timestamp = timestamp
}
// LineDbOptions представляет опции для создания LineDb
// Соответствует TypeScript интерфейсу LineDbOptions
type LineDbOptions struct {
CacheSize int `json:"cacheSize,omitempty"`
CacheTTL time.Duration `json:"cacheTTL,omitempty"`
DBFolder string `json:"dbFolder,omitempty"`
ObjName string `json:"objName,omitempty"`
}
// LineDbInitOptions представляет опции инициализации LineDb
// Соответствует TypeScript интерфейсу LineDbInitOptions
type LineDbInitOptions struct {
CacheSize int `json:"cacheSize,omitempty"`
CacheTTL time.Duration `json:"cacheTTL,omitempty"`
Collections []JSONLFileOptions `json:"collections"`
DBFolder string `json:"dbFolder,omitempty"`
Partitions []PartitionCollection `json:"partitions,omitempty"`
}
// JSONLFileOptions представляет опции для JSONL файла
// Соответствует TypeScript интерфейсу JSONLFileOptions
type JSONLFileOptions struct {
CollectionName string `json:"collectionName,omitempty"`
AllocSize int `json:"allocSize,omitempty"`
IndexedFields []string `json:"indexedFields,omitempty"`
EncryptKeyForLineDb string `json:"encryptKeyForLineDb,omitempty"`
SkipInvalidLines bool `json:"skipInvalidLines,omitempty"`
DecryptKey string `json:"decryptKey,omitempty"`
ConvertStringIdToNumber bool `json:"convertStringIdToNumber,omitempty"`
// Функции сериализации и десериализации JSON
JSONMarshal func(any) ([]byte, error) `json:"-"`
JSONUnmarshal func([]byte, any) error `json:"-"`
}
// PartitionCollection представляет конфигурацию партиционирования
// Соответствует TypeScript интерфейсу PartitionCollection
type PartitionCollection struct {
CollectionName string `json:"collectionName"`
PartIDFn func(any) string `json:"-"`
PartIDFnStr string `json:"partIdFn,omitempty"`
}
// JoinType представляет тип операции JOIN
type JoinType string
const (
JoinTypeInner JoinType = "inner"
JoinTypeLeft JoinType = "left"
JoinTypeRight JoinType = "right"
JoinTypeFull JoinType = "full"
)
// JoinOptions представляет опции для операции JOIN
// Соответствует TypeScript интерфейсу JoinOptions
type JoinOptions struct {
Type JoinType `json:"type"`
LeftFields []string `json:"leftFields"`
RightFields []string `json:"rightFields"`
StrictCompare bool `json:"strictCompare,omitempty"`
InTransaction bool `json:"inTransaction,omitempty"`
TransactionID string `json:"transactionId,omitempty"`
LeftFilter map[string]any `json:"leftFilter,omitempty"`
RightFilter map[string]any `json:"rightFilter,omitempty"`
OnlyOneFromRight bool `json:"onlyOneFromRight,omitempty"`
}
// PaginatedResult представляет результат пагинации
// Соответствует TypeScript интерфейсу PaginatedResult
type PaginatedResult struct {
Data []any `json:"data"`
Total int `json:"total"`
Limit int `json:"limit"`
Pages int `json:"pages"`
Page int `json:"page"`
}
// BackupMetaData представляет метаданные резервной копии
// Соответствует TypeScript интерфейсу BackupMetaData
type BackupMetaData struct {
CollectionNames []string `json:"collectionNames"`
Gzip bool `json:"gzip"`
EncryptKey string `json:"encryptKey"`
NoLock bool `json:"noLock"`
Timestamp int64 `json:"timestamp"`
BackupDate string `json:"backupDate"`
}
// FilterFunction представляет функцию фильтрации
type FilterFunction func(data any) bool
// LineDbAdapterOptions представляет опции для операций с адаптером
// Соответствует TypeScript интерфейсу LineDbAdapterOptions
type LineDbAdapterOptions struct {
InTransaction bool `json:"inTransaction,omitempty"`
StrictCompare bool `json:"strictCompare,omitempty"`
TransactionID string `json:"transactionId,omitempty"`
DebugTag string `json:"debugTag,omitempty"`
FilterType string `json:"filterType,omitempty"`
Method string `json:"method,omitempty"`
RepeatCount int `json:"repeatCount,omitempty"`
InternalCall bool `json:"internalCall,omitempty"`
SkipCheckExistingForWrite bool `json:"skipCheckExistingForWrite,omitempty"`
OptimisticRead bool `json:"optimisticRead,omitempty"`
ReturnChain bool `json:"returnChain,omitempty"`
}
// TransactionOptions представляет опции транзакции
// Соответствует TypeScript интерфейсу TransactionOptions
type TransactionOptions struct {
Rollback bool `json:"rollback,omitempty"`
BackupFile string `json:"backupFile,omitempty"`
DoNotDeleteBackupFile bool `json:"doNotDeleteBackupFile,omitempty"`
Timeout int `json:"timeout,omitempty"`
}
// LineDbTransactionOptions представляет опции транзакции LineDb
// Соответствует TypeScript интерфейсу LineDbTransactionOptions
type LineDbTransactionOptions struct {
Rollback bool `json:"rollback,omitempty"`
BackupFile string `json:"backupFile,omitempty"`
DoNotDeleteBackupFile bool `json:"doNotDeleteBackupFile,omitempty"`
Timeout int `json:"timeout,omitempty"`
}
// JoinResult представляет результат операции JOIN
type JoinResult struct {
Left any `json:"left"`
Right any `json:"right"`
}
// CollectionChain представляет цепочку коллекций (аналог lodash chain)
// Улучшенная версия с дополнительными методами
type CollectionChain struct {
data []any
}
// NewCollectionChain создает новую цепочку коллекций
func NewCollectionChain(data []any) *CollectionChain {
return &CollectionChain{data: data}
}
// Value возвращает данные цепочки
func (c *CollectionChain) Value() []any {
return c.data
}
// Where фильтрует данные
func (c *CollectionChain) Where(filter func(any) bool) *CollectionChain {
var filtered []any
for _, item := range c.data {
if filter(item) {
filtered = append(filtered, item)
}
}
return &CollectionChain{data: filtered}
}
// Sort сортирует данные
func (c *CollectionChain) Sort(compare func(a, b any) bool) *CollectionChain {
// Используем более эффективную сортировку
sorted := make([]any, len(c.data))
copy(sorted, c.data)
// Quick sort implementation
c.quickSort(sorted, 0, len(sorted)-1, compare)
return &CollectionChain{data: sorted}
}
// quickSort реализует быструю сортировку
func (c *CollectionChain) quickSort(arr []any, low, high int, compare func(a, b any) bool) {
if low < high {
pi := c.partition(arr, low, high, compare)
c.quickSort(arr, low, pi-1, compare)
c.quickSort(arr, pi+1, high, compare)
}
}
// partition вспомогательная функция для quickSort
func (c *CollectionChain) partition(arr []any, low, high int, compare func(a, b any) bool) int {
pivot := arr[high]
i := low - 1
for j := low; j < high; j++ {
if compare(arr[j], pivot) {
i++
arr[i], arr[j] = arr[j], arr[i]
}
}
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
}
// Map применяет функцию к каждому элементу
func (c *CollectionChain) Map(fn func(any) any) *CollectionChain {
mapped := make([]any, len(c.data))
for i, item := range c.data {
mapped[i] = fn(item)
}
return &CollectionChain{data: mapped}
}
// Take возвращает первые n элементов
func (c *CollectionChain) Take(n int) *CollectionChain {
if n >= len(c.data) {
return c
}
return &CollectionChain{data: c.data[:n]}
}
// Skip пропускает первые n элементов
func (c *CollectionChain) Skip(n int) *CollectionChain {
if n >= len(c.data) {
return &CollectionChain{data: []any{}}
}
return &CollectionChain{data: c.data[n:]}
}
// Size возвращает размер коллекции
func (c *CollectionChain) Size() int {
return len(c.data)
}
// IsEmpty проверяет, пуста ли коллекция
func (c *CollectionChain) IsEmpty() bool {
return len(c.data) == 0
}
// First возвращает первый элемент
func (c *CollectionChain) First() any {
if len(c.data) == 0 {
return nil
}
return c.data[0]
}
// Last возвращает последний элемент
func (c *CollectionChain) Last() any {
if len(c.data) == 0 {
return nil
}
return c.data[len(c.data)-1]
}