package db import ( "database/sql" "errors" "github.com/bluesky-social/indigo/atproto/syntax" ) type Repo struct { Knot string Owner syntax.DID Rkey syntax.RecordKey RepoDid syntax.DID CreatedAt string } func (d *DB) AddRepo(repo Repo) error { var createdAt sql.NullString if repo.CreatedAt != "" { createdAt = sql.NullString{String: repo.CreatedAt, Valid: true} } _, err := d.Exec( `insert into repos (knot, owner, rkey, repo_did, created_at) values (?, ?, ?, ?, ?) on conflict(owner, rkey) do update set knot = excluded.knot, repo_did = excluded.repo_did, created_at = coalesce(excluded.created_at, repos.created_at)`, repo.Knot, repo.Owner.String(), repo.Rkey.String(), repo.RepoDid.String(), createdAt, ) return err } func (d *DB) CollapseRepoSiblings(owner, repoDid syntax.DID) (int64, error) { res, err := d.Exec( `delete from repos where owner = ? and repo_did = ? and ( (created_at is null and exists ( select 1 from repos r2 where r2.owner = repos.owner and r2.repo_did = repos.repo_did and r2.created_at is not null and r2.rkey <> repos.rkey )) or (created_at is not null and created_at < ( select max(created_at) from repos where owner = ? and repo_did = ? and created_at is not null )) )`, owner.String(), repoDid.String(), owner.String(), repoDid.String(), ) if err != nil { return 0, err } return res.RowsAffected() } func (d *DB) Knots() ([]string, error) { rows, err := d.Query(`select distinct knot from repos`) if err != nil { return nil, err } defer rows.Close() var knots []string for rows.Next() { var knot string if err := rows.Scan(&knot); err != nil { return nil, err } knots = append(knots, knot) } if err := rows.Err(); err != nil { return nil, err } return knots, nil } func scanRepo(row interface{ Scan(...any) error }) (*Repo, error) { var knot, owner, rkey, repoDid string if err := row.Scan(&knot, &owner, &rkey, &repoDid); err != nil { return nil, err } return &Repo{ Knot: knot, Owner: syntax.DID(owner), Rkey: syntax.RecordKey(rkey), RepoDid: syntax.DID(repoDid), }, nil } func (d *DB) SiblingRkeysForRepoDid(owner, repoDid syntax.DID, excludeRkey syntax.RecordKey) ([]string, error) { rows, err := d.Query( `select rkey from repos where owner = ? and coalesce(repo_did, '') = ? and rkey <> ?`, owner.String(), repoDid.String(), excludeRkey.String(), ) if err != nil { return nil, err } defer rows.Close() var collect func(acc []string) ([]string, error) collect = func(acc []string) ([]string, error) { if !rows.Next() { return acc, rows.Err() } var r string if err := rows.Scan(&r); err != nil { return acc, err } return collect(append(acc, r)) } return collect(nil) } func (d *DB) GetRepoByDid(repoDid syntax.DID) (*Repo, error) { return scanRepo(d.QueryRow( `select knot, owner, rkey, repo_did from repos where repo_did = ?`, repoDid.String(), )) } func (d *DB) GetRepoByOwnerRkey(owner syntax.DID, rkey syntax.RecordKey) (*Repo, error) { return scanRepo(d.QueryRow( `select knot, owner, rkey, repo_did from repos where owner = ? and rkey = ?`, owner.String(), rkey.String(), )) } func (d *DB) AllRepos() ([]Repo, error) { rows, err := d.Query(`select knot, owner, rkey, repo_did from repos`) if err != nil { return nil, err } defer rows.Close() var repos []Repo for rows.Next() { r, err := scanRepo(rows) if err != nil { return nil, err } repos = append(repos, *r) } if err := rows.Err(); err != nil { return nil, err } return repos, nil } func (d *DB) DeleteRepo(repoDid syntax.DID) error { _, err := d.Exec(`delete from repos where repo_did = ?`, repoDid) return err } // DeleteRepoByOwnerRkey deletes a repo by (owner,rkey) pair and returns deleted repos DID. func (d *DB) DeleteRepoByOwnerRkey(owner syntax.DID, rkey syntax.RecordKey) (syntax.DID, error) { var repoDid string err := d.QueryRow( `delete from repos where owner = ? and rkey = ? returning repo_did`, owner.String(), rkey.String(), ).Scan(&repoDid) if errors.Is(err, sql.ErrNoRows) { return "", nil } if err != nil { return "", err } return syntax.DID(repoDid), nil }