mirror of https://github.com/lqs/sqlingo
Compare commits
5 Commits
e6ea482d73
...
99a6d5b37a
| Author | SHA1 | Date |
|---|---|---|
|
|
99a6d5b37a | |
|
|
a201a9b563 | |
|
|
6ea08e79aa | |
|
|
333f52e702 | |
|
|
8718eca7b5 |
|
|
@ -139,7 +139,7 @@ func commaOrderBys(scope scope, orderBys []OrderBy) (string, error) {
|
||||||
return sqlBuilder.String(), nil
|
return sqlBuilder.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCallerInfo(db database, retry bool) string {
|
func getCallerInfo(db *database, retry bool) string {
|
||||||
if !db.enableCallerInfo {
|
if !db.enableCallerInfo {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
database.go
27
database.go
|
|
@ -26,6 +26,10 @@ type Database interface {
|
||||||
GetDB() *sql.DB
|
GetDB() *sql.DB
|
||||||
// BeginTx starts a transaction and executes the function f.
|
// BeginTx starts a transaction and executes the function f.
|
||||||
BeginTx(ctx context.Context, opts *sql.TxOptions, f func(tx Transaction) error) error
|
BeginTx(ctx context.Context, opts *sql.TxOptions, f func(tx Transaction) error) error
|
||||||
|
// EnsureTx ensures the function f runs within a transaction.
|
||||||
|
// If ctx already contains a transaction started by a previous EnsureTx call, it reuses that transaction.
|
||||||
|
// Otherwise, it begins a new transaction and stores it in the context.
|
||||||
|
EnsureTx(ctx context.Context, opts *sql.TxOptions, f func(ctx context.Context) error) error
|
||||||
// Query executes a query and returns the cursor
|
// Query executes a query and returns the cursor
|
||||||
Query(sql string) (Cursor, error)
|
Query(sql string) (Cursor, error)
|
||||||
// QueryContext executes a query with context and returns the cursor
|
// QueryContext executes a query with context and returns the cursor
|
||||||
|
|
@ -183,22 +187,27 @@ func Use(driverName string, sqlDB *sql.DB) Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d database) GetDB() *sql.DB {
|
func (d *database) GetDB() *sql.DB {
|
||||||
return d.db
|
return d.db
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d database) getTxOrDB() txOrDB {
|
func (d *database) getTxOrDB(ctx context.Context) txOrDB {
|
||||||
if d.tx != nil {
|
if d.tx != nil {
|
||||||
return d.tx
|
return d.tx
|
||||||
}
|
}
|
||||||
|
if ctx != nil {
|
||||||
|
if tx, ok := ctx.Value(txContextKey{}).(Transaction); ok {
|
||||||
|
return tx.GetTx()
|
||||||
|
}
|
||||||
|
}
|
||||||
return d.db
|
return d.db
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d database) Query(sqlString string) (Cursor, error) {
|
func (d *database) Query(sqlString string) (Cursor, error) {
|
||||||
return d.QueryContext(context.Background(), sqlString)
|
return d.QueryContext(context.Background(), sqlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d database) QueryContext(ctx context.Context, sqlString string) (Cursor, error) {
|
func (d *database) QueryContext(ctx context.Context, sqlString string) (Cursor, error) {
|
||||||
isRetry := false
|
isRetry := false
|
||||||
for {
|
for {
|
||||||
sqlStringWithCallerInfo := getCallerInfo(d, isRetry) + sqlString
|
sqlStringWithCallerInfo := getCallerInfo(d, isRetry) + sqlString
|
||||||
|
|
@ -214,7 +223,7 @@ func (d database) QueryContext(ctx context.Context, sqlString string) (Cursor, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d database) queryContextOnce(ctx context.Context, sqlString string, retry bool) (*sql.Rows, error) {
|
func (d *database) queryContextOnce(ctx context.Context, sqlString string, retry bool) (*sql.Rows, error) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +238,7 @@ func (d database) queryContextOnce(ctx context.Context, sqlString string, retry
|
||||||
interceptor := d.interceptor
|
interceptor := d.interceptor
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
invoker := func(ctx context.Context, sql string) (err error) {
|
invoker := func(ctx context.Context, sql string) (err error) {
|
||||||
rows, err = d.getTxOrDB().QueryContext(ctx, sql)
|
rows, err = d.getTxOrDB(ctx).QueryContext(ctx, sql)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,12 +255,12 @@ func (d database) queryContextOnce(ctx context.Context, sqlString string, retry
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d database) Execute(sqlString string) (sql.Result, error) {
|
func (d *database) Execute(sqlString string) (sql.Result, error) {
|
||||||
return d.ExecuteContext(context.Background(), sqlString)
|
return d.ExecuteContext(context.Background(), sqlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteContext todo Is there need retry?
|
// ExecuteContext todo Is there need retry?
|
||||||
func (d database) ExecuteContext(ctx context.Context, sqlString string) (sql.Result, error) {
|
func (d *database) ExecuteContext(ctx context.Context, sqlString string) (sql.Result, error) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
@ -266,7 +275,7 @@ func (d database) ExecuteContext(ctx context.Context, sqlString string) (sql.Res
|
||||||
|
|
||||||
var result sql.Result
|
var result sql.Result
|
||||||
invoker := func(ctx context.Context, sql string) (err error) {
|
invoker := func(ctx context.Context, sql string) (err error) {
|
||||||
result, err = d.getTxOrDB().ExecContext(ctx, sql)
|
result, err = d.getTxOrDB(ctx).ExecContext(ctx, sql)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ type NumberExpression interface {
|
||||||
// StringExpression is the interface of an SQL expression with string value.
|
// StringExpression is the interface of an SQL expression with string value.
|
||||||
type StringExpression interface {
|
type StringExpression interface {
|
||||||
Expression
|
Expression
|
||||||
|
JsonExpression
|
||||||
Min() UnknownExpression
|
Min() UnknownExpression
|
||||||
Max() UnknownExpression
|
Max() UnknownExpression
|
||||||
Like(other interface{}) BooleanExpression
|
Like(other interface{}) BooleanExpression
|
||||||
|
|
@ -134,6 +135,17 @@ type UnknownExpression interface {
|
||||||
Left(count interface{}) StringExpression
|
Left(count interface{}) StringExpression
|
||||||
Right(count interface{}) StringExpression
|
Right(count interface{}) StringExpression
|
||||||
Trim() StringExpression
|
Trim() StringExpression
|
||||||
|
|
||||||
|
JsonType() StringExpression
|
||||||
|
JsonValid() BooleanExpression
|
||||||
|
JsonDepth() NumberExpression
|
||||||
|
JsonLength() NumberExpression
|
||||||
|
JsonExtract(paths ...string) StringExpression
|
||||||
|
JsonUnquote() UnknownExpression
|
||||||
|
JsonMergePatch(others ...interface{}) StringExpression
|
||||||
|
JsonMergePreserve(others ...interface{}) StringExpression
|
||||||
|
JsonContainsPathOne(paths ...string) BooleanExpression
|
||||||
|
JsonContainsPathAll(paths ...string) BooleanExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
type expression struct {
|
type expression struct {
|
||||||
|
|
@ -568,7 +580,27 @@ func (e expression) Concat(other interface{}) StringExpression {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e expression) Contains(substring string) BooleanExpression {
|
func (e expression) Contains(substring string) BooleanExpression {
|
||||||
return function("LOCATE", substring, e).GreaterThan(0)
|
return expression{builder: func(scope scope) (string, error) {
|
||||||
|
exprSQL, err := e.GetSQL(scope)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
substringSQL := quoteString(substring)
|
||||||
|
|
||||||
|
dialect := dialectUnknown
|
||||||
|
if scope.Database != nil {
|
||||||
|
dialect = scope.Database.dialect
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dialect {
|
||||||
|
case dialectPostgres:
|
||||||
|
return "STRPOS(" + exprSQL + ", " + substringSQL + ") > 0", nil
|
||||||
|
case dialectSqlite3:
|
||||||
|
return "INSTR(" + exprSQL + ", " + substringSQL + ") > 0", nil
|
||||||
|
default:
|
||||||
|
return "LOCATE(" + substringSQL + ", " + exprSQL + ") > 0", nil
|
||||||
|
}
|
||||||
|
}, priority: 11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e expression) binaryOperation(operator string, value interface{}, priority priority, isBool bool) expression {
|
func (e expression) binaryOperation(operator string, value interface{}, priority priority, isBool bool) expression {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package sqlingo
|
||||||
|
|
||||||
|
type JsonExpression interface {
|
||||||
|
JsonType() StringExpression
|
||||||
|
JsonValid() BooleanExpression
|
||||||
|
JsonDepth() NumberExpression
|
||||||
|
JsonLength() NumberExpression
|
||||||
|
JsonExtract(paths ...string) StringExpression
|
||||||
|
JsonUnquote() UnknownExpression
|
||||||
|
JsonMergePatch(others ...interface{}) StringExpression
|
||||||
|
JsonMergePreserve(others ...interface{}) StringExpression
|
||||||
|
JsonContainsPathOne(paths ...string) BooleanExpression
|
||||||
|
JsonContainsPathAll(paths ...string) BooleanExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonType() StringExpression {
|
||||||
|
return function("JSON_TYPE", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonValid() BooleanExpression {
|
||||||
|
return function("JSON_VALID", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonDepth() NumberExpression {
|
||||||
|
return function("JSON_DEPTH", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonLength() NumberExpression {
|
||||||
|
return function("JSON_LENGTH", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonExtract(paths ...string) StringExpression {
|
||||||
|
args := make([]interface{}, 0, 1+len(paths))
|
||||||
|
args = append(args, e)
|
||||||
|
for _, path := range paths {
|
||||||
|
args = append(args, path)
|
||||||
|
}
|
||||||
|
return function("JSON_EXTRACT", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonUnquote() UnknownExpression {
|
||||||
|
return function("JSON_UNQUOTE", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonMergePatch(others ...interface{}) StringExpression {
|
||||||
|
args := make([]interface{}, 0, 1+len(others))
|
||||||
|
args = append(args, e)
|
||||||
|
args = append(args, others...)
|
||||||
|
return function("JSON_MERGE_PATCH", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonMergePreserve(others ...interface{}) StringExpression {
|
||||||
|
args := make([]interface{}, 0, 1+len(others))
|
||||||
|
args = append(args, e)
|
||||||
|
args = append(args, others...)
|
||||||
|
return function("JSON_MERGE_PRESERVE", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonContainsPathOne(paths ...string) BooleanExpression {
|
||||||
|
args := make([]interface{}, 0, 2+len(paths))
|
||||||
|
args = append(args, e)
|
||||||
|
args = append(args, "one")
|
||||||
|
for _, path := range paths {
|
||||||
|
args = append(args, path)
|
||||||
|
}
|
||||||
|
return function("JSON_CONTAINS_PATH", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e expression) JsonContainsPathAll(paths ...string) BooleanExpression {
|
||||||
|
args := make([]interface{}, 0, 2+len(paths))
|
||||||
|
args = append(args, e)
|
||||||
|
args = append(args, "all")
|
||||||
|
for _, path := range paths {
|
||||||
|
args = append(args, path)
|
||||||
|
}
|
||||||
|
return function("JSON_CONTAINS_PATH", args...)
|
||||||
|
}
|
||||||
6
table.go
6
table.go
|
|
@ -24,7 +24,11 @@ func (t table) GetName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t table) GetSQL(scope scope) string {
|
func (t table) GetSQL(scope scope) string {
|
||||||
return t.sqlDialects[scope.Database.dialect]
|
dialect := dialectUnknown
|
||||||
|
if scope.Database != nil {
|
||||||
|
dialect = scope.Database.dialect
|
||||||
|
}
|
||||||
|
return t.sqlDialects[dialect]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t table) getOperatorPriority() int {
|
func (t table) getOperatorPriority() int {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,18 @@ type Transaction interface {
|
||||||
DeleteFrom(table Table) deleteWithTable
|
DeleteFrom(table Table) deleteWithTable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type txContextKey struct{}
|
||||||
|
|
||||||
|
// WithTransaction stores the transaction in the context.
|
||||||
|
func WithTransaction(ctx context.Context, tx Transaction) context.Context {
|
||||||
|
return context.WithValue(ctx, txContextKey{}, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutTransaction returns a context without the transaction.
|
||||||
|
func WithoutTransaction(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, txContextKey{}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *database) GetTx() *sql.Tx {
|
func (d *database) GetTx() *sql.Tx {
|
||||||
return d.tx
|
return d.tx
|
||||||
}
|
}
|
||||||
|
|
@ -56,3 +68,21 @@ func (d *database) BeginTx(ctx context.Context, opts *sql.TxOptions, f func(tx T
|
||||||
isCommitted = true
|
isCommitted = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureTx ensures the function f runs within a transaction.
|
||||||
|
// If ctx already contains a transaction started by a previous EnsureTx call, it reuses that transaction.
|
||||||
|
// Otherwise, it begins a new transaction and stores it in the context.
|
||||||
|
func (d *database) EnsureTx(ctx context.Context, opts *sql.TxOptions, f func(ctx context.Context) error) error {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if _, ok := ctx.Value(txContextKey{}).(Transaction); ok {
|
||||||
|
return f(ctx)
|
||||||
|
}
|
||||||
|
if d.tx != nil {
|
||||||
|
return f(WithTransaction(ctx, d))
|
||||||
|
}
|
||||||
|
return d.BeginTx(ctx, opts, func(tx Transaction) error {
|
||||||
|
return f(WithTransaction(ctx, tx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,3 +81,152 @@ func TestTransaction(t *testing.T) {
|
||||||
t.Error("should get error here")
|
t.Error("should get error here")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWithTransaction(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// ctx without transaction
|
||||||
|
if _, ok := ctx.Value(txContextKey{}).(Transaction); ok {
|
||||||
|
t.Error("should not have transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := newMockDatabase()
|
||||||
|
err := db.BeginTx(ctx, nil, func(tx Transaction) error {
|
||||||
|
txCtx := WithTransaction(ctx, tx)
|
||||||
|
got, ok := txCtx.Value(txContextKey{}).(Transaction)
|
||||||
|
if !ok {
|
||||||
|
t.Error("should have transaction")
|
||||||
|
}
|
||||||
|
if got.GetTx() != tx.GetTx() {
|
||||||
|
t.Error("should be the same tx")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithoutTransaction(t *testing.T) {
|
||||||
|
db := newMockDatabase()
|
||||||
|
err := db.BeginTx(context.Background(), nil, func(tx Transaction) error {
|
||||||
|
txCtx := WithTransaction(context.Background(), tx)
|
||||||
|
cleanCtx := WithoutTransaction(txCtx)
|
||||||
|
if _, ok := cleanCtx.Value(txContextKey{}).(Transaction); ok {
|
||||||
|
t.Error("should not have transaction after WithoutTransaction")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureTx(t *testing.T) {
|
||||||
|
db := newMockDatabase()
|
||||||
|
sharedMockConn.mockTx = nil
|
||||||
|
|
||||||
|
// EnsureTx should create a new transaction
|
||||||
|
err := db.EnsureTx(context.Background(), nil, func(ctx context.Context) error {
|
||||||
|
tx, ok := ctx.Value(txContextKey{}).(Transaction)
|
||||||
|
if !ok || tx.GetTx() == nil {
|
||||||
|
t.Error("should have transaction in ctx")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if !sharedMockConn.mockTx.isCommitted {
|
||||||
|
t.Error("should be committed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureTx with nil ctx
|
||||||
|
err = db.EnsureTx(nil, nil, func(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureTx should rollback on error
|
||||||
|
err = db.EnsureTx(context.Background(), nil, func(ctx context.Context) error {
|
||||||
|
return errors.New("error")
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should get error here")
|
||||||
|
}
|
||||||
|
if !sharedMockConn.mockTx.isRolledBack {
|
||||||
|
t.Error("should be rolled back")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureTx should reuse transaction from ctx
|
||||||
|
sharedMockConn.mockTx = nil
|
||||||
|
err = db.EnsureTx(context.Background(), nil, func(ctx context.Context) error {
|
||||||
|
outerTx := ctx.Value(txContextKey{}).(Transaction)
|
||||||
|
|
||||||
|
// nested EnsureTx should reuse the same tx
|
||||||
|
return db.EnsureTx(ctx, nil, func(innerCtx context.Context) error {
|
||||||
|
innerTx := innerCtx.Value(txContextKey{}).(Transaction)
|
||||||
|
if innerTx.GetTx() != outerTx.GetTx() {
|
||||||
|
t.Error("nested EnsureTx should reuse the same transaction")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureTx should reuse transaction from BeginTx
|
||||||
|
err = db.BeginTx(context.Background(), nil, func(tx Transaction) error {
|
||||||
|
txDb := tx.(*database)
|
||||||
|
return txDb.EnsureTx(context.Background(), nil, func(ctx context.Context) error {
|
||||||
|
ctxTx, ok := ctx.Value(txContextKey{}).(Transaction)
|
||||||
|
if !ok {
|
||||||
|
t.Error("should have transaction in ctx")
|
||||||
|
}
|
||||||
|
if ctxTx.GetTx() != tx.GetTx() {
|
||||||
|
t.Error("EnsureTx inside BeginTx should reuse the same transaction")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureTx should fail if BeginTx fails
|
||||||
|
sharedMockConn.beginTxError = errors.New("error")
|
||||||
|
err = db.EnsureTx(context.Background(), nil, func(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should get error here")
|
||||||
|
}
|
||||||
|
sharedMockConn.beginTxError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTxOrDBWithContext(t *testing.T) {
|
||||||
|
db := newMockDatabase()
|
||||||
|
|
||||||
|
// without tx in ctx, should return db
|
||||||
|
d := db.(*database)
|
||||||
|
result := d.getTxOrDB(context.Background())
|
||||||
|
if result != d.db {
|
||||||
|
t.Error("should return db when no tx in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// with tx in ctx, should return tx
|
||||||
|
err := db.BeginTx(context.Background(), nil, func(tx Transaction) error {
|
||||||
|
txCtx := WithTransaction(context.Background(), tx)
|
||||||
|
result := d.getTxOrDB(txCtx)
|
||||||
|
if result != tx.GetTx() {
|
||||||
|
t.Error("should return tx from context")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue