Compare commits

...

4 Commits

Author SHA1 Message Date
Qishuai Liu 2a5c602f5e
handle MySQL zero date 2024-09-06 17:23:12 +09:00
Qishuai Liu 65c4cf7d2d
add ChainInterceptors 2024-09-06 11:40:41 +09:00
Qishuai Liu 2512270cf6
fix comments 2024-09-06 11:40:08 +09:00
Qishuai Liu 3b42e2a6b0
fix time layout 2024-09-06 11:12:21 +09:00
6 changed files with 159 additions and 37 deletions

View File

@ -4,7 +4,9 @@ import (
"database/sql"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
)
@ -31,23 +33,36 @@ func (c cursor) Next() bool {
var timeType = reflect.TypeOf(time.Time{})
var timeLayouts = []string{
"2006-01-02",
"2006-01-02 15:04:05",
"2006-01-02 15:04:05.000",
"2006-01-02 15:04:05.000000",
"2006-01-02 15:04:05.000000000",
time.RFC3339Nano,
var simpleTimeLayoutRegexp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.(\d+))?$`)
func guessTimeLayout(s string) string {
matches := simpleTimeLayoutRegexp.FindStringSubmatch(s)
if len(matches) > 0 {
var sb strings.Builder
sb.Grow(32)
sb.WriteString("2006-01-02 15:04:05")
if matches[1] != "" {
sb.WriteString(".")
for i := 0; i < len(matches[2]); i++ {
sb.WriteByte('0')
}
}
return sb.String()
}
return time.RFC3339Nano
}
func parseTime(s string) (time.Time, error) {
for _, layout := range timeLayouts {
t, err := time.Parse(layout, s)
if err == nil {
return t, nil
}
if strings.HasPrefix(s, "0000-00-00") {
// MySQL zero date
return time.Time{}, nil
}
return time.Time{}, fmt.Errorf("unknown time format %s", s)
layout := guessTimeLayout(s)
t, err := time.Parse(layout, s)
if err != nil {
return time.Time{}, fmt.Errorf("unknown time format %s: %w", s, err)
}
return t, nil
}
func isScanner(val reflect.Value) bool {

View File

@ -209,3 +209,26 @@ func TestCursorMap(t *testing.T) {
t.Error()
}
}
func TestParseTime(t *testing.T) {
tests := []struct {
input string
output time.Time
}{
{"2024-09-06 11:22:33", time.Date(2024, 9, 6, 11, 22, 33, 0, time.UTC)},
{"2024-09-06 11:22:33.444", time.Date(2024, 9, 6, 11, 22, 33, 444000000, time.UTC)},
{"2024-09-06 11:22:33.444555666", time.Date(2024, 9, 6, 11, 22, 33, 444555666, time.UTC)},
{"2024-09-06T11:22:33.444555666Z", time.Date(2024, 9, 6, 11, 22, 33, 444555666, time.UTC)},
{"0000-00-00 00:00:00", time.Time{}},
}
for _, test := range tests {
tm, err := parseTime(test.input)
if err != nil {
t.Error(err)
continue
}
if tm != test.output {
t.Error(tm, test.output)
}
}
}

View File

@ -22,41 +22,43 @@ const (
// Database is the interface of a database with underlying sql.DB object.
type Database interface {
// Get the underlying sql.DB object of the database
// GetDB returns the underlying sql.DB object of the database
GetDB() *sql.DB
// BeginTx starts a transaction and executes the function f.
BeginTx(ctx context.Context, opts *sql.TxOptions, f func(tx Transaction) error) error
// Executes a query and return the cursor
// Query executes a query and returns the cursor
Query(sql string) (Cursor, error)
// Executes a query with context and return the cursor
// QueryContext executes a query with context and returns the cursor
QueryContext(ctx context.Context, sqlString string) (Cursor, error)
// Executes a statement
// Execute executes a statement
Execute(sql string) (sql.Result, error)
// Executes a statement with context
// ExecuteContext executes a statement with context
ExecuteContext(ctx context.Context, sql string) (sql.Result, error)
// Set the logger function
// SetLogger sets the logger function.
// Deprecated: use SetInterceptor instead
SetLogger(logger LoggerFunc)
// Set the retry policy function.
// The retry policy function returns true if needs retry.
// SetRetryPolicy sets the retry policy function.
// Deprecated: use SetInterceptor instead
SetRetryPolicy(retryPolicy func(err error) bool)
// EnableCallerInfo enable or disable the caller info in the log.
// Deprecated: use SetLogger instead
// Deprecated: use SetInterceptor instead
EnableCallerInfo(enableCallerInfo bool)
// Set a interceptor function
// SetInterceptor sets an interceptor function
SetInterceptor(interceptor InterceptorFunc)
// Initiate a SELECT statement
// Select initiates a SELECT statement
Select(fields ...interface{}) selectWithFields
// Initiate a SELECT DISTINCT statement
// SelectDistinct initiates a SELECT DISTINCT statement
SelectDistinct(fields ...interface{}) selectWithFields
// Initiate a SELECT * FROM statement
// SelectFrom initiates a SELECT * FROM statement
SelectFrom(tables ...Table) selectWithTables
// Initiate a INSERT INTO statement
// InsertInto initiates a INSERT INTO statement
InsertInto(table Table) insertWithTable
// Initiate a REPLACE INTO statement
// ReplaceInto initiates a REPLACE INTO statement
ReplaceInto(table Table) insertWithTable
// Initiate a UPDATE statement
// Update initiates a UPDATE statement
Update(table Table) updateWithSet
// Initiate a DELETE FROM statement
// DeleteFrom initiates a DELETE FROM statement
DeleteFrom(table Table) deleteWithTable
}

View File

@ -328,6 +328,7 @@ func quoteString(s string) string {
}
func getSQL(scope scope, value interface{}) (sql string, priority priority, err error) {
const mysqlTimeFormat = "2006-01-02 15:04:05.000000"
if value == nil {
sql = "NULL"
return
@ -355,14 +356,19 @@ func getSQL(scope scope, value interface{}) (sql string, priority priority, err
case CaseExpression:
sql, err = value.(CaseExpression).End().GetSQL(scope)
case time.Time:
tmStr := value.(time.Time).Format("2006-01-02 15:04:05")
sql = quoteString(tmStr)
case *time.Time:
tm := value.(*time.Time)
if tm == nil {
tm := value.(time.Time)
if tm.IsZero() {
sql = "NULL"
} else {
tmStr := tm.Format("2006-01-02 15:04:05")
tmStr := tm.Format(mysqlTimeFormat)
sql = quoteString(tmStr)
}
case *time.Time:
tm := value.(*time.Time)
if tm == nil || tm.IsZero() {
sql = "NULL"
} else {
tmStr := tm.Format(mysqlTimeFormat)
sql = quoteString(tmStr)
}
default:

View File

@ -10,4 +10,25 @@ type InvokerFunc = func(ctx context.Context, sql string) error
// InterceptorFunc is the function type of an interceptor. An interceptor should implement this function to fulfill it's purpose.
type InterceptorFunc = func(ctx context.Context, sql string, invoker InvokerFunc) error
// TODO: add some common interceptors
func noopInterceptor(ctx context.Context, sql string, invoker InvokerFunc) error {
return invoker(ctx, sql)
}
// ChainInterceptors chains multiple interceptors into one interceptor.
func ChainInterceptors(interceptors ...InterceptorFunc) InterceptorFunc {
if len(interceptors) == 0 {
return noopInterceptor
}
return func(ctx context.Context, sql string, invoker InvokerFunc) error {
var chain func(int, context.Context, string) error
chain = func(i int, ctx context.Context, sql string) error {
if i == len(interceptors) {
return invoker(ctx, sql)
}
return interceptors[i](ctx, sql, func(ctx context.Context, sql string) error {
return chain(i+1, ctx, sql)
})
}
return chain(0, ctx, sql)
}
}

55
interceptor_test.go Normal file
View File

@ -0,0 +1,55 @@
package sqlingo
import (
"context"
"testing"
)
func TestChainInterceptors(t *testing.T) {
s := ""
i1 := func(ctx context.Context, sql string, invoker InvokerFunc) error {
s += "<i1>"
s += sql
defer func() {
s += "</i1>"
}()
return invoker(ctx, sql+"s1")
}
i2 := func(ctx context.Context, sql string, invoker InvokerFunc) error {
s += "<i2>"
s += sql
defer func() {
s += "</i2>"
}()
return invoker(ctx, sql+"s2")
}
chain := ChainInterceptors(i1, i2)
_ = chain(context.Background(), "sql", func(ctx context.Context, sql string) error {
s += "<invoker>"
s += sql
defer func() {
s += "</invoker>"
}()
return nil
})
if s != "<i1>sql<i2>sqls1<invoker>sqls1s2</invoker></i2></i1>" {
t.Error(s)
}
}
func TestEmptyChainInterceptors(t *testing.T) {
s := ""
chain := ChainInterceptors()
_ = chain(context.Background(), "sql", func(ctx context.Context, sql string) error {
s += "<invoker>"
defer func() {
s += "</invoker>"
}()
s += sql
return nil
})
if s != "<invoker>sql</invoker>" {
t.Error(s)
}
}