diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd980f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +grok \ No newline at end of file diff --git a/Makefile b/Makefile index a2a596f..adbdfd6 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,12 @@ build: test: go test ./tests/... -v +# Запуск тестов с отладкой +# go test -run TestLineDbBasic ./tests/... -v +# make test-single TEST=TestLineDbBasic +test-single: + go test -run $(TEST) ./tests/... -v + # Очистка clean: rm -rf testdata diff --git a/data/users.jsonl b/examples/basic/data/users.jsonl similarity index 81% rename from data/users.jsonl rename to examples/basic/data/users.jsonl index 4d708d7..bb063be 100644 --- a/data/users.jsonl +++ b/examples/basic/data/users.jsonl @@ -1,2 +1,2 @@ -{"age":31,"created":"2026-03-03T14:27:14+06:00","email":"john@example.com","id":1,"name":"John Doe"} -{"age":25,"created":"2026-03-03T14:27:14+06:00","email":"jane@example.com","id":2,"name":"Jane Smith"} +{"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"} diff --git a/examples/basic/main.go b/examples/basic/main.go index 34bf500..ac5b888 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -21,7 +21,7 @@ func main() { initOptions := &linedb.LineDbInitOptions{ CacheSize: 1000, CacheTTL: 5 * time.Minute, - DBFolder: "./data", + DBFolder: "./examples/basic/data", Collections: []linedb.JSONLFileOptions{ { CollectionName: "users", @@ -41,21 +41,31 @@ func main() { } defer db.Close() + //delete all entries from users collection + if _, err := db.Delete(map[string]any{}, "users", linedb.LineDbAdapterOptions{}); err != nil { + log.Fatalf("Failed to delete users: %v", err) + } + + fmt.Println("All entries deleted from users collection") + // Создаем тестовых пользователей users := []any{ map[string]any{ + "id": 1, "name": "John Doe", "email": "john@example.com", "age": 30, "created": time.Now().Format(time.RFC3339), }, map[string]any{ + "id": 2, "name": "Jane Smith", "email": "jane@example.com", "age": 25, "created": time.Now().Format(time.RFC3339), }, map[string]any{ + "id": 3, "name": "Bob Johnson", "email": "bob@example.com", "age": 35, @@ -94,17 +104,24 @@ func main() { } // Обновляем пользователя - updateData := map[string]any{"age": 31} + updateData := map[string]any{"age": 31, "email": "jane@example.com"} updateFilter := map[string]any{"email": "john@example.com"} updatedUsers, err := db.Update(updateData, "users", updateFilter, linedb.LineDbAdapterOptions{}) if err != nil { - log.Fatalf("Failed to update user: %v", err) + log.Printf("Failed to update user: %v", err) + } + + updateData = map[string]any{"age": 31, "email": "john@example.com"} + updateFilter = map[string]any{"email": "john@example.com"} + updatedUsers, err = db.Update(updateData, "users", updateFilter, linedb.LineDbAdapterOptions{}) + if err != nil { + log.Printf("Failed again to update user: %v", err) } fmt.Printf("Updated %d users\n", len(updatedUsers)) // Удаляем пользователя - deleteFilter := map[string]any{"email": "bob@example.com"} + deleteFilter := map[string]any{"email": "jane@example.com"} deletedUsers, err := db.Delete(deleteFilter, "users", linedb.LineDbAdapterOptions{}) if err != nil { log.Fatalf("Failed to delete user: %v", err) diff --git a/examples/insert/data/test-linedb-insert/unique_users.jsonl b/examples/insert/data/test-linedb-insert/unique_users.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/pkg/linedb/line_db.go b/pkg/linedb/line_db.go index 8457963..20f471a 100644 --- a/pkg/linedb/line_db.go +++ b/pkg/linedb/line_db.go @@ -362,8 +362,10 @@ func (db *LineDb) Select(filter any, collectionName string, options LineDbAdapte // ReadByFilter читает записи по фильтру func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDbAdapterOptions) ([]any, error) { - db.mutex.RLock() - defer db.mutex.RUnlock() + if !options.InTransaction { + db.mutex.RLock() + defer db.mutex.RUnlock() + } if collectionName == "" { collectionName = db.getFirstCollection() diff --git a/testdata/test.jsonl b/testdata/test.jsonl new file mode 100644 index 0000000..b7c987f --- /dev/null +++ b/testdata/test.jsonl @@ -0,0 +1 @@ +{"debug":true,"id":1,"name":"test","updated":true,"value":456} diff --git a/tests/linedb_test.go b/tests/linedb_test.go index 4842208..39a7fd6 100644 --- a/tests/linedb_test.go +++ b/tests/linedb_test.go @@ -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)) + } +}