backend/pkg/library/cassandra/sai.go

248 lines
7.4 KiB
Go
Raw Normal View History

2025-11-19 05:33:06 +00:00
package cassandra
import (
"context"
"fmt"
"strings"
"github.com/gocql/gocql"
)
// SAIIndexType 定義 SAI 索引類型
type SAIIndexType string
const (
// SAIIndexTypeStandard 標準索引(預設)
SAIIndexTypeStandard SAIIndexType = "standard"
// SAIIndexTypeFrozen 用於 frozen 類型
SAIIndexTypeFrozen SAIIndexType = "frozen"
)
// SAIIndexOptions 定義 SAI 索引選項
type SAIIndexOptions struct {
CaseSensitive *bool // 是否區分大小寫預設true
Normalize *bool // 是否正規化預設false
Analyzer string // 分析器(如 "StandardAnalyzer"
}
// SAIIndexInfo 表示 SAI 索引資訊
type SAIIndexInfo struct {
KeyspaceName string // Keyspace 名稱
TableName string // 表名稱
IndexName string // 索引名稱
ColumnName string // 欄位名稱
IndexType string // 索引類型
Options map[string]string // 索引選項
}
// CreateSAIIndex 建立 SAI 索引
// keyspace: keyspace 名稱,如果為空則使用預設 keyspace
// table: 表名稱
// column: 欄位名稱
// indexName: 索引名稱(可選,如果為空則自動生成)
// options: 索引選項(可選)
func (db *DB) CreateSAIIndex(ctx context.Context, keyspace, table, column string, indexName string, options *SAIIndexOptions) error {
if !db.saiSupported {
return ErrSAINotSupported
}
if keyspace == "" {
keyspace = db.defaultKeyspace
}
if keyspace == "" {
return ErrInvalidInput.WithError(fmt.Errorf("keyspace is required"))
}
if table == "" {
return ErrInvalidInput.WithError(fmt.Errorf("table is required"))
}
if column == "" {
return ErrInvalidInput.WithError(fmt.Errorf("column is required"))
}
// 生成索引名稱(如果未提供)
if indexName == "" {
indexName = fmt.Sprintf("%s_%s_%s_idx", table, column, "sai")
}
// 構建 CREATE INDEX 語句
stmt := fmt.Sprintf("CREATE INDEX %s ON %s.%s (%s) USING 'sai'", indexName, keyspace, table, column)
// 添加選項
if options != nil {
opts := make([]string, 0)
if options.CaseSensitive != nil {
opts = append(opts, fmt.Sprintf("'case_sensitive': %v", *options.CaseSensitive))
}
if options.Normalize != nil {
opts = append(opts, fmt.Sprintf("'normalize': %v", *options.Normalize))
}
if options.Analyzer != "" {
opts = append(opts, fmt.Sprintf("'analyzer': '%s'", options.Analyzer))
}
if len(opts) > 0 {
stmt += " WITH OPTIONS = {" + strings.Join(opts, ", ") + "}"
}
}
// 執行建立索引
q := db.session.Query(stmt, nil).WithContext(ctx).Consistency(gocql.Quorum)
if err := q.ExecRelease(); err != nil {
return ErrInvalidInput.WithTable(table).WithError(fmt.Errorf("failed to create SAI index: %w", err))
}
return nil
}
// DropSAIIndex 刪除 SAI 索引
// keyspace: keyspace 名稱,如果為空則使用預設 keyspace
// indexName: 索引名稱
func (db *DB) DropSAIIndex(ctx context.Context, keyspace, indexName string) error {
if !db.saiSupported {
return ErrSAINotSupported
}
if keyspace == "" {
keyspace = db.defaultKeyspace
}
if keyspace == "" {
return ErrInvalidInput.WithError(fmt.Errorf("keyspace is required"))
}
if indexName == "" {
return ErrInvalidInput.WithError(fmt.Errorf("index name is required"))
}
// 構建 DROP INDEX 語句
stmt := fmt.Sprintf("DROP INDEX IF EXISTS %s.%s", keyspace, indexName)
// 執行刪除索引
q := db.session.Query(stmt, nil).WithContext(ctx).Consistency(gocql.Quorum)
if err := q.ExecRelease(); err != nil {
return ErrInvalidInput.WithError(fmt.Errorf("failed to drop SAI index: %w", err))
}
return nil
}
// ListSAIIndexes 列出指定表的 SAI 索引
// keyspace: keyspace 名稱,如果為空則使用預設 keyspace
// table: 表名稱(可選,如果為空則列出所有表的索引)
func (db *DB) ListSAIIndexes(ctx context.Context, keyspace, table string) ([]SAIIndexInfo, error) {
if !db.saiSupported {
return nil, ErrSAINotSupported
}
if keyspace == "" {
keyspace = db.defaultKeyspace
}
if keyspace == "" {
return nil, ErrInvalidInput.WithError(fmt.Errorf("keyspace is required"))
}
// 構建查詢語句
// system_schema.indexes 表的欄位keyspace_name, table_name, index_name, kind, options, index_type
stmt := "SELECT keyspace_name, table_name, index_name, kind, options FROM system_schema.indexes WHERE keyspace_name = ?"
args := []interface{}{keyspace}
names := []string{"keyspace_name"}
if table != "" {
stmt += " AND table_name = ?"
args = append(args, table)
names = append(names, "table_name")
}
// 執行查詢
var indexes []SAIIndexInfo
iter := db.session.Query(stmt, names).Bind(args...).WithContext(ctx).Consistency(gocql.One).Iter()
var keyspaceName, tableName, indexName, kind string
var options map[string]string
for iter.Scan(&keyspaceName, &tableName, &indexName, &kind, &options) {
// 只處理 SAI 索引kind = 'CUSTOM' 且 index_type 在 options 中)
indexType, ok := options["class_name"]
if !ok || !strings.Contains(indexType, "StorageAttachedIndex") {
continue
}
// 從 options 中提取 column_name
// SAI 索引的 target 欄位在 options 中
columnName := ""
if target, ok := options["target"]; ok {
// target 格式通常是 "column_name" 或 "(column_name)"
columnName = strings.Trim(target, "()\"'")
}
indexes = append(indexes, SAIIndexInfo{
KeyspaceName: keyspaceName,
TableName: tableName,
IndexName: indexName,
ColumnName: columnName,
IndexType: "sai",
Options: options,
})
}
if err := iter.Close(); err != nil {
return nil, ErrInvalidInput.WithError(fmt.Errorf("failed to list SAI indexes: %w", err))
}
return indexes, nil
}
// GetSAIIndex 獲取指定索引的資訊
// keyspace: keyspace 名稱,如果為空則使用預設 keyspace
// indexName: 索引名稱
func (db *DB) GetSAIIndex(ctx context.Context, keyspace, indexName string) (*SAIIndexInfo, error) {
if !db.saiSupported {
return nil, ErrSAINotSupported
}
if keyspace == "" {
keyspace = db.defaultKeyspace
}
if keyspace == "" {
return nil, ErrInvalidInput.WithError(fmt.Errorf("keyspace is required"))
}
if indexName == "" {
return nil, ErrInvalidInput.WithError(fmt.Errorf("index name is required"))
}
// 構建查詢語句
stmt := "SELECT keyspace_name, table_name, index_name, kind, options FROM system_schema.indexes WHERE keyspace_name = ? AND index_name = ?"
args := []interface{}{keyspace, indexName}
names := []string{"keyspace_name", "index_name"}
var keyspaceName, tableName, idxName, kind string
var options map[string]string
// 執行查詢
err := db.session.Query(stmt, names).Bind(args...).WithContext(ctx).Consistency(gocql.One).Scan(&keyspaceName, &tableName, &idxName, &kind, &options)
if err != nil {
if err == gocql.ErrNotFound {
return nil, ErrNotFound.WithError(fmt.Errorf("index not found: %s", indexName))
}
return nil, ErrInvalidInput.WithError(fmt.Errorf("failed to get index: %w", err))
}
// 檢查是否為 SAI 索引
indexType, ok := options["class_name"]
if !ok || !strings.Contains(indexType, "StorageAttachedIndex") {
return nil, ErrInvalidInput.WithError(fmt.Errorf("index %s is not a SAI index", indexName))
}
// 從 options 中提取 column_name
columnName := ""
if target, ok := options["target"]; ok {
columnName = strings.Trim(target, "()\"'")
}
return &SAIIndexInfo{
KeyspaceName: keyspaceName,
TableName: tableName,
IndexName: idxName,
ColumnName: columnName,
IndexType: "sai",
Options: options,
}, nil
}