package tests import ( "os" "testing" "time" "linedb/pkg/linedb" ) // mockMemcached — in-memory реализация MemcachedClient для тестов. type mockMemcached struct { data map[string][]byte } func newMockMemcached() *mockMemcached { return &mockMemcached{data: make(map[string][]byte)} } func (m *mockMemcached) Get(key string) ([]byte, error) { v, ok := m.data[key] if !ok { return nil, nil } return v, nil } func (m *mockMemcached) Set(key string, value []byte, _ int) error { m.data[key] = value return nil } func (m *mockMemcached) Delete(key string) error { delete(m.data, key) return nil } func setupIndexedCollection(t *testing.T, indexStore linedb.IndexStore) (*linedb.LineDb, func()) { t.Helper() os.RemoveAll("./testdata") opts := &linedb.LineDbOptions{} if indexStore != nil { opts.IndexStore = indexStore } initOptions := &linedb.LineDbInitOptions{ CacheSize: 100, CacheTTL: time.Minute, DBFolder: "./data/test-linedb-index", Collections: []linedb.JSONLFileOptions{ { CollectionName: "users", AllocSize: 256, IndexedFields: []string{"id", "email", "name"}, }, }, } db := linedb.NewLineDb(opts) if err := db.Init(false, initOptions); err != nil { t.Fatalf("Init failed: %v", err) } return db, func() { db.Close(); os.RemoveAll("./testdata") } } func TestIndexInMemoryReadByFilter(t *testing.T) { db, cleanup := setupIndexedCollection(t, nil) // nil = auto in-memory 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) } if err := db.Insert(map[string]any{"name": "charlie", "email": "charlie@test.com"}, "users", opts); err != nil { t.Fatalf("Insert 3 failed: %v", err) } // Поиск по индексированному полю email found, err := db.ReadByFilter("email:bob@test.com,name:bob", "users", opts) // found, err := db.ReadByFilter(map[string]any{"email": "bob@test.com"}, "users", opts) if err != nil { t.Fatalf("ReadByFilter failed: %v", err) } if len(found) != 1 { t.Fatalf("Expected 1 record for email bob@test.com, got %d", len(found)) } if m, ok := found[0].(map[string]any); ok { if m["name"] != "bob" { t.Errorf("Expected name bob, got %v", m["name"]) } } // Поиск по name found2, err := db.ReadByFilter(map[string]any{"name": "alice"}, "users", opts) if err != nil { t.Fatalf("ReadByFilter name failed: %v", err) } if len(found2) != 1 { t.Fatalf("Expected 1 record for name alice, got %d", len(found2)) } } // TestIndexEncodedCollectionCache проверяет шифрованную коллекцию и двойное чтение одного фильтра (кэш). func TestIndexEncodedCollectionCache(t *testing.T) { err := os.RemoveAll("./data/test-linedb-index-enc") if err != nil { t.Logf("RemoveAll failed: %v", err) } initOptions := &linedb.LineDbInitOptions{ CacheSize: 100, CacheTTL: time.Minute*10, DBFolder: "./data/test-linedb-index-enc", Collections: []linedb.JSONLFileOptions{ { CollectionName: "users", AllocSize: 512, IndexedFields: []string{"id", "email", "name"}, Encode: false, 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": "alice", "email": "alice@secret.com"}, "users", opts); err != nil { t.Fatalf("Insert 1 failed: %v", err) } if err := db.Insert(map[string]any{"name": "bob", "email": "bob@secret.com"}, "users", opts); err != nil { t.Fatalf("Insert 2 failed: %v", err) } filter := map[string]any{"email": "bob@secret.com"} // Первое чтение — из файла (с дешифровкой) found1, err := db.ReadByFilter(filter, "users", opts) if err != nil { t.Fatalf("ReadByFilter (1st) failed: %v", err) } if len(found1) != 1 { t.Fatalf("Expected 1 record, got %d", len(found1)) } if m, ok := found1[0].(map[string]any); ok { if m["name"] != "bob" || m["email"] != "bob@secret.com" { t.Errorf("Expected name=bob email=bob@secret.com, got %+v", m) } } cacheSize1 := db.GetActualCacheSize() if cacheSize1 < 1 { t.Errorf("Expected cache to have at least 1 entry after first read, got %d", cacheSize1) } // Второе чтение — из кэша (должно вернуть те же данные без повторного чтения файла) found2, err := db.ReadByFilter(filter, "users", opts) if err != nil { t.Fatalf("ReadByFilter (2nd, cached) failed: %v", err) } if len(found2) != 1 { t.Fatalf("Expected 1 record on second read, got %d", len(found2)) } if m, ok := found2[0].(map[string]any); ok { if m["name"] != "bob" || m["email"] != "bob@secret.com" { t.Errorf("Expected name=bob email=bob@secret.com on cached read, got %+v", m) } } // Кэш не должен расти при повторном запросе с тем же ключом cacheSize2 := db.GetActualCacheSize() if cacheSize2 != cacheSize1 { t.Errorf("Cache size should stay same after cache hit: was %d, now %d", cacheSize1, cacheSize2) } // Обновляем запись (name) и проверяем, что кэш и индекс обновились _, err = db.Update( map[string]any{"name": "bob_updated"}, "users", map[string]any{"email": "bob@secret.com"}, opts, ) if err != nil { t.Fatalf("Update failed: %v", err) } // После Update кэш сбрасывается — читаем снова, чтобы заполнить кэш актуальными данными found3, err := db.ReadByFilter(filter, "users", opts) if err != nil { t.Fatalf("ReadByFilter after update failed: %v", err) } if len(found3) != 1 { t.Fatalf("Expected 1 record after update, got %d", len(found3)) } if m, ok := found3[0].(map[string]any); ok && m["name"] != "bob_updated" { t.Errorf("Expected name=bob_updated after update, got %v", m["name"]) } // Проверяем сырой кэш: в нём должна быть запись с name=bob_updated rawCache := db.GetCacheForTest("give_me_cache") if len(rawCache) == 0 { t.Error("Expected cache to have entries after read") } var foundInCache bool for _, v := range rawCache { arr, ok := v.([]any) if !ok || len(arr) != 1 { continue } m, ok := arr[0].(map[string]any) if !ok { continue } if m["email"] == "bob@secret.com" { foundInCache = true if m["name"] != "bob_updated" { t.Errorf("Expected cached name=bob_updated, got %v", m["name"]) } break } } if !foundInCache { t.Error("Expected to find bob record in raw cache with updated name") } // Проверяем индекс: по email bob@secret.com должна быть одна строка, по name bob_updated — тоже idxSnapshot := db.GetIndexSnapshotForTest("give_me_cache") if len(idxSnapshot) == 0 { t.Error("Expected index snapshot to have entries") } if emailIdx, ok := idxSnapshot["users:email"].(map[string][]int); ok { if lines, ok := emailIdx["bob@secret.com"]; !ok || len(lines) != 1 { t.Errorf("Expected index users:email bob@secret.com to have 1 line, got %v", emailIdx["bob@secret.com"]) } } if nameIdx, ok := idxSnapshot["users:name"].(map[string][]int); ok { if lines, ok := nameIdx["bob_updated"]; !ok || len(lines) != 1 { t.Errorf("Expected index users:name bob_updated to have 1 line, got %v", nameIdx["bob_updated"]) } } // Удаляем запись и проверяем, что в кэше и в индексе её больше нет _, err = db.Delete(map[string]any{"email": "bob@secret.com"}, "users", opts) if err != nil { t.Fatalf("Delete failed: %v", err) } // После Delete кэш сбрасывается — читаем снова found4, err := db.ReadByFilter(filter, "users", opts) if err != nil { t.Fatalf("ReadByFilter after delete failed: %v", err) } if len(found4) != 0 { t.Fatalf("Expected 0 records after delete, got %d", len(found4)) } // В сыром кэше не должно остаться записи bob rawCache2 := db.GetCacheForTest("give_me_cache") for _, v := range rawCache2 { arr, ok := v.([]any) if !ok || len(arr) != 1 { continue } if m, ok := arr[0].(map[string]any); ok && m["email"] == "bob@secret.com" { t.Error("Cached result after delete should not contain bob record") } } // Индекс: bob@secret.com и bob_updated не должны быть в индексе (или пустые срезы) idxSnapshot2 := db.GetIndexSnapshotForTest("give_me_cache") if emailIdx, ok := idxSnapshot2["users:email"].(map[string][]int); ok { if lines, has := emailIdx["bob@secret.com"]; has && len(lines) > 0 { t.Errorf("After delete, index users:email bob@secret.com should be empty, got %v", lines) } } if nameIdx, ok := idxSnapshot2["users:name"].(map[string][]int); ok { if lines, has := nameIdx["bob_updated"]; has && len(lines) > 0 { t.Errorf("After delete, index users:name bob_updated should be empty, got %v", lines) } } } func TestIndexExplicitInMemory(t *testing.T) { store := linedb.NewInMemoryIndexStore() db, cleanup := setupIndexedCollection(t, store) defer cleanup() opts := linedb.LineDbAdapterOptions{} if err := db.Insert(map[string]any{"name": "x", "email": "x@y.com"}, "users", opts); err != nil { t.Fatalf("Insert failed: %v", err) } found, err := db.ReadByFilter(map[string]any{"email": "x@y.com"}, "users", opts) if err != nil { t.Fatalf("ReadByFilter failed: %v", err) } if len(found) != 1 { t.Fatalf("Expected 1 record, got %d", len(found)) } } func TestIndexMemcachedStore(t *testing.T) { mock := newMockMemcached() store, err := linedb.NewMemcachedIndexStore(linedb.MemcachedIndexStoreOptions{ Client: mock, }) if err != nil { t.Fatalf("NewMemcachedIndexStore: %v", err) } db, cleanup := setupIndexedCollection(t, store) defer cleanup() opts := linedb.LineDbAdapterOptions{} if err := db.Insert(map[string]any{"name": "mem", "email": "mem@test.com"}, "users", opts); err != nil { t.Fatalf("Insert failed: %v", err) } found, err := db.ReadByFilter(map[string]any{"email": "mem@test.com"}, "users", opts) if err != nil { t.Fatalf("ReadByFilter failed: %v", err) } if len(found) != 1 { t.Fatalf("Expected 1 record, got %d", len(found)) } // Проверяем, что в mockMemcached что-то записалось if len(mock.data) == 0 { t.Error("Expected memcached store to have data") } } func TestIndexUpdateRebuild(t *testing.T) { db, cleanup := setupIndexedCollection(t, nil) 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) } _, err := db.Update( map[string]any{"email": "a_updated@test.com"}, "users", map[string]any{"email": "a@test.com"}, opts, ) if err != nil { t.Fatalf("Update failed: %v", err) } // Старый email не должен находиться old, _ := db.ReadByFilter(map[string]any{"email": "a@test.com"}, "users", opts) if len(old) != 0 { t.Fatalf("Expected 0 records for old email, got %d", len(old)) } // Новый — должен newFound, _ := db.ReadByFilter(map[string]any{"email": "a_updated@test.com"}, "users", opts) if len(newFound) != 1 { t.Fatalf("Expected 1 record for new email, got %d", len(newFound)) } } func TestIndexDeleteRebuild(t *testing.T) { db, cleanup := setupIndexedCollection(t, nil) defer cleanup() opts := linedb.LineDbAdapterOptions{} if err := db.Insert(map[string]any{"name": "del", "email": "del@test.com"}, "users", opts); err != nil { t.Fatalf("Insert failed: %v", err) } if err := db.Insert(map[string]any{"name": "keep", "email": "keep@test.com"}, "users", opts); err != nil { t.Fatalf("Insert 2 failed: %v", err) } _, err := db.Delete(map[string]any{"email": "del@test.com"}, "users", opts) if err != nil { t.Fatalf("Delete failed: %v", err) } found, _ := db.ReadByFilter(map[string]any{"email": "del@test.com"}, "users", opts) if len(found) != 0 { t.Fatalf("Expected 0 after delete, got %d", len(found)) } kept, _ := db.ReadByFilter(map[string]any{"email": "keep@test.com"}, "users", opts) if len(kept) != 1 { t.Fatalf("Expected 1 kept record, got %d", len(kept)) } } func TestIndexNoIndexedFields(t *testing.T) { os.RemoveAll("./testdata") initOptions := &linedb.LineDbInitOptions{ CacheSize: 100, CacheTTL: time.Minute, DBFolder: "./testdata", Collections: []linedb.JSONLFileOptions{ { CollectionName: "users", AllocSize: 256, IndexedFields: nil, // без индексов }, }, } 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") }() if err := db.Insert(map[string]any{"name": "a", "email": "a@test.com"}, "users", linedb.LineDbAdapterOptions{}); err != nil { t.Fatalf("Insert failed: %v", err) } // ReadByFilter всё равно работает (полный скан) found, err := db.ReadByFilter(map[string]any{"email": "a@test.com"}, "users", linedb.LineDbAdapterOptions{}) if err != nil { t.Fatalf("ReadByFilter failed: %v", err) } if len(found) != 1 { t.Fatalf("Expected 1 record, got %d", len(found)) } }