added unique fields

This commit is contained in:
2026-03-04 10:10:57 +06:00
parent f3f2b7b394
commit 0481cde1c3
8 changed files with 309 additions and 9 deletions

View File

@@ -184,7 +184,7 @@ func TestLineDbPartitioning(t *testing.T) {
}
}
// toString конвертирует значение в строку
// toString конвертирует значение в строку (для партиционирования)
func toString(value any) string {
switch v := value.(type) {
case string:
@@ -199,3 +199,276 @@ func toString(value any) string {
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))
}
}