mirror of https://github.com/lqs/sqlingo
Compare commits
4 Commits
d32d0eace4
...
2a5c602f5e
| Author | SHA1 | Date |
|---|---|---|
|
|
2a5c602f5e | |
|
|
65c4cf7d2d | |
|
|
2512270cf6 | |
|
|
3b42e2a6b0 |
41
cursor.go
41
cursor.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
database.go
36
database.go
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue