added unique fields
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
grok
|
||||
6
Makefile
6
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
|
||||
|
||||
@@ -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"}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
1
testdata/test.jsonl
vendored
Normal file
1
testdata/test.jsonl
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"debug":true,"id":1,"name":"test","updated":true,"value":456}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user