Files
elowdb-go/tests/linedb_test.go
2026-04-07 15:04:38 +06:00

754 lines
26 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")
}
}