248 lines
7.4 KiB
Go
248 lines
7.4 KiB
Go
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
|
||
}
|