754 lines
26 KiB
Go
754 lines
26 KiB
Go
package tests
|
||
|
||
import (
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"direct-dev.ru/gitea/GiteaAdmin/elowdb-go/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 ""
|
||
}
|
||
}
|
||
|
||
// setupUniqueCollection создаёт БД с коллекцией, у которой заданы UniqueFields
|
||
func setupUniqueCollection(t *testing.T, uniqueFields []string) (*linedb.LineDb, func()) {
|
||
t.Helper()
|
||
os.RemoveAll("./testdata")
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
CacheSize: 100,
|
||
CacheTTL: time.Minute,
|
||
DBFolder: "./testdata",
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "users",
|
||
AllocSize: 256,
|
||
UniqueFields: uniqueFields,
|
||
},
|
||
},
|
||
}
|
||
db := linedb.NewLineDb(nil)
|
||
if err := db.Init(false, initOptions); err != nil {
|
||
t.Fatalf("Failed to init database: %v", err)
|
||
}
|
||
return db, func() { db.Close(); os.RemoveAll("./testdata") }
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsNonEmpty проверяет, что дубликаты непустых значений в уникальном поле запрещены
|
||
func TestLineDbUniqueFieldsNonEmpty(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "alice", "email": "alice@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 1 failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "bob", "email": "bob@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 2 failed: %v", err)
|
||
}
|
||
|
||
// Дубликат email — ожидаем ошибку
|
||
err := db.Insert(map[string]any{"name": "alice2", "email": "alice@test.com"}, "users", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected unique constraint violation for duplicate email")
|
||
}
|
||
if err.Error() == "" {
|
||
t.Fatalf("Expected meaningful error, got: %v", err)
|
||
}
|
||
|
||
all, _ := db.Read("users", opts)
|
||
if len(all) != 2 {
|
||
t.Fatalf("Expected 2 records, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsEmptyValues проверяет, что пустые значения (nil, "") в уникальном поле допускают несколько записей
|
||
func TestLineDbUniqueFieldsEmptyValues(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
// Несколько записей с отсутствующим полем email — должно быть разрешено
|
||
if err := db.Insert(map[string]any{"name": "a"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert with nil email failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "b"}, "users", opts); err != nil {
|
||
t.Fatalf("Second insert with nil email failed: %v", err)
|
||
}
|
||
|
||
// Несколько записей с nil в email — должно быть разрешено
|
||
if err := db.Insert(map[string]any{"name": "a", "email": nil}, "users", opts); err != nil {
|
||
t.Fatalf("Insert with nil email failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "b", "email": nil}, "users", opts); err != nil {
|
||
t.Fatalf("Second insert with nil email failed: %v", err)
|
||
}
|
||
|
||
// Несколько записей с пустой строкой
|
||
if err := db.Insert(map[string]any{"name": "c", "email": ""}, "users", opts); err != nil {
|
||
t.Fatalf("Insert with empty email failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "d", "email": ""}, "users", opts); err != nil {
|
||
t.Fatalf("Second insert with empty email failed: %v", err)
|
||
}
|
||
|
||
all, err := db.Read("users", opts)
|
||
if err != nil {
|
||
t.Fatalf("Read failed: %v", err)
|
||
}
|
||
if len(all) != 6 {
|
||
t.Fatalf("Expected 6 records (2 with nil email + 2 with empty string + 2 without email), got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsSkipCheckExistingForWrite проверяет режим SkipCheckExistingForWrite — уникальность не проверяется
|
||
func TestLineDbUniqueFieldsSkipCheckExistingForWrite(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{SkipCheckExistingForWrite: true}
|
||
|
||
if err := db.Insert(map[string]any{"name": "a", "email": "same@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 1 failed: %v", err)
|
||
}
|
||
// С тем же email, но с пропуском проверки — должно пройти
|
||
if err := db.Insert(map[string]any{"name": "b", "email": "same@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 2 with same email (skip check) failed: %v", err)
|
||
}
|
||
|
||
all, _ := db.Read("users", opts)
|
||
if len(all) != 2 {
|
||
t.Fatalf("Expected 2 records with SkipCheckExistingForWrite, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsUpdateConflict проверяет, что Update не может установить значение, уже занятое другой записью
|
||
func TestLineDbUniqueFieldsUpdateConflict(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "alice", "email": "alice@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert alice failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "bob", "email": "bob@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert bob failed: %v", err)
|
||
}
|
||
|
||
// Пытаемся обновить bob на email alice
|
||
_, err := db.Update(
|
||
map[string]any{"email": "alice@test.com"},
|
||
"users",
|
||
map[string]any{"name": "bob"},
|
||
opts,
|
||
)
|
||
if err == nil {
|
||
t.Fatal("Expected unique constraint violation when updating to existing email")
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsUpdateSameValue проверяет, что Update на ту же запись (тот же unique-значение) допустим
|
||
func TestLineDbUniqueFieldsUpdateSameValue(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "alice", "email": "alice@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert failed: %v", err)
|
||
}
|
||
|
||
// Обновляем имя, оставляя email тем же — допустимо
|
||
updated, err := db.Update(
|
||
map[string]any{"name": "alice_updated"},
|
||
"users",
|
||
map[string]any{"email": "alice@test.com"},
|
||
opts,
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("Update same record failed: %v", err)
|
||
}
|
||
if len(updated) != 1 {
|
||
t.Fatalf("Expected 1 updated record, got %d", len(updated))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsMultipleFields проверяет несколько уникальных полей
|
||
func TestLineDbUniqueFieldsMultipleFields(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email", "username"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "a", "email": "a@test.com", "username": "user_a"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 1 failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "b", "email": "b@test.com", "username": "user_b"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 2 failed: %v", err)
|
||
}
|
||
|
||
// Дубликат email
|
||
err := db.Insert(map[string]any{"name": "c", "email": "a@test.com", "username": "user_c"}, "users", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected unique constraint violation for duplicate email")
|
||
}
|
||
|
||
// Дубликат username
|
||
err = db.Insert(map[string]any{"name": "d", "email": "d@test.com", "username": "user_a"}, "users", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected unique constraint violation for duplicate username")
|
||
}
|
||
|
||
all, _ := db.Read("users", opts)
|
||
if len(all) != 2 {
|
||
t.Fatalf("Expected 2 records, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsBatchInsertDuplicate проверяет, что дубликат внутри одного batch Insert даёт ошибку
|
||
func TestLineDbUniqueFieldsBatchInsertDuplicate(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
batch := []any{
|
||
map[string]any{"name": "a", "email": "x@test.com"},
|
||
map[string]any{"name": "b", "email": "x@test.com"},
|
||
}
|
||
err := db.Insert(batch, "users", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected unique constraint violation in batch insert")
|
||
}
|
||
|
||
all, _ := db.Read("users", opts)
|
||
if len(all) != 0 {
|
||
t.Fatalf("Expected 0 records (batch should rollback or not persist), got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsUpdateToEmpty проверяет, что Update может установить пустое значение в unique-поле
|
||
func TestLineDbUniqueFieldsUpdateToEmpty(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, []string{"email"})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "a", "email": "a@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "b", "email": "b@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 2 failed: %v", err)
|
||
}
|
||
|
||
// Обновляем одну запись: email -> ""
|
||
updated, err := db.Update(
|
||
map[string]any{"email": ""},
|
||
"users",
|
||
map[string]any{"email": "a@test.com"},
|
||
opts,
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("Update to empty email failed: %v", err)
|
||
}
|
||
if len(updated) != 1 {
|
||
t.Fatalf("Expected 1 updated record, got %d", len(updated))
|
||
}
|
||
|
||
all, _ := db.Read("users", opts)
|
||
if len(all) != 2 {
|
||
t.Fatalf("Expected 2 records, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbUniqueFieldsNoUniqueFields проверяет, что без UniqueFields дубликаты разрешены
|
||
func TestLineDbUniqueFieldsNoUniqueFields(t *testing.T) {
|
||
db, cleanup := setupUniqueCollection(t, nil)
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "a", "email": "dup@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 1 failed: %v", err)
|
||
}
|
||
if err := db.Insert(map[string]any{"name": "b", "email": "dup@test.com"}, "users", opts); err != nil {
|
||
t.Fatalf("Insert 2 (duplicate email, no unique) failed: %v", err)
|
||
}
|
||
|
||
all, _ := db.Read("users", opts)
|
||
if len(all) != 2 {
|
||
t.Fatalf("Expected 2 records when no unique fields, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbEncode проверяет шифрование записей при Encode
|
||
func TestLineDbEncode(t *testing.T) {
|
||
os.RemoveAll("./testdata")
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
CacheSize: 100,
|
||
CacheTTL: time.Minute,
|
||
DBFolder: "./testdata",
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "secret",
|
||
AllocSize: 512,
|
||
Encode: true,
|
||
EncodeKey: "test-secret-key",
|
||
},
|
||
},
|
||
}
|
||
db := linedb.NewLineDb(nil)
|
||
if err := db.Init(false, initOptions); err != nil {
|
||
t.Fatalf("Init failed: %v", err)
|
||
}
|
||
defer func() { db.Close(); os.RemoveAll("./testdata") }()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
if err := db.Insert(map[string]any{"name": "secret", "value": 42}, "secret", opts); err != nil {
|
||
t.Fatalf("Insert failed: %v", err)
|
||
}
|
||
|
||
all, err := db.Read("secret", opts)
|
||
if err != nil {
|
||
t.Fatalf("Read failed: %v", err)
|
||
}
|
||
if len(all) != 1 {
|
||
t.Fatalf("Expected 1 record, got %d", len(all))
|
||
}
|
||
r := all[0].(map[string]any)
|
||
if r["name"] != "secret" || r["value"] != float64(42) {
|
||
t.Fatalf("Expected name=secret value=42, got %+v", r)
|
||
}
|
||
}
|
||
|
||
// TestLineDbAllocSizeOverflow проверяет ошибку при записи, превышающей allocSize-1
|
||
func TestLineDbAllocSizeOverflow(t *testing.T) {
|
||
os.RemoveAll("./testdata")
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
CacheSize: 100,
|
||
CacheTTL: time.Minute,
|
||
DBFolder: "./testdata",
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{CollectionName: "tiny", AllocSize: 64},
|
||
},
|
||
}
|
||
db := linedb.NewLineDb(nil)
|
||
if err := db.Init(false, initOptions); err != nil {
|
||
t.Fatalf("Init failed: %v", err)
|
||
}
|
||
defer func() { db.Close(); os.RemoveAll("./testdata") }()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
// Короткая запись — поместится (allocSize-1=63)
|
||
if err := db.Insert(map[string]any{"x": "short"}, "tiny", opts); err != nil {
|
||
t.Fatalf("Insert short failed: %v", err)
|
||
}
|
||
// Длинная — не поместится (JSON > 63 символов)
|
||
longStr := strings.Repeat("a", 80)
|
||
err := db.Insert(map[string]any{"x": longStr}, "tiny", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected error for record exceeding allocSize-1")
|
||
}
|
||
if !strings.Contains(err.Error(), "exceeds") {
|
||
t.Fatalf("Expected 'exceeds' in error, got: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestLineDbInitNormalizeExistingFile проверяет нормализацию при Init существующего файла
|
||
func TestLineDbInitNormalizeExistingFile(t *testing.T) {
|
||
os.RemoveAll("./testdata")
|
||
os.MkdirAll("./testdata", 0755)
|
||
// Файл с записями разной длины
|
||
content := `{"id":1,"a":"x"}
|
||
{"id":2,"a":"yy"}
|
||
{"id":3,"a":"zzz"}`
|
||
if err := os.WriteFile("./testdata/norm.jsonl", []byte(content), 0644); err != nil {
|
||
t.Fatalf("Write file: %v", err)
|
||
}
|
||
defer os.RemoveAll("./testdata")
|
||
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
CacheSize: 100,
|
||
CacheTTL: time.Minute,
|
||
DBFolder: "./testdata",
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{CollectionName: "norm", AllocSize: 100},
|
||
},
|
||
}
|
||
db := linedb.NewLineDb(nil)
|
||
if err := db.Init(false, initOptions); err != nil {
|
||
t.Fatalf("Init failed: %v", err)
|
||
}
|
||
defer db.Close()
|
||
|
||
all, err := db.Read("norm", linedb.LineDbAdapterOptions{})
|
||
if err != nil {
|
||
t.Fatalf("Read failed: %v", err)
|
||
}
|
||
if len(all) != 3 {
|
||
t.Fatalf("Expected 3 records, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// setupCollectionWithFieldConstraints создаёт БД с FieldConstraints (required/unique)
|
||
func setupCollectionWithFieldConstraints(t *testing.T, constraints []linedb.FieldConstraint) (*linedb.LineDb, func()) {
|
||
t.Helper()
|
||
os.RemoveAll("./testdata")
|
||
initOptions := &linedb.LineDbInitOptions{
|
||
CacheSize: 100,
|
||
CacheTTL: time.Minute,
|
||
DBFolder: "./testdata",
|
||
Collections: []linedb.JSONLFileOptions{
|
||
{
|
||
CollectionName: "items",
|
||
AllocSize: 256,
|
||
FieldConstraints: constraints,
|
||
},
|
||
},
|
||
}
|
||
db := linedb.NewLineDb(nil)
|
||
if err := db.Init(false, initOptions); err != nil {
|
||
t.Fatalf("Failed to init database: %v", err)
|
||
}
|
||
return db, func() { db.Close(); os.RemoveAll("./testdata") }
|
||
}
|
||
|
||
// TestLineDbRequiredFieldsInsert проверяет, что required поля обязательны при Insert
|
||
func TestLineDbRequiredFieldsInsert(t *testing.T) {
|
||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||
{Name: "email", Required: true},
|
||
{Name: "name", Required: true},
|
||
})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
// Успех — оба поля заполнены
|
||
if err := db.Insert(map[string]any{"name": "alice", "email": "a@test.com"}, "items", opts); err != nil {
|
||
t.Fatalf("Insert with required fields failed: %v", err)
|
||
}
|
||
|
||
// Ошибка — отсутствует email (AbsentKey)
|
||
err := db.Insert(map[string]any{"name": "bob"}, "items", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected error for missing required field email")
|
||
} else {
|
||
log.Println("Error: ", err)
|
||
}
|
||
|
||
// Ошибка — email = nil
|
||
err = db.Insert(map[string]any{"name": "bob", "email": nil}, "items", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected error for nil required field email")
|
||
} else {
|
||
log.Println("Error: ", err)
|
||
}
|
||
|
||
// Ошибка — email = ""
|
||
err = db.Insert(map[string]any{"name": "bob", "email": ""}, "items", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected error for empty string in required field email")
|
||
} else {
|
||
log.Println("Error: ", err)
|
||
}
|
||
|
||
all, _ := db.Read("items", opts)
|
||
if len(all) != 1 {
|
||
t.Fatalf("Expected 1 record, got %d", len(all))
|
||
}
|
||
}
|
||
|
||
// TestLineDbRequiredFieldsEmptyMode проверяет разные режимы интерпретации пустоты
|
||
func TestLineDbRequiredFieldsEmptyMode(t *testing.T) {
|
||
// Режим: только отсутствие ключа — nil и "" допустимы
|
||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||
{Name: "title", Required: true, EmptyMode: linedb.EmptyModeAbsentKey},
|
||
})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
// nil — допустимо (только AbsentKey проверяется)
|
||
if err := db.Insert(map[string]any{"title": nil}, "items", opts); err != nil {
|
||
t.Fatalf("Insert with nil (AbsentKey only) failed: %v", err)
|
||
}
|
||
// "" — допустимо
|
||
if err := db.Insert(map[string]any{"title": ""}, "items", opts); err != nil {
|
||
t.Fatalf("Insert with empty string (AbsentKey only) failed: %v", err)
|
||
}
|
||
// Отсутствие ключа — ошибка (поле не в записи)
|
||
err := db.Insert(map[string]any{"other": "x"}, "items", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected error for absent required key")
|
||
}
|
||
|
||
// Режим: только nil — AbsentKey и "" допустимы? Нет: AbsentKey при отсутствии — пусто.
|
||
// При EmptyModeNil: пусто только когда v==nil. AbsentKey при отсутствии ключа: exists=false.
|
||
// isEmptyByMode: if mode&EmptyModeAbsentKey && !exists return true. So if we use only EmptyModeNil,
|
||
// AbsentKey is not in the mask, so we don't return true for !exists. Good.
|
||
db2, cleanup2 := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||
{Name: "flag", Required: true, EmptyMode: linedb.EmptyModeNil},
|
||
})
|
||
defer cleanup2()
|
||
|
||
// Ключ отсутствует — при EmptyModeNil мы не проверяем AbsentKey, так что !exists не даёт return true.
|
||
// isEmptyByMode: проверяет mode&EmptyModeAbsentKey - если нет, то !exists не приводит к true.
|
||
// Потом mode&EmptyModeNil && v==nil - но v при !exists будет нулевым при v, exists := m[key]. v = nil, exists = false.
|
||
// Но мы заходим в v, exists := m[key]. Если !exists, v будет nil (zero value). So v==nil is true.
|
||
// And mode&EmptyModeNil - if we have only Nil, we'd return true. So absent key would still be "empty" if we access v.
|
||
// Actually no - we first check mode&EmptyModeAbsentKey && !exists - if that's not in mode, we skip.
|
||
// Then mode&EmptyModeNil && v==nil - for absent key, v is nil. So we'd return true. So absent key + Nil mode would still fail.
|
||
// The logic is: we return true (empty) if ANY of the conditions matching the mode are true. So:
|
||
// - AbsentKey + !exists -> empty
|
||
// - Nil + v==nil -> empty
|
||
// - ZeroValue + ("" or 0) -> empty
|
||
// For EmptyModeNil only: we don't check AbsentKey. We check Nil - and for absent key, v is nil (zero value from map lookup). So we'd return true. So absent key would still be empty. Good.
|
||
// For EmptyModeZeroValue only: we'd only check ZeroValue. Absent key: v is nil, we don't have a case for nil in the switch (we'd fall through and return false). So absent key would NOT be empty. And nil would not be empty. Only "" and 0 would be.
|
||
// Let me add a test for EmptyModeZeroValue: required field, value 0 should fail, value "" should fail, but absent key... would not fail? That might be surprising. Let me document it.
|
||
if err := db2.Insert(map[string]any{"flag": false}, "items", opts); err != nil {
|
||
t.Fatalf("Insert with false (bool, Nil mode) failed: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestLineDbRequiredFieldsUpdate проверяет, что Update не может установить пустое в required
|
||
func TestLineDbRequiredFieldsUpdate(t *testing.T) {
|
||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||
{Name: "email", Required: true},
|
||
})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"name": "a", "email": "a@test.com"}, "items", opts); err != nil {
|
||
t.Fatalf("Insert failed: %v", err)
|
||
}
|
||
|
||
// Ошибка — пытаемся установить email в ""
|
||
_, err := db.Update(map[string]any{"email": ""}, "items", map[string]any{"email": "a@test.com"}, opts)
|
||
if err == nil {
|
||
t.Fatal("Expected error when setting required field to empty")
|
||
}
|
||
|
||
// Успех — обновляем другое поле
|
||
_, err = db.Update(map[string]any{"name": "alice"}, "items", map[string]any{"email": "a@test.com"}, opts)
|
||
if err != nil {
|
||
t.Fatalf("Update non-required field failed: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestLineDbRequiredAndUniqueCombined проверяет комбинацию required и unique
|
||
func TestLineDbRequiredAndUniqueCombined(t *testing.T) {
|
||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||
{Name: "email", Required: true, Unique: true},
|
||
})
|
||
defer cleanup()
|
||
|
||
opts := linedb.LineDbAdapterOptions{}
|
||
|
||
if err := db.Insert(map[string]any{"email": "x@test.com"}, "items", opts); err != nil {
|
||
t.Fatalf("Insert failed: %v", err)
|
||
}
|
||
err := db.Insert(map[string]any{"email": "x@test.com"}, "items", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected unique violation")
|
||
}
|
||
err = db.Insert(map[string]any{"email": ""}, "items", opts)
|
||
if err == nil {
|
||
t.Fatal("Expected required violation for empty email")
|
||
}
|
||
}
|