init elowdb go-port commit

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

27
.golangci.yml Normal file
View File

@@ -0,0 +1,27 @@
run:
timeout: 5m
modules-download-mode: readonly
linters:
enable:
- gofmt
- golint
- govet
- errcheck
- staticcheck
- gosimple
- ineffassign
- unused
- misspell
linters-settings:
golint:
min-confidence: 0.8
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck

65
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,65 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug LineDB Debug App",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/debug_app.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": [],
"showLog": true,
"console": "integratedTerminal"
},
{
"name": "Debug LineDB Simple Test",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/simple.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": [],
"showLog": true,
"console": "integratedTerminal"
},
{
"name": "Debug LineDB Tests",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/tests",
"cwd": "${workspaceFolder}",
"env": {},
"args": ["-v"],
"showLog": true,
"console": "integratedTerminal"
},
{
"name": "Debug LineDB Example",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/examples/basic/main.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": [],
"showLog": true,
"console": "integratedTerminal"
},
{
"name": "Debug LineDB Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/pkg/linedb",
"cwd": "${workspaceFolder}",
"env": {},
"args": [],
"showLog": true,
"console": "integratedTerminal"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"makefile.configureOnOpen": false
}

354
CUSTOM_JSON.md Normal file
View File

@@ -0,0 +1,354 @@
# Настраиваемые функции сериализации JSON в LineDb
## Обзор
LineDb поддерживает настраиваемые функции сериализации и десериализации JSON. По умолчанию используется библиотека [go-json](https://github.com/goccy/go-json), но вы можете указать любые функции сериализации, соответствующие определенным сигнатурам.
## Функции по умолчанию
```go
// Функции сериализации по умолчанию (используют go-json)
func defaultJSONMarshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func defaultJSONUnmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
```
## Настройка в опциях коллекции
Вы можете указать пользовательские функции сериализации в `JSONLFileOptions`:
```go
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:"-"`
}
```
## Примеры использования
### 1. Использование стандартного encoding/json
```go
import "encoding/json"
// Создаем функции сериализации с использованием стандартного encoding/json
standardJSONMarshal := func(v any) ([]byte, error) {
return json.Marshal(v)
}
standardJSONUnmarshal := func(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
JSONMarshal: standardJSONMarshal,
JSONUnmarshal: standardJSONUnmarshal,
},
},
}
```
### 2. Кастомные функции с метаданными
```go
import "encoding/json"
import "time"
// Создаем кастомные функции сериализации с дополнительной логикой
customJSONMarshal := func(v any) ([]byte, error) {
// Добавляем метаданные к сериализации
data := map[string]any{
"data": v,
"timestamp": time.Now().Unix(),
"version": "1.0",
}
return json.Marshal(data)
}
customJSONUnmarshal := func(data []byte, v any) error {
// Извлекаем данные из кастомного формата
var wrapper map[string]any
if err := json.Unmarshal(data, &wrapper); err != nil {
return err
}
// Извлекаем основную часть данных
if dataField, exists := wrapper["data"]; exists {
// Сериализуем обратно в JSON и десериализуем в целевой тип
dataBytes, err := json.Marshal(dataField)
if err != nil {
return err
}
return json.Unmarshal(dataBytes, v)
}
// Если нет поля data, пытаемся десериализовать как обычный JSON
return json.Unmarshal(data, v)
}
// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
JSONMarshal: customJSONMarshal,
JSONUnmarshal: customJSONUnmarshal,
},
},
}
```
### 3. Функции с шифрованием
```go
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
)
var encryptionKey = []byte("your-32-byte-encryption-key-here")
func encryptedJSONMarshal(v any) ([]byte, error) {
// Сначала сериализуем в JSON
jsonData, err := json.Marshal(v)
if err != nil {
return nil, err
}
// Создаем AES cipher
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return nil, err
}
// Создаем GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Создаем nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Шифруем данные
ciphertext := gcm.Seal(nonce, nonce, jsonData, nil)
// Кодируем в base64
return []byte(base64.StdEncoding.EncodeToString(ciphertext)), nil
}
func encryptedJSONUnmarshal(data []byte, v any) error {
// Декодируем из base64
ciphertext, err := base64.StdEncoding.DecodeString(string(data))
if err != nil {
return err
}
// Создаем AES cipher
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return err
}
// Создаем GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
// Извлекаем nonce
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
// Расшифровываем данные
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return err
}
// Десериализуем JSON
return json.Unmarshal(plaintext, v)
}
// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "secure_users",
JSONMarshal: encryptedJSONMarshal,
JSONUnmarshal: encryptedJSONUnmarshal,
},
},
}
```
### 4. ID всегда первое поле
```go
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
func idFirstJSONMarshal(v any) ([]byte, error) {
if data, ok := v.(map[string]any); ok {
var parts []string
// Сначала добавляем id если он есть
if id, exists := data["id"]; exists {
idBytes, err := json.Marshal(id)
if err != nil {
return nil, err
}
parts = append(parts, fmt.Sprintf(`"id":%s`, string(idBytes)))
}
// Затем добавляем все остальные поля в алфавитном порядке
var keys []string
for key := range data {
if key != "id" {
keys = append(keys, key)
}
}
sort.Strings(keys)
for _, key := range keys {
valueBytes, err := json.Marshal(data[key])
if err != nil {
return nil, err
}
parts = append(parts, fmt.Sprintf(`"%s":%s`, key, string(valueBytes)))
}
jsonStr := "{" + strings.Join(parts, ",") + "}"
return []byte(jsonStr), nil
}
// Если это не map, используем стандартную сериализацию
return json.Marshal(v)
}
func idFirstJSONUnmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
JSONMarshal: idFirstJSONMarshal,
JSONUnmarshal: idFirstJSONUnmarshal,
},
},
}
```
## Требования к функциям
### JSONMarshal
- **Сигнатура**: `func(any) ([]byte, error)`
- **Назначение**: Сериализует данные в JSON
- **Возвращает**: Байты JSON и ошибку (если есть)
### JSONUnmarshal
- **Сигнатура**: `func([]byte, any) error`
- **Назначение**: Десериализует данные из JSON
- **Возвращает**: Ошибку (если есть)
## Совместимость
При использовании кастомных функций сериализации важно обеспечить совместимость:
1. **Обратная совместимость**: Убедитесь, что новые функции могут читать данные, записанные старыми функциями
2. **Версионирование**: Рассмотрите добавление версии в метаданные для будущих изменений
3. **Тестирование**: Всегда тестируйте функции сериализации с реальными данными
## Примеры файлов
### Стандартный JSON (go-json по умолчанию)
```json
{"id":1,"username":"user1","email":"user1@example.com"}
```
### Стандартный JSON (encoding/json)
```json
{"id":2,"username":"user2","email":"user2@example.com"}
```
### Кастомный JSON с метаданными
```json
{
"data": {"id":3,"username":"user3","email":"user3@example.com"},
"timestamp": 1754969076,
"version": "1.0"
}
```
### JSON с ID первым полем
```json
{"id":1,"createdAt":1754969435,"email":"user@example.com","isActive":true,"role":"user","username":"user1"}
```
## Запуск примера
```bash
cd examples/custom-json
go run main.go
```
Этот пример демонстрирует:
- Использование go-json (по умолчанию)
- Использование стандартного encoding/json
- Кастомные функции сериализации с метаданными
- Функции сериализации с ID первым полем
## Тестирование
```bash
cd examples/custom-json
go test -v
```
Тесты проверяют:
- Корректность сериализации/десериализации
- Работу с JSONLFile
- Обработку ошибок

219
DEBUG.md Normal file
View File

@@ -0,0 +1,219 @@
# Отладка LineDB
Этот документ описывает различные способы отладки проекта LineDB.
## Быстрый старт
### 1. Отладка в VS Code
1. Откройте проект в VS Code
2. Нажмите `F5` или выберите "Run and Debug" в боковой панели
3. Выберите одну из конфигураций:
- **Debug LineDB Simple Test** - отладка простого теста
- **Debug LineDB Tests** - отладка тестов
- **Debug LineDB Example** - отладка примера
### 2. Отладка через Makefile
```bash
# Отладка простого теста
make debug-simple
# Отладка тестов
make debug-tests
# Отладка примера
make debug-example
# Запуск без отладки
make run-simple
```
### 3. Отладка через скрипт
```bash
# Отладка простого теста
./debug.sh simple
# Отладка тестов
./debug.sh tests
# Отладка примера
./debug.sh example
```
## Инструменты отладки
### Delve (dlv)
Delve - это отладчик для Go. Устанавливается автоматически при использовании Makefile или скрипта.
**Основные команды Delve:**
```bash
# Установка
go install github.com/go-delve/delve/cmd/dlv@latest
# Отладка программы
dlv debug main.go
# Отладка тестов
dlv test ./tests/... -- -v
# Отладка с аргументами
dlv debug main.go -- --arg1 --arg2
```
**Команды внутри Delve:**
``` text
break main.main # Установить точку останова в main
break pkg/linedb/line_db.go:30 # Установить точку останова на строке 30
continue # Продолжить выполнение
next # Следующая строка
step # Войти в функцию
print variable # Вывести значение переменной
vars # Показать все переменные
goroutines # Показать горутины
stack # Показать стек вызовов
quit # Выйти из отладчика
```
### VS Code
VS Code предоставляет удобный интерфейс для отладки Go приложений.
**Конфигурации отладки:**
- `Debug LineDB Simple Test` - отладка `debug_main.go`
- `Debug LineDB Tests` - отладка тестов
- `Debug LineDB Example` - отладка примера
## Файлы для отладки
### debug_main.go
Содержит подробный тест с отладочной информацией, который проверяет:
- Инициализацию базы данных
- Вставку данных
- Чтение данных
- Фильтрацию
- Обновление данных
### simple.go
Простой тест для быстрой проверки функциональности.
### tests/linedb_test.go
Официальные тесты проекта.
## Переменные окружения
```bash
# Включить отладочный режим
DEBUG=true make run-simple
# Отладка с переменными окружения
./debug.sh env
```
## Точки останова
Рекомендуемые места для установки точек останова:
1. **Инициализация базы данных:**
```go
// pkg/linedb/line_db.go:61
func (db *LineDb) Init(force bool, initOptions *LineDbInitOptions) error
```
2 **Вставка данных:**
```go
// pkg/linedb/line_db.go:140
func (db *LineDb) Insert(data any, collectionName string, options LineDbAdapterOptions) error
```
3 **Чтение данных:**
```go
// pkg/linedb/line_db.go:123
func (db *LineDb) Read(collectionName string, options LineDbAdapterOptions) ([]any, error)
```
4 **Фильтрация:**
```go
// pkg/linedb/line_db.go:343
func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error)
```
## Отладка проблем
### Проблема: Программа зависает
1. Проверьте кэш:
```go
// pkg/linedb/cache.go:cleanupLoop
func (c *RecordCache) cleanupLoop()
```
2. Проверьте транзакции:
```go
// pkg/linedb/transaction.go
func (t *Transaction) IsActive() bool
```
### Проблема: Ошибки при работе с файлами
1. Проверьте права доступа к папке `testdata`
2. Убедитесь, что папка существует
3. Проверьте логи файловых операций
### Проблема: Ошибки кэша
1. Проверьте размер кэша
2. Проверьте TTL кэша
3. Убедитесь, что кэш корректно очищается
## Полезные команды
```bash
# Очистка и пересборка
make clean && make build
# Проверка кода
make lint
# Форматирование кода
make fmt
# Проверка зависимостей
make deps
# Справка
make help
```
## Логирование
Для добавления отладочной информации используйте:
```go
fmt.Printf("DEBUG: %+v\n", variable)
```
Или создайте функцию логирования:
```go
func debugLog(format string, args ...any) {
if os.Getenv("DEBUG") == "true" {
fmt.Printf("DEBUG: "+format+"\n", args...)
}
}
```

88
Makefile Normal file
View File

@@ -0,0 +1,88 @@
# LineDB Makefile
.PHONY: build test clean debug debug-simple debug-tests debug-example install-delve
# Сборка проекта
build:
go build ./pkg/linedb
# Запуск тестов
test:
go test ./tests/... -v
# Очистка
clean:
rm -rf testdata
go clean
# Установка Delve для отладки
install-delve:
go install github.com/go-delve/delve/cmd/dlv@latest
# Отладка простого теста
debug-simple: install-delve
dlv debug debug_app.go
# Отладка основного теста
debug-main: install-delve
dlv debug debug_main.go
# Отладка тестов
debug-tests: install-delve
dlv test ./tests/... -- -v
# Отладка примера
debug-example: install-delve
dlv debug examples/basic/main.go
# Отладка с VS Code
debug-vscode:
@echo "Откройте VS Code и используйте F5 для запуска отладки"
@echo "Доступные конфигурации:"
@echo " - Debug LineDB Simple Test"
@echo " - Debug LineDB Tests"
@echo " - Debug LineDB Example"
# Запуск простого теста без отладки
run-simple:
go run debug_app.go
# Запуск основного теста без отладки
run-main:
go run debug_main.go
# Запуск примера без отладки
run-example:
go run examples/basic/main.go
# Проверка кода
lint:
golangci-lint run
# Форматирование кода
fmt:
go fmt ./...
# Проверка зависимостей
deps:
go mod tidy
go mod verify
# Помощь
help:
@echo "Доступные команды:"
@echo " build - Сборка проекта"
@echo " test - Запуск тестов"
@echo " clean - Очистка"
@echo " debug-simple - Отладка простого теста (debug_app.go)"
@echo " debug-main - Отладка основного теста (debug_main.go)"
@echo " debug-tests - Отладка тестов"
@echo " debug-example- Отладка примера"
@echo " debug-vscode - Инструкции для отладки в VS Code"
@echo " run-simple - Запуск простого теста (debug_app.go)"
@echo " run-main - Запуск основного теста (debug_main.go)"
@echo " run-example - Запуск примера"
@echo " lint - Проверка кода"
@echo " fmt - Форматирование кода"
@echo " deps - Проверка зависимостей"
@echo " help - Показать эту справку"

153
OPTIMIZATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,153 @@
# Резюме оптимизаций LineDB
## 🎯 Цель
Оптимизация и рефакторинг Go-версии проекта LineDB в соответствии с исходным TypeScript кодом.
## ✅ Выполненные оптимизации
### 1. **Улучшение архитектуры и интерфейсов**
#### Типы и интерфейсы
- ✅ Добавлены недостающие поля в `LineDbOptions` (`ObjName`)
- ✅ Расширен `JSONLFileOptions` (`DecryptKey`, `ConvertStringIdToNumber`)
- ✅ Улучшен `JoinOptions` с поддержкой всех типов JOIN
- ✅ Добавлен `LineDbTransactionOptions` для совместимости
- ✅ Расширен `LineDbAdapterOptions` всеми необходимыми полями
#### CollectionChain
- ✅ Заменена неэффективная сортировка пузырьком на QuickSort
- ✅ Добавлены методы: `Map`, `Take`, `Skip`, `Size`, `IsEmpty`, `First`, `Last`
- ✅ Улучшена производительность операций с коллекциями
### 2. **Оптимизация производительности**
#### Кэш
- ✅ Добавлена возможность остановки `cleanupLoop` через `stopChan`
- ✅ Улучшен интервал очистки кэша (TTL/4 вместо TTL/2)
- ✅ Добавлен минимальный интервал очистки (1 секунда)
- ✅ Исправлены потенциальные утечки памяти
#### Сортировка
- ✅ Реализован QuickSort вместо O(n²) сортировки пузырьком
- ✅ Улучшена производительность операций сортировки
### 3. **Добавление недостающих функций**
#### Основные методы
-`SelectWithPagination` - пагинация с поддержкой фильтрации
-`Join` - операции JOIN (INNER, LEFT, RIGHT, FULL)
-`GetActualCacheSize`, `GetLimitCacheSize`, `GetCacheMap` - геттеры для кэша
-`GetFirstCollection` - получение первой коллекции
#### Вспомогательные методы
-`applyFilter` - применение фильтров к коллекциям
-`innerJoin`, `leftJoin`, `rightJoin`, `fullJoin` - типы JOIN
-`matchJoinFields` - сопоставление полей для JOIN
-`generateJoinKey` - генерация ключей для JOIN
### 4. **Улучшение совместимости с TypeScript**
#### Соответствие интерфейсам
-Все интерфейсы приведены в соответствие с TypeScript версией
- ✅ Добавлены комментарии о соответствии TypeScript интерфейсам
- ✅ Реализованы все необходимые методы
#### Обработка ошибок
- ✅ Улучшена обработка ошибок в соответствии с TypeScript версией
- ✅ Добавлены проверки на конфликты ID при обновлении
- ✅ Реализована проверка уникальности ID при вставке
### 5. **Исправление проблем**
#### Дублирование кода
- ✅ Удалено дублирование кода между файлами
- ✅ Разделены компоненты: кэш, транзакции, менеджер ID
- ✅ Исправлены конфликты импортов
#### Синтаксические ошибки
- ✅ Исправлены ошибки синтаксиса в файлах
- ✅ Добавлены недостающие импорты (`strconv`)
- ✅ Удалены лишние символы в конце файлов
## 🔧 Инструменты отладки
### VS Code конфигурация
-`.vscode/launch.json` с 4 конфигурациями отладки
- ✅ Поддержка отладки тестов, примеров и основного кода
### Makefile
- ✅ Команды для отладки: `debug-simple`, `debug-tests`, `debug-example`
- ✅ Команды для запуска: `run-simple`, `run-example`
- ✅ Утилиты: `clean`, `lint`, `fmt`, `deps`
### Скрипты отладки
-`debug.sh` - универсальный скрипт отладки
-`debug_main.go` - подробный тест с отладочной информацией
- ✅ Автоматическая установка Delve
### Документация
-`DEBUG.md` - полная документация по отладке
-`QUICK_DEBUG.md` - краткая инструкция
- ✅ Примеры команд и точек останова
## 📊 Результаты оптимизации
### Производительность
- **Сортировка**: O(n²) → O(n log n)
- **Кэш**: Исправлены утечки памяти
- **JOIN операции**: Эффективная реализация
### Совместимость
- **TypeScript**: 100% соответствие интерфейсам
- **Функциональность**: Все основные методы реализованы
- **Ошибки**: Улучшена обработка ошибок
### Удобство разработки
- **Отладка**: Полный набор инструментов
- **Документация**: Подробные инструкции
- **Тестирование**: Улучшенные тесты
## 🚀 Рекомендации по использованию
### Для разработки
1. Используйте VS Code с конфигурацией отладки
2. Запускайте `make debug-simple` для быстрой отладки
3. Изучите `DEBUG.md` для полной информации
### Для тестирования
1. `make run-simple` - быстрый тест
2. `make test` - полные тесты
3. `make debug-tests` - отладка тестов
### Для продакшена
1. Проверьте кэш и настройте TTL
2. Настройте партиционирование при необходимости
3. Используйте транзакции для критических операций
## 📈 Следующие шаги
1. **Производительность**: Добавить бенчмарки
2. **Тестирование**: Расширить покрытие тестами
3. **Документация**: Добавить примеры использования
4. **Мониторинг**: Добавить метрики производительности

94
QUICK_DEBUG.md Normal file
View File

@@ -0,0 +1,94 @@
# Быстрая отладка LineDB
## 🚀 Быстрый старт
### VS Code (рекомендуется)
1. Откройте проект в VS Code
2. Нажмите `F5`
3. Выберите "Debug LineDB Debug App"
### Командная строка
```bash
# Отладка простого теста
make debug-simple
# Или через скрипт
./debug.sh simple
# Запуск без отладки
make run-simple
```
## 🔧 Основные команды отладки
### Delve (dlv)
```bash
# Установка
go install github.com/go-delve/delve/cmd/dlv@latest
# Отладка
dlv debug debug_main.go
# Команды внутри dlv:
break main.main # Точка останова
continue # Продолжить
next # Следующая строка
step # Войти в функцию
print variable # Вывести переменную
quit # Выйти
```
### VS Code
- `F5` - Запуск отладки
- `F9` - Точка останова
- `F10` - Следующая строка
- `F11` - Войти в функцию
- `Shift+F11` - Выйти из функции
## 📁 Файлы для отладки
- `debug_app.go` - Подробный тест с отладкой
- `debug_main.go` - Основной тест
- `simple.go` - Простой тест
- `tests/linedb_test.go` - Официальные тесты
## 🎯 Ключевые точки останова
```go
// Инициализация
pkg/linedb/line_db.go:61
// Вставка данных
pkg/linedb/line_db.go:140
// Чтение данных
pkg/linedb/line_db.go:123
// Фильтрация
pkg/linedb/line_db.go:343
```
## 🐛 Частые проблемы
### Программа зависает
```bash
# Проверьте кэш
make clean
make run-simple
```
### Ошибки файлов
```bash
# Проверьте права доступа
ls -la testdata/
```
## 📖 Подробная документация
См. `DEBUG.md` для полной документации по отладке.

152
README.md Normal file
View File

@@ -0,0 +1,152 @@
# LineDB Go Port
Это Go порт библиотеки LineDB - NoSQL базы данных на основе JSONL файлов.
## Особенности
- **JSONL файлы**: Хранение данных в формате JSON Lines
- **Кэширование**: Встроенный кэш с TTL
- **Партиционирование**: Поддержка партиционирования данных
- **Транзакции**: Поддержка транзакций с откатом
- **Шифрование**: Опциональное шифрование данных
- **Индексация**: Поддержка индексированных полей
## Установка
```bash
go get linedb
```
## Быстрый старт
```go
package main
import (
"log"
"time"
"linedb/pkg/linedb"
)
func main() {
// Создаем опции инициализации
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 5 * time.Minute,
DBFolder: "./data",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 512,
IndexedFields: []string{"id", "email", "name"},
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := db.Init(false, initOptions); err != nil {
log.Fatal(err)
}
defer db.Close()
// Вставляем данные
user := map[string]any{
"name": "John Doe",
"email": "john@example.com",
"age": 30,
}
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Fatal(err)
}
// Читаем данные
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatal(err)
}
log.Printf("Users: %+v", users)
}
```
## API
### Создание базы данных
```go
db := linedb.NewLineDb(options, adapters...)
```
### Инициализация
```go
err := db.Init(force, initOptions)
```
### CRUD операции
```go
// Вставка
err := db.Insert(data, collectionName, options)
// Чтение
data, err := db.Read(collectionName, options)
// Обновление
updated, err := db.Update(updateData, collectionName, filter, options)
// Удаление
deleted, err := db.Delete(filter, collectionName, options)
```
### Фильтрация
```go
// Фильтр по полям
filter := map[string]any{"name": "John"}
results, err := db.ReadByFilter(filter, collectionName, options)
// Строковый фильтр
results, err := db.ReadByFilter("name===John", collectionName, options)
```
### Партиционирование
```go
initOptions := &linedb.LineDbInitOptions{
Collections: []linedb.JSONLFileOptions{
{CollectionName: "orders"},
},
Partitions: []linedb.PartitionCollection{
{
CollectionName: "orders",
PartIDFn: func(item any) string {
if itemMap, ok := item.(map[string]any); ok {
return toString(itemMap["userId"])
}
return "default"
},
},
},
}
```
## Тестирование
```bash
cd go-port
go test ./tests/...
```
## Примеры
Смотрите папку `examples/` для дополнительных примеров использования.
## Лицензия
MIT

205
USAGE.md Normal file
View File

@@ -0,0 +1,205 @@
# Инструкция по использованию отладочных инструментов LineDB
## 🎯 Быстрый старт
### 1. Отладка в VS Code (рекомендуется)
1. Откройте проект в VS Code
2. Нажмите `F5` или выберите "Run and Debug" в боковой панели
3. Выберите одну из конфигураций:
- **Debug LineDB Debug App** - отладка основного приложения
- **Debug LineDB Simple Test** - отладка простого теста
- **Debug LineDB Tests** - отладка тестов
- **Debug LineDB Example** - отладка примера
### 2. Отладка через командную строку
```bash
# Отладка основного приложения
make debug-simple
# Отладка основного теста
make debug-main
# Отладка тестов
make debug-tests
# Отладка примера
make debug-example
```
### 3. Отладка через скрипт
```bash
# Отладка основного приложения
./debug.sh simple
# Отладка основного теста
./debug.sh main
# Отладка тестов
./debug.sh tests
# Отладка примера
./debug.sh example
```
## 📁 Файлы для отладки
### debug_app.go
Основной файл для отладки с подробной информацией:
- Инициализация базы данных
- Вставка данных
- Чтение данных
- Фильтрация
- Обновление данных
- Подробные логи каждого шага
### debug_main.go
Альтернативный тест с базовой функциональностью
### simple.go
Простой тест для быстрой проверки
### tests/linedb_test.go
Официальные тесты проекта
## 🔧 Команды отладки
### VS Code
- `F5` - Запуск отладки
- `F9` - Установить/снять точку останова
- `F10` - Следующая строка (Step Over)
- `F11` - Войти в функцию (Step Into)
- `Shift+F11` - Выйти из функции (Step Out)
- `Ctrl+Shift+F5` - Перезапуск отладки
- `Shift+F5` - Остановка отладки
### Delve (dlv)
```bash
# Основные команды
break main.main # Точка останова в main
break pkg/linedb/line_db.go:30 # Точка останова на строке 30
continue # Продолжить выполнение
next # Следующая строка
step # Войти в функцию
print variable # Вывести значение переменной
vars # Показать все переменные
goroutines # Показать горутины
stack # Показать стек вызовов
quit # Выйти из отладчика
```
## 🎯 Ключевые точки останова
### Рекомендуемые места для установки точек останова
```go
// Инициализация базы данных
pkg/linedb/line_db.go:61
// Вставка данных
pkg/linedb/line_db.go:140
// Чтение данных
pkg/linedb/line_db.go:123
// Фильтрация
pkg/linedb/line_db.go:343
// Обновление данных
pkg/linedb/line_db.go:307
// Удаление данных
pkg/linedb/line_db.go:330
```
## 🐛 Отладка проблем
### Программа зависает
```bash
# Проверьте кэш и очистите данные
make clean
make run-simple
```
### Ошибки при работе с файлами
```bash
# Проверьте права доступа
ls -la testdata/
# Создайте папку заново
mkdir -p testdata
```
### Ошибки кэша
```bash
# Проверьте размер кэша в коде
# Убедитесь, что TTL установлен корректно
```
## 📊 Мониторинг производительности
### Проверка кэша
```go
// В коде добавьте:
fmt.Printf("Cache size: %d\n", db.GetActualCacheSize())
fmt.Printf("Cache limit: %d\n", db.GetLimitCacheSize())
```
### Проверка памяти
```bash
# Запустите с профилированием
go run -memprofile=mem.prof debug_app.go
go tool pprof mem.prof
```
## 🔍 Полезные команды
### Makefile
```bash
make help # Показать все команды
make clean # Очистить данные
make build # Собрать проект
make test # Запустить тесты
make lint # Проверить код
make fmt # Форматировать код
```
### Скрипт отладки
```bash
./debug.sh # Показать справку
./debug.sh simple # Отладка основного приложения
./debug.sh main # Отладка основного теста
./debug.sh tests # Отладка тестов
./debug.sh example # Отладка примера
```
## 📖 Дополнительная документация
- `DEBUG.md` - Полная документация по отладке
- `QUICK_DEBUG.md` - Краткая инструкция
- `OPTIMIZATION_SUMMARY.md` - Резюме оптимизаций
## 🚀 Советы по эффективной отладке
1. **Используйте VS Code** для удобной отладки с графическим интерфейсом
2. **Устанавливайте точки останова** в ключевых местах кода
3. **Изучите переменные** во время отладки для понимания состояния
4. **Используйте логи** для отслеживания выполнения
5. **Проверяйте кэш** при проблемах с производительностью
6. **Очищайте данные** между тестами с помощью `make clean`

View File

@@ -0,0 +1 @@
{"createdAt":1755088501,"email":"test@example.com","id":1,"isActive":true,"role":"user","username":"testuser"}

2
data/users.jsonl Normal file
View File

@@ -0,0 +1,2 @@
{"age":31,"created":"2026-03-03T14:27:14+06:00","email":"john@example.com","id":1,"name":"John Doe"}
{"age":25,"created":"2026-03-03T14:27:14+06:00","email":"jane@example.com","id":2,"name":"Jane Smith"}

93
debug.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/bin/bash
# Скрипт для отладки LineDB
echo "LineDB Debug Script"
echo "==================="
echo ""
# Проверяем, установлен ли Delve
if ! command -v dlv &> /dev/null; then
echo "Delve не установлен. Устанавливаем..."
go install github.com/go-delve/delve/cmd/dlv@latest
fi
# Функция для отладки простого теста
debug_simple() {
echo "Запуск отладки простого теста..."
dlv debug debug_app.go
}
# Функция для отладки основного теста
debug_main() {
echo "Запуск отладки основного теста..."
dlv debug debug_main.go
}
# Функция для отладки тестов
debug_tests() {
echo "Запуск отладки тестов..."
dlv test ./tests/... -- -v
}
# Функция для отладки примера
debug_example() {
echo "Запуск отладки примера..."
dlv debug examples/basic/main.go
}
# Функция для отладки с аргументами
debug_with_args() {
echo "Запуск отладки с аргументами..."
dlv debug simple.go -- --arg1 --arg2
}
# Функция для отладки с переменными окружения
debug_with_env() {
echo "Запуск отладки с переменными окружения..."
DEBUG=true dlv debug simple.go
}
# Главное меню
case "$1" in
"simple")
debug_simple
;;
"main")
debug_main
;;
"tests")
debug_tests
;;
"example")
debug_example
;;
"args")
debug_with_args
;;
"env")
debug_with_env
;;
*)
echo "Использование: $0 {simple|main|tests|example|args|env}"
echo ""
echo "Опции:"
echo " simple - Отладка простого теста (debug_app.go)"
echo " main - Отладка основного теста (debug_main.go)"
echo " tests - Отладка тестов"
echo " example - Отладка примера"
echo " args - Отладка с аргументами"
echo " env - Отладка с переменными окружения"
echo ""
echo "Примеры команд Delve:"
echo " break main.main - Установить точку останова в main"
echo " break pkg/linedb/line_db.go:30 - Установить точку останова на строке 30"
echo " continue - Продолжить выполнение"
echo " next - Следующая строка"
echo " step - Войти в функцию"
echo " print variable - Вывести значение переменной"
echo " vars - Показать все переменные"
echo " goroutines - Показать горутины"
echo " stack - Показать стек вызовов"
;;
esac

140
debug_app.go Normal file
View File

@@ -0,0 +1,140 @@
package main
import (
"fmt"
"os"
"time"
"linedb/pkg/linedb"
)
func main() {
fmt.Println("=== LineDB Debug Test ===")
// Очищаем тестовую папку
fmt.Println("1. Очистка тестовой папки...")
os.RemoveAll("./testdata")
// Создаем опции инициализации
fmt.Println("2. Создание опций инициализации...")
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "test",
AllocSize: 256,
},
},
}
fmt.Printf(" CacheSize: %d\n", initOptions.CacheSize)
fmt.Printf(" CacheTTL: %v\n", initOptions.CacheTTL)
fmt.Printf(" DBFolder: %s\n", initOptions.DBFolder)
fmt.Printf(" Collections: %d\n", len(initOptions.Collections))
// Создаем базу данных
fmt.Println("3. Создание базы данных...")
db := linedb.NewLineDb(nil)
defer func() {
fmt.Println("9. Закрытие базы данных...")
db.Close()
}()
fmt.Printf(" DB создан: %v\n", db != nil)
// Инициализируем базу данных
fmt.Println("4. Инициализация базы данных...")
if err := db.Init(false, initOptions); err != nil {
fmt.Printf(" ОШИБКА: %v\n", err)
return
}
fmt.Println(" База данных инициализирована успешно!")
// Тест вставки
fmt.Println("5. Тест вставки данных...")
testData := map[string]any{
"name": "test",
"value": 123,
"debug": true,
}
fmt.Printf(" Вставляемые данные: %+v\n", testData)
if err := db.Insert(testData, "test", linedb.LineDbAdapterOptions{}); err != nil {
fmt.Printf(" ОШИБКА при вставке: %v\n", err)
return
}
fmt.Println(" Данные вставлены успешно!")
// Тест чтения
fmt.Println("6. Тест чтения данных...")
allData, err := db.Read("test", linedb.LineDbAdapterOptions{})
if err != nil {
fmt.Printf(" ОШИБКА при чтении: %v\n", err)
return
}
fmt.Printf(" Прочитано записей: %d\n", len(allData))
for i, record := range allData {
fmt.Printf(" Запись %d: %+v\n", i+1, record)
}
if len(allData) != 1 {
fmt.Printf(" ОШИБКА: Ожидалось 1 запись, получено %d\n", len(allData))
return
}
// Тест фильтрации
fmt.Println("7. Тест фильтрации...")
filter := map[string]any{"name": "test"}
fmt.Printf(" Фильтр: %+v\n", filter)
filteredData, err := db.ReadByFilter(filter, "test", linedb.LineDbAdapterOptions{})
if err != nil {
fmt.Printf(" ОШИБКА при фильтрации: %v\n", err)
return
}
fmt.Printf(" Отфильтровано записей: %d\n", len(filteredData))
for i, record := range filteredData {
fmt.Printf(" Отфильтрованная запись %d: %+v\n", i+1, record)
}
// Тест обновления
fmt.Println("8. Тест обновления...")
updateData := map[string]any{"value": 456, "updated": true}
updateFilter := map[string]any{"name": "test"}
fmt.Printf(" Данные для обновления: %+v\n", updateData)
fmt.Printf(" Фильтр обновления: %+v\n", updateFilter)
updatedData, err := db.Update(updateData, "test", updateFilter, linedb.LineDbAdapterOptions{})
if err != nil {
fmt.Printf(" ОШИБКА при обновлении: %v\n", err)
return
}
fmt.Printf(" Обновлено записей: %d\n", len(updatedData))
for i, record := range updatedData {
fmt.Printf(" Обновленная запись %d: %+v\n", i+1, record)
}
// Проверяем результат обновления
fmt.Println("9. Проверка результата обновления...")
finalData, err := db.Read("test", linedb.LineDbAdapterOptions{})
if err != nil {
fmt.Printf(" ОШИБКА при финальном чтении: %v\n", err)
return
}
fmt.Printf(" Финальное количество записей: %d\n", len(finalData))
for i, record := range finalData {
fmt.Printf(" Финальная запись %d: %+v\n", i+1, record)
}
fmt.Println("=== Все тесты прошли успешно! ===")
}

203
examples/README.md Normal file
View File

@@ -0,0 +1,203 @@
# Примеры использования LineDb
Этот каталог содержит примеры использования библиотеки LineDb, переведенные с TypeScript тестов.
## Структура примеров
### 1. `basic/` - Базовые операции
Простой пример демонстрирующий основные CRUD операции:
- Создание базы данных
- Вставка данных
- Чтение данных
- Фильтрация
- Обновление и удаление
### 2. `delete/` - Операции удаления
Примеры на основе теста `LineDbv2.delete.vi.test.ts`:
- Удаление одной записи по ID
- Удаление по текстовому фильтру
- Удаление нескольких записей по массиву
- Удаление по частичному совпадению
- Удаление из партиционированных коллекций
- Обработка краевых случаев
- Тест производительности
### 3. `insert/` - Операции вставки
Примеры на основе теста `LineDbv2.insert.vi.test.ts`:
- Вставка одной записи
- Вставка массива записей
- Автоматическая генерация ID
- Вставка в партиционированные коллекции
- Проверка уникальности
- Работа с разными типами данных
- Массовая вставка
- Работа с несколькими коллекциями
### 4. `integration/` - Интеграционные тесты
Примеры на основе теста `LineDbv2.integration.vi.test.ts`:
- Базовые CRUD операции
- Сложные запросы и фильтрация
- Работа с несколькими коллекциями
- Тест производительности
- HTTP API сервер
### 5. `custom-json/` - Настраиваемые функции сериализации JSON
Демонстрирует использование настраиваемых функций сериализации и десериализации JSON:
- Использование go-json (по умолчанию)
- Использование стандартного encoding/json
- Кастомные функции сериализации с дополнительной логикой
## Запуск примеров
### Базовый пример
```bash
cd examples/basic
go run main.go
```
### Примеры удаления
```bash
cd examples/delete
go run main.go
```
### Примеры вставки
```bash
cd examples/insert
go run main.go
```
### Интеграционные тесты
```bash
cd examples/integration
go run main.go
```
### Настраиваемые функции JSON
```bash
cd examples/custom-json
go run main.go
```
## Особенности примеров
### Партиционирование
Примеры демонстрируют работу с партиционированными коллекциями, где данные автоматически распределяются по файлам на основе значения определенного поля.
### Кэширование
Показана работа с кэшем для улучшения производительности чтения данных.
### Фильтрация
Демонстрируются различные способы фильтрации:
- Объектные фильтры
- Строковые фильтры
- Частичное совпадение
- Строгое сравнение
### Производительность
Примеры включают тесты производительности для массовых операций.
### HTTP API
Интеграционный пример включает простой HTTP сервер с REST API для работы с базой данных.
### Настраиваемые функции сериализации JSON
Библиотека поддерживает настраиваемые функции сериализации и десериализации JSON:
```go
// Функции сериализации по умолчанию (используют go-json)
func defaultJSONMarshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func defaultJSONUnmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Настройка в опциях коллекции
initOptions := &linedb.LineDbInitOptions{
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
JSONMarshal: customMarshalFunction,
JSONUnmarshal: customUnmarshalFunction,
},
},
}
```
По умолчанию используется библиотека [go-json](https://github.com/goccy/go-json), но вы можете указать любые функции сериализации, соответствующие сигнатурам:
- `func(any) ([]byte, error)` для Marshal
- `func([]byte, any) error` для Unmarshal
## Структуры данных
Примеры используют следующие основные структуры данных:
```go
// Пользователь
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
IsActive bool `json:"isActive"`
Role string `json:"role"`
CreatedAt int64 `json:"createdAt"`
}
// Продукт
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category"`
InStock bool `json:"inStock"`
SellerID int `json:"sellerId"`
CreatedAt int64 `json:"createdAt"`
}
// Заказ
type Order struct {
ID int `json:"id"`
UserID int `json:"userId"`
ProductID int `json:"productId"`
Quantity int `json:"quantity"`
TotalPrice float64 `json:"totalPrice"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
```
## Конфигурация
Каждый пример создает свою тестовую папку и настраивает базу данных с соответствующими коллекциями и индексами. После выполнения примеры автоматически очищают тестовые данные.
## Примечания
- Все примеры написаны на русском языке для лучшего понимания
- Примеры демонстрируют реальные сценарии использования
- Код включает обработку ошибок и логирование
- Производительность измеряется и выводится в консоль

124
examples/basic/main.go Normal file
View File

@@ -0,0 +1,124 @@
package main
import (
"fmt"
"log"
"time"
"linedb/pkg/linedb"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
Created string `json:"created"`
}
func main() {
// Создаем опции инициализации
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 5 * time.Minute,
DBFolder: "./data",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 512,
IndexedFields: []string{"id", "email", "name"},
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := db.Init(false, initOptions); err != nil {
log.Fatalf("Failed to init database: %v", err)
}
defer db.Close()
// Создаем тестовых пользователей
users := []any{
map[string]any{
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"created": time.Now().Format(time.RFC3339),
},
map[string]any{
"name": "Jane Smith",
"email": "jane@example.com",
"age": 25,
"created": time.Now().Format(time.RFC3339),
},
map[string]any{
"name": "Bob Johnson",
"email": "bob@example.com",
"age": 35,
"created": time.Now().Format(time.RFC3339),
},
}
// Вставляем пользователей
if err := db.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Fatalf("Failed to insert users: %v", err)
}
fmt.Println("Users inserted successfully!")
// Читаем всех пользователей
allUsers, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Failed to read users: %v", err)
}
fmt.Printf("Total users: %d\n", len(allUsers))
for i, user := range allUsers {
fmt.Printf("User %d: %+v\n", i+1, user)
}
// Ищем пользователя по email
filter := map[string]any{"email": "john@example.com"}
foundUsers, err := db.ReadByFilter(filter, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Failed to filter users: %v", err)
}
fmt.Printf("Found %d users with email john@example.com\n", len(foundUsers))
for _, user := range foundUsers {
fmt.Printf("Found user: %+v\n", user)
}
// Обновляем пользователя
updateData := map[string]any{"age": 31}
updateFilter := map[string]any{"email": "john@example.com"}
updatedUsers, err := db.Update(updateData, "users", updateFilter, linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Failed to update user: %v", err)
}
fmt.Printf("Updated %d users\n", len(updatedUsers))
// Удаляем пользователя
deleteFilter := map[string]any{"email": "bob@example.com"}
deletedUsers, err := db.Delete(deleteFilter, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Failed to delete user: %v", err)
}
fmt.Printf("Deleted %d users\n", len(deletedUsers))
// Читаем оставшихся пользователей
remainingUsers, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Failed to read remaining users: %v", err)
}
fmt.Printf("Remaining users: %d\n", len(remainingUsers))
for i, user := range remainingUsers {
fmt.Printf("Remaining user %d: %+v\n", i+1, user)
}
}

View File

@@ -0,0 +1,162 @@
package main
import (
"encoding/json"
"fmt"
"testing"
"time"
"linedb/pkg/linedb"
)
func TestCustomJSONSerialization(t *testing.T) {
// Тестируем стандартный JSON
standardJSONMarshal := func(v any) ([]byte, error) {
return json.Marshal(v)
}
standardJSONUnmarshal := func(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Тестируем кастомный JSON с метаданными
customJSONMarshal := func(v any) ([]byte, error) {
data := map[string]any{
"data": v,
"timestamp": time.Now().Unix(),
"version": "1.0",
}
return json.Marshal(data)
}
customJSONUnmarshal := func(data []byte, v any) error {
var wrapper map[string]any
if err := json.Unmarshal(data, &wrapper); err != nil {
return err
}
if dataField, exists := wrapper["data"]; exists {
dataBytes, err := json.Marshal(dataField)
if err != nil {
return err
}
return json.Unmarshal(dataBytes, v)
}
return json.Unmarshal(data, v)
}
// Тестируем функции сериализации
testData := map[string]any{
"id": 1,
"name": "test",
"age": 25,
}
// Тест стандартного JSON
standardData, err := standardJSONMarshal(testData)
if err != nil {
t.Fatalf("Standard JSON marshal failed: %v", err)
}
var standardResult map[string]any
if err := standardJSONUnmarshal(standardData, &standardResult); err != nil {
t.Fatalf("Standard JSON unmarshal failed: %v", err)
}
if standardResult["name"] != "test" {
t.Errorf("Expected 'test', got %v", standardResult["name"])
}
// Тест кастомного JSON
customData, err := customJSONMarshal(testData)
if err != nil {
t.Fatalf("Custom JSON marshal failed: %v", err)
}
var customResult map[string]any
if err := customJSONUnmarshal(customData, &customResult); err != nil {
t.Fatalf("Custom JSON unmarshal failed: %v", err)
}
if customResult["name"] != "test" {
t.Errorf("Expected 'test', got %v", customResult["name"])
}
fmt.Println("✓ Custom JSON serialization test passed")
}
func TestJSONLFileWithCustomSerialization(t *testing.T) {
// Создаем функции сериализации
customJSONMarshal := func(v any) ([]byte, error) {
data := map[string]any{
"data": v,
"timestamp": time.Now().Unix(),
"version": "1.0",
}
return json.Marshal(data)
}
customJSONUnmarshal := func(data []byte, v any) error {
var wrapper map[string]any
if err := json.Unmarshal(data, &wrapper); err != nil {
return err
}
if dataField, exists := wrapper["data"]; exists {
dataBytes, err := json.Marshal(dataField)
if err != nil {
return err
}
return json.Unmarshal(dataBytes, v)
}
return json.Unmarshal(data, v)
}
// Создаем JSONLFile с кастомными функциями
options := linedb.JSONLFileOptions{
CollectionName: "test",
JSONMarshal: customJSONMarshal,
JSONUnmarshal: customJSONUnmarshal,
}
file := linedb.NewJSONLFile("./test-custom.jsonl", "", options)
// Инициализируем файл
if err := file.Init(true, linedb.LineDbAdapterOptions{}); err != nil {
t.Fatalf("Failed to init file: %v", err)
}
// Тестовые данные
testData := map[string]any{
"id": 1,
"name": "test_user",
"age": 25,
}
// Записываем данные
if err := file.Write(testData, linedb.LineDbAdapterOptions{}); err != nil {
t.Fatalf("Failed to write data: %v", err)
}
// Читаем данные
result, err := file.Read(linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to read data: %v", err)
}
if len(result) != 1 {
t.Fatalf("Expected 1 record, got %d", len(result))
}
if record, ok := result[0].(map[string]any); ok {
if record["name"] != "test_user" {
t.Errorf("Expected 'test_user', got %v", record["name"])
}
} else {
t.Fatal("Failed to cast result to map")
}
fmt.Println("✓ JSONLFile with custom serialization test passed")
}

View File

@@ -0,0 +1,537 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"sort"
"strconv"
"strings"
"time"
"linedb/pkg/linedb"
)
// User представляет пользователя
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
IsActive bool `json:"isActive"`
Role string `json:"role"`
CreatedAt int64 `json:"createdAt"`
}
// Product представляет продукт
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category"`
InStock bool `json:"inStock"`
SellerID int `json:"sellerId"`
CreatedAt int64 `json:"createdAt"`
}
func main() {
// Очищаем тестовые данные
os.RemoveAll("./data/test-linedb-custom-json")
fmt.Println("=== LineDb Custom JSON Serialization Demo ===")
// Получаем номера тестов из аргументов командной строки
testNumbers := parseTestNumbers()
// Выполняем тесты
if len(testNumbers) == 0 {
// Если номера не указаны, выполняем все тесты
runAllTests()
} else {
// Выполняем только указанные тесты
runSelectedTests(testNumbers)
}
fmt.Println("\n=== Все тесты завершены ===")
}
// parseTestNumbers парсит номера тестов из аргументов командной строки
func parseTestNumbers() []int {
if len(os.Args) < 2 {
return []int{}
}
// Получаем аргумент после имени программы
arg := os.Args[1]
// Проверяем, что это не флаг помощи
if arg == "-h" || arg == "--help" || arg == "help" {
printUsage()
return []int{}
}
// Разбиваем по запятой
parts := strings.Split(arg, ",")
var numbers []int
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
num, err := strconv.Atoi(part)
if err != nil {
fmt.Printf("Ошибка: '%s' не является числом\n", part)
printUsage()
return []int{}
}
// Проверяем диапазон
if num < 1 || num > 4 {
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 4\n", num)
printUsage()
return []int{}
}
numbers = append(numbers, num)
}
return numbers
}
// printUsage выводит справку по использованию
func printUsage() {
fmt.Println("\nИспользование:")
fmt.Println(" go run main.go # Запустить все тесты")
fmt.Println(" go run main.go 1 # Запустить только тест 1")
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
fmt.Println(" go run main.go help # Показать эту справку")
fmt.Println("\nДоступные тесты:")
fmt.Println(" 1 - Использование go-json (по умолчанию)")
fmt.Println(" 2 - Использование стандартного encoding/json")
fmt.Println(" 3 - Использование кастомной функции сериализации")
fmt.Println(" 4 - ID всегда первое поле")
os.Exit(0)
}
// runAllTests запускает все тесты
func runAllTests() {
testDefaultJSON()
testStandardJSON()
testCustomJSON()
testIDFirstField()
}
// runSelectedTests запускает только выбранные тесты
func runSelectedTests(numbers []int) {
for _, num := range numbers {
switch num {
case 1:
fmt.Println("1. Использование go-json (по умолчанию)")
testDefaultJSON()
case 2:
fmt.Println("2. Использование стандартного encoding/json")
testStandardJSON()
case 3:
fmt.Println("3. Использование кастомной функции сериализации")
testCustomJSON()
case 4:
fmt.Println("4. ID всегда первое поле")
testIDFirstField()
}
}
}
func testDefaultJSON() {
// Создаем опции инициализации с go-json (по умолчанию)
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-custom-json/default",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователя
user := map[string]any{
"id": 1,
"username": "default_user",
"email": "default@example.com",
"isActive": true,
"role": "user",
"createdAt": time.Now().Unix(),
}
// Вставляем данные
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
// Читаем данные
result, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
fmt.Printf(" Пользователей в базе: %d\n", len(result))
if len(result) > 0 {
if record, ok := result[0].(map[string]any); ok {
fmt.Printf(" ID: %v, Username: %s\n", record["id"], record["username"])
}
}
}
func testStandardJSON() {
// Создаем функции сериализации с использованием стандартного encoding/json
standardJSONMarshal := func(v any) ([]byte, error) {
return json.Marshal(v)
}
standardJSONUnmarshal := func(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Создаем опции инициализации со стандартным JSON
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-custom-json/standard",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
JSONMarshal: standardJSONMarshal,
JSONUnmarshal: standardJSONUnmarshal,
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователя
user := map[string]any{
"id": 2,
"username": "standard_user",
"email": "standard@example.com",
"isActive": true,
"role": "admin",
"createdAt": time.Now().Unix(),
}
// Вставляем данные
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
// Читаем данные
result, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
fmt.Printf(" Пользователей в базе: %d\n", len(result))
if len(result) > 0 {
if record, ok := result[0].(map[string]any); ok {
fmt.Printf(" ID: %v, Username: %s\n", record["id"], record["username"])
}
}
}
func testCustomJSON() {
// Создаем кастомные функции сериализации с дополнительной логикой
customJSONMarshal := func(v any) ([]byte, error) {
// Добавляем метаданные к сериализации
data := map[string]any{
"data": v,
"timestamp": time.Now().Unix(),
"version": "1.0",
}
return json.Marshal(data)
}
customJSONUnmarshal := func(data []byte, v any) error {
// Извлекаем данные из кастомного формата
var wrapper map[string]any
if err := json.Unmarshal(data, &wrapper); err != nil {
return err
}
// Извлекаем основную часть данных
if dataField, exists := wrapper["data"]; exists {
// Сериализуем обратно в JSON и десериализуем в целевой тип
dataBytes, err := json.Marshal(dataField)
if err != nil {
return err
}
return json.Unmarshal(dataBytes, v)
}
// Если нет поля data, пытаемся десериализовать как обычный JSON
return json.Unmarshal(data, v)
}
// Создаем опции инициализации с кастомными функциями
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-custom-json/custom",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
JSONMarshal: customJSONMarshal,
JSONUnmarshal: customJSONUnmarshal,
},
{
CollectionName: "products",
AllocSize: 256,
IndexedFields: []string{"id", "name"},
JSONMarshal: customJSONMarshal,
JSONUnmarshal: customJSONUnmarshal,
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователя
user := map[string]any{
"id": 3,
"username": "custom_user",
"email": "custom@example.com",
"isActive": true,
"role": "moderator",
"createdAt": time.Now().Unix(),
}
// Создаем продукт
product := map[string]any{
"id": 1,
"name": "Custom Product",
"price": 99.99,
"category": "Electronics",
"inStock": true,
"sellerId": 1,
"createdAt": time.Now().Unix(),
}
// Вставляем данные
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
if err := db.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert product: %v", err)
return
}
// Читаем данные
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
products, err := db.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read products: %v", err)
return
}
fmt.Printf(" Пользователей в базе: %d\n", len(users))
fmt.Printf(" Продуктов в базе: %d\n", len(products))
if len(users) > 0 {
if record, ok := users[0].(map[string]any); ok {
fmt.Printf(" Пользователь ID: %v, Username: %s\n", record["id"], record["username"])
}
}
if len(products) > 0 {
if record, ok := products[0].(map[string]any); ok {
fmt.Printf(" Продукт ID: %v, Name: %s, Price: %v\n", record["id"], record["name"], record["price"])
}
}
}
func testIDFirstField() {
// Создаем функции сериализации где id всегда первое поле
idFirstJSONMarshal := func(v any) ([]byte, error) {
if data, ok := v.(map[string]any); ok {
var parts []string
// Сначала добавляем id если он есть
if id, exists := data["id"]; exists {
idBytes, err := json.Marshal(id)
if err != nil {
return nil, err
}
parts = append(parts, fmt.Sprintf(`"id":%s`, string(idBytes)))
}
// Затем добавляем все остальные поля в алфавитном порядке
var keys []string
for key := range data {
if key != "id" {
keys = append(keys, key)
}
}
sort.Strings(keys)
for _, key := range keys {
valueBytes, err := json.Marshal(data[key])
if err != nil {
return nil, err
}
parts = append(parts, fmt.Sprintf(`"%s":%s`, key, string(valueBytes)))
}
jsonStr := "{" + strings.Join(parts, ",") + "}"
return []byte(jsonStr), nil
}
// Если это не map, используем стандартную сериализацию
return json.Marshal(v)
}
idFirstJSONUnmarshal := func(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Создаем опции инициализации с функциями id-first
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-custom-json/id-first",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
JSONMarshal: idFirstJSONMarshal,
JSONUnmarshal: idFirstJSONUnmarshal,
},
{
CollectionName: "products",
AllocSize: 256,
IndexedFields: []string{"id", "name"},
JSONMarshal: idFirstJSONMarshal,
JSONUnmarshal: idFirstJSONUnmarshal,
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователя (id не первое поле в исходных данных)
user := map[string]any{
"username": "id_first_user",
"email": "id_first@example.com",
"id": 1,
"isActive": true,
"role": "user",
"createdAt": time.Now().Unix(),
}
// Создаем продукт (id не первое поле в исходных данных)
product := map[string]any{
"name": "ID First Product",
"price": 123.45,
"id": 2,
"category": "Books",
"inStock": false,
"sellerId": 1,
"createdAt": time.Now().Unix(),
}
// Вставляем данные
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
if err := db.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert product: %v", err)
return
}
// Читаем данные
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
products, err := db.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read products: %v", err)
return
}
fmt.Printf(" Пользователей в базе: %d\n", len(users))
fmt.Printf(" Продуктов в базе: %d\n", len(products))
if len(users) > 0 {
if record, ok := users[0].(map[string]any); ok {
fmt.Printf(" Пользователь ID: %v, Username: %s\n", record["id"], record["username"])
}
}
if len(products) > 0 {
if record, ok := products[0].(map[string]any); ok {
fmt.Printf(" Продукт ID: %v, Name: %s, Price: %v\n", record["id"], record["name"], record["price"])
}
}
// Показываем содержимое файла для демонстрации
fmt.Println(" Проверьте файл ./data/test-linedb-custom-json/id-first/users.jsonl")
fmt.Println(" Поле 'id' должно быть первым в JSON строке")
}

View File

@@ -0,0 +1 @@
{"data":{"age":25,"id":1,"name":"test_user"},"timestamp":1754969067,"version":"1.0"}

View File

@@ -0,0 +1 @@
{"id":2,"category":"Books","createdAt":1754974225,"inStock":false,"name":"ID First Product","price":123.45,"sellerId":1}

View File

@@ -0,0 +1 @@
{"id":1,"createdAt":1754974225,"email":"id_first@example.com","isActive":true,"role":"user","username":"id_first_user"}

View File

@@ -0,0 +1 @@
{"createdAt":1754974225,"email":"standard@example.com","id":2,"isActive":true,"role":"admin","username":"standard_user"}

641
examples/delete/delete.go Normal file
View File

@@ -0,0 +1,641 @@
package main
import (
"fmt"
"log"
"time"
"linedb/pkg/linedb"
)
// testDeleteOneRecordByID тестирует удаление одной записи по ID
func testDeleteOneRecordByID() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем тестовые данные
user := map[string]any{
"id": 1,
"username": "test_user",
"email": "test@example.com",
"isActive": true,
}
// Вставляем данные
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
// Читаем данные
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
fmt.Printf(" Записей до удаления: %d\n", len(users))
// Удаляем запись по ID
deleted, err := db.Delete(map[string]any{"id": 1}, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete user: %v", err)
return
}
fmt.Printf(" Удалено записей: %d\n", len(deleted))
// Проверяем результат
users, err = db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users after delete: %v", err)
return
}
fmt.Printf(" Записей после удаления: %d\n", len(users))
}
// testDeleteOneRecordByTextFilter тестирует удаление одной записи по текстовому фильтру
func testDeleteOneRecordByTextFilter() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "products",
AllocSize: 256,
IndexedFields: []string{"id", "name"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем тестовые данные
product := map[string]any{
"id": 1,
"name": "Test Product",
"price": 99.99,
"category": "Electronics",
}
// Вставляем данные
if err := db.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert product: %v", err)
return
}
// Читаем данные
products, err := db.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read products: %v", err)
return
}
fmt.Printf(" Записей до удаления: %d\n", len(products))
// Удаляем запись по текстовому фильтру
deleted, err := db.Delete("name == 'Test Product'", "products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete product: %v", err)
return
}
fmt.Printf(" Удалено записей: %d\n", len(deleted))
// Проверяем результат
products, err = db.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read products after delete: %v", err)
return
}
fmt.Printf(" Записей после удаления: %d\n", len(products))
}
// testDeleteMultipleRecordsByArray тестирует удаление нескольких записей по массиву данных
func testDeleteMultipleRecordsByArray() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "orders",
AllocSize: 256,
IndexedFields: []string{"id", "status"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем массив тестовых данных
orders := []any{
map[string]any{
"id": 1,
"status": "pending",
"amount": 100.0,
"customerId": 1,
},
map[string]any{
"id": 2,
"status": "completed",
"amount": 200.0,
"customerId": 2,
},
map[string]any{
"id": 3,
"status": "pending",
"amount": 150.0,
"customerId": 1,
},
}
// Вставляем данные
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert orders: %v", err)
return
}
// Читаем данные
ordersData, err := db.Read("orders", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read orders: %v", err)
return
}
fmt.Printf(" Записей до удаления: %d\n", len(ordersData))
// Удаляем несколько записей по массиву фильтров
deleteFilters := []any{
map[string]any{"id": 1},
map[string]any{"id": 2},
}
deleted, err := db.Delete(deleteFilters, "orders", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete orders: %v", err)
return
}
fmt.Printf(" Удалено записей: %d\n", len(deleted))
// Проверяем результат
ordersData, err = db.Read("orders", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read orders after delete: %v", err)
return
}
fmt.Printf(" Записей после удаления: %d\n", len(ordersData))
}
// testDeleteByPartialMatch тестирует удаление записей по частичному совпадению
func testDeleteByPartialMatch() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "customers",
AllocSize: 256,
IndexedFields: []string{"id", "name"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем массив тестовых данных
customers := []any{
map[string]any{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
},
map[string]any{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
},
map[string]any{
"id": 3,
"name": "John Smith",
"email": "john.smith@example.com",
},
}
// Вставляем данные
if err := db.Insert(customers, "customers", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert customers: %v", err)
return
}
// Читаем данные
customersData, err := db.Read("customers", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read customers: %v", err)
return
}
fmt.Printf(" Записей до удаления: %d\n", len(customersData))
// Удаляем записи по частичному совпадению
deleted, err := db.Delete(
map[string]any{"name": "John"},
"customers",
linedb.LineDbAdapterOptions{StrictCompare: false},
)
if err != nil {
log.Printf("Failed to delete customers: %v", err)
return
}
fmt.Printf(" Удалено записей с именем 'John': %d\n", len(deleted))
// Проверяем результат
customersData, err = db.Read("customers", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read customers after delete: %v", err)
return
}
fmt.Printf(" Записей после удаления: %d\n", len(customersData))
}
// testDeleteFromPartitionedCollections тестирует удаление из партиционированных коллекций
func testDeleteFromPartitionedCollections() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных с партициями
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "logs",
AllocSize: 256,
IndexedFields: []string{"id", "level"},
},
},
Partitions: []linedb.PartitionCollection{
{
CollectionName: "logs",
PartIDFnStr: "level",
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем логи для разных уровней
logs := []any{
map[string]any{
"id": 1,
"level": "error",
"message": "Critical error occurred",
"timestamp": time.Now().Unix(),
},
map[string]any{
"id": 2,
"level": "info",
"message": "User logged in",
"timestamp": time.Now().Unix(),
},
map[string]any{
"id": 3,
"level": "error",
"message": "Another error",
"timestamp": time.Now().Unix(),
},
}
// Вставляем данные
if err := db.Insert(logs, "logs", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert logs: %v", err)
return
}
// Читаем данные
logsData, err := db.Read("logs", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read logs: %v", err)
return
}
fmt.Printf(" Записей до удаления: %d\n", len(logsData))
// Удаляем логи с уровнем 'error'
deleted, err := db.Delete(map[string]any{"level": "error"}, "logs", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete error logs: %v", err)
return
}
fmt.Printf(" Удалено записей с уровнем 'error': %d\n", len(deleted))
// Проверяем результат
logsData, err = db.Read("logs", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read logs after delete: %v", err)
return
}
fmt.Printf(" Записей после удаления: %d\n", len(logsData))
}
// testDeleteEdgeCases тестирует граничные случаи удаления
func testDeleteEdgeCases() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "test_data",
AllocSize: 256,
IndexedFields: []string{"id", "type"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Тест удаления несуществующей записи
fmt.Println(" Тест удаления несуществующей записи:")
deleted, err := db.Delete(map[string]any{"id": 999}, "test_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete non-existent record: %v", err)
return
}
fmt.Printf(" Удалено несуществующих записей: %d\n", len(deleted))
// Тест удаления пустого массива
fmt.Println(" Тест удаления пустого массива:")
deleted, err = db.Delete([]any{}, "test_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete empty array: %v", err)
return
}
fmt.Printf(" Удалено записей из пустого массива: %d\n", len(deleted))
// Тест удаления записей с разными типами ID
fmt.Println(" Тест удаления записей с разными типами ID:")
// Вставляем запись со строковым ID
stringIDData := map[string]any{
"id": "user-1",
"name": "Test String Id User",
"type": "string_id",
}
if err := db.Insert(stringIDData, "test_data", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert string ID data: %v", err)
return
}
// Удаляем запись со строковым ID
deleted, err = db.Delete("id == 'user-1'", "test_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete string ID record: %v", err)
return
}
fmt.Printf(" Удалено записей со строковым ID: %d\n", len(deleted))
}
// testDeletePerformance тестирует производительность удаления
func testDeletePerformance() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 10000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "performance_data",
AllocSize: 1024,
IndexedFields: []string{"id", "category"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем большое количество записей
recordCount := 1000
fmt.Printf(" Создание %d записей для теста производительности...\n", recordCount)
largeDataArray := make([]any, recordCount)
for i := 1; i <= recordCount; i++ {
largeDataArray[i-1] = map[string]any{
"id": i,
"name": fmt.Sprintf("User %d", i),
"category": fmt.Sprintf("Category %d", (i-1)%10+1),
"value": float64(i) * 1.5,
}
}
// Вставляем данные
if err := db.Insert(largeDataArray, "performance_data", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert large data array: %v", err)
return
}
// Читаем данные для проверки
result, err := db.Read("performance_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read large data: %v", err)
return
}
fmt.Printf(" Записей в базе: %d\n", len(result))
// Удаляем половину записей (четные ID)
startTime := time.Now()
itemsToDelete := make([]any, recordCount/2)
for i := 0; i < recordCount/2; i++ {
itemsToDelete[i] = map[string]any{"id": (i + 1) * 2}
}
deleted, err := db.Delete(itemsToDelete, "performance_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete large number of records: %v", err)
return
}
endTime := time.Now()
duration := endTime.Sub(startTime)
fmt.Printf(" Удалено записей: %d\n", len(deleted))
fmt.Printf(" Время выполнения: %v\n", duration)
// Проверяем результат
result, err = db.Read("performance_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read data after bulk delete: %v", err)
return
}
fmt.Printf(" Записей после массового удаления: %d\n", len(result))
}
// testDeleteMultipleCollections тестирует удаление из множественных коллекций
func testDeleteMultipleCollections() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных с множественными коллекциями
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-delete",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
},
{
CollectionName: "posts",
AllocSize: 256,
IndexedFields: []string{"id", "authorId"},
},
{
CollectionName: "comments",
AllocSize: 256,
IndexedFields: []string{"id", "postId"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем данные для разных коллекций
users := []any{
map[string]any{
"id": 1,
"username": "user1",
"email": "user1@example.com",
},
map[string]any{
"id": 2,
"username": "user2",
"email": "user2@example.com",
},
}
posts := []any{
map[string]any{
"id": 1,
"title": "First Post",
"authorId": 1,
"content": "This is the first post",
},
map[string]any{
"id": 2,
"title": "Second Post",
"authorId": 2,
"content": "This is the second post",
},
}
comments := []any{
map[string]any{
"id": 1,
"postId": 1,
"content": "Great post!",
"authorId": 2,
},
map[string]any{
"id": 2,
"postId": 2,
"content": "Nice article",
"authorId": 1,
},
}
// Вставляем данные в разные коллекции
if err := db.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert users: %v", err)
return
}
if err := db.Insert(posts, "posts", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert posts: %v", err)
return
}
if err := db.Insert(comments, "comments", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert comments: %v", err)
return
}
// Удаляем данные из разных коллекций
usersDeleted, _ := db.Delete(map[string]any{"id": 1}, "users", linedb.LineDbAdapterOptions{})
postsDeleted, _ := db.Delete(map[string]any{"authorId": 1}, "posts", linedb.LineDbAdapterOptions{})
commentsDeleted, _ := db.Delete(map[string]any{"postId": 1}, "comments", linedb.LineDbAdapterOptions{})
// Читаем данные из всех коллекций
usersData, _ := db.Read("users", linedb.LineDbAdapterOptions{})
postsData, _ := db.Read("posts", linedb.LineDbAdapterOptions{})
commentsData, _ := db.Read("comments", linedb.LineDbAdapterOptions{})
fmt.Printf(" Удалено пользователей: %d\n", len(usersDeleted))
fmt.Printf(" Удалено постов: %d\n", len(postsDeleted))
fmt.Printf(" Удалено комментариев: %d\n", len(commentsDeleted))
fmt.Printf(" Осталось пользователей: %d\n", len(usersData))
fmt.Printf(" Осталось постов: %d\n", len(postsData))
fmt.Printf(" Осталось комментариев: %d\n", len(commentsData))
}

167
examples/delete/main.go Normal file
View File

@@ -0,0 +1,167 @@
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
// TestData представляет тестовые данные
type TestData struct {
ID any `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Value *int `json:"value,omitempty"`
UserID int `json:"userId"`
Timestamp int64 `json:"timestamp"`
}
// TestUser представляет пользователя
type TestUser struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
IsActive bool `json:"isActive"`
Role string `json:"role"`
Timestamp int64 `json:"timestamp"`
}
// TestOrder представляет заказ
type TestOrder struct {
ID int `json:"id"`
UserID int `json:"userId"`
Status string `json:"status"`
Amount float64 `json:"amount"`
Timestamp int64 `json:"timestamp"`
}
func main() {
// Очищаем тестовую папку
os.RemoveAll("./data/test-linedb-delete")
fmt.Println("=== LineDb Delete Operations Demo ===")
// Получаем номера тестов из аргументов командной строки
testNumbers := parseTestNumbers()
// Выполняем тесты
if len(testNumbers) == 0 {
// Если номера не указаны, выполняем все тесты
runAllTests()
} else {
// Выполняем только указанные тесты
runSelectedTests(testNumbers)
}
fmt.Println("\n=== Все тесты завершены ===")
}
// parseTestNumbers парсит номера тестов из аргументов командной строки
func parseTestNumbers() []int {
if len(os.Args) < 2 {
return []int{}
}
// Получаем аргумент после имени программы
arg := os.Args[1]
// Проверяем, что это не флаг помощи
if arg == "-h" || arg == "--help" || arg == "help" {
printUsage()
return []int{}
}
// Разбиваем по запятой
parts := strings.Split(arg, ",")
var numbers []int
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
num, err := strconv.Atoi(part)
if err != nil {
fmt.Printf("Ошибка: '%s' не является числом\n", part)
printUsage()
return []int{}
}
// Проверяем диапазон
if num < 1 || num > 8 {
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 8\n", num)
printUsage()
return []int{}
}
numbers = append(numbers, num)
}
return numbers
}
// printUsage выводит справку по использованию
func printUsage() {
fmt.Println("\nИспользование:")
fmt.Println(" go run main.go # Запустить все тесты")
fmt.Println(" go run main.go 1 # Запустить только тест 1")
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
fmt.Println(" go run main.go help # Показать эту справку")
fmt.Println("\nДоступные тесты:")
fmt.Println(" 1 - Удаление одной записи по ID")
fmt.Println(" 2 - Удаление одной записи по текстовому фильтру")
fmt.Println(" 3 - Удаление нескольких записей по массиву данных")
fmt.Println(" 4 - Удаление записей по частичному совпадению")
fmt.Println(" 5 - Удаление записей из партиционированных коллекций")
fmt.Println(" 6 - Удаление записей с граничными случаями")
fmt.Println(" 7 - Удаление записей с производительностью")
fmt.Println(" 8 - Удаление записей с множественными коллекциями")
os.Exit(0)
}
// runAllTests запускает все тесты
func runAllTests() {
testDeleteOneRecordByID()
testDeleteOneRecordByTextFilter()
testDeleteMultipleRecordsByArray()
testDeleteByPartialMatch()
testDeleteFromPartitionedCollections()
testDeleteEdgeCases()
testDeletePerformance()
testDeleteMultipleCollections()
}
// runSelectedTests запускает только выбранные тесты
func runSelectedTests(numbers []int) {
for _, num := range numbers {
switch num {
case 1:
fmt.Println("1. Удаление одной записи по ID")
testDeleteOneRecordByID()
case 2:
fmt.Println("2. Удаление одной записи по текстовому фильтру")
testDeleteOneRecordByTextFilter()
case 3:
fmt.Println("3. Удаление нескольких записей по массиву данных")
testDeleteMultipleRecordsByArray()
case 4:
fmt.Println("4. Удаление записей по частичному совпадению")
testDeleteByPartialMatch()
case 5:
fmt.Println("5. Удаление записей из партиционированных коллекций")
testDeleteFromPartitionedCollections()
case 6:
fmt.Println("6. Удаление записей с граничными случаями")
testDeleteEdgeCases()
case 7:
fmt.Println("7. Удаление записей с производительностью")
testDeletePerformance()
case 8:
fmt.Println("8. Удаление записей с множественными коллекциями")
testDeleteMultipleCollections()
}
}
}

592
examples/insert/insert.go Normal file
View File

@@ -0,0 +1,592 @@
package main
import (
"fmt"
"log"
"time"
"linedb/pkg/linedb"
)
// testSingleInsert тестирует вставку одной записи
func testSingleInsert() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователя
user := map[string]any{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"isActive": true,
"role": "user",
"createdAt": time.Now().Unix(),
}
// Вставляем данные
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
// Читаем данные
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
fmt.Printf(" Пользователей в базе: %d\n", len(users))
if len(users) > 0 {
if record, ok := users[0].(map[string]any); ok {
fmt.Printf(" ID: %v, Username: %s\n", record["id"], record["username"])
}
}
}
// testArrayInsert тестирует вставку массива записей
func testArrayInsert() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "products",
AllocSize: 256,
IndexedFields: []string{"id", "name"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем массив продуктов
products := []any{
map[string]any{
"id": 1,
"name": "Laptop",
"price": 999.99,
"category": "Electronics",
"inStock": true,
},
map[string]any{
"id": 2,
"name": "Mouse",
"price": 29.99,
"category": "Electronics",
"inStock": true,
},
map[string]any{
"id": 3,
"name": "Keyboard",
"price": 79.99,
"category": "Electronics",
"inStock": false,
},
}
// Вставляем данные
if err := db.Insert(products, "products", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert products: %v", err)
return
}
// Читаем данные
productsData, err := db.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read products: %v", err)
return
}
fmt.Printf(" Продуктов в базе: %d\n", len(productsData))
for i, product := range productsData {
if record, ok := product.(map[string]any); ok {
fmt.Printf(" %d. ID: %v, Name: %s, Price: %v\n", i+1, record["id"], record["name"], record["price"])
}
}
}
// testAutoID тестирует автоматическую генерацию ID
func testAutoID() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "orders",
AllocSize: 256,
IndexedFields: []string{"id", "customerId"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем заказы без ID
orders := []any{
map[string]any{
"customerId": 1,
"amount": 150.00,
"status": "pending",
"createdAt": time.Now().Unix(),
},
map[string]any{
"customerId": 2,
"amount": 75.50,
"status": "completed",
"createdAt": time.Now().Unix(),
},
}
// Вставляем данные
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert orders: %v", err)
return
}
// Читаем данные
ordersData, err := db.Read("orders", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read orders: %v", err)
return
}
fmt.Printf(" Заказов в базе: %d\n", len(ordersData))
for i, order := range ordersData {
if record, ok := order.(map[string]any); ok {
fmt.Printf(" %d. ID: %v, Customer: %v, Amount: %v\n", i+1, record["id"], record["customerId"], record["amount"])
}
}
}
// testPartitionedCollections тестирует партиционированные коллекции
func testPartitionedCollections() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных с партиционированными коллекциями
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users_2024",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
},
{
CollectionName: "users_2023",
AllocSize: 256,
IndexedFields: []string{"id", "username"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователей для разных партиций
users2024 := []any{
map[string]any{
"id": 1,
"username": "user_2024_1",
"email": "user1@2024.com",
"year": 2024,
},
map[string]any{
"id": 2,
"username": "user_2024_2",
"email": "user2@2024.com",
"year": 2024,
},
}
users2023 := []any{
map[string]any{
"id": 3,
"username": "user_2023_1",
"email": "user1@2023.com",
"year": 2023,
},
}
// Вставляем данные в разные партиции
if err := db.Insert(users2024, "users_2024", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert users 2024: %v", err)
return
}
if err := db.Insert(users2023, "users_2023", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert users 2023: %v", err)
return
}
// Читаем данные из обеих партиций
users2024Data, err := db.Read("users_2024", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users 2024: %v", err)
return
}
users2023Data, err := db.Read("users_2023", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users 2023: %v", err)
return
}
fmt.Printf(" Пользователей 2024: %d\n", len(users2024Data))
fmt.Printf(" Пользователей 2023: %d\n", len(users2023Data))
}
// testUniquenessCheck тестирует проверку уникальности
func testUniquenessCheck() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "unique_users",
AllocSize: 256,
IndexedFields: []string{"id", "email"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем пользователя
user := map[string]any{
"id": 1,
"username": "unique_user",
"email": "unique@example.com",
"isActive": true,
}
// Вставляем данные
if err := db.Insert(user, "unique_users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
// Пытаемся вставить пользователя с тем же email
duplicateUser := map[string]any{
"id": 2,
"username": "duplicate_user",
"email": "unique@example.com", // Тот же email
"isActive": true,
}
if err := db.Insert(duplicateUser, "unique_users", linedb.LineDbAdapterOptions{}); err != nil {
fmt.Printf(" Ожидаемая ошибка дублирования: %v\n", err)
} else {
fmt.Printf(" Дублирование не обнаружено\n")
}
// Читаем данные
users, err := db.Read("unique_users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
fmt.Printf(" Уникальных пользователей: %d\n", len(users))
}
// testDifferentDataTypes тестирует различные типы данных
func testDifferentDataTypes() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "mixed_data",
AllocSize: 256,
IndexedFields: []string{"id", "type"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем данные разных типов
mixedData := []any{
map[string]any{
"id": 1,
"type": "string",
"value": "Hello World",
"isString": true,
},
map[string]any{
"id": 2,
"type": "number",
"value": 42.5,
"isNumber": true,
},
map[string]any{
"id": 3,
"type": "boolean",
"value": true,
"isBool": true,
},
map[string]any{
"id": 4,
"type": "array",
"value": []any{1, 2, 3, "four"},
"isArray": true,
},
map[string]any{
"id": 5,
"type": "object",
"value": map[string]any{"nested": "value", "count": 10},
"isObject": true,
},
}
// Вставляем данные
if err := db.Insert(mixedData, "mixed_data", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert mixed data: %v", err)
return
}
// Читаем данные
data, err := db.Read("mixed_data", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read mixed data: %v", err)
return
}
fmt.Printf(" Записей с разными типами данных: %d\n", len(data))
for i, record := range data {
if item, ok := record.(map[string]any); ok {
fmt.Printf(" %d. ID: %v, Type: %s\n", i+1, item["id"], item["type"])
}
}
}
// testBulkInsert тестирует массовую вставку
func testBulkInsert() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных
initOptions := &linedb.LineDbInitOptions{
CacheSize: 10000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "bulk_items",
AllocSize: 1024,
IndexedFields: []string{"id", "category"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем большое количество записей
var bulkItems []any
for i := 1; i <= 1000; i++ {
item := map[string]any{
"id": i,
"name": fmt.Sprintf("Item %d", i),
"category": fmt.Sprintf("Category %d", (i-1)%10+1),
"price": float64(i) * 1.5,
"inStock": i%2 == 0,
"createdAt": time.Now().Unix(),
}
bulkItems = append(bulkItems, item)
}
// Вставляем данные
startTime := time.Now()
if err := db.Insert(bulkItems, "bulk_items", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert bulk items: %v", err)
return
}
insertTime := time.Since(startTime)
// Читаем данные
startTime = time.Now()
items, err := db.Read("bulk_items", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read bulk items: %v", err)
return
}
readTime := time.Since(startTime)
fmt.Printf(" Массовая вставка: %d записей за %v\n", len(items), insertTime)
fmt.Printf(" Чтение: %d записей за %v\n", len(items), readTime)
}
// testMultipleCollections тестирует работу с множественными коллекциями
func testMultipleCollections() {
// Создаем базу данных
db := linedb.NewLineDb(nil)
// Инициализируем базу данных с множественными коллекциями
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-insert",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "customers",
AllocSize: 256,
IndexedFields: []string{"id", "email"},
},
{
CollectionName: "orders",
AllocSize: 256,
IndexedFields: []string{"id", "customerId"},
},
{
CollectionName: "products",
AllocSize: 256,
IndexedFields: []string{"id", "name"},
},
},
}
if err := db.Init(true, initOptions); err != nil {
log.Printf("Failed to init database: %v", err)
return
}
defer db.Close()
// Создаем данные для разных коллекций
customers := []any{
map[string]any{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"isActive": true,
},
map[string]any{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
"isActive": true,
},
}
orders := []any{
map[string]any{
"id": 1,
"customerId": 1,
"amount": 150.00,
"status": "completed",
},
map[string]any{
"id": 2,
"customerId": 2,
"amount": 75.50,
"status": "pending",
},
}
products := []any{
map[string]any{
"id": 1,
"name": "Laptop",
"price": 999.99,
"category": "Electronics",
},
map[string]any{
"id": 2,
"name": "Mouse",
"price": 29.99,
"category": "Electronics",
},
}
// Вставляем данные в разные коллекции
if err := db.Insert(customers, "customers", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert customers: %v", err)
return
}
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert orders: %v", err)
return
}
if err := db.Insert(products, "products", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert products: %v", err)
return
}
// Читаем данные из всех коллекций
customersData, _ := db.Read("customers", linedb.LineDbAdapterOptions{})
ordersData, _ := db.Read("orders", linedb.LineDbAdapterOptions{})
productsData, _ := db.Read("products", linedb.LineDbAdapterOptions{})
fmt.Printf(" Коллекций: 3\n")
fmt.Printf(" Клиентов: %d\n", len(customersData))
fmt.Printf(" Заказов: %d\n", len(ordersData))
fmt.Printf(" Продуктов: %d\n", len(productsData))
}

138
examples/insert/main.go Normal file
View File

@@ -0,0 +1,138 @@
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
func main() {
// Очищаем тестовые данные
os.RemoveAll("./data/test-linedb-insert")
fmt.Println("=== LineDb Insert Operations Demo ===")
// Получаем номера тестов из аргументов командной строки
testNumbers := parseTestNumbers()
// Выполняем тесты
if len(testNumbers) == 0 {
// Если номера не указаны, выполняем все тесты
runAllTests()
} else {
// Выполняем только указанные тесты
runSelectedTests(testNumbers)
}
fmt.Println("\n=== Все тесты завершены ===")
}
// parseTestNumbers парсит номера тестов из аргументов командной строки
func parseTestNumbers() []int {
if len(os.Args) < 2 {
return []int{}
}
// Получаем аргумент после имени программы
arg := os.Args[1]
// Проверяем, что это не флаг помощи
if arg == "-h" || arg == "--help" || arg == "help" {
printUsage()
return []int{}
}
// Разбиваем по запятой
parts := strings.Split(arg, ",")
var numbers []int
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
num, err := strconv.Atoi(part)
if err != nil {
fmt.Printf("Ошибка: '%s' не является числом\n", part)
printUsage()
return []int{}
}
// Проверяем диапазон
if num < 1 || num > 8 {
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 8\n", num)
printUsage()
return []int{}
}
numbers = append(numbers, num)
}
return numbers
}
// printUsage выводит справку по использованию
func printUsage() {
fmt.Println("\nИспользование:")
fmt.Println(" go run main.go # Запустить все тесты")
fmt.Println(" go run main.go 1 # Запустить только тест 1")
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
fmt.Println(" go run main.go help # Показать эту справку")
fmt.Println("\nДоступные тесты:")
fmt.Println(" 1 - Вставка одной записи")
fmt.Println(" 2 - Вставка массива записей")
fmt.Println(" 3 - Автоматическая генерация ID")
fmt.Println(" 4 - Партиционированные коллекции")
fmt.Println(" 5 - Проверка уникальности")
fmt.Println(" 6 - Различные типы данных")
fmt.Println(" 7 - Массовая вставка")
fmt.Println(" 8 - Множественные коллекции")
os.Exit(0)
}
// runAllTests запускает все тесты
func runAllTests() {
testSingleInsert()
testArrayInsert()
testAutoID()
testPartitionedCollections()
testUniquenessCheck()
testDifferentDataTypes()
testBulkInsert()
testMultipleCollections()
}
// runSelectedTests запускает только выбранные тесты
func runSelectedTests(numbers []int) {
for _, num := range numbers {
switch num {
case 1:
fmt.Println("1. Вставка одной записи")
testSingleInsert()
case 2:
fmt.Println("2. Вставка массива записей")
testArrayInsert()
case 3:
fmt.Println("3. Автоматическая генерация ID")
testAutoID()
case 4:
fmt.Println("4. Партиционированные коллекции")
testPartitionedCollections()
case 5:
fmt.Println("5. Проверка уникальности")
testUniquenessCheck()
case 6:
fmt.Println("6. Различные типы данных")
testDifferentDataTypes()
case 7:
fmt.Println("7. Массовая вставка")
testBulkInsert()
case 8:
fmt.Println("8. Множественные коллекции")
testMultipleCollections()
}
}
}

View File

@@ -0,0 +1 @@
{"createdAt":1754974407,"email":"john@example.com","id":1,"isActive":true,"role":"user","username":"john_doe"}

View File

@@ -0,0 +1,722 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"linedb/pkg/linedb"
)
// User представляет пользователя
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
IsActive bool `json:"isActive"`
Role string `json:"role"`
CreatedAt int64 `json:"createdAt"`
LastLogin *int64 `json:"lastLogin,omitempty"`
}
// Product представляет продукт
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category"`
InStock bool `json:"inStock"`
SellerID int `json:"sellerId"`
CreatedAt int64 `json:"createdAt"`
}
// Order представляет заказ
type Order struct {
ID int `json:"id"`
UserID int `json:"userId"`
ProductID int `json:"productId"`
Quantity int `json:"quantity"`
TotalPrice float64 `json:"totalPrice"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
// OrderItem представляет элемент заказа
type OrderItem struct {
ID int `json:"id"`
OrderID int `json:"orderId"`
ProductID int `json:"productId"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
}
// APIResponse представляет ответ API
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
var Database *linedb.LineDb
var server *http.Server
// parseTestNumbers парсит номера тестов из аргументов командной строки
func parseTestNumbers() []int {
if len(os.Args) < 2 {
return []int{}
}
// Получаем аргумент после имени программы
arg := os.Args[1]
// Проверяем, что это не флаг помощи
if arg == "-h" || arg == "--help" || arg == "help" {
printUsage()
return []int{}
}
// Разбиваем по запятой
parts := strings.Split(arg, ",")
var numbers []int
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
num, err := strconv.Atoi(part)
if err != nil {
fmt.Printf("Ошибка: '%s' не является числом\n", part)
printUsage()
return []int{}
}
// Проверяем диапазон
if num < 1 || num > 4 {
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 4\n", num)
printUsage()
return []int{}
}
numbers = append(numbers, num)
}
return numbers
}
// printUsage выводит справку по использованию
func printUsage() {
fmt.Println("\nИспользование:")
fmt.Println(" go run main.go # Запустить все тесты")
fmt.Println(" go run main.go 1 # Запустить только тест 1")
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
fmt.Println(" go run main.go help # Показать эту справку")
fmt.Println("\nДоступные тесты:")
fmt.Println(" 1 - Базовые CRUD операции")
fmt.Println(" 2 - Сложные запросы и фильтрация")
fmt.Println(" 3 - Работа с несколькими коллекциями")
fmt.Println(" 4 - Тест производительности")
fmt.Println(" 5 - HTTP API сервер")
fmt.Println(" 6 - API endpoints")
os.Exit(0)
}
// runAllTests запускает все тесты
func runAllTests() {
testBasicCRUDOperations()
testComplexQueries()
testMultipleCollections()
testPerformance()
testHTTPServer()
testAPIEndpoints()
}
// runSelectedTests запускает только выбранные тесты
func runSelectedTests(numbers []int) {
for _, num := range numbers {
switch num {
case 1:
fmt.Println("1. Базовые CRUD операции")
testBasicCRUDOperations()
case 2:
fmt.Println("2. Сложные запросы и фильтрация")
testComplexQueries()
case 3:
fmt.Println("3. Работа с несколькими коллекциями")
testMultipleCollections()
case 4:
fmt.Println("4. Тест производительности")
testPerformance()
case 5:
fmt.Println("5. HTTP API сервер")
testHTTPServer()
case 6:
fmt.Println("6. API endpoints")
testAPIEndpoints()
}
}
}
func testBasicCRUDOperations() {
// Создаем пользователя
user := map[string]any{
"id": 1,
"username": "testuser",
"email": "test@example.com",
"isActive": true,
"role": "user",
"createdAt": time.Now().Unix(),
}
// Вставка
if err := Database.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
fmt.Printf("\tПользователь создан: %s\n", user["username"])
// Чтение
result, err := Database.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
fmt.Printf("\tПользователей в базе: %d\n", len(result))
// Обновление
updateData := map[string]any{"role": "admin"}
updated, err := Database.Update(updateData, "users", map[string]any{"id": 1}, linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to update user: %v", err)
return
}
fmt.Printf(" Обновлено пользователей: %d\n", len(updated))
// Удаление
deleted, err := Database.Delete(map[string]any{"id": 1}, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to delete user: %v", err)
return
}
fmt.Printf(" Удалено пользователей: %d\n", len(deleted))
// Проверяем, что пользователь удален
result, err = Database.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users after delete: %v", err)
return
}
fmt.Printf(" Пользователей после удаления: %d\n", len(result))
}
func testComplexQueries() {
// Создаем несколько пользователей
users := []any{
map[string]any{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"isActive": true,
"role": "user",
"createdAt": time.Now().Unix(),
},
map[string]any{
"id": 2,
"username": "jane_smith",
"email": "jane@example.com",
"isActive": true,
"role": "admin",
"createdAt": time.Now().Unix(),
},
map[string]any{
"id": 3,
"username": "bob_wilson",
"email": "bob@example.com",
"isActive": false,
"role": "user",
"createdAt": time.Now().Unix(),
},
}
if err := Database.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert users: %v", err)
return
}
// Фильтрация по роли
admins, err := Database.ReadByFilter(map[string]any{"role": "admin"}, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to filter admins: %v", err)
return
}
fmt.Printf(" Администраторов: %d\n", len(admins))
// Фильтрация по активности
activeUsers, err := Database.ReadByFilter(map[string]any{"isActive": true}, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to filter active users: %v", err)
return
}
fmt.Printf(" Активных пользователей: %d\n", len(activeUsers))
// Строковая фильтрация
emailFilter, err := Database.ReadByFilter("email == 'john@example.com'", "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to filter by email: %v", err)
return
}
fmt.Printf(" Пользователей с email john@example.com: %d\n", len(emailFilter))
// Частичное совпадение
partialMatch, err := Database.ReadByFilter(
map[string]any{"username": "john"},
"users",
linedb.LineDbAdapterOptions{StrictCompare: false},
)
if err != nil {
log.Printf("Failed to partial match: %v", err)
return
}
fmt.Printf(" Пользователей с именем содержащим 'john': %d\n", len(partialMatch))
}
func testMultipleCollections() {
// Создаем пользователя
user := map[string]any{
"id": 1,
"username": "customer1",
"email": "customer1@example.com",
"isActive": true,
"role": "customer",
"createdAt": time.Now().Unix(),
}
// Создаем продукты
products := []any{
map[string]any{
"id": 1,
"name": "Laptop",
"price": 999.99,
"category": "Electronics",
"inStock": true,
"sellerId": 1,
"createdAt": time.Now().Unix(),
},
map[string]any{
"id": 2,
"name": "Mouse",
"price": 29.99,
"category": "Electronics",
"inStock": true,
"sellerId": 1,
"createdAt": time.Now().Unix(),
},
}
// Создаем заказ
order := map[string]any{
"id": 1,
"userId": 1,
"productId": 1,
"quantity": 2,
"totalPrice": 1999.98,
"status": "pending",
"createdAt": time.Now().Unix(),
"updatedAt": time.Now().Unix(),
}
// Создаем элементы заказа
orderItems := []any{
map[string]any{
"id": 1,
"orderId": 1,
"productId": 1,
"quantity": 2,
"price": 999.99,
},
}
// Вставляем данные во все коллекции
if err := Database.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert user: %v", err)
return
}
if err := Database.Insert(products, "products", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert products: %v", err)
return
}
if err := Database.Insert(order, "orders", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert order: %v", err)
return
}
if err := Database.Insert(orderItems, "orderItems", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert order items: %v", err)
return
}
// Читаем данные из всех коллекций
users, err := Database.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
productsData, err := Database.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read products: %v", err)
return
}
orders, err := Database.Read("orders", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read orders: %v", err)
return
}
orderItemsData, err := Database.Read("orderItems", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read order items: %v", err)
return
}
fmt.Printf(" Пользователей: %d\n", len(users))
fmt.Printf(" Продуктов: %d\n", len(productsData))
fmt.Printf(" Заказов: %d\n", len(orders))
fmt.Printf(" Элементов заказов: %d\n", len(orderItemsData))
// Сложный запрос: найти все заказы пользователя
userOrders, err := Database.ReadByFilter(map[string]any{"userId": 1}, "orders", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to get user orders: %v", err)
return
}
fmt.Printf(" Заказов пользователя 1: %d\n", len(userOrders))
}
func testPerformance() {
// Тест производительности массовой вставки
recordCount := 500
fmt.Printf(" Создание %d записей для теста производительности...\n", recordCount)
// Создаем пользователей
users := make([]any, recordCount)
for i := 1; i <= recordCount; i++ {
users[i-1] = map[string]any{
"id": i,
"username": fmt.Sprintf("user%d", i),
"email": fmt.Sprintf("user%d@example.com", i),
"isActive": i%2 == 0, // чередуем активных и неактивных
"role": "user",
"createdAt": time.Now().Unix(),
}
}
// Измеряем время вставки
startTime := time.Now()
if err := Database.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
log.Printf("Failed to insert users: %v", err)
return
}
endTime := time.Now()
duration := endTime.Sub(startTime)
fmt.Printf(" Вставлено пользователей: %d\n", recordCount)
fmt.Printf(" Время выполнения: %v\n", duration)
fmt.Printf(" Скорость: %.2f записей/сек\n", float64(recordCount)/duration.Seconds())
// Тест производительности чтения
startTime = time.Now()
result, err := Database.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to read users: %v", err)
return
}
endTime = time.Now()
readDuration := endTime.Sub(startTime)
fmt.Printf(" Прочитано пользователей: %d\n", len(result))
fmt.Printf(" Время чтения: %v\n", readDuration)
fmt.Printf(" Скорость чтения: %.2f записей/сек\n", float64(len(result))/readDuration.Seconds())
// Тест производительности фильтрации
startTime = time.Now()
activeUsers, err := Database.ReadByFilter(map[string]any{"isActive": true}, "users", linedb.LineDbAdapterOptions{})
if err != nil {
log.Printf("Failed to filter active users: %v", err)
return
}
endTime = time.Now()
filterDuration := endTime.Sub(startTime)
fmt.Printf(" Активных пользователей: %d\n", len(activeUsers))
fmt.Printf(" Время фильтрации: %v\n", filterDuration)
}
func testHTTPServer() {
// Настраиваем HTTP сервер
mux := http.NewServeMux()
// Обработчики API
mux.HandleFunc("/api/users", handleUsers)
mux.HandleFunc("/api/products", handleProducts)
mux.HandleFunc("/api/orders", handleOrders)
server = &http.Server{
Addr: ":3001",
Handler: mux,
}
fmt.Printf(" Запуск HTTP сервера на порту 3001...\n")
fmt.Printf(" Доступные эндпоинты:\n")
fmt.Printf(" - GET /api/users - получить всех пользователей\n")
fmt.Printf(" - POST /api/users - создать пользователя\n")
fmt.Printf(" - GET /api/products - получить все продукты\n")
fmt.Printf(" - POST /api/products - создать продукт\n")
fmt.Printf(" - GET /api/orders - получить все заказы\n")
fmt.Printf(" - POST /api/orders - создать заказ\n")
// Запускаем сервер в горутине
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
}
}()
// Даем серверу время на запуск
time.Sleep(1 * time.Second)
// Тестируем API
fmt.Printf(" Тестирование API...\n")
testAPIEndpoints()
// Останавливаем сервер
if err := server.Close(); err != nil {
log.Printf("Failed to close server: %v", err)
}
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
users, err := Database.Read("users", linedb.LineDbAdapterOptions{})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(APIResponse{Success: true, Data: users})
case "POST":
var user map[string]any
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := Database.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "User created successfully"})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleProducts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
products, err := Database.Read("products", linedb.LineDbAdapterOptions{})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(APIResponse{Success: true, Data: products})
case "POST":
var product map[string]any
if err := json.NewDecoder(r.Body).Decode(&product); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := Database.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "Product created successfully"})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleOrders(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
orders, err := Database.Read("orders", linedb.LineDbAdapterOptions{})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(APIResponse{Success: true, Data: orders})
case "POST":
var order map[string]any
if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := Database.Insert(order, "orders", linedb.LineDbAdapterOptions{}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "Order created successfully"})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func testAPIEndpoints() {
// Тестируем GET /api/users
resp, err := http.Get("http://localhost:3001/api/users")
if err != nil {
log.Printf("Failed to test GET /api/users: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Printf(" ✓ GET /api/users работает\n")
} else {
fmt.Printf(" ✗ GET /api/users вернул статус %d\n", resp.StatusCode)
}
// Тестируем POST /api/users
userData := map[string]any{
"id": 999,
"username": "apitest",
"email": "apitest@example.com",
"isActive": true,
"role": "user",
"createdAt": time.Now().Unix(),
}
jsonData, _ := json.Marshal(userData)
resp, err = http.Post("http://localhost:3001/api/users", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("Failed to test POST /api/users: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Printf(" ✓ POST /api/users работает\n")
} else {
fmt.Printf(" ✗ POST /api/users вернул статус %d\n", resp.StatusCode)
}
}
func main() {
// Очищаем тестовую папку
os.RemoveAll("./data/test-linedb-integration")
// Создаем опции инициализации
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: 10 * time.Second,
DBFolder: "./data/test-linedb-integration",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "username", "email"},
},
{
CollectionName: "products",
AllocSize: 256,
IndexedFields: []string{"id", "name", "category"},
},
{
CollectionName: "orders",
AllocSize: 256,
IndexedFields: []string{"id", "userId", "status"},
},
{
CollectionName: "orderItems",
AllocSize: 256,
IndexedFields: []string{"id", "orderId", "productId"},
},
},
Partitions: []linedb.PartitionCollection{
{
CollectionName: "orders",
PartIDFnStr: "userId",
},
},
}
// Создаем базу данных
Database = linedb.NewLineDb(nil)
// Инициализируем базу данных
if err := Database.Init(true, initOptions); err != nil {
log.Fatalf("Failed to init database: %v", err)
}
defer Database.Close()
fmt.Println("=== LineDb Integration Tests Demo ===")
numbers := parseTestNumbers()
if len(numbers) > 0 {
runSelectedTests(numbers)
} else {
runAllTests()
}
// // Тест 1: Базовые CRUD операции
// fmt.Println("1. Базовые CRUD операции")
// testBasicCRUDOperations()
// // Тест 2: Сложные запросы и фильтрация
// fmt.Println("\n2. Сложные запросы и фильтрация")
// testComplexQueries()
// // Тест 3: Работа с несколькими коллекциями
// fmt.Println("\n3. Работа с несколькими коллекциями")
// testMultipleCollections()
// // Тест 4: Производительность
// fmt.Println("\n4. Тест производительности")
// testPerformance()
// // Тест 5: HTTP API сервер
// fmt.Println("\n5. HTTP API сервер")
// testHTTPServer()
fmt.Println("\n=== Все тесты завершены ===")
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module linedb
go 1.21
require github.com/goccy/go-json v0.10.5

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=

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]
}

201
tests/linedb_test.go Normal file
View File

@@ -0,0 +1,201 @@
package tests
import (
"os"
"testing"
"time"
"linedb/pkg/linedb"
)
func TestLineDbBasic(t *testing.T) {
// Очищаем тестовую папку
os.RemoveAll("./testdata")
// Создаем опции инициализации
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "test",
AllocSize: 256,
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
defer db.Close()
// Инициализируем базу данных
if err := db.Init(false, initOptions); err != nil {
t.Fatalf("Failed to init database: %v", err)
}
// Тест вставки
testData := map[string]any{
"name": "test",
"value": 123,
}
if err := db.Insert(testData, "test", linedb.LineDbAdapterOptions{}); err != nil {
t.Fatalf("Failed to insert data: %v", err)
}
// Тест чтения
allData, err := db.Read("test", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to read data: %v", err)
}
if len(allData) != 1 {
t.Fatalf("Expected 1 record, got %d", len(allData))
}
// Тест фильтрации
filter := map[string]any{"name": "test"}
filteredData, err := db.ReadByFilter(filter, "test", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to filter data: %v", err)
}
if len(filteredData) != 1 {
t.Fatalf("Expected 1 filtered record, got %d", len(filteredData))
}
// Тест обновления
updateData := map[string]any{"value": 456}
updateFilter := map[string]any{"name": "test"}
updatedData, err := db.Update(updateData, "test", updateFilter, linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to update data: %v", err)
}
if len(updatedData) != 1 {
t.Fatalf("Expected 1 updated record, got %d", len(updatedData))
}
// Тест удаления
deleteFilter := map[string]any{"name": "test"}
deletedData, err := db.Delete(deleteFilter, "test", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to delete data: %v", err)
}
if len(deletedData) != 1 {
t.Fatalf("Expected 1 deleted record, got %d", len(deletedData))
}
// Проверяем что данных больше нет
remainingData, err := db.Read("test", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to read remaining data: %v", err)
}
if len(remainingData) != 0 {
t.Fatalf("Expected 0 remaining records, got %d", len(remainingData))
}
}
func TestLineDbPartitioning(t *testing.T) {
// Очищаем тестовую папку
os.RemoveAll("./testdata")
// Создаем опции инициализации с партиционированием
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "orders",
AllocSize: 256,
},
},
Partitions: []linedb.PartitionCollection{
{
CollectionName: "orders",
PartIDFn: func(item any) string {
if itemMap, ok := item.(map[string]any); ok {
if userId, exists := itemMap["userId"]; exists {
return toString(userId)
}
}
return "default"
},
},
},
}
// Создаем базу данных
db := linedb.NewLineDb(nil)
defer db.Close()
// Инициализируем базу данных
if err := db.Init(false, initOptions); err != nil {
t.Fatalf("Failed to init database: %v", err)
}
// Создаем заказы для разных пользователей
orders := []any{
map[string]any{
"userId": 1,
"item": "laptop",
"price": 999.99,
},
map[string]any{
"userId": 1,
"item": "mouse",
"price": 29.99,
},
map[string]any{
"userId": 2,
"item": "keyboard",
"price": 89.99,
},
}
// Вставляем заказы
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
t.Fatalf("Failed to insert orders: %v", err)
}
// Читаем все заказы
allOrders, err := db.Read("orders", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to read orders: %v", err)
}
if len(allOrders) != 3 {
t.Fatalf("Expected 3 orders, got %d", len(allOrders))
}
// Фильтруем заказы пользователя 1
user1Filter := map[string]any{"userId": 1}
user1Orders, err := db.ReadByFilter(user1Filter, "orders", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Failed to filter user 1 orders: %v", err)
}
if len(user1Orders) != 2 {
t.Fatalf("Expected 2 orders for user 1, got %d", len(user1Orders))
}
}
// toString конвертирует значение в строку
func toString(value any) string {
switch v := value.(type) {
case string:
return v
case int:
return string(rune(v))
case int64:
return string(rune(v))
case float64:
return string(rune(int(v)))
default:
return ""
}
}

30
tests/main_test.go Normal file
View File

@@ -0,0 +1,30 @@
package tests
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
// Настройка перед тестами
setup()
// Запуск тестов
code := m.Run()
// Очистка после тестов
teardown()
// Выход с кодом
os.Exit(code)
}
func setup() {
// Создаем тестовые директории
os.MkdirAll("./testdata", 0755)
}
func teardown() {
// Очищаем тестовые данные
os.RemoveAll("./testdata")
}