This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/postgres/helpers.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

260 lines
6.2 KiB
Go

package postgres
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"switchboard-core/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, personas.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
}
// SetNull adds a col = NULL pair to the UPDATE.
func (b *UpdateBuilder) SetNull(col string) *UpdateBuilder {
b.sets = append(b.sets, fmt.Sprintf("%s = NULL", col))
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
}