before merge to main
This commit is contained in:
@@ -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"}
|
||||
|
||||
74
examples/custom-serializer/main.go
Normal file
74
examples/custom-serializer/main.go
Normal 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
61
examples/encode/main.go
Normal 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
206
examples/perf/main.go
Normal 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)
|
||||
}
|
||||
95
examples/test-alloc-overflow/main.go
Normal file
95
examples/test-alloc-overflow/main.go
Normal 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)
|
||||
}
|
||||
57
examples/test-init-normalize/main.go
Normal file
57
examples/test-init-normalize/main.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user