package cassandra import ( "context" "testing" "time" "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3/qb" "github.com/stretchr/testify/assert" "github.com/testcontainers/testcontainers-go" ) type Consistency struct { ID gocql.UUID `db:"id" partition:"true"` ConsistencyName string `db:"consistency_name" sai:"true"` // can editor ConsistencyType string `db:"consistency_type"` LastTaskID string `db:"last_task_id"` // ConsistencyTask ID Target string `db:"target"` // file name can editor Status string `db:"status" sai:"true"` ConsistencyMap string `db:"consistency_map"` // JSON string CreateAT int64 `db:"create_at"` UpdateAT int64 `db:"update_at"` } func (c *Consistency) TableName() string { return "consistency" } func TestInsert(t *testing.T) { ctx, cassandraContainer, host, port := setupCassandraContainer(t) defer cassandraContainer.Terminate(ctx) // 連線 hosts := []string{host} db, err := NewCassandraDB( hosts, WithPort(port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) assert.NotNil(t, db) // 建立 keyspace + table err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") assert.NoError(t, err, "should success ensure table") err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( id UUID, name TEXT, update_at TIMESTAMP, create_at TIMESTAMP, PRIMARY KEY ((id), name) );`) assert.NoError(t, err) now := time.Now() // 測試案例(可擴充) tests := []struct { name string input MonkeyEntity }{ { name: "insert George", input: MonkeyEntity{ ID: gocql.TimeUUID(), Name: "George", UpdateAt: now, CreateAt: now, }, }, { name: "insert Bob", input: MonkeyEntity{ ID: gocql.TimeUUID(), Name: "Bob", UpdateAt: now, CreateAt: now, }, }, { name: "insert Alice", input: MonkeyEntity{ ID: gocql.TimeUUID(), Name: "Alice", UpdateAt: now, CreateAt: now, }, }, } // 執行測試 for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := db.Insert(ctx, &tc.input, "my_keyspace") assert.NoError(t, err) // 驗證寫入 var name string q := db.GetSession().Query("SELECT name FROM my_keyspace.monkey_entity WHERE id = ?", []string{"name"}) err = q.Bind(tc.input.ID).GetRelease(&name) assert.NoError(t, err) assert.Equal(t, tc.input.Name, name) }) } } func TestGet(t *testing.T) { ctx, cassandraContainer, host, port := setupCassandraContainer(t) defer cassandraContainer.Terminate(ctx) db, err := NewCassandraDB( []string{host}, WithPort(port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) err = db.EnsureTable(` CREATE KEYSPACE IF NOT EXISTS my_keyspace WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 1 };`) assert.NoError(t, err) err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( id UUID, name TEXT, update_at TIMESTAMP, create_at TIMESTAMP, PRIMARY KEY ((id), name) );`) assert.NoError(t, err) now := time.Now() monkey := MonkeyEntity{ ID: gocql.TimeUUID(), Name: "George", UpdateAt: now, CreateAt: now, } // 插入一筆資料 err = db.Insert(ctx, &monkey, "my_keyspace") assert.NoError(t, err) tests := []struct { name string filter MonkeyEntity expect string }{ { name: "Get existing monkey", filter: MonkeyEntity{ID: monkey.ID, Name: monkey.Name}, expect: "George", }, { name: "Get non-existent monkey", filter: MonkeyEntity{ID: gocql.TimeUUID(), Name: "GG"}, expect: "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := tc.filter // 預設填入主鍵 err := db.Get(ctx, &result, "my_keyspace") if tc.expect == "" { assert.Error(t, err, "expected error for missing record") } else { assert.NoError(t, err) assert.Equal(t, tc.expect, result.Name) } }) } } func TestDelete(t *testing.T) { ctx, cassandraContainer, host, port := setupCassandraContainer(t) defer cassandraContainer.Terminate(ctx) db, err := NewCassandraDB( []string{host}, WithPort(port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) // 建立 keyspace & table err = db.EnsureTable(` CREATE KEYSPACE IF NOT EXISTS my_keyspace WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 1 };`) assert.NoError(t, err) err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( id UUID, name TEXT, update_at TIMESTAMP, create_at TIMESTAMP, PRIMARY KEY ((id), name) );`) assert.NoError(t, err) now := time.Now() monkey := MonkeyEntity{ ID: gocql.TimeUUID(), Name: "DeleteMe", UpdateAt: now, CreateAt: now, } // 插入資料 err = db.Insert(ctx, &monkey, "my_keyspace") assert.NoError(t, err) // 先確認有插入成功 verify := MonkeyEntity{ID: monkey.ID, Name: monkey.Name} err = db.Get(ctx, &verify, "my_keyspace") assert.NoError(t, err) assert.Equal(t, "DeleteMe", verify.Name) // 執行刪除 err = db.Delete(ctx, &monkey, "my_keyspace") assert.NoError(t, err) // 再查,應該查不到 result := MonkeyEntity{ID: monkey.ID, Name: monkey.Name} err = db.Get(ctx, &result, "my_keyspace") assert.Error(t, err, "expected error because record should be deleted") } func TestUpdate(t *testing.T) { ctx, cassandraContainer, host, port := setupCassandraContainer(t) defer cassandraContainer.Terminate(ctx) db, err := NewCassandraDB( []string{host}, WithPort(port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) // 建立 keyspace & table err = db.EnsureTable(` CREATE KEYSPACE IF NOT EXISTS my_keyspace WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 1 };`) assert.NoError(t, err) err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( id UUID, name TEXT, update_at TIMESTAMP, create_at TIMESTAMP, PRIMARY KEY ((id), name) );`) assert.NoError(t, err) now := time.Now() id := gocql.TimeUUID() // Step 1: 插入初始資料 monkey := MonkeyEntity{ ID: id, Name: "OldName", UpdateAt: now, CreateAt: now, } err = db.Insert(ctx, &monkey, "my_keyspace") assert.NoError(t, err) // Step 2: 更新 UpdateAt 欄位(模擬只更新一欄) updatedTime := now.Add(10 * time.Minute) updateDoc := MonkeyEntity{ ID: id, Name: "OldName", // 主鍵 UpdateAt: updatedTime, // CreateAt 是零值,不會被更新 } err = db.Update(ctx, &updateDoc, "my_keyspace") assert.NoError(t, err) // Step 3: 查詢回來驗證更新 result := MonkeyEntity{ ID: id, Name: "OldName", } err = db.Get(ctx, &result, "my_keyspace") assert.NoError(t, err) assert.WithinDuration(t, updatedTime, result.UpdateAt, time.Second) assert.WithinDuration(t, now, result.CreateAt, time.Second) // 未被更新 } func setupTestQueryBuilder(t *testing.T) (*CassandraDB, testcontainers.Container, context.Context) { ctx, cassandraContainer, host, port := setupCassandraContainer(t) // 連線 hosts := []string{host} db, err := NewCassandraDB( hosts, WithPort(port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) assert.NotNil(t, db) // 建立 keyspace + table err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") assert.NoError(t, err, "should success ensure table") err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.consistency ( id UUID, consistency_name TEXT, last_task_id TEXT, target TEXT, status TEXT, consistency_type TEXT, consistency_map TEXT, create_at BIGINT, update_at BIGINT, PRIMARY KEY ((id)) );`) assert.NoError(t, err) return db, cassandraContainer, ctx } func insertSampleConsistency(t *testing.T, db *CassandraDB, ctx context.Context, keyspace string) *Consistency { c := &Consistency{ ID: gocql.TimeUUID(), ConsistencyName: "query-test", LastTaskID: "task-1", Target: "test.csv", Status: "Running", ConsistencyType: "simple", ConsistencyMap: `{"example": "value"}`, CreateAT: time.Now().UnixNano(), UpdateAT: time.Now().UnixNano(), } err := db.Insert(ctx, c, keyspace) assert.NoError(t, err) return c } func TestQueryBuilder_WithWhere(t *testing.T) { db, def, ctx := setupTestQueryBuilder(t) defer def.Terminate(ctx) saved := insertSampleConsistency(t, db, ctx, "my_keyspace") t.Run("query by id", func(t *testing.T) { var results []*Consistency e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( ctx, &Consistency{}, &results, "my_keyspace", WithWhere( []qb.Cmp{qb.Eq(field)}, map[string]any{field: saved.ID.String()}, ), ) assert.NoError(t, err) assert.NotEmpty(t, results) found := false for _, r := range results { if r.ID == saved.ID { found = true break } } assert.True(t, found, "should find inserted consistency") }) t.Run("query with unmatched id", func(t *testing.T) { var results []*Consistency e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( ctx, &Consistency{}, &results, "my_keyspace", WithWhere( []qb.Cmp{qb.Eq(field)}, map[string]any{field: "NonExist"}, ), ) assert.Error(t, err) assert.Empty(t, results) }) t.Run("query by in", func(t *testing.T) { var results []*Consistency e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( ctx, &Consistency{}, &results, "my_keyspace", WithWhere( []qb.Cmp{qb.In(field)}, map[string]any{field: []gocql.UUID{saved.ID}}, ), ) assert.NoError(t, err) assert.NotEmpty(t, results) found := false for _, r := range results { if r.ID == saved.ID { found = true break } } assert.True(t, found, "should find inserted consistency") }) t.Run("query by one is not in", func(t *testing.T) { var results []*Consistency e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( ctx, &Consistency{}, &results, "my_keyspace", WithWhere( []qb.Cmp{qb.In(field)}, map[string]any{field: []gocql.UUID{saved.ID, gocql.TimeUUID()}}, ), ) assert.NoError(t, err) assert.NotEmpty(t, results) found := false for _, r := range results { if r.ID == saved.ID { found = true break } } assert.True(t, found, "should find inserted consistency") }) t.Run("query get all", func(t *testing.T) { var results []*Consistency e := &Consistency{} err := db.QueryBuilder( ctx, e, &results, "my_keyspace", ) assert.NoError(t, err) assert.NotEmpty(t, results) found := false for _, r := range results { if r.ID == saved.ID { found = true break } } assert.True(t, found, "should find inserted consistency") }) } // ====================================================================================================================== func TestSearchBySAIFields(t *testing.T) { container, err := initCassandraContainer("5.0.4") ctx := context.Background() defer container.Container.Terminate(ctx) // 連線 hosts := []string{container.Host} db, err := NewCassandraDB( hosts, WithPort(container.Port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) assert.NotNil(t, db) // 建立 keyspace + table err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") assert.NoError(t, err, "should success ensure table") err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.consistency ( id UUID, consistency_name TEXT, last_task_id TEXT, target TEXT, status TEXT, consistency_type TEXT, consistency_map TEXT, create_at BIGINT, update_at BIGINT, PRIMARY KEY ((id)) );`) assert.NoError(t, err) _ = db.AutoCreateSAIIndexes(&Consistency{}, "my_keyspace") c := &Consistency{ ID: gocql.TimeUUID(), ConsistencyName: "query-test", LastTaskID: "task-1", Target: "test.csv", Status: "Running", ConsistencyType: "simple", ConsistencyMap: `{"example": "value"}`, CreateAT: time.Now().UnixNano(), UpdateAT: time.Now().UnixNano(), } err = db.Insert(ctx, c, "my_keyspace") assert.NoError(t, err) results := []Consistency{} err = db.SearchBySAIFields(ctx, &Consistency{}, &results, "my_keyspace", []qb.Cmp{qb.Eq("consistency_name")}, map[string]any{"consistency_name": "query-test"}, ) assert.NoError(t, err) assert.Len(t, results, 1) results2 := []Consistency{} err = db.SearchBySAIFields(ctx, &Consistency{}, &results, "my_keyspace", []qb.Cmp{qb.Eq("consistency_name")}, map[string]any{"consistency_name": "vvvvvvv"}, ) assert.NoError(t, err) assert.Len(t, results2, 0) }