From 1f31ab074bbfe2271f12a0807e9c7f2eb48fa0b1 Mon Sep 17 00:00:00 2001 From: "direct-dev.ru" Date: Tue, 3 Mar 2026 14:36:38 +0600 Subject: [PATCH] init elowdb go-port commit --- .golangci.yml | 27 + .vscode/launch.json | 65 ++ .vscode/settings.json | 3 + CUSTOM_JSON.md | 354 +++++++ DEBUG.md | 219 ++++ Makefile | 88 ++ OPTIMIZATION_SUMMARY.md | 153 +++ QUICK_DEBUG.md | 94 ++ README.md | 152 +++ USAGE.md | 205 ++++ data/test-linedb-integration/orderItems.jsonl | 0 data/test-linedb-integration/orders.jsonl | 0 data/test-linedb-integration/products.jsonl | 0 data/test-linedb-integration/users.jsonl | 1 + data/users.jsonl | 2 + debug.sh | 93 ++ debug_app.go | 140 +++ examples/README.md | 203 ++++ examples/basic/main.go | 124 +++ examples/custom-json/custom_json_test.go | 162 +++ examples/custom-json/main.go | 537 ++++++++++ examples/custom-json/test-custom.jsonl | 1 + .../id-first/products.jsonl | 1 + .../id-first/users.jsonl | 1 + .../standard/users.jsonl | 1 + examples/delete/delete.go | 641 ++++++++++++ examples/delete/main.go | 167 +++ examples/insert/insert.go | 592 +++++++++++ examples/insert/main.go | 138 +++ .../insert/test-linedb-insert/users.jsonl | 1 + examples/integration/main.go | 722 +++++++++++++ go.mod | 5 + go.sum | 2 + pkg/linedb/cache.go | 200 ++++ pkg/linedb/jsonl_file.go | 484 +++++++++ pkg/linedb/last_id_manager.go | 75 ++ pkg/linedb/line_db.go | 962 ++++++++++++++++++ pkg/linedb/transaction.go | 147 +++ pkg/linedb/types.go | 280 +++++ tests/linedb_test.go | 201 ++++ tests/main_test.go | 30 + 41 files changed, 7273 insertions(+) create mode 100644 .golangci.yml create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 CUSTOM_JSON.md create mode 100644 DEBUG.md create mode 100644 Makefile create mode 100644 OPTIMIZATION_SUMMARY.md create mode 100644 QUICK_DEBUG.md create mode 100644 README.md create mode 100644 USAGE.md create mode 100644 data/test-linedb-integration/orderItems.jsonl create mode 100644 data/test-linedb-integration/orders.jsonl create mode 100644 data/test-linedb-integration/products.jsonl create mode 100644 data/test-linedb-integration/users.jsonl create mode 100644 data/users.jsonl create mode 100755 debug.sh create mode 100644 debug_app.go create mode 100644 examples/README.md create mode 100644 examples/basic/main.go create mode 100644 examples/custom-json/custom_json_test.go create mode 100644 examples/custom-json/main.go create mode 100644 examples/custom-json/test-custom.jsonl create mode 100644 examples/custom-json/test-linedb-custom-json/id-first/products.jsonl create mode 100644 examples/custom-json/test-linedb-custom-json/id-first/users.jsonl create mode 100644 examples/custom-json/test-linedb-custom-json/standard/users.jsonl create mode 100644 examples/delete/delete.go create mode 100644 examples/delete/main.go create mode 100644 examples/insert/insert.go create mode 100644 examples/insert/main.go create mode 100644 examples/insert/test-linedb-insert/users.jsonl create mode 100644 examples/integration/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/linedb/cache.go create mode 100644 pkg/linedb/jsonl_file.go create mode 100644 pkg/linedb/last_id_manager.go create mode 100644 pkg/linedb/line_db.go create mode 100644 pkg/linedb/transaction.go create mode 100644 pkg/linedb/types.go create mode 100644 tests/linedb_test.go create mode 100644 tests/main_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..da3e882 --- /dev/null +++ b/.golangci.yml @@ -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 + + diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a52e029 --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..082b194 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "makefile.configureOnOpen": false +} \ No newline at end of file diff --git a/CUSTOM_JSON.md b/CUSTOM_JSON.md new file mode 100644 index 0000000..939a77a --- /dev/null +++ b/CUSTOM_JSON.md @@ -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 +- Обработку ошибок diff --git a/DEBUG.md b/DEBUG.md new file mode 100644 index 0000000..4edde3f --- /dev/null +++ b/DEBUG.md @@ -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...) + } +} +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a2a596f --- /dev/null +++ b/Makefile @@ -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 - Показать эту справку" \ No newline at end of file diff --git a/OPTIMIZATION_SUMMARY.md b/OPTIMIZATION_SUMMARY.md new file mode 100644 index 0000000..8597104 --- /dev/null +++ b/OPTIMIZATION_SUMMARY.md @@ -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. **Мониторинг**: Добавить метрики производительности diff --git a/QUICK_DEBUG.md b/QUICK_DEBUG.md new file mode 100644 index 0000000..9b9e59f --- /dev/null +++ b/QUICK_DEBUG.md @@ -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` для полной документации по отладке. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d010b2 --- /dev/null +++ b/README.md @@ -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 + diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..52ee8c4 --- /dev/null +++ b/USAGE.md @@ -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` diff --git a/data/test-linedb-integration/orderItems.jsonl b/data/test-linedb-integration/orderItems.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/data/test-linedb-integration/orders.jsonl b/data/test-linedb-integration/orders.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/data/test-linedb-integration/products.jsonl b/data/test-linedb-integration/products.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/data/test-linedb-integration/users.jsonl b/data/test-linedb-integration/users.jsonl new file mode 100644 index 0000000..d4a320b --- /dev/null +++ b/data/test-linedb-integration/users.jsonl @@ -0,0 +1 @@ +{"createdAt":1755088501,"email":"test@example.com","id":1,"isActive":true,"role":"user","username":"testuser"} diff --git a/data/users.jsonl b/data/users.jsonl new file mode 100644 index 0000000..4d708d7 --- /dev/null +++ b/data/users.jsonl @@ -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"} diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..56bdcc2 --- /dev/null +++ b/debug.sh @@ -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 diff --git a/debug_app.go b/debug_app.go new file mode 100644 index 0000000..160e77e --- /dev/null +++ b/debug_app.go @@ -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("=== Все тесты прошли успешно! ===") +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..812afb3 --- /dev/null +++ b/examples/README.md @@ -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"` +} +``` + +## Конфигурация + +Каждый пример создает свою тестовую папку и настраивает базу данных с соответствующими коллекциями и индексами. После выполнения примеры автоматически очищают тестовые данные. + +## Примечания + +- Все примеры написаны на русском языке для лучшего понимания +- Примеры демонстрируют реальные сценарии использования +- Код включает обработку ошибок и логирование +- Производительность измеряется и выводится в консоль diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..9322658 --- /dev/null +++ b/examples/basic/main.go @@ -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) + } +} diff --git a/examples/custom-json/custom_json_test.go b/examples/custom-json/custom_json_test.go new file mode 100644 index 0000000..c9ccbf0 --- /dev/null +++ b/examples/custom-json/custom_json_test.go @@ -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") +} diff --git a/examples/custom-json/main.go b/examples/custom-json/main.go new file mode 100644 index 0000000..1251f80 --- /dev/null +++ b/examples/custom-json/main.go @@ -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 строке") +} diff --git a/examples/custom-json/test-custom.jsonl b/examples/custom-json/test-custom.jsonl new file mode 100644 index 0000000..68ec08f --- /dev/null +++ b/examples/custom-json/test-custom.jsonl @@ -0,0 +1 @@ +{"data":{"age":25,"id":1,"name":"test_user"},"timestamp":1754969067,"version":"1.0"} diff --git a/examples/custom-json/test-linedb-custom-json/id-first/products.jsonl b/examples/custom-json/test-linedb-custom-json/id-first/products.jsonl new file mode 100644 index 0000000..aa898f8 --- /dev/null +++ b/examples/custom-json/test-linedb-custom-json/id-first/products.jsonl @@ -0,0 +1 @@ +{"id":2,"category":"Books","createdAt":1754974225,"inStock":false,"name":"ID First Product","price":123.45,"sellerId":1} diff --git a/examples/custom-json/test-linedb-custom-json/id-first/users.jsonl b/examples/custom-json/test-linedb-custom-json/id-first/users.jsonl new file mode 100644 index 0000000..0eb1ca3 --- /dev/null +++ b/examples/custom-json/test-linedb-custom-json/id-first/users.jsonl @@ -0,0 +1 @@ +{"id":1,"createdAt":1754974225,"email":"id_first@example.com","isActive":true,"role":"user","username":"id_first_user"} diff --git a/examples/custom-json/test-linedb-custom-json/standard/users.jsonl b/examples/custom-json/test-linedb-custom-json/standard/users.jsonl new file mode 100644 index 0000000..5e891d8 --- /dev/null +++ b/examples/custom-json/test-linedb-custom-json/standard/users.jsonl @@ -0,0 +1 @@ +{"createdAt":1754974225,"email":"standard@example.com","id":2,"isActive":true,"role":"admin","username":"standard_user"} diff --git a/examples/delete/delete.go b/examples/delete/delete.go new file mode 100644 index 0000000..39c6c69 --- /dev/null +++ b/examples/delete/delete.go @@ -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)) +} diff --git a/examples/delete/main.go b/examples/delete/main.go new file mode 100644 index 0000000..6ae8c72 --- /dev/null +++ b/examples/delete/main.go @@ -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() + } + } +} diff --git a/examples/insert/insert.go b/examples/insert/insert.go new file mode 100644 index 0000000..0231cc9 --- /dev/null +++ b/examples/insert/insert.go @@ -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)) +} diff --git a/examples/insert/main.go b/examples/insert/main.go new file mode 100644 index 0000000..fbc89dd --- /dev/null +++ b/examples/insert/main.go @@ -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() + } + } +} diff --git a/examples/insert/test-linedb-insert/users.jsonl b/examples/insert/test-linedb-insert/users.jsonl new file mode 100644 index 0000000..4b1b10f --- /dev/null +++ b/examples/insert/test-linedb-insert/users.jsonl @@ -0,0 +1 @@ +{"createdAt":1754974407,"email":"john@example.com","id":1,"isActive":true,"role":"user","username":"john_doe"} diff --git a/examples/integration/main.go b/examples/integration/main.go new file mode 100644 index 0000000..43cad84 --- /dev/null +++ b/examples/integration/main.go @@ -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=== Все тесты завершены ===") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c0843c3 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module linedb + +go 1.21 + +require github.com/goccy/go-json v0.10.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d801e82 --- /dev/null +++ b/go.sum @@ -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= diff --git a/pkg/linedb/cache.go b/pkg/linedb/cache.go new file mode 100644 index 0000000..8cacfb0 --- /dev/null +++ b/pkg/linedb/cache.go @@ -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" + } +} diff --git a/pkg/linedb/jsonl_file.go b/pkg/linedb/jsonl_file.go new file mode 100644 index 0000000..1eac80c --- /dev/null +++ b/pkg/linedb/jsonl_file.go @@ -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()) +} diff --git a/pkg/linedb/last_id_manager.go b/pkg/linedb/last_id_manager.go new file mode 100644 index 0000000..d93ed29 --- /dev/null +++ b/pkg/linedb/last_id_manager.go @@ -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 +} diff --git a/pkg/linedb/line_db.go b/pkg/linedb/line_db.go new file mode 100644 index 0000000..b52645e --- /dev/null +++ b/pkg/linedb/line_db.go @@ -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() +} diff --git a/pkg/linedb/transaction.go b/pkg/linedb/transaction.go new file mode 100644 index 0000000..4579fba --- /dev/null +++ b/pkg/linedb/transaction.go @@ -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 +} diff --git a/pkg/linedb/types.go b/pkg/linedb/types.go new file mode 100644 index 0000000..a3bc0b0 --- /dev/null +++ b/pkg/linedb/types.go @@ -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] +} diff --git a/tests/linedb_test.go b/tests/linedb_test.go new file mode 100644 index 0000000..4842208 --- /dev/null +++ b/tests/linedb_test.go @@ -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 "" + } +} diff --git a/tests/main_test.go b/tests/main_test.go new file mode 100644 index 0000000..25a69c0 --- /dev/null +++ b/tests/main_test.go @@ -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") +}