Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions v2/dbutils/bob_helpers/bob_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import (
"database/sql"
"reflect"
"time"

"github.com/aarondl/opt"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql"
"github.com/stephenafamo/bob/dialect/psql/dialect"
"github.com/stephenafamo/bob/dialect/psql/sm"
"github.com/stephenafamo/bob/expr"
"github.com/stephenafamo/scan"
)

// Ptr returns a pointer to the given value.
Expand Down Expand Up @@ -57,3 +65,97 @@ func isZero[T any](v T) bool {
var zero T
return reflect.DeepEqual(v, zero)
}

// IncludeSubqueryAsCTE appends a subquery as a Common Table Expression (CTE)
// to the provided query modifiers slice.
//
// This helper simplifies adding a named subquery (`WITH <alias> AS (...)`) to a
// `bob`-built query, allowing you to keep query composition modular.
//
// Example:
//
// var mods []bob.Mod[*dialect.SelectQuery]
// sub := []bob.Mod[*dialect.SelectQuery]{
// psql.From("users").Where(psql.Col("active").Eq(true)),
// }
// IncludeSubqueryAsCTE(&mods, sub, "active_users")
//
// Generates:
//
// WITH active_users AS (
// SELECT * FROM users WHERE active = true
// )
//
// Parameters:
// - q: pointer to the main query modifiers slice to be extended.
// - subQuery: slice of modifiers defining the subquery to include.
// - alias: name of the CTE (the identifier after WITH).
func IncludeSubqueryAsCTE(q *[]bob.Mod[*dialect.SelectQuery], subQuery []bob.Mod[*dialect.SelectQuery], alias string) {
sub := psql.Select(
subQuery...,
)
*q = append(*q,
sm.With(alias).As(sub),
)
}

// TableWithPrefix prefixes all columns in the given ColumnsExpr with the table alias.
// Equivalent to using `--prefix:<alias>.` in bob SQL files.
func TableWithPrefix(alias string, col expr.ColumnsExpr) expr.ColumnsExpr {
return col.WithPrefix(alias + ".")
}

// TableWithPrefixAndParent prefixes all columns and sets the parent to the alias.
// Useful for nested structs when mapping joined tables.
func TableWithPrefixAndParent(alias string, col expr.ColumnsExpr) expr.ColumnsExpr {
return col.WithPrefix(alias + ".").WithParent(alias)
}

// GroupByWithParent groups by columns and sets their parent to the given alias.
// Useful for grouping joined table columns in nested mappings.
func GroupByWithParent(alias string, col expr.ColumnsExpr) bob.Mod[*dialect.SelectQuery] {
return sm.GroupBy(col.WithParent(alias).DisableAlias())
}

// Scan is a helper function that creates a StructMapper with NullTypeConverter, so that NULL values are skipped during scanning.
// Example usage: bob.All(ctx, exec, psql.Select(query...), bob_helpers.Scan[BottomUpRow]())
func Scan[T any](opts ...scan.MappingOption) scan.Mapper[T] {
allOpts := append([]scan.MappingOption{scan.WithTypeConverter(nullTypeConverter{})}, opts...)
return scan.StructMapper[T](allOpts...)
}

// nullTypeConverter is a custom TypeConverter that skips NULL values during scanning even if the destination type does not support NULLs.
// This was stolen from https://github.com/stephenafamo/bob/blame/96da65fd88a50ae532079e8ea69746183f4af3a1/orm/load.go#L380
type nullTypeConverter struct{}

type wrapper struct {
IsNull bool
V any
}

// Scan implements the sql.Scanner interface. If the wrapped type implements
// sql.Scanner then it will call that.
func (v *wrapper) Scan(value any) error {
if value == nil {
v.IsNull = true
return nil
}

if scanner, ok := v.V.(sql.Scanner); ok {
return scanner.Scan(value)
}

return opt.ConvertAssign(v.V, value)
}

func (nullTypeConverter) TypeToDestination(typ reflect.Type) reflect.Value {
val := reflect.ValueOf(&wrapper{
V: reflect.New(typ).Interface(),
})

return val
}

func (nullTypeConverter) ValueFromDestination(val reflect.Value) reflect.Value {
return val.Elem().FieldByName("V").Elem().Elem()
}
13 changes: 6 additions & 7 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
module github.com/top-solution/go-libs/v2

go 1.24
go 1.24.0

require (
github.com/ardanlabs/conf/v3 v3.4.0
github.com/danielgtaylor/huma/v2 v2.34.1
github.com/goccy/go-yaml v1.15.19
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/lib/pq v1.10.7
github.com/lib/pq v1.10.9
github.com/lmittmann/tint v1.0.7
github.com/ory/ladon v1.3.0
github.com/pressly/goose/v3 v3.24.1
github.com/serjlee/frequency v1.1.0
github.com/stephenafamo/bob v0.30.0
github.com/stephenafamo/bob v0.41.1
github.com/stretchr/testify v1.10.0
github.com/volatiletech/sqlboiler/v4 v4.18.0
golang.org/x/text v0.25.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)

require (
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 // indirect
github.com/aarondl/opt v0.0.0-20250607033636-982744e1bd65 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/friendsofgo/errors v0.9.2 // indirect
Expand All @@ -32,8 +31,8 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/stephenafamo/scan v0.6.1 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/stephenafamo/scan v0.7.0 // indirect
github.com/volatiletech/inflect v0.0.1 // indirect
github.com/volatiletech/strmangle v0.0.6 // indirect
go.uber.org/multierr v1.11.0 // indirect
Expand Down
Loading