253 lines
5.7 KiB
Go
253 lines
5.7 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"g.fc.run/ai/byre/internal/git"
|
|
"g.fc.run/ai/byre/internal/ids"
|
|
)
|
|
|
|
var idToken = regexp.MustCompile(`[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}|queued-[0-9]+|[0-9A-Za-z]+`)
|
|
|
|
var keptColumns = map[string]bool{
|
|
"agent_sessions.vendor_session_id": true,
|
|
"agent_sessions.forked_from": true,
|
|
}
|
|
|
|
func migratePrefixIDs(tx *sql.Tx, dataDir string) error {
|
|
renames, err := idRenames(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rewrite := func(s string) string {
|
|
return idToken.ReplaceAllStringFunc(s, func(tok string) string {
|
|
if to, ok := renames[tok]; ok {
|
|
return to
|
|
}
|
|
return tok
|
|
})
|
|
}
|
|
|
|
tables, err := textColumns(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for table, columns := range tables {
|
|
err = rewriteTable(tx, table, columns, rewrite)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", table, err)
|
|
}
|
|
}
|
|
|
|
repos := map[string]string{}
|
|
rows, err := tx.Query(`SELECT id, repo_path FROM projects`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for rows.Next() {
|
|
id, repo := "", ""
|
|
err = rows.Scan(&id, &repo)
|
|
if err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
repos[id] = repo
|
|
}
|
|
rows.Close()
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, sub := range []string{"blobs", "hands", "antigravity", "worktrees"} {
|
|
err = renameEntries(filepath.Join(dataDir, sub), rewrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for project, repo := range repos {
|
|
dir := filepath.Join(dataDir, "worktrees", project)
|
|
err = renameEntries(dir, rewrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
worktrees, err := filepath.Glob(filepath.Join(dir, "*", ".git"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, w := range worktrees {
|
|
worktrees[i] = filepath.Dir(w)
|
|
}
|
|
err = git.RepairWorktrees(repo, worktrees)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = git.RenameRefs(repo, rewrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = git.RewriteNotes(repo, rewrite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func idRenames(tx *sql.Tx) (map[string]string, error) {
|
|
sources := []struct{ prefix, query string }{
|
|
{ids.Project, `SELECT id FROM projects`},
|
|
{ids.Task, `SELECT id FROM tasks`},
|
|
{ids.WorkflowRun, `SELECT DISTINCT workflow_run FROM tasks`},
|
|
{ids.Session, `SELECT id FROM agent_sessions`},
|
|
{ids.Snapshot, `SELECT id FROM snapshots`},
|
|
{ids.Review, `SELECT id FROM reviews`},
|
|
{ids.InboxItem, `SELECT id FROM inbox_items`},
|
|
{ids.InboxResponse, `SELECT id FROM inbox_responses`},
|
|
{ids.Blob, `SELECT id FROM blobs`},
|
|
{ids.LeadNote, `SELECT id FROM lead_notes`},
|
|
{ids.WorkflowDefinition, `SELECT id FROM workflow_definitions`},
|
|
{ids.Send, `SELECT DISTINCT send_id FROM task_inputs`},
|
|
{ids.Send, `SELECT DISTINCT send_id FROM lead_inputs`},
|
|
{ids.Send, `SELECT DISTINCT json_extract(payload, '$.send_id') FROM events WHERE kind IN ('user.queued', 'user.message') AND json_extract(payload, '$.send_id') IS NOT NULL`},
|
|
}
|
|
|
|
renames := map[string]string{}
|
|
for _, src := range sources {
|
|
rows, err := tx.Query(src.query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
old := ""
|
|
err = rows.Scan(&old)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
_, mapped := renames[old]
|
|
if old == "" || mapped || strings.HasPrefix(old, src.prefix) {
|
|
continue
|
|
}
|
|
renames[old] = ids.Derive(src.prefix, old)
|
|
}
|
|
rows.Close()
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return renames, nil
|
|
}
|
|
|
|
func textColumns(tx *sql.Tx) (map[string][]string, error) {
|
|
rows, err := tx.Query(`SELECT m.name, p.name FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table' AND m.name NOT IN ('migrations', 'sqlite_sequence') AND upper(p.type) = 'TEXT'`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
tables := map[string][]string{}
|
|
for rows.Next() {
|
|
table, column := "", ""
|
|
err = rows.Scan(&table, &column)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if keptColumns[table+"."+column] {
|
|
continue
|
|
}
|
|
tables[table] = append(tables[table], column)
|
|
}
|
|
return tables, rows.Err()
|
|
}
|
|
|
|
func rewriteTable(tx *sql.Tx, table string, columns []string, rewrite func(string) string) error {
|
|
quoted := []string{}
|
|
for _, c := range columns {
|
|
quoted = append(quoted, `"`+c+`"`)
|
|
}
|
|
query := fmt.Sprintf(`SELECT rowid, %s FROM "%s" WHERE rowid > ? ORDER BY rowid LIMIT 2000`, strings.Join(quoted, ", "), table)
|
|
|
|
after := int64(0)
|
|
for {
|
|
type change struct {
|
|
rowid int64
|
|
column string
|
|
value string
|
|
}
|
|
changes := []change{}
|
|
n := 0
|
|
|
|
rows, err := tx.Query(query, after)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for rows.Next() {
|
|
values := make([]sql.NullString, len(columns))
|
|
dest := []any{&after}
|
|
for i := range values {
|
|
dest = append(dest, &values[i])
|
|
}
|
|
err = rows.Scan(dest...)
|
|
if err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
n++
|
|
for i, v := range values {
|
|
if !v.Valid {
|
|
continue
|
|
}
|
|
to := rewrite(v.String)
|
|
if to != v.String {
|
|
changes = append(changes, change{after, columns[i], to})
|
|
}
|
|
}
|
|
}
|
|
rows.Close()
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, c := range changes {
|
|
_, err = tx.Exec(fmt.Sprintf(`UPDATE "%s" SET "%s" = ? WHERE rowid = ?`, table, c.column), c.value, c.rowid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func renameEntries(dir string, rewrite func(string) string) error {
|
|
entries, err := os.ReadDir(dir)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, e := range entries {
|
|
to := rewrite(e.Name())
|
|
if to == e.Name() {
|
|
continue
|
|
}
|
|
err = os.Rename(filepath.Join(dir, e.Name()), filepath.Join(dir, to))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|