mirror of
https://github.com/hanzoai/dbx.git
synced 2026-10-30 18:40:28 +00:00
484 lines
11 KiB
Go
484 lines
11 KiB
Go
// Copyright 2026 Hanzo AI. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build ignore
|
|
|
|
package dbx
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// CompatEngine wraps a *DB to provide xorm-style fluent query methods.
|
|
// This enables mechanical migration from xorm without rewriting every call site.
|
|
//
|
|
// Usage:
|
|
//
|
|
// engine := dbx.NewCompatEngine(db)
|
|
// engine.Where("owner=?", "admin").Find(&users)
|
|
// engine.ID([]interface{}{"admin","app1"}).Get(&app)
|
|
// engine.Insert(&user)
|
|
// engine.ID(pk).Update(&user)
|
|
// engine.ID(pk).Delete(&User{})
|
|
type CompatEngine struct {
|
|
db *DB
|
|
where string
|
|
whereArgs []interface{}
|
|
orderBy string
|
|
limit int
|
|
offset int
|
|
id []interface{}
|
|
useBool bool
|
|
allCols bool
|
|
mustCols []string
|
|
tableName string
|
|
}
|
|
|
|
// NewCompatEngine creates a xorm-compatible wrapper around a dbx.DB.
|
|
func NewCompatEngine(db *DB) *CompatEngine {
|
|
return &CompatEngine{db: db}
|
|
}
|
|
|
|
func (e *CompatEngine) clone() *CompatEngine {
|
|
return &CompatEngine{
|
|
db: e.db,
|
|
where: e.where,
|
|
whereArgs: e.whereArgs,
|
|
orderBy: e.orderBy,
|
|
limit: e.limit,
|
|
offset: e.offset,
|
|
id: e.id,
|
|
useBool: e.useBool,
|
|
allCols: e.allCols,
|
|
mustCols: e.mustCols,
|
|
tableName: e.tableName,
|
|
}
|
|
}
|
|
|
|
func (e *CompatEngine) reset() {
|
|
e.where = ""
|
|
e.whereArgs = nil
|
|
e.orderBy = ""
|
|
e.limit = 0
|
|
e.offset = 0
|
|
e.id = nil
|
|
e.useBool = false
|
|
e.allCols = false
|
|
e.mustCols = nil
|
|
e.tableName = ""
|
|
}
|
|
|
|
// Where adds a WHERE condition. Chainable.
|
|
func (e *CompatEngine) Where(cond string, args ...interface{}) *CompatEngine {
|
|
c := e.clone()
|
|
if c.where != "" {
|
|
c.where += " AND " + cond
|
|
c.whereArgs = append(c.whereArgs, args...)
|
|
} else {
|
|
c.where = cond
|
|
c.whereArgs = args
|
|
}
|
|
return c
|
|
}
|
|
|
|
// ID sets the primary key for Get/Update/Delete. Accepts single value or []interface{} for composite PKs.
|
|
func (e *CompatEngine) ID(id interface{}) *CompatEngine {
|
|
c := e.clone()
|
|
switch v := id.(type) {
|
|
case []interface{}:
|
|
c.id = v
|
|
default:
|
|
c.id = []interface{}{v}
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Desc adds ORDER BY ... DESC.
|
|
func (e *CompatEngine) Desc(cols ...string) *CompatEngine {
|
|
c := e.clone()
|
|
for _, col := range cols {
|
|
if c.orderBy != "" {
|
|
c.orderBy += ", "
|
|
}
|
|
c.orderBy += col + " DESC"
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Asc adds ORDER BY ... ASC.
|
|
func (e *CompatEngine) Asc(cols ...string) *CompatEngine {
|
|
c := e.clone()
|
|
for _, col := range cols {
|
|
if c.orderBy != "" {
|
|
c.orderBy += ", "
|
|
}
|
|
c.orderBy += col + " ASC"
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Limit sets LIMIT and OFFSET.
|
|
func (e *CompatEngine) Limit(limit int, offset ...int) *CompatEngine {
|
|
c := e.clone()
|
|
c.limit = limit
|
|
if len(offset) > 0 {
|
|
c.offset = offset[0]
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Table sets an explicit table name.
|
|
func (e *CompatEngine) Table(name string) *CompatEngine {
|
|
c := e.clone()
|
|
c.tableName = name
|
|
return c
|
|
}
|
|
|
|
// UseBool is a no-op for compatibility (dbx handles bools natively).
|
|
func (e *CompatEngine) UseBool(cols ...string) *CompatEngine {
|
|
c := e.clone()
|
|
c.useBool = true
|
|
return c
|
|
}
|
|
|
|
// AllCols marks all columns for update (default in dbx).
|
|
func (e *CompatEngine) AllCols() *CompatEngine {
|
|
c := e.clone()
|
|
c.allCols = true
|
|
return c
|
|
}
|
|
|
|
// MustCols forces specific columns in update even if zero value.
|
|
func (e *CompatEngine) MustCols(cols ...string) *CompatEngine {
|
|
c := e.clone()
|
|
c.mustCols = append(c.mustCols, cols...)
|
|
return c
|
|
}
|
|
|
|
// Cols is an alias for MustCols.
|
|
func (e *CompatEngine) Cols(cols ...string) *CompatEngine {
|
|
return e.MustCols(cols...)
|
|
}
|
|
|
|
// Find queries all matching rows into a slice pointer.
|
|
func (e *CompatEngine) Find(rowsSlicePtr interface{}, conds ...interface{}) error {
|
|
defer e.reset()
|
|
table := e.resolveTable(rowsSlicePtr)
|
|
q := e.db.Select().From(table)
|
|
q = e.applyConditions(q)
|
|
|
|
// Additional conditions from args
|
|
for _, cond := range conds {
|
|
if hm, ok := cond.(map[string]interface{}); ok {
|
|
for k, v := range hm {
|
|
q = q.Where(HashExp{k: v})
|
|
}
|
|
}
|
|
}
|
|
|
|
return q.All(rowsSlicePtr)
|
|
}
|
|
|
|
// Get queries a single row by PK or conditions.
|
|
func (e *CompatEngine) Get(bean interface{}) (bool, error) {
|
|
defer e.reset()
|
|
table := e.resolveTable(bean)
|
|
q := e.db.Select().From(table)
|
|
q = e.applyConditions(q)
|
|
|
|
err := q.One(bean)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Insert inserts a struct into its table.
|
|
func (e *CompatEngine) Insert(beans ...interface{}) (int64, error) {
|
|
defer e.reset()
|
|
var total int64
|
|
for _, bean := range beans {
|
|
_, err := e.db.Model(bean).Insert()
|
|
if err != nil {
|
|
return total, err
|
|
}
|
|
total++
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// Update updates a row by PK or conditions.
|
|
func (e *CompatEngine) Update(bean interface{}, conds ...interface{}) (int64, error) {
|
|
defer e.reset()
|
|
table := e.resolveTable(bean)
|
|
|
|
// Build SET clause from struct fields
|
|
sv := e.db.NewStructValue(bean, nil)
|
|
if sv == nil {
|
|
return 0, fmt.Errorf("cannot resolve struct for update")
|
|
}
|
|
|
|
sets := make(map[string]interface{})
|
|
v := reflect.ValueOf(bean)
|
|
if v.Kind() == reflect.Ptr {
|
|
v = v.Elem()
|
|
}
|
|
t := v.Type()
|
|
for i := 0; i < v.NumField(); i++ {
|
|
field := t.Field(i)
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
tag := field.Tag.Get(DbTag)
|
|
if tag == "-" || tag == "pk" || strings.HasPrefix(tag, "pk,") {
|
|
continue
|
|
}
|
|
fv := v.Field(i)
|
|
// Include if AllCols or non-zero or in MustCols
|
|
if e.allCols || !fv.IsZero() || e.inMustCols(tag, field.Name) {
|
|
colName := tag
|
|
if colName == "" {
|
|
colName = defaultFieldName(field.Name)
|
|
}
|
|
sets[colName] = fv.Interface()
|
|
}
|
|
}
|
|
|
|
if len(sets) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
// Build WHERE from ID or conditions
|
|
where, args := e.buildWhere(bean)
|
|
|
|
// Build UPDATE SQL
|
|
var setCols []string
|
|
var setArgs []interface{}
|
|
i := 0
|
|
for col, val := range sets {
|
|
setCols = append(setCols, fmt.Sprintf(`"%s"=?`, col))
|
|
setArgs = append(setArgs, val)
|
|
i++
|
|
}
|
|
|
|
sql := fmt.Sprintf(`UPDATE "%s" SET %s`, table, strings.Join(setCols, ", "))
|
|
if where != "" {
|
|
sql += " WHERE " + where
|
|
setArgs = append(setArgs, args...)
|
|
}
|
|
|
|
result, err := e.db.NewQuery(sql).Execute(setArgs...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
affected, _ := result.RowsAffected()
|
|
return affected, nil
|
|
}
|
|
|
|
// Delete deletes rows by PK or conditions.
|
|
func (e *CompatEngine) Delete(bean interface{}) (int64, error) {
|
|
defer e.reset()
|
|
table := e.resolveTable(bean)
|
|
where, args := e.buildWhere(bean)
|
|
|
|
sql := fmt.Sprintf(`DELETE FROM "%s"`, table)
|
|
if where != "" {
|
|
sql += " WHERE " + where
|
|
}
|
|
|
|
result, err := e.db.NewQuery(sql).Execute(args...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
affected, _ := result.RowsAffected()
|
|
return affected, nil
|
|
}
|
|
|
|
// Count returns the number of matching rows.
|
|
func (e *CompatEngine) Count(bean ...interface{}) (int64, error) {
|
|
defer e.reset()
|
|
var table string
|
|
if len(bean) > 0 {
|
|
table = e.resolveTable(bean[0])
|
|
} else if e.tableName != "" {
|
|
table = e.tableName
|
|
} else {
|
|
return 0, fmt.Errorf("Count requires a bean or Table()")
|
|
}
|
|
|
|
sql := fmt.Sprintf(`SELECT COUNT(*) FROM "%s"`, table)
|
|
var args []interface{}
|
|
if e.where != "" {
|
|
sql += " WHERE " + e.where
|
|
args = e.whereArgs
|
|
}
|
|
|
|
var count int64
|
|
err := e.db.NewQuery(sql).Row(&count, args...)
|
|
return count, err
|
|
}
|
|
|
|
// Exec executes raw SQL.
|
|
func (e *CompatEngine) Exec(sql string, args ...interface{}) (sql.Result, error) {
|
|
return e.db.NewQuery(sql).Execute(args...)
|
|
}
|
|
|
|
// Sync creates or updates tables from struct definitions.
|
|
func (e *CompatEngine) Sync(beans ...interface{}) error {
|
|
return e.db.Sync(beans...)
|
|
}
|
|
|
|
// Sync2 is an alias for Sync (xorm compat).
|
|
func (e *CompatEngine) Sync2(beans ...interface{}) error {
|
|
return e.db.Sync(beans...)
|
|
}
|
|
|
|
// QueryString executes raw SQL and returns string maps.
|
|
func (e *CompatEngine) QueryString(sql string, args ...interface{}) ([]map[string]string, error) {
|
|
rows, err := e.db.NewQuery(sql).Rows(args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
cols, _ := rows.Columns()
|
|
var results []map[string]string
|
|
|
|
for rows.Next() {
|
|
values := make([]interface{}, len(cols))
|
|
ptrs := make([]interface{}, len(cols))
|
|
for i := range values {
|
|
ptrs[i] = &values[i]
|
|
}
|
|
rows.Scan(ptrs...)
|
|
|
|
row := make(map[string]string)
|
|
for i, col := range cols {
|
|
row[col] = fmt.Sprintf("%v", values[i])
|
|
}
|
|
results = append(results, row)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// ShowSQL is a no-op for compatibility.
|
|
func (e *CompatEngine) ShowSQL(show ...bool) {}
|
|
|
|
// Ping checks the database connection.
|
|
func (e *CompatEngine) Ping() error {
|
|
return e.db.DB().Ping()
|
|
}
|
|
|
|
// RawDB returns the underlying *sql.DB.
|
|
func (e *CompatEngine) RawDB() *sql.DB {
|
|
return e.db.DB()
|
|
}
|
|
|
|
// DB returns the underlying dbx.DB.
|
|
func (e *CompatEngine) DBX() *DB {
|
|
return e.db
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func (e *CompatEngine) resolveTable(bean interface{}) string {
|
|
if e.tableName != "" {
|
|
return e.tableName
|
|
}
|
|
t := reflect.TypeOf(bean)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() == reflect.Slice {
|
|
t = t.Elem()
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
}
|
|
return defaultFieldName(t.Name())
|
|
}
|
|
|
|
func (e *CompatEngine) applyConditions(q *SelectQuery) *SelectQuery {
|
|
if e.id != nil {
|
|
// Build WHERE from PK columns
|
|
// For composite PKs, we need the struct info — use positional matching
|
|
// This is a simplification; real code should use struct PK names
|
|
// For now, use owner + name convention
|
|
switch len(e.id) {
|
|
case 1:
|
|
q = q.Where(HashExp{"id": e.id[0]})
|
|
case 2:
|
|
q = q.Where(HashExp{"owner": e.id[0], "name": e.id[1]})
|
|
case 3:
|
|
q = q.Where(HashExp{"owner": e.id[0], "name": e.id[1], "application": e.id[2]})
|
|
}
|
|
}
|
|
if e.where != "" {
|
|
q = q.Where(NewExp(e.where, e.whereArgs...))
|
|
}
|
|
if e.orderBy != "" {
|
|
q = q.OrderBy(e.orderBy)
|
|
}
|
|
if e.limit > 0 {
|
|
q = q.Limit(int64(e.limit))
|
|
}
|
|
if e.offset > 0 {
|
|
q = q.Offset(int64(e.offset))
|
|
}
|
|
return q
|
|
}
|
|
|
|
func (e *CompatEngine) buildWhere(bean interface{}) (string, []interface{}) {
|
|
if e.id != nil {
|
|
switch len(e.id) {
|
|
case 1:
|
|
return `"id"=?`, e.id
|
|
case 2:
|
|
return `"owner"=? AND "name"=?`, e.id
|
|
case 3:
|
|
return `"owner"=? AND "name"=? AND "application"=?`, e.id
|
|
}
|
|
}
|
|
if e.where != "" {
|
|
return e.where, e.whereArgs
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (e *CompatEngine) inMustCols(tag, fieldName string) bool {
|
|
for _, mc := range e.mustCols {
|
|
if mc == tag || mc == fieldName || mc == defaultFieldName(fieldName) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NewExp creates a raw SQL expression with positional args.
|
|
func NewExp(expr string, args ...interface{}) Expression {
|
|
return &RawExpression{expr: expr, args: args}
|
|
}
|
|
|
|
// RawExpression implements Expression for raw SQL.
|
|
type RawExpression struct {
|
|
expr string
|
|
args []interface{}
|
|
}
|
|
|
|
func (e *RawExpression) Build(db *DB, params Params) string {
|
|
sql := e.expr
|
|
for i, arg := range e.args {
|
|
key := fmt.Sprintf("_raw_%d", i)
|
|
params[key] = arg
|
|
sql = strings.Replace(sql, "?", "{:"+key+"}", 1)
|
|
}
|
|
return sql
|
|
}
|