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
|
|||
|
|
}
|