ResolveSpec/pkg/common/adapters/database/bun.go
2025-11-11 11:03:02 +02:00

442 lines
12 KiB
Go

package database
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/uptrace/bun"
"github.com/bitechdev/ResolveSpec/pkg/common"
)
// BunAdapter adapts Bun to work with our Database interface
// This demonstrates how the abstraction works with different ORMs
type BunAdapter struct {
db *bun.DB
}
// NewBunAdapter creates a new Bun adapter
func NewBunAdapter(db *bun.DB) *BunAdapter {
return &BunAdapter{db: db}
}
func (b *BunAdapter) NewSelect() common.SelectQuery {
return &BunSelectQuery{
query: b.db.NewSelect(),
db: b.db,
}
}
func (b *BunAdapter) NewInsert() common.InsertQuery {
return &BunInsertQuery{query: b.db.NewInsert()}
}
func (b *BunAdapter) NewUpdate() common.UpdateQuery {
return &BunUpdateQuery{query: b.db.NewUpdate()}
}
func (b *BunAdapter) NewDelete() common.DeleteQuery {
return &BunDeleteQuery{query: b.db.NewDelete()}
}
func (b *BunAdapter) Exec(ctx context.Context, query string, args ...interface{}) (common.Result, error) {
result, err := b.db.ExecContext(ctx, query, args...)
return &BunResult{result: result}, err
}
func (b *BunAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
return b.db.NewRaw(query, args...).Scan(ctx, dest)
}
func (b *BunAdapter) BeginTx(ctx context.Context) (common.Database, error) {
tx, err := b.db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return nil, err
}
// For Bun, we'll return a special wrapper that holds the transaction
return &BunTxAdapter{tx: tx}, nil
}
func (b *BunAdapter) CommitTx(ctx context.Context) error {
// For Bun, we need to handle this differently
// This is a simplified implementation
return nil
}
func (b *BunAdapter) RollbackTx(ctx context.Context) error {
// For Bun, we need to handle this differently
// This is a simplified implementation
return nil
}
func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Database) error) error {
return b.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
// Create adapter with transaction
adapter := &BunTxAdapter{tx: tx}
return fn(adapter)
})
}
// BunSelectQuery implements SelectQuery for Bun
type BunSelectQuery struct {
query *bun.SelectQuery
db bun.IDB // Store DB connection for count queries
hasModel bool // Track if Model() was called
schema string // Separated schema name
tableName string // Just the table name, without schema
tableAlias string
}
func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
b.query = b.query.Model(model)
b.hasModel = true // Mark that we have a model
// Try to get table name from model if it implements TableNameProvider
if provider, ok := model.(common.TableNameProvider); ok {
fullTableName := provider.TableName()
// Check if the table name contains schema (e.g., "schema.table")
b.schema, b.tableName = parseTableName(fullTableName)
}
return b
}
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
b.query = b.query.Table(table)
// Check if the table name contains schema (e.g., "schema.table")
b.schema, b.tableName = parseTableName(table)
return b
}
func (b *BunSelectQuery) Column(columns ...string) common.SelectQuery {
b.query = b.query.Column(columns...)
return b
}
func (b *BunSelectQuery) Where(query string, args ...interface{}) common.SelectQuery {
b.query = b.query.Where(query, args...)
return b
}
func (b *BunSelectQuery) WhereOr(query string, args ...interface{}) common.SelectQuery {
b.query = b.query.WhereOr(query, args...)
return b
}
func (b *BunSelectQuery) Join(query string, args ...interface{}) common.SelectQuery {
// Extract optional prefix from args
// If the last arg is a string that looks like a table prefix, use it
var prefix string
sqlArgs := args
if len(args) > 0 {
if lastArg, ok := args[len(args)-1].(string); ok && len(lastArg) < 50 && !strings.Contains(lastArg, " ") {
// Likely a prefix, not a SQL parameter
prefix = lastArg
sqlArgs = args[:len(args)-1]
}
}
// If no prefix provided, use the table name as prefix (already separated from schema)
if prefix == "" && b.tableName != "" {
prefix = b.tableName
}
// If prefix is provided, add it as an alias in the join
// Bun expects: "JOIN table AS alias ON condition"
joinClause := query
if prefix != "" && !strings.Contains(strings.ToUpper(query), " AS ") {
// If query doesn't already have AS, check if it's a simple table name
parts := strings.Fields(query)
if len(parts) > 0 && !strings.HasPrefix(strings.ToUpper(parts[0]), "JOIN") {
// Simple table name, add prefix: "table AS prefix"
joinClause = fmt.Sprintf("%s AS %s", parts[0], prefix)
if len(parts) > 1 {
// Has ON clause: "table ON ..." becomes "table AS prefix ON ..."
joinClause += " " + strings.Join(parts[1:], " ")
}
}
}
b.query = b.query.Join(joinClause, sqlArgs...)
return b
}
func (b *BunSelectQuery) LeftJoin(query string, args ...interface{}) common.SelectQuery {
// Extract optional prefix from args
var prefix string
sqlArgs := args
if len(args) > 0 {
if lastArg, ok := args[len(args)-1].(string); ok && len(lastArg) < 50 && !strings.Contains(lastArg, " ") {
prefix = lastArg
sqlArgs = args[:len(args)-1]
}
}
// If no prefix provided, use the table name as prefix (already separated from schema)
if prefix == "" && b.tableName != "" {
prefix = b.tableName
}
// Construct LEFT JOIN with prefix
joinClause := query
if prefix != "" && !strings.Contains(strings.ToUpper(query), " AS ") {
parts := strings.Fields(query)
if len(parts) > 0 && !strings.HasPrefix(strings.ToUpper(parts[0]), "LEFT") && !strings.HasPrefix(strings.ToUpper(parts[0]), "JOIN") {
joinClause = fmt.Sprintf("%s AS %s", parts[0], prefix)
if len(parts) > 1 {
joinClause += " " + strings.Join(parts[1:], " ")
}
}
}
b.query = b.query.Join("LEFT JOIN "+joinClause, sqlArgs...)
return b
}
func (b *BunSelectQuery) Preload(relation string, conditions ...interface{}) common.SelectQuery {
// Bun uses Relation() method for preloading
// For now, we'll just pass the relation name without conditions
// TODO: Implement proper condition handling for Bun
b.query = b.query.Relation(relation)
return b
}
func (b *BunSelectQuery) Order(order string) common.SelectQuery {
b.query = b.query.Order(order)
return b
}
func (b *BunSelectQuery) Limit(n int) common.SelectQuery {
b.query = b.query.Limit(n)
return b
}
func (b *BunSelectQuery) Offset(n int) common.SelectQuery {
b.query = b.query.Offset(n)
return b
}
func (b *BunSelectQuery) Group(group string) common.SelectQuery {
b.query = b.query.Group(group)
return b
}
func (b *BunSelectQuery) Having(having string, args ...interface{}) common.SelectQuery {
b.query = b.query.Having(having, args...)
return b
}
func (b *BunSelectQuery) Scan(ctx context.Context, dest interface{}) error {
return b.query.Scan(ctx, dest)
}
func (b *BunSelectQuery) Count(ctx context.Context) (int, error) {
// If Model() was set, use bun's native Count() which works properly
if b.hasModel {
count, err := b.query.Count(ctx)
return count, err
}
// Otherwise, wrap as subquery to avoid "Model(nil)" error
// This is needed when only Table() is set without a model
var count int
err := b.db.NewSelect().
TableExpr("(?) AS subquery", b.query).
ColumnExpr("COUNT(*)").
Scan(ctx, &count)
return count, err
}
func (b *BunSelectQuery) Exists(ctx context.Context) (bool, error) {
return b.query.Exists(ctx)
}
// BunInsertQuery implements InsertQuery for Bun
type BunInsertQuery struct {
query *bun.InsertQuery
values map[string]interface{}
}
func (b *BunInsertQuery) Model(model interface{}) common.InsertQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunInsertQuery) Table(table string) common.InsertQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunInsertQuery) Value(column string, value interface{}) common.InsertQuery {
if b.values == nil {
b.values = make(map[string]interface{})
}
b.values[column] = value
return b
}
func (b *BunInsertQuery) OnConflict(action string) common.InsertQuery {
b.query = b.query.On(action)
return b
}
func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
if len(columns) > 0 {
b.query = b.query.Returning(columns[0])
}
return b
}
func (b *BunInsertQuery) Exec(ctx context.Context) (common.Result, error) {
if b.values != nil {
// For Bun, we need to handle this differently
for k, v := range b.values {
b.query = b.query.Set("? = ?", bun.Ident(k), v)
}
}
result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err
}
// BunUpdateQuery implements UpdateQuery for Bun
type BunUpdateQuery struct {
query *bun.UpdateQuery
}
func (b *BunUpdateQuery) Model(model interface{}) common.UpdateQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunUpdateQuery) Table(table string) common.UpdateQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunUpdateQuery) Set(column string, value interface{}) common.UpdateQuery {
b.query = b.query.Set(column+" = ?", value)
return b
}
func (b *BunUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuery {
for column, value := range values {
b.query = b.query.Set(column+" = ?", value)
}
return b
}
func (b *BunUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
b.query = b.query.Where(query, args...)
return b
}
func (b *BunUpdateQuery) Returning(columns ...string) common.UpdateQuery {
if len(columns) > 0 {
b.query = b.query.Returning(columns[0])
}
return b
}
func (b *BunUpdateQuery) Exec(ctx context.Context) (common.Result, error) {
result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err
}
// BunDeleteQuery implements DeleteQuery for Bun
type BunDeleteQuery struct {
query *bun.DeleteQuery
}
func (b *BunDeleteQuery) Model(model interface{}) common.DeleteQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunDeleteQuery) Table(table string) common.DeleteQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunDeleteQuery) Where(query string, args ...interface{}) common.DeleteQuery {
b.query = b.query.Where(query, args...)
return b
}
func (b *BunDeleteQuery) Exec(ctx context.Context) (common.Result, error) {
result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err
}
// BunResult implements Result for Bun
type BunResult struct {
result sql.Result
}
func (b *BunResult) RowsAffected() int64 {
if b.result == nil {
return 0
}
rows, _ := b.result.RowsAffected()
return rows
}
func (b *BunResult) LastInsertId() (int64, error) {
if b.result == nil {
return 0, nil
}
return b.result.LastInsertId()
}
// BunTxAdapter wraps a Bun transaction to implement the Database interface
type BunTxAdapter struct {
tx bun.Tx
}
func (b *BunTxAdapter) NewSelect() common.SelectQuery {
return &BunSelectQuery{
query: b.tx.NewSelect(),
db: b.tx,
}
}
func (b *BunTxAdapter) NewInsert() common.InsertQuery {
return &BunInsertQuery{query: b.tx.NewInsert()}
}
func (b *BunTxAdapter) NewUpdate() common.UpdateQuery {
return &BunUpdateQuery{query: b.tx.NewUpdate()}
}
func (b *BunTxAdapter) NewDelete() common.DeleteQuery {
return &BunDeleteQuery{query: b.tx.NewDelete()}
}
func (b *BunTxAdapter) Exec(ctx context.Context, query string, args ...interface{}) (common.Result, error) {
result, err := b.tx.ExecContext(ctx, query, args...)
return &BunResult{result: result}, err
}
func (b *BunTxAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
return b.tx.NewRaw(query, args...).Scan(ctx, dest)
}
func (b *BunTxAdapter) BeginTx(ctx context.Context) (common.Database, error) {
return nil, fmt.Errorf("nested transactions not supported")
}
func (b *BunTxAdapter) CommitTx(ctx context.Context) error {
return b.tx.Commit()
}
func (b *BunTxAdapter) RollbackTx(ctx context.Context) error {
return b.tx.Rollback()
}
func (b *BunTxAdapter) RunInTransaction(ctx context.Context, fn func(common.Database) error) error {
return fn(b) // Already in transaction
}