···11+package db
22+33+import (
44+ "database/sql"
55+)
66+77+// GetOrAssignOwnerUID returns the virtual UID for ownerDID, minting a new one
88+// from the uid_counter table if this owner has not been seen before.
99+// UIDs start at 100000 and increment by one per unique owner.
1010+//
1111+// A process-wide mutex serialises concurrent callers so two simultaneous
1212+// requests for distinct DIDs do not race to claim the same counter value.
1313+// The mutex is local to this DB instance and SQLite itself only permits one
1414+// writer at a time, so this does not impede throughput in practice.
1515+func (d *DB) GetOrAssignOwnerUID(ownerDID string) (uint32, error) {
1616+ d.uidAssignMu.Lock()
1717+ defer d.uidAssignMu.Unlock()
1818+1919+ tx, err := d.db.Begin()
2020+ if err != nil {
2121+ return 0, err
2222+ }
2323+ defer tx.Rollback()
2424+2525+ var uid uint32
2626+ err = tx.QueryRow(
2727+ `SELECT uid FROM owner_uid_assignments WHERE owner_did = ?`,
2828+ ownerDID,
2929+ ).Scan(&uid)
3030+ if err == nil {
3131+ return uid, tx.Commit()
3232+ }
3333+ if err != sql.ErrNoRows {
3434+ return 0, err
3535+ }
3636+3737+ if err := tx.QueryRow(`SELECT next_uid FROM uid_counter`).Scan(&uid); err != nil {
3838+ return 0, err
3939+ }
4040+ if _, err := tx.Exec(`UPDATE uid_counter SET next_uid = next_uid + 1`); err != nil {
4141+ return 0, err
4242+ }
4343+ if _, err := tx.Exec(
4444+ `INSERT INTO owner_uid_assignments (owner_did, uid) VALUES (?, ?)`,
4545+ ownerDID, uid,
4646+ ); err != nil {
4747+ return 0, err
4848+ }
4949+5050+ return uid, tx.Commit()
5151+}
5252+5353+// AllReposForMigration returns all (repo_did, owner_did) pairs with a
5454+// non-null owner. Pass force=true to include already-migrated repos.
5555+func (d *DB) AllReposForMigration(force bool) ([]RepoMigrationRow, error) {
5656+ query := `SELECT repo_did, owner_did FROM repo_keys WHERE owner_did IS NOT NULL`
5757+ if !force {
5858+ query += ` AND isolated_at IS NULL`
5959+ }
6060+ rows, err := d.db.Query(query)
6161+ if err != nil {
6262+ return nil, err
6363+ }
6464+ defer rows.Close()
6565+6666+ var result []RepoMigrationRow
6767+ for rows.Next() {
6868+ var r RepoMigrationRow
6969+ if err := rows.Scan(&r.RepoDID, &r.OwnerDID); err != nil {
7070+ return nil, err
7171+ }
7272+ result = append(result, r)
7373+ }
7474+ return result, rows.Err()
7575+}
7676+7777+// CountUnmigratedRepos returns the number of repos that have not yet been
7878+// isolation-migrated.
7979+func (d *DB) CountUnmigratedRepos() (int, error) {
8080+ var n int
8181+ err := d.db.QueryRow(`
8282+ SELECT count(1) FROM repo_keys
8383+ WHERE owner_did IS NOT NULL
8484+ AND isolated_at IS NULL
8585+ `).Scan(&n)
8686+ return n, err
8787+}
8888+8989+// MarkRepoIsolated sets isolated_at to the current time for repoDID.
9090+func (d *DB) MarkRepoIsolated(repoDID string) error {
9191+ _, err := d.db.Exec(
9292+ `UPDATE repo_keys SET isolated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE repo_did = ?`,
9393+ repoDID,
9494+ )
9595+ return err
9696+}
9797+9898+// RepoMigrationRow is a row returned by AllReposForMigration.
9999+type RepoMigrationRow struct {
100100+ RepoDID string
101101+ OwnerDID string
102102+}