commit f80de360baba708a9aa57ccec280639455954334 Author: Magnus Ă…hall Date: Tue Aug 1 17:10:35 2023 +0200 Initial commit diff --git a/database.go b/database.go new file mode 100644 index 0000000..18b3856 --- /dev/null +++ b/database.go @@ -0,0 +1,31 @@ +package dbschema + +import ( + // Standard + "database/sql" + "fmt" +) + +func newDatabase(host string, port int, dbName, user, pass string) (dbase Database, err error) {// {{{ + dbase.Host = host + dbase.Port = port + dbase.DbName = dbName + dbase.Username = user + dbase.Password = pass + + dbase.db, err = sql.Open("postgres", dbase.sqlConnString()) + return +}// }}} + +func (dbase Database) sqlConnString() string {// {{{ + return fmt.Sprintf( + "postgresql://%s:%s@%s:%d/%s?sslmode=disable", + dbase.Username, + dbase.Password, + dbase.Host, + dbase.Port, + dbase.DbName, + ) +}// }}} + +// vim: foldmethod=marker diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eb920fa --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.gibonuddevalla.se/go/dbschema + +go 1.20 + +require github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aeddeae --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..56050c2 --- /dev/null +++ b/schema.go @@ -0,0 +1,71 @@ +package dbschema + +import ( + // External + _ "github.com/lib/pq" + + // Standard + "database/sql" +) + +type Upgrader struct { + schemaDb Database + databases map[string]Database + logCallback func(string, string) + sqlCallback func(string, int) ([]byte, bool) +} + +type Database struct { + Host string + Port int + DbName string + Username string + Password string + + db *sql.DB + schemaDb *Database +} + +/* +func dbUpdate() (err error) {// {{{ + var rows *sqlx.Rows + var schemaStr string + var schema int + rows, err = db.Queryx(`SELECT value FROM _internal.db WHERE "key"='schema'`) + if err != nil { return } + defer rows.Close() + + if !rows.Next() { + return errors.New("Table _interval.db missing schema row") + } + + if err = rows.Scan(&schemaStr); err != nil { + return + } + + // Run updates + schema, err = strconv.Atoi(schemaStr) + if err != nil { + return err + } + for i := (schema+1); i <= DB_SCHEMA; i++ { + log.Printf("\x1b[32mNotes\x1b[0m Upgrading SQL schema to revision %d...", i) + sql, _ := embedded.ReadFile( + fmt.Sprintf("sql/%04d.sql", i), + ) + _, err = db.Exec(string(sql)) + if err != nil { + return + } + _, err = db.Exec(`UPDATE _internal.db SET "value"=$1 WHERE "key"='schema'`, i) + if err != nil { + return + } + log.Printf("\x1b[32mNotes\x1b[0m OK: %d", i) + } + + return +}// }}} +*/ + +// vim: foldmethod=marker diff --git a/upgrader.go b/upgrader.go new file mode 100644 index 0000000..27e4f4f --- /dev/null +++ b/upgrader.go @@ -0,0 +1,154 @@ +package dbschema + +import ( + // Standard + "database/sql" + "fmt" +) + +func defaultCallback(topic, msg string) {// {{{ + fmt.Printf("[%s] %s\n", topic, msg) +}// }}} +func NewUpgrader(host string, port int, dbName, user, pass string) (upgrader Upgrader, err error) {// {{{ + upgrader.logCallback = defaultCallback + upgrader.databases = map[string]Database{} + upgrader.schemaDb, err = newDatabase( + host, + port, + dbName, + user, + pass, + ) + err = upgrader.verifySchemaTable() + return +}// }}} + +func (upgrader *Upgrader) SetLogCallback(callback func(string, string)) {// {{{ + upgrader.logCallback = callback +}// }}} +func (upgrader *Upgrader) SetSqlCallback(callback func(string, int) ([]byte, bool)) {// {{{ + upgrader.sqlCallback = callback +}// }}} +func (upgrader Upgrader) verifySchemaTable() (err error) {// {{{ + var rows *sql.Rows + if rows, err = upgrader.schemaDb.db.Query( + `SELECT EXISTS ( + SELECT FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = '_db' + AND c.relname = 'schema' + )`, + ); err != nil { + return + } + defer rows.Close() + var exists bool + rows.Next() + if err = rows.Scan(&exists); err != nil { + return + } + + if !exists { + upgrader.logCallback("create", "_db.schema") + upgrader.schemaDb.db.Exec(`CREATE SCHEMA "_db"`) + + if _, err = upgrader.schemaDb.db.Exec(` + CREATE TABLE "_db"."schema" ( + database varchar NOT NULL, + version int4 NOT NULL, + updated timestamp NOT NULL DEFAULT NOW(), + + CONSTRAINT schema_pk PRIMARY KEY (database) + ); + `, + ); err != nil { + return + } + } + return +}// }}} +func (upgrader Upgrader) verifySchemaEntry(dbase Database) (err error) {// {{{ + var rows *sql.Rows + rows, err = upgrader.schemaDb.db.Query(`SELECT version FROM _db.schema WHERE database=$1`, dbase.DbName) + if err != nil { + return + } + defer rows.Close() + + if !rows.Next() { + upgrader.logCallback("initiate version", dbase.DbName) + _, err = upgrader.schemaDb.db.Exec(`INSERT INTO _db.schema(database, version) VALUES($1, 0)`, dbase.DbName) + if err != nil { + return + } + } + + return +}// }}} +func (upgrader Upgrader) version(dbName string) (version int, err error) {// {{{ + var rows *sql.Rows + rows, err = upgrader.schemaDb.db.Query( + `SELECT version FROM _db.schema WHERE database=$1`, + dbName, + ) + if err != nil { + return + } + defer rows.Close() + + if rows.Next() { + err = rows.Scan(&version) + } else { + err = fmt.Errorf(`Database "%s" is missing an entry in _db.schema`, dbName) + } + return +}// }}} + +func (upgrader Upgrader) AddDatabase(host string, port int, dbName, user, pass string) (err error) {// {{{ + var db Database + if db, err = newDatabase(host, port, dbName, user, pass); err != nil { + return + } + + upgrader.databases[dbName] = db + + err = upgrader.verifySchemaEntry(db) + return +}// }}} +func (upgrader Upgrader) Run() (err error) {// {{{ + var version int + + for dbName, db := range upgrader.databases { + version, err = upgrader.version(dbName) + if err != nil { + return + } + upgrader.logCallback("version", fmt.Sprintf("%s: %d", dbName, version)) + + for { + version++ + sql, found := upgrader.sqlCallback(dbName, version) + if !found { + break + } + + upgrader.logCallback("exec", fmt.Sprintf("%s: %d", dbName, version)) + if _, err = db.db.Exec(string(sql)); err != nil { + return + } + _, err = upgrader.schemaDb.db.Exec(` + UPDATE _db.schema + SET + version=$1, + updated=NOW() + WHERE database=$2 + `, version, dbName) + if err != nil { + return + } + } + } + return +}// }}} + +// vim: foldmethod=marker