From 5a343d80f820bf337fb137a7ffd3d19abb17de7a Mon Sep 17 00:00:00 2001 From: Bel LaPointe <153096461+breel-render@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:23:08 -0600 Subject: [PATCH] welp at least it runs --- main.go | 236 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 170 insertions(+), 66 deletions(-) diff --git a/main.go b/main.go index 025b62f..7c1dd11 100644 --- a/main.go +++ b/main.go @@ -11,13 +11,14 @@ import ( "slices" "strings" - _ "github.com/glebarez/sqlite" + //_ "github.com/glebarez/sqlite" "github.com/google/uuid" + _ "modernc.org/sqlite" ) const ( - jellyDB = "jellyfin.db" - libraryDB = "library.db" + jellyDB = "jellyfin.db" + libDB = "library.db" ) func main() { @@ -34,7 +35,7 @@ func main() { { if err := cp(path.Join(data, jellyDB), path.Join(workd, jellyDB)); err != nil { log.Fatalf("%v", err) - } else if err := cp(path.Join(data, libraryDB), path.Join(workd, libraryDB)); err != nil { + } else if err := cp(path.Join(data, libDB), path.Join(workd, libDB)); err != nil { log.Fatalf("%v", err) } } @@ -58,7 +59,7 @@ func main() { if n, err := SelectOne[int](jellyfinDB, `SELECT COUNT(*) FROM Users WHERE Username = $1`, toU); err != nil { log.Fatalf("%v", err) } else if n == 1 { - } else if err := CloneForColumn(jellyfinDB, `Users`, `Username`, fromU, toU, `Id`, `InternalId`); err != nil { + } else if err := CloneForColumn(jellyfinDB, `Users`, `Username`, fromU, toU, `InternalId`); err != nil { // TODO how is InternalId maintained? Chance? log.Fatalf("%v", err) } else if n, err := SelectOne[int](jellyfinDB, `SELECT COUNT(*) FROM Users WHERE Username = $1`, toU); err != nil { log.Fatalf("%v", err) @@ -76,61 +77,73 @@ func main() { } log.Println(toU, toUUID, toID) - log.Fatalf("not impl get toU, toUUID") - - tables, err := Tables(jellyfinDB) + libraryDB, err := sql.Open("sqlite", path.Join(workd, libDB)) if err != nil { log.Fatalf("%v", err) } + defer libraryDB.Close() - for _, table := range tables { - columns, err := Columns(jellyfinDB, table) + for _, db := range []*sql.DB{jellyfinDB, libraryDB} { + tables, err := Tables(db) if err != nil { log.Fatalf("%v", err) } - log.Println(table, columns) - userColumns := slices.DeleteFunc(slices.Clone(columns), func(s string) bool { - return !slices.Contains([]string{ - "user", - "userid", - }, strings.ToLower(s)) - }) - if len(userColumns) == 0 { - continue - } + for _, table := range tables { + columns, err := Columns(db, table) + if err != nil { + log.Fatalf("%v", err) + } - for _, column := range userColumns { - if n, err := SelectOne[int](jellyfinDB, fmt.Sprintf(`SELECT COUNT(*) FROM %q WHERE %q = $1`, table, column), fromID); err != nil { - log.Fatalf("%v", err) - } else if n > 0 { - if err := CloneForColumn(jellyfinDB, table, column, fromID, toID); err != nil { + userColumns := slices.DeleteFunc(slices.Clone(columns), func(s string) bool { + return !slices.Contains([]string{ + "user", + "userid", + }, strings.ToLower(s)) + }) + if len(userColumns) == 0 { + continue + } + + for _, column := range userColumns { + log.Println(table, column, "...") + + if n, err := SelectOne[int](db, fmt.Sprintf(`SELECT COUNT(*) FROM %q WHERE %q = $1`, table, column), fromID); err != nil { log.Fatalf("%v", err) - } - } else if n, err := SelectOne[int](jellyfinDB, fmt.Sprintf(`SELECT COUNT(*) FROM %q WHERE %q = $1`, table, column), fromUUID); err != nil { - log.Fatalf("%v", err) - } else if n > 0 { - if err := CloneForColumn(jellyfinDB, table, column, fromUUID, toUUID); err != nil { + } else if n > 0 { + if err := CloneForColumn(db, table, column, fromID, toID); err != nil { + log.Fatalf("%v", err) + } + } else if n, err := SelectOne[int](db, fmt.Sprintf(`SELECT COUNT(*) FROM %q WHERE %q = $1`, table, column), fromUUID); err != nil { log.Fatalf("%v", err) + } else if n > 0 { + if err := CloneForColumn(db, table, column, fromUUID, toUUID); err != nil { + log.Fatalf("%v", err) + } } } } - - log.Println(table, userColumns) - log.Fatalf("not impl") } } -func CloneForColumn[T any](db *sql.DB, table, column string, from, to T, omit ...string) error { +func CloneForColumn[T any](db *sql.DB, table, column string, from, to T, extraUniques ...string) error { columns, err := Columns(db, table) if err != nil { return err } - // TODO SELECT idx_name FROM PRAGMA_INDEX_LIST('Users') WHERE "unique" to get unique constraints - // TODO SELECT col_name FROM PRAGMA_INDEX_INFO(idx_name); - // TODO how is .Users.InternalId maintained? by chance? + uniqueIntColumns, err := UniqueTypeColumns(db, "INTEGER", table) + if err != nil { + return err + } + uniqueTextColumns, err := UniqueTypeColumns(db, "TEXT", table) + if err != nil { + return err + } + log.Printf("unique text columns %+v, unique int columns %+v", uniqueTextColumns, uniqueIntColumns) + + omit := append(append(extraUniques, uniqueIntColumns...), uniqueTextColumns...) notTheseColumns := slices.DeleteFunc(slices.Clone(columns), func(s string) bool { return s == column || slices.Contains(omit, s) }) for i := range notTheseColumns { notTheseColumns[i] = fmt.Sprintf("%q", notTheseColumns[i]) @@ -140,7 +153,7 @@ func CloneForColumn[T any](db *sql.DB, table, column string, from, to T, omit .. if err != nil { return err } - uuidGenColumns := slices.DeleteFunc(notNullTextColumns, func(s string) bool { return !slices.Contains(omit, s) }) + uuidGenColumns := slices.DeleteFunc(notNullTextColumns, func(s string) bool { return s == column || !slices.Contains(omit, s) }) for i := range uuidGenColumns { uuidGenColumns[i] = fmt.Sprintf("%q", uuidGenColumns[i]) } @@ -154,34 +167,90 @@ func CloneForColumn[T any](db *sql.DB, table, column string, from, to T, omit .. incrGenColumns[i] = fmt.Sprintf("%q", incrGenColumns[i]) } - q := fmt.Sprintf( - `INSERT INTO %q (%q, %s) SELECT $1, %s %s %s FROM %q WHERE %q = $2`, - table, column, strings.Join(append(incrGenColumns, append(uuidGenColumns, notTheseColumns...)...), ", "), - func() string { - incrs := make([]string, len(incrGenColumns)) - for i := range incrs { - incrs[i] = fmt.Sprintf(`%s+100`, incrGenColumns[i]) - } - s := strings.Join(incrs, ", ") - if len(incrs) > 0 { - s += ", " - } - return s - }(), - func() string { - uuids := make([]string, len(uuidGenColumns)) - for i := range uuids { - uuids[i] = fmt.Sprintf("'%s'", strings.ToUpper(uuid.New().String())) - } - s := strings.Join(uuids, ", ") - if len(uuids) > 0 { - s += ", " - } - return s - }(), strings.Join(notTheseColumns, ", "), table, column, - ) - log.Printf("EXEC | %s (%v, %v)", q, to, from) - return Insert(db, q, to, from) + return ForEach(db, func(args []any) error { + selectMaxes := "" + for _, col := range incrGenColumns { + selectMaxes += fmt.Sprintf(`(SELECT COALESCE(MAX(%s)+1, 1) FROM %q), `, col, table) + } + + values := []any{} + for _ = range uuidGenColumns { + values = append(values, fmt.Sprintf("'%s'", strings.ToUpper(uuid.New().String()))) + } + for _, arg := range args { + values = append(values, *(arg.(*any))) + } + values = append(values, to) + + q := fmt.Sprintf( + `INSERT INTO %q (%s, %q) VALUES (%s %s)`, + table, strings.Join(append(incrGenColumns, append(uuidGenColumns, notTheseColumns...)...), ", "), column, + selectMaxes, strings.Join(slices.Repeat([]string{"?"}, len(values)), ", "), + ) + + log.Printf("INSERT | %s (%+v)", q, values) + return Exec(db, q, values...) + }, fmt.Sprintf(`SELECT %s FROM %q WHERE %q = $1`, strings.Join(notTheseColumns, ", "), table, column), from) + /* + q := fmt.Sprintf( + `INSERT INTO %q (%q, %s) SELECT $1, %s %s %s FROM %q WHERE %q = $2`, + table, column, strings.Join(append(incrGenColumns, append(uuidGenColumns, notTheseColumns...)...), ", "), + func() string { + incrs := make([]string, len(incrGenColumns)) + for i := range incrs { + incrs[i] = fmt.Sprintf(`(SELECT COALESCE(MAX(%s), 1) FROM %q)`, incrGenColumns[i], table) + } + s := strings.Join(incrs, ", ") + if len(incrs) > 0 { + s += ", " + } + return s + }(), + func() string { + uuids := make([]string, len(uuidGenColumns)) + for i := range uuids { + uuids[i] = fmt.Sprintf("'%s'", strings.ToUpper(uuid.New().String())) + } + s := strings.Join(uuids, ", ") + if len(uuids) > 0 { + s += ", " + } + return s + }(), strings.Join(notTheseColumns, ", "), table, column, + ) + log.Printf("EXEC | %s (%v, %v)", q, to, from) + return Insert(db, q, to, from) + */ +} + +func ForEach(db *sql.DB, cb func([]any) error, q string, args ...any) error { + rows, err := db.Query(q, args...) + if err != nil { + return err + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return err + } + cols := make([]any, len(columns)) + + for rows.Next() { + for i := range cols { + var a any + cols[i] = &a + } + + if err := rows.Scan(cols...); err != nil { + return err + } + if err := cb(cols); err != nil { + return err + } + } + + return rows.Err() } func cp(from, to string) error { @@ -217,6 +286,41 @@ func NotNullIntColumns(db *sql.DB, table string) ([]string, error) { return Select[string](db, `SELECT name FROM PRAGMA_TABLE_INFO($1) WHERE "notnull" = 1 AND "type" = 'INTEGER'`, table) } +func UniqueTypeColumns(db *sql.DB, t, table string) ([]string, error) { + pks, err := Select[string](db, `SELECT name FROM PRAGMA_TABLE_INFO($1) WHERE "pk" AND "type" = $2`, table, t) + if err != nil { + return nil, err + } + + idxes, err := Select[string](db, `SELECT name AS idx_name FROM PRAGMA_INDEX_LIST($1) WHERE "unique"`, table) + if err != nil { + return nil, err + } + uniqueIdxCols := []string{} + for _, idx := range idxes { + cols, err := Select[string](db, `SELECT name FROM PRAGMA_INDEX_INFO($1)`, idx) + if err != nil { + return nil, err + } + if len(cols) > 1 { + log.Printf("not impl: compound unique indexes like %s.%s's %+v", table, idx, cols) + continue + } + col := cols[0] + + if n, err := SelectOne[int](db, `SELECT COUNT(*) FROM PRAGMA_TABLE_INFO($1) WHERE "name" = $2 AND "type" = $3`, table, col, t); err != nil { + return nil, err + } else if n > 0 { + uniqueIdxCols = append(uniqueIdxCols, col) + } + } + + cols := append(pks, uniqueIdxCols...) + slices.Sort(cols) + cols = slices.Compact(cols) + return cols, nil +} + func Insert(db *sql.DB, q string, args ...any) error { _, err := db.Exec(q, args...) return err