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