package cassandra import ( "context" "fmt" "strings" "github.com/gocql/gocql" ) // SAIIndexType 定義 SAI 索引類型 type SAIIndexType string const ( // SAIIndexTypeStandard 標準索引(等於查詢) SAIIndexTypeStandard SAIIndexType = "STANDARD" // SAIIndexTypeCollection 集合索引(用於 list、set、map) SAIIndexTypeCollection SAIIndexType = "COLLECTION" // SAIIndexTypeFullText 全文索引 SAIIndexTypeFullText SAIIndexType = "FULL_TEXT" ) // SAIIndexOptions 定義 SAI 索引選項 type SAIIndexOptions struct { IndexType SAIIndexType // 索引類型 IsAsync bool // 是否異步建立索引 CaseSensitive bool // 是否區分大小寫(用於全文索引) } // DefaultSAIIndexOptions 返回預設的 SAI 索引選項 func DefaultSAIIndexOptions() *SAIIndexOptions { return &SAIIndexOptions{ IndexType: SAIIndexTypeStandard, IsAsync: false, CaseSensitive: true, } } // CreateSAIIndex 建立 SAI 索引 // keyspace: keyspace 名稱 // table: 資料表名稱 // column: 欄位名稱 // indexName: 索引名稱(可選,如果為空則自動生成) // opts: 索引選項(可選,如果為 nil 則使用預設選項) func (db *DB) CreateSAIIndex(ctx context.Context, keyspace, table, column, indexName string, opts *SAIIndexOptions) error { // 檢查是否支援 SAI if !db.saiSupported { return ErrInvalidInput.WithError(fmt.Errorf("SAI is not supported in Cassandra version %s (requires 4.0.9+ or 5.0+)", db.version)) } // 驗證參數 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 opts == nil { opts = DefaultSAIIndexOptions() } // 生成索引名稱如果未提供 if indexName == "" { indexName = fmt.Sprintf("%s_%s_sai_idx", table, column) } // 構建 CREATE INDEX 語句 var stmt strings.Builder stmt.WriteString("CREATE CUSTOM INDEX IF NOT EXISTS ") stmt.WriteString(indexName) stmt.WriteString(" ON ") stmt.WriteString(keyspace) stmt.WriteString(".") stmt.WriteString(table) stmt.WriteString(" (") stmt.WriteString(column) stmt.WriteString(") USING 'StorageAttachedIndex'") // 添加選項 var options []string if opts.IsAsync { options = append(options, "'async'='true'") } // 根據索引類型添加特定選項 switch opts.IndexType { case SAIIndexTypeFullText: if !opts.CaseSensitive { options = append(options, "'case_sensitive'='false'") } else { options = append(options, "'case_sensitive'='true'") } case SAIIndexTypeCollection: // Collection 索引不需要額外選項 } // 如果有選項,添加到語句中 if len(options) > 0 { stmt.WriteString(" WITH OPTIONS = {") stmt.WriteString(strings.Join(options, ", ")) stmt.WriteString("}") } // 執行建立索引語句 query := db.session.Query(stmt.String(), nil). WithContext(ctx). Consistency(gocql.Quorum) err := query.ExecRelease() if err != nil { return ErrInvalidInput.WithError(fmt.Errorf("failed to create SAI index: %w", err)) } return nil } // DropSAIIndex 刪除 SAI 索引 // keyspace: keyspace 名稱 // indexName: 索引名稱 func (db *DB) DropSAIIndex(ctx context.Context, keyspace, indexName string) error { // 驗證參數 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) // 執行刪除索引語句 query := db.session.Query(stmt, nil). WithContext(ctx). Consistency(gocql.Quorum) err := query.ExecRelease() if err != nil { return ErrInvalidInput.WithError(fmt.Errorf("failed to drop SAI index: %w", err)) } return nil } // ListSAIIndexes 列出指定資料表的所有 SAI 索引 // keyspace: keyspace 名稱 // table: 資料表名稱 func (db *DB) ListSAIIndexes(ctx context.Context, keyspace, table string) ([]SAIIndexInfo, error) { // 驗證參數 if keyspace == "" { return nil, ErrInvalidInput.WithError(fmt.Errorf("keyspace is required")) } if table == "" { return nil, ErrInvalidInput.WithError(fmt.Errorf("table is required")) } // 查詢系統表獲取索引資訊 // system_schema.indexes 表的結構:keyspace_name, table_name, index_name, kind, options stmt := ` SELECT index_name, kind, options FROM system_schema.indexes WHERE keyspace_name = ? AND table_name = ? ` var indexes []SAIIndexInfo iter := db.session.Query(stmt, []string{"keyspace_name", "table_name"}). WithContext(ctx). Consistency(gocql.One). Bind(keyspace, table). Iter() var indexName, kind string var options map[string]string for iter.Scan(&indexName, &kind, &options) { // 檢查是否為 SAI 索引(kind = 'CUSTOM' 且 class_name 包含 StorageAttachedIndex) if kind == "CUSTOM" { if className, ok := options["class_name"]; ok && strings.Contains(className, "StorageAttachedIndex") { // 從 options 中提取 target(欄位名稱) columnName := "" if target, ok := options["target"]; ok { columnName = strings.Trim(target, "()\"'") } indexes = append(indexes, SAIIndexInfo{ Name: indexName, Type: "StorageAttachedIndex", Options: options, Column: columnName, }) } } } if err := iter.Close(); err != nil { return nil, ErrInvalidInput.WithError(fmt.Errorf("failed to list SAI indexes: %w", err)) } return indexes, nil } // SAIIndexInfo 表示 SAI 索引資訊 type SAIIndexInfo struct { Name string // 索引名稱 Type string // 索引類型 Options map[string]string // 索引選項 Column string // 索引欄位名稱 } // CheckSAIIndexExists 檢查 SAI 索引是否存在 // keyspace: keyspace 名稱 // indexName: 索引名稱 func (db *DB) CheckSAIIndexExists(ctx context.Context, keyspace, indexName string) (bool, error) { // 驗證參數 if keyspace == "" { return false, ErrInvalidInput.WithError(fmt.Errorf("keyspace is required")) } if indexName == "" { return false, ErrInvalidInput.WithError(fmt.Errorf("index name is required")) } // 查詢系統表檢查索引是否存在 stmt := ` SELECT index_name, kind, options FROM system_schema.indexes WHERE keyspace_name = ? AND index_name = ? LIMIT 1 ` var foundIndexName, kind string var options map[string]string err := db.session.Query(stmt, []string{"keyspace_name", "index_name"}). WithContext(ctx). Consistency(gocql.One). Bind(keyspace, indexName). Scan(&foundIndexName, &kind, &options) if err == gocql.ErrNotFound { return false, nil } if err != nil { return false, ErrInvalidInput.WithError(fmt.Errorf("failed to check SAI index existence: %w", err)) } // 檢查是否為 SAI 索引 if kind == "CUSTOM" { if className, ok := options["class_name"]; ok && strings.Contains(className, "StorageAttachedIndex") { return true, nil } } return false, nil } // WaitForSAIIndex 等待 SAI 索引建立完成(用於異步建立) // keyspace: keyspace 名稱 // indexName: 索引名稱 // maxWaitTime: 最大等待時間(秒) func (db *DB) WaitForSAIIndex(ctx context.Context, keyspace, indexName string, maxWaitTime int) error { // 驗證參數 if keyspace == "" { return ErrInvalidInput.WithError(fmt.Errorf("keyspace is required")) } if indexName == "" { return ErrInvalidInput.WithError(fmt.Errorf("index name is required")) } // 查詢索引狀態 // 注意:Cassandra 沒有直接的索引狀態查詢,這裡需要通過檢查索引是否可用來判斷 // 實際實作可能需要根據具體的 Cassandra 版本調整 // 簡單實作:檢查索引是否存在 exists, err := db.CheckSAIIndexExists(ctx, keyspace, indexName) if err != nil { return err } if !exists { return ErrInvalidInput.WithError(fmt.Errorf("index %s does not exist", indexName)) } // 注意:實際的等待邏輯可能需要查詢系統表或使用其他方法 // 這裡只是基本框架,實際使用時可能需要根據具體需求調整 return nil }