567 lines
13 KiB
Go
567 lines
13 KiB
Go
|
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)
|
||
|
}
|