backend/pkg/library/cassandra/sai.go

248 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}