-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
329 additions
and
228 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package db | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/domonda/go-sqldb" | ||
"github.com/domonda/go-sqldb/impl" | ||
) | ||
|
||
// // WrapNonNilErrorWithQuery wraps non nil errors with a formatted query | ||
// // if the error was not already wrapped with a query. | ||
// // If the passed error is nil, then nil will be returned. | ||
// func WrapNonNilErrorWithQuery(err error, query string, args []any, argFmt sqldb.PlaceholderFormatter) error { | ||
// if err == nil { | ||
// return nil | ||
// } | ||
// var wrapped errWithQuery | ||
// if errors.As(err, &wrapped) { | ||
// return err // already wrapped | ||
// } | ||
// return errWithQuery{err, query, args, argFmt} | ||
// } | ||
|
||
func wrapErrorWithQuery(err error, query string, args []any, argFmt sqldb.PlaceholderFormatter) error { | ||
return errWithQuery{err, query, args, argFmt} | ||
} | ||
|
||
type errWithQuery struct { | ||
err error | ||
query string | ||
args []any | ||
argFmt sqldb.PlaceholderFormatter | ||
} | ||
|
||
func (e errWithQuery) Unwrap() error { return e.err } | ||
|
||
func (e errWithQuery) Error() string { | ||
return fmt.Sprintf("%s from query: %s", e.err, impl.FormatQuery2(e.query, e.argFmt, e.args...)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
package db | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/domonda/go-sqldb" | ||
"github.com/domonda/go-sqldb/impl" | ||
) | ||
|
||
func writeInsertQuery(w *strings.Builder, table string, names []string, format sqldb.PlaceholderFormatter) { | ||
fmt.Fprintf(w, `INSERT INTO %s(`, table) | ||
for i, name := range names { | ||
if i > 0 { | ||
w.WriteByte(',') | ||
} | ||
w.WriteByte('"') | ||
w.WriteString(name) | ||
w.WriteByte('"') | ||
} | ||
w.WriteString(`) VALUES(`) | ||
for i := range names { | ||
if i > 0 { | ||
w.WriteByte(',') | ||
} | ||
w.WriteString(format.Placeholder(i)) | ||
} | ||
w.WriteByte(')') | ||
} | ||
|
||
func insertStructValues(table string, rowStruct any, namer sqldb.StructFieldMapper, ignoreColumns []sqldb.ColumnFilter) (columns []string, vals []any, err error) { | ||
v := reflect.ValueOf(rowStruct) | ||
for v.Kind() == reflect.Ptr && !v.IsNil() { | ||
v = v.Elem() | ||
} | ||
switch { | ||
case v.Kind() == reflect.Ptr && v.IsNil(): | ||
return nil, nil, fmt.Errorf("InsertStruct into table %s: can't insert nil", table) | ||
case v.Kind() != reflect.Struct: | ||
return nil, nil, fmt.Errorf("InsertStruct into table %s: expected struct but got %T", table, rowStruct) | ||
} | ||
|
||
columns, _, vals = impl.ReflectStructValues(v, namer, append(ignoreColumns, sqldb.IgnoreReadOnly)) | ||
return columns, vals, nil | ||
} | ||
|
||
// Insert a new row into table using the values. | ||
func Insert(ctx context.Context, table string, values sqldb.Values) error { | ||
if len(values) == 0 { | ||
return fmt.Errorf("Insert into table %s: no values", table) | ||
} | ||
conn := Conn(ctx) | ||
|
||
var query strings.Builder | ||
names, vals := values.Sorted() | ||
writeInsertQuery(&query, table, names, conn) | ||
|
||
err := conn.Exec(query.String(), vals...) | ||
if err != nil { | ||
return wrapErrorWithQuery(err, query.String(), vals, conn) | ||
} | ||
return nil | ||
} | ||
|
||
// InsertUnique inserts a new row into table using the passed values | ||
// or does nothing if the onConflict statement applies. | ||
// Returns if a row was inserted. | ||
func InsertUnique(ctx context.Context, table string, values sqldb.Values, onConflict string) (inserted bool, err error) { | ||
if len(values) == 0 { | ||
return false, fmt.Errorf("InsertUnique into table %s: no values", table) | ||
} | ||
conn := Conn(ctx) | ||
|
||
if strings.HasPrefix(onConflict, "(") && strings.HasSuffix(onConflict, ")") { | ||
onConflict = onConflict[1 : len(onConflict)-1] | ||
} | ||
|
||
var query strings.Builder | ||
names, vals := values.Sorted() | ||
writeInsertQuery(&query, table, names, conn) | ||
fmt.Fprintf(&query, " ON CONFLICT (%s) DO NOTHING RETURNING TRUE", onConflict) | ||
|
||
err = conn.QueryRow(query.String(), vals...).Scan(&inserted) | ||
err = sqldb.ReplaceErrNoRows(err, nil) | ||
if err != nil { | ||
return false, wrapErrorWithQuery(err, query.String(), vals, conn) | ||
} | ||
return inserted, err | ||
} | ||
|
||
// InsertReturning inserts a new row into table using values | ||
// and returns values from the inserted row listed in returning. | ||
func InsertReturning(ctx context.Context, table string, values sqldb.Values, returning string) sqldb.RowScanner { | ||
if len(values) == 0 { | ||
return sqldb.RowScannerWithError(fmt.Errorf("InsertReturning into table %s: no values", table)) | ||
} | ||
conn := Conn(ctx) | ||
|
||
var query strings.Builder | ||
names, vals := values.Sorted() | ||
writeInsertQuery(&query, table, names, conn) | ||
query.WriteString(" RETURNING ") | ||
query.WriteString(returning) | ||
return conn.QueryRow(query.String(), vals...) // TODO wrap error with query | ||
} | ||
|
||
// InsertStruct inserts a new row into table using the connection's | ||
// StructFieldMapper to map struct fields to column names. | ||
// Optional ColumnFilter can be passed to ignore mapped columns. | ||
func InsertStruct(ctx context.Context, table string, rowStruct any, ignoreColumns ...sqldb.ColumnFilter) error { | ||
conn := Conn(ctx) | ||
columns, vals, err := insertStructValues(table, rowStruct, conn.StructFieldMapper(), ignoreColumns) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var query strings.Builder | ||
writeInsertQuery(&query, table, columns, conn) | ||
|
||
err = conn.Exec(query.String(), vals...) | ||
if err != nil { | ||
return wrapErrorWithQuery(err, query.String(), vals, conn) | ||
} | ||
return nil | ||
} | ||
|
||
// InsertUniqueStruct inserts a new row into table using the connection's | ||
// StructFieldMapper to map struct fields to column names. | ||
// Optional ColumnFilter can be passed to ignore mapped columns. | ||
// Does nothing if the onConflict statement applies | ||
// and returns if a row was inserted. | ||
func InsertUniqueStruct(ctx context.Context, table string, rowStruct any, onConflict string, ignoreColumns ...sqldb.ColumnFilter) (inserted bool, err error) { | ||
conn := Conn(ctx) | ||
columns, vals, err := insertStructValues(table, rowStruct, conn.StructFieldMapper(), ignoreColumns) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
if strings.HasPrefix(onConflict, "(") && strings.HasSuffix(onConflict, ")") { | ||
onConflict = onConflict[1 : len(onConflict)-1] | ||
} | ||
|
||
var query strings.Builder | ||
writeInsertQuery(&query, table, columns, conn) | ||
fmt.Fprintf(&query, " ON CONFLICT (%s) DO NOTHING RETURNING TRUE", onConflict) | ||
|
||
err = conn.QueryRow(query.String(), vals...).Scan(&inserted) | ||
err = sqldb.ReplaceErrNoRows(err, nil) | ||
if err != nil { | ||
return false, wrapErrorWithQuery(err, query.String(), vals, conn) | ||
} | ||
return inserted, err | ||
} | ||
|
||
// InsertStructs inserts a slice or array of structs | ||
// as new rows into table using the connection's | ||
// StructFieldMapper to map struct fields to column names. | ||
// Optional ColumnFilter can be passed to ignore mapped columns. | ||
// | ||
// TODO optimized version with single query if possible | ||
// split into multiple queries depending or maxArgs for query | ||
func InsertStructs(ctx context.Context, table string, rowStructs any, ignoreColumns ...sqldb.ColumnFilter) error { | ||
v := reflect.ValueOf(rowStructs) | ||
if k := v.Type().Kind(); k != reflect.Slice && k != reflect.Array { | ||
return fmt.Errorf("InsertStructs expects a slice or array as rowStructs, got %T", rowStructs) | ||
} | ||
numRows := v.Len() | ||
return Transaction(ctx, func(ctx context.Context) error { | ||
for i := 0; i < numRows; i++ { | ||
err := InsertStruct(ctx, table, v.Index(i).Interface(), ignoreColumns...) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.