From 19fc8e032267e87aff474e4f3d1063b80c94ce79 Mon Sep 17 00:00:00 2001 From: zegkljan Date: Sat, 13 Aug 2022 02:20:34 +0200 Subject: [PATCH] DB migration, proposals. * DB schema is migrated. * Added mechanism for proposing app improvements. --- .gitignore | 3 +- models/models.go | 7 ++ server/handling.go | 2 +- server/server.go | 110 ++++++++++++++++-- server/{initdb.sql => sql_schema/V1_init.sql} | 13 +-- server/sql_schema/V2_proposals.sql | 7 ++ 6 files changed, 122 insertions(+), 20 deletions(-) rename server/{initdb.sql => sql_schema/V1_init.sql} (72%) create mode 100644 server/sql_schema/V2_proposals.sql diff --git a/.gitignore b/.gitignore index 8b61cf5..cd4363b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ go.work # Project-specific oko-server *.sqlite* -.vscode \ No newline at end of file +.vscode +__debug_bin \ No newline at end of file diff --git a/models/models.go b/models/models.go index 36a94c2..a08dda5 100644 --- a/models/models.go +++ b/models/models.go @@ -60,6 +60,7 @@ type Update struct { Update []Feature `json:"update"` Delete []FeatureID `json:"delete"` DeletePhotos []FeaturePhotoID `json:"delete_photos"` + Proposals []Proposal `json:"proposals"` } type HandshakeChallenge struct { @@ -92,3 +93,9 @@ type PhotoMetadata struct { ID FeaturePhotoID `json:"id"` ThumbnailFilename string `json:"thumbnail_filename"` } + +type Proposal struct { + OwnerID int `json:"owner_id"` + Description string `json:"description"` + How string `json:"how"` +} diff --git a/server/handling.go b/server/handling.go index e83224b..7d53233 100644 --- a/server/handling.go +++ b/server/handling.go @@ -113,7 +113,7 @@ func (s *Server) setupRouter() *gin.Engine { } func (s *Server) handlePOSTReset(gc *gin.Context) { - err := s.reinitDB() + err := s.initDB(true) if err != nil { internalError(gc, err) return diff --git a/server/server.go b/server/server.go index 73d6038..7616087 100644 --- a/server/server.go +++ b/server/server.go @@ -22,8 +22,11 @@ import ( "github.com/sirupsen/logrus" ) -//go:embed initdb.sql -var initDB string +//go:embed sql_schema/V1_init.sql +var sql_v1 string + +//go:embed sql_schema/V2_proposals.sql +var sql_v2 string type Server struct { config ServerConfig @@ -141,6 +144,7 @@ func (s *Server) checkpointDb(conn *sqlite.Conn, truncate bool) { func (s *Server) setupDB() { sqlitex.PoolCloseTimeout = time.Second * 10 + s.log.Debugf("Using db %s", s.config.DbPath) dbpool, err := sqlitex.Open(fmt.Sprintf("file:%s", s.config.DbPath), 0, 10) if err != nil { s.log.WithError(err).Fatal("Failed to open/create DB.") @@ -148,11 +152,9 @@ func (s *Server) setupDB() { s.dbpool = dbpool s.checkpointNotice = make(chan struct{}) - if s.config.ReinitDB { - err = s.reinitDB() - if err != nil { - s.log.WithError(err).Fatal("init DB transaction failed") - } + err = s.initDB(s.config.ReinitDB) + if err != nil { + s.log.WithError(err).Fatal("init DB transaction failed") } // aggressively checkpoint the database on idle times @@ -217,13 +219,62 @@ func (s *Server) setupTiles() { s.mapPackSize = info.Size() } -func (s *Server) reinitDB() error { - s.log.Info("Reinitializing DB.") +func (s *Server) initDB(reinit bool) error { + s.log.Info("Initializing DB.") conn := s.getDbConn() defer s.dbpool.Put(conn) defer s.requestCheckpoint() - return sqlitex.ExecScript(conn, initDB) + + if reinit { + s.log.Warn("REinitializing DB.") + tables := []string{} + err := sqlitex.Exec(conn, "select name from sqlite_master where type = 'table'", func(stmt *sqlite.Stmt) error { + tables = append(tables, stmt.ColumnText(0)) + return nil + }) + if err != nil { + return fmt.Errorf("failed to get table names: %w", err) + } + + for _, table := range tables { + err = sqlitex.Exec(conn, "drop table "+table, nil) + if err != nil { + return fmt.Errorf("failed to drop tables: %w", err) + } + } + + err = sqlitex.Exec(conn, "PRAGMA user_version = 0", nil) + if err != nil { + return fmt.Errorf("failed to reset user version: %w", err) + } + } + var version int + err := sqlitex.Exec(conn, "PRAGMA user_version", func(stmt *sqlite.Stmt) error { + version = stmt.ColumnInt(0) + return nil + }) + if err != nil { + return fmt.Errorf("failed to get user version: %w", err) + } + + s.log.Debugf("Current db version: %d", version) + if version <= 0 { + s.log.Debugf("Running db migration V1") + err = sqlitex.ExecScript(conn, sql_v1) + if err != nil { + return fmt.Errorf("failed to run V1 init script") + } + } + if version <= 1 { + s.log.Debugf("Running db migration V2") + err = sqlitex.ExecScript(conn, sql_v2) + if err != nil { + return fmt.Errorf("failed to run V2 init script") + } + } + + return nil } func (s *Server) handshake(hc models.HandshakeChallenge) (models.UserID, error) { @@ -412,7 +463,7 @@ func (s *Server) getDataWithPhotos() (file *os.File, err error) { } func (s *Server) update(data models.Update, photos map[string]models.Photo) error { - s.log.Debugf("Updating data: %d created, %d created photos, %d updated, %d deleted, %d deleted photos, %d photo files", len(data.Create), len(data.CreatedPhotos), len(data.Update), len(data.Delete), len(data.DeletePhotos), len(photos)) + s.log.Debugf("Updating data: %d created, %d created photos, %d updated, %d deleted, %d deleted photos, %d photo files, %d proposals", len(data.Create), len(data.CreatedPhotos), len(data.Update), len(data.Delete), len(data.DeletePhotos), len(photos), len(data.Proposals)) conn := s.getDbConn() defer s.dbpool.Put(conn) @@ -459,6 +510,13 @@ func (s *Server) update(data models.Update, photos map[string]models.Photo) erro return } } + if data.Proposals != nil { + err = s.addProposals(conn, data.Proposals) + if err != nil { + err = fmt.Errorf("failed to add proposals: %w", err) + return + } + } return nil }() @@ -860,3 +918,33 @@ func (s *Server) getPhoto(featureID models.FeatureID, photoID models.FeaturePhot return data, *contentType, nil } + +func (s *Server) addProposals(conn *sqlite.Conn, proposals []models.Proposal) error { + stmt, err := conn.Prepare("insert into proposals(owner_id, description, how) values(?, ?, ?)") + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Finalize() + + for _, proposal := range proposals { + err := stmt.Reset() + if err != nil { + return fmt.Errorf("failed to reset prepared statement: %w", err) + } + err = stmt.ClearBindings() + if err != nil { + return fmt.Errorf("failed to clear bindings of prepared statement: %w", err) + } + + stmt.BindInt64(1, int64(proposal.OwnerID)) + stmt.BindText(2, proposal.Description) + stmt.BindText(3, proposal.How) + + _, err = stmt.Step() + if err != nil { + return fmt.Errorf("failed to evaluate prepared statement: %w", err) + } + } + s.requestCheckpoint() + return nil +} diff --git a/server/initdb.sql b/server/sql_schema/V1_init.sql similarity index 72% rename from server/initdb.sql rename to server/sql_schema/V1_init.sql index e86834c..bb77e0b 100644 --- a/server/initdb.sql +++ b/server/sql_schema/V1_init.sql @@ -1,12 +1,10 @@ -DROP TABLE IF EXISTS users; -CREATE TABLE IF NOT EXISTS users ( +CREATE TABLE users ( id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL UNIQUE ); INSERT INTO users(id, name) VALUES(0, 'system'); -DROP TABLE IF EXISTS features; -CREATE TABLE IF NOT EXISTS features ( +CREATE TABLE features ( id integer PRIMARY KEY AUTOINCREMENT, owner_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, name text NOT NULL, @@ -15,12 +13,13 @@ CREATE TABLE IF NOT EXISTS features ( geom text NOT NULL ); -DROP TABLE IF EXISTS feature_photos; -CREATE TABLE IF NOT EXISTS feature_photos ( +CREATE TABLE feature_photos ( id integer PRIMARY KEY AUTOINCREMENT, feature_id integer NOT NULL REFERENCES features(id) ON DELETE CASCADE, thumbnail_content_type text NOT NULL, content_type text NOT NULL, thumbnail_contents blob NOT NULL, contents blob NOT NULL -); \ No newline at end of file +); + +PRAGMA user_version = 1; \ No newline at end of file diff --git a/server/sql_schema/V2_proposals.sql b/server/sql_schema/V2_proposals.sql new file mode 100644 index 0000000..45e8495 --- /dev/null +++ b/server/sql_schema/V2_proposals.sql @@ -0,0 +1,7 @@ +CREATE TABLE proposals ( + owner_id integer NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + description text NOT NULL, + how text NOT NULL +); + +PRAGMA user_version = 2; \ No newline at end of file