420 lines
14 KiB
Go
420 lines
14 KiB
Go
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][]linedb.IndexPosition); ok {
|
||
if positions, ok := emailIdx["bob@secret.com"]; !ok || len(positions) != 1 {
|
||
t.Errorf("Expected index users:email bob@secret.com to have 1 position, got %v", emailIdx["bob@secret.com"])
|
||
}
|
||
}
|
||
if nameIdx, ok := idxSnapshot["users:name"].(map[string][]linedb.IndexPosition); ok {
|
||
if positions, ok := nameIdx["bob_updated"]; !ok || len(positions) != 1 {
|
||
t.Errorf("Expected index users:name bob_updated to have 1 position, 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][]linedb.IndexPosition); ok {
|
||
if positions, has := emailIdx["bob@secret.com"]; has && len(positions) > 0 {
|
||
t.Errorf("After delete, index users:email bob@secret.com should be empty, got %v", positions)
|
||
}
|
||
}
|
||
if nameIdx, ok := idxSnapshot2["users:name"].(map[string][]linedb.IndexPosition); ok {
|
||
if positions, has := nameIdx["bob_updated"]; has && len(positions) > 0 {
|
||
t.Errorf("After delete, index users:name bob_updated should be empty, got %v", positions)
|
||
}
|
||
}
|
||
}
|
||
|
||
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))
|
||
}
|
||
}
|