package postgres import ( "database/sql" "encoding/json" "fmt" "strings" "git.gobha.me/xcaliber/chat-switchboard/store" ) // DB is the shared database connection pool. // Set during initialization via SetDB. var DB *sql.DB // SetDB configures the shared database connection for all stores. func SetDB(db *sql.DB) { DB = db } // ── Dynamic SQL Builder ───────────────────── // Replaces the copy-pasted addClause/addField pattern // found in admin.go, presets.go, team_providers.go, apiconfigs.go. // UpdateBuilder constructs a dynamic UPDATE statement. type UpdateBuilder struct { table string sets []string args []interface{} where []string argIdx int } // NewUpdate creates an UpdateBuilder for the given table. func NewUpdate(table string) *UpdateBuilder { return &UpdateBuilder{table: table} } // Set adds a column=value pair to the UPDATE. func (b *UpdateBuilder) Set(col string, val interface{}) *UpdateBuilder { b.argIdx++ b.sets = append(b.sets, fmt.Sprintf("%s = $%d", col, b.argIdx)) b.args = append(b.args, val) return b } // SetJSON adds a JSONB column from a map. func (b *UpdateBuilder) SetJSON(col string, val interface{}) *UpdateBuilder { data, err := json.Marshal(val) if err != nil { data = []byte("{}") } return b.Set(col, string(data)) } // SetIf conditionally adds a column if the pointer is non-nil. func (b *UpdateBuilder) SetIf(col string, val interface{}, set bool) *UpdateBuilder { if set { return b.Set(col, val) } return b } // Where adds a WHERE condition. func (b *UpdateBuilder) Where(col string, val interface{}) *UpdateBuilder { b.argIdx++ b.where = append(b.where, fmt.Sprintf("%s = $%d", col, b.argIdx)) b.args = append(b.args, val) return b } // HasSets returns true if any SET clauses were added. func (b *UpdateBuilder) HasSets() bool { return len(b.sets) > 0 } // Build returns the SQL string and args. func (b *UpdateBuilder) Build() (string, []interface{}) { sql := fmt.Sprintf("UPDATE %s SET %s", b.table, strings.Join(b.sets, ", ")) if len(b.where) > 0 { sql += " WHERE " + strings.Join(b.where, " AND ") } return sql, b.args } // Exec executes the built UPDATE. func (b *UpdateBuilder) Exec(db *sql.DB) (sql.Result, error) { q, args := b.Build() return db.Exec(q, args...) } // ── Query Builder ─────────────────────────── // SelectBuilder constructs a dynamic SELECT statement. type SelectBuilder struct { cols string table string joins []string where []string args []interface{} orderBy string limit int offset int argIdx int } // NewSelect creates a SelectBuilder. func NewSelect(cols, table string) *SelectBuilder { return &SelectBuilder{cols: cols, table: table} } // Join adds a JOIN clause. func (b *SelectBuilder) Join(join string) *SelectBuilder { b.joins = append(b.joins, join) return b } // Where adds a WHERE condition with a parameter. func (b *SelectBuilder) Where(clause string, args ...interface{}) *SelectBuilder { for _, arg := range args { b.argIdx++ clause = strings.Replace(clause, "?", fmt.Sprintf("$%d", b.argIdx), 1) b.args = append(b.args, arg) } b.where = append(b.where, clause) return b } // WhereRaw adds a WHERE condition without parameters. func (b *SelectBuilder) WhereRaw(clause string) *SelectBuilder { b.where = append(b.where, clause) return b } // OrderBy sets the ORDER BY clause. func (b *SelectBuilder) OrderBy(col, order string) *SelectBuilder { if order == "" { order = "DESC" } b.orderBy = fmt.Sprintf("%s %s", col, strings.ToUpper(order)) return b } // Paginate sets LIMIT and OFFSET from ListOptions. func (b *SelectBuilder) Paginate(opts store.ListOptions) *SelectBuilder { if opts.Limit > 0 { b.limit = opts.Limit } if opts.Offset > 0 { b.offset = opts.Offset } if opts.Sort != "" { b.OrderBy(opts.Sort, opts.Order) } return b } // Build returns the SQL string and args. func (b *SelectBuilder) Build() (string, []interface{}) { q := fmt.Sprintf("SELECT %s FROM %s", b.cols, b.table) for _, j := range b.joins { q += " " + j } if len(b.where) > 0 { q += " WHERE " + strings.Join(b.where, " AND ") } if b.orderBy != "" { q += " ORDER BY " + b.orderBy } if b.limit > 0 { q += fmt.Sprintf(" LIMIT %d", b.limit) } if b.offset > 0 { q += fmt.Sprintf(" OFFSET %d", b.offset) } return q, b.args } // CountBuild returns a SELECT COUNT(*) version of the query (no order/limit). func (b *SelectBuilder) CountBuild() (string, []interface{}) { q := fmt.Sprintf("SELECT COUNT(*) FROM %s", b.table) for _, j := range b.joins { q += " " + j } if len(b.where) > 0 { q += " WHERE " + strings.Join(b.where, " AND ") } return q, b.args } // ── JSONB Helpers ─────────────────────────── // ToJSON marshals a value to JSON bytes for JSONB columns. func ToJSON(v interface{}) []byte { if v == nil { return []byte("{}") } b, err := json.Marshal(v) if err != nil { return []byte("{}") } return b } // ScanJSON scans a JSONB column into a target. func ScanJSON(src interface{}, dst interface{}) error { if src == nil { return nil } var data []byte switch v := src.(type) { case []byte: data = v case string: data = []byte(v) default: return fmt.Errorf("unsupported JSONB type: %T", src) } return json.Unmarshal(data, dst) } // NullableString returns the string value or empty string from sql.NullString. func NullableString(ns sql.NullString) string { if ns.Valid { return ns.String } return "" } // NullableStringPtr returns a *string from sql.NullString (nil if not valid). func NullableStringPtr(ns sql.NullString) *string { if ns.Valid { return &ns.String } return nil } // NullableFloat64Ptr returns a *float64 from sql.NullFloat64. func NullableFloat64Ptr(nf sql.NullFloat64) *float64 { if nf.Valid { return &nf.Float64 } return nil } // NullableIntPtr returns an *int from sql.NullInt64. func NullableIntPtr(ni sql.NullInt64) *int { if ni.Valid { v := int(ni.Int64) return &v } return nil }