package db import ( "database/sql" "github.com/bluesky-social/indigo/atproto/syntax" ) type Repo struct { Knot string Owner syntax.DID Rkey syntax.RecordKey RepoDid syntax.DID Name string CreatedAt string } func (d *DB) AddRepo(repo Repo) error { var createdAt sql.NullString if repo.CreatedAt != "" { createdAt = sql.NullString{String: repo.CreatedAt, Valid: true} } var name sql.NullString if repo.Name != "" { name = sql.NullString{String: repo.Name, Valid: true} } _, err := d.Exec( `insert into repos (knot, owner, rkey, repo_did, name, created_at) values (?, ?, ?, ?, ?, ?) on conflict(owner, rkey) do update set knot = excluded.knot, repo_did = excluded.repo_did, name = coalesce(excluded.name, repos.name), created_at = coalesce(excluded.created_at, repos.created_at)`, repo.Knot, repo.Owner.String(), repo.Rkey.String(), repo.RepoDid.String(), name, 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, name string if err := row.Scan(&knot, &owner, &rkey, &repoDid, &name); err != nil { return nil, err } return &Repo{ Knot: knot, Owner: syntax.DID(owner), Rkey: syntax.RecordKey(rkey), RepoDid: syntax.DID(repoDid), Name: name, }, 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, coalesce(repo_did, ''), coalesce(name, '') 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, coalesce(repo_did, ''), coalesce(name, '') from repos where owner = ? and rkey = ?`, owner.String(), rkey.String(), )) } func (d *DB) AllRepos() ([]Repo, error) { rows, err := d.Query(`select knot, owner, rkey, coalesce(repo_did, ''), coalesce(name, '') 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) DeleteRepoByOwnerRkey(owner syntax.DID, rkey syntax.RecordKey) error { _, err := d.Exec(`delete from repos where owner = ? and rkey = ?`, owner.String(), rkey.String()) return err } // one repo did can have several rows after renames func (d *DB) ReposByDid(repoDid syntax.DID) ([]Repo, error) { rows, err := d.Query( `select knot, owner, rkey, repo_did, coalesce(created_at, '') from repos where repo_did = ?`, repoDid.String(), ) if err != nil { return nil, err } defer rows.Close() var repos []Repo for rows.Next() { var r Repo var owner, rkey, did string if err := rows.Scan(&r.Knot, &owner, &rkey, &did, &r.CreatedAt); err != nil { return nil, err } r.Owner = syntax.DID(owner) r.Rkey = syntax.RecordKey(rkey) r.RepoDid = syntax.DID(did) repos = append(repos, r) } return repos, rows.Err() } func (d *DB) DeleteReposByDid(repoDid syntax.DID) error { _, err := d.Exec(`delete from repos where repo_did = ?`, repoDid.String()) return err } func (d *DB) CountReposByOwner(owner syntax.DID) (int, error) { var n int err := d.QueryRow(`select count(*) from repos where owner = ?`, owner.String()).Scan(&n) return n, err }