-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdot_db.go
151 lines (133 loc) · 3.9 KB
/
dot_db.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package xtemplate
import (
"context"
"database/sql"
"fmt"
"log/slog"
"time"
)
// DotDB is used to create a dot field value that can query a SQL database. When
// any of its statement executing methods are called, it creates a new
// transaction. When template execution finishes, if there were no errors it
// automatically commits any uncommitted transactions remaining after template
// execution completes, but if there were errors then it calls rollback on the
// transaction.
type DotDB struct {
db *sql.DB
log *slog.Logger
ctx context.Context
opt *sql.TxOptions
tx *sql.Tx
}
func (d *DotDB) makeTx() (err error) {
if d.tx == nil {
d.tx, err = d.db.BeginTx(d.ctx, d.opt)
}
return
}
// Exec executes a statement with parameters and returns the raw [sql.Result].
// Note: this can be a bit difficult to use inside a template, consider using
// other methods that provide easier to use return values.
func (c *DotDB) Exec(query string, params ...any) (result sql.Result, err error) {
if err = c.makeTx(); err != nil {
return
}
defer func(start time.Time) {
c.log.Debug("Exec", slog.String("query", query), slog.Any("params", params), slog.Any("error", err), slog.Duration("queryduration", time.Since(start)))
}(time.Now())
return c.tx.Exec(query, params...)
}
// QueryRows executes a query and buffers all rows into a []map[string]any object.
func (c *DotDB) QueryRows(query string, params ...any) (rows []map[string]any, err error) {
if err = c.makeTx(); err != nil {
return
}
defer func(start time.Time) {
c.log.Debug("QueryRows", slog.String("query", query), slog.Any("params", params), slog.Any("error", err), slog.Duration("queryduration", time.Since(start)))
}(time.Now())
result, err := c.tx.Query(query, params...)
if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err)
}
defer result.Close()
var columns []string
// prepare scan output array
columns, err = result.Columns()
if err != nil {
return nil, err
}
n := len(columns)
out := make([]any, n)
for i := range columns {
out[i] = new(any)
}
for result.Next() {
err = result.Scan(out...)
if err != nil {
return nil, err
}
row := make(map[string]any, n)
for i, c := range columns {
row[c] = *out[i].(*any)
}
rows = append(rows, row)
}
return rows, result.Err()
}
// QueryRow executes a query, which must return one row, and returns it as a
// map[string]any.
func (c *DotDB) QueryRow(query string, params ...any) (map[string]any, error) {
rows, err := c.QueryRows(query, params...)
if err != nil {
return nil, err
}
if len(rows) != 1 {
return nil, fmt.Errorf("query returned %d rows, expected exactly 1 row", len(rows))
}
return rows[0], nil
}
// QueryVal executes a query, which must return one row with one column, and
// returns the value of the column.
func (c *DotDB) QueryVal(query string, params ...any) (any, error) {
row, err := c.QueryRow(query, params...)
if err != nil {
return nil, err
}
if len(row) != 1 {
return nil, fmt.Errorf("query returned %d columns, expected 1", len(row))
}
for _, v := range row {
return v, nil
}
panic("impossible condition")
}
// Commit manually commits any implicit transactions opened by this DotDB. This
// is called automatically if there were no errors at the end of template
// execution.
func (c *DotDB) Commit() (string, error) {
return "", c.commit()
}
func (c *DotDB) commit() error {
if c.tx != nil {
err := c.tx.Commit()
c.log.Debug("commit", slog.Any("error", err))
c.tx = nil
return err
}
return nil
}
// Rollback manually rolls back any implicit tranactions opened by this DotDB.
// This is called automatically if there were any errors that occurred during
// template exeuction.
func (c *DotDB) Rollback() (string, error) {
return "", c.rollback()
}
func (c *DotDB) rollback() error {
if c.tx != nil {
err := c.tx.Rollback()
c.log.Debug("rollback", slog.Any("error", err))
c.tx = nil
return err
}
return nil
}