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  }