Browse Source

Refactor database hooks to make them more useful

master
Jan-Lukas Else 4 months ago
parent
commit
da9ea7c0d7
  1. 107
      database.go
  2. 58
      databaseHooks.go
  3. 1
      go.mod
  4. 5
      go.sum

107
database.go

@ -1,15 +1,14 @@
package main
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"log"
"os"
"strings"
"sync"
"github.com/gchaincl/sqlhooks/v2"
sqlite "github.com/mattn/go-sqlite3"
"github.com/schollz/sqlite3dump"
"golang.org/x/sync/singleflight"
@ -22,10 +21,11 @@ type database struct {
sg singleflight.Group // singleflight group for prepared statements
ps sync.Map // map with prepared statements
// Other things
pc singleflight.Group // persistant cache
pcm sync.Mutex // post creation
sp singleflight.Group // singleflight group for short path requests
spc sync.Map // shortpath cache
pc singleflight.Group // persistant cache
pcm sync.Mutex // post creation
sp singleflight.Group // singleflight group for short path requests
spc sync.Map // shortpath cache
debug bool
}
func (a *goBlog) initDatabase(logging bool) (err error) {
@ -60,8 +60,8 @@ func (a *goBlog) initDatabase(logging bool) (err error) {
func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
// Register driver
dbDriverName := generateRandomString(15)
var dr driver.Driver = &sqlite.SQLiteDriver{
dbDriverName := "goblog_db_" + generateRandomString(15)
sql.Register(dbDriverName, &sqlite.SQLiteDriver{
ConnectHook: func(c *sqlite.SQLiteConn) error {
// Register functions
for n, f := range map[string]interface{}{
@ -79,13 +79,9 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
}
return nil
},
}
if c := a.cfg.Db; c != nil && c.Debug {
dr = sqlhooks.Wrap(dr, &dbHooks{})
}
sql.Register("goblog_db_"+dbDriverName, dr)
})
// Open db
db, err := sql.Open("goblog_db_"+dbDriverName, file+"?mode=rwc&_journal_mode=WAL&_busy_timeout=100&cache=shared")
db, err := sql.Open(dbDriverName, file+"?mode=rwc&_journal_mode=WAL&_busy_timeout=100&cache=shared")
if err != nil {
return nil, err
}
@ -118,8 +114,14 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
if err != nil {
return nil, err
}
// Debug
debug := false
if c := a.cfg.Db; c != nil && c.Debug {
debug = true
}
return &database{
db: db,
db: db,
debug: debug,
}, nil
}
@ -161,6 +163,9 @@ func (db *database) prepare(query string) (*sql.Stmt, error) {
return st, nil
})
if err != nil {
if db.debug {
log.Printf(`Failed to prepare query "%s": %s`, query, err.Error())
}
return nil, err
}
return stmt.(*sql.Stmt), nil
@ -172,45 +177,69 @@ func (db *database) exec(query string, args ...interface{}) (sql.Result, error)
// Lock execution
db.em.Lock()
defer db.em.Unlock()
// Check if prepared cache should be skipped
// Check if no cache arg set
cache := true
if len(args) > 0 && args[0] == dbNoCache {
return db.db.Exec(query, args[1:]...)
}
// Use prepared statement
st, _ := db.prepare(query)
cache = false
args = args[1:]
}
// Maybe prepare
var st *sql.Stmt
if cache {
st, _ = db.prepare(query)
}
// Prepare context, call hook
ctx := db.dbBefore(context.Background(), query, args...)
defer db.dbAfter(ctx, query, args...)
// Execute
if st != nil {
return st.Exec(args...)
return st.ExecContext(ctx, args...)
}
// Or execute directly
return db.db.Exec(query, args...)
return db.db.ExecContext(ctx, query, args...)
}
func (db *database) query(query string, args ...interface{}) (*sql.Rows, error) {
// Check if prepared cache should be skipped
// Check if no cache arg set
cache := true
if len(args) > 0 && args[0] == dbNoCache {
return db.db.Query(query, args[1:]...)
}
// Use prepared statement
st, _ := db.prepare(query)
cache = false
args = args[1:]
}
// Maybe prepare
var st *sql.Stmt
if cache {
st, _ = db.prepare(query)
}
// Prepare context, call hook
ctx := db.dbBefore(context.Background(), query, args...)
defer db.dbAfter(ctx, query, args...)
// Query
if st != nil {
return st.Query(args...)
return st.QueryContext(ctx, args...)
}
// Or query directly
return db.db.Query(query, args...)
return db.db.QueryContext(ctx, query, args...)
}
func (db *database) queryRow(query string, args ...interface{}) (*sql.Row, error) {
// Check if prepared cache should be skipped
// Check if no cache arg set
cache := true
if len(args) > 0 && args[0] == dbNoCache {
return db.db.QueryRow(query, args[1:]...), nil
}
// Use prepared statement
st, _ := db.prepare(query)
cache = false
args = args[1:]
}
// Maybe prepare
var st *sql.Stmt
if cache {
st, _ = db.prepare(query)
}
// Prepare context, call hook
ctx := db.dbBefore(context.Background(), query, args...)
defer db.dbAfter(ctx, query, args...)
// Query
if st != nil {
return st.QueryRow(args...), nil
return st.QueryRowContext(ctx, args...), nil
}
// Or query directly
return db.db.QueryRow(query, args...), nil
return db.db.QueryRowContext(ctx, query, args...), nil
}
// Other things

58
databaseHooks.go

@ -2,20 +2,62 @@ package main
import (
"context"
"database/sql"
"fmt"
"log"
"strings"
"time"
)
type dbHooks struct{}
"github.com/spf13/cast"
)
const dbHooksBegin contextKey = "begin"
func (h *dbHooks) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
return context.WithValue(ctx, dbHooksBegin, time.Now()), nil
func (db *database) dbBefore(ctx context.Context, query string, args ...interface{}) context.Context {
if !db.debug {
return ctx
}
return context.WithValue(ctx, dbHooksBegin, time.Now())
}
func (db *database) dbAfter(ctx context.Context, query string, args ...interface{}) {
if !db.debug {
return
}
dur := time.Since(ctx.Value(dbHooksBegin).(time.Time))
var logBuilder strings.Builder
logBuilder.WriteString("\nQuery: ")
logBuilder.WriteString(`"`)
logBuilder.WriteString(query)
logBuilder.WriteString(`"`)
if len(args) > 0 {
logBuilder.WriteString("\nArgs: ")
for i, arg := range args {
if i > 0 {
logBuilder.WriteString(", ")
}
if named, ok := arg.(sql.NamedArg); ok && named.Name != "" {
logBuilder.WriteString("(")
logBuilder.WriteString(named.Name)
logBuilder.WriteString(`) "`)
logBuilder.WriteString(argToString(named.Value))
logBuilder.WriteString(`"`)
} else {
logBuilder.WriteString(`"`)
logBuilder.WriteString(argToString(arg))
logBuilder.WriteString(`"`)
}
}
}
logBuilder.WriteString("\nDuration: ")
logBuilder.WriteString(dur.String())
log.Println(logBuilder.String())
}
func (h *dbHooks) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
begin := ctx.Value(dbHooksBegin).(time.Time)
log.Printf("SQL: %s %q (%s)\n", query, args, time.Since(begin))
return ctx, nil
func argToString(arg interface{}) string {
val := cast.ToString(arg)
if val == "" {
val = fmt.Sprintf("%v", arg)
}
return val
}

1
go.mod

@ -16,7 +16,6 @@ require (
github.com/dgraph-io/ristretto v0.1.0
github.com/elnormous/contenttype v1.0.0
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/gchaincl/sqlhooks/v2 v2.0.1
github.com/go-chi/chi/v5 v5.0.3
github.com/go-fed/httpsig v1.1.0
github.com/go-sql-driver/mysql v1.5.0 // indirect

5
go.sum

@ -111,8 +111,6 @@ github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gchaincl/sqlhooks/v2 v2.0.1 h1:j9WZAq1Tx/xngDfEdsgUww+o9iY3rLOYGYhYWcFxdmI=
github.com/gchaincl/sqlhooks/v2 v2.0.1/go.mod h1:Qv7HXjGB9TehamVK52yW5H+a0RRhprIj3ESTcWOG2jw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4=
github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@ -261,7 +259,6 @@ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECae
github.com/lestrrat-go/strftime v1.0.5 h1:A7H3tT8DhTz8u65w+JRpiBxM4dINQhUXAZnhBa2xeOE=
github.com/lestrrat-go/strftime v1.0.5/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
@ -272,7 +269,6 @@ github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
@ -292,7 +288,6 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=

Loading…
Cancel
Save