before merge to main

This commit is contained in:
2026-03-04 16:29:24 +06:00
parent 0481cde1c3
commit 491ccbea89
21 changed files with 6776 additions and 47 deletions

View File

@@ -1,2 +1,2 @@
{"age":31,"created":"2026-03-04T09:07:22+06:00","email":"john@example.com","id":1,"name":"John Doe"}
{"age":35,"created":"2026-03-04T09:07:22+06:00","email":"bob@example.com","id":3,"name":"Bob Johnson"}
{"age":31,"created":"2026-03-04T13:58:25+06:00","email":"john@example.com","id":1,"name":"John Doe"}
{"age":35,"created":"2026-03-04T13:58:25+06:00","email":"bob@example.com","id":3,"name":"Bob Johnson"}

View File

@@ -0,0 +1,74 @@
// Пример использования KeyOrder в опциях коллекции — встроенная сортировка ключей при сериализации.
package main
import (
"fmt"
"log"
"os"
"time"
"linedb/pkg/linedb"
)
// Item — структура для вставки (LineDB поддерживает struct и map)
type Item struct {
Name string `json:"name"`
Value int `json:"value"`
Active bool `json:"active"`
Atime string `json:"atime"`
}
func main() {
os.RemoveAll("./data")
// KeyOrder в опциях коллекции — при задании используется кастомная сериализация с порядком ключей
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./data",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "items",
AllocSize: 256,
KeyOrder: []linedb.KeyOrder{
{Key: "id", Order: 0},
{Key: "atime", Order: -1},
},
},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
log.Fatalf("Init failed: %v", err)
}
defer db.Close()
opts := linedb.LineDbAdapterOptions{}
// Инсерты: map и структуры (LineDB принимает оба)
items := []any{
map[string]any{"Value": 42, "Name": "Test", "Active": true, "atime": "2024-01-01"},
map[string]any{"Active": false, "Name": "Alice", "Value": 100, "atime": "2024-01-02"},
map[string]any{"Name": "Bob", "Value": 0, "Active": true, "atime": "2024-01-03"},
Item{Name: "Charlie", Value: 7, Active: true, Atime: "2024-01-04"},
Item{Name: "Diana", Value: 99, Active: false, Atime: "2024-01-05"},
}
if err := db.Insert(items, "items", opts); err != nil {
log.Fatalf("Insert failed: %v", err)
}
// Read
all, err := db.Read("items", opts)
if err != nil {
log.Fatalf("Read failed: %v", err)
}
fmt.Printf("Records: %d\n", len(all))
for _, r := range all {
fmt.Printf(" %+v\n", r)
}
raw, _ := os.ReadFile("./data/items.jsonl")
fmt.Printf("\nRaw file:\n%s\n", string(raw))
}

61
examples/encode/main.go Normal file
View File

@@ -0,0 +1,61 @@
// Пример использования Encode — шифрование записей в коллекции (AES-256-GCM).
package main
import (
"fmt"
"log"
"os"
"time"
"linedb/pkg/linedb"
)
func main() {
os.RemoveAll("./data")
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./data",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "secret_users",
AllocSize: 256,
Encode: true,
EncodeKey: "my-secret-password-12345",
},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
log.Fatalf("Init failed: %v", err)
}
defer db.Close()
opts := linedb.LineDbAdapterOptions{}
// Вставка
users := []any{
map[string]any{"name": "alice", "email": "alice@secret.com", "role": "admin"},
map[string]any{"name": "bob", "email": "bob@secret.com", "role": "user"},
}
if err := db.Insert(users, "secret_users", opts); err != nil {
log.Fatalf("Insert failed: %v", err)
}
fmt.Println("Insert OK")
// Чтение
all, err := db.Read("secret_users", opts)
if err != nil {
log.Fatalf("Read failed: %v", err)
}
fmt.Printf("Records: %d\n", len(all))
for _, r := range all {
fmt.Printf(" %+v\n", r)
}
// Файл содержит зашифрованные строки (base64)
raw, _ := os.ReadFile("./data/secret_users.jsonl")
fmt.Printf("\nRaw file (encrypted base64):\n%s\n", string(raw))
}

206
examples/perf/main.go Normal file
View File

@@ -0,0 +1,206 @@
// Пример комплексного performance-теста LineDb.
// Запуск:
//
// go run ./examples/perf/main.go
//
// Сценарий:
// 1. Вставка 5000 записей, каждая ~1800 символов.
// 2. 10 случайных обновлений, замер среднего времени.
// 3. 100 случайных удалений, замер среднего времени.
// 4. Замеры памяти процесса через runtime.MemStats.
package main
import (
"fmt"
"log"
"math/rand"
"os"
"runtime"
"strings"
"time"
"linedb/pkg/linedb"
)
const (
recordsCount = 5000
payloadSize = 1800
updateOps = 100
deleteOps = 100
collectionName = "perf_items"
dbDir = "./data/perf-benchmark"
allocSizeEstimate = 2048
)
func main() {
rand.Seed(time.Now().UnixNano())
if err := os.MkdirAll(dbDir, 0755); err != nil {
log.Fatalf("mkdir: %v", err)
}
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: time.Minute,
DBFolder: dbDir,
Collections: []linedb.JSONLFileOptions{
{
CollectionName: collectionName,
AllocSize: allocSizeEstimate,
},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(true, initOptions); err != nil {
log.Fatalf("Init failed: %v", err)
}
defer db.Close()
printMem("Before insert")
fmt.Printf("1) Insert %d records (payload ~%d chars)...\n", recordsCount, payloadSize)
start := time.Now()
if err := insertRecords(db); err != nil {
log.Fatalf("InsertRecords failed: %v", err)
}
elapsedInsert := time.Since(start)
fmt.Printf(" Total insert time: %v, per record: %v\n",
elapsedInsert, elapsedInsert/time.Duration(recordsCount))
printMem("After insert")
all, err := db.Read(collectionName, linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Read after insert failed: %v", err)
}
fmt.Printf(" Records in collection: %d\n", len(all))
ids := collectIDs(all)
if len(ids) == 0 {
log.Fatalf("No IDs collected, cannot continue")
}
fmt.Printf("\n2) Random update of %d records...\n", updateOps)
avgUpdate := benchmarkUpdates(db, ids, updateOps)
fmt.Printf(" Average update time: %v\n", avgUpdate)
printMem("After updates")
fmt.Printf("\n3) Random delete of %d records...\n", deleteOps)
avgDelete := benchmarkDeletes(db, ids, deleteOps)
fmt.Printf(" Average delete time: %v\n", avgDelete)
printMem("After deletes")
final, err := db.Read(collectionName, linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Final read failed: %v", err)
}
fmt.Printf("\nFinal records in collection: %d\n", len(final))
fmt.Printf("Data directory: %s\n", dbDir)
}
func insertRecords(db *linedb.LineDb) error {
// Сгенерируем базовый payload
base := strings.Repeat("X", payloadSize)
batch := make([]any, 0, 100)
for i := 0; i < recordsCount; i++ {
rec := map[string]any{
"index": i,
"payload": base,
"created": time.Now().Format(time.RFC3339Nano),
}
batch = append(batch, rec)
if len(batch) >= cap(batch) {
if err := db.Insert(batch, collectionName, linedb.LineDbAdapterOptions{}); err != nil {
return err
}
batch = batch[:0]
}
}
if len(batch) > 0 {
if err := db.Insert(batch, collectionName, linedb.LineDbAdapterOptions{}); err != nil {
return err
}
}
return nil
}
func collectIDs(all []any) []any {
ids := make([]any, 0, len(all))
for _, r := range all {
if m, ok := r.(map[string]any); ok {
if id, ok := m["id"]; ok {
ids = append(ids, id)
}
}
}
return ids
}
func benchmarkUpdates(db *linedb.LineDb, ids []any, ops int) time.Duration {
if len(ids) == 0 {
return 0
}
var total time.Duration
for i := 0; i < ops; i++ {
id := ids[rand.Intn(len(ids))]
update := map[string]any{
"updatedAt": time.Now().Format(time.RFC3339Nano),
}
start := time.Now()
_, err := db.Update(update, collectionName, map[string]any{"id": id}, linedb.LineDbAdapterOptions{})
dur := time.Since(start)
if err != nil {
log.Printf("Update error (id=%v): %v", id, err)
continue
}
total += dur
}
if ops == 0 {
return 0
}
return total / time.Duration(ops)
}
func benchmarkDeletes(db *linedb.LineDb, ids []any, ops int) time.Duration {
if len(ids) == 0 {
return 0
}
var total time.Duration
used := make(map[int]struct{})
for i := 0; i < ops; i++ {
// чтобы не удалять по одному и тому же id постоянно, постараемся брать разные индексы
var idx int
for tries := 0; tries < 10; tries++ {
idx = rand.Intn(len(ids))
if _, ok := used[idx]; !ok {
break
}
}
used[idx] = struct{}{}
id := ids[idx]
start := time.Now()
_, err := db.Delete(map[string]any{"id": id}, collectionName, linedb.LineDbAdapterOptions{})
dur := time.Since(start)
if err != nil {
log.Printf("Delete error (id=%v): %v", id, err)
continue
}
total += dur
}
if ops == 0 {
return 0
}
return total / time.Duration(ops)
}
func printMem(label string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("\n=== %s ===\n", label)
fmt.Printf(" Alloc: %.2f MB\n", float64(m.Alloc)/1024.0/1024.0)
fmt.Printf(" TotalAlloc: %.2f MB\n", float64(m.TotalAlloc)/1024.0/1024.0)
fmt.Printf(" Sys: %.2f MB\n", float64(m.Sys)/1024.0/1024.0)
fmt.Printf(" NumGC: %d\n", m.NumGC)
}

View File

@@ -0,0 +1,95 @@
// Тест: ошибка при записи, превышающей allocSize-1.
// Запуск: go run ./examples/test-alloc-overflow/main.go
// Файлы остаются в ./data/test-alloc-overflow/
package main
import (
"fmt"
"log"
"os"
"strings"
"time"
"linedb/pkg/linedb"
)
func main() {
dataDir := "./data/test-alloc-overflow"
os.MkdirAll(dataDir, 0755)
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: dataDir,
Collections: []linedb.JSONLFileOptions{
{CollectionName: "tiny", AllocSize: 64},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
log.Fatalf("Init failed: %v", err)
}
defer db.Close()
opts := linedb.LineDbAdapterOptions{}
// Короткая запись — должна пройти
fmt.Println("1. Insert короткой записи...")
if err := db.Insert(map[string]any{"x": "short"}, "tiny", opts); err != nil {
log.Fatalf("Insert short failed: %v", err)
}
fmt.Println(" OK")
// Длинная — ожидаем ошибку
fmt.Println("2. Insert длинной записи (ожидаем ошибку)...")
longStr := strings.Repeat("a", 80)
err := db.Insert(map[string]any{"x": longStr}, "tiny", opts)
if err == nil {
log.Fatal("Ожидалась ошибка, но Insert прошёл")
}
fmt.Printf(" Ошибка (ожидаемо): %v\n", err)
if !strings.Contains(err.Error(), "exceeds") {
log.Fatalf("Ожидалось 'exceeds' в ошибке, получено: %v", err)
}
fmt.Println(" OK — ошибка корректная")
// Итог
all, _ := db.Read("tiny", opts)
fmt.Printf("\nЗаписей в коллекции после insert-теста: %d\n", len(all))
//короткий update
fmt.Println("\n3. Update с коротким значением...")
_, err = db.Update(
map[string]any{"x": "short"},
"tiny",
map[string]any{"x": "short"},
opts,
)
if err != nil {
log.Fatalf("Update short failed: %v", err)
}
fmt.Println(" OK")
// Длинный update — ожидаем ошибку при переписывании файла
fmt.Println("\n4. Update с длинным значением (ожидаем ошибку)...")
longUpdate := strings.Repeat("b", 80)
_, err = db.Update(
map[string]any{"x": longUpdate},
"tiny",
map[string]any{"x": "short"},
opts,
)
if err == nil {
log.Fatal("Ожидалась ошибка, но Update прошёл")
}
fmt.Printf(" Ошибка (ожидаемо): %v\n", err)
if !strings.Contains(err.Error(), "exceeds") {
log.Fatalf("Ожидалось 'exceeds' в ошибке, получено: %v", err)
}
fmt.Println(" OK — ошибка при update корректная")
all, _ = db.Read("tiny", opts)
fmt.Printf("\nЗаписей в коллекции после update-теста: %d\n", len(all))
fmt.Printf("Файл сохранён: %s/tiny.jsonl\n", dataDir)
}

View File

@@ -0,0 +1,57 @@
// Тест: нормализация существующего файла при Init (приведение записей к allocSize-1).
// Запуск: go run ./examples/test-init-normalize/main.go
// Файлы остаются в ./data/test-init-normalize/
package main
import (
"fmt"
"log"
"os"
"time"
"linedb/pkg/linedb"
)
func main() {
dataDir := "./data/test-init-normalize"
os.MkdirAll(dataDir, 0755)
// Создаём файл с записями разной длины ДО Init
filePath := dataDir + "/norm.jsonl"
content := `{"id":1,"a":"x"}
{"id":2,"a":"yy"}
{"id":3,"a":"zzz"} `
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
log.Fatalf("Write file: %v", err)
}
fmt.Printf("Создан файл с записями разной длины:\n%s\n", content)
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: dataDir,
Collections: []linedb.JSONLFileOptions{
{CollectionName: "norm", AllocSize: 128},
},
}
db := linedb.NewLineDb(nil)
fmt.Println("\nInit (нормализация записей к allocSize-1=99)...")
if err := db.Init(false, initOptions); err != nil {
log.Fatalf("Init failed: %v", err)
}
defer db.Close()
all, err := db.Read("norm", linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("Read failed: %v", err)
}
fmt.Printf("Прочитано записей: %d\n", len(all))
for i, r := range all {
fmt.Printf(" [%d] %+v\n", i+1, r)
}
raw, _ := os.ReadFile(filePath)
fmt.Printf("\nФайл после нормализации:\n%s\n", string(raw))
fmt.Printf("Файл сохранён: %s\n", filePath)
}