From b088241a51c05a060a2a7e730f9c344563e6c5ce Mon Sep 17 00:00:00 2001 From: Bel LaPointe <153096461+breel-render@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:03:47 -0600 Subject: [PATCH] grrr i can cli insert select but neither sqlite lib does it --- go.mod | 11 ++++- go.sum | 10 +++++ main.go | 136 +++++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 133 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index d505a98..a095872 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,23 @@ go 1.23.0 toolchain go1.23.9 -require modernc.org/sqlite v1.37.1 +require ( + github.com/glebarez/sqlite v1.11.0 + github.com/google/uuid v1.6.0 + modernc.org/sqlite v1.37.1 // indirect +) require ( github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.33.0 // indirect + gorm.io/gorm v1.25.7 // indirect modernc.org/libc v1.65.7 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 92cf755..5c2db28 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -21,6 +29,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= diff --git a/main.go b/main.go index edaeb8e..81a8ea1 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,8 @@ import ( "slices" "strings" - _ "modernc.org/sqlite" + _ "github.com/glebarez/sqlite" + "github.com/google/uuid" ) const ( @@ -27,51 +28,65 @@ func main() { workd, err := ioutil.TempDir(os.TempDir(), "jellyfin-user-clone.*") if err != nil { - panic(err) + log.Fatalf("%v", err) } { if err := cp(path.Join(data, jellyDB), path.Join(workd, jellyDB)); err != nil { - panic(err) + log.Fatalf("%v", err) } else if err := cp(path.Join(data, libraryDB), path.Join(workd, libraryDB)); err != nil { - panic(err) + log.Fatalf("%v", err) } } jellyfinDB, err := sql.Open("sqlite", path.Join(workd, jellyDB)) if err != nil { - panic(err) + log.Fatalf("%v", err) } defer jellyfinDB.Close() fromUUID, err := SelectOne[string](jellyfinDB, `SELECT Id FROM Users WHERE Username = $1`, fromU) if err != nil { - panic(err) + log.Fatalf("%v", err) } fromID, err := SelectOne[int](jellyfinDB, `SELECT InternalId FROM Users WHERE Id = $1`, fromUUID) if err != nil { - panic(err) + log.Fatalf("%v", err) } log.Println(fromU, fromUUID, fromID) if n, err := SelectOne[int](jellyfinDB, `SELECT COUNT(*) FROM Users WHERE Username = $1`, toU); err != nil { - panic(err) + log.Fatalf("%v", err) } else if n == 1 { - } else if err := Exec(jellyfinDB, `INSERT INTO Users () SELECT * FROM Users WHERE Id = $1`, fromUUID); err != nil { - panic(err) + } else if err := CloneForColumn(jellyfinDB, `Users`, `Username`, fromU, toU, `Id`, `InternalId`); err != nil { + 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) + } else if n != 1 { + log.Fatalf("still no username=%q after insert", toU) } - panic("not impl get toU, toUUID") + toUUID, err := SelectOne[string](jellyfinDB, `SELECT Id FROM Users WHERE Username = $1`, toU) + if err != nil { + log.Fatalf("%v", err) + } + toID, err := SelectOne[int](jellyfinDB, `SELECT InternalId FROM Users WHERE Id = $1`, toUUID) + if err != nil { + log.Fatalf("%v", err) + } + log.Println(toU, toUUID, toID) + + log.Fatalf("not impl get toU, toUUID") tables, err := Tables(jellyfinDB) if err != nil { - panic(err) + log.Fatalf("%v", err) } for _, table := range tables { columns, err := Columns(jellyfinDB, table) if err != nil { - panic(err) + log.Fatalf("%v", err) } log.Println(table, columns) @@ -87,24 +102,88 @@ func main() { for _, column := range userColumns { if n, err := SelectOne[int](jellyfinDB, fmt.Sprintf(`SELECT COUNT(*) FROM %q WHERE %q = $1`, table, column), fromID); err != nil { - panic(err) + log.Fatalf("%v", err) } else if n > 0 { - notThisColumn := strings.Join(slices.DeleteFunc(slices.Clone(columns), func(s string) bool { return s == column }), ", ") - if err := Exec(jellyfinDB, fmt.Sprintf(`INSERT INTO %q (%q, %s) SELECT $2, %s FROM %q WHERE %q = $1`, table, column, notThisColumn, notThisColumn, table, column), fromID, toID); err != nil { - panic(err) + if err := CloneForColumn(jellyfinDB, table, column, fromID, toID); 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 { - panic(err) + log.Fatalf("%v", err) } else if n > 0 { - panic("not impl: col is uuid") + if err := CloneForColumn(jellyfinDB, table, column, fromUUID, toUUID); err != nil { + log.Fatalf("%v", err) + } } } log.Println(table, userColumns) - panic("not impl") + log.Fatalf("not impl") } } +func CloneForColumn[T any](db *sql.DB, table, column string, from, to T, omit ...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? + + 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]) + } + + notNullTextColumns, err := NotNullTextColumns(db, table) + if err != nil { + return err + } + uuidGenColumns := slices.DeleteFunc(notNullTextColumns, func(s string) bool { return !slices.Contains(omit, s) }) + for i := range uuidGenColumns { + uuidGenColumns[i] = fmt.Sprintf("%q", uuidGenColumns[i]) + } + + notNullIntColumns, err := NotNullIntColumns(db, table) + if err != nil { + return err + } + incrGenColumns := slices.DeleteFunc(notNullIntColumns, func(s string) bool { return !slices.Contains(omit, s) }) + for i := range incrGenColumns { + incrGenColumns[i] = fmt.Sprintf("%q", incrGenColumns[i]) + } + + q := fmt.Sprintf( + `INSERT INTO %q (%q, %s) SELECT $2, %s %s %s FROM %q WHERE %q = $1`, + 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, from, to) + return Insert(db, q, to, from) +} + func cp(from, to string) error { fromF, err := os.Open(from) if err != nil { @@ -123,11 +202,24 @@ func cp(from, to string) error { } func Tables(db *sql.DB) ([]string, error) { - return Select[string](jellyfinDB, `SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`) + return Select[string](db, `SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`) } func Columns(db *sql.DB, table string) ([]string, error) { - return Select[string](jellyfinDB, `SELECT name FROM PRAGMA_TABLE_INFO($1)`, table) + return Select[string](db, `SELECT name FROM PRAGMA_TABLE_INFO($1)`, table) +} + +func NotNullTextColumns(db *sql.DB, table string) ([]string, error) { + return Select[string](db, `SELECT name FROM PRAGMA_TABLE_INFO($1) WHERE "notnull" = 1 AND "type" = 'TEXT'`, table) +} + +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 Insert(db *sql.DB, q string, args ...any) error { + _, err := db.Exec(q, args...) + return err } func Exec(db *sql.DB, q string, args ...any) error {