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 }