add dialect for different database

This commit is contained in:
kk
2024-10-09 16:32:27 +08:00
parent dcf5235d6c
commit 81c9b744df
24 changed files with 1078 additions and 740 deletions

71
assets/gorose.drawio Normal file
View File

@@ -0,0 +1,71 @@
<mxfile host="65bd71144e">
<diagram id="nHGE5eSV5kb8BZl-27XF" name="第 1 页">
<mxGraphModel dx="965" dy="767" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="2336" background="none" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="22" value="" style="edgeStyle=none;html=1;exitX=0.386;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="2" target="11">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="23" style="edgeStyle=none;html=1;exitX=0.909;exitY=-0.075;exitDx=0;exitDy=0;entryX=0.704;entryY=1.013;entryDx=0;entryDy=0;entryPerimeter=0;exitPerimeter=0;" edge="1" parent="1" source="2" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="2" value="Context&lt;br&gt;Select,Table,Join,Where,GroupBy,Having,OrderBy,LimitOffset" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="580" y="460" width="350" height="40" as="geometry"/>
</mxCell>
<mxCell id="33" style="edgeStyle=none;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="7" target="10">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1020" y="300" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="36" style="edgeStyle=none;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="7">
<mxGeometry relative="1" as="geometry">
<mxPoint x="980" y="360" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="7" value="Engin&lt;br&gt;Query,Exec,Transaction,Log" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="940" y="390" width="160" height="38" as="geometry"/>
</mxCell>
<mxCell id="24" value="" style="edgeStyle=none;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" target="7">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1020" y="460" as="sourcePoint"/>
<mxPoint x="1040" y="400" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="34" style="edgeStyle=none;html=1;entryX=0.844;entryY=1.113;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.643;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="8" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="8" value="database/sql" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="940" y="460" width="280" height="40" as="geometry"/>
</mxCell>
<mxCell id="35" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.35;entryY=0.988;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="9" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="9" value="Database ORM API" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="580" y="320" width="450" height="40" as="geometry"/>
</mxCell>
<mxCell id="10" value="GoRose V3" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="580" y="256" width="640" height="40" as="geometry"/>
</mxCell>
<mxCell id="18" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.3;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="11" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="Driver Dialect&lt;br&gt;Mysql,MsSQL,Postgresql,Oracle,Sqlite3..." style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="580" y="388" width="270" height="40" as="geometry"/>
</mxCell>
<mxCell id="31" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.938;entryY=1.013;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="12" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="12" value="Config" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1140" y="320" width="80" height="40" as="geometry"/>
</mxCell>
<mxCell id="30" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="28" target="12">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="28" value="Master,Slave" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="1140" y="390" width="80" height="40" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

1
assets/gorose.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -27,8 +27,8 @@ type TypeToSqlUpdateCase struct {
}
type TypeToSqlIncDecCase struct {
Symbol string
Data map[string]any
Symbol string // +/-
Data map[string]any // {count: 2} => count = count + 2
}
type TypeToSqlInsertCase struct {
@@ -38,3 +38,11 @@ type TypeToSqlInsertCase struct {
UpdateFields []string
MustColumn []string
}
type TypeLock int8
const (
TypeLockUnknown TypeLock = iota
TypeLockInShareMode
TypeLockForUpdate
)

View File

@@ -11,7 +11,7 @@ type Context struct {
LimitOffsetClause LimitOffsetClause
UnionClause UnionClause
PessimisticLocking string
PessimisticLocking TypeLock
Prefix string
}
@@ -137,10 +137,10 @@ func (db *Context) Page(num int) *Context {
return db
}
func (db *Context) SharedLock() *Context {
db.PessimisticLocking = "LOCK IN SHARE MODE"
db.PessimisticLocking = TypeLockInShareMode
return db
}
func (db *Context) LockForUpdate() *Context {
db.PessimisticLocking = "FOR UPDATE"
db.PessimisticLocking = TypeLockForUpdate
return db
}

View File

@@ -1,17 +1,6 @@
package builder
import (
"reflect"
"strings"
)
func IsExpression(obj any) (b bool) {
rfv := reflect.Indirect(reflect.ValueOf(obj))
if rfv.Kind() == reflect.String && strings.Contains(rfv.String(), "?") {
b = true
}
return
}
import "reflect"
func ToSlice(arg any) []any {
ref := reflect.Indirect(reflect.ValueOf(arg))

View File

@@ -162,13 +162,13 @@ func (w *WhereClause) whereRaw(boolean string, sqlSeg string, bindingsAndBoolean
// Examples:
//
// Where("id=1")
// Where("id=?",1)
// Where("id=?",1), 不支持
// Where("id",1)
// Where("id","=",1)
// Where("id","=",1,"AND")
// Where("id","=",(select id from table limit 1))
// Where("id","in",(select id from table), "AND")
// Where(func(wh iface.WhereClause){wh.Where().OrWhere().WhereRaw()...})
// Where(func(wh builder.WhereClause){wh.Where().OrWhere().WhereRaw()...})
// Where(["id=1"])
// Where(["id","=",1])
// Where(["id",1])
@@ -226,9 +226,9 @@ func (w *WhereClause) where(boolean string, column any, args ...any) IWhere {
w.Err = errors.New("not supported where params")
}
case 1:
if IsExpression(column) {
return w.whereRaw(boolean, column.(string), args...)
}
//if driver.IsExpression(column) {
// return w.whereRaw(boolean, column.(string), args...)
//}
return w.where(boolean, column, "=", args[0], boolean)
case 2:
return w.where(boolean, column, args[0], args[1], boolean)

View File

@@ -11,13 +11,13 @@ import (
type Database struct {
*Engin
Driver driver.IDriver
Driver *driver.Driver
Context *builder.Context
}
func NewDatabase(g *GoRose) *Database {
return &Database{
Driver: driver.GetDriver(g.driver),
Driver: driver.NewDriver(g.driver),
Engin: NewEngin(g),
Context: builder.NewContext(g.prefix),
}
@@ -139,31 +139,31 @@ func (db *Database) OrderByRaw(column string) *Database {
// Limit 设置查询结果的限制数量。
func (db *Database) Limit(limit int) *Database {
db.Context.LimitOffsetClause.Limit = limit
db.Context.Limit(limit)
return db
}
// Offset 设置查询结果的偏移量。
func (db *Database) Offset(offset int) *Database {
db.Context.LimitOffsetClause.Offset = offset
db.Context.Offset(offset)
return db
}
// Page 页数,根据limit确定
func (db *Database) Page(num int) *Database {
db.Context.LimitOffsetClause.Page = num
db.Context.Page(num)
return db
}
// SharedLock 4 select ... locking in share mode
func (db *Database) SharedLock() *Database {
db.Context.PessimisticLocking = "LOCK IN SHARE MODE"
db.Context.SharedLock()
return db
}
// LockForUpdate 4 select ... for update
func (db *Database) LockForUpdate() *Database {
db.Context.PessimisticLocking = "FOR UPDATE"
db.Context.LockForUpdate()
return db
}

40
driver/dialect/dialect.go Normal file
View File

@@ -0,0 +1,40 @@
package dialect
import "sync"
type IDialect interface {
New() IDialect
Placeholder() string // 占位符处理,例如 MySQL 使用 `?`, PostgreSQL 使用 `$1`
AutoIncrement() string // 自增字段的声明方式
LimitOffset(limit, offset int) string // 分页查询的 SQL 片段
//InsertQuery(table string, columns []string, values [][]interface{}) (string, []interface{}) // 批量插入 SQL 生成
QuoteIdentifier(identifier string) string // 对标识符(字段名、表名)加引号
Upsert() string
LockInShareMode() string
LockForUpdate() string
}
var dialectMap = map[string]IDialect{}
var dialectLock sync.RWMutex
func Register(driver string, parser IDialect) {
dialectLock.Lock()
defer dialectLock.Unlock()
dialectMap[driver] = parser
}
func GetDialect(driver string) IDialect {
dialectLock.RLock()
defer dialectLock.RUnlock()
return dialectMap[driver]
}
func DialectList() (dr []string) {
dialectLock.RLock()
defer dialectLock.RUnlock()
for d := range dialectMap {
dr = append(dr, d)
}
return
}

46
driver/dialect/mssql.go Normal file
View File

@@ -0,0 +1,46 @@
package dialect
import "fmt"
type MsSQLDialect struct {
placeHolderIndex int
}
func init() {
Register("mssql", &MsSQLDialect{})
}
func (d *MsSQLDialect) New() IDialect {
return &MsSQLDialect{}
}
func (d *MsSQLDialect) Placeholder() string {
//return fmt.Sprintf("$%d", idx)
d.placeHolderIndex += 1
return fmt.Sprintf("@p%d", d.placeHolderIndex)
}
func (d *MsSQLDialect) AutoIncrement() string {
return "IDENTITY(1,1)"
}
func (d *MsSQLDialect) LimitOffset(limit, offset int) string {
return fmt.Sprintf("OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", offset, limit)
}
func (d *MsSQLDialect) QuoteIdentifier(identifier string) string {
if identifier == "" || identifier == "*" {
return identifier
}
return fmt.Sprintf("[%s]", identifier)
}
func (d *MsSQLDialect) Upsert() string {
return "MERGE INTO"
}
func (d *MsSQLDialect) LockInShareMode() string { return "" }
//func (d *MsSQLDialect) LockInShareMode() string { return "WITH (HOLDLOCK)" }
func (d *MsSQLDialect) LockForUpdate() string { return "WITH (ROWLOCK)" }

40
driver/dialect/mysql.go Normal file
View File

@@ -0,0 +1,40 @@
package dialect
import "fmt"
type MySQLDialect struct{}
func init() {
Register("mysql", &MySQLDialect{})
}
func (d *MySQLDialect) New() IDialect {
return &MySQLDialect{}
}
func (d *MySQLDialect) Placeholder() string {
return "?"
}
func (d *MySQLDialect) AutoIncrement() string {
return "AUTO_INCREMENT"
}
func (d *MySQLDialect) LimitOffset(limit, offset int) string {
if offset == 0 {
return fmt.Sprintf("LIMIT %d", limit)
}
return fmt.Sprintf("LIMIT %d OFFSET %d", limit, offset)
}
func (d *MySQLDialect) QuoteIdentifier(identifier string) string {
if identifier == "" || identifier == "*" {
return identifier
}
return fmt.Sprintf("`%s`", identifier)
}
func (d *MySQLDialect) Upsert() string { return "ON DUPLICATE KEY UPDATE" }
func (d *MySQLDialect) LockInShareMode() string { return "LOCK IN SHARE MODE" }
func (d *MySQLDialect) LockForUpdate() string { return "FOR UPDATE" }

43
driver/dialect/oracle.go Normal file
View File

@@ -0,0 +1,43 @@
package dialect
import "fmt"
type OracleDialect struct {
placeHolderIndex int
}
func init() {
Register("oracle", &OracleDialect{})
}
func (d *OracleDialect) New() IDialect {
return &OracleDialect{}
}
func (d *OracleDialect) Placeholder() string {
//return fmt.Sprintf("$%d", idx)
d.placeHolderIndex += 1
return fmt.Sprintf("@p%d", d.placeHolderIndex)
}
func (d *OracleDialect) AutoIncrement() string {
return "" // 需要使用序列
}
func (d *OracleDialect) LimitOffset(limit, offset int) string {
return fmt.Sprintf("OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", offset, limit)
}
func (d *OracleDialect) QuoteIdentifier(identifier string) string {
if identifier == "" || identifier == "*" {
return identifier
}
return fmt.Sprintf("\"%s\"", identifier)
}
func (d *OracleDialect) Upsert() string {
return "MERGE INTO"
}
func (d *OracleDialect) LockInShareMode() string { return "" }
func (d *OracleDialect) LockForUpdate() string { return "FOR UPDATE" }

View File

@@ -0,0 +1,76 @@
package dialect
import (
"fmt"
)
type PostgresqlDialect struct {
placeHolderIndex int
}
func init() {
Register("postgresql", &PostgresqlDialect{})
}
func (d *PostgresqlDialect) New() IDialect {
return &PostgresqlDialect{}
}
func (d *PostgresqlDialect) Placeholder() string {
//return fmt.Sprintf("$%d", idx)
d.placeHolderIndex += 1
return fmt.Sprintf("$%d", d.placeHolderIndex)
}
//func (d *PostgresqlDialect) IsExpression(obj any) (b bool) {
// rfv := reflect.Indirect(reflect.ValueOf(obj))
// if rfv.Kind() == reflect.String && strings.Contains(rfv.String(), "$") {
// b = true
// }
// return
//}
//func (d *PostgresqlDialect) PlaceholderMulti(total int) string {
// var arr = make([]string, 0, total)
// for i := 0; i < total; i++ {
// arr = append(arr, fmt.Sprintf("$%d", i+1))
// }
// return strings.Join(arr, ",")
//}
//
//func (d *PostgresqlDialect) PlaceholderMultiInsert(cols, rows int) string {
// placeholders := make([]string, 0, rows)
//
// for i := 0; i < rows; i++ {
// ph := make([]string, cols)
// for j := 0; j < cols; j++ {
// ph[j] = fmt.Sprintf("$%d", i*cols+j+1)
// }
// placeholders = append(placeholders, "("+strings.Join(ph, ",")+")")
// }
// return strings.Join(placeholders, ",")
//}
func (d *PostgresqlDialect) AutoIncrement() string {
return "SERIAL"
}
func (d *PostgresqlDialect) LimitOffset(limit, offset int) string {
if offset == 0 {
return fmt.Sprintf("LIMIT %d", limit)
}
return fmt.Sprintf("LIMIT %d OFFSET %d", limit, offset)
}
func (d *PostgresqlDialect) QuoteIdentifier(identifier string) string {
if identifier == "" || identifier == "*" {
return identifier
}
return fmt.Sprintf("\"%s\"", identifier)
}
func (d *PostgresqlDialect) Upsert() string {
return "ON CONFLICT DO UPDATE"
}
func (d *PostgresqlDialect) LockInShareMode() string { return "FOR SHARE" }
func (d *PostgresqlDialect) LockForUpdate() string { return "FOR UPDATE" }

42
driver/dialect/sqlite3.go Normal file
View File

@@ -0,0 +1,42 @@
package dialect
import "fmt"
type SQLite3Dialect struct{}
func init() {
Register("sqlite3", &SQLite3Dialect{})
}
func (d *SQLite3Dialect) New() IDialect {
return &SQLite3Dialect{}
}
func (d *SQLite3Dialect) Placeholder() string {
return "?"
}
func (d *SQLite3Dialect) AutoIncrement() string {
return "AUTOINCREMENT"
}
func (d *SQLite3Dialect) LimitOffset(limit, offset int) string {
if offset == 0 {
return fmt.Sprintf("LIMIT %d", limit)
}
return fmt.Sprintf("LIMIT %d OFFSET %d", limit, offset)
}
func (d *SQLite3Dialect) QuoteIdentifier(identifier string) string {
if identifier == "" || identifier == "*" {
return identifier
}
return fmt.Sprintf("\"%s\"", identifier)
}
func (d *SQLite3Dialect) Upsert() string {
return "INSERT OR REPLACE"
}
func (d *SQLite3Dialect) LockInShareMode() string { return "" }
func (d *SQLite3Dialect) LockForUpdate() string { return "" }

View File

@@ -1,8 +1,16 @@
package driver
import (
"errors"
"fmt"
"github.com/gohouse/gorose/v3/builder"
"sync"
"github.com/gohouse/gorose/v3/driver/dialect"
"github.com/gohouse/gorose/v3/parser"
"regexp"
"sort"
"reflect"
"strings"
)
type IDriver interface {
@@ -19,26 +27,544 @@ type IDriver interface {
ToSqlDelete(c *builder.Context, obj any, mustColumn ...string) (sqlSegment string, binds []any, err error)
}
var driverMap = map[string]IDriver{}
var driverLock sync.RWMutex
func Register(driver string, parser IDriver) {
driverLock.Lock()
defer driverLock.Unlock()
driverMap[driver] = parser
type Driver struct {
//driver string
Dialect dialect.IDialect
}
func GetDriver(driver string) IDriver {
driverLock.RLock()
defer driverLock.RUnlock()
return driverMap[driver]
func NewDriver(driver string) *Driver {
return &Driver{Dialect: dialect.GetDialect(driver).New()}
}
func DriverList() (dr []string) {
driverLock.RLock()
defer driverLock.RUnlock()
for d := range driverMap {
dr = append(dr, d)
func (d Driver) ToSql(c *builder.Context) (sql4prepare string, binds []any, err error) {
sql4prepare, binds, err = d.toSql(c)
if len(c.UnionClause.Unions) > 0 {
for _, u := range c.UnionClause.Unions {
sql4prepare2, binds2, err2 := u.ToSql()
if err2 != nil {
return
}
if sql4prepare2 == "" {
continue
}
var unions = "UNION"
if u.IsUnionAll {
unions = "UNION ALL"
}
sql4prepare = fmt.Sprintf("%s %s %s", sql4prepare, unions, sql4prepare2)
binds = append(binds, binds2...)
}
}
return
}
func (d Driver) toSql(c *builder.Context) (sql4prepare string, binds []any, err error) {
selects, anies := d.ToSqlSelect(c)
table, binds2, err := d.ToSqlTable(c)
if err != nil {
return sql4prepare, binds2, err
}
joins, binds3, err := d.ToSqlJoin(c)
if err != nil {
return sql4prepare, binds3, err
}
wheres, binds4, err := d.ToSqlWhere(c)
if err != nil {
return sql4prepare, binds4, err
}
orderBy := d.ToSqlOrderBy(c)
limit, binds5 := d.ToSqlLimitOffset(c)
groupBys := d.ToSqlGroupBy(c)
havings, binds6, err := d.ToSqlHaving(c)
binds = append(binds, anies...)
binds = append(binds, binds2...)
binds = append(binds, binds3...)
binds = append(binds, binds4...)
binds = append(binds, binds5...)
binds = append(binds, binds6...)
var locking string
if c.PessimisticLocking == builder.TypeLockInShareMode {
locking = d.Dialect.LockInShareMode()
} else if c.PessimisticLocking == builder.TypeLockForUpdate {
locking = d.Dialect.LockForUpdate()
}
//sql4prepare = NamedSprintf("SELECT :selects FROM :table :join :wheres :groupBys :havings :orderBy :pagination :PessimisticLocking", selects, table, joins, wheres, groupBys, havings, orderBy, limit, c.PessimisticLocking)
sql4prepare = fmt.Sprintf("SELECT %s FROM %s %s %s %s %s %s %s %s", selects, table, joins, wheres, groupBys, havings, orderBy, limit, locking)
sql4prepare = regexp.MustCompile(`\s{2,}`).ReplaceAllString(strings.TrimSpace(sql4prepare), " ")
return
}
func (d Driver) ToSqlSelect(c *builder.Context) (sql4prepare string, binds []any) {
var cols []string
for _, col := range c.SelectClause.Columns {
if col.IsRaw {
cols = append(cols, col.Name)
binds = append(binds, col.Binds...)
} else {
if col.Alias == "" {
cols = append(cols, d.Dialect.QuoteIdentifier(col.Name))
} else {
cols = append(cols, fmt.Sprintf("%s AS %s", d.Dialect.QuoteIdentifier(col.Name), col.Alias))
}
}
}
if len(cols) == 0 {
cols = []string{"*"}
}
var distinct string
if c.SelectClause.Distinct {
distinct = "DISTINCT "
}
sql4prepare = fmt.Sprintf("%s%s", distinct, strings.Join(cols, ", "))
return
}
func (d Driver) ToSqlTable(c *builder.Context) (sql4prepare string, binds []any, err error) {
return d.buildSqlTable(c.TableClause, c.Prefix)
}
func (d Driver) buildSqlTable(tab builder.TableClause, prefix string) (sql4prepare string, binds []any, err error) {
if v, ok := tab.Tables.(builder.IBuilder); ok {
sql4prepare, binds, err = v.ToSql()
if tab.Alias != "" {
sql4prepare = fmt.Sprintf("(%s) %s", sql4prepare, d.Dialect.QuoteIdentifier(tab.Alias))
}
return
}
rfv := reflect.Indirect(reflect.ValueOf(tab.Tables))
switch rfv.Kind() {
case reflect.String:
sql4prepare = d.Dialect.QuoteIdentifier(fmt.Sprintf("%s%s", prefix, tab.Tables))
case reflect.Struct:
sql4prepare = d.buildTableName(rfv.Type(), prefix)
case reflect.Slice:
if rfv.Type().Elem().Kind() == reflect.Struct {
sql4prepare = d.buildTableName(rfv.Type().Elem(), prefix)
} else {
err = errors.New("table param must be string or struct(slice) bind with 1 or 2 params")
return
}
default:
err = errors.New("table must be string | struct | slice")
return
}
return strings.TrimSpace(fmt.Sprintf("%s %s", sql4prepare, d.Dialect.QuoteIdentifier(tab.Alias))), binds, err
}
func (d Driver) toSqlWhere(c *builder.Context, wc builder.WhereClause) (sql4prepare string, binds []any, err error) {
if len(wc.Conditions) == 0 {
return
}
var sql4prepareArr []string
for _, v := range wc.Conditions {
switch item := v.(type) {
case builder.TypeWhereRaw:
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s", item.LogicalOp, item.Column))
binds = append(binds, item.Bindings...)
case builder.TypeWhereStandard:
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s %s", item.LogicalOp, d.Dialect.QuoteIdentifier(item.Column), item.Operator, d.Dialect.Placeholder()))
binds = append(binds, item.Value)
case builder.TypeWhereIn:
values := ToSlice(item.Value)
var phs []string
for range values {
phs = append(phs, d.Dialect.Placeholder())
}
//sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, d.Dialect.QuoteIdentifier(item.Column), item.Operator, strings.Repeat("?,", len(values)-1)+"?"))
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, d.Dialect.QuoteIdentifier(item.Column), item.Operator, strings.Join(phs, ",")))
binds = append(binds, values...)
case builder.TypeWhereBetween:
values := ToSlice(item.Value)
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s %s AND %s", item.LogicalOp, d.Dialect.QuoteIdentifier(item.Column), d.Dialect.Placeholder(), item.Operator, d.Dialect.Placeholder()))
binds = append(binds, values...)
case builder.TypeWhereNested:
var tmp = builder.Context{}
item.WhereNested(&tmp.WhereClause)
prepare, anies, err := d.ToSqlWhere(&tmp)
if err != nil {
return sql4prepare, binds, err
}
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s (%s)", item.LogicalOp, strings.TrimPrefix(prepare, "WHERE ")))
binds = append(binds, anies...)
case builder.TypeWhereSubQuery:
query, anies, err := item.SubQuery.ToSql()
if err != nil {
return sql4prepare, binds, err
}
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, d.Dialect.QuoteIdentifier(item.Column), item.Operator, query))
binds = append(binds, anies...)
case builder.TypeWhereSubHandler:
var ctx = builder.NewContext(c.Prefix)
item.Sub(ctx)
query, anies, err := d.ToSql(ctx)
if err != nil {
return sql4prepare, binds, err
}
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, d.Dialect.QuoteIdentifier(item.Column), item.Operator, query))
binds = append(binds, anies...)
}
}
if len(sql4prepareArr) > 0 {
sql4prepare = strings.TrimSpace(strings.Trim(strings.Trim(strings.TrimSpace(strings.Join(sql4prepareArr, " ")), "AND"), "OR"))
}
return
}
func (d Driver) ToSqlWhere(c *builder.Context) (sql4prepare string, binds []any, err error) {
sql4prepare, binds, err = d.toSqlWhere(c, c.WhereClause)
if sql4prepare != "" {
if c.WhereClause.Not {
sql4prepare = fmt.Sprintf("WHERE NOT %s", sql4prepare)
}
sql4prepare = fmt.Sprintf("WHERE %s", sql4prepare)
}
return
}
func (d Driver) ToSqlJoin(c *builder.Context) (sql4prepare string, binds []any, err error) {
if c.JoinClause.Err != nil {
return sql4prepare, binds, c.JoinClause.Err
}
if len(c.JoinClause.JoinItems) == 0 {
return
}
for _, v := range c.JoinClause.JoinItems {
var prepare string
var sql4 string
var bind []any
switch item := v.(type) {
case builder.TypeJoinStandard:
prepare, bind, err = d.buildSqlTable(item.TableClause, c.Prefix)
if err != nil {
return
}
sql4 = fmt.Sprintf("%s %s ON %s %s %s", item.Type, prepare, d.Dialect.QuoteIdentifier(item.Column1), item.Operator, d.Dialect.QuoteIdentifier(item.Column2))
case builder.TypeJoinSub:
sql4, bind, err = item.ToSql()
if err != nil {
return
}
case builder.TypeJoinOn:
var tjo builder.TypeJoinOnCondition
item.OnClause(&tjo)
if len(tjo.Conditions) == 0 {
return
}
var sqlArr []string
for _, cond := range tjo.Conditions {
sqlArr = append(sqlArr, fmt.Sprintf("%s %s %s %s", cond.Relation, d.Dialect.QuoteIdentifier(cond.Column1), cond.Operator, d.Dialect.QuoteIdentifier(cond.Column2)))
}
sql4 = TrimPrefixAndOr(strings.Join(sqlArr, " "))
}
sql4prepare = fmt.Sprintf("%s %s", sql4prepare, sql4)
binds = append(binds, bind...)
}
return
}
func (d Driver) ToSqlGroupBy(c *builder.Context) (sql4prepare string) {
if len(c.GroupClause.Groups) > 0 {
var tmp []string
for _, col := range c.GroupClause.Groups {
if col.IsRaw {
tmp = append(tmp, col.Column)
} else {
tmp = append(tmp, d.Dialect.QuoteIdentifier(col.Column))
}
}
sql4prepare = fmt.Sprintf("GROUP BY %s", strings.Join(tmp, ","))
}
return
}
func (d Driver) ToSqlHaving(c *builder.Context) (sql4prepare string, binds []any, err error) {
sql4prepare, binds, err = d.toSqlWhere(c, c.HavingClause.WhereClause)
if sql4prepare != "" {
sql4prepare = fmt.Sprintf("HAVING %s", sql4prepare)
}
return
}
func (d Driver) ToSqlOrderBy(c *builder.Context) (sql4prepare string) {
if len(c.OrderByClause.Columns) == 0 {
return
}
var orderBys []string
for _, v := range c.OrderByClause.Columns {
if v.IsRaw {
orderBys = append(orderBys, v.Column)
} else {
if v.Direction == "" {
orderBys = append(orderBys, d.Dialect.QuoteIdentifier(v.Column))
} else {
orderBys = append(orderBys, fmt.Sprintf("%s %s", d.Dialect.QuoteIdentifier(v.Column), v.Direction))
}
}
}
sql4prepare = fmt.Sprintf("ORDER BY %s", strings.Join(orderBys, ", "))
return
}
func (d Driver) ToSqlLimitOffset(c *builder.Context) (sqlSegment string, binds []any) {
var offset int
if c.LimitOffsetClause.Offset > 0 {
offset = c.LimitOffsetClause.Offset
} else if c.LimitOffsetClause.Page > 0 {
offset = c.LimitOffsetClause.Limit * (c.LimitOffsetClause.Page - 1)
}
if c.LimitOffsetClause.Limit > 0 {
sqlSegment = d.Dialect.LimitOffset(c.LimitOffsetClause.Limit, offset)
//if offset > 0 {
// sqlSegment = "LIMIT ? OFFSET ?"
// binds = append(binds, c.LimitOffsetClause.Limit, offset)
//} else {
// sqlSegment = "LIMIT ?"
// binds = append(binds, c.LimitOffsetClause.Limit)
//}
}
return
}
// ToSqlInsert insert
func (d Driver) ToSqlInsert(c *builder.Context, obj any, args ...builder.TypeToSqlInsertCase) (sqlSegment string, binds []any, err error) {
var arg builder.TypeToSqlInsertCase
if len(args) > 0 {
arg = args[0]
}
var ctx = *c
rfv := reflect.Indirect(reflect.ValueOf(obj))
switch rfv.Kind() {
case reflect.Struct:
var datas []map[string]any
datas, err = parser.StructsToInsert(obj, arg.MustColumn...)
if err != nil {
return
}
ctx.TableClause.Table(obj)
return d.toSqlInsert(&ctx, datas, arg)
case reflect.Slice:
switch rfv.Type().Elem().Kind() {
case reflect.Struct:
c.TableClause.Table(obj)
var datas []map[string]any
datas, err = parser.StructsToInsert(obj, arg.MustColumn...)
if err != nil {
return
}
return d.toSqlInsert(c, datas, arg)
default:
return d.toSqlInsert(c, obj, arg)
}
default:
return d.toSqlInsert(c, obj, arg)
}
}
func (d Driver) ToSqlDelete(c *builder.Context, obj any, mustColumn ...string) (sqlSegment string, binds []any, err error) {
var ctx = *c
rfv := reflect.Indirect(reflect.ValueOf(obj))
switch rfv.Kind() {
case reflect.Struct:
data, err := parser.StructToDelete(obj, mustColumn...)
if err != nil {
return sqlSegment, binds, err
}
ctx.TableClause.Table(obj)
ctx.WhereClause.Where(data)
return d.toSqlDelete(&ctx)
case reflect.Int64, reflect.Int32, reflect.String:
ctx.WhereClause.Where("id", obj)
return d.toSqlDelete(&ctx)
default:
err = errors.New("obj must be struct or id value")
}
return
}
func (d Driver) ToSqlUpdate(c *builder.Context, arg any) (sqlSegment string, binds []any, err error) {
switch v := arg.(type) {
case builder.TypeToSqlUpdateCase:
return d.toSqlUpdate(c, v.BindOrData, v.MustColumn...)
case builder.TypeToSqlIncDecCase:
return d.toSqlIncDec(c, v.Symbol, v.Data)
default:
return
}
}
func (d Driver) toSqlUpdate(c *builder.Context, obj any, mustColumn ...string) (sqlSegment string, binds []any, err error) {
rfv := reflect.Indirect(reflect.ValueOf(obj))
switch rfv.Kind() {
case reflect.Struct:
dataMap, pk, pkValue, err := parser.StructToUpdate(obj, mustColumn...)
if err != nil {
return sqlSegment, binds, err
}
var ctx = *c
ctx.TableClause.Table(obj)
if pk != "" {
ctx.WhereClause.Where(pk, pkValue)
}
return d.toSqlUpdateReal(&ctx, dataMap)
case reflect.Map:
return d.toSqlUpdateReal(c, obj)
default:
err = errors.New("no support update obj")
return
}
}
func (d Driver) toSqlIncDec(c *builder.Context, symbol string, data map[string]any) (sql4prepare string, values []any, err error) {
prepare, anies, err := d.ToSqlTable(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, anies...)
var tmp []string
for k, v := range data {
tmp = append(tmp, fmt.Sprintf("%s=%s%s%s", d.Dialect.QuoteIdentifier(k), d.Dialect.QuoteIdentifier(k), symbol, d.Dialect.Placeholder()))
values = append(values, v)
}
where, val, err := d.ToSqlWhere(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, val...)
sql4prepare = fmt.Sprintf("UPDATE %s SET %s %s", prepare, strings.Join(tmp, ","), where)
return
}
func (d Driver) buildTableName(rft reflect.Type, prefix string) (tab string) {
return d.Dialect.QuoteIdentifier(fmt.Sprintf("%s%s", prefix, parser.StructsToTableName(rft)))
}
// func (b Driver) toSqlInsert(c *gorose.Context, data any, ignoreCase string, onDuplicateKeys []string) (sql4prepare string, values []any, err error) {
func (d Driver) toSqlInsert(c *builder.Context, data any, insertCase builder.TypeToSqlInsertCase) (sql4prepare string, values []any, err error) {
rfv := reflect.Indirect(reflect.ValueOf(data))
var fields []string
var valuesPlaceholderArr []string
switch rfv.Kind() {
case reflect.Map:
keys := rfv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
var valuesPlaceholderTmp []string
for _, key := range keys {
fields = append(fields, d.Dialect.QuoteIdentifier(key.String()))
valuesPlaceholderTmp = append(valuesPlaceholderTmp, d.Dialect.Placeholder())
values = append(values, rfv.MapIndex(key).Interface())
}
valuesPlaceholderArr = append(valuesPlaceholderArr, fmt.Sprintf("(%s)", strings.Join(valuesPlaceholderTmp, ",")))
case reflect.Slice:
if rfv.Len() == 0 {
return
}
if rfv.Type().Elem().Kind() == reflect.Map {
// 先获取到插入字段
keys := rfv.Index(0).MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
for _, key := range keys {
fields = append(fields, d.Dialect.QuoteIdentifier(key.String()))
}
// 组合插入数据
for i := 0; i < rfv.Len(); i++ {
var valuesPlaceholderTmp []string
for _, key := range keys {
valuesPlaceholderTmp = append(valuesPlaceholderTmp, d.Dialect.Placeholder())
values = append(values, rfv.Index(i).MapIndex(key).Interface())
}
valuesPlaceholderArr = append(valuesPlaceholderArr, fmt.Sprintf("(%s)", strings.Join(valuesPlaceholderTmp, ",")))
}
} else {
err = errors.New("only map(slice) data supported")
return
}
default:
err = errors.New("only map(slice) data supported")
return
}
if err != nil {
return
}
var onDuplicateKey string
if len(insertCase.UpdateFields) > 0 {
var tmp []string
for _, v := range insertCase.UpdateFields {
tmp = append(tmp, fmt.Sprintf("%s=VALUES(%s)", d.Dialect.QuoteIdentifier(v), d.Dialect.QuoteIdentifier(v)))
}
onDuplicateKey = fmt.Sprintf("%s %s", d.Dialect.Upsert(), strings.Join(tmp, ", "))
}
var insert = "INSERT"
if insertCase.IsReplace {
insert = "REPLACE"
} else if insertCase.IsIgnoreCase {
insert = "INSERT IGNORE"
}
var tables string
tables, _, err = d.ToSqlTable(c)
if err != nil {
return
}
//sql4prepare = NamedSprintf(":insert INTO :tables (:fields) VALUES :placeholder :onDuplicateKey", insert, tables, strings.Join(fields, ","), strings.Join(valuesPlaceholderArr, ","), onDuplicateKey)
sql4prepare = NamedSprintf("%s INTO %s (%s) VALUES %s %s", insert, tables, strings.Join(fields, ","), strings.Join(valuesPlaceholderArr, ","), onDuplicateKey)
return
}
func (d Driver) toSqlUpdateReal(c *builder.Context, data any) (sql4prepare string, values []any, err error) {
rfv := reflect.Indirect(reflect.ValueOf(data))
var updates []string
switch rfv.Kind() {
case reflect.Map:
keys := rfv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
for _, key := range keys {
updates = append(updates, fmt.Sprintf("%s = %s", d.Dialect.QuoteIdentifier(key.String()), d.Dialect.Placeholder()))
values = append(values, rfv.MapIndex(key).Interface())
}
default:
err = errors.New("only map data supported")
return
}
var tables string
tables, _, err = d.ToSqlTable(c)
if err != nil {
return
}
wheres, binds, err := d.ToSqlWhere(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, binds...)
//sql4prepare = NamedSprintf("UPDATE :tables SET :updates :wheres", tables, strings.Join(updates, ", "), wheres)
sql4prepare = fmt.Sprintf("UPDATE %s SET %s %s", tables, strings.Join(updates, ", "), wheres)
return
}
func (d Driver) toSqlDelete(c *builder.Context) (sql4prepare string, values []any, err error) {
var tables string
tables, _, err = d.ToSqlTable(c)
if err != nil {
return
}
wheres, binds, err := d.ToSqlWhere(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, binds...)
//sql4prepare = NamedSprintf("DELETE FROM :tables :wheres", tables, wheres)
sql4prepare = NamedSprintf("DELETE FROM %s %s", tables, wheres)
return
}

View File

@@ -1,409 +0,0 @@
package mysql
import (
"errors"
"fmt"
"github.com/gohouse/gorose/v3/builder"
"github.com/gohouse/gorose/v3/parser"
//_ "github.com/go-sql-driver/mysql"
"github.com/gohouse/gorose/v3/driver"
"reflect"
"strings"
)
const DriverName = "mysql"
type Builder struct {
//prefix string
}
func init() {
driver.Register(DriverName, &Builder{})
}
func (b Builder) ToSql(c *builder.Context) (sql4prepare string, binds []any, err error) {
sql4prepare, binds, err = b.toSql(c)
if len(c.UnionClause.Unions) > 0 {
for _, u := range c.UnionClause.Unions {
sql4prepare2, binds2, err2 := u.ToSql()
if err2 != nil {
return
}
if sql4prepare2 == "" {
continue
}
var unions = "UNION"
if u.IsUnionAll {
unions = "UNION ALL"
}
sql4prepare = fmt.Sprintf("%s %s %s", sql4prepare, unions, sql4prepare2)
binds = append(binds, binds2...)
}
}
return
}
func (b Builder) toSql(c *builder.Context) (sql4prepare string, binds []any, err error) {
selects, anies := b.ToSqlSelect(c)
table, binds2, err := b.ToSqlTable(c)
if err != nil {
return sql4prepare, binds2, err
}
joins, binds3, err := b.ToSqlJoin(c)
if err != nil {
return sql4prepare, binds3, err
}
wheres, binds4, err := b.ToSqlWhere(c)
if err != nil {
return sql4prepare, binds4, err
}
orderBy := b.ToSqlOrderBy(c)
limit, binds5 := b.ToSqlLimitOffset(c)
groupBys := b.ToSqlGroupBy(c)
havings, binds6, err := b.ToSqlHaving(c)
binds = append(binds, anies...)
binds = append(binds, binds2...)
binds = append(binds, binds3...)
binds = append(binds, binds4...)
binds = append(binds, binds5...)
binds = append(binds, binds6...)
sql4prepare = NamedSprintf("SELECT :selects FROM :table :join :wheres :groupBys :havings :orderBy :pagination :PessimisticLocking", selects, table, joins, wheres, groupBys, havings, orderBy, limit, c.PessimisticLocking)
return
}
func (Builder) ToSqlSelect(c *builder.Context) (sql4prepare string, binds []any) {
var cols []string
for _, col := range c.SelectClause.Columns {
if col.IsRaw {
cols = append(cols, col.Name)
binds = append(binds, col.Binds...)
} else {
if col.Alias == "" {
cols = append(cols, BackQuotes(col.Name))
} else {
cols = append(cols, fmt.Sprintf("%s AS %s", BackQuotes(col.Name), col.Alias))
}
}
}
if len(cols) == 0 {
cols = []string{"*"}
}
var distinct string
if c.SelectClause.Distinct {
distinct = "DISTINCT "
}
sql4prepare = fmt.Sprintf("%s%s", distinct, strings.Join(cols, ", "))
return
}
func (b Builder) ToSqlTable(c *builder.Context) (sql4prepare string, binds []any, err error) {
return b.buildSqlTable(c.TableClause, c.Prefix)
}
func (b Builder) buildSqlTable(tab builder.TableClause, prefix string) (sql4prepare string, binds []any, err error) {
if v, ok := tab.Tables.(builder.IBuilder); ok {
sql4prepare, binds, err = v.ToSql()
if tab.Alias != "" {
sql4prepare = fmt.Sprintf("(%s) %s", sql4prepare, BackQuotes(tab.Alias))
}
return
}
rfv := reflect.Indirect(reflect.ValueOf(tab.Tables))
switch rfv.Kind() {
case reflect.String:
sql4prepare = BackQuotes(fmt.Sprintf("%s%s", prefix, tab.Tables))
case reflect.Struct:
sql4prepare = b.buildTableName(rfv.Type(), prefix)
case reflect.Slice:
if rfv.Type().Elem().Kind() == reflect.Struct {
sql4prepare = b.buildTableName(rfv.Type().Elem(), prefix)
} else {
err = errors.New("table param must be string or struct(slice) bind with 1 or 2 params")
return
}
default:
err = errors.New("table must be string | struct | slice")
return
}
return strings.TrimSpace(fmt.Sprintf("%s %s", sql4prepare, BackQuotes(tab.Alias))), binds, err
}
func (b Builder) toSqlWhere(c *builder.Context, wc builder.WhereClause) (sql4prepare string, binds []any, err error) {
if len(wc.Conditions) == 0 {
return
}
var sql4prepareArr []string
for _, v := range wc.Conditions {
switch item := v.(type) {
case builder.TypeWhereRaw:
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s", item.LogicalOp, item.Column))
binds = append(binds, item.Bindings...)
case builder.TypeWhereStandard:
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s ?", item.LogicalOp, BackQuotes(item.Column), item.Operator))
binds = append(binds, item.Value)
case builder.TypeWhereIn:
values := ToSlice(item.Value)
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, BackQuotes(item.Column), item.Operator, strings.Repeat("?,", len(values)-1)+"?"))
binds = append(binds, values...)
case builder.TypeWhereBetween:
values := ToSlice(item.Value)
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s ? AND ?", item.LogicalOp, BackQuotes(item.Column), item.Operator))
binds = append(binds, values...)
case builder.TypeWhereNested:
var tmp = builder.Context{}
item.WhereNested(&tmp.WhereClause)
prepare, anies, err := b.ToSqlWhere(&tmp)
if err != nil {
return sql4prepare, binds, err
}
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s (%s)", item.LogicalOp, strings.TrimPrefix(prepare, "WHERE ")))
binds = append(binds, anies...)
case builder.TypeWhereSubQuery:
query, anies, err := item.SubQuery.ToSql()
if err != nil {
return sql4prepare, binds, err
}
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, BackQuotes(item.Column), item.Operator, query))
binds = append(binds, anies...)
case builder.TypeWhereSubHandler:
var ctx = builder.NewContext(c.Prefix)
item.Sub(ctx)
query, anies, err := b.ToSql(ctx)
if err != nil {
return sql4prepare, binds, err
}
sql4prepareArr = append(sql4prepareArr, fmt.Sprintf("%s %s %s (%s)", item.LogicalOp, BackQuotes(item.Column), item.Operator, query))
binds = append(binds, anies...)
}
}
if len(sql4prepareArr) > 0 {
sql4prepare = strings.TrimSpace(strings.Trim(strings.Trim(strings.TrimSpace(strings.Join(sql4prepareArr, " ")), "AND"), "OR"))
}
return
}
func (b Builder) ToSqlWhere(c *builder.Context) (sql4prepare string, binds []any, err error) {
sql4prepare, binds, err = b.toSqlWhere(c, c.WhereClause)
if sql4prepare != "" {
if c.WhereClause.Not {
sql4prepare = fmt.Sprintf("WHERE NOT %s", sql4prepare)
}
sql4prepare = fmt.Sprintf("WHERE %s", sql4prepare)
}
return
}
func (b Builder) ToSqlJoin(c *builder.Context) (sql4prepare string, binds []any, err error) {
if c.JoinClause.Err != nil {
return sql4prepare, binds, c.JoinClause.Err
}
if len(c.JoinClause.JoinItems) == 0 {
return
}
for _, v := range c.JoinClause.JoinItems {
var prepare string
var sql4 string
var bind []any
switch item := v.(type) {
case builder.TypeJoinStandard:
prepare, bind, err = b.buildSqlTable(item.TableClause, c.Prefix)
if err != nil {
return
}
sql4 = fmt.Sprintf("%s %s ON %s %s %s", item.Type, prepare, BackQuotes(item.Column1), item.Operator, BackQuotes(item.Column2))
case builder.TypeJoinSub:
sql4, bind, err = item.ToSql()
if err != nil {
return
}
case builder.TypeJoinOn:
var tjo builder.TypeJoinOnCondition
item.OnClause(&tjo)
if len(tjo.Conditions) == 0 {
return
}
var sqlArr []string
for _, cond := range tjo.Conditions {
sqlArr = append(sqlArr, fmt.Sprintf("%s %s %s %s", cond.Relation, BackQuotes(cond.Column1), cond.Operator, BackQuotes(cond.Column2)))
}
sql4 = TrimPrefixAndOr(strings.Join(sqlArr, " "))
}
sql4prepare = fmt.Sprintf("%s %s", sql4prepare, sql4)
binds = append(binds, bind...)
}
return
}
func (b Builder) ToSqlGroupBy(c *builder.Context) (sql4prepare string) {
if len(c.GroupClause.Groups) > 0 {
var tmp []string
for _, col := range c.GroupClause.Groups {
if col.IsRaw {
tmp = append(tmp, col.Column)
} else {
tmp = append(tmp, BackQuotes(col.Column))
}
}
sql4prepare = fmt.Sprintf("GROUP BY %s", strings.Join(tmp, ","))
}
return
}
func (b Builder) ToSqlHaving(c *builder.Context) (sql4prepare string, binds []any, err error) {
sql4prepare, binds, err = b.toSqlWhere(c, c.HavingClause.WhereClause)
if sql4prepare != "" {
sql4prepare = fmt.Sprintf("HAVING %s", sql4prepare)
}
return
}
func (b Builder) ToSqlOrderBy(c *builder.Context) (sql4prepare string) {
if len(c.OrderByClause.Columns) == 0 {
return
}
var orderBys []string
for _, v := range c.OrderByClause.Columns {
if v.IsRaw {
orderBys = append(orderBys, v.Column)
} else {
if v.Direction == "" {
orderBys = append(orderBys, BackQuotes(v.Column))
} else {
orderBys = append(orderBys, fmt.Sprintf("%s %s", BackQuotes(v.Column), v.Direction))
}
}
}
sql4prepare = fmt.Sprintf("ORDER BY %s", strings.Join(orderBys, ", "))
return
}
func (b Builder) ToSqlLimitOffset(c *builder.Context) (sqlSegment string, binds []any) {
var offset int
if c.LimitOffsetClause.Offset > 0 {
offset = c.LimitOffsetClause.Offset
} else if c.LimitOffsetClause.Page > 0 {
offset = c.LimitOffsetClause.Limit * (c.LimitOffsetClause.Page - 1)
}
if c.LimitOffsetClause.Limit > 0 {
if offset > 0 {
sqlSegment = "LIMIT ? OFFSET ?"
binds = append(binds, c.LimitOffsetClause.Limit, offset)
} else {
sqlSegment = "LIMIT ?"
binds = append(binds, c.LimitOffsetClause.Limit)
}
}
return
}
// ToSqlInsert insert
func (b Builder) ToSqlInsert(c *builder.Context, obj any, args ...builder.TypeToSqlInsertCase) (sqlSegment string, binds []any, err error) {
var arg builder.TypeToSqlInsertCase
if len(args) > 0 {
arg = args[0]
}
var ctx = *c
rfv := reflect.Indirect(reflect.ValueOf(obj))
switch rfv.Kind() {
case reflect.Struct:
var datas []map[string]any
datas, err = parser.StructsToInsert(obj, arg.MustColumn...)
if err != nil {
return
}
ctx.TableClause.Table(obj)
return b.toSqlInsert(&ctx, datas, arg)
case reflect.Slice:
switch rfv.Type().Elem().Kind() {
case reflect.Struct:
c.TableClause.Table(obj)
var datas []map[string]any
datas, err = parser.StructsToInsert(obj, arg.MustColumn...)
if err != nil {
return
}
return b.toSqlInsert(c, datas, arg)
default:
return b.toSqlInsert(c, obj, arg)
}
default:
return b.toSqlInsert(c, obj, arg)
}
}
func (b Builder) ToSqlDelete(c *builder.Context, obj any, mustColumn ...string) (sqlSegment string, binds []any, err error) {
var ctx = *c
rfv := reflect.Indirect(reflect.ValueOf(obj))
switch rfv.Kind() {
case reflect.Struct:
data, err := parser.StructToDelete(obj, mustColumn...)
if err != nil {
return sqlSegment, binds, err
}
ctx.TableClause.Table(obj)
ctx.WhereClause.Where(data)
return b.toSqlDelete(&ctx)
case reflect.Int64, reflect.Int32, reflect.String:
ctx.WhereClause.Where("id", obj)
return b.toSqlDelete(&ctx)
default:
err = errors.New("obj must be struct or id value")
}
return
}
func (b Builder) ToSqlUpdate(c *builder.Context, arg any) (sqlSegment string, binds []any, err error) {
switch v := arg.(type) {
case builder.TypeToSqlUpdateCase:
return b.toSqlUpdate(c, v.BindOrData, v.MustColumn...)
case builder.TypeToSqlIncDecCase:
return b.toSqlIncDec(c, v.Symbol, v.Data)
default:
return
}
}
func (b Builder) toSqlUpdate(c *builder.Context, obj any, mustColumn ...string) (sqlSegment string, binds []any, err error) {
rfv := reflect.Indirect(reflect.ValueOf(obj))
switch rfv.Kind() {
case reflect.Struct:
dataMap, pk, pkValue, err := parser.StructToUpdate(obj, mustColumn...)
if err != nil {
return sqlSegment, binds, err
}
var ctx = *c
ctx.TableClause.Table(obj)
if pk != "" {
ctx.WhereClause.Where(pk, pkValue)
}
return b.toSqlUpdateReal(&ctx, dataMap)
case reflect.Map:
return b.toSqlUpdateReal(c, obj)
default:
err = errors.New("no support update obj")
return
}
}
func (b Builder) toSqlIncDec(c *builder.Context, symbol string, data map[string]any) (sql4prepare string, values []any, err error) {
prepare, anies, err := b.ToSqlTable(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, anies...)
var tmp []string
for k, v := range data {
tmp = append(tmp, fmt.Sprintf("%s=%s%s?", BackQuotes(k), BackQuotes(k), symbol))
values = append(values, v)
}
where, val, err := b.ToSqlWhere(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, val...)
sql4prepare = fmt.Sprintf("UPDATE %s SET %s %s", prepare, strings.Join(tmp, ","), where)
return
}

View File

@@ -1,80 +0,0 @@
package mysql
import (
"github.com/gohouse/gorose/v3"
"testing"
)
type User struct {
Id int64 `db:"id,pk"`
Name string `db:"name"`
}
var dbg = gorose.Open(nil)
func db() *gorose.Database {
return dbg.NewDatabase()
}
func TestDatabase_ToSqlTo(t *testing.T) {
var user = User{Id: 1}
prepare, values, err := db().ToSqlTo(&user)
assertsError(t, err)
var expect = "SELECT `id`, `name` FROM `User` WHERE `id` = ? LIMIT ?"
assertsEqual(t, expect, prepare)
var expectValues = []int{1, 1}
assertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlToSlice(t *testing.T) {
var user []User
prepare, values, err := db().Where("id", ">", 1).OrderBy("id").Limit(10).Page(2).ToSqlTo(&user)
assertsError(t, err)
var expect = "SELECT `id`, `name` FROM `User` WHERE `id` > ? ORDER BY `id` LIMIT ? OFFSET ?"
assertsEqual(t, expect, prepare)
var expectValues = []int{1, 10, 10}
assertsEqual(t, expectValues, values)
}
func TestDatabase_ToSql(t *testing.T) {
prepare, values, err := db().Table("users").Select("b").Where("c", 1).GroupBy("a").Having("a", 1).OrderBy("id").Limit(10).Page(2).ToSql()
assertsError(t, err)
var expect = "SELECT `b` FROM `users` WHERE `c` = ? GROUP BY `a` HAVING `a` = ? ORDER BY `id` LIMIT ? OFFSET ?"
assertsEqual(t, expect, prepare)
var expectValues = []int{1, 10, 10, 1}
assertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlInsert(t *testing.T) {
var user = User{Name: "john"}
prepare, values, err := db().ToSqlInsert(&user)
assertsError(t, err)
var expect = "INSERT INTO `User` (`name`) VALUES (?)"
assertsEqual(t, expect, prepare)
var expectValues = []string{"john"}
assertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlInserts(t *testing.T) {
var user = []User{{Name: "John"}, {Name: "Alice"}}
prepare, values, err := db().ToSqlInsert(&user)
assertsError(t, err)
var expect = "INSERT INTO `User` (`name`) VALUES (?),(?)"
assertsEqual(t, expect, prepare)
var expectValues = []string{"John", "Alice"}
assertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlUpdate(t *testing.T) {
var user = User{Id: 1, Name: "john"}
prepare, values, err := db().ToSqlUpdate(&user)
assertsError(t, err)
var expect = "UPDATE `User` SET `name` = ? WHERE `id` = ?"
assertsEqual(t, expect, prepare)
var expectValues = []any{"john", 1}
assertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlDelete(t *testing.T) {
var user = User{Id: 1}
prepare, values, err := db().ToSqlDelete(&user, "name")
assertsError(t, err)
var expect = "DELETE FROM `User` WHERE `id` = ? AND `name` = ?"
assertsEqual(t, expect, prepare)
var expectValues = []any{1, ""}
assertsEqual(t, expectValues, values)
}

View File

@@ -1,140 +0,0 @@
package mysql
import (
"errors"
"fmt"
"github.com/gohouse/gorose/v3/builder"
"github.com/gohouse/gorose/v3/parser"
"reflect"
"sort"
"strings"
)
func (b Builder) buildTableName(rft reflect.Type, prefix string) (tab string) {
return BackQuotes(fmt.Sprintf("%s%s", prefix, parser.StructsToTableName(rft)))
}
// func (b Builder) toSqlInsert(c *gorose.Context, data any, ignoreCase string, onDuplicateKeys []string) (sql4prepare string, values []any, err error) {
func (b Builder) toSqlInsert(c *builder.Context, data any, insertCase builder.TypeToSqlInsertCase) (sql4prepare string, values []any, err error) {
rfv := reflect.Indirect(reflect.ValueOf(data))
var fields []string
var valuesPlaceholderArr []string
switch rfv.Kind() {
case reflect.Map:
keys := rfv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
var valuesPlaceholderTmp []string
for _, key := range keys {
fields = append(fields, BackQuotes(key.String()))
valuesPlaceholderTmp = append(valuesPlaceholderTmp, "?")
values = append(values, rfv.MapIndex(key).Interface())
}
valuesPlaceholderArr = append(valuesPlaceholderArr, fmt.Sprintf("(%s)", strings.Join(valuesPlaceholderTmp, ",")))
case reflect.Slice:
if rfv.Len() == 0 {
return
}
if rfv.Type().Elem().Kind() == reflect.Map {
// 先获取到插入字段
keys := rfv.Index(0).MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
for _, key := range keys {
fields = append(fields, BackQuotes(key.String()))
}
// 组合插入数据
for i := 0; i < rfv.Len(); i++ {
var valuesPlaceholderTmp []string
for _, key := range keys {
valuesPlaceholderTmp = append(valuesPlaceholderTmp, "?")
values = append(values, rfv.Index(i).MapIndex(key).Interface())
}
valuesPlaceholderArr = append(valuesPlaceholderArr, fmt.Sprintf("(%s)", strings.Join(valuesPlaceholderTmp, ",")))
}
} else {
err = errors.New("only map(slice) data supported")
return
}
default:
err = errors.New("only map(slice) data supported")
return
}
if err != nil {
return
}
var onDuplicateKey string
if len(insertCase.UpdateFields) > 0 {
var tmp []string
for _, v := range insertCase.UpdateFields {
tmp = append(tmp, fmt.Sprintf("%s=VALUES(%s)", BackQuotes(v), BackQuotes(v)))
}
onDuplicateKey = fmt.Sprintf("ON DUPLICATE KEY UPDATE %s", strings.Join(tmp, ", "))
}
var insert = "INSERT"
if insertCase.IsReplace {
insert = "REPLACE"
} else if insertCase.IsIgnoreCase {
insert = "INSERT IGNORE"
}
var tables string
tables, _, err = b.ToSqlTable(c)
if err != nil {
return
}
sql4prepare = NamedSprintf(":insert INTO :tables (:fields) VALUES :placeholder :onDuplicateKey", insert, tables, strings.Join(fields, ","), strings.Join(valuesPlaceholderArr, ","), onDuplicateKey)
return
}
func (b Builder) toSqlUpdateReal(c *builder.Context, data any) (sql4prepare string, values []any, err error) {
rfv := reflect.Indirect(reflect.ValueOf(data))
var updates []string
switch rfv.Kind() {
case reflect.Map:
keys := rfv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
for _, key := range keys {
updates = append(updates, fmt.Sprintf("%s = ?", BackQuotes(key.String())))
values = append(values, rfv.MapIndex(key).Interface())
}
default:
err = errors.New("only map data supported")
return
}
var tables string
tables, _, err = b.ToSqlTable(c)
if err != nil {
return
}
wheres, binds, err := b.ToSqlWhere(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, binds...)
sql4prepare = NamedSprintf("UPDATE :tables SET :updates :wheres", tables, strings.Join(updates, ", "), wheres)
return
}
func (b Builder) toSqlDelete(c *builder.Context) (sql4prepare string, values []any, err error) {
var tables string
tables, _, err = b.ToSqlTable(c)
if err != nil {
return
}
wheres, binds, err := b.ToSqlWhere(c)
if err != nil {
return sql4prepare, values, err
}
values = append(values, binds...)
sql4prepare = NamedSprintf("DELETE FROM :tables :wheres", tables, wheres)
return
}

View File

@@ -1,18 +0,0 @@
package sqlite3
import (
//_ "github.com/go-sql-driver/mysql"
"github.com/gohouse/gorose/v3/driver"
"github.com/gohouse/gorose/v3/driver/mysql"
)
const DriverName = "sqlite3"
type Builder struct {
//prefix string
mysql.Builder
}
func init() {
driver.Register(DriverName, &Builder{})
}

View File

@@ -1,4 +1,4 @@
package mysql
package driver
import (
"encoding/json"
@@ -10,6 +10,14 @@ import (
"testing"
)
func IsExpression(obj any) (b bool) {
rfv := reflect.Indirect(reflect.ValueOf(obj))
if rfv.Kind() == reflect.String && strings.Contains(rfv.String(), "?") {
b = true
}
return
}
func BackQuotes(arg any) string {
var tmp []string
if v, ok := arg.(string); ok {
@@ -56,18 +64,19 @@ func TrimPrefixAndOr(s string) string {
return regexp.MustCompile(`(?i)^\s*(and|or)\s+`).ReplaceAllString(s, "")
}
func assertsEqual(t *testing.T, expect, real any) {
func AssertsEqual(t *testing.T, expect, real any) {
marshal, err := json.Marshal(expect)
assertsError(t, err)
AssertsError(t, err)
bytes, err := json.Marshal(real)
assertsError(t, err)
AssertsError(t, err)
if string(marshal) != string(bytes) {
methodName, file, line := getCallerInfo()
t.Errorf("[%s] Error\n\t Trace - %s:%v\n\tExpect - %T %s\n\t Got - %T %s\n------------------------------------------------------", methodName, file, line, expect, marshal, real, bytes)
t.Errorf("[%s] Error\n\t Trace - %s:%v\n\tExpect - %T %s\n\t Got - %T %s\n------------------------------------------------------",
methodName, file, line, expect, marshal, real, bytes)
}
}
func assertsError(t *testing.T, err error) {
func AssertsError(t *testing.T, err error) {
if err != nil {
methodName, file, line := getCallerInfo()
t.Errorf("[%s] Error\n\t Trace - %s:%v\n\t%s\n------------------------------------------------------", methodName, file, line, err.Error())
@@ -99,7 +108,7 @@ func getCallerInfo() (string, string, int) {
func jsonLog(t *testing.T, data any) {
marshal, err := json.Marshal(data)
assertsError(t, err)
AssertsError(t, err)
t.Logf("json data: %s", marshal)
}

View File

@@ -66,7 +66,7 @@ func (s *Engin) SavePoint(name any) (err error) {
return
}
func (s *Engin) RollbackTo(name any) (err error) {
_, err = s.tx.Exec("ROLLBACK TO ?", name)
_, err = s.tx.Exec("ROLLBACK TO SAVEPOINT ?", name)
return
}
func (s *Engin) Rollback() (err error) {

View File

@@ -2,21 +2,19 @@ package gorose
import (
"database/sql"
"github.com/gohouse/gorose/v3/builder"
_ "github.com/gohouse/gorose/v3/driver/mysql"
)
type GoRose struct {
Cluster *ConfigCluster
master []*sql.DB
slave []*sql.DB
driver string
prefix string
handlers HandlersChain
Cluster *ConfigCluster
master []*sql.DB
slave []*sql.DB
driver string
prefix string
//handlers HandlersChain
}
type HandlerFunc func(*builder.Context)
type HandlersChain []HandlerFunc
//type HandlerFunc func(*builder.Context)
//type HandlersChain []HandlerFunc
//const abortIndex int8 = math.MaxInt8 >> 1
@@ -33,10 +31,10 @@ type HandlersChain []HandlerFunc
// }
//}
func (g *GoRose) Use(h ...HandlerFunc) *GoRose {
g.handlers = append(g.handlers, h...)
return g
}
//func (g *GoRose) Use(h ...HandlerFunc) *GoRose {
// g.handlers = append(g.handlers, h...)
// return g
//}
//func (dg *GoRose) Use(h ...HandlerFunc) *GoRose {
//}
@@ -69,16 +67,18 @@ func Open(conf ...any) *GoRose {
return &g
}
g.master, g.slave = g.Cluster.init()
} else {
g.driver = "mysql" // for toSql test
} else if dr, ok := conf[0].(string); ok {
g.driver = dr // for toSql test
}
case 2:
g.driver = conf[0].(string)
db, err := sql.Open(g.driver, conf[1].(string))
if err != nil {
panic(err.Error())
}
g.master = append(g.master, db)
if conf[1].(string) != "" {
db, err := sql.Open(g.driver, conf[1].(string))
if err != nil {
panic(err.Error())
}
g.master = append(g.master, db)
} // else: for toSql test
default:
panic("config must be *gorose.ConfigCluster or sql.Open() origin params")
}

View File

@@ -1,13 +1,15 @@
# GoRose ORM V3
PHP Laravel ORM 的 go 实现, 与 laravel 官方文档保持一致 https://laravel.com/docs/10.x/queries .
分为 go 风格 (struct 结构绑定用法) 和 php 风格 (map 结构用法).
php 风格用法, 完全可以使用 laravel query builder 的文档做参考, 尽量做到 1:1 还原.
php 风格用法, 完全可以使用 laravel query builder 的文档做参考, 尽量做到 1:1 还原.
## GoRose V3 架构图
![](./assets/gorose.svg)
## 安装
目前还处于beta阶段, 请谨慎使用. 由于没有打 tag, 只能使用 go mod 的方式引入
```shell
# go.mod
require github.com/gohouse/gorose/v3 master
```
@@ -57,18 +59,21 @@ func main() {
db().Delete(&user)
}
```
php风格用法 和 go风格用法对比
php风格用法 和 go风格用法查询如下sql对比:
```sql
select id,name,email from users where id=1 and name="test"
```
```go
// select id,name,email from users where id=1 and name="test"
db().Table("users").
// php
user,err := db().Table("users").
Select("id","name","email").
Where("id", "=", 1).Where("name", "test").
First()
// 等同于
// go
var user = User{Id: 1, Name: "test"}
db().To(&user)
err := db().To(&user)
```
由此可以看出, 除了对 表 模型的绑定区别, 其他方法通用
上边的两种用法结果相同,由此可以看出,除了对 表 模型的绑定区别, 其他方法通用
## 配置
单数据库连接, 可以直接同官方接口一样用法
@@ -105,6 +110,13 @@ var rose = gorose.Open(
)
```
## 驱动支持
- MySQL
- MsSQL
- Postgresql
- Oracle
- Sqlite3
## 事务
```go
// 全自动事务, 有错误会自动回滚, 无错误会自动提交
@@ -346,7 +358,7 @@ type Result struct {
Bname string `db:"bname"`
}
var res Result
// select a.id, a.name aname, b.name bname from a inner join b on a.id=b.id where a.id>1
// select a.id, a.name aname, b.name bname from a inner join b on a.id=b.aid where a.id>1
db().Table("a").Join("b", "a.id","b.aid").Select("a.id", "a.name aname","b.name bname").Where("a.id", ">", 1).Bind(&res)
```
查询字段的显示名字一定要跟 结构体的字段 tag(db) 名字相同, 否则不会被赋值

View File

@@ -88,9 +88,14 @@ func (db *Database) ToSqlDelete(obj any, mustColumn ...string) (sqlSegment strin
}
func (db *Database) ToSqlUpdate(obj any, mustColumn ...string) (sqlSegment string, binds []any, err error) {
return db.Driver.ToSqlUpdate(db.Context, builder.TypeToSqlUpdateCase{obj, mustColumn})
return db.Driver.ToSqlUpdate(db.Context, builder.TypeToSqlUpdateCase{BindOrData: obj, MustColumn: mustColumn})
}
// ToSqlIncDec
//
// symbol: +/-
// data: {count: 2} => count = count + 2
func (db *Database) ToSqlIncDec(symbol string, data map[string]any) (sql4prepare string, values []any, err error) {
//return db.Driver.ToSqlIncDec(db.Context, symbol, data)
return db.Driver.ToSqlUpdate(db.Context, builder.TypeToSqlIncDecCase{symbol, data})
return db.Driver.ToSqlUpdate(db.Context, builder.TypeToSqlIncDecCase{Symbol: symbol, Data: data})
}

View File

@@ -1,14 +1,91 @@
package gorose
import (
"github.com/gohouse/gorose/v3/driver"
"testing"
)
func TestDatabase_ToSql(t *testing.T) {
//db := Open("mysql", "root:root@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true")
//prepare, values, err := db.NewDatabase().Table("user").Where("id", 1).Where("name", "kk").Where("age", 18).Where("sex", 1).Where("status", 1).
// Union(
// db.NewDatabase().Table("card").Where("id", 1),
// ).ToSql()
//log.Println(prepare, values, err)
type User struct {
Id int64 `db:"id,pk"`
Name string `db:"name"`
}
// var dbg = Open("mysql") // just test toSql
// var dbg = Open("postgresql") // just test toSql
// var dbg = Open("mssql") // just test toSql
// var dbg = Open("oracle") // just test toSql
var dbg = Open("sqlite3") // just test toSql
func db() *Database {
return dbg.NewDatabase()
}
func TestDatabase_ToSqlTo(t *testing.T) {
var user = User{Id: 1}
prepare, values, err := db().ToSqlTo(&user)
driver.AssertsError(t, err)
//db().Driver.Dialect.Placeholder()
var expect = map[string]string{
"mysql": "SELECT `id`, `name` FROM `User` WHERE `id` = ? LIMIT 1",
"postgresql": `SELECT "id", "name" FROM "User" WHERE "id" = $1 LIMIT 1`,
"mssql": `SELECT [id], [name] FROM [User] WHERE [id] = @p1 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY`,
"oracle": `SELECT "id", "name" FROM "User" WHERE "id" = @p1 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY`,
"sqlite3": `SELECT "id", "name" FROM "User" WHERE "id" = ? LIMIT 1`,
}
driver.AssertsEqual(t, expect[dbg.driver], prepare)
var expectValues = []interface{}{1}
driver.AssertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlToSlice(t *testing.T) {
var user []User
prepare, values, err := db().Where("id", ">", 1).OrderBy("id").Limit(10).Page(2).ToSqlTo(&user)
driver.AssertsError(t, err)
var expect = "SELECT `id`, `name` FROM `User` WHERE `id` > ? ORDER BY `id` LIMIT 10 OFFSET 10"
driver.AssertsEqual(t, expect, prepare)
var expectValues = []interface{}{1}
driver.AssertsEqual(t, expectValues, values)
}
func TestDatabase_ToSql(t *testing.T) {
prepare, values, err := db().Table("users").Select("b").Where("c", 1).GroupBy("a").Having("a", 1).OrderBy("id").Limit(10).Page(2).ToSql()
driver.AssertsError(t, err)
var expect = "SELECT `b` FROM `users` WHERE `c` = ? GROUP BY `a` HAVING `a` = ? ORDER BY `id` LIMIT 10 OFFSET 10"
driver.AssertsEqual(t, expect, prepare)
var expectValues = []int{1, 1}
driver.AssertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlInsert(t *testing.T) {
var user = User{Name: "john"}
prepare, values, err := db().ToSqlInsert(&user)
driver.AssertsError(t, err)
var expect = "INSERT INTO `User` (`name`) VALUES (?)"
driver.AssertsEqual(t, expect, prepare)
var expectValues = []string{"john"}
driver.AssertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlInserts(t *testing.T) {
var user = []User{{Name: "John"}, {Name: "Alice"}}
prepare, values, err := db().ToSqlInsert(&user)
driver.AssertsError(t, err)
var expect = "INSERT INTO `User` (`name`) VALUES (?),(?)"
driver.AssertsEqual(t, expect, prepare)
var expectValues = []string{"John", "Alice"}
driver.AssertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlUpdate(t *testing.T) {
var user = User{Id: 1, Name: "john"}
prepare, values, err := db().ToSqlUpdate(&user)
driver.AssertsError(t, err)
var expect = "UPDATE `User` SET `name` = ? WHERE `id` = ?"
driver.AssertsEqual(t, expect, prepare)
var expectValues = []any{"john", 1}
driver.AssertsEqual(t, expectValues, values)
}
func TestDatabase_ToSqlDelete(t *testing.T) {
var user = User{Id: 1}
prepare, values, err := db().ToSqlDelete(&user, "name")
driver.AssertsError(t, err)
var expect = "DELETE FROM `User` WHERE `id` = ? AND `name` = ?"
driver.AssertsEqual(t, expect, prepare)
var expectValues = []any{1, ""}
driver.AssertsEqual(t, expectValues, values)
}