Skip to content

Commit ce32db2

Browse files
committed
replace Connection.Insert methods
1 parent e019dac commit ce32db2

15 files changed

+329
-228
lines changed

connection.go

+10-33
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ type (
1414
OnUnlistenFunc func(channel string)
1515
)
1616

17+
// PlaceholderFormatter is an interface for formatting query parameter placeholders
18+
// implemented by database connections.
19+
type PlaceholderFormatter interface {
20+
// Placeholder formats a query parameter placeholder
21+
// for the paramIndex starting at zero.
22+
Placeholder(paramIndex int) string
23+
}
24+
1725
// Connection represents a database connection or transaction
1826
type Connection interface {
27+
PlaceholderFormatter
28+
1929
// Context that all connection operations use.
2030
// See also WithContext.
2131
Context() context.Context
@@ -53,39 +63,6 @@ type Connection interface {
5363
// Exec executes a query with optional args.
5464
Exec(query string, args ...any) error
5565

56-
// Insert a new row into table using the values.
57-
Insert(table string, values Values) error
58-
59-
// InsertUnique inserts a new row into table using the passed values
60-
// or does nothing if the onConflict statement applies.
61-
// Returns if a row was inserted.
62-
InsertUnique(table string, values Values, onConflict string) (inserted bool, err error)
63-
64-
// InsertReturning inserts a new row into table using values
65-
// and returns values from the inserted row listed in returning.
66-
InsertReturning(table string, values Values, returning string) RowScanner
67-
68-
// InsertStruct inserts a new row into table using the connection's
69-
// StructFieldMapper to map struct fields to column names.
70-
// Optional ColumnFilter can be passed to ignore mapped columns.
71-
InsertStruct(table string, rowStruct any, ignoreColumns ...ColumnFilter) error
72-
73-
// InsertStructs inserts a slice or array of structs
74-
// as new rows into table using the connection's
75-
// StructFieldMapper to map struct fields to column names.
76-
// Optional ColumnFilter can be passed to ignore mapped columns.
77-
//
78-
// TODO optimized version with single query if possible
79-
// split into multiple queries depending or maxArgs for query
80-
InsertStructs(table string, rowStructs any, ignoreColumns ...ColumnFilter) error
81-
82-
// InsertUniqueStruct inserts a new row into table using the connection's
83-
// StructFieldMapper to map struct fields to column names.
84-
// Optional ColumnFilter can be passed to ignore mapped columns.
85-
// Does nothing if the onConflict statement applies
86-
// and returns if a row was inserted.
87-
InsertUniqueStruct(table string, rowStruct any, onConflict string, ignoreColumns ...ColumnFilter) (inserted bool, err error)
88-
8966
// Update table rows(s) with values using the where statement with passed in args starting at $1.
9067
Update(table string, values Values, where string, args ...any) error
9168

db/errors.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package db
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/domonda/go-sqldb"
7+
"github.com/domonda/go-sqldb/impl"
8+
)
9+
10+
// // WrapNonNilErrorWithQuery wraps non nil errors with a formatted query
11+
// // if the error was not already wrapped with a query.
12+
// // If the passed error is nil, then nil will be returned.
13+
// func WrapNonNilErrorWithQuery(err error, query string, args []any, argFmt sqldb.PlaceholderFormatter) error {
14+
// if err == nil {
15+
// return nil
16+
// }
17+
// var wrapped errWithQuery
18+
// if errors.As(err, &wrapped) {
19+
// return err // already wrapped
20+
// }
21+
// return errWithQuery{err, query, args, argFmt}
22+
// }
23+
24+
func wrapErrorWithQuery(err error, query string, args []any, argFmt sqldb.PlaceholderFormatter) error {
25+
return errWithQuery{err, query, args, argFmt}
26+
}
27+
28+
type errWithQuery struct {
29+
err error
30+
query string
31+
args []any
32+
argFmt sqldb.PlaceholderFormatter
33+
}
34+
35+
func (e errWithQuery) Unwrap() error { return e.err }
36+
37+
func (e errWithQuery) Error() string {
38+
return fmt.Sprintf("%s from query: %s", e.err, impl.FormatQuery2(e.query, e.argFmt, e.args...))
39+
}

db/insert.go

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package db
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
"strings"
8+
9+
"github.com/domonda/go-sqldb"
10+
"github.com/domonda/go-sqldb/impl"
11+
)
12+
13+
func writeInsertQuery(w *strings.Builder, table string, names []string, format sqldb.PlaceholderFormatter) {
14+
fmt.Fprintf(w, `INSERT INTO %s(`, table)
15+
for i, name := range names {
16+
if i > 0 {
17+
w.WriteByte(',')
18+
}
19+
w.WriteByte('"')
20+
w.WriteString(name)
21+
w.WriteByte('"')
22+
}
23+
w.WriteString(`) VALUES(`)
24+
for i := range names {
25+
if i > 0 {
26+
w.WriteByte(',')
27+
}
28+
w.WriteString(format.Placeholder(i))
29+
}
30+
w.WriteByte(')')
31+
}
32+
33+
func insertStructValues(table string, rowStruct any, namer sqldb.StructFieldMapper, ignoreColumns []sqldb.ColumnFilter) (columns []string, vals []any, err error) {
34+
v := reflect.ValueOf(rowStruct)
35+
for v.Kind() == reflect.Ptr && !v.IsNil() {
36+
v = v.Elem()
37+
}
38+
switch {
39+
case v.Kind() == reflect.Ptr && v.IsNil():
40+
return nil, nil, fmt.Errorf("InsertStruct into table %s: can't insert nil", table)
41+
case v.Kind() != reflect.Struct:
42+
return nil, nil, fmt.Errorf("InsertStruct into table %s: expected struct but got %T", table, rowStruct)
43+
}
44+
45+
columns, _, vals = impl.ReflectStructValues(v, namer, append(ignoreColumns, sqldb.IgnoreReadOnly))
46+
return columns, vals, nil
47+
}
48+
49+
// Insert a new row into table using the values.
50+
func Insert(ctx context.Context, table string, values sqldb.Values) error {
51+
if len(values) == 0 {
52+
return fmt.Errorf("Insert into table %s: no values", table)
53+
}
54+
conn := Conn(ctx)
55+
56+
var query strings.Builder
57+
names, vals := values.Sorted()
58+
writeInsertQuery(&query, table, names, conn)
59+
60+
err := conn.Exec(query.String(), vals...)
61+
if err != nil {
62+
return wrapErrorWithQuery(err, query.String(), vals, conn)
63+
}
64+
return nil
65+
}
66+
67+
// InsertUnique inserts a new row into table using the passed values
68+
// or does nothing if the onConflict statement applies.
69+
// Returns if a row was inserted.
70+
func InsertUnique(ctx context.Context, table string, values sqldb.Values, onConflict string) (inserted bool, err error) {
71+
if len(values) == 0 {
72+
return false, fmt.Errorf("InsertUnique into table %s: no values", table)
73+
}
74+
conn := Conn(ctx)
75+
76+
if strings.HasPrefix(onConflict, "(") && strings.HasSuffix(onConflict, ")") {
77+
onConflict = onConflict[1 : len(onConflict)-1]
78+
}
79+
80+
var query strings.Builder
81+
names, vals := values.Sorted()
82+
writeInsertQuery(&query, table, names, conn)
83+
fmt.Fprintf(&query, " ON CONFLICT (%s) DO NOTHING RETURNING TRUE", onConflict)
84+
85+
err = conn.QueryRow(query.String(), vals...).Scan(&inserted)
86+
err = sqldb.ReplaceErrNoRows(err, nil)
87+
if err != nil {
88+
return false, wrapErrorWithQuery(err, query.String(), vals, conn)
89+
}
90+
return inserted, err
91+
}
92+
93+
// InsertReturning inserts a new row into table using values
94+
// and returns values from the inserted row listed in returning.
95+
func InsertReturning(ctx context.Context, table string, values sqldb.Values, returning string) sqldb.RowScanner {
96+
if len(values) == 0 {
97+
return sqldb.RowScannerWithError(fmt.Errorf("InsertReturning into table %s: no values", table))
98+
}
99+
conn := Conn(ctx)
100+
101+
var query strings.Builder
102+
names, vals := values.Sorted()
103+
writeInsertQuery(&query, table, names, conn)
104+
query.WriteString(" RETURNING ")
105+
query.WriteString(returning)
106+
return conn.QueryRow(query.String(), vals...) // TODO wrap error with query
107+
}
108+
109+
// InsertStruct inserts a new row into table using the connection's
110+
// StructFieldMapper to map struct fields to column names.
111+
// Optional ColumnFilter can be passed to ignore mapped columns.
112+
func InsertStruct(ctx context.Context, table string, rowStruct any, ignoreColumns ...sqldb.ColumnFilter) error {
113+
conn := Conn(ctx)
114+
columns, vals, err := insertStructValues(table, rowStruct, conn.StructFieldMapper(), ignoreColumns)
115+
if err != nil {
116+
return err
117+
}
118+
119+
var query strings.Builder
120+
writeInsertQuery(&query, table, columns, conn)
121+
122+
err = conn.Exec(query.String(), vals...)
123+
if err != nil {
124+
return wrapErrorWithQuery(err, query.String(), vals, conn)
125+
}
126+
return nil
127+
}
128+
129+
// InsertUniqueStruct inserts a new row into table using the connection's
130+
// StructFieldMapper to map struct fields to column names.
131+
// Optional ColumnFilter can be passed to ignore mapped columns.
132+
// Does nothing if the onConflict statement applies
133+
// and returns if a row was inserted.
134+
func InsertUniqueStruct(ctx context.Context, table string, rowStruct any, onConflict string, ignoreColumns ...sqldb.ColumnFilter) (inserted bool, err error) {
135+
conn := Conn(ctx)
136+
columns, vals, err := insertStructValues(table, rowStruct, conn.StructFieldMapper(), ignoreColumns)
137+
if err != nil {
138+
return false, err
139+
}
140+
141+
if strings.HasPrefix(onConflict, "(") && strings.HasSuffix(onConflict, ")") {
142+
onConflict = onConflict[1 : len(onConflict)-1]
143+
}
144+
145+
var query strings.Builder
146+
writeInsertQuery(&query, table, columns, conn)
147+
fmt.Fprintf(&query, " ON CONFLICT (%s) DO NOTHING RETURNING TRUE", onConflict)
148+
149+
err = conn.QueryRow(query.String(), vals...).Scan(&inserted)
150+
err = sqldb.ReplaceErrNoRows(err, nil)
151+
if err != nil {
152+
return false, wrapErrorWithQuery(err, query.String(), vals, conn)
153+
}
154+
return inserted, err
155+
}
156+
157+
// InsertStructs inserts a slice or array of structs
158+
// as new rows into table using the connection's
159+
// StructFieldMapper to map struct fields to column names.
160+
// Optional ColumnFilter can be passed to ignore mapped columns.
161+
//
162+
// TODO optimized version with single query if possible
163+
// split into multiple queries depending or maxArgs for query
164+
func InsertStructs(ctx context.Context, table string, rowStructs any, ignoreColumns ...sqldb.ColumnFilter) error {
165+
v := reflect.ValueOf(rowStructs)
166+
if k := v.Type().Kind(); k != reflect.Slice && k != reflect.Array {
167+
return fmt.Errorf("InsertStructs expects a slice or array as rowStructs, got %T", rowStructs)
168+
}
169+
numRows := v.Len()
170+
return Transaction(ctx, func(ctx context.Context) error {
171+
for i := 0; i < numRows; i++ {
172+
err := InsertStruct(ctx, table, v.Index(i).Interface(), ignoreColumns...)
173+
if err != nil {
174+
return err
175+
}
176+
}
177+
return nil
178+
})
179+
}

db/query.go

-7
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,3 @@ func QueryStructSlice[S any](ctx context.Context, query string, args ...any) (ro
210210
}
211211
return rows, nil
212212
}
213-
214-
// InsertStruct inserts a new row into table using the connection's
215-
// StructFieldMapper to map struct fields to column names.
216-
// Optional ColumnFilter can be passed to ignore mapped columns.
217-
func InsertStruct(ctx context.Context, table string, rowStruct any, ignoreColumns ...sqldb.ColumnFilter) error {
218-
return Conn(ctx).InsertStruct(table, rowStruct, ignoreColumns...)
219-
}

errors.go

+5-24
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
"fmt"
78
"time"
89
)
910

@@ -208,38 +209,18 @@ func (e connectionWithError) Config() *Config {
208209
return &Config{Err: e.err}
209210
}
210211

211-
func (e connectionWithError) ValidateColumnName(name string) error {
212-
return e.err
213-
}
214-
215-
func (e connectionWithError) Exec(query string, args ...any) error {
216-
return e.err
217-
}
218-
219-
func (e connectionWithError) Insert(table string, values Values) error {
220-
return e.err
212+
func (e connectionWithError) Placeholder(paramIndex int) string {
213+
return fmt.Sprintf("$%d", paramIndex+1)
221214
}
222215

223-
func (e connectionWithError) InsertUnique(table string, values Values, onConflict string) (inserted bool, err error) {
224-
return false, e.err
225-
}
226-
227-
func (e connectionWithError) InsertReturning(table string, values Values, returning string) RowScanner {
228-
return RowScannerWithError(e.err)
229-
}
230-
231-
func (e connectionWithError) InsertStruct(table string, rowStruct any, ignoreColumns ...ColumnFilter) error {
216+
func (e connectionWithError) ValidateColumnName(name string) error {
232217
return e.err
233218
}
234219

235-
func (e connectionWithError) InsertStructs(table string, rowStructs any, ignoreColumns ...ColumnFilter) error {
220+
func (e connectionWithError) Exec(query string, args ...any) error {
236221
return e.err
237222
}
238223

239-
func (e connectionWithError) InsertUniqueStruct(table string, rowStruct any, onConflict string, ignoreColumns ...ColumnFilter) (inserted bool, err error) {
240-
return false, e.err
241-
}
242-
243224
func (e connectionWithError) Update(table string, values Values, where string, args ...any) error {
244225
return e.err
245226
}

impl/connection.go

+4-24
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ func (conn *connection) Config() *sqldb.Config {
7878
return conn.config
7979
}
8080

81+
func (conn *connection) Placeholder(paramIndex int) string {
82+
return fmt.Sprintf(conn.argFmt, paramIndex+1)
83+
}
84+
8185
func (conn *connection) ValidateColumnName(name string) error {
8286
return conn.validateColumnName(name)
8387
}
@@ -87,30 +91,6 @@ func (conn *connection) Exec(query string, args ...any) error {
8791
return WrapNonNilErrorWithQuery(err, query, conn.argFmt, args)
8892
}
8993

90-
func (conn *connection) Insert(table string, columValues sqldb.Values) error {
91-
return Insert(conn, table, conn.argFmt, columValues)
92-
}
93-
94-
func (conn *connection) InsertUnique(table string, values sqldb.Values, onConflict string) (inserted bool, err error) {
95-
return InsertUnique(conn, table, conn.argFmt, values, onConflict)
96-
}
97-
98-
func (conn *connection) InsertReturning(table string, values sqldb.Values, returning string) sqldb.RowScanner {
99-
return InsertReturning(conn, table, conn.argFmt, values, returning)
100-
}
101-
102-
func (conn *connection) InsertStruct(table string, rowStruct any, ignoreColumns ...sqldb.ColumnFilter) error {
103-
return InsertStruct(conn, table, rowStruct, conn.structFieldNamer, conn.argFmt, ignoreColumns)
104-
}
105-
106-
func (conn *connection) InsertStructs(table string, rowStructs any, ignoreColumns ...sqldb.ColumnFilter) error {
107-
return InsertStructs(conn, table, rowStructs, ignoreColumns...)
108-
}
109-
110-
func (conn *connection) InsertUniqueStruct(table string, rowStruct any, onConflict string, ignoreColumns ...sqldb.ColumnFilter) (inserted bool, err error) {
111-
return InsertUniqueStruct(conn, table, rowStruct, onConflict, conn.structFieldNamer, conn.argFmt, ignoreColumns)
112-
}
113-
11494
func (conn *connection) Update(table string, values sqldb.Values, where string, args ...any) error {
11595
return Update(conn, table, values, where, conn.argFmt, args)
11696
}

impl/errors.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import (
99
// if the error was not already wrapped with a query.
1010
// If the passed error is nil, then nil will be returned.
1111
func WrapNonNilErrorWithQuery(err error, query, argFmt string, args []any) error {
12+
if err == nil {
13+
return nil
14+
}
1215
var wrapped errWithQuery
13-
if err == nil || errors.As(err, &wrapped) {
14-
return err
16+
if errors.As(err, &wrapped) {
17+
return err // already wrapped
1518
}
1619
return errWithQuery{err, query, argFmt, args}
1720
}

0 commit comments

Comments
 (0)