github.com/kyleu/dbaudit@v0.0.2-0.20240321155047-ff2f2c940496/app/controller/clib/database.go (about) 1 // Package clib - Content managed by Project Forge, see [projectforge.md] for details. 2 package clib 3 4 import ( 5 "fmt" 6 "net/http" 7 "strconv" 8 9 "github.com/pkg/errors" 10 11 "github.com/kyleu/dbaudit/app" 12 "github.com/kyleu/dbaudit/app/controller" 13 "github.com/kyleu/dbaudit/app/controller/cutil" 14 "github.com/kyleu/dbaudit/app/lib/database" 15 "github.com/kyleu/dbaudit/app/util" 16 "github.com/kyleu/dbaudit/views/vdatabase" 17 ) 18 19 const KeyAnalyze = "analyze" 20 21 func DatabaseList(w http.ResponseWriter, r *http.Request) { 22 controller.Act("database.list", w, r, func(as *app.State, ps *cutil.PageState) (string, error) { 23 keys := database.RegistryKeys() 24 if len(keys) == 1 { 25 return "/admin/database/" + keys[0], nil 26 } 27 svcs := make(map[string]*database.Service, len(keys)) 28 for _, key := range keys { 29 svc, err := database.RegistryGet(key) 30 if err != nil { 31 return "", errors.Wrapf(err, "no database found with key [%s]", key) 32 } 33 svcs[key] = svc 34 } 35 return controller.Render(w, r, as, &vdatabase.List{Keys: keys, Services: svcs}, ps, "admin", "Database") 36 }) 37 } 38 39 func DatabaseDetail(w http.ResponseWriter, r *http.Request) { 40 controller.Act("database.detail", w, r, func(as *app.State, ps *cutil.PageState) (string, error) { 41 svc, err := getDatabaseService(r) 42 if err != nil { 43 return "", err 44 } 45 return controller.Render(w, r, as, &vdatabase.Detail{Mode: "", Svc: svc}, ps, "admin", "Database||/admin/database", svc.Key) 46 }) 47 } 48 49 func DatabaseAction(w http.ResponseWriter, r *http.Request) { 50 controller.Act("database.action", w, r, func(as *app.State, ps *cutil.PageState) (string, error) { 51 svc, err := getDatabaseService(r) 52 if err != nil { 53 return "", err 54 } 55 act, err := cutil.RCRequiredString(r, "act", true) 56 if err != nil { 57 return "", err 58 } 59 bc := []string{"admin", "Database||/admin/database", fmt.Sprintf("%s||/admin/database/%s", svc.Key, svc.Key), act} 60 switch act { 61 case "enable": 62 _ = svc.EnableTracing(r.URL.Query().Get("tracing"), ps.Logger) 63 return "/admin/database/" + svc.Key + "/recent", nil 64 case "recent": 65 if idxStr := r.URL.Query().Get("idx"); idxStr != "" { 66 idx, _ := strconv.ParseInt(idxStr, 10, 32) 67 st := database.GetDebugStatement(svc.Key, int(idx)) 68 if st != nil { 69 return controller.Render(w, r, as, &vdatabase.Statement{Statement: st}, ps, bc...) 70 } 71 } 72 recent := database.GetDebugStatements(svc.Key) 73 return controller.Render(w, r, as, &vdatabase.Detail{Mode: "recent", Svc: svc, Recent: recent}, ps, bc...) 74 case "tables": 75 sizes, dberr := svc.Sizes(ps.Context, ps.Logger) 76 if dberr != nil { 77 return "", errors.Wrapf(dberr, "unable to calculate sizes for database [%s]", svc.Key) 78 } 79 return controller.Render(w, r, as, &vdatabase.Detail{Mode: "tables", Svc: svc, Sizes: sizes}, ps, bc...) 80 case KeyAnalyze: 81 t := util.TimerStart() 82 var tmp []any 83 err = svc.Select(ps.Context, &tmp, KeyAnalyze, nil, ps.Logger) 84 if err != nil { 85 return "", err 86 } 87 msg := fmt.Sprintf("Analyzed database in [%s]", util.MicrosToMillis(t.End())) 88 return controller.FlashAndRedir(true, msg, "/admin/database/"+svc.Key+"/tables", w, ps) 89 case "sql": 90 return controller.Render(w, r, as, &vdatabase.Detail{Mode: "sql", Svc: svc, SQL: "select 1;"}, ps, bc...) 91 default: 92 return "", errors.Errorf("invalid database action [%s]", act) 93 } 94 }) 95 } 96 97 func DatabaseTableView(w http.ResponseWriter, r *http.Request) { 98 controller.Act("database.sql.run", w, r, func(as *app.State, ps *cutil.PageState) (string, error) { 99 prms := ps.Params.Get("table", []string{"*"}, ps.Logger).Sanitize("table") 100 svc, err := getDatabaseService(r) 101 if err != nil { 102 return "", err 103 } 104 schema, _ := cutil.RCRequiredString(r, "schema", true) 105 table, _ := cutil.RCRequiredString(r, "table", true) 106 107 tbl := fmt.Sprintf("%q", table) 108 if schema != "default" { 109 tbl = fmt.Sprintf("%q.%q", schema, table) 110 } 111 112 q := database.SQLSelect("*", tbl, "", prms.OrderByString(), prms.Limit, prms.Offset, svc.Type) 113 res, err := svc.QueryRows(ps.Context, q, nil, ps.Logger) 114 ps.Data = res 115 bc := []string{"admin", "Database||/admin/database", fmt.Sprintf("%s||/admin/database/%s", svc.Key, svc.Key), "Tables"} 116 return controller.Render(w, r, as, &vdatabase.Results{Svc: svc, Schema: schema, Table: table, Results: res, Params: prms, Error: err}, ps, bc...) 117 }) 118 } 119 120 func DatabaseSQLRun(w http.ResponseWriter, r *http.Request) { 121 controller.Act("database.sql.run", w, r, func(as *app.State, ps *cutil.PageState) (string, error) { 122 svc, err := getDatabaseService(r) 123 if err != nil { 124 return "", err 125 } 126 _ = r.ParseForm() 127 f := r.PostForm 128 sql := f.Get("sql") 129 c := f.Get("commit") 130 commit := c == util.BoolTrue 131 action := f.Get("action") 132 if action == KeyAnalyze { 133 sql = "explain analyze " + sql 134 } 135 136 tx, err := svc.StartTransaction(ps.Logger) 137 if err != nil { 138 return "", errors.Wrap(err, "unable to start transaction") 139 } 140 defer func() { _ = tx.Rollback() }() 141 142 var columns []string 143 results := [][]any{} 144 145 timer := util.TimerStart() 146 result, err := svc.Query(ps.Context, sql, tx, ps.Logger) 147 if err != nil { 148 return "", err 149 } 150 defer func() { _ = result.Close() }() 151 152 elapsed := timer.End() 153 154 if result != nil { 155 for result.Next() { 156 if columns == nil { 157 columns, _ = result.Columns() 158 } 159 row, e := result.SliceScan() 160 if e != nil { 161 return "", errors.Wrap(e, "unable to read row") 162 } 163 results = append(results, row) 164 } 165 } 166 if commit { 167 err = tx.Commit() 168 if err != nil { 169 return "", errors.Wrap(err, "unable to commit transaction") 170 } 171 } else { 172 _ = tx.Rollback() 173 } 174 175 ps.SetTitleAndData("SQL Results", results) 176 page := &vdatabase.Detail{Mode: "sql", Svc: svc, SQL: sql, Columns: columns, Results: results, Timing: elapsed, Commit: commit} 177 return controller.Render(w, r, as, page, ps, "admin", "Database||/admin/database", svc.Key+"||/admin/database/"+svc.Key, "Results") 178 }) 179 } 180 181 func getDatabaseService(r *http.Request) (*database.Service, error) { 182 key, err := cutil.RCRequiredString(r, "key", true) 183 if err != nil { 184 return nil, err 185 } 186 svc, err := database.RegistryGet(key) 187 if err != nil { 188 return nil, errors.Wrapf(err, "no database found with key [%s]", key) 189 } 190 return svc, nil 191 }