package core import ( "database/sql" "fmt" ) // DetectCycle checks if adding a directed edge (from -> to) would create a cycle // in a directed graph stored in a SQLite table. // It performs BFS from toNode following edges where the filter column is non-empty. // If it reaches fromNode, a cycle exists. // // Parameters: // - conn: open *sql.DB connection // - table: table name containing the edges (e.g. "relations") // - fromCol: column name for edge source (e.g. "from_entity") // - toCol: column name for edge destination (e.g. "to_entity") // - filterCol: column name that must be non-empty for causal edges (e.g. "via"); pass "" to consider all edges // - fromNode: source node of the proposed new edge // - toNode: destination node of the proposed new edge func DetectCycle(conn *sql.DB, table, fromCol, toCol, filterCol, fromNode, toNode string) error { if fromNode == "" || toNode == "" { return nil } var query string if filterCol != "" { query = fmt.Sprintf("SELECT %s FROM %s WHERE %s = ? AND %s != ''", toCol, table, fromCol, filterCol) } else { query = fmt.Sprintf("SELECT %s FROM %s WHERE %s = ?", toCol, table, fromCol) } visited := map[string]bool{} queue := []string{toNode} for len(queue) > 0 { current := queue[0] queue = queue[1:] if visited[current] { continue } visited[current] = true if current == fromNode { return fmt.Errorf("cycle detected: adding edge %s -> %s would create a cycle", fromNode, toNode) } rows, err := conn.Query(query, current) if err != nil { return fmt.Errorf("querying %s for cycle detection: %w", table, err) } for rows.Next() { var next string if err := rows.Scan(&next); err != nil { rows.Close() return err } if !visited[next] { queue = append(queue, next) } } rows.Close() } return nil }