package db import ( "fmt" "context" "database/sql" ) type Repo struct{ DB *sql.DB } func NewRepo(db *sql.DB) *Repo { return &Repo{DB: db} } // --- Conversations --- const qCreateConversation = ` INSERT INTO conversations (owner_id, title) VALUES (?, ?); ` func (r *Repo) CreateConversation(ctx context.Context, ownerID int64, title string) (int64, error) { res, err := r.DB.ExecContext(ctx, qCreateConversation, ownerID, title) if err != nil { return 0, err } return res.LastInsertId() } const qListConversations = ` SELECT id, owner_id, title, created_at FROM conversations WHERE owner_id = ? ORDER BY created_at DESC, id DESC; ` func (r *Repo) ListConversations(ctx context.Context, ownerID int64) (*sql.Rows, error) { return r.DB.QueryContext(ctx, qListConversations, ownerID) } // --- Nodes (commits) --- const qCreateNode = ` INSERT INTO nodes (conversation_id, author_kind, content) VALUES (?, ?, ?); ` func (r *Repo) CreateNode(ctx context.Context, convID int64, authorKind, content string) (int64, error) { res, err := r.DB.ExecContext(ctx, qCreateNode, convID, authorKind, content) if err != nil { return 0, err } return res.LastInsertId() } // --- Branches --- const qCreateBranch = ` INSERT INTO branches (conversation_id, name, head_node_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE head_node_id = VALUES(head_node_id); ` func (r *Repo) CreateOrUpdateBranch(ctx context.Context, convID int64, name string, headNodeID int64) (int64, error) { res, err := r.DB.ExecContext(ctx, qCreateBranch, convID, name, headNodeID) if err != nil { return 0, err } return res.LastInsertId() } const qGetBranch = ` SELECT id, conversation_id, name, head_node_id, created_at FROM branches WHERE conversation_id = ? AND name = ? LIMIT 1; ` func (r *Repo) GetBranch(ctx context.Context, convID int64, name string) *sql.Row { return r.DB.QueryRowContext(ctx, qGetBranch, convID, name) } const qMoveBranchHead = ` UPDATE branches SET head_node_id = ? WHERE id = ?; ` func (r *Repo) MoveBranchHead(ctx context.Context, branchID, newHead int64) error { _, err := r.DB.ExecContext(ctx, qMoveBranchHead, newHead, branchID) return err } // --- Edges (DAG) --- const qCycleGuard = ` WITH RECURSIVE downchain AS ( SELECT e.child_id FROM edges e WHERE e.parent_id = ? UNION ALL SELECT e.child_id FROM edges e JOIN downchain d ON d.child_id = e.parent_id ) SELECT 1 FROM downchain WHERE child_id = ? LIMIT 1; ` const qInsertEdge = `INSERT IGNORE INTO edges (parent_id, child_id) VALUES (?, ?);` func (r *Repo) LinkEdgeCycleSafe(ctx context.Context, tx *sql.Tx, parentID, childID int64) error { // cycle check: is parent reachable from child already? var one int err := tx.QueryRowContext(ctx, qCycleGuard, childID, parentID).Scan(&one) if err != nil && err != sql.ErrNoRows { return err } if err == nil { // found a row -> would create a cycle return fmt.Errorf("cycle detected: %d -> %d", parentID, childID) } _, err = tx.ExecContext(ctx, qInsertEdge, parentID, childID) return err } // --- Ancestor step (latest parent) --- const qLatestParent = ` SELECT p.id, p.conversation_id, p.author_kind, p.content, p.created_at FROM edges e JOIN nodes p ON p.id = e.parent_id WHERE e.child_id = ? ORDER BY p.created_at DESC, p.id DESC LIMIT 1; ` func (r *Repo) LatestParent(ctx context.Context, childID int64) *sql.Row { return r.DB.QueryRowContext(ctx, qLatestParent, childID) }