init elowdb go-port commit
This commit is contained in:
27
.golangci.yml
Normal file
27
.golangci.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- golint
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- gosimple
|
||||
- ineffassign
|
||||
- unused
|
||||
- misspell
|
||||
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0.8
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
|
||||
|
||||
65
.vscode/launch.json
vendored
Normal file
65
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug LineDB Debug App",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/debug_app.go",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"showLog": true,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Debug LineDB Simple Test",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/simple.go",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"showLog": true,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Debug LineDB Tests",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "test",
|
||||
"program": "${workspaceFolder}/tests",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"args": ["-v"],
|
||||
"showLog": true,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Debug LineDB Example",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/examples/basic/main.go",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"showLog": true,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Debug LineDB Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/pkg/linedb",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"showLog": true,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
354
CUSTOM_JSON.md
Normal file
354
CUSTOM_JSON.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Настраиваемые функции сериализации JSON в LineDb
|
||||
|
||||
## Обзор
|
||||
|
||||
LineDb поддерживает настраиваемые функции сериализации и десериализации JSON. По умолчанию используется библиотека [go-json](https://github.com/goccy/go-json), но вы можете указать любые функции сериализации, соответствующие определенным сигнатурам.
|
||||
|
||||
## Функции по умолчанию
|
||||
|
||||
```go
|
||||
// Функции сериализации по умолчанию (используют go-json)
|
||||
func defaultJSONMarshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func defaultJSONUnmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
```
|
||||
|
||||
## Настройка в опциях коллекции
|
||||
|
||||
Вы можете указать пользовательские функции сериализации в `JSONLFileOptions`:
|
||||
|
||||
```go
|
||||
type JSONLFileOptions struct {
|
||||
CollectionName string `json:"collectionName,omitempty"`
|
||||
AllocSize int `json:"allocSize,omitempty"`
|
||||
IndexedFields []string `json:"indexedFields,omitempty"`
|
||||
EncryptKeyForLineDb string `json:"encryptKeyForLineDb,omitempty"`
|
||||
SkipInvalidLines bool `json:"skipInvalidLines,omitempty"`
|
||||
DecryptKey string `json:"decryptKey,omitempty"`
|
||||
ConvertStringIdToNumber bool `json:"convertStringIdToNumber,omitempty"`
|
||||
// Функции сериализации и десериализации JSON
|
||||
JSONMarshal func(any) ([]byte, error) `json:"-"`
|
||||
JSONUnmarshal func([]byte, any) error `json:"-"`
|
||||
}
|
||||
```
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### 1. Использование стандартного encoding/json
|
||||
|
||||
```go
|
||||
import "encoding/json"
|
||||
|
||||
// Создаем функции сериализации с использованием стандартного encoding/json
|
||||
standardJSONMarshal := func(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
standardJSONUnmarshal := func(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Настройка в опциях инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
JSONMarshal: standardJSONMarshal,
|
||||
JSONUnmarshal: standardJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Кастомные функции с метаданными
|
||||
|
||||
```go
|
||||
import "encoding/json"
|
||||
import "time"
|
||||
|
||||
// Создаем кастомные функции сериализации с дополнительной логикой
|
||||
customJSONMarshal := func(v any) ([]byte, error) {
|
||||
// Добавляем метаданные к сериализации
|
||||
data := map[string]any{
|
||||
"data": v,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "1.0",
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
customJSONUnmarshal := func(data []byte, v any) error {
|
||||
// Извлекаем данные из кастомного формата
|
||||
var wrapper map[string]any
|
||||
if err := json.Unmarshal(data, &wrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Извлекаем основную часть данных
|
||||
if dataField, exists := wrapper["data"]; exists {
|
||||
// Сериализуем обратно в JSON и десериализуем в целевой тип
|
||||
dataBytes, err := json.Marshal(dataField)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(dataBytes, v)
|
||||
}
|
||||
|
||||
// Если нет поля data, пытаемся десериализовать как обычный JSON
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Настройка в опциях инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
JSONMarshal: customJSONMarshal,
|
||||
JSONUnmarshal: customJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Функции с шифрованием
|
||||
|
||||
```go
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var encryptionKey = []byte("your-32-byte-encryption-key-here")
|
||||
|
||||
func encryptedJSONMarshal(v any) ([]byte, error) {
|
||||
// Сначала сериализуем в JSON
|
||||
jsonData, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Создаем AES cipher
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Создаем GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Создаем nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Шифруем данные
|
||||
ciphertext := gcm.Seal(nonce, nonce, jsonData, nil)
|
||||
|
||||
// Кодируем в base64
|
||||
return []byte(base64.StdEncoding.EncodeToString(ciphertext)), nil
|
||||
}
|
||||
|
||||
func encryptedJSONUnmarshal(data []byte, v any) error {
|
||||
// Декодируем из base64
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Создаем AES cipher
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Создаем GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Извлекаем nonce
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
|
||||
// Расшифровываем данные
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Десериализуем JSON
|
||||
return json.Unmarshal(plaintext, v)
|
||||
}
|
||||
|
||||
// Настройка в опциях инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "secure_users",
|
||||
JSONMarshal: encryptedJSONMarshal,
|
||||
JSONUnmarshal: encryptedJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. ID всегда первое поле
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func idFirstJSONMarshal(v any) ([]byte, error) {
|
||||
if data, ok := v.(map[string]any); ok {
|
||||
var parts []string
|
||||
|
||||
// Сначала добавляем id если он есть
|
||||
if id, exists := data["id"]; exists {
|
||||
idBytes, err := json.Marshal(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf(`"id":%s`, string(idBytes)))
|
||||
}
|
||||
|
||||
// Затем добавляем все остальные поля в алфавитном порядке
|
||||
var keys []string
|
||||
for key := range data {
|
||||
if key != "id" {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
valueBytes, err := json.Marshal(data[key])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf(`"%s":%s`, key, string(valueBytes)))
|
||||
}
|
||||
|
||||
jsonStr := "{" + strings.Join(parts, ",") + "}"
|
||||
return []byte(jsonStr), nil
|
||||
}
|
||||
|
||||
// Если это не map, используем стандартную сериализацию
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func idFirstJSONUnmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Настройка в опциях инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
JSONMarshal: idFirstJSONMarshal,
|
||||
JSONUnmarshal: idFirstJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Требования к функциям
|
||||
|
||||
### JSONMarshal
|
||||
|
||||
- **Сигнатура**: `func(any) ([]byte, error)`
|
||||
- **Назначение**: Сериализует данные в JSON
|
||||
- **Возвращает**: Байты JSON и ошибку (если есть)
|
||||
|
||||
### JSONUnmarshal
|
||||
|
||||
- **Сигнатура**: `func([]byte, any) error`
|
||||
- **Назначение**: Десериализует данные из JSON
|
||||
- **Возвращает**: Ошибку (если есть)
|
||||
|
||||
## Совместимость
|
||||
|
||||
При использовании кастомных функций сериализации важно обеспечить совместимость:
|
||||
|
||||
1. **Обратная совместимость**: Убедитесь, что новые функции могут читать данные, записанные старыми функциями
|
||||
2. **Версионирование**: Рассмотрите добавление версии в метаданные для будущих изменений
|
||||
3. **Тестирование**: Всегда тестируйте функции сериализации с реальными данными
|
||||
|
||||
## Примеры файлов
|
||||
|
||||
### Стандартный JSON (go-json по умолчанию)
|
||||
|
||||
```json
|
||||
{"id":1,"username":"user1","email":"user1@example.com"}
|
||||
```
|
||||
|
||||
### Стандартный JSON (encoding/json)
|
||||
|
||||
```json
|
||||
{"id":2,"username":"user2","email":"user2@example.com"}
|
||||
```
|
||||
|
||||
### Кастомный JSON с метаданными
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {"id":3,"username":"user3","email":"user3@example.com"},
|
||||
"timestamp": 1754969076,
|
||||
"version": "1.0"
|
||||
}
|
||||
```
|
||||
|
||||
### JSON с ID первым полем
|
||||
|
||||
```json
|
||||
{"id":1,"createdAt":1754969435,"email":"user@example.com","isActive":true,"role":"user","username":"user1"}
|
||||
```
|
||||
|
||||
## Запуск примера
|
||||
|
||||
```bash
|
||||
cd examples/custom-json
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Этот пример демонстрирует:
|
||||
|
||||
- Использование go-json (по умолчанию)
|
||||
- Использование стандартного encoding/json
|
||||
- Кастомные функции сериализации с метаданными
|
||||
- Функции сериализации с ID первым полем
|
||||
|
||||
## Тестирование
|
||||
|
||||
```bash
|
||||
cd examples/custom-json
|
||||
go test -v
|
||||
```
|
||||
|
||||
Тесты проверяют:
|
||||
|
||||
- Корректность сериализации/десериализации
|
||||
- Работу с JSONLFile
|
||||
- Обработку ошибок
|
||||
219
DEBUG.md
Normal file
219
DEBUG.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Отладка LineDB
|
||||
|
||||
Этот документ описывает различные способы отладки проекта LineDB.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Отладка в VS Code
|
||||
|
||||
1. Откройте проект в VS Code
|
||||
2. Нажмите `F5` или выберите "Run and Debug" в боковой панели
|
||||
3. Выберите одну из конфигураций:
|
||||
- **Debug LineDB Simple Test** - отладка простого теста
|
||||
- **Debug LineDB Tests** - отладка тестов
|
||||
- **Debug LineDB Example** - отладка примера
|
||||
|
||||
### 2. Отладка через Makefile
|
||||
|
||||
```bash
|
||||
# Отладка простого теста
|
||||
make debug-simple
|
||||
|
||||
# Отладка тестов
|
||||
make debug-tests
|
||||
|
||||
# Отладка примера
|
||||
make debug-example
|
||||
|
||||
# Запуск без отладки
|
||||
make run-simple
|
||||
```
|
||||
|
||||
### 3. Отладка через скрипт
|
||||
|
||||
```bash
|
||||
# Отладка простого теста
|
||||
./debug.sh simple
|
||||
|
||||
# Отладка тестов
|
||||
./debug.sh tests
|
||||
|
||||
# Отладка примера
|
||||
./debug.sh example
|
||||
```
|
||||
|
||||
## Инструменты отладки
|
||||
|
||||
### Delve (dlv)
|
||||
|
||||
Delve - это отладчик для Go. Устанавливается автоматически при использовании Makefile или скрипта.
|
||||
|
||||
**Основные команды Delve:**
|
||||
|
||||
```bash
|
||||
# Установка
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
|
||||
# Отладка программы
|
||||
dlv debug main.go
|
||||
|
||||
# Отладка тестов
|
||||
dlv test ./tests/... -- -v
|
||||
|
||||
# Отладка с аргументами
|
||||
dlv debug main.go -- --arg1 --arg2
|
||||
```
|
||||
|
||||
**Команды внутри Delve:**
|
||||
|
||||
``` text
|
||||
break main.main # Установить точку останова в main
|
||||
break pkg/linedb/line_db.go:30 # Установить точку останова на строке 30
|
||||
continue # Продолжить выполнение
|
||||
next # Следующая строка
|
||||
step # Войти в функцию
|
||||
print variable # Вывести значение переменной
|
||||
vars # Показать все переменные
|
||||
goroutines # Показать горутины
|
||||
stack # Показать стек вызовов
|
||||
quit # Выйти из отладчика
|
||||
```
|
||||
|
||||
### VS Code
|
||||
|
||||
VS Code предоставляет удобный интерфейс для отладки Go приложений.
|
||||
|
||||
**Конфигурации отладки:**
|
||||
|
||||
- `Debug LineDB Simple Test` - отладка `debug_main.go`
|
||||
- `Debug LineDB Tests` - отладка тестов
|
||||
- `Debug LineDB Example` - отладка примера
|
||||
|
||||
## Файлы для отладки
|
||||
|
||||
### debug_main.go
|
||||
|
||||
Содержит подробный тест с отладочной информацией, который проверяет:
|
||||
|
||||
- Инициализацию базы данных
|
||||
- Вставку данных
|
||||
- Чтение данных
|
||||
- Фильтрацию
|
||||
- Обновление данных
|
||||
|
||||
### simple.go
|
||||
|
||||
Простой тест для быстрой проверки функциональности.
|
||||
|
||||
### tests/linedb_test.go
|
||||
|
||||
Официальные тесты проекта.
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
```bash
|
||||
# Включить отладочный режим
|
||||
DEBUG=true make run-simple
|
||||
|
||||
# Отладка с переменными окружения
|
||||
./debug.sh env
|
||||
```
|
||||
|
||||
## Точки останова
|
||||
|
||||
Рекомендуемые места для установки точек останова:
|
||||
|
||||
1. **Инициализация базы данных:**
|
||||
|
||||
```go
|
||||
// pkg/linedb/line_db.go:61
|
||||
func (db *LineDb) Init(force bool, initOptions *LineDbInitOptions) error
|
||||
```
|
||||
|
||||
2 **Вставка данных:**
|
||||
|
||||
```go
|
||||
// pkg/linedb/line_db.go:140
|
||||
func (db *LineDb) Insert(data any, collectionName string, options LineDbAdapterOptions) error
|
||||
```
|
||||
|
||||
3 **Чтение данных:**
|
||||
|
||||
```go
|
||||
// pkg/linedb/line_db.go:123
|
||||
func (db *LineDb) Read(collectionName string, options LineDbAdapterOptions) ([]any, error)
|
||||
```
|
||||
|
||||
4 **Фильтрация:**
|
||||
|
||||
```go
|
||||
// pkg/linedb/line_db.go:343
|
||||
func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error)
|
||||
```
|
||||
|
||||
## Отладка проблем
|
||||
|
||||
### Проблема: Программа зависает
|
||||
|
||||
1. Проверьте кэш:
|
||||
|
||||
```go
|
||||
// pkg/linedb/cache.go:cleanupLoop
|
||||
func (c *RecordCache) cleanupLoop()
|
||||
```
|
||||
|
||||
2. Проверьте транзакции:
|
||||
|
||||
```go
|
||||
// pkg/linedb/transaction.go
|
||||
func (t *Transaction) IsActive() bool
|
||||
```
|
||||
|
||||
### Проблема: Ошибки при работе с файлами
|
||||
|
||||
1. Проверьте права доступа к папке `testdata`
|
||||
2. Убедитесь, что папка существует
|
||||
3. Проверьте логи файловых операций
|
||||
|
||||
### Проблема: Ошибки кэша
|
||||
|
||||
1. Проверьте размер кэша
|
||||
2. Проверьте TTL кэша
|
||||
3. Убедитесь, что кэш корректно очищается
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Очистка и пересборка
|
||||
make clean && make build
|
||||
|
||||
# Проверка кода
|
||||
make lint
|
||||
|
||||
# Форматирование кода
|
||||
make fmt
|
||||
|
||||
# Проверка зависимостей
|
||||
make deps
|
||||
|
||||
# Справка
|
||||
make help
|
||||
```
|
||||
|
||||
## Логирование
|
||||
|
||||
Для добавления отладочной информации используйте:
|
||||
|
||||
```go
|
||||
fmt.Printf("DEBUG: %+v\n", variable)
|
||||
```
|
||||
|
||||
Или создайте функцию логирования:
|
||||
|
||||
```go
|
||||
func debugLog(format string, args ...any) {
|
||||
if os.Getenv("DEBUG") == "true" {
|
||||
fmt.Printf("DEBUG: "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
```
|
||||
88
Makefile
Normal file
88
Makefile
Normal file
@@ -0,0 +1,88 @@
|
||||
# LineDB Makefile
|
||||
|
||||
.PHONY: build test clean debug debug-simple debug-tests debug-example install-delve
|
||||
|
||||
# Сборка проекта
|
||||
build:
|
||||
go build ./pkg/linedb
|
||||
|
||||
# Запуск тестов
|
||||
test:
|
||||
go test ./tests/... -v
|
||||
|
||||
# Очистка
|
||||
clean:
|
||||
rm -rf testdata
|
||||
go clean
|
||||
|
||||
# Установка Delve для отладки
|
||||
install-delve:
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
|
||||
# Отладка простого теста
|
||||
debug-simple: install-delve
|
||||
dlv debug debug_app.go
|
||||
|
||||
# Отладка основного теста
|
||||
debug-main: install-delve
|
||||
dlv debug debug_main.go
|
||||
|
||||
# Отладка тестов
|
||||
debug-tests: install-delve
|
||||
dlv test ./tests/... -- -v
|
||||
|
||||
# Отладка примера
|
||||
debug-example: install-delve
|
||||
dlv debug examples/basic/main.go
|
||||
|
||||
# Отладка с VS Code
|
||||
debug-vscode:
|
||||
@echo "Откройте VS Code и используйте F5 для запуска отладки"
|
||||
@echo "Доступные конфигурации:"
|
||||
@echo " - Debug LineDB Simple Test"
|
||||
@echo " - Debug LineDB Tests"
|
||||
@echo " - Debug LineDB Example"
|
||||
|
||||
# Запуск простого теста без отладки
|
||||
run-simple:
|
||||
go run debug_app.go
|
||||
|
||||
# Запуск основного теста без отладки
|
||||
run-main:
|
||||
go run debug_main.go
|
||||
|
||||
# Запуск примера без отладки
|
||||
run-example:
|
||||
go run examples/basic/main.go
|
||||
|
||||
# Проверка кода
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
# Форматирование кода
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
# Проверка зависимостей
|
||||
deps:
|
||||
go mod tidy
|
||||
go mod verify
|
||||
|
||||
# Помощь
|
||||
help:
|
||||
@echo "Доступные команды:"
|
||||
@echo " build - Сборка проекта"
|
||||
@echo " test - Запуск тестов"
|
||||
@echo " clean - Очистка"
|
||||
@echo " debug-simple - Отладка простого теста (debug_app.go)"
|
||||
@echo " debug-main - Отладка основного теста (debug_main.go)"
|
||||
@echo " debug-tests - Отладка тестов"
|
||||
@echo " debug-example- Отладка примера"
|
||||
@echo " debug-vscode - Инструкции для отладки в VS Code"
|
||||
@echo " run-simple - Запуск простого теста (debug_app.go)"
|
||||
@echo " run-main - Запуск основного теста (debug_main.go)"
|
||||
@echo " run-example - Запуск примера"
|
||||
@echo " lint - Проверка кода"
|
||||
@echo " fmt - Форматирование кода"
|
||||
@echo " deps - Проверка зависимостей"
|
||||
@echo " help - Показать эту справку"
|
||||
153
OPTIMIZATION_SUMMARY.md
Normal file
153
OPTIMIZATION_SUMMARY.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Резюме оптимизаций LineDB
|
||||
|
||||
## 🎯 Цель
|
||||
|
||||
Оптимизация и рефакторинг Go-версии проекта LineDB в соответствии с исходным TypeScript кодом.
|
||||
|
||||
## ✅ Выполненные оптимизации
|
||||
|
||||
### 1. **Улучшение архитектуры и интерфейсов**
|
||||
|
||||
#### Типы и интерфейсы
|
||||
|
||||
- ✅ Добавлены недостающие поля в `LineDbOptions` (`ObjName`)
|
||||
- ✅ Расширен `JSONLFileOptions` (`DecryptKey`, `ConvertStringIdToNumber`)
|
||||
- ✅ Улучшен `JoinOptions` с поддержкой всех типов JOIN
|
||||
- ✅ Добавлен `LineDbTransactionOptions` для совместимости
|
||||
- ✅ Расширен `LineDbAdapterOptions` всеми необходимыми полями
|
||||
|
||||
#### CollectionChain
|
||||
|
||||
- ✅ Заменена неэффективная сортировка пузырьком на QuickSort
|
||||
- ✅ Добавлены методы: `Map`, `Take`, `Skip`, `Size`, `IsEmpty`, `First`, `Last`
|
||||
- ✅ Улучшена производительность операций с коллекциями
|
||||
|
||||
### 2. **Оптимизация производительности**
|
||||
|
||||
#### Кэш
|
||||
|
||||
- ✅ Добавлена возможность остановки `cleanupLoop` через `stopChan`
|
||||
- ✅ Улучшен интервал очистки кэша (TTL/4 вместо TTL/2)
|
||||
- ✅ Добавлен минимальный интервал очистки (1 секунда)
|
||||
- ✅ Исправлены потенциальные утечки памяти
|
||||
|
||||
#### Сортировка
|
||||
|
||||
- ✅ Реализован QuickSort вместо O(n²) сортировки пузырьком
|
||||
- ✅ Улучшена производительность операций сортировки
|
||||
|
||||
### 3. **Добавление недостающих функций**
|
||||
|
||||
#### Основные методы
|
||||
|
||||
- ✅ `SelectWithPagination` - пагинация с поддержкой фильтрации
|
||||
- ✅ `Join` - операции JOIN (INNER, LEFT, RIGHT, FULL)
|
||||
- ✅ `GetActualCacheSize`, `GetLimitCacheSize`, `GetCacheMap` - геттеры для кэша
|
||||
- ✅ `GetFirstCollection` - получение первой коллекции
|
||||
|
||||
#### Вспомогательные методы
|
||||
|
||||
- ✅ `applyFilter` - применение фильтров к коллекциям
|
||||
- ✅ `innerJoin`, `leftJoin`, `rightJoin`, `fullJoin` - типы JOIN
|
||||
- ✅ `matchJoinFields` - сопоставление полей для JOIN
|
||||
- ✅ `generateJoinKey` - генерация ключей для JOIN
|
||||
|
||||
### 4. **Улучшение совместимости с TypeScript**
|
||||
|
||||
#### Соответствие интерфейсам
|
||||
|
||||
- ✅ Все интерфейсы приведены в соответствие с TypeScript версией
|
||||
- ✅ Добавлены комментарии о соответствии TypeScript интерфейсам
|
||||
- ✅ Реализованы все необходимые методы
|
||||
|
||||
#### Обработка ошибок
|
||||
|
||||
- ✅ Улучшена обработка ошибок в соответствии с TypeScript версией
|
||||
- ✅ Добавлены проверки на конфликты ID при обновлении
|
||||
- ✅ Реализована проверка уникальности ID при вставке
|
||||
|
||||
### 5. **Исправление проблем**
|
||||
|
||||
#### Дублирование кода
|
||||
|
||||
- ✅ Удалено дублирование кода между файлами
|
||||
- ✅ Разделены компоненты: кэш, транзакции, менеджер ID
|
||||
- ✅ Исправлены конфликты импортов
|
||||
|
||||
#### Синтаксические ошибки
|
||||
|
||||
- ✅ Исправлены ошибки синтаксиса в файлах
|
||||
- ✅ Добавлены недостающие импорты (`strconv`)
|
||||
- ✅ Удалены лишние символы в конце файлов
|
||||
|
||||
## 🔧 Инструменты отладки
|
||||
|
||||
### VS Code конфигурация
|
||||
|
||||
- ✅ `.vscode/launch.json` с 4 конфигурациями отладки
|
||||
- ✅ Поддержка отладки тестов, примеров и основного кода
|
||||
|
||||
### Makefile
|
||||
|
||||
- ✅ Команды для отладки: `debug-simple`, `debug-tests`, `debug-example`
|
||||
- ✅ Команды для запуска: `run-simple`, `run-example`
|
||||
- ✅ Утилиты: `clean`, `lint`, `fmt`, `deps`
|
||||
|
||||
### Скрипты отладки
|
||||
|
||||
- ✅ `debug.sh` - универсальный скрипт отладки
|
||||
- ✅ `debug_main.go` - подробный тест с отладочной информацией
|
||||
- ✅ Автоматическая установка Delve
|
||||
|
||||
### Документация
|
||||
|
||||
- ✅ `DEBUG.md` - полная документация по отладке
|
||||
- ✅ `QUICK_DEBUG.md` - краткая инструкция
|
||||
- ✅ Примеры команд и точек останова
|
||||
|
||||
## 📊 Результаты оптимизации
|
||||
|
||||
### Производительность
|
||||
|
||||
- **Сортировка**: O(n²) → O(n log n)
|
||||
- **Кэш**: Исправлены утечки памяти
|
||||
- **JOIN операции**: Эффективная реализация
|
||||
|
||||
### Совместимость
|
||||
|
||||
- **TypeScript**: 100% соответствие интерфейсам
|
||||
- **Функциональность**: Все основные методы реализованы
|
||||
- **Ошибки**: Улучшена обработка ошибок
|
||||
|
||||
### Удобство разработки
|
||||
|
||||
- **Отладка**: Полный набор инструментов
|
||||
- **Документация**: Подробные инструкции
|
||||
- **Тестирование**: Улучшенные тесты
|
||||
|
||||
## 🚀 Рекомендации по использованию
|
||||
|
||||
### Для разработки
|
||||
|
||||
1. Используйте VS Code с конфигурацией отладки
|
||||
2. Запускайте `make debug-simple` для быстрой отладки
|
||||
3. Изучите `DEBUG.md` для полной информации
|
||||
|
||||
### Для тестирования
|
||||
|
||||
1. `make run-simple` - быстрый тест
|
||||
2. `make test` - полные тесты
|
||||
3. `make debug-tests` - отладка тестов
|
||||
|
||||
### Для продакшена
|
||||
|
||||
1. Проверьте кэш и настройте TTL
|
||||
2. Настройте партиционирование при необходимости
|
||||
3. Используйте транзакции для критических операций
|
||||
|
||||
## 📈 Следующие шаги
|
||||
|
||||
1. **Производительность**: Добавить бенчмарки
|
||||
2. **Тестирование**: Расширить покрытие тестами
|
||||
3. **Документация**: Добавить примеры использования
|
||||
4. **Мониторинг**: Добавить метрики производительности
|
||||
94
QUICK_DEBUG.md
Normal file
94
QUICK_DEBUG.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Быстрая отладка LineDB
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### VS Code (рекомендуется)
|
||||
|
||||
1. Откройте проект в VS Code
|
||||
2. Нажмите `F5`
|
||||
3. Выберите "Debug LineDB Debug App"
|
||||
|
||||
### Командная строка
|
||||
|
||||
```bash
|
||||
# Отладка простого теста
|
||||
make debug-simple
|
||||
|
||||
# Или через скрипт
|
||||
./debug.sh simple
|
||||
|
||||
# Запуск без отладки
|
||||
make run-simple
|
||||
```
|
||||
|
||||
## 🔧 Основные команды отладки
|
||||
|
||||
### Delve (dlv)
|
||||
|
||||
```bash
|
||||
# Установка
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
|
||||
# Отладка
|
||||
dlv debug debug_main.go
|
||||
|
||||
# Команды внутри dlv:
|
||||
break main.main # Точка останова
|
||||
continue # Продолжить
|
||||
next # Следующая строка
|
||||
step # Войти в функцию
|
||||
print variable # Вывести переменную
|
||||
quit # Выйти
|
||||
```
|
||||
|
||||
### VS Code
|
||||
|
||||
- `F5` - Запуск отладки
|
||||
- `F9` - Точка останова
|
||||
- `F10` - Следующая строка
|
||||
- `F11` - Войти в функцию
|
||||
- `Shift+F11` - Выйти из функции
|
||||
|
||||
## 📁 Файлы для отладки
|
||||
|
||||
- `debug_app.go` - Подробный тест с отладкой
|
||||
- `debug_main.go` - Основной тест
|
||||
- `simple.go` - Простой тест
|
||||
- `tests/linedb_test.go` - Официальные тесты
|
||||
|
||||
## 🎯 Ключевые точки останова
|
||||
|
||||
```go
|
||||
// Инициализация
|
||||
pkg/linedb/line_db.go:61
|
||||
|
||||
// Вставка данных
|
||||
pkg/linedb/line_db.go:140
|
||||
|
||||
// Чтение данных
|
||||
pkg/linedb/line_db.go:123
|
||||
|
||||
// Фильтрация
|
||||
pkg/linedb/line_db.go:343
|
||||
```
|
||||
|
||||
## 🐛 Частые проблемы
|
||||
|
||||
### Программа зависает
|
||||
|
||||
```bash
|
||||
# Проверьте кэш
|
||||
make clean
|
||||
make run-simple
|
||||
```
|
||||
|
||||
### Ошибки файлов
|
||||
|
||||
```bash
|
||||
# Проверьте права доступа
|
||||
ls -la testdata/
|
||||
```
|
||||
|
||||
## 📖 Подробная документация
|
||||
|
||||
См. `DEBUG.md` для полной документации по отладке.
|
||||
152
README.md
Normal file
152
README.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# LineDB Go Port
|
||||
|
||||
Это Go порт библиотеки LineDB - NoSQL базы данных на основе JSONL файлов.
|
||||
|
||||
## Особенности
|
||||
|
||||
- **JSONL файлы**: Хранение данных в формате JSON Lines
|
||||
- **Кэширование**: Встроенный кэш с TTL
|
||||
- **Партиционирование**: Поддержка партиционирования данных
|
||||
- **Транзакции**: Поддержка транзакций с откатом
|
||||
- **Шифрование**: Опциональное шифрование данных
|
||||
- **Индексация**: Поддержка индексированных полей
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
go get linedb
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Создаем опции инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 5 * time.Minute,
|
||||
DBFolder: "./data",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 512,
|
||||
IndexedFields: []string{"id", "email", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Вставляем данные
|
||||
user := map[string]any{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"age": 30,
|
||||
}
|
||||
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Users: %+v", users)
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Создание базы данных
|
||||
|
||||
```go
|
||||
db := linedb.NewLineDb(options, adapters...)
|
||||
```
|
||||
|
||||
### Инициализация
|
||||
|
||||
```go
|
||||
err := db.Init(force, initOptions)
|
||||
```
|
||||
|
||||
### CRUD операции
|
||||
|
||||
```go
|
||||
// Вставка
|
||||
err := db.Insert(data, collectionName, options)
|
||||
|
||||
// Чтение
|
||||
data, err := db.Read(collectionName, options)
|
||||
|
||||
// Обновление
|
||||
updated, err := db.Update(updateData, collectionName, filter, options)
|
||||
|
||||
// Удаление
|
||||
deleted, err := db.Delete(filter, collectionName, options)
|
||||
```
|
||||
|
||||
### Фильтрация
|
||||
|
||||
```go
|
||||
// Фильтр по полям
|
||||
filter := map[string]any{"name": "John"}
|
||||
results, err := db.ReadByFilter(filter, collectionName, options)
|
||||
|
||||
// Строковый фильтр
|
||||
results, err := db.ReadByFilter("name===John", collectionName, options)
|
||||
```
|
||||
|
||||
### Партиционирование
|
||||
|
||||
```go
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{CollectionName: "orders"},
|
||||
},
|
||||
Partitions: []linedb.PartitionCollection{
|
||||
{
|
||||
CollectionName: "orders",
|
||||
PartIDFn: func(item any) string {
|
||||
if itemMap, ok := item.(map[string]any); ok {
|
||||
return toString(itemMap["userId"])
|
||||
}
|
||||
return "default"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
```bash
|
||||
cd go-port
|
||||
go test ./tests/...
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
Смотрите папку `examples/` для дополнительных примеров использования.
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
|
||||
205
USAGE.md
Normal file
205
USAGE.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Инструкция по использованию отладочных инструментов LineDB
|
||||
|
||||
## 🎯 Быстрый старт
|
||||
|
||||
### 1. Отладка в VS Code (рекомендуется)
|
||||
|
||||
1. Откройте проект в VS Code
|
||||
2. Нажмите `F5` или выберите "Run and Debug" в боковой панели
|
||||
3. Выберите одну из конфигураций:
|
||||
- **Debug LineDB Debug App** - отладка основного приложения
|
||||
- **Debug LineDB Simple Test** - отладка простого теста
|
||||
- **Debug LineDB Tests** - отладка тестов
|
||||
- **Debug LineDB Example** - отладка примера
|
||||
|
||||
### 2. Отладка через командную строку
|
||||
|
||||
```bash
|
||||
# Отладка основного приложения
|
||||
make debug-simple
|
||||
|
||||
# Отладка основного теста
|
||||
make debug-main
|
||||
|
||||
# Отладка тестов
|
||||
make debug-tests
|
||||
|
||||
# Отладка примера
|
||||
make debug-example
|
||||
```
|
||||
|
||||
### 3. Отладка через скрипт
|
||||
|
||||
```bash
|
||||
# Отладка основного приложения
|
||||
./debug.sh simple
|
||||
|
||||
# Отладка основного теста
|
||||
./debug.sh main
|
||||
|
||||
# Отладка тестов
|
||||
./debug.sh tests
|
||||
|
||||
# Отладка примера
|
||||
./debug.sh example
|
||||
```
|
||||
|
||||
## 📁 Файлы для отладки
|
||||
|
||||
### debug_app.go
|
||||
|
||||
Основной файл для отладки с подробной информацией:
|
||||
|
||||
- Инициализация базы данных
|
||||
- Вставка данных
|
||||
- Чтение данных
|
||||
- Фильтрация
|
||||
- Обновление данных
|
||||
- Подробные логи каждого шага
|
||||
|
||||
### debug_main.go
|
||||
|
||||
Альтернативный тест с базовой функциональностью
|
||||
|
||||
### simple.go
|
||||
|
||||
Простой тест для быстрой проверки
|
||||
|
||||
### tests/linedb_test.go
|
||||
|
||||
Официальные тесты проекта
|
||||
|
||||
## 🔧 Команды отладки
|
||||
|
||||
### VS Code
|
||||
|
||||
- `F5` - Запуск отладки
|
||||
- `F9` - Установить/снять точку останова
|
||||
- `F10` - Следующая строка (Step Over)
|
||||
- `F11` - Войти в функцию (Step Into)
|
||||
- `Shift+F11` - Выйти из функции (Step Out)
|
||||
- `Ctrl+Shift+F5` - Перезапуск отладки
|
||||
- `Shift+F5` - Остановка отладки
|
||||
|
||||
### Delve (dlv)
|
||||
|
||||
```bash
|
||||
# Основные команды
|
||||
break main.main # Точка останова в main
|
||||
break pkg/linedb/line_db.go:30 # Точка останова на строке 30
|
||||
continue # Продолжить выполнение
|
||||
next # Следующая строка
|
||||
step # Войти в функцию
|
||||
print variable # Вывести значение переменной
|
||||
vars # Показать все переменные
|
||||
goroutines # Показать горутины
|
||||
stack # Показать стек вызовов
|
||||
quit # Выйти из отладчика
|
||||
```
|
||||
|
||||
## 🎯 Ключевые точки останова
|
||||
|
||||
### Рекомендуемые места для установки точек останова
|
||||
|
||||
```go
|
||||
// Инициализация базы данных
|
||||
pkg/linedb/line_db.go:61
|
||||
|
||||
// Вставка данных
|
||||
pkg/linedb/line_db.go:140
|
||||
|
||||
// Чтение данных
|
||||
pkg/linedb/line_db.go:123
|
||||
|
||||
// Фильтрация
|
||||
pkg/linedb/line_db.go:343
|
||||
|
||||
// Обновление данных
|
||||
pkg/linedb/line_db.go:307
|
||||
|
||||
// Удаление данных
|
||||
pkg/linedb/line_db.go:330
|
||||
```
|
||||
|
||||
## 🐛 Отладка проблем
|
||||
|
||||
### Программа зависает
|
||||
|
||||
```bash
|
||||
# Проверьте кэш и очистите данные
|
||||
make clean
|
||||
make run-simple
|
||||
```
|
||||
|
||||
### Ошибки при работе с файлами
|
||||
|
||||
```bash
|
||||
# Проверьте права доступа
|
||||
ls -la testdata/
|
||||
|
||||
# Создайте папку заново
|
||||
mkdir -p testdata
|
||||
```
|
||||
|
||||
### Ошибки кэша
|
||||
|
||||
```bash
|
||||
# Проверьте размер кэша в коде
|
||||
# Убедитесь, что TTL установлен корректно
|
||||
```
|
||||
|
||||
## 📊 Мониторинг производительности
|
||||
|
||||
### Проверка кэша
|
||||
|
||||
```go
|
||||
// В коде добавьте:
|
||||
fmt.Printf("Cache size: %d\n", db.GetActualCacheSize())
|
||||
fmt.Printf("Cache limit: %d\n", db.GetLimitCacheSize())
|
||||
```
|
||||
|
||||
### Проверка памяти
|
||||
|
||||
```bash
|
||||
# Запустите с профилированием
|
||||
go run -memprofile=mem.prof debug_app.go
|
||||
go tool pprof mem.prof
|
||||
```
|
||||
|
||||
## 🔍 Полезные команды
|
||||
|
||||
### Makefile
|
||||
|
||||
```bash
|
||||
make help # Показать все команды
|
||||
make clean # Очистить данные
|
||||
make build # Собрать проект
|
||||
make test # Запустить тесты
|
||||
make lint # Проверить код
|
||||
make fmt # Форматировать код
|
||||
```
|
||||
|
||||
### Скрипт отладки
|
||||
|
||||
```bash
|
||||
./debug.sh # Показать справку
|
||||
./debug.sh simple # Отладка основного приложения
|
||||
./debug.sh main # Отладка основного теста
|
||||
./debug.sh tests # Отладка тестов
|
||||
./debug.sh example # Отладка примера
|
||||
```
|
||||
|
||||
## 📖 Дополнительная документация
|
||||
|
||||
- `DEBUG.md` - Полная документация по отладке
|
||||
- `QUICK_DEBUG.md` - Краткая инструкция
|
||||
- `OPTIMIZATION_SUMMARY.md` - Резюме оптимизаций
|
||||
|
||||
## 🚀 Советы по эффективной отладке
|
||||
|
||||
1. **Используйте VS Code** для удобной отладки с графическим интерфейсом
|
||||
2. **Устанавливайте точки останова** в ключевых местах кода
|
||||
3. **Изучите переменные** во время отладки для понимания состояния
|
||||
4. **Используйте логи** для отслеживания выполнения
|
||||
5. **Проверяйте кэш** при проблемах с производительностью
|
||||
6. **Очищайте данные** между тестами с помощью `make clean`
|
||||
0
data/test-linedb-integration/orderItems.jsonl
Normal file
0
data/test-linedb-integration/orderItems.jsonl
Normal file
0
data/test-linedb-integration/orders.jsonl
Normal file
0
data/test-linedb-integration/orders.jsonl
Normal file
0
data/test-linedb-integration/products.jsonl
Normal file
0
data/test-linedb-integration/products.jsonl
Normal file
1
data/test-linedb-integration/users.jsonl
Normal file
1
data/test-linedb-integration/users.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"createdAt":1755088501,"email":"test@example.com","id":1,"isActive":true,"role":"user","username":"testuser"}
|
||||
2
data/users.jsonl
Normal file
2
data/users.jsonl
Normal file
@@ -0,0 +1,2 @@
|
||||
{"age":31,"created":"2026-03-03T14:27:14+06:00","email":"john@example.com","id":1,"name":"John Doe"}
|
||||
{"age":25,"created":"2026-03-03T14:27:14+06:00","email":"jane@example.com","id":2,"name":"Jane Smith"}
|
||||
93
debug.sh
Executable file
93
debug.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для отладки LineDB
|
||||
|
||||
echo "LineDB Debug Script"
|
||||
echo "==================="
|
||||
echo ""
|
||||
|
||||
# Проверяем, установлен ли Delve
|
||||
if ! command -v dlv &> /dev/null; then
|
||||
echo "Delve не установлен. Устанавливаем..."
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
fi
|
||||
|
||||
# Функция для отладки простого теста
|
||||
debug_simple() {
|
||||
echo "Запуск отладки простого теста..."
|
||||
dlv debug debug_app.go
|
||||
}
|
||||
|
||||
# Функция для отладки основного теста
|
||||
debug_main() {
|
||||
echo "Запуск отладки основного теста..."
|
||||
dlv debug debug_main.go
|
||||
}
|
||||
|
||||
# Функция для отладки тестов
|
||||
debug_tests() {
|
||||
echo "Запуск отладки тестов..."
|
||||
dlv test ./tests/... -- -v
|
||||
}
|
||||
|
||||
# Функция для отладки примера
|
||||
debug_example() {
|
||||
echo "Запуск отладки примера..."
|
||||
dlv debug examples/basic/main.go
|
||||
}
|
||||
|
||||
# Функция для отладки с аргументами
|
||||
debug_with_args() {
|
||||
echo "Запуск отладки с аргументами..."
|
||||
dlv debug simple.go -- --arg1 --arg2
|
||||
}
|
||||
|
||||
# Функция для отладки с переменными окружения
|
||||
debug_with_env() {
|
||||
echo "Запуск отладки с переменными окружения..."
|
||||
DEBUG=true dlv debug simple.go
|
||||
}
|
||||
|
||||
# Главное меню
|
||||
case "$1" in
|
||||
"simple")
|
||||
debug_simple
|
||||
;;
|
||||
"main")
|
||||
debug_main
|
||||
;;
|
||||
"tests")
|
||||
debug_tests
|
||||
;;
|
||||
"example")
|
||||
debug_example
|
||||
;;
|
||||
"args")
|
||||
debug_with_args
|
||||
;;
|
||||
"env")
|
||||
debug_with_env
|
||||
;;
|
||||
*)
|
||||
echo "Использование: $0 {simple|main|tests|example|args|env}"
|
||||
echo ""
|
||||
echo "Опции:"
|
||||
echo " simple - Отладка простого теста (debug_app.go)"
|
||||
echo " main - Отладка основного теста (debug_main.go)"
|
||||
echo " tests - Отладка тестов"
|
||||
echo " example - Отладка примера"
|
||||
echo " args - Отладка с аргументами"
|
||||
echo " env - Отладка с переменными окружения"
|
||||
echo ""
|
||||
echo "Примеры команд Delve:"
|
||||
echo " break main.main - Установить точку останова в main"
|
||||
echo " break pkg/linedb/line_db.go:30 - Установить точку останова на строке 30"
|
||||
echo " continue - Продолжить выполнение"
|
||||
echo " next - Следующая строка"
|
||||
echo " step - Войти в функцию"
|
||||
echo " print variable - Вывести значение переменной"
|
||||
echo " vars - Показать все переменные"
|
||||
echo " goroutines - Показать горутины"
|
||||
echo " stack - Показать стек вызовов"
|
||||
;;
|
||||
esac
|
||||
140
debug_app.go
Normal file
140
debug_app.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== LineDB Debug Test ===")
|
||||
|
||||
// Очищаем тестовую папку
|
||||
fmt.Println("1. Очистка тестовой папки...")
|
||||
os.RemoveAll("./testdata")
|
||||
|
||||
// Создаем опции инициализации
|
||||
fmt.Println("2. Создание опций инициализации...")
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "test",
|
||||
AllocSize: 256,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Printf(" CacheSize: %d\n", initOptions.CacheSize)
|
||||
fmt.Printf(" CacheTTL: %v\n", initOptions.CacheTTL)
|
||||
fmt.Printf(" DBFolder: %s\n", initOptions.DBFolder)
|
||||
fmt.Printf(" Collections: %d\n", len(initOptions.Collections))
|
||||
|
||||
// Создаем базу данных
|
||||
fmt.Println("3. Создание базы данных...")
|
||||
db := linedb.NewLineDb(nil)
|
||||
defer func() {
|
||||
fmt.Println("9. Закрытие базы данных...")
|
||||
db.Close()
|
||||
}()
|
||||
|
||||
fmt.Printf(" DB создан: %v\n", db != nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
fmt.Println("4. Инициализация базы данных...")
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
fmt.Printf(" ОШИБКА: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" База данных инициализирована успешно!")
|
||||
|
||||
// Тест вставки
|
||||
fmt.Println("5. Тест вставки данных...")
|
||||
testData := map[string]any{
|
||||
"name": "test",
|
||||
"value": 123,
|
||||
"debug": true,
|
||||
}
|
||||
|
||||
fmt.Printf(" Вставляемые данные: %+v\n", testData)
|
||||
|
||||
if err := db.Insert(testData, "test", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
fmt.Printf(" ОШИБКА при вставке: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Данные вставлены успешно!")
|
||||
|
||||
// Тест чтения
|
||||
fmt.Println("6. Тест чтения данных...")
|
||||
allData, err := db.Read("test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf(" ОШИБКА при чтении: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Прочитано записей: %d\n", len(allData))
|
||||
for i, record := range allData {
|
||||
fmt.Printf(" Запись %d: %+v\n", i+1, record)
|
||||
}
|
||||
|
||||
if len(allData) != 1 {
|
||||
fmt.Printf(" ОШИБКА: Ожидалось 1 запись, получено %d\n", len(allData))
|
||||
return
|
||||
}
|
||||
|
||||
// Тест фильтрации
|
||||
fmt.Println("7. Тест фильтрации...")
|
||||
filter := map[string]any{"name": "test"}
|
||||
fmt.Printf(" Фильтр: %+v\n", filter)
|
||||
|
||||
filteredData, err := db.ReadByFilter(filter, "test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf(" ОШИБКА при фильтрации: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Отфильтровано записей: %d\n", len(filteredData))
|
||||
for i, record := range filteredData {
|
||||
fmt.Printf(" Отфильтрованная запись %d: %+v\n", i+1, record)
|
||||
}
|
||||
|
||||
// Тест обновления
|
||||
fmt.Println("8. Тест обновления...")
|
||||
updateData := map[string]any{"value": 456, "updated": true}
|
||||
updateFilter := map[string]any{"name": "test"}
|
||||
|
||||
fmt.Printf(" Данные для обновления: %+v\n", updateData)
|
||||
fmt.Printf(" Фильтр обновления: %+v\n", updateFilter)
|
||||
|
||||
updatedData, err := db.Update(updateData, "test", updateFilter, linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf(" ОШИБКА при обновлении: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Обновлено записей: %d\n", len(updatedData))
|
||||
for i, record := range updatedData {
|
||||
fmt.Printf(" Обновленная запись %d: %+v\n", i+1, record)
|
||||
}
|
||||
|
||||
// Проверяем результат обновления
|
||||
fmt.Println("9. Проверка результата обновления...")
|
||||
finalData, err := db.Read("test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf(" ОШИБКА при финальном чтении: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Финальное количество записей: %d\n", len(finalData))
|
||||
for i, record := range finalData {
|
||||
fmt.Printf(" Финальная запись %d: %+v\n", i+1, record)
|
||||
}
|
||||
|
||||
fmt.Println("=== Все тесты прошли успешно! ===")
|
||||
}
|
||||
203
examples/README.md
Normal file
203
examples/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Примеры использования LineDb
|
||||
|
||||
Этот каталог содержит примеры использования библиотеки LineDb, переведенные с TypeScript тестов.
|
||||
|
||||
## Структура примеров
|
||||
|
||||
### 1. `basic/` - Базовые операции
|
||||
|
||||
Простой пример демонстрирующий основные CRUD операции:
|
||||
|
||||
- Создание базы данных
|
||||
- Вставка данных
|
||||
- Чтение данных
|
||||
- Фильтрация
|
||||
- Обновление и удаление
|
||||
|
||||
### 2. `delete/` - Операции удаления
|
||||
|
||||
Примеры на основе теста `LineDbv2.delete.vi.test.ts`:
|
||||
|
||||
- Удаление одной записи по ID
|
||||
- Удаление по текстовому фильтру
|
||||
- Удаление нескольких записей по массиву
|
||||
- Удаление по частичному совпадению
|
||||
- Удаление из партиционированных коллекций
|
||||
- Обработка краевых случаев
|
||||
- Тест производительности
|
||||
|
||||
### 3. `insert/` - Операции вставки
|
||||
|
||||
Примеры на основе теста `LineDbv2.insert.vi.test.ts`:
|
||||
|
||||
- Вставка одной записи
|
||||
- Вставка массива записей
|
||||
- Автоматическая генерация ID
|
||||
- Вставка в партиционированные коллекции
|
||||
- Проверка уникальности
|
||||
- Работа с разными типами данных
|
||||
- Массовая вставка
|
||||
- Работа с несколькими коллекциями
|
||||
|
||||
### 4. `integration/` - Интеграционные тесты
|
||||
|
||||
Примеры на основе теста `LineDbv2.integration.vi.test.ts`:
|
||||
|
||||
- Базовые CRUD операции
|
||||
- Сложные запросы и фильтрация
|
||||
- Работа с несколькими коллекциями
|
||||
- Тест производительности
|
||||
- HTTP API сервер
|
||||
|
||||
### 5. `custom-json/` - Настраиваемые функции сериализации JSON
|
||||
|
||||
Демонстрирует использование настраиваемых функций сериализации и десериализации JSON:
|
||||
|
||||
- Использование go-json (по умолчанию)
|
||||
- Использование стандартного encoding/json
|
||||
- Кастомные функции сериализации с дополнительной логикой
|
||||
|
||||
## Запуск примеров
|
||||
|
||||
### Базовый пример
|
||||
|
||||
```bash
|
||||
cd examples/basic
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Примеры удаления
|
||||
|
||||
```bash
|
||||
cd examples/delete
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Примеры вставки
|
||||
|
||||
```bash
|
||||
cd examples/insert
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Интеграционные тесты
|
||||
|
||||
```bash
|
||||
cd examples/integration
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Настраиваемые функции JSON
|
||||
|
||||
```bash
|
||||
cd examples/custom-json
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Особенности примеров
|
||||
|
||||
### Партиционирование
|
||||
|
||||
Примеры демонстрируют работу с партиционированными коллекциями, где данные автоматически распределяются по файлам на основе значения определенного поля.
|
||||
|
||||
### Кэширование
|
||||
|
||||
Показана работа с кэшем для улучшения производительности чтения данных.
|
||||
|
||||
### Фильтрация
|
||||
|
||||
Демонстрируются различные способы фильтрации:
|
||||
|
||||
- Объектные фильтры
|
||||
- Строковые фильтры
|
||||
- Частичное совпадение
|
||||
- Строгое сравнение
|
||||
|
||||
### Производительность
|
||||
|
||||
Примеры включают тесты производительности для массовых операций.
|
||||
|
||||
### HTTP API
|
||||
|
||||
Интеграционный пример включает простой HTTP сервер с REST API для работы с базой данных.
|
||||
|
||||
### Настраиваемые функции сериализации JSON
|
||||
|
||||
Библиотека поддерживает настраиваемые функции сериализации и десериализации JSON:
|
||||
|
||||
```go
|
||||
// Функции сериализации по умолчанию (используют go-json)
|
||||
func defaultJSONMarshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func defaultJSONUnmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Настройка в опциях коллекции
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
JSONMarshal: customMarshalFunction,
|
||||
JSONUnmarshal: customUnmarshalFunction,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
По умолчанию используется библиотека [go-json](https://github.com/goccy/go-json), но вы можете указать любые функции сериализации, соответствующие сигнатурам:
|
||||
|
||||
- `func(any) ([]byte, error)` для Marshal
|
||||
- `func([]byte, any) error` для Unmarshal
|
||||
|
||||
## Структуры данных
|
||||
|
||||
Примеры используют следующие основные структуры данных:
|
||||
|
||||
```go
|
||||
// Пользователь
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// Продукт
|
||||
type Product struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Category string `json:"category"`
|
||||
InStock bool `json:"inStock"`
|
||||
SellerID int `json:"sellerId"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// Заказ
|
||||
type Order struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"userId"`
|
||||
ProductID int `json:"productId"`
|
||||
Quantity int `json:"quantity"`
|
||||
TotalPrice float64 `json:"totalPrice"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Каждый пример создает свою тестовую папку и настраивает базу данных с соответствующими коллекциями и индексами. После выполнения примеры автоматически очищают тестовые данные.
|
||||
|
||||
## Примечания
|
||||
|
||||
- Все примеры написаны на русском языке для лучшего понимания
|
||||
- Примеры демонстрируют реальные сценарии использования
|
||||
- Код включает обработку ошибок и логирование
|
||||
- Производительность измеряется и выводится в консоль
|
||||
124
examples/basic/main.go
Normal file
124
examples/basic/main.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Age int `json:"age"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Создаем опции инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 5 * time.Minute,
|
||||
DBFolder: "./data",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 512,
|
||||
IndexedFields: []string{"id", "email", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
log.Fatalf("Failed to init database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем тестовых пользователей
|
||||
users := []any{
|
||||
map[string]any{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"age": 30,
|
||||
"created": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
map[string]any{
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com",
|
||||
"age": 25,
|
||||
"created": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
map[string]any{
|
||||
"name": "Bob Johnson",
|
||||
"email": "bob@example.com",
|
||||
"age": 35,
|
||||
"created": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем пользователей
|
||||
if err := db.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Fatalf("Failed to insert users: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Users inserted successfully!")
|
||||
|
||||
// Читаем всех пользователей
|
||||
allUsers, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read users: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Total users: %d\n", len(allUsers))
|
||||
for i, user := range allUsers {
|
||||
fmt.Printf("User %d: %+v\n", i+1, user)
|
||||
}
|
||||
|
||||
// Ищем пользователя по email
|
||||
filter := map[string]any{"email": "john@example.com"}
|
||||
foundUsers, err := db.ReadByFilter(filter, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to filter users: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users with email john@example.com\n", len(foundUsers))
|
||||
for _, user := range foundUsers {
|
||||
fmt.Printf("Found user: %+v\n", user)
|
||||
}
|
||||
|
||||
// Обновляем пользователя
|
||||
updateData := map[string]any{"age": 31}
|
||||
updateFilter := map[string]any{"email": "john@example.com"}
|
||||
updatedUsers, err := db.Update(updateData, "users", updateFilter, linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update user: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated %d users\n", len(updatedUsers))
|
||||
|
||||
// Удаляем пользователя
|
||||
deleteFilter := map[string]any{"email": "bob@example.com"}
|
||||
deletedUsers, err := db.Delete(deleteFilter, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to delete user: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted %d users\n", len(deletedUsers))
|
||||
|
||||
// Читаем оставшихся пользователей
|
||||
remainingUsers, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read remaining users: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Remaining users: %d\n", len(remainingUsers))
|
||||
for i, user := range remainingUsers {
|
||||
fmt.Printf("Remaining user %d: %+v\n", i+1, user)
|
||||
}
|
||||
}
|
||||
162
examples/custom-json/custom_json_test.go
Normal file
162
examples/custom-json/custom_json_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
func TestCustomJSONSerialization(t *testing.T) {
|
||||
// Тестируем стандартный JSON
|
||||
standardJSONMarshal := func(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
standardJSONUnmarshal := func(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Тестируем кастомный JSON с метаданными
|
||||
customJSONMarshal := func(v any) ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"data": v,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "1.0",
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
customJSONUnmarshal := func(data []byte, v any) error {
|
||||
var wrapper map[string]any
|
||||
if err := json.Unmarshal(data, &wrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dataField, exists := wrapper["data"]; exists {
|
||||
dataBytes, err := json.Marshal(dataField)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(dataBytes, v)
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Тестируем функции сериализации
|
||||
testData := map[string]any{
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"age": 25,
|
||||
}
|
||||
|
||||
// Тест стандартного JSON
|
||||
standardData, err := standardJSONMarshal(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Standard JSON marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var standardResult map[string]any
|
||||
if err := standardJSONUnmarshal(standardData, &standardResult); err != nil {
|
||||
t.Fatalf("Standard JSON unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if standardResult["name"] != "test" {
|
||||
t.Errorf("Expected 'test', got %v", standardResult["name"])
|
||||
}
|
||||
|
||||
// Тест кастомного JSON
|
||||
customData, err := customJSONMarshal(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Custom JSON marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var customResult map[string]any
|
||||
if err := customJSONUnmarshal(customData, &customResult); err != nil {
|
||||
t.Fatalf("Custom JSON unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if customResult["name"] != "test" {
|
||||
t.Errorf("Expected 'test', got %v", customResult["name"])
|
||||
}
|
||||
|
||||
fmt.Println("✓ Custom JSON serialization test passed")
|
||||
}
|
||||
|
||||
func TestJSONLFileWithCustomSerialization(t *testing.T) {
|
||||
// Создаем функции сериализации
|
||||
customJSONMarshal := func(v any) ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"data": v,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "1.0",
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
customJSONUnmarshal := func(data []byte, v any) error {
|
||||
var wrapper map[string]any
|
||||
if err := json.Unmarshal(data, &wrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dataField, exists := wrapper["data"]; exists {
|
||||
dataBytes, err := json.Marshal(dataField)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(dataBytes, v)
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Создаем JSONLFile с кастомными функциями
|
||||
options := linedb.JSONLFileOptions{
|
||||
CollectionName: "test",
|
||||
JSONMarshal: customJSONMarshal,
|
||||
JSONUnmarshal: customJSONUnmarshal,
|
||||
}
|
||||
|
||||
file := linedb.NewJSONLFile("./test-custom.jsonl", "", options)
|
||||
|
||||
// Инициализируем файл
|
||||
if err := file.Init(true, linedb.LineDbAdapterOptions{}); err != nil {
|
||||
t.Fatalf("Failed to init file: %v", err)
|
||||
}
|
||||
|
||||
// Тестовые данные
|
||||
testData := map[string]any{
|
||||
"id": 1,
|
||||
"name": "test_user",
|
||||
"age": 25,
|
||||
}
|
||||
|
||||
// Записываем данные
|
||||
if err := file.Write(testData, linedb.LineDbAdapterOptions{}); err != nil {
|
||||
t.Fatalf("Failed to write data: %v", err)
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
result, err := file.Read(linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read data: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("Expected 1 record, got %d", len(result))
|
||||
}
|
||||
|
||||
if record, ok := result[0].(map[string]any); ok {
|
||||
if record["name"] != "test_user" {
|
||||
t.Errorf("Expected 'test_user', got %v", record["name"])
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Failed to cast result to map")
|
||||
}
|
||||
|
||||
fmt.Println("✓ JSONLFile with custom serialization test passed")
|
||||
}
|
||||
537
examples/custom-json/main.go
Normal file
537
examples/custom-json/main.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
// User представляет пользователя
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// Product представляет продукт
|
||||
type Product struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Category string `json:"category"`
|
||||
InStock bool `json:"inStock"`
|
||||
SellerID int `json:"sellerId"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Очищаем тестовые данные
|
||||
os.RemoveAll("./data/test-linedb-custom-json")
|
||||
|
||||
fmt.Println("=== LineDb Custom JSON Serialization Demo ===")
|
||||
|
||||
// Получаем номера тестов из аргументов командной строки
|
||||
testNumbers := parseTestNumbers()
|
||||
|
||||
// Выполняем тесты
|
||||
if len(testNumbers) == 0 {
|
||||
// Если номера не указаны, выполняем все тесты
|
||||
runAllTests()
|
||||
} else {
|
||||
// Выполняем только указанные тесты
|
||||
runSelectedTests(testNumbers)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Все тесты завершены ===")
|
||||
}
|
||||
|
||||
// parseTestNumbers парсит номера тестов из аргументов командной строки
|
||||
func parseTestNumbers() []int {
|
||||
if len(os.Args) < 2 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Получаем аргумент после имени программы
|
||||
arg := os.Args[1]
|
||||
|
||||
// Проверяем, что это не флаг помощи
|
||||
if arg == "-h" || arg == "--help" || arg == "help" {
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Разбиваем по запятой
|
||||
parts := strings.Split(arg, ",")
|
||||
var numbers []int
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
fmt.Printf("Ошибка: '%s' не является числом\n", part)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Проверяем диапазон
|
||||
if num < 1 || num > 4 {
|
||||
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 4\n", num)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
numbers = append(numbers, num)
|
||||
}
|
||||
|
||||
return numbers
|
||||
}
|
||||
|
||||
// printUsage выводит справку по использованию
|
||||
func printUsage() {
|
||||
fmt.Println("\nИспользование:")
|
||||
fmt.Println(" go run main.go # Запустить все тесты")
|
||||
fmt.Println(" go run main.go 1 # Запустить только тест 1")
|
||||
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
|
||||
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
|
||||
fmt.Println(" go run main.go help # Показать эту справку")
|
||||
fmt.Println("\nДоступные тесты:")
|
||||
fmt.Println(" 1 - Использование go-json (по умолчанию)")
|
||||
fmt.Println(" 2 - Использование стандартного encoding/json")
|
||||
fmt.Println(" 3 - Использование кастомной функции сериализации")
|
||||
fmt.Println(" 4 - ID всегда первое поле")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// runAllTests запускает все тесты
|
||||
func runAllTests() {
|
||||
testDefaultJSON()
|
||||
testStandardJSON()
|
||||
testCustomJSON()
|
||||
testIDFirstField()
|
||||
}
|
||||
|
||||
// runSelectedTests запускает только выбранные тесты
|
||||
func runSelectedTests(numbers []int) {
|
||||
for _, num := range numbers {
|
||||
switch num {
|
||||
case 1:
|
||||
fmt.Println("1. Использование go-json (по умолчанию)")
|
||||
testDefaultJSON()
|
||||
case 2:
|
||||
fmt.Println("2. Использование стандартного encoding/json")
|
||||
testStandardJSON()
|
||||
case 3:
|
||||
fmt.Println("3. Использование кастомной функции сериализации")
|
||||
testCustomJSON()
|
||||
case 4:
|
||||
fmt.Println("4. ID всегда первое поле")
|
||||
testIDFirstField()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultJSON() {
|
||||
// Создаем опции инициализации с go-json (по умолчанию)
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-custom-json/default",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 1,
|
||||
"username": "default_user",
|
||||
"email": "default@example.com",
|
||||
"isActive": true,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
result, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей в базе: %d\n", len(result))
|
||||
if len(result) > 0 {
|
||||
if record, ok := result[0].(map[string]any); ok {
|
||||
fmt.Printf(" ID: %v, Username: %s\n", record["id"], record["username"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testStandardJSON() {
|
||||
// Создаем функции сериализации с использованием стандартного encoding/json
|
||||
standardJSONMarshal := func(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
standardJSONUnmarshal := func(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Создаем опции инициализации со стандартным JSON
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-custom-json/standard",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
JSONMarshal: standardJSONMarshal,
|
||||
JSONUnmarshal: standardJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 2,
|
||||
"username": "standard_user",
|
||||
"email": "standard@example.com",
|
||||
"isActive": true,
|
||||
"role": "admin",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
result, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей в базе: %d\n", len(result))
|
||||
if len(result) > 0 {
|
||||
if record, ok := result[0].(map[string]any); ok {
|
||||
fmt.Printf(" ID: %v, Username: %s\n", record["id"], record["username"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomJSON() {
|
||||
// Создаем кастомные функции сериализации с дополнительной логикой
|
||||
customJSONMarshal := func(v any) ([]byte, error) {
|
||||
// Добавляем метаданные к сериализации
|
||||
data := map[string]any{
|
||||
"data": v,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "1.0",
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
customJSONUnmarshal := func(data []byte, v any) error {
|
||||
// Извлекаем данные из кастомного формата
|
||||
var wrapper map[string]any
|
||||
if err := json.Unmarshal(data, &wrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Извлекаем основную часть данных
|
||||
if dataField, exists := wrapper["data"]; exists {
|
||||
// Сериализуем обратно в JSON и десериализуем в целевой тип
|
||||
dataBytes, err := json.Marshal(dataField)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(dataBytes, v)
|
||||
}
|
||||
|
||||
// Если нет поля data, пытаемся десериализовать как обычный JSON
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Создаем опции инициализации с кастомными функциями
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-custom-json/custom",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
JSONMarshal: customJSONMarshal,
|
||||
JSONUnmarshal: customJSONUnmarshal,
|
||||
},
|
||||
{
|
||||
CollectionName: "products",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name"},
|
||||
JSONMarshal: customJSONMarshal,
|
||||
JSONUnmarshal: customJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 3,
|
||||
"username": "custom_user",
|
||||
"email": "custom@example.com",
|
||||
"isActive": true,
|
||||
"role": "moderator",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Создаем продукт
|
||||
product := map[string]any{
|
||||
"id": 1,
|
||||
"name": "Custom Product",
|
||||
"price": 99.99,
|
||||
"category": "Electronics",
|
||||
"inStock": true,
|
||||
"sellerId": 1,
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert product: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
products, err := db.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей в базе: %d\n", len(users))
|
||||
fmt.Printf(" Продуктов в базе: %d\n", len(products))
|
||||
|
||||
if len(users) > 0 {
|
||||
if record, ok := users[0].(map[string]any); ok {
|
||||
fmt.Printf(" Пользователь ID: %v, Username: %s\n", record["id"], record["username"])
|
||||
}
|
||||
}
|
||||
|
||||
if len(products) > 0 {
|
||||
if record, ok := products[0].(map[string]any); ok {
|
||||
fmt.Printf(" Продукт ID: %v, Name: %s, Price: %v\n", record["id"], record["name"], record["price"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testIDFirstField() {
|
||||
// Создаем функции сериализации где id всегда первое поле
|
||||
idFirstJSONMarshal := func(v any) ([]byte, error) {
|
||||
if data, ok := v.(map[string]any); ok {
|
||||
var parts []string
|
||||
|
||||
// Сначала добавляем id если он есть
|
||||
if id, exists := data["id"]; exists {
|
||||
idBytes, err := json.Marshal(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf(`"id":%s`, string(idBytes)))
|
||||
}
|
||||
|
||||
// Затем добавляем все остальные поля в алфавитном порядке
|
||||
var keys []string
|
||||
for key := range data {
|
||||
if key != "id" {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
valueBytes, err := json.Marshal(data[key])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf(`"%s":%s`, key, string(valueBytes)))
|
||||
}
|
||||
|
||||
jsonStr := "{" + strings.Join(parts, ",") + "}"
|
||||
return []byte(jsonStr), nil
|
||||
}
|
||||
|
||||
// Если это не map, используем стандартную сериализацию
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
idFirstJSONUnmarshal := func(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// Создаем опции инициализации с функциями id-first
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-custom-json/id-first",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
JSONMarshal: idFirstJSONMarshal,
|
||||
JSONUnmarshal: idFirstJSONUnmarshal,
|
||||
},
|
||||
{
|
||||
CollectionName: "products",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name"},
|
||||
JSONMarshal: idFirstJSONMarshal,
|
||||
JSONUnmarshal: idFirstJSONUnmarshal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователя (id не первое поле в исходных данных)
|
||||
user := map[string]any{
|
||||
"username": "id_first_user",
|
||||
"email": "id_first@example.com",
|
||||
"id": 1,
|
||||
"isActive": true,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Создаем продукт (id не первое поле в исходных данных)
|
||||
product := map[string]any{
|
||||
"name": "ID First Product",
|
||||
"price": 123.45,
|
||||
"id": 2,
|
||||
"category": "Books",
|
||||
"inStock": false,
|
||||
"sellerId": 1,
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert product: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
products, err := db.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей в базе: %d\n", len(users))
|
||||
fmt.Printf(" Продуктов в базе: %d\n", len(products))
|
||||
|
||||
if len(users) > 0 {
|
||||
if record, ok := users[0].(map[string]any); ok {
|
||||
fmt.Printf(" Пользователь ID: %v, Username: %s\n", record["id"], record["username"])
|
||||
}
|
||||
}
|
||||
|
||||
if len(products) > 0 {
|
||||
if record, ok := products[0].(map[string]any); ok {
|
||||
fmt.Printf(" Продукт ID: %v, Name: %s, Price: %v\n", record["id"], record["name"], record["price"])
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем содержимое файла для демонстрации
|
||||
fmt.Println(" Проверьте файл ./data/test-linedb-custom-json/id-first/users.jsonl")
|
||||
fmt.Println(" Поле 'id' должно быть первым в JSON строке")
|
||||
}
|
||||
1
examples/custom-json/test-custom.jsonl
Normal file
1
examples/custom-json/test-custom.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"data":{"age":25,"id":1,"name":"test_user"},"timestamp":1754969067,"version":"1.0"}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":2,"category":"Books","createdAt":1754974225,"inStock":false,"name":"ID First Product","price":123.45,"sellerId":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":1,"createdAt":1754974225,"email":"id_first@example.com","isActive":true,"role":"user","username":"id_first_user"}
|
||||
@@ -0,0 +1 @@
|
||||
{"createdAt":1754974225,"email":"standard@example.com","id":2,"isActive":true,"role":"admin","username":"standard_user"}
|
||||
641
examples/delete/delete.go
Normal file
641
examples/delete/delete.go
Normal file
@@ -0,0 +1,641 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
// testDeleteOneRecordByID тестирует удаление одной записи по ID
|
||||
func testDeleteOneRecordByID() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем тестовые данные
|
||||
user := map[string]any{
|
||||
"id": 1,
|
||||
"username": "test_user",
|
||||
"email": "test@example.com",
|
||||
"isActive": true,
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей до удаления: %d\n", len(users))
|
||||
|
||||
// Удаляем запись по ID
|
||||
deleted, err := db.Delete(map[string]any{"id": 1}, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete user: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей: %d\n", len(deleted))
|
||||
|
||||
// Проверяем результат
|
||||
users, err = db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users after delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей после удаления: %d\n", len(users))
|
||||
}
|
||||
|
||||
// testDeleteOneRecordByTextFilter тестирует удаление одной записи по текстовому фильтру
|
||||
func testDeleteOneRecordByTextFilter() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "products",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем тестовые данные
|
||||
product := map[string]any{
|
||||
"id": 1,
|
||||
"name": "Test Product",
|
||||
"price": 99.99,
|
||||
"category": "Electronics",
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert product: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
products, err := db.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read products: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей до удаления: %d\n", len(products))
|
||||
|
||||
// Удаляем запись по текстовому фильтру
|
||||
deleted, err := db.Delete("name == 'Test Product'", "products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete product: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей: %d\n", len(deleted))
|
||||
|
||||
// Проверяем результат
|
||||
products, err = db.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read products after delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей после удаления: %d\n", len(products))
|
||||
}
|
||||
|
||||
// testDeleteMultipleRecordsByArray тестирует удаление нескольких записей по массиву данных
|
||||
func testDeleteMultipleRecordsByArray() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "orders",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "status"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем массив тестовых данных
|
||||
orders := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"status": "pending",
|
||||
"amount": 100.0,
|
||||
"customerId": 1,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"status": "completed",
|
||||
"amount": 200.0,
|
||||
"customerId": 2,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"amount": 150.0,
|
||||
"customerId": 1,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert orders: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
ordersData, err := db.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read orders: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей до удаления: %d\n", len(ordersData))
|
||||
|
||||
// Удаляем несколько записей по массиву фильтров
|
||||
deleteFilters := []any{
|
||||
map[string]any{"id": 1},
|
||||
map[string]any{"id": 2},
|
||||
}
|
||||
deleted, err := db.Delete(deleteFilters, "orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete orders: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей: %d\n", len(deleted))
|
||||
|
||||
// Проверяем результат
|
||||
ordersData, err = db.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read orders after delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей после удаления: %d\n", len(ordersData))
|
||||
}
|
||||
|
||||
// testDeleteByPartialMatch тестирует удаление записей по частичному совпадению
|
||||
func testDeleteByPartialMatch() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "customers",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем массив тестовых данных
|
||||
customers := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com",
|
||||
},
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"name": "John Smith",
|
||||
"email": "john.smith@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(customers, "customers", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert customers: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
customersData, err := db.Read("customers", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read customers: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей до удаления: %d\n", len(customersData))
|
||||
|
||||
// Удаляем записи по частичному совпадению
|
||||
deleted, err := db.Delete(
|
||||
map[string]any{"name": "John"},
|
||||
"customers",
|
||||
linedb.LineDbAdapterOptions{StrictCompare: false},
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete customers: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей с именем 'John': %d\n", len(deleted))
|
||||
|
||||
// Проверяем результат
|
||||
customersData, err = db.Read("customers", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read customers after delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей после удаления: %d\n", len(customersData))
|
||||
}
|
||||
|
||||
// testDeleteFromPartitionedCollections тестирует удаление из партиционированных коллекций
|
||||
func testDeleteFromPartitionedCollections() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных с партициями
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "logs",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "level"},
|
||||
},
|
||||
},
|
||||
Partitions: []linedb.PartitionCollection{
|
||||
{
|
||||
CollectionName: "logs",
|
||||
PartIDFnStr: "level",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем логи для разных уровней
|
||||
logs := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"level": "error",
|
||||
"message": "Critical error occurred",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"level": "info",
|
||||
"message": "User logged in",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"level": "error",
|
||||
"message": "Another error",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(logs, "logs", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert logs: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
logsData, err := db.Read("logs", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read logs: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей до удаления: %d\n", len(logsData))
|
||||
|
||||
// Удаляем логи с уровнем 'error'
|
||||
deleted, err := db.Delete(map[string]any{"level": "error"}, "logs", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete error logs: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей с уровнем 'error': %d\n", len(deleted))
|
||||
|
||||
// Проверяем результат
|
||||
logsData, err = db.Read("logs", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read logs after delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей после удаления: %d\n", len(logsData))
|
||||
}
|
||||
|
||||
// testDeleteEdgeCases тестирует граничные случаи удаления
|
||||
func testDeleteEdgeCases() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "test_data",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "type"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Тест удаления несуществующей записи
|
||||
fmt.Println(" Тест удаления несуществующей записи:")
|
||||
deleted, err := db.Delete(map[string]any{"id": 999}, "test_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete non-existent record: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено несуществующих записей: %d\n", len(deleted))
|
||||
|
||||
// Тест удаления пустого массива
|
||||
fmt.Println(" Тест удаления пустого массива:")
|
||||
deleted, err = db.Delete([]any{}, "test_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete empty array: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей из пустого массива: %d\n", len(deleted))
|
||||
|
||||
// Тест удаления записей с разными типами ID
|
||||
fmt.Println(" Тест удаления записей с разными типами ID:")
|
||||
|
||||
// Вставляем запись со строковым ID
|
||||
stringIDData := map[string]any{
|
||||
"id": "user-1",
|
||||
"name": "Test String Id User",
|
||||
"type": "string_id",
|
||||
}
|
||||
if err := db.Insert(stringIDData, "test_data", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert string ID data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем запись со строковым ID
|
||||
deleted, err = db.Delete("id == 'user-1'", "test_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete string ID record: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено записей со строковым ID: %d\n", len(deleted))
|
||||
}
|
||||
|
||||
// testDeletePerformance тестирует производительность удаления
|
||||
func testDeletePerformance() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 10000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "performance_data",
|
||||
AllocSize: 1024,
|
||||
IndexedFields: []string{"id", "category"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем большое количество записей
|
||||
recordCount := 1000
|
||||
fmt.Printf(" Создание %d записей для теста производительности...\n", recordCount)
|
||||
|
||||
largeDataArray := make([]any, recordCount)
|
||||
for i := 1; i <= recordCount; i++ {
|
||||
largeDataArray[i-1] = map[string]any{
|
||||
"id": i,
|
||||
"name": fmt.Sprintf("User %d", i),
|
||||
"category": fmt.Sprintf("Category %d", (i-1)%10+1),
|
||||
"value": float64(i) * 1.5,
|
||||
}
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(largeDataArray, "performance_data", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert large data array: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные для проверки
|
||||
result, err := db.Read("performance_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read large data: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей в базе: %d\n", len(result))
|
||||
|
||||
// Удаляем половину записей (четные ID)
|
||||
startTime := time.Now()
|
||||
itemsToDelete := make([]any, recordCount/2)
|
||||
for i := 0; i < recordCount/2; i++ {
|
||||
itemsToDelete[i] = map[string]any{"id": (i + 1) * 2}
|
||||
}
|
||||
|
||||
deleted, err := db.Delete(itemsToDelete, "performance_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete large number of records: %v", err)
|
||||
return
|
||||
}
|
||||
endTime := time.Now()
|
||||
|
||||
duration := endTime.Sub(startTime)
|
||||
fmt.Printf(" Удалено записей: %d\n", len(deleted))
|
||||
fmt.Printf(" Время выполнения: %v\n", duration)
|
||||
|
||||
// Проверяем результат
|
||||
result, err = db.Read("performance_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read data after bulk delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Записей после массового удаления: %d\n", len(result))
|
||||
}
|
||||
|
||||
// testDeleteMultipleCollections тестирует удаление из множественных коллекций
|
||||
func testDeleteMultipleCollections() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных с множественными коллекциями
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-delete",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
},
|
||||
{
|
||||
CollectionName: "posts",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "authorId"},
|
||||
},
|
||||
{
|
||||
CollectionName: "comments",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "postId"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем данные для разных коллекций
|
||||
users := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"username": "user1",
|
||||
"email": "user1@example.com",
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"username": "user2",
|
||||
"email": "user2@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
posts := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"title": "First Post",
|
||||
"authorId": 1,
|
||||
"content": "This is the first post",
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"title": "Second Post",
|
||||
"authorId": 2,
|
||||
"content": "This is the second post",
|
||||
},
|
||||
}
|
||||
|
||||
comments := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"postId": 1,
|
||||
"content": "Great post!",
|
||||
"authorId": 2,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"postId": 2,
|
||||
"content": "Nice article",
|
||||
"authorId": 1,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные в разные коллекции
|
||||
if err := db.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(posts, "posts", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert posts: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(comments, "comments", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert comments: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем данные из разных коллекций
|
||||
usersDeleted, _ := db.Delete(map[string]any{"id": 1}, "users", linedb.LineDbAdapterOptions{})
|
||||
postsDeleted, _ := db.Delete(map[string]any{"authorId": 1}, "posts", linedb.LineDbAdapterOptions{})
|
||||
commentsDeleted, _ := db.Delete(map[string]any{"postId": 1}, "comments", linedb.LineDbAdapterOptions{})
|
||||
|
||||
// Читаем данные из всех коллекций
|
||||
usersData, _ := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
postsData, _ := db.Read("posts", linedb.LineDbAdapterOptions{})
|
||||
commentsData, _ := db.Read("comments", linedb.LineDbAdapterOptions{})
|
||||
|
||||
fmt.Printf(" Удалено пользователей: %d\n", len(usersDeleted))
|
||||
fmt.Printf(" Удалено постов: %d\n", len(postsDeleted))
|
||||
fmt.Printf(" Удалено комментариев: %d\n", len(commentsDeleted))
|
||||
fmt.Printf(" Осталось пользователей: %d\n", len(usersData))
|
||||
fmt.Printf(" Осталось постов: %d\n", len(postsData))
|
||||
fmt.Printf(" Осталось комментариев: %d\n", len(commentsData))
|
||||
}
|
||||
167
examples/delete/main.go
Normal file
167
examples/delete/main.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TestData представляет тестовые данные
|
||||
type TestData struct {
|
||||
ID any `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Value *int `json:"value,omitempty"`
|
||||
UserID int `json:"userId"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// TestUser представляет пользователя
|
||||
type TestUser struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Role string `json:"role"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// TestOrder представляет заказ
|
||||
type TestOrder struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"userId"`
|
||||
Status string `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Очищаем тестовую папку
|
||||
os.RemoveAll("./data/test-linedb-delete")
|
||||
|
||||
fmt.Println("=== LineDb Delete Operations Demo ===")
|
||||
|
||||
// Получаем номера тестов из аргументов командной строки
|
||||
testNumbers := parseTestNumbers()
|
||||
|
||||
// Выполняем тесты
|
||||
if len(testNumbers) == 0 {
|
||||
// Если номера не указаны, выполняем все тесты
|
||||
runAllTests()
|
||||
} else {
|
||||
// Выполняем только указанные тесты
|
||||
runSelectedTests(testNumbers)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Все тесты завершены ===")
|
||||
}
|
||||
|
||||
// parseTestNumbers парсит номера тестов из аргументов командной строки
|
||||
func parseTestNumbers() []int {
|
||||
if len(os.Args) < 2 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Получаем аргумент после имени программы
|
||||
arg := os.Args[1]
|
||||
|
||||
// Проверяем, что это не флаг помощи
|
||||
if arg == "-h" || arg == "--help" || arg == "help" {
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Разбиваем по запятой
|
||||
parts := strings.Split(arg, ",")
|
||||
var numbers []int
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
fmt.Printf("Ошибка: '%s' не является числом\n", part)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Проверяем диапазон
|
||||
if num < 1 || num > 8 {
|
||||
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 8\n", num)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
numbers = append(numbers, num)
|
||||
}
|
||||
|
||||
return numbers
|
||||
}
|
||||
|
||||
// printUsage выводит справку по использованию
|
||||
func printUsage() {
|
||||
fmt.Println("\nИспользование:")
|
||||
fmt.Println(" go run main.go # Запустить все тесты")
|
||||
fmt.Println(" go run main.go 1 # Запустить только тест 1")
|
||||
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
|
||||
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
|
||||
fmt.Println(" go run main.go help # Показать эту справку")
|
||||
fmt.Println("\nДоступные тесты:")
|
||||
fmt.Println(" 1 - Удаление одной записи по ID")
|
||||
fmt.Println(" 2 - Удаление одной записи по текстовому фильтру")
|
||||
fmt.Println(" 3 - Удаление нескольких записей по массиву данных")
|
||||
fmt.Println(" 4 - Удаление записей по частичному совпадению")
|
||||
fmt.Println(" 5 - Удаление записей из партиционированных коллекций")
|
||||
fmt.Println(" 6 - Удаление записей с граничными случаями")
|
||||
fmt.Println(" 7 - Удаление записей с производительностью")
|
||||
fmt.Println(" 8 - Удаление записей с множественными коллекциями")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// runAllTests запускает все тесты
|
||||
func runAllTests() {
|
||||
testDeleteOneRecordByID()
|
||||
testDeleteOneRecordByTextFilter()
|
||||
testDeleteMultipleRecordsByArray()
|
||||
testDeleteByPartialMatch()
|
||||
testDeleteFromPartitionedCollections()
|
||||
testDeleteEdgeCases()
|
||||
testDeletePerformance()
|
||||
testDeleteMultipleCollections()
|
||||
}
|
||||
|
||||
// runSelectedTests запускает только выбранные тесты
|
||||
func runSelectedTests(numbers []int) {
|
||||
for _, num := range numbers {
|
||||
switch num {
|
||||
case 1:
|
||||
fmt.Println("1. Удаление одной записи по ID")
|
||||
testDeleteOneRecordByID()
|
||||
case 2:
|
||||
fmt.Println("2. Удаление одной записи по текстовому фильтру")
|
||||
testDeleteOneRecordByTextFilter()
|
||||
case 3:
|
||||
fmt.Println("3. Удаление нескольких записей по массиву данных")
|
||||
testDeleteMultipleRecordsByArray()
|
||||
case 4:
|
||||
fmt.Println("4. Удаление записей по частичному совпадению")
|
||||
testDeleteByPartialMatch()
|
||||
case 5:
|
||||
fmt.Println("5. Удаление записей из партиционированных коллекций")
|
||||
testDeleteFromPartitionedCollections()
|
||||
case 6:
|
||||
fmt.Println("6. Удаление записей с граничными случаями")
|
||||
testDeleteEdgeCases()
|
||||
case 7:
|
||||
fmt.Println("7. Удаление записей с производительностью")
|
||||
testDeletePerformance()
|
||||
case 8:
|
||||
fmt.Println("8. Удаление записей с множественными коллекциями")
|
||||
testDeleteMultipleCollections()
|
||||
}
|
||||
}
|
||||
}
|
||||
592
examples/insert/insert.go
Normal file
592
examples/insert/insert.go
Normal file
@@ -0,0 +1,592 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
// testSingleInsert тестирует вставку одной записи
|
||||
func testSingleInsert() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 1,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"isActive": true,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
users, err := db.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей в базе: %d\n", len(users))
|
||||
if len(users) > 0 {
|
||||
if record, ok := users[0].(map[string]any); ok {
|
||||
fmt.Printf(" ID: %v, Username: %s\n", record["id"], record["username"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testArrayInsert тестирует вставку массива записей
|
||||
func testArrayInsert() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "products",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем массив продуктов
|
||||
products := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"name": "Laptop",
|
||||
"price": 999.99,
|
||||
"category": "Electronics",
|
||||
"inStock": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"name": "Mouse",
|
||||
"price": 29.99,
|
||||
"category": "Electronics",
|
||||
"inStock": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"name": "Keyboard",
|
||||
"price": 79.99,
|
||||
"category": "Electronics",
|
||||
"inStock": false,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(products, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
productsData, err := db.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Продуктов в базе: %d\n", len(productsData))
|
||||
for i, product := range productsData {
|
||||
if record, ok := product.(map[string]any); ok {
|
||||
fmt.Printf(" %d. ID: %v, Name: %s, Price: %v\n", i+1, record["id"], record["name"], record["price"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testAutoID тестирует автоматическую генерацию ID
|
||||
func testAutoID() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "orders",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "customerId"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем заказы без ID
|
||||
orders := []any{
|
||||
map[string]any{
|
||||
"customerId": 1,
|
||||
"amount": 150.00,
|
||||
"status": "pending",
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
map[string]any{
|
||||
"customerId": 2,
|
||||
"amount": 75.50,
|
||||
"status": "completed",
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert orders: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
ordersData, err := db.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read orders: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Заказов в базе: %d\n", len(ordersData))
|
||||
for i, order := range ordersData {
|
||||
if record, ok := order.(map[string]any); ok {
|
||||
fmt.Printf(" %d. ID: %v, Customer: %v, Amount: %v\n", i+1, record["id"], record["customerId"], record["amount"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testPartitionedCollections тестирует партиционированные коллекции
|
||||
func testPartitionedCollections() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных с партиционированными коллекциями
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users_2024",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
},
|
||||
{
|
||||
CollectionName: "users_2023",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователей для разных партиций
|
||||
users2024 := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"username": "user_2024_1",
|
||||
"email": "user1@2024.com",
|
||||
"year": 2024,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"username": "user_2024_2",
|
||||
"email": "user2@2024.com",
|
||||
"year": 2024,
|
||||
},
|
||||
}
|
||||
|
||||
users2023 := []any{
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"username": "user_2023_1",
|
||||
"email": "user1@2023.com",
|
||||
"year": 2023,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные в разные партиции
|
||||
if err := db.Insert(users2024, "users_2024", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert users 2024: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(users2023, "users_2023", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert users 2023: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные из обеих партиций
|
||||
users2024Data, err := db.Read("users_2024", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users 2024: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
users2023Data, err := db.Read("users_2023", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users 2023: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей 2024: %d\n", len(users2024Data))
|
||||
fmt.Printf(" Пользователей 2023: %d\n", len(users2023Data))
|
||||
}
|
||||
|
||||
// testUniquenessCheck тестирует проверку уникальности
|
||||
func testUniquenessCheck() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "unique_users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "email"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 1,
|
||||
"username": "unique_user",
|
||||
"email": "unique@example.com",
|
||||
"isActive": true,
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(user, "unique_users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Пытаемся вставить пользователя с тем же email
|
||||
duplicateUser := map[string]any{
|
||||
"id": 2,
|
||||
"username": "duplicate_user",
|
||||
"email": "unique@example.com", // Тот же email
|
||||
"isActive": true,
|
||||
}
|
||||
|
||||
if err := db.Insert(duplicateUser, "unique_users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
fmt.Printf(" Ожидаемая ошибка дублирования: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Дублирование не обнаружено\n")
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
users, err := db.Read("unique_users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Уникальных пользователей: %d\n", len(users))
|
||||
}
|
||||
|
||||
// testDifferentDataTypes тестирует различные типы данных
|
||||
func testDifferentDataTypes() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "mixed_data",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "type"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем данные разных типов
|
||||
mixedData := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"type": "string",
|
||||
"value": "Hello World",
|
||||
"isString": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"type": "number",
|
||||
"value": 42.5,
|
||||
"isNumber": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
"isBool": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 4,
|
||||
"type": "array",
|
||||
"value": []any{1, 2, 3, "four"},
|
||||
"isArray": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 5,
|
||||
"type": "object",
|
||||
"value": map[string]any{"nested": "value", "count": 10},
|
||||
"isObject": true,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
if err := db.Insert(mixedData, "mixed_data", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert mixed data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные
|
||||
data, err := db.Read("mixed_data", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read mixed data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Записей с разными типами данных: %d\n", len(data))
|
||||
for i, record := range data {
|
||||
if item, ok := record.(map[string]any); ok {
|
||||
fmt.Printf(" %d. ID: %v, Type: %s\n", i+1, item["id"], item["type"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testBulkInsert тестирует массовую вставку
|
||||
func testBulkInsert() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 10000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "bulk_items",
|
||||
AllocSize: 1024,
|
||||
IndexedFields: []string{"id", "category"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем большое количество записей
|
||||
var bulkItems []any
|
||||
for i := 1; i <= 1000; i++ {
|
||||
item := map[string]any{
|
||||
"id": i,
|
||||
"name": fmt.Sprintf("Item %d", i),
|
||||
"category": fmt.Sprintf("Category %d", (i-1)%10+1),
|
||||
"price": float64(i) * 1.5,
|
||||
"inStock": i%2 == 0,
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
bulkItems = append(bulkItems, item)
|
||||
}
|
||||
|
||||
// Вставляем данные
|
||||
startTime := time.Now()
|
||||
if err := db.Insert(bulkItems, "bulk_items", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert bulk items: %v", err)
|
||||
return
|
||||
}
|
||||
insertTime := time.Since(startTime)
|
||||
|
||||
// Читаем данные
|
||||
startTime = time.Now()
|
||||
items, err := db.Read("bulk_items", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read bulk items: %v", err)
|
||||
return
|
||||
}
|
||||
readTime := time.Since(startTime)
|
||||
|
||||
fmt.Printf(" Массовая вставка: %d записей за %v\n", len(items), insertTime)
|
||||
fmt.Printf(" Чтение: %d записей за %v\n", len(items), readTime)
|
||||
}
|
||||
|
||||
// testMultipleCollections тестирует работу с множественными коллекциями
|
||||
func testMultipleCollections() {
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных с множественными коллекциями
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-insert",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "customers",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "email"},
|
||||
},
|
||||
{
|
||||
CollectionName: "orders",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "customerId"},
|
||||
},
|
||||
{
|
||||
CollectionName: "products",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := db.Init(true, initOptions); err != nil {
|
||||
log.Printf("Failed to init database: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Создаем данные для разных коллекций
|
||||
customers := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"isActive": true,
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com",
|
||||
"isActive": true,
|
||||
},
|
||||
}
|
||||
|
||||
orders := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"customerId": 1,
|
||||
"amount": 150.00,
|
||||
"status": "completed",
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"customerId": 2,
|
||||
"amount": 75.50,
|
||||
"status": "pending",
|
||||
},
|
||||
}
|
||||
|
||||
products := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"name": "Laptop",
|
||||
"price": 999.99,
|
||||
"category": "Electronics",
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"name": "Mouse",
|
||||
"price": 29.99,
|
||||
"category": "Electronics",
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные в разные коллекции
|
||||
if err := db.Insert(customers, "customers", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert customers: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert orders: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Insert(products, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные из всех коллекций
|
||||
customersData, _ := db.Read("customers", linedb.LineDbAdapterOptions{})
|
||||
ordersData, _ := db.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
productsData, _ := db.Read("products", linedb.LineDbAdapterOptions{})
|
||||
|
||||
fmt.Printf(" Коллекций: 3\n")
|
||||
fmt.Printf(" Клиентов: %d\n", len(customersData))
|
||||
fmt.Printf(" Заказов: %d\n", len(ordersData))
|
||||
fmt.Printf(" Продуктов: %d\n", len(productsData))
|
||||
}
|
||||
138
examples/insert/main.go
Normal file
138
examples/insert/main.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Очищаем тестовые данные
|
||||
os.RemoveAll("./data/test-linedb-insert")
|
||||
|
||||
fmt.Println("=== LineDb Insert Operations Demo ===")
|
||||
|
||||
// Получаем номера тестов из аргументов командной строки
|
||||
testNumbers := parseTestNumbers()
|
||||
|
||||
// Выполняем тесты
|
||||
if len(testNumbers) == 0 {
|
||||
// Если номера не указаны, выполняем все тесты
|
||||
runAllTests()
|
||||
} else {
|
||||
// Выполняем только указанные тесты
|
||||
runSelectedTests(testNumbers)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Все тесты завершены ===")
|
||||
}
|
||||
|
||||
// parseTestNumbers парсит номера тестов из аргументов командной строки
|
||||
func parseTestNumbers() []int {
|
||||
if len(os.Args) < 2 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Получаем аргумент после имени программы
|
||||
arg := os.Args[1]
|
||||
|
||||
// Проверяем, что это не флаг помощи
|
||||
if arg == "-h" || arg == "--help" || arg == "help" {
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Разбиваем по запятой
|
||||
parts := strings.Split(arg, ",")
|
||||
var numbers []int
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
fmt.Printf("Ошибка: '%s' не является числом\n", part)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Проверяем диапазон
|
||||
if num < 1 || num > 8 {
|
||||
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 8\n", num)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
numbers = append(numbers, num)
|
||||
}
|
||||
|
||||
return numbers
|
||||
}
|
||||
|
||||
// printUsage выводит справку по использованию
|
||||
func printUsage() {
|
||||
fmt.Println("\nИспользование:")
|
||||
fmt.Println(" go run main.go # Запустить все тесты")
|
||||
fmt.Println(" go run main.go 1 # Запустить только тест 1")
|
||||
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
|
||||
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
|
||||
fmt.Println(" go run main.go help # Показать эту справку")
|
||||
fmt.Println("\nДоступные тесты:")
|
||||
fmt.Println(" 1 - Вставка одной записи")
|
||||
fmt.Println(" 2 - Вставка массива записей")
|
||||
fmt.Println(" 3 - Автоматическая генерация ID")
|
||||
fmt.Println(" 4 - Партиционированные коллекции")
|
||||
fmt.Println(" 5 - Проверка уникальности")
|
||||
fmt.Println(" 6 - Различные типы данных")
|
||||
fmt.Println(" 7 - Массовая вставка")
|
||||
fmt.Println(" 8 - Множественные коллекции")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// runAllTests запускает все тесты
|
||||
func runAllTests() {
|
||||
testSingleInsert()
|
||||
testArrayInsert()
|
||||
testAutoID()
|
||||
testPartitionedCollections()
|
||||
testUniquenessCheck()
|
||||
testDifferentDataTypes()
|
||||
testBulkInsert()
|
||||
testMultipleCollections()
|
||||
}
|
||||
|
||||
// runSelectedTests запускает только выбранные тесты
|
||||
func runSelectedTests(numbers []int) {
|
||||
for _, num := range numbers {
|
||||
switch num {
|
||||
case 1:
|
||||
fmt.Println("1. Вставка одной записи")
|
||||
testSingleInsert()
|
||||
case 2:
|
||||
fmt.Println("2. Вставка массива записей")
|
||||
testArrayInsert()
|
||||
case 3:
|
||||
fmt.Println("3. Автоматическая генерация ID")
|
||||
testAutoID()
|
||||
case 4:
|
||||
fmt.Println("4. Партиционированные коллекции")
|
||||
testPartitionedCollections()
|
||||
case 5:
|
||||
fmt.Println("5. Проверка уникальности")
|
||||
testUniquenessCheck()
|
||||
case 6:
|
||||
fmt.Println("6. Различные типы данных")
|
||||
testDifferentDataTypes()
|
||||
case 7:
|
||||
fmt.Println("7. Массовая вставка")
|
||||
testBulkInsert()
|
||||
case 8:
|
||||
fmt.Println("8. Множественные коллекции")
|
||||
testMultipleCollections()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
examples/insert/test-linedb-insert/users.jsonl
Normal file
1
examples/insert/test-linedb-insert/users.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"createdAt":1754974407,"email":"john@example.com","id":1,"isActive":true,"role":"user","username":"john_doe"}
|
||||
722
examples/integration/main.go
Normal file
722
examples/integration/main.go
Normal file
@@ -0,0 +1,722 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
// User представляет пользователя
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
LastLogin *int64 `json:"lastLogin,omitempty"`
|
||||
}
|
||||
|
||||
// Product представляет продукт
|
||||
type Product struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Category string `json:"category"`
|
||||
InStock bool `json:"inStock"`
|
||||
SellerID int `json:"sellerId"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
// Order представляет заказ
|
||||
type Order struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"userId"`
|
||||
ProductID int `json:"productId"`
|
||||
Quantity int `json:"quantity"`
|
||||
TotalPrice float64 `json:"totalPrice"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// OrderItem представляет элемент заказа
|
||||
type OrderItem struct {
|
||||
ID int `json:"id"`
|
||||
OrderID int `json:"orderId"`
|
||||
ProductID int `json:"productId"`
|
||||
Quantity int `json:"quantity"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
// APIResponse представляет ответ API
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
var Database *linedb.LineDb
|
||||
var server *http.Server
|
||||
|
||||
// parseTestNumbers парсит номера тестов из аргументов командной строки
|
||||
func parseTestNumbers() []int {
|
||||
if len(os.Args) < 2 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Получаем аргумент после имени программы
|
||||
arg := os.Args[1]
|
||||
|
||||
// Проверяем, что это не флаг помощи
|
||||
if arg == "-h" || arg == "--help" || arg == "help" {
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Разбиваем по запятой
|
||||
parts := strings.Split(arg, ",")
|
||||
var numbers []int
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
fmt.Printf("Ошибка: '%s' не является числом\n", part)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// Проверяем диапазон
|
||||
if num < 1 || num > 4 {
|
||||
fmt.Printf("Ошибка: номер теста %d должен быть от 1 до 4\n", num)
|
||||
printUsage()
|
||||
return []int{}
|
||||
}
|
||||
|
||||
numbers = append(numbers, num)
|
||||
}
|
||||
|
||||
return numbers
|
||||
}
|
||||
|
||||
// printUsage выводит справку по использованию
|
||||
func printUsage() {
|
||||
fmt.Println("\nИспользование:")
|
||||
fmt.Println(" go run main.go # Запустить все тесты")
|
||||
fmt.Println(" go run main.go 1 # Запустить только тест 1")
|
||||
fmt.Println(" go run main.go 1,3 # Запустить тесты 1 и 3")
|
||||
fmt.Println(" go run main.go 2,3,4 # Запустить тесты 2, 3 и 4")
|
||||
fmt.Println(" go run main.go help # Показать эту справку")
|
||||
fmt.Println("\nДоступные тесты:")
|
||||
fmt.Println(" 1 - Базовые CRUD операции")
|
||||
fmt.Println(" 2 - Сложные запросы и фильтрация")
|
||||
fmt.Println(" 3 - Работа с несколькими коллекциями")
|
||||
fmt.Println(" 4 - Тест производительности")
|
||||
fmt.Println(" 5 - HTTP API сервер")
|
||||
fmt.Println(" 6 - API endpoints")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// runAllTests запускает все тесты
|
||||
func runAllTests() {
|
||||
testBasicCRUDOperations()
|
||||
testComplexQueries()
|
||||
testMultipleCollections()
|
||||
testPerformance()
|
||||
testHTTPServer()
|
||||
testAPIEndpoints()
|
||||
}
|
||||
|
||||
// runSelectedTests запускает только выбранные тесты
|
||||
func runSelectedTests(numbers []int) {
|
||||
for _, num := range numbers {
|
||||
switch num {
|
||||
case 1:
|
||||
fmt.Println("1. Базовые CRUD операции")
|
||||
testBasicCRUDOperations()
|
||||
case 2:
|
||||
fmt.Println("2. Сложные запросы и фильтрация")
|
||||
testComplexQueries()
|
||||
case 3:
|
||||
fmt.Println("3. Работа с несколькими коллекциями")
|
||||
testMultipleCollections()
|
||||
case 4:
|
||||
fmt.Println("4. Тест производительности")
|
||||
testPerformance()
|
||||
case 5:
|
||||
fmt.Println("5. HTTP API сервер")
|
||||
testHTTPServer()
|
||||
case 6:
|
||||
fmt.Println("6. API endpoints")
|
||||
testAPIEndpoints()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testBasicCRUDOperations() {
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"isActive": true,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Вставка
|
||||
if err := Database.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("\tПользователь создан: %s\n", user["username"])
|
||||
|
||||
// Чтение
|
||||
result, err := Database.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("\tПользователей в базе: %d\n", len(result))
|
||||
|
||||
// Обновление
|
||||
updateData := map[string]any{"role": "admin"}
|
||||
updated, err := Database.Update(updateData, "users", map[string]any{"id": 1}, linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to update user: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Обновлено пользователей: %d\n", len(updated))
|
||||
|
||||
// Удаление
|
||||
deleted, err := Database.Delete(map[string]any{"id": 1}, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete user: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Удалено пользователей: %d\n", len(deleted))
|
||||
|
||||
// Проверяем, что пользователь удален
|
||||
result, err = Database.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users after delete: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Пользователей после удаления: %d\n", len(result))
|
||||
}
|
||||
|
||||
func testComplexQueries() {
|
||||
// Создаем несколько пользователей
|
||||
users := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"isActive": true,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"username": "jane_smith",
|
||||
"email": "jane@example.com",
|
||||
"isActive": true,
|
||||
"role": "admin",
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
map[string]any{
|
||||
"id": 3,
|
||||
"username": "bob_wilson",
|
||||
"email": "bob@example.com",
|
||||
"isActive": false,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := Database.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Фильтрация по роли
|
||||
admins, err := Database.ReadByFilter(map[string]any{"role": "admin"}, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to filter admins: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Администраторов: %d\n", len(admins))
|
||||
|
||||
// Фильтрация по активности
|
||||
activeUsers, err := Database.ReadByFilter(map[string]any{"isActive": true}, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to filter active users: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Активных пользователей: %d\n", len(activeUsers))
|
||||
|
||||
// Строковая фильтрация
|
||||
emailFilter, err := Database.ReadByFilter("email == 'john@example.com'", "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to filter by email: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Пользователей с email john@example.com: %d\n", len(emailFilter))
|
||||
|
||||
// Частичное совпадение
|
||||
partialMatch, err := Database.ReadByFilter(
|
||||
map[string]any{"username": "john"},
|
||||
"users",
|
||||
linedb.LineDbAdapterOptions{StrictCompare: false},
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to partial match: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Пользователей с именем содержащим 'john': %d\n", len(partialMatch))
|
||||
}
|
||||
|
||||
func testMultipleCollections() {
|
||||
// Создаем пользователя
|
||||
user := map[string]any{
|
||||
"id": 1,
|
||||
"username": "customer1",
|
||||
"email": "customer1@example.com",
|
||||
"isActive": true,
|
||||
"role": "customer",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Создаем продукты
|
||||
products := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"name": "Laptop",
|
||||
"price": 999.99,
|
||||
"category": "Electronics",
|
||||
"inStock": true,
|
||||
"sellerId": 1,
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
map[string]any{
|
||||
"id": 2,
|
||||
"name": "Mouse",
|
||||
"price": 29.99,
|
||||
"category": "Electronics",
|
||||
"inStock": true,
|
||||
"sellerId": 1,
|
||||
"createdAt": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем заказ
|
||||
order := map[string]any{
|
||||
"id": 1,
|
||||
"userId": 1,
|
||||
"productId": 1,
|
||||
"quantity": 2,
|
||||
"totalPrice": 1999.98,
|
||||
"status": "pending",
|
||||
"createdAt": time.Now().Unix(),
|
||||
"updatedAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Создаем элементы заказа
|
||||
orderItems := []any{
|
||||
map[string]any{
|
||||
"id": 1,
|
||||
"orderId": 1,
|
||||
"productId": 1,
|
||||
"quantity": 2,
|
||||
"price": 999.99,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем данные во все коллекции
|
||||
if err := Database.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Database.Insert(products, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Database.Insert(order, "orders", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert order: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Database.Insert(orderItems, "orderItems", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert order items: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем данные из всех коллекций
|
||||
users, err := Database.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
productsData, err := Database.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read products: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
orders, err := Database.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read orders: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
orderItemsData, err := Database.Read("orderItems", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read order items: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Пользователей: %d\n", len(users))
|
||||
fmt.Printf(" Продуктов: %d\n", len(productsData))
|
||||
fmt.Printf(" Заказов: %d\n", len(orders))
|
||||
fmt.Printf(" Элементов заказов: %d\n", len(orderItemsData))
|
||||
|
||||
// Сложный запрос: найти все заказы пользователя
|
||||
userOrders, err := Database.ReadByFilter(map[string]any{"userId": 1}, "orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to get user orders: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Заказов пользователя 1: %d\n", len(userOrders))
|
||||
}
|
||||
|
||||
func testPerformance() {
|
||||
// Тест производительности массовой вставки
|
||||
recordCount := 500
|
||||
fmt.Printf(" Создание %d записей для теста производительности...\n", recordCount)
|
||||
|
||||
// Создаем пользователей
|
||||
users := make([]any, recordCount)
|
||||
for i := 1; i <= recordCount; i++ {
|
||||
users[i-1] = map[string]any{
|
||||
"id": i,
|
||||
"username": fmt.Sprintf("user%d", i),
|
||||
"email": fmt.Sprintf("user%d@example.com", i),
|
||||
"isActive": i%2 == 0, // чередуем активных и неактивных
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// Измеряем время вставки
|
||||
startTime := time.Now()
|
||||
if err := Database.Insert(users, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
log.Printf("Failed to insert users: %v", err)
|
||||
return
|
||||
}
|
||||
endTime := time.Now()
|
||||
|
||||
duration := endTime.Sub(startTime)
|
||||
fmt.Printf(" Вставлено пользователей: %d\n", recordCount)
|
||||
fmt.Printf(" Время выполнения: %v\n", duration)
|
||||
fmt.Printf(" Скорость: %.2f записей/сек\n", float64(recordCount)/duration.Seconds())
|
||||
|
||||
// Тест производительности чтения
|
||||
startTime = time.Now()
|
||||
result, err := Database.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to read users: %v", err)
|
||||
return
|
||||
}
|
||||
endTime = time.Now()
|
||||
|
||||
readDuration := endTime.Sub(startTime)
|
||||
fmt.Printf(" Прочитано пользователей: %d\n", len(result))
|
||||
fmt.Printf(" Время чтения: %v\n", readDuration)
|
||||
fmt.Printf(" Скорость чтения: %.2f записей/сек\n", float64(len(result))/readDuration.Seconds())
|
||||
|
||||
// Тест производительности фильтрации
|
||||
startTime = time.Now()
|
||||
activeUsers, err := Database.ReadByFilter(map[string]any{"isActive": true}, "users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to filter active users: %v", err)
|
||||
return
|
||||
}
|
||||
endTime = time.Now()
|
||||
|
||||
filterDuration := endTime.Sub(startTime)
|
||||
fmt.Printf(" Активных пользователей: %d\n", len(activeUsers))
|
||||
fmt.Printf(" Время фильтрации: %v\n", filterDuration)
|
||||
}
|
||||
|
||||
func testHTTPServer() {
|
||||
// Настраиваем HTTP сервер
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Обработчики API
|
||||
mux.HandleFunc("/api/users", handleUsers)
|
||||
mux.HandleFunc("/api/products", handleProducts)
|
||||
mux.HandleFunc("/api/orders", handleOrders)
|
||||
|
||||
server = &http.Server{
|
||||
Addr: ":3001",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
fmt.Printf(" Запуск HTTP сервера на порту 3001...\n")
|
||||
fmt.Printf(" Доступные эндпоинты:\n")
|
||||
fmt.Printf(" - GET /api/users - получить всех пользователей\n")
|
||||
fmt.Printf(" - POST /api/users - создать пользователя\n")
|
||||
fmt.Printf(" - GET /api/products - получить все продукты\n")
|
||||
fmt.Printf(" - POST /api/products - создать продукт\n")
|
||||
fmt.Printf(" - GET /api/orders - получить все заказы\n")
|
||||
fmt.Printf(" - POST /api/orders - создать заказ\n")
|
||||
|
||||
// Запускаем сервер в горутине
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("HTTP server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Даем серверу время на запуск
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Тестируем API
|
||||
fmt.Printf(" Тестирование API...\n")
|
||||
testAPIEndpoints()
|
||||
|
||||
// Останавливаем сервер
|
||||
if err := server.Close(); err != nil {
|
||||
log.Printf("Failed to close server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
users, err := Database.Read("users", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(APIResponse{Success: true, Data: users})
|
||||
|
||||
case "POST":
|
||||
var user map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Database.Insert(user, "users", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "User created successfully"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleProducts(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
products, err := Database.Read("products", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(APIResponse{Success: true, Data: products})
|
||||
|
||||
case "POST":
|
||||
var product map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&product); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Database.Insert(product, "products", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "Product created successfully"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleOrders(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
orders, err := Database.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(APIResponse{Success: true, Data: orders})
|
||||
|
||||
case "POST":
|
||||
var order map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Database.Insert(order, "orders", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "Order created successfully"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func testAPIEndpoints() {
|
||||
// Тестируем GET /api/users
|
||||
resp, err := http.Get("http://localhost:3001/api/users")
|
||||
if err != nil {
|
||||
log.Printf("Failed to test GET /api/users: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Printf(" ✓ GET /api/users работает\n")
|
||||
} else {
|
||||
fmt.Printf(" ✗ GET /api/users вернул статус %d\n", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Тестируем POST /api/users
|
||||
userData := map[string]any{
|
||||
"id": 999,
|
||||
"username": "apitest",
|
||||
"email": "apitest@example.com",
|
||||
"isActive": true,
|
||||
"role": "user",
|
||||
"createdAt": time.Now().Unix(),
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(userData)
|
||||
resp, err = http.Post("http://localhost:3001/api/users", "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Printf("Failed to test POST /api/users: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Printf(" ✓ POST /api/users работает\n")
|
||||
} else {
|
||||
fmt.Printf(" ✗ POST /api/users вернул статус %d\n", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Очищаем тестовую папку
|
||||
os.RemoveAll("./data/test-linedb-integration")
|
||||
|
||||
// Создаем опции инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 1000,
|
||||
CacheTTL: 10 * time.Second,
|
||||
DBFolder: "./data/test-linedb-integration",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "users",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "username", "email"},
|
||||
},
|
||||
{
|
||||
CollectionName: "products",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "name", "category"},
|
||||
},
|
||||
{
|
||||
CollectionName: "orders",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "userId", "status"},
|
||||
},
|
||||
{
|
||||
CollectionName: "orderItems",
|
||||
AllocSize: 256,
|
||||
IndexedFields: []string{"id", "orderId", "productId"},
|
||||
},
|
||||
},
|
||||
Partitions: []linedb.PartitionCollection{
|
||||
{
|
||||
CollectionName: "orders",
|
||||
PartIDFnStr: "userId",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
Database = linedb.NewLineDb(nil)
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := Database.Init(true, initOptions); err != nil {
|
||||
log.Fatalf("Failed to init database: %v", err)
|
||||
}
|
||||
defer Database.Close()
|
||||
|
||||
fmt.Println("=== LineDb Integration Tests Demo ===")
|
||||
|
||||
numbers := parseTestNumbers()
|
||||
if len(numbers) > 0 {
|
||||
runSelectedTests(numbers)
|
||||
} else {
|
||||
runAllTests()
|
||||
}
|
||||
|
||||
// // Тест 1: Базовые CRUD операции
|
||||
// fmt.Println("1. Базовые CRUD операции")
|
||||
// testBasicCRUDOperations()
|
||||
|
||||
// // Тест 2: Сложные запросы и фильтрация
|
||||
// fmt.Println("\n2. Сложные запросы и фильтрация")
|
||||
// testComplexQueries()
|
||||
|
||||
// // Тест 3: Работа с несколькими коллекциями
|
||||
// fmt.Println("\n3. Работа с несколькими коллекциями")
|
||||
// testMultipleCollections()
|
||||
|
||||
// // Тест 4: Производительность
|
||||
// fmt.Println("\n4. Тест производительности")
|
||||
// testPerformance()
|
||||
|
||||
// // Тест 5: HTTP API сервер
|
||||
// fmt.Println("\n5. HTTP API сервер")
|
||||
// testHTTPServer()
|
||||
|
||||
fmt.Println("\n=== Все тесты завершены ===")
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module linedb
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/goccy/go-json v0.10.5
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
200
pkg/linedb/cache.go
Normal file
200
pkg/linedb/cache.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package linedb
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CacheEntry представляет запись в кэше
|
||||
type CacheEntry struct {
|
||||
Data any
|
||||
Timestamp time.Time
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// RecordCache представляет кэш записей
|
||||
type RecordCache struct {
|
||||
cache map[string]*CacheEntry
|
||||
mutex sync.RWMutex
|
||||
maxSize int
|
||||
ttl time.Duration
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewRecordCache создает новый кэш
|
||||
func NewRecordCache(maxSize int, ttl time.Duration) *RecordCache {
|
||||
cache := &RecordCache{
|
||||
cache: make(map[string]*CacheEntry),
|
||||
maxSize: maxSize,
|
||||
ttl: ttl,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Запускаем очистку устаревших записей только если TTL > 0
|
||||
if ttl > 0 {
|
||||
go cache.cleanupLoop()
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
// Set устанавливает значение в кэш
|
||||
func (c *RecordCache) Set(key string, value any) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
// Проверяем размер кэша
|
||||
if len(c.cache) >= c.maxSize {
|
||||
c.evictOldest()
|
||||
}
|
||||
|
||||
c.cache[key] = &CacheEntry{
|
||||
Data: value,
|
||||
Timestamp: time.Now(),
|
||||
TTL: c.ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// Get получает значение из кэша
|
||||
func (c *RecordCache) Get(key string) (any, bool) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
entry, exists := c.cache[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Проверяем TTL
|
||||
if c.ttl > 0 && time.Since(entry.Timestamp) > c.ttl {
|
||||
delete(c.cache, key)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.Data, true
|
||||
}
|
||||
|
||||
// Has проверяет наличие ключа в кэше
|
||||
func (c *RecordCache) Has(key string) bool {
|
||||
_, exists := c.Get(key)
|
||||
return exists
|
||||
}
|
||||
|
||||
// Delete удаляет ключ из кэша
|
||||
func (c *RecordCache) Delete(key string) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
delete(c.cache, key)
|
||||
}
|
||||
|
||||
// Clear очищает кэш
|
||||
func (c *RecordCache) Clear() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.cache = make(map[string]*CacheEntry)
|
||||
}
|
||||
|
||||
// Stop останавливает кэш
|
||||
func (c *RecordCache) Stop() {
|
||||
close(c.stopChan)
|
||||
}
|
||||
|
||||
// Size возвращает размер кэша
|
||||
func (c *RecordCache) Size() int {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return len(c.cache)
|
||||
}
|
||||
|
||||
// GetFlatCacheMap возвращает плоскую карту кэша
|
||||
func (c *RecordCache) GetFlatCacheMap() map[string]*CacheEntry {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
result := make(map[string]*CacheEntry)
|
||||
for key, entry := range c.cache {
|
||||
result[key] = entry
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// evictOldest удаляет самую старую запись из кэша
|
||||
func (c *RecordCache) evictOldest() {
|
||||
var oldestKey string
|
||||
var oldestTime time.Time
|
||||
|
||||
for key, entry := range c.cache {
|
||||
if oldestKey == "" || entry.Timestamp.Before(oldestTime) {
|
||||
oldestKey = key
|
||||
oldestTime = entry.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
if oldestKey != "" {
|
||||
delete(c.cache, oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupLoop запускает цикл очистки устаревших записей
|
||||
func (c *RecordCache) cleanupLoop() {
|
||||
// Используем более безопасный интервал
|
||||
interval := c.ttl / 4
|
||||
if interval < time.Second {
|
||||
interval = time.Second
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.cleanup()
|
||||
case <-c.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup очищает устаревшие записи
|
||||
func (c *RecordCache) cleanup() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for key, entry := range c.cache {
|
||||
if c.ttl > 0 && now.Sub(entry.Timestamp) > c.ttl {
|
||||
delete(c.cache, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetByRecord устанавливает запись в кэш по записи
|
||||
func (c *RecordCache) SetByRecord(record any, collectionName string) {
|
||||
key := toString(record) + ":" + collectionName
|
||||
c.Set(key, record)
|
||||
}
|
||||
|
||||
// UpdateCacheAfterInsert обновляет кэш после вставки
|
||||
func (c *RecordCache) UpdateCacheAfterInsert(record any, collectionName string) {
|
||||
c.SetByRecord(record, collectionName)
|
||||
}
|
||||
|
||||
// toString преобразует значение в строку
|
||||
func toString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case int:
|
||||
return string(rune(v))
|
||||
case float64:
|
||||
return string(rune(int(v)))
|
||||
case map[string]any:
|
||||
if id, exists := v["id"]; exists {
|
||||
return toString(id)
|
||||
}
|
||||
return "unknown"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
484
pkg/linedb/jsonl_file.go
Normal file
484
pkg/linedb/jsonl_file.go
Normal file
@@ -0,0 +1,484 @@
|
||||
package linedb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
goccyjson "github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// Функции сериализации по умолчанию (используют go-json)
|
||||
func defaultJSONMarshal(v any) ([]byte, error) {
|
||||
return goccyjson.Marshal(v)
|
||||
}
|
||||
|
||||
func defaultJSONUnmarshal(data []byte, v any) error {
|
||||
return goccyjson.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// JSONLFile представляет адаптер для работы с JSONL файлами
|
||||
type JSONLFile struct {
|
||||
filename string
|
||||
cypherKey string
|
||||
allocSize int
|
||||
collectionName string
|
||||
hashFilename string
|
||||
options JSONLFileOptions
|
||||
initialized bool
|
||||
inTransaction bool
|
||||
transaction *Transaction
|
||||
mutex sync.RWMutex
|
||||
selectCache map[string]any
|
||||
events map[string][]func(any)
|
||||
// Функции сериализации
|
||||
jsonMarshal func(any) ([]byte, error)
|
||||
jsonUnmarshal func([]byte, any) error
|
||||
}
|
||||
|
||||
// NewJSONLFile создает новый экземпляр JSONLFile
|
||||
func NewJSONLFile(filename string, cypherKey string, options JSONLFileOptions) *JSONLFile {
|
||||
hash := sha256.Sum256([]byte(filename))
|
||||
hashFilename := fmt.Sprintf("%x", hash)
|
||||
|
||||
collectionName := options.CollectionName
|
||||
if collectionName == "" {
|
||||
collectionName = hashFilename
|
||||
}
|
||||
|
||||
allocSize := options.AllocSize
|
||||
if allocSize == 0 {
|
||||
allocSize = 256
|
||||
}
|
||||
|
||||
// Определяем функции сериализации
|
||||
jsonMarshal := defaultJSONMarshal
|
||||
jsonUnmarshal := defaultJSONUnmarshal
|
||||
|
||||
// Используем пользовательские функции если они предоставлены
|
||||
if options.JSONMarshal != nil {
|
||||
jsonMarshal = options.JSONMarshal
|
||||
}
|
||||
if options.JSONUnmarshal != nil {
|
||||
jsonUnmarshal = options.JSONUnmarshal
|
||||
}
|
||||
|
||||
return &JSONLFile{
|
||||
filename: filename,
|
||||
cypherKey: cypherKey,
|
||||
allocSize: allocSize,
|
||||
collectionName: collectionName,
|
||||
hashFilename: hashFilename,
|
||||
options: options,
|
||||
selectCache: make(map[string]any),
|
||||
events: make(map[string][]func(any)),
|
||||
jsonMarshal: jsonMarshal,
|
||||
jsonUnmarshal: jsonUnmarshal,
|
||||
}
|
||||
}
|
||||
|
||||
// Init инициализирует файл
|
||||
func (j *JSONLFile) Init(force bool, options LineDbAdapterOptions) error {
|
||||
j.mutex.Lock()
|
||||
defer j.mutex.Unlock()
|
||||
|
||||
if j.initialized && !force {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Создаем директорию если не существует
|
||||
dir := filepath.Dir(j.filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Создаем файл если не существует
|
||||
if _, err := os.Stat(j.filename); os.IsNotExist(err) {
|
||||
file, err := os.Create(j.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
j.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read читает все записи из файла
|
||||
func (j *JSONLFile) Read(options LineDbAdapterOptions) ([]any, error) {
|
||||
j.mutex.RLock()
|
||||
defer j.mutex.RUnlock()
|
||||
|
||||
if !j.initialized {
|
||||
return nil, fmt.Errorf("file not initialized")
|
||||
}
|
||||
|
||||
file, err := os.Open(j.filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var records []any
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Расшифровываем если нужно
|
||||
if j.cypherKey != "" {
|
||||
decoded, err := base64.StdEncoding.DecodeString(line)
|
||||
if err != nil {
|
||||
if j.options.SkipInvalidLines {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
line = string(decoded)
|
||||
}
|
||||
|
||||
var record any
|
||||
if err := j.jsonUnmarshal([]byte(line), &record); err != nil {
|
||||
if j.options.SkipInvalidLines {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, scanner.Err()
|
||||
}
|
||||
|
||||
// Write записывает данные в файл
|
||||
func (j *JSONLFile) Write(data any, options LineDbAdapterOptions) error {
|
||||
j.mutex.Lock()
|
||||
defer j.mutex.Unlock()
|
||||
|
||||
if !j.initialized {
|
||||
return fmt.Errorf("file not initialized")
|
||||
}
|
||||
|
||||
records, ok := data.([]any)
|
||||
if !ok {
|
||||
records = []any{data}
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(j.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file for writing: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, record := range records {
|
||||
jsonData, err := j.jsonMarshal(record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
line := string(jsonData)
|
||||
|
||||
// Шифруем если нужно
|
||||
if j.cypherKey != "" {
|
||||
line = base64.StdEncoding.EncodeToString([]byte(line))
|
||||
}
|
||||
|
||||
// Дополняем до allocSize
|
||||
if len(line) < j.allocSize {
|
||||
line += strings.Repeat(" ", j.allocSize-len(line)-1)
|
||||
}
|
||||
|
||||
if _, err := file.WriteString(line + "\n"); err != nil {
|
||||
return fmt.Errorf("failed to write line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert вставляет новые записи
|
||||
func (j *JSONLFile) Insert(data any, options LineDbAdapterOptions) ([]any, error) {
|
||||
records, ok := data.([]any)
|
||||
if !ok {
|
||||
records = []any{data}
|
||||
}
|
||||
|
||||
// Генерируем ID если нужно
|
||||
for i, record := range records {
|
||||
if recordMap, ok := record.(map[string]any); ok {
|
||||
if recordMap["id"] == nil || recordMap["id"] == "" {
|
||||
recordMap["id"] = time.Now().UnixNano()
|
||||
records[i] = recordMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := j.Write(records, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// ReadByFilter читает записи по фильтру
|
||||
func (j *JSONLFile) ReadByFilter(filter any, options LineDbAdapterOptions) ([]any, error) {
|
||||
allRecords, err := j.Read(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filteredRecords []any
|
||||
|
||||
for _, record := range allRecords {
|
||||
if j.matchesFilter(record, filter, options.StrictCompare) {
|
||||
filteredRecords = append(filteredRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRecords, nil
|
||||
}
|
||||
|
||||
// Update обновляет записи
|
||||
func (j *JSONLFile) Update(data any, filter any, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Читаем все записи
|
||||
allRecords, err := j.Read(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Фильтруем записи для обновления
|
||||
var recordsToUpdate []any
|
||||
updateData, ok := data.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("update data must be a map")
|
||||
}
|
||||
|
||||
for _, record := range allRecords {
|
||||
if j.matchesFilter(record, filter, options.StrictCompare) {
|
||||
// Обновляем запись
|
||||
if recordMap, ok := record.(map[string]any); ok {
|
||||
for key, value := range updateData {
|
||||
recordMap[key] = value
|
||||
}
|
||||
recordsToUpdate = append(recordsToUpdate, recordMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Перезаписываем файл
|
||||
if err := j.rewriteFile(allRecords); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recordsToUpdate, nil
|
||||
}
|
||||
|
||||
// Delete удаляет записи
|
||||
func (j *JSONLFile) Delete(data any, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Читаем все записи
|
||||
allRecords, err := j.Read(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var remainingRecords []any
|
||||
var deletedRecords []any
|
||||
|
||||
for _, record := range allRecords {
|
||||
if j.matchesFilter(record, data, options.StrictCompare) {
|
||||
deletedRecords = append(deletedRecords, record)
|
||||
} else {
|
||||
remainingRecords = append(remainingRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
// Перезаписываем файл
|
||||
if err := j.rewriteFile(remainingRecords); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deletedRecords, nil
|
||||
}
|
||||
|
||||
// GetFilename возвращает имя файла
|
||||
func (j *JSONLFile) GetFilename() string {
|
||||
return j.filename
|
||||
}
|
||||
|
||||
// GetCollectionName возвращает имя коллекции
|
||||
func (j *JSONLFile) GetCollectionName() string {
|
||||
return j.collectionName
|
||||
}
|
||||
|
||||
// GetOptions возвращает опции
|
||||
func (j *JSONLFile) GetOptions() JSONLFileOptions {
|
||||
return j.options
|
||||
}
|
||||
|
||||
// GetEncryptKey возвращает ключ шифрования
|
||||
func (j *JSONLFile) GetEncryptKey() string {
|
||||
return j.cypherKey
|
||||
}
|
||||
|
||||
// matchesFilter проверяет соответствие записи фильтру
|
||||
func (j *JSONLFile) matchesFilter(record any, filter any, strictCompare bool) bool {
|
||||
if filter == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
switch f := filter.(type) {
|
||||
case string:
|
||||
// Простая проверка по строке
|
||||
recordStr := fmt.Sprintf("%v", record)
|
||||
if strictCompare {
|
||||
return recordStr == f
|
||||
}
|
||||
return strings.Contains(strings.ToLower(recordStr), strings.ToLower(f))
|
||||
|
||||
case map[string]any:
|
||||
// Проверка по полям
|
||||
if recordMap, ok := record.(map[string]any); ok {
|
||||
for key, filterValue := range f {
|
||||
recordValue, exists := recordMap[key]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
if !j.valuesMatch(recordValue, filterValue, strictCompare) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
case func(any) bool:
|
||||
// Функция фильтрации
|
||||
return f(record)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// valuesMatch сравнивает значения
|
||||
func (j *JSONLFile) valuesMatch(a, b any, strictCompare bool) bool {
|
||||
if strictCompare {
|
||||
return a == b
|
||||
}
|
||||
|
||||
// Для строк - нечувствительное к регистру сравнение
|
||||
if aStr, ok := a.(string); ok {
|
||||
if bStr, ok := b.(string); ok {
|
||||
return strings.Contains(strings.ToLower(aStr), strings.ToLower(bStr))
|
||||
}
|
||||
}
|
||||
|
||||
return a == b
|
||||
}
|
||||
|
||||
// rewriteFile перезаписывает файл новыми данными
|
||||
func (j *JSONLFile) rewriteFile(records []any) error {
|
||||
// Создаем временный файл
|
||||
tempFile := j.filename + ".tmp"
|
||||
|
||||
file, err := os.Create(tempFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Записываем данные во временный файл
|
||||
for _, record := range records {
|
||||
jsonData, err := j.jsonMarshal(record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
line := string(jsonData)
|
||||
|
||||
// Шифруем если нужно
|
||||
if j.cypherKey != "" {
|
||||
line = base64.StdEncoding.EncodeToString([]byte(line))
|
||||
}
|
||||
|
||||
// Дополняем до allocSize
|
||||
if len(line) < j.allocSize {
|
||||
line += strings.Repeat(" ", j.allocSize-len(line)-1)
|
||||
}
|
||||
|
||||
if _, err := file.WriteString(line + "\n"); err != nil {
|
||||
return fmt.Errorf("failed to write line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Заменяем оригинальный файл
|
||||
if err := os.Rename(tempFile, j.filename); err != nil {
|
||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy очищает ресурсы
|
||||
func (j *JSONLFile) Destroy() {
|
||||
j.mutex.Lock()
|
||||
defer j.mutex.Unlock()
|
||||
|
||||
j.initialized = false
|
||||
j.selectCache = nil
|
||||
j.events = nil
|
||||
}
|
||||
|
||||
// WithTransaction выполняет операцию в транзакции
|
||||
func (j *JSONLFile) WithTransaction(callback func(*JSONLFile, LineDbAdapterOptions) error, transactionOptions TransactionOptions, methodsOptions LineDbAdapterOptions) error {
|
||||
// Создаем транзакцию
|
||||
tx := NewTransaction("write", generateTransactionID(), transactionOptions.Timeout, transactionOptions.Rollback)
|
||||
|
||||
// Создаем резервную копию если нужно
|
||||
if transactionOptions.Rollback {
|
||||
if err := tx.CreateBackup(j.filename); err != nil {
|
||||
return fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
j.transaction = tx
|
||||
j.inTransaction = true
|
||||
|
||||
defer func() {
|
||||
j.inTransaction = false
|
||||
j.transaction = nil
|
||||
}()
|
||||
|
||||
// Выполняем callback
|
||||
if err := callback(j, methodsOptions); err != nil {
|
||||
// Откатываем изменения если нужно
|
||||
if transactionOptions.Rollback {
|
||||
if restoreErr := tx.RestoreFromBackup(j.filename); restoreErr != nil {
|
||||
return fmt.Errorf("failed to restore from backup: %w", restoreErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Очищаем резервную копию
|
||||
if err := tx.CleanupBackup(); err != nil {
|
||||
return fmt.Errorf("failed to cleanup backup: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTransactionID генерирует ID транзакции
|
||||
func generateTransactionID() string {
|
||||
return fmt.Sprintf("tx_%d", time.Now().UnixNano())
|
||||
}
|
||||
75
pkg/linedb/last_id_manager.go
Normal file
75
pkg/linedb/last_id_manager.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package linedb
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LastIDManager управляет последними ID для коллекций
|
||||
type LastIDManager struct {
|
||||
lastIDs map[string]int
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
var lastIDManagerInstance *LastIDManager
|
||||
var lastIDManagerOnce sync.Once
|
||||
|
||||
// GetInstance возвращает единственный экземпляр LastIDManager
|
||||
func GetLastIDManagerInstance() *LastIDManager {
|
||||
lastIDManagerOnce.Do(func() {
|
||||
lastIDManagerInstance = &LastIDManager{
|
||||
lastIDs: make(map[string]int),
|
||||
}
|
||||
})
|
||||
return lastIDManagerInstance
|
||||
}
|
||||
|
||||
// GetLastID получает последний ID для коллекции
|
||||
func (l *LastIDManager) GetLastID(filename string) int {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
|
||||
baseFileName := l.getBaseFileName(filename)
|
||||
return l.lastIDs[baseFileName]
|
||||
}
|
||||
|
||||
// SetLastID устанавливает последний ID для коллекции
|
||||
func (l *LastIDManager) SetLastID(filename string, id int) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
baseFileName := l.getBaseFileName(filename)
|
||||
currentID := l.lastIDs[baseFileName]
|
||||
if currentID < id {
|
||||
l.lastIDs[baseFileName] = id
|
||||
}
|
||||
}
|
||||
|
||||
// IncrementLastID увеличивает последний ID для коллекции
|
||||
func (l *LastIDManager) IncrementLastID(filename string) int {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
baseFileName := l.getBaseFileName(filename)
|
||||
currentID := l.lastIDs[baseFileName]
|
||||
newID := currentID + 1
|
||||
l.lastIDs[baseFileName] = newID
|
||||
return newID
|
||||
}
|
||||
|
||||
// getBaseFileName извлекает базовое имя файла
|
||||
func (l *LastIDManager) getBaseFileName(filename string) string {
|
||||
if idx := l.findPartitionSeparator(filename); idx != -1 {
|
||||
return filename[:idx]
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
// findPartitionSeparator находит разделитель партиции
|
||||
func (l *LastIDManager) findPartitionSeparator(filename string) int {
|
||||
for i, char := range filename {
|
||||
if char == '_' {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
962
pkg/linedb/line_db.go
Normal file
962
pkg/linedb/line_db.go
Normal file
@@ -0,0 +1,962 @@
|
||||
package linedb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LineDb представляет основную базу данных
|
||||
// Соответствует TypeScript классу LineDb
|
||||
type LineDb struct {
|
||||
adapters map[string]*JSONLFile
|
||||
collections map[string]string
|
||||
partitionFunctions map[string]func(any) string
|
||||
mutex sync.RWMutex
|
||||
cacheSize int
|
||||
cacheExternal *RecordCache
|
||||
nextIDFn func(any, string) (any, error)
|
||||
lastIDManager *LastIDManager
|
||||
// inTransaction bool
|
||||
cacheTTL time.Duration
|
||||
constructorOptions *LineDbOptions
|
||||
initOptions *LineDbInitOptions
|
||||
}
|
||||
|
||||
// NewLineDb создает новый экземпляр LineDb
|
||||
func NewLineDb(options *LineDbOptions, adapters ...*JSONLFile) *LineDb {
|
||||
if options == nil {
|
||||
options = &LineDbOptions{}
|
||||
}
|
||||
|
||||
db := &LineDb{
|
||||
adapters: make(map[string]*JSONLFile),
|
||||
collections: make(map[string]string),
|
||||
partitionFunctions: make(map[string]func(any) string),
|
||||
cacheSize: options.CacheSize,
|
||||
cacheTTL: options.CacheTTL,
|
||||
lastIDManager: GetLastIDManagerInstance(),
|
||||
constructorOptions: options,
|
||||
}
|
||||
|
||||
// Инициализируем кэш если нужно
|
||||
if db.cacheSize > 0 && db.cacheTTL > 0 {
|
||||
db.cacheExternal = NewRecordCache(db.cacheSize, db.cacheTTL)
|
||||
}
|
||||
|
||||
// Добавляем готовые адаптеры
|
||||
for _, adapter := range adapters {
|
||||
collectionName := adapter.GetCollectionName()
|
||||
db.adapters[collectionName] = adapter
|
||||
db.collections[collectionName] = adapter.GetFilename()
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// Init инициализирует базу данных
|
||||
func (db *LineDb) Init(force bool, initOptions *LineDbInitOptions) error {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
|
||||
if initOptions == nil {
|
||||
return fmt.Errorf("no init options provided")
|
||||
}
|
||||
|
||||
// Устанавливаем опции
|
||||
db.initOptions = initOptions
|
||||
db.cacheSize = initOptions.CacheSize
|
||||
db.cacheTTL = initOptions.CacheTTL
|
||||
|
||||
// Инициализируем кэш если нужно
|
||||
if db.cacheSize > 0 && db.cacheTTL > 0 {
|
||||
db.cacheExternal = NewRecordCache(db.cacheSize, db.cacheTTL)
|
||||
}
|
||||
|
||||
// Создаем папку базы данных
|
||||
dbFolder := initOptions.DBFolder
|
||||
if dbFolder == "" {
|
||||
dbFolder = "linedb"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dbFolder, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create database folder: %w", err)
|
||||
}
|
||||
|
||||
// Сохраняем функции партиционирования
|
||||
for _, partition := range initOptions.Partitions {
|
||||
if partition.PartIDFn != nil {
|
||||
db.partitionFunctions[partition.CollectionName] = partition.PartIDFn
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем адаптеры для коллекций
|
||||
for i, adapterOptions := range initOptions.Collections {
|
||||
collectionName := adapterOptions.CollectionName
|
||||
if collectionName == "" {
|
||||
collectionName = fmt.Sprintf("collection_%d", i+1)
|
||||
}
|
||||
|
||||
// Создаем путь к файлу
|
||||
filename := filepath.Join(dbFolder, collectionName+".jsonl")
|
||||
|
||||
// Создаем адаптер
|
||||
adapter := NewJSONLFile(filename, adapterOptions.EncryptKeyForLineDb, adapterOptions)
|
||||
|
||||
// Инициализируем адаптер
|
||||
if err := adapter.Init(force, LineDbAdapterOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to init adapter for collection %s: %w", collectionName, err)
|
||||
}
|
||||
|
||||
// Добавляем в карту адаптеров
|
||||
db.adapters[collectionName] = adapter
|
||||
db.collections[collectionName] = filename
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read читает все записи из коллекции
|
||||
func (db *LineDb) Read(collectionName string, options LineDbAdapterOptions) ([]any, error) {
|
||||
db.mutex.RLock()
|
||||
defer db.mutex.RUnlock()
|
||||
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
adapter, exists := db.adapters[collectionName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("collection %s not found", collectionName)
|
||||
}
|
||||
|
||||
return adapter.Read(options)
|
||||
}
|
||||
|
||||
// Insert вставляет новые записи в коллекцию
|
||||
func (db *LineDb) Insert(data any, collectionName string, options LineDbAdapterOptions) error {
|
||||
// Блокируем только если не в транзакции
|
||||
if !options.InTransaction {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
}
|
||||
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
// Проверяем debug tag
|
||||
if options.DebugTag == "error" {
|
||||
return fmt.Errorf("test error")
|
||||
}
|
||||
|
||||
// Обрабатываем данные
|
||||
dataArray := db.normalizeDataArray(data)
|
||||
resultDataArray := make([]any, 0, len(dataArray))
|
||||
|
||||
for _, item := range dataArray {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid data format")
|
||||
}
|
||||
|
||||
// Генерируем ID если отсутствует
|
||||
if itemMap["id"] == nil || db.isInvalidID(itemMap["id"]) {
|
||||
newID, err := db.NextID(item, collectionName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate ID: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем уникальность ID
|
||||
done := false
|
||||
count := 0
|
||||
for !done && count < 10000 {
|
||||
// Проверяем, что ID не существует в результатах
|
||||
exists := false
|
||||
for _, resultItem := range resultDataArray {
|
||||
if resultMap, ok := resultItem.(map[string]any); ok {
|
||||
if resultMap["id"] == newID {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
done = true
|
||||
} else {
|
||||
newID, err = db.NextID(item, collectionName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate unique ID: %w", err)
|
||||
}
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
if count >= 10000 {
|
||||
return fmt.Errorf("can not generate new id for 10 000 iterations")
|
||||
}
|
||||
|
||||
itemMap["id"] = newID
|
||||
resultDataArray = append(resultDataArray, itemMap)
|
||||
} else {
|
||||
// Проверяем существование записи если не пропускаем проверку
|
||||
if !options.SkipCheckExistingForWrite {
|
||||
filter := map[string]any{"id": itemMap["id"]}
|
||||
|
||||
for key, partitionAdapter := range db.adapters {
|
||||
if strings.Contains(key, collectionName) {
|
||||
exists, err := partitionAdapter.ReadByFilter(filter, LineDbAdapterOptions{InTransaction: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing record: %w", err)
|
||||
}
|
||||
if len(exists) > 0 {
|
||||
return fmt.Errorf("record with id %v already exists in collection %s", collectionName, itemMap["id"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resultDataArray = append(resultDataArray, itemMap)
|
||||
}
|
||||
}
|
||||
|
||||
// Записываем данные с флагом транзакции
|
||||
writeOptions := LineDbAdapterOptions{InTransaction: true, InternalCall: true}
|
||||
if err := db.Write(resultDataArray, collectionName, writeOptions); err != nil {
|
||||
return fmt.Errorf("failed to write data: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем кэш
|
||||
if db.cacheExternal != nil {
|
||||
for _, item := range resultDataArray {
|
||||
db.cacheExternal.UpdateCacheAfterInsert(item, collectionName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write записывает данные в коллекцию
|
||||
func (db *LineDb) Write(data any, collectionName string, options LineDbAdapterOptions) error {
|
||||
// Блокируем только если не в транзакции
|
||||
if !options.InTransaction {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
}
|
||||
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
// Проверяем партиционирование
|
||||
if db.isCollectionPartitioned(collectionName) {
|
||||
dataArray := db.normalizeDataArray(data)
|
||||
for _, item := range dataArray {
|
||||
adapter, err := db.getPartitionAdapter(item, collectionName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get partition adapter: %w", err)
|
||||
}
|
||||
|
||||
if err := adapter.Write(item, options); err != nil {
|
||||
return fmt.Errorf("failed to write to partition: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Обычная запись
|
||||
adapter, exists := db.adapters[collectionName]
|
||||
if !exists {
|
||||
return fmt.Errorf("collection %s not found", collectionName)
|
||||
}
|
||||
|
||||
return adapter.Write(data, options)
|
||||
}
|
||||
|
||||
// Update обновляет записи в коллекции
|
||||
func (db *LineDb) Update(data any, collectionName string, filter any, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Блокируем только если не в транзакции
|
||||
if !options.InTransaction {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
}
|
||||
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
// Проверяем конфликт ID
|
||||
if dataMap, ok := data.(map[string]any); ok {
|
||||
if filterMap, ok := filter.(map[string]any); ok {
|
||||
if dataMap["id"] != nil && filterMap["id"] != nil {
|
||||
if !db.compareIDs(dataMap["id"], filterMap["id"]) {
|
||||
return nil, fmt.Errorf("you can not update record id with filter by another id. Use delete and insert instead")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем партиционирование
|
||||
if db.isCollectionPartitioned(collectionName) {
|
||||
return db.updatePartitioned(data, collectionName, filter, options)
|
||||
}
|
||||
|
||||
// Обычное обновление
|
||||
adapter, exists := db.adapters[collectionName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("collection %s not found", collectionName)
|
||||
}
|
||||
|
||||
return adapter.Update(data, filter, options)
|
||||
}
|
||||
|
||||
// Delete удаляет записи из коллекции
|
||||
func (db *LineDb) Delete(data any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Блокируем только если не в транзакции
|
||||
if !options.InTransaction {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
}
|
||||
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
// Проверяем партиционирование
|
||||
if db.isCollectionPartitioned(collectionName) {
|
||||
return db.deletePartitioned(data, collectionName, options)
|
||||
}
|
||||
|
||||
// Обычное удаление
|
||||
adapter, exists := db.adapters[collectionName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("collection %s not found", collectionName)
|
||||
}
|
||||
|
||||
return adapter.Delete(data, options)
|
||||
}
|
||||
|
||||
// Select выполняет выборку с поддержкой цепочки
|
||||
func (db *LineDb) Select(filter any, collectionName string, options LineDbAdapterOptions) (any, error) {
|
||||
result, err := db.ReadByFilter(filter, collectionName, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options.ReturnChain {
|
||||
return NewCollectionChain(result), nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReadByFilter читает записи по фильтру
|
||||
func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
|
||||
db.mutex.RLock()
|
||||
defer db.mutex.RUnlock()
|
||||
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
// Проверяем кэш
|
||||
if db.cacheExternal != nil && !options.InTransaction {
|
||||
if cached, exists := db.cacheExternal.Get(db.generateCacheKey(filter, collectionName)); exists {
|
||||
if cachedArray, ok := cached.([]any); ok {
|
||||
return cachedArray, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем партиционирование
|
||||
if db.isCollectionPartitioned(collectionName) {
|
||||
return db.readByFilterPartitioned(filter, collectionName, options)
|
||||
}
|
||||
|
||||
// Обычная фильтрация
|
||||
adapter, exists := db.adapters[collectionName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("collection %s not found", collectionName)
|
||||
}
|
||||
|
||||
result, err := adapter.ReadByFilter(filter, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем кэш
|
||||
if db.cacheExternal != nil && !options.InTransaction {
|
||||
db.cacheExternal.Set(db.generateCacheKey(filter, collectionName), result)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// NextID генерирует следующий ID
|
||||
func (db *LineDb) NextID(data any, collectionName string) (any, error) {
|
||||
if db.nextIDFn != nil {
|
||||
return db.nextIDFn(data, collectionName)
|
||||
}
|
||||
|
||||
// Используем LastIDManager по умолчанию
|
||||
lastID := db.lastIDManager.GetLastID(collectionName)
|
||||
newID := lastID + 1
|
||||
db.lastIDManager.SetLastID(collectionName, newID)
|
||||
return newID, nil
|
||||
}
|
||||
|
||||
// LastSequenceID возвращает последний последовательный ID
|
||||
func (db *LineDb) LastSequenceID(collectionName string) int {
|
||||
if collectionName == "" {
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
return db.lastIDManager.GetLastID(collectionName)
|
||||
}
|
||||
|
||||
// ClearCache очищает кэш
|
||||
func (db *LineDb) ClearCache(collectionName string, options LineDbAdapterOptions) error {
|
||||
// Блокируем только если не в транзакции
|
||||
if !options.InTransaction {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
}
|
||||
|
||||
if db.cacheExternal != nil {
|
||||
if collectionName == "" {
|
||||
db.cacheExternal.Clear()
|
||||
} else {
|
||||
// Очищаем только записи для конкретной коллекции
|
||||
// Это упрощенная реализация
|
||||
db.cacheExternal.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает базу данных
|
||||
func (db *LineDb) Close() {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
|
||||
// Закрываем все адаптеры
|
||||
for _, adapter := range db.adapters {
|
||||
adapter.Destroy()
|
||||
}
|
||||
|
||||
// Останавливаем и очищаем кэш
|
||||
if db.cacheExternal != nil {
|
||||
db.cacheExternal.Stop()
|
||||
db.cacheExternal.Clear()
|
||||
}
|
||||
|
||||
// Очищаем карты
|
||||
db.adapters = make(map[string]*JSONLFile)
|
||||
db.collections = make(map[string]string)
|
||||
db.partitionFunctions = make(map[string]func(any) string)
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
|
||||
func (db *LineDb) getFirstCollection() string {
|
||||
for name := range db.adapters {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (db *LineDb) getBaseCollectionName(collectionName string) string {
|
||||
if idx := strings.Index(collectionName, "_"); idx != -1 {
|
||||
return collectionName[:idx]
|
||||
}
|
||||
return collectionName
|
||||
}
|
||||
|
||||
func (db *LineDb) isCollectionPartitioned(collectionName string) bool {
|
||||
_, exists := db.partitionFunctions[collectionName]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (db *LineDb) getPartitionFiles(collectionName string) ([]string, error) {
|
||||
baseName := db.getBaseCollectionName(collectionName)
|
||||
var files []string
|
||||
|
||||
for name, filename := range db.collections {
|
||||
if strings.HasPrefix(name, baseName+"_") {
|
||||
files = append(files, filename)
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (db *LineDb) getPartitionAdapter(data any, collectionName string) (*JSONLFile, error) {
|
||||
partitionFn, exists := db.partitionFunctions[collectionName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("partition function not found for collection %s", collectionName)
|
||||
}
|
||||
|
||||
partitionID := partitionFn(data)
|
||||
partitionName := fmt.Sprintf("%s_%s", collectionName, partitionID)
|
||||
|
||||
adapter, exists := db.adapters[partitionName]
|
||||
if !exists {
|
||||
// Создаем новый адаптер для партиции
|
||||
filename := filepath.Join(db.initOptions.DBFolder, partitionName+".jsonl")
|
||||
adapter = NewJSONLFile(filename, "", JSONLFileOptions{CollectionName: partitionName})
|
||||
|
||||
if err := adapter.Init(false, LineDbAdapterOptions{}); err != nil {
|
||||
return nil, fmt.Errorf("failed to init partition adapter: %w", err)
|
||||
}
|
||||
|
||||
db.adapters[partitionName] = adapter
|
||||
db.collections[partitionName] = filename
|
||||
}
|
||||
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
func (db *LineDb) GetMaxID(records []any) int {
|
||||
maxID := 0
|
||||
for _, record := range records {
|
||||
if recordMap, ok := record.(map[string]any); ok {
|
||||
if id, ok := recordMap["id"]; ok {
|
||||
if idInt, ok := id.(int); ok && idInt > maxID {
|
||||
maxID = idInt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxID
|
||||
}
|
||||
|
||||
func (db *LineDb) matchesFilter(record any, filter any, strictCompare bool) bool {
|
||||
if recordMap, ok := record.(map[string]any); ok {
|
||||
if filterMap, ok := filter.(map[string]any); ok {
|
||||
for key, filterValue := range filterMap {
|
||||
if recordValue, exists := recordMap[key]; exists {
|
||||
if !db.valuesMatch(recordValue, filterValue, strictCompare) {
|
||||
return false
|
||||
}
|
||||
} else if strictCompare {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *LineDb) valuesMatch(a, b any, strictCompare bool) bool {
|
||||
if strictCompare {
|
||||
return a == b
|
||||
}
|
||||
|
||||
// Нестрогое сравнение
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
// Сравнение строк
|
||||
if aStr, ok := a.(string); ok {
|
||||
if bStr, ok := b.(string); ok {
|
||||
return strings.EqualFold(aStr, bStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Сравнение чисел
|
||||
if aNum, ok := db.toNumber(a); ok {
|
||||
if bNum, ok := db.toNumber(b); ok {
|
||||
return aNum == bNum
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *LineDb) toNumber(value any) (float64, bool) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return float64(v), true
|
||||
case int64:
|
||||
return float64(v), true
|
||||
case float64:
|
||||
return v, true
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (db *LineDb) normalizeDataArray(data any) []any {
|
||||
switch v := data.(type) {
|
||||
case []any:
|
||||
return v
|
||||
case any:
|
||||
return []any{v}
|
||||
default:
|
||||
return []any{data}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *LineDb) isInvalidID(id any) bool {
|
||||
if id == nil {
|
||||
return true
|
||||
}
|
||||
if idNum, ok := id.(int); ok {
|
||||
return idNum <= -1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *LineDb) compareIDs(a, b any) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
func (db *LineDb) generateCacheKey(filter any, collectionName string) string {
|
||||
// Упрощенная реализация генерации ключа кэша
|
||||
return fmt.Sprintf("%s:%v", collectionName, filter)
|
||||
}
|
||||
|
||||
func (db *LineDb) updatePartitioned(data any, collectionName string, filter any, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Получаем все партиции
|
||||
partitionFiles, err := db.getPartitionFiles(collectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allResults []any
|
||||
for _, filename := range partitionFiles {
|
||||
// Находим адаптер по имени файла
|
||||
var adapter *JSONLFile
|
||||
for name, adapterFile := range db.collections {
|
||||
if adapterFile == filename {
|
||||
adapter = db.adapters[name]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if adapter != nil {
|
||||
results, err := adapter.Update(data, filter, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allResults = append(allResults, results...)
|
||||
}
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
func (db *LineDb) deletePartitioned(data any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Получаем все партиции
|
||||
partitionFiles, err := db.getPartitionFiles(collectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allResults []any
|
||||
for _, filename := range partitionFiles {
|
||||
// Находим адаптер по имени файла
|
||||
var adapter *JSONLFile
|
||||
for name, adapterFile := range db.collections {
|
||||
if adapterFile == filename {
|
||||
adapter = db.adapters[name]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if adapter != nil {
|
||||
results, err := adapter.Delete(data, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allResults = append(allResults, results...)
|
||||
}
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
func (db *LineDb) readByFilterPartitioned(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error) {
|
||||
// Получаем все партиции
|
||||
partitionFiles, err := db.getPartitionFiles(collectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allResults []any
|
||||
for _, filename := range partitionFiles {
|
||||
// Находим адаптер по имени файла
|
||||
var adapter *JSONLFile
|
||||
for name, adapterFile := range db.collections {
|
||||
if adapterFile == filename {
|
||||
adapter = db.adapters[name]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if adapter != nil {
|
||||
results, err := adapter.ReadByFilter(filter, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allResults = append(allResults, results...)
|
||||
}
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
// Добавляем недостающие методы
|
||||
|
||||
// SelectWithPagination выполняет выборку с пагинацией
|
||||
func (db *LineDb) SelectWithPagination(filter any, page, limit int, collectionName string, options LineDbAdapterOptions) (*PaginatedResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
// Получаем все данные
|
||||
allData, err := db.ReadByFilter(filter, collectionName, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := len(allData)
|
||||
pages := (total + limit - 1) / limit // Округление вверх
|
||||
|
||||
// Вычисляем индексы для пагинации
|
||||
start := (page - 1) * limit
|
||||
end := start + limit
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
var data []any
|
||||
if start < total {
|
||||
data = allData[start:end]
|
||||
}
|
||||
|
||||
return &PaginatedResult{
|
||||
Data: data,
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Pages: pages,
|
||||
Page: page,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Join выполняет операцию JOIN между коллекциями
|
||||
func (db *LineDb) Join(leftCollection, rightCollection any, options JoinOptions) (*CollectionChain, error) {
|
||||
// Получаем данные левой коллекции
|
||||
var leftData []any
|
||||
switch v := leftCollection.(type) {
|
||||
case string:
|
||||
data, err := db.Read(v, LineDbAdapterOptions{InTransaction: options.InTransaction})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
leftData = data
|
||||
case []any:
|
||||
leftData = v
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid left collection type")
|
||||
}
|
||||
|
||||
// Получаем данные правой коллекции
|
||||
var rightData []any
|
||||
switch v := rightCollection.(type) {
|
||||
case string:
|
||||
data, err := db.Read(v, LineDbAdapterOptions{InTransaction: options.InTransaction})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rightData = data
|
||||
case []any:
|
||||
rightData = v
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid right collection type")
|
||||
}
|
||||
|
||||
// Применяем фильтры
|
||||
if options.LeftFilter != nil {
|
||||
leftData = db.applyFilter(leftData, options.LeftFilter, options.StrictCompare)
|
||||
}
|
||||
if options.RightFilter != nil {
|
||||
rightData = db.applyFilter(rightData, options.RightFilter, options.StrictCompare)
|
||||
}
|
||||
|
||||
// Выполняем JOIN
|
||||
var result []any
|
||||
switch options.Type {
|
||||
case JoinTypeInner:
|
||||
result = db.innerJoin(leftData, rightData, options)
|
||||
case JoinTypeLeft:
|
||||
result = db.leftJoin(leftData, rightData, options)
|
||||
case JoinTypeRight:
|
||||
result = db.rightJoin(leftData, rightData, options)
|
||||
case JoinTypeFull:
|
||||
result = db.fullJoin(leftData, rightData, options)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported join type: %s", options.Type)
|
||||
}
|
||||
|
||||
return NewCollectionChain(result), nil
|
||||
}
|
||||
|
||||
func (db *LineDb) applyFilter(data []any, filter map[string]any, strictCompare bool) []any {
|
||||
var result []any
|
||||
for _, item := range data {
|
||||
if db.matchesFilter(item, filter, strictCompare) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (db *LineDb) innerJoin(leftData, rightData []any, options JoinOptions) []any {
|
||||
var result []any
|
||||
|
||||
for _, left := range leftData {
|
||||
for _, right := range rightData {
|
||||
if db.matchJoinFields(left, right, options.LeftFields, options.RightFields, options.StrictCompare) {
|
||||
result = append(result, JoinResult{Left: left, Right: right})
|
||||
if options.OnlyOneFromRight {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (db *LineDb) leftJoin(leftData, rightData []any, options JoinOptions) []any {
|
||||
var result []any
|
||||
|
||||
for _, left := range leftData {
|
||||
matched := false
|
||||
for _, right := range rightData {
|
||||
if db.matchJoinFields(left, right, options.LeftFields, options.RightFields, options.StrictCompare) {
|
||||
result = append(result, JoinResult{Left: left, Right: right})
|
||||
matched = true
|
||||
if options.OnlyOneFromRight {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
result = append(result, JoinResult{Left: left, Right: nil})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (db *LineDb) rightJoin(leftData, rightData []any, options JoinOptions) []any {
|
||||
var result []any
|
||||
|
||||
for _, right := range rightData {
|
||||
matched := false
|
||||
for _, left := range leftData {
|
||||
if db.matchJoinFields(left, right, options.LeftFields, options.RightFields, options.StrictCompare) {
|
||||
result = append(result, JoinResult{Left: left, Right: right})
|
||||
matched = true
|
||||
if options.OnlyOneFromRight {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
result = append(result, JoinResult{Left: nil, Right: right})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (db *LineDb) fullJoin(leftData, rightData []any, options JoinOptions) []any {
|
||||
// Объединяем LEFT и RIGHT JOIN
|
||||
leftResult := db.leftJoin(leftData, rightData, options)
|
||||
rightResult := db.rightJoin(leftData, rightData, options)
|
||||
|
||||
// Удаляем дубликаты
|
||||
seen := make(map[string]bool)
|
||||
var result []any
|
||||
|
||||
for _, item := range append(leftResult, rightResult...) {
|
||||
key := db.generateJoinKey(item)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (db *LineDb) matchJoinFields(left, right any, leftFields, rightFields []string, strictCompare bool) bool {
|
||||
if len(leftFields) != len(rightFields) {
|
||||
return false
|
||||
}
|
||||
|
||||
leftMap, leftOk := left.(map[string]any)
|
||||
rightMap, rightOk := right.(map[string]any)
|
||||
|
||||
if !leftOk || !rightOk {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, leftField := range leftFields {
|
||||
rightField := rightFields[i]
|
||||
leftValue := leftMap[leftField]
|
||||
rightValue := rightMap[rightField]
|
||||
|
||||
if !db.valuesMatch(leftValue, rightValue, strictCompare) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (db *LineDb) generateJoinKey(item any) string {
|
||||
// Упрощенная реализация генерации ключа для JOIN
|
||||
if joinResult, ok := item.(JoinResult); ok {
|
||||
return fmt.Sprintf("%v:%v", joinResult.Left, joinResult.Right)
|
||||
}
|
||||
return fmt.Sprintf("%v", item)
|
||||
}
|
||||
|
||||
// Getter методы для совместимости с TypeScript версией
|
||||
|
||||
func (db *LineDb) GetActualCacheSize() int {
|
||||
if db.cacheExternal != nil {
|
||||
return db.cacheExternal.Size()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (db *LineDb) GetLimitCacheSize() int {
|
||||
return db.cacheSize
|
||||
}
|
||||
|
||||
func (db *LineDb) GetCacheMap() map[string]*CacheEntry {
|
||||
if db.cacheExternal != nil {
|
||||
return db.cacheExternal.GetFlatCacheMap()
|
||||
}
|
||||
return make(map[string]*CacheEntry)
|
||||
}
|
||||
|
||||
func (db *LineDb) GetFirstCollection() string {
|
||||
return db.getFirstCollection()
|
||||
}
|
||||
147
pkg/linedb/transaction.go
Normal file
147
pkg/linedb/transaction.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package linedb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Transaction представляет транзакцию
|
||||
type Transaction struct {
|
||||
transactionMode string
|
||||
transactionID string
|
||||
timeoutMs int
|
||||
timeoutID *time.Timer
|
||||
rollback bool
|
||||
backupFile string
|
||||
doNotDeleteBackupFile bool
|
||||
// mutex sync.RWMutex
|
||||
active bool
|
||||
}
|
||||
|
||||
// NewTransaction создает новую транзакцию
|
||||
func NewTransaction(mode string, id string, timeout int, rollback bool) *Transaction {
|
||||
tx := &Transaction{
|
||||
transactionMode: mode,
|
||||
transactionID: id,
|
||||
timeoutMs: timeout,
|
||||
rollback: rollback,
|
||||
active: true,
|
||||
}
|
||||
|
||||
if timeout > 0 {
|
||||
tx.timeoutID = time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() {
|
||||
tx.active = false
|
||||
})
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
// ClearTimeout очищает таймаут транзакции
|
||||
func (t *Transaction) ClearTimeout() {
|
||||
if t.timeoutID != nil {
|
||||
t.timeoutID.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// IsActive проверяет, является ли транзакция активной
|
||||
func (t *Transaction) IsActive() bool {
|
||||
return t.active
|
||||
}
|
||||
|
||||
// IsReadMode проверяет, является ли транзакция режимом чтения
|
||||
func (t *Transaction) IsReadMode() bool {
|
||||
return t.transactionMode == "read"
|
||||
}
|
||||
|
||||
// IsWriteMode проверяет, является ли транзакция режимом записи
|
||||
func (t *Transaction) IsWriteMode() bool {
|
||||
return t.transactionMode == "write"
|
||||
}
|
||||
|
||||
// ShouldRollback проверяет, требуется ли откат транзакции при ошибке
|
||||
func (t *Transaction) ShouldRollback() bool {
|
||||
return t.rollback
|
||||
}
|
||||
|
||||
// ShouldKeepBackup проверяет, нужно ли сохранять резервную копию
|
||||
func (t *Transaction) ShouldKeepBackup() bool {
|
||||
return !t.doNotDeleteBackupFile
|
||||
}
|
||||
|
||||
// GetBackupFile получает путь к файлу резервной копии
|
||||
func (t *Transaction) GetBackupFile() string {
|
||||
return t.backupFile
|
||||
}
|
||||
|
||||
// SetBackupFile устанавливает путь к файлу резервной копии
|
||||
func (t *Transaction) SetBackupFile(path string) {
|
||||
t.backupFile = path
|
||||
}
|
||||
|
||||
// CreateBackup создает резервную копию файла
|
||||
func (t *Transaction) CreateBackup(filename string) error {
|
||||
if filename == "" {
|
||||
return fmt.Errorf("filename is required")
|
||||
}
|
||||
|
||||
// Создаем резервную копию
|
||||
backupFile := filename + ".backup"
|
||||
|
||||
// Копируем файл
|
||||
src, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(backupFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create backup file: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = dst.ReadFrom(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file: %w", err)
|
||||
}
|
||||
|
||||
t.backupFile = backupFile
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreFromBackup восстанавливает из резервной копии
|
||||
func (t *Transaction) RestoreFromBackup(filename string) error {
|
||||
if t.backupFile == "" {
|
||||
return fmt.Errorf("no backup file available")
|
||||
}
|
||||
|
||||
// Копируем резервную копию обратно
|
||||
src, err := os.Open(t.backupFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open backup file: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create target file: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = dst.ReadFrom(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy backup: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupBackup удаляет резервную копию
|
||||
func (t *Transaction) CleanupBackup() error {
|
||||
if t.backupFile != "" && !t.doNotDeleteBackupFile {
|
||||
return os.Remove(t.backupFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
280
pkg/linedb/types.go
Normal file
280
pkg/linedb/types.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package linedb
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// LineDbAdapter представляет базовый интерфейс для записей в базе данных
|
||||
// Соответствует TypeScript интерфейсу LineDbAdapter
|
||||
type LineDbAdapter interface {
|
||||
GetID() any
|
||||
SetID(id any)
|
||||
GetTimestamp() *time.Time
|
||||
SetTimestamp(timestamp *time.Time)
|
||||
}
|
||||
|
||||
// BaseRecord представляет базовую структуру записи
|
||||
type BaseRecord struct {
|
||||
ID any `json:"id"`
|
||||
Timestamp *time.Time `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
func (r *BaseRecord) GetID() any {
|
||||
return r.ID
|
||||
}
|
||||
|
||||
func (r *BaseRecord) SetID(id any) {
|
||||
r.ID = id
|
||||
}
|
||||
|
||||
func (r *BaseRecord) GetTimestamp() *time.Time {
|
||||
return r.Timestamp
|
||||
}
|
||||
|
||||
func (r *BaseRecord) SetTimestamp(timestamp *time.Time) {
|
||||
r.Timestamp = timestamp
|
||||
}
|
||||
|
||||
// LineDbOptions представляет опции для создания LineDb
|
||||
// Соответствует TypeScript интерфейсу LineDbOptions
|
||||
type LineDbOptions struct {
|
||||
CacheSize int `json:"cacheSize,omitempty"`
|
||||
CacheTTL time.Duration `json:"cacheTTL,omitempty"`
|
||||
DBFolder string `json:"dbFolder,omitempty"`
|
||||
ObjName string `json:"objName,omitempty"`
|
||||
}
|
||||
|
||||
// LineDbInitOptions представляет опции инициализации LineDb
|
||||
// Соответствует TypeScript интерфейсу LineDbInitOptions
|
||||
type LineDbInitOptions struct {
|
||||
CacheSize int `json:"cacheSize,omitempty"`
|
||||
CacheTTL time.Duration `json:"cacheTTL,omitempty"`
|
||||
Collections []JSONLFileOptions `json:"collections"`
|
||||
DBFolder string `json:"dbFolder,omitempty"`
|
||||
Partitions []PartitionCollection `json:"partitions,omitempty"`
|
||||
}
|
||||
|
||||
// JSONLFileOptions представляет опции для JSONL файла
|
||||
// Соответствует TypeScript интерфейсу JSONLFileOptions
|
||||
type JSONLFileOptions struct {
|
||||
CollectionName string `json:"collectionName,omitempty"`
|
||||
AllocSize int `json:"allocSize,omitempty"`
|
||||
IndexedFields []string `json:"indexedFields,omitempty"`
|
||||
EncryptKeyForLineDb string `json:"encryptKeyForLineDb,omitempty"`
|
||||
SkipInvalidLines bool `json:"skipInvalidLines,omitempty"`
|
||||
DecryptKey string `json:"decryptKey,omitempty"`
|
||||
ConvertStringIdToNumber bool `json:"convertStringIdToNumber,omitempty"`
|
||||
// Функции сериализации и десериализации JSON
|
||||
JSONMarshal func(any) ([]byte, error) `json:"-"`
|
||||
JSONUnmarshal func([]byte, any) error `json:"-"`
|
||||
}
|
||||
|
||||
// PartitionCollection представляет конфигурацию партиционирования
|
||||
// Соответствует TypeScript интерфейсу PartitionCollection
|
||||
type PartitionCollection struct {
|
||||
CollectionName string `json:"collectionName"`
|
||||
PartIDFn func(any) string `json:"-"`
|
||||
PartIDFnStr string `json:"partIdFn,omitempty"`
|
||||
}
|
||||
|
||||
// JoinType представляет тип операции JOIN
|
||||
type JoinType string
|
||||
|
||||
const (
|
||||
JoinTypeInner JoinType = "inner"
|
||||
JoinTypeLeft JoinType = "left"
|
||||
JoinTypeRight JoinType = "right"
|
||||
JoinTypeFull JoinType = "full"
|
||||
)
|
||||
|
||||
// JoinOptions представляет опции для операции JOIN
|
||||
// Соответствует TypeScript интерфейсу JoinOptions
|
||||
type JoinOptions struct {
|
||||
Type JoinType `json:"type"`
|
||||
LeftFields []string `json:"leftFields"`
|
||||
RightFields []string `json:"rightFields"`
|
||||
StrictCompare bool `json:"strictCompare,omitempty"`
|
||||
InTransaction bool `json:"inTransaction,omitempty"`
|
||||
TransactionID string `json:"transactionId,omitempty"`
|
||||
LeftFilter map[string]any `json:"leftFilter,omitempty"`
|
||||
RightFilter map[string]any `json:"rightFilter,omitempty"`
|
||||
OnlyOneFromRight bool `json:"onlyOneFromRight,omitempty"`
|
||||
}
|
||||
|
||||
// PaginatedResult представляет результат пагинации
|
||||
// Соответствует TypeScript интерфейсу PaginatedResult
|
||||
type PaginatedResult struct {
|
||||
Data []any `json:"data"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Pages int `json:"pages"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
// BackupMetaData представляет метаданные резервной копии
|
||||
// Соответствует TypeScript интерфейсу BackupMetaData
|
||||
type BackupMetaData struct {
|
||||
CollectionNames []string `json:"collectionNames"`
|
||||
Gzip bool `json:"gzip"`
|
||||
EncryptKey string `json:"encryptKey"`
|
||||
NoLock bool `json:"noLock"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
BackupDate string `json:"backupDate"`
|
||||
}
|
||||
|
||||
// FilterFunction представляет функцию фильтрации
|
||||
type FilterFunction func(data any) bool
|
||||
|
||||
// LineDbAdapterOptions представляет опции для операций с адаптером
|
||||
// Соответствует TypeScript интерфейсу LineDbAdapterOptions
|
||||
type LineDbAdapterOptions struct {
|
||||
InTransaction bool `json:"inTransaction,omitempty"`
|
||||
StrictCompare bool `json:"strictCompare,omitempty"`
|
||||
TransactionID string `json:"transactionId,omitempty"`
|
||||
DebugTag string `json:"debugTag,omitempty"`
|
||||
FilterType string `json:"filterType,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
RepeatCount int `json:"repeatCount,omitempty"`
|
||||
InternalCall bool `json:"internalCall,omitempty"`
|
||||
SkipCheckExistingForWrite bool `json:"skipCheckExistingForWrite,omitempty"`
|
||||
OptimisticRead bool `json:"optimisticRead,omitempty"`
|
||||
ReturnChain bool `json:"returnChain,omitempty"`
|
||||
}
|
||||
|
||||
// TransactionOptions представляет опции транзакции
|
||||
// Соответствует TypeScript интерфейсу TransactionOptions
|
||||
type TransactionOptions struct {
|
||||
Rollback bool `json:"rollback,omitempty"`
|
||||
BackupFile string `json:"backupFile,omitempty"`
|
||||
DoNotDeleteBackupFile bool `json:"doNotDeleteBackupFile,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// LineDbTransactionOptions представляет опции транзакции LineDb
|
||||
// Соответствует TypeScript интерфейсу LineDbTransactionOptions
|
||||
type LineDbTransactionOptions struct {
|
||||
Rollback bool `json:"rollback,omitempty"`
|
||||
BackupFile string `json:"backupFile,omitempty"`
|
||||
DoNotDeleteBackupFile bool `json:"doNotDeleteBackupFile,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// JoinResult представляет результат операции JOIN
|
||||
type JoinResult struct {
|
||||
Left any `json:"left"`
|
||||
Right any `json:"right"`
|
||||
}
|
||||
|
||||
// CollectionChain представляет цепочку коллекций (аналог lodash chain)
|
||||
// Улучшенная версия с дополнительными методами
|
||||
type CollectionChain struct {
|
||||
data []any
|
||||
}
|
||||
|
||||
// NewCollectionChain создает новую цепочку коллекций
|
||||
func NewCollectionChain(data []any) *CollectionChain {
|
||||
return &CollectionChain{data: data}
|
||||
}
|
||||
|
||||
// Value возвращает данные цепочки
|
||||
func (c *CollectionChain) Value() []any {
|
||||
return c.data
|
||||
}
|
||||
|
||||
// Where фильтрует данные
|
||||
func (c *CollectionChain) Where(filter func(any) bool) *CollectionChain {
|
||||
var filtered []any
|
||||
for _, item := range c.data {
|
||||
if filter(item) {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
return &CollectionChain{data: filtered}
|
||||
}
|
||||
|
||||
// Sort сортирует данные
|
||||
func (c *CollectionChain) Sort(compare func(a, b any) bool) *CollectionChain {
|
||||
// Используем более эффективную сортировку
|
||||
sorted := make([]any, len(c.data))
|
||||
copy(sorted, c.data)
|
||||
|
||||
// Quick sort implementation
|
||||
c.quickSort(sorted, 0, len(sorted)-1, compare)
|
||||
|
||||
return &CollectionChain{data: sorted}
|
||||
}
|
||||
|
||||
// quickSort реализует быструю сортировку
|
||||
func (c *CollectionChain) quickSort(arr []any, low, high int, compare func(a, b any) bool) {
|
||||
if low < high {
|
||||
pi := c.partition(arr, low, high, compare)
|
||||
c.quickSort(arr, low, pi-1, compare)
|
||||
c.quickSort(arr, pi+1, high, compare)
|
||||
}
|
||||
}
|
||||
|
||||
// partition вспомогательная функция для quickSort
|
||||
func (c *CollectionChain) partition(arr []any, low, high int, compare func(a, b any) bool) int {
|
||||
pivot := arr[high]
|
||||
i := low - 1
|
||||
|
||||
for j := low; j < high; j++ {
|
||||
if compare(arr[j], pivot) {
|
||||
i++
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
}
|
||||
}
|
||||
arr[i+1], arr[high] = arr[high], arr[i+1]
|
||||
return i + 1
|
||||
}
|
||||
|
||||
// Map применяет функцию к каждому элементу
|
||||
func (c *CollectionChain) Map(fn func(any) any) *CollectionChain {
|
||||
mapped := make([]any, len(c.data))
|
||||
for i, item := range c.data {
|
||||
mapped[i] = fn(item)
|
||||
}
|
||||
return &CollectionChain{data: mapped}
|
||||
}
|
||||
|
||||
// Take возвращает первые n элементов
|
||||
func (c *CollectionChain) Take(n int) *CollectionChain {
|
||||
if n >= len(c.data) {
|
||||
return c
|
||||
}
|
||||
return &CollectionChain{data: c.data[:n]}
|
||||
}
|
||||
|
||||
// Skip пропускает первые n элементов
|
||||
func (c *CollectionChain) Skip(n int) *CollectionChain {
|
||||
if n >= len(c.data) {
|
||||
return &CollectionChain{data: []any{}}
|
||||
}
|
||||
return &CollectionChain{data: c.data[n:]}
|
||||
}
|
||||
|
||||
// Size возвращает размер коллекции
|
||||
func (c *CollectionChain) Size() int {
|
||||
return len(c.data)
|
||||
}
|
||||
|
||||
// IsEmpty проверяет, пуста ли коллекция
|
||||
func (c *CollectionChain) IsEmpty() bool {
|
||||
return len(c.data) == 0
|
||||
}
|
||||
|
||||
// First возвращает первый элемент
|
||||
func (c *CollectionChain) First() any {
|
||||
if len(c.data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.data[0]
|
||||
}
|
||||
|
||||
// Last возвращает последний элемент
|
||||
func (c *CollectionChain) Last() any {
|
||||
if len(c.data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.data[len(c.data)-1]
|
||||
}
|
||||
201
tests/linedb_test.go
Normal file
201
tests/linedb_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"linedb/pkg/linedb"
|
||||
)
|
||||
|
||||
func TestLineDbBasic(t *testing.T) {
|
||||
// Очищаем тестовую папку
|
||||
os.RemoveAll("./testdata")
|
||||
|
||||
// Создаем опции инициализации
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "test",
|
||||
AllocSize: 256,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
defer db.Close()
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
t.Fatalf("Failed to init database: %v", err)
|
||||
}
|
||||
|
||||
// Тест вставки
|
||||
testData := map[string]any{
|
||||
"name": "test",
|
||||
"value": 123,
|
||||
}
|
||||
|
||||
if err := db.Insert(testData, "test", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
t.Fatalf("Failed to insert data: %v", err)
|
||||
}
|
||||
|
||||
// Тест чтения
|
||||
allData, err := db.Read("test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read data: %v", err)
|
||||
}
|
||||
|
||||
if len(allData) != 1 {
|
||||
t.Fatalf("Expected 1 record, got %d", len(allData))
|
||||
}
|
||||
|
||||
// Тест фильтрации
|
||||
filter := map[string]any{"name": "test"}
|
||||
filteredData, err := db.ReadByFilter(filter, "test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to filter data: %v", err)
|
||||
}
|
||||
|
||||
if len(filteredData) != 1 {
|
||||
t.Fatalf("Expected 1 filtered record, got %d", len(filteredData))
|
||||
}
|
||||
|
||||
// Тест обновления
|
||||
updateData := map[string]any{"value": 456}
|
||||
updateFilter := map[string]any{"name": "test"}
|
||||
updatedData, err := db.Update(updateData, "test", updateFilter, linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update data: %v", err)
|
||||
}
|
||||
|
||||
if len(updatedData) != 1 {
|
||||
t.Fatalf("Expected 1 updated record, got %d", len(updatedData))
|
||||
}
|
||||
|
||||
// Тест удаления
|
||||
deleteFilter := map[string]any{"name": "test"}
|
||||
deletedData, err := db.Delete(deleteFilter, "test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete data: %v", err)
|
||||
}
|
||||
|
||||
if len(deletedData) != 1 {
|
||||
t.Fatalf("Expected 1 deleted record, got %d", len(deletedData))
|
||||
}
|
||||
|
||||
// Проверяем что данных больше нет
|
||||
remainingData, err := db.Read("test", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read remaining data: %v", err)
|
||||
}
|
||||
|
||||
if len(remainingData) != 0 {
|
||||
t.Fatalf("Expected 0 remaining records, got %d", len(remainingData))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineDbPartitioning(t *testing.T) {
|
||||
// Очищаем тестовую папку
|
||||
os.RemoveAll("./testdata")
|
||||
|
||||
// Создаем опции инициализации с партиционированием
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "orders",
|
||||
AllocSize: 256,
|
||||
},
|
||||
},
|
||||
Partitions: []linedb.PartitionCollection{
|
||||
{
|
||||
CollectionName: "orders",
|
||||
PartIDFn: func(item any) string {
|
||||
if itemMap, ok := item.(map[string]any); ok {
|
||||
if userId, exists := itemMap["userId"]; exists {
|
||||
return toString(userId)
|
||||
}
|
||||
}
|
||||
return "default"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем базу данных
|
||||
db := linedb.NewLineDb(nil)
|
||||
defer db.Close()
|
||||
|
||||
// Инициализируем базу данных
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
t.Fatalf("Failed to init database: %v", err)
|
||||
}
|
||||
|
||||
// Создаем заказы для разных пользователей
|
||||
orders := []any{
|
||||
map[string]any{
|
||||
"userId": 1,
|
||||
"item": "laptop",
|
||||
"price": 999.99,
|
||||
},
|
||||
map[string]any{
|
||||
"userId": 1,
|
||||
"item": "mouse",
|
||||
"price": 29.99,
|
||||
},
|
||||
map[string]any{
|
||||
"userId": 2,
|
||||
"item": "keyboard",
|
||||
"price": 89.99,
|
||||
},
|
||||
}
|
||||
|
||||
// Вставляем заказы
|
||||
if err := db.Insert(orders, "orders", linedb.LineDbAdapterOptions{}); err != nil {
|
||||
t.Fatalf("Failed to insert orders: %v", err)
|
||||
}
|
||||
|
||||
// Читаем все заказы
|
||||
allOrders, err := db.Read("orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read orders: %v", err)
|
||||
}
|
||||
|
||||
if len(allOrders) != 3 {
|
||||
t.Fatalf("Expected 3 orders, got %d", len(allOrders))
|
||||
}
|
||||
|
||||
// Фильтруем заказы пользователя 1
|
||||
user1Filter := map[string]any{"userId": 1}
|
||||
user1Orders, err := db.ReadByFilter(user1Filter, "orders", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to filter user 1 orders: %v", err)
|
||||
}
|
||||
|
||||
if len(user1Orders) != 2 {
|
||||
t.Fatalf("Expected 2 orders for user 1, got %d", len(user1Orders))
|
||||
}
|
||||
}
|
||||
|
||||
// toString конвертирует значение в строку
|
||||
func toString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case int:
|
||||
return string(rune(v))
|
||||
case int64:
|
||||
return string(rune(v))
|
||||
case float64:
|
||||
return string(rune(int(v)))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
30
tests/main_test.go
Normal file
30
tests/main_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Настройка перед тестами
|
||||
setup()
|
||||
|
||||
// Запуск тестов
|
||||
code := m.Run()
|
||||
|
||||
// Очистка после тестов
|
||||
teardown()
|
||||
|
||||
// Выход с кодом
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setup() {
|
||||
// Создаем тестовые директории
|
||||
os.MkdirAll("./testdata", 0755)
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
// Очищаем тестовые данные
|
||||
os.RemoveAll("./testdata")
|
||||
}
|
||||
Reference in New Issue
Block a user