Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ea9d2cb
feat(psql): make derived select queries immutable
jay-babu Apr 20, 2026
bb27f9f
refactor(psql): route select with through immutable queries
jay-babu Apr 20, 2026
2eb36df
refactor(psql): hide immutable query internals
jay-babu Apr 20, 2026
352c651
feat(psql): add immutable with paths for write queries
jay-babu Apr 20, 2026
230c162
test(psql): add with regression coverage
jay-babu Apr 20, 2026
835cb44
test(psql): use test contexts in query tests
jay-babu Apr 20, 2026
e8aafe0
refactor(psql): make queries immutable by default
jay-babu Apr 21, 2026
d81d2a7
refactor(psql): drop materialized query cache
jay-babu Apr 21, 2026
d07fed2
refactor(psql): simplify immutable view state
jay-babu Apr 21, 2026
ac889e8
refactor(psql): remove view wrapper fallback state
jay-babu Apr 21, 2026
c967589
refactor(psql): drop wrapper base query helpers
jay-babu Apr 21, 2026
6737d42
refactor(psql): remove table returning contextual mods
jay-babu Apr 21, 2026
5345cd6
feat(psql): support native immutable combined selects
jay-babu Apr 21, 2026
9c7f391
refactor(orm): make table queries immutable by default
jay-babu Apr 21, 2026
f69f122
refactor(psql): run view hooks on immutable queries
jay-babu Apr 21, 2026
cb9440e
refactor(psql): drop dead immutable contextual state
jay-babu Apr 21, 2026
29f2602
refactor(psql): remove dead immutable select fallback
jay-babu Apr 21, 2026
d16c25a
refactor(psql): support common immutable select mods
jay-babu Apr 21, 2026
0a91ca3
refactor(psql): support common immutable write mods
jay-babu Apr 21, 2026
faf981f
refactor(psql): build table queries directly
jay-babu Apr 21, 2026
8f279d9
refactor(psql): localize table returning behavior
jay-babu Apr 21, 2026
5e0b358
perf(psql): build common queries immutably
jay-babu Apr 21, 2026
831ca50
refactor(query): make base queries immutable by default
jay-babu Apr 21, 2026
aa059fe
refactor(psql): add clone support for dialect queries
jay-babu Apr 21, 2026
2710d2b
refactor(psql): use a single immutable query core
jay-babu Apr 21, 2026
a64a500
refactor(psql): simplify table returning defaults
jay-babu Apr 21, 2026
744a5b1
refactor(psql): move derivation into dialect queries
jay-babu Apr 21, 2026
d9deace
refactor(psql): reduce target table mod glue
jay-babu Apr 21, 2026
5caedca
refactor(psql): simplify update derivation
jay-babu Apr 21, 2026
6446a55
refactor(psql): simplify view query wrapper
jay-babu Apr 21, 2026
a8ea6d8
refactor(psql): simplify table returning behavior
jay-babu Apr 21, 2026
46ec3ae
fix(psql): address immutable query review feedback
jay-babu Apr 21, 2026
d6e79df
refactor: remove query With methods
jay-babu May 3, 2026
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
25 changes: 14 additions & 11 deletions dialect/mysql/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ func (t *Table[T, Tslice, Tset, C]) Insert(queryMods ...bob.Mod[*dialect.InsertQ
table: t,
}

q.Apply(queryMods...)

return q
return q.Apply(queryMods...)
}

// Starts an update query for this table
Expand All @@ -104,9 +102,7 @@ func (t *Table[T, Tslice, Tset, C]) Update(queryMods ...bob.Mod[*dialect.UpdateQ
BaseQuery: Update(um.Table(t.NameAs())),
Hooks: &t.UpdateQueryHooks,
}
q.Apply(queryMods...)

return q
return q.Apply(queryMods...)
}

// Starts a delete query for this table
Expand All @@ -116,16 +112,24 @@ func (t *Table[T, Tslice, Tset, C]) Delete(queryMods ...bob.Mod[*dialect.DeleteQ
Hooks: &t.DeleteQueryHooks,
}

q.Apply(queryMods...)

return q
return q.Apply(queryMods...)
}

type insertQuery[T any, Ts ~[]T, Tset setter[T], C bob.Expression] struct {
orm.ExecQuery[*dialect.InsertQuery]
table *Table[T, Ts, Tset, C]
}

func (t *insertQuery[T, Ts, Tset, C]) Apply(queryMods ...bob.Mod[*dialect.InsertQuery]) *insertQuery[T, Ts, Tset, C] {
if t == nil {
return nil
}

next := *t
next.ExecQuery = *t.ExecQuery.Apply(queryMods...)
return &next
}

// Insert One Row
// NOTE: Because MySQL does not support RETURNING, this will insert the row and then run a SELECT query
// to retrieve the row.
Expand Down Expand Up @@ -287,8 +291,7 @@ func (t *insertQuery[T, Tslice, Tset, C]) getInserted(vals []clause.Value, resul
filters = append(filters, Group(t.table.uniqueColNames(i)...).In(args...))
}

query.Apply(sm.Where(Or(filters...)))

query = query.Apply(sm.Where(Or(filters...)))
return query, nil
}

Expand Down
6 changes: 3 additions & 3 deletions dialect/mysql/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ func (v *View[T, Tslice, C]) Query(queryMods ...bob.Mod[*dialect.SelectQuery]) *
},
)

q.Apply(queryMods...)

return q
next := *q
next.Query = *q.Query.Apply(queryMods...)
return &next
}

type ViewQuery[T any, Ts ~[]T] struct {
Expand Down
25 changes: 20 additions & 5 deletions dialect/psql/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@ import (
"github.com/stephenafamo/bob/dialect/psql/dialect"
)

func Delete(queryMods ...bob.Mod[*dialect.DeleteQuery]) bob.BaseQuery[*dialect.DeleteQuery] {
type DeleteQuery struct {
bob.BaseQuery[*dialect.DeleteQuery]
}

func (q DeleteQuery) Apply(queryMods ...bob.Mod[*dialect.DeleteQuery]) DeleteQuery {
if next, ok := q.Expression.Derive(queryMods...); ok {
q.Expression = next
return q
}
q.BaseQuery = q.BaseQuery.Apply(queryMods...)
return q
}

func Delete(queryMods ...bob.Mod[*dialect.DeleteQuery]) DeleteQuery {
q := &dialect.DeleteQuery{}
for _, mod := range queryMods {
mod.Apply(q)
}

return bob.BaseQuery[*dialect.DeleteQuery]{
Expression: q,
Dialect: dialect.Dialect,
QueryType: bob.QueryTypeDelete,
return DeleteQuery{
BaseQuery: bob.BaseQuery[*dialect.DeleteQuery]{
Expression: q,
Dialect: dialect.Dialect,
QueryType: bob.QueryTypeDelete,
},
}
}
273 changes: 273 additions & 0 deletions dialect/psql/dialect/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package dialect

import (
"context"

"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/clause"
)

func cloneAnySlice(values []any) []any {
if values == nil {
return nil
}
return append(make([]any, 0, len(values)), values...)
}

func cloneStringSlice(values []string) []string {
if values == nil {
return nil
}
return append([]string(nil), values...)
}

func cloneExpressionSlice(values []bob.Expression) []bob.Expression {
if values == nil {
return nil
}
return append([]bob.Expression(nil), values...)
}

func cloneWith(with clause.With) clause.With {
return clause.With{
Recursive: with.Recursive,
CTEs: cloneExpressionSlice(with.CTEs),
}
}

func cloneSelectList(list clause.SelectList) clause.SelectList {
return clause.SelectList{
Columns: cloneAnySlice(list.Columns),
PreloadColumns: cloneAnySlice(list.PreloadColumns),
}
}

func cloneWhere(where clause.Where) clause.Where {
return clause.Where{Conditions: cloneAnySlice(where.Conditions)}
}

func cloneGroupBy(groupBy clause.GroupBy) clause.GroupBy {
return clause.GroupBy{
Groups: cloneAnySlice(groupBy.Groups),
Distinct: groupBy.Distinct,
With: groupBy.With,
}
}

func cloneHaving(having clause.Having) clause.Having {
return clause.Having{Conditions: cloneAnySlice(having.Conditions)}
}

func cloneWindows(windows clause.Windows) clause.Windows {
return clause.Windows{Windows: cloneExpressionSlice(windows.Windows)}
}

func cloneOrderBy(orderBy clause.OrderBy) clause.OrderBy {
return clause.OrderBy{Expressions: cloneExpressionSlice(orderBy.Expressions)}
}

func cloneLocks(locks clause.Locks) clause.Locks {
return clause.Locks{Locks: cloneExpressionSlice(locks.Locks)}
}

func cloneLimit(limit clause.Limit) clause.Limit {
return clause.Limit{Count: limit.Count}
}

func cloneOffset(offset clause.Offset) clause.Offset {
return clause.Offset{Count: offset.Count}
}

func cloneFetch(fetch clause.Fetch) clause.Fetch {
return clause.Fetch{
Count: fetch.Count,
WithTies: fetch.WithTies,
}
}

func cloneReturning(returning clause.Returning) clause.Returning {
return clause.Returning{Expressions: cloneAnySlice(returning.Expressions)}
}

func cloneSet(set clause.Set) clause.Set {
return clause.Set{Set: cloneAnySlice(set.Set)}
}

func cloneConflict(conflict clause.Conflict) clause.Conflict {
return clause.Conflict{Expression: conflict.Expression}
}

func cloneValues(values clause.Values) clause.Values {
cloned := clause.Values{
Query: values.Query,
Vals: make([]clause.Value, 0, len(values.Vals)),
}

for _, row := range values.Vals {
cloned.Vals = append(cloned.Vals, append(clause.Value(nil), row...))
}

return cloned
}

func cloneCombines(combines clause.Combines) clause.Combines {
if combines.Queries == nil {
return clause.Combines{}
}

queries := make([]clause.Combine, 0, len(combines.Queries))
for _, combine := range combines.Queries {
queries = append(queries, clause.Combine{
Strategy: combine.Strategy,
Query: combine.Query,
All: combine.All,
})
}

return clause.Combines{Queries: queries}
}

func cloneTableRef(ref clause.TableRef) clause.TableRef {
var indexedBy *string
if ref.IndexedBy != nil {
indexed := *ref.IndexedBy
indexedBy = &indexed
}

indexHints := make([]clause.IndexHint, 0, len(ref.IndexHints))
for _, hint := range ref.IndexHints {
indexHints = append(indexHints, clause.IndexHint{
Type: hint.Type,
Indexes: cloneStringSlice(hint.Indexes),
For: hint.For,
})
}

joins := make([]clause.Join, 0, len(ref.Joins))
for _, join := range ref.Joins {
joins = append(joins, clause.Join{
Type: join.Type,
Natural: join.Natural,
To: cloneTableRef(join.To),
On: cloneExpressionSlice(join.On),
Using: cloneStringSlice(join.Using),
})
}

return clause.TableRef{
Expression: ref.Expression,
Alias: ref.Alias,
Columns: cloneStringSlice(ref.Columns),
Only: ref.Only,
Lateral: ref.Lateral,
WithOrdinality: ref.WithOrdinality,
IndexedBy: indexedBy,
Partitions: cloneStringSlice(ref.Partitions),
IndexHints: indexHints,
Joins: joins,
}
}

func cloneLoad(load bob.Load) bob.Load {
var cloned bob.Load
cloned.SetLoaders(load.GetLoaders()...)
cloned.SetMapperMods(load.GetMapperMods()...)
return cloned
}

func cloneEmbeddedHook(hook bob.EmbeddedHook) bob.EmbeddedHook {
return bob.EmbeddedHook{
Hooks: append([]func(context.Context, bob.Executor) (context.Context, error){}, hook.Hooks...),
}
}

func cloneContextualModdable[T any](mods bob.ContextualModdable[T]) bob.ContextualModdable[T] {
return bob.ContextualModdable[T]{
Mods: append([]bob.ContextualMod[T](nil), mods.Mods...),
}
}

func (s *SelectQuery) Clone() *SelectQuery {
if s == nil {
return nil
}

return &SelectQuery{
With: cloneWith(s.With),
SelectList: cloneSelectList(s.SelectList),
Distinct: Distinct{On: cloneAnySlice(s.Distinct.On)},
TableRef: cloneTableRef(s.TableRef),
Where: cloneWhere(s.Where),
GroupBy: cloneGroupBy(s.GroupBy),
Having: cloneHaving(s.Having),
Windows: cloneWindows(s.Windows),
Combines: cloneCombines(s.Combines),
OrderBy: cloneOrderBy(s.OrderBy),
Limit: cloneLimit(s.Limit),
Offset: cloneOffset(s.Offset),
Fetch: cloneFetch(s.Fetch),
Locks: cloneLocks(s.Locks),
Load: cloneLoad(s.Load),
EmbeddedHook: cloneEmbeddedHook(s.EmbeddedHook),
ContextualModdable: cloneContextualModdable(s.ContextualModdable),
CombinedOrder: cloneOrderBy(s.CombinedOrder),
CombinedLimit: cloneLimit(s.CombinedLimit),
CombinedFetch: cloneFetch(s.CombinedFetch),
CombinedOffset: cloneOffset(s.CombinedOffset),
}
}

func (u *UpdateQuery) Clone() *UpdateQuery {
if u == nil {
return nil
}

return &UpdateQuery{
With: cloneWith(u.With),
Only: u.Only,
Table: cloneTableRef(u.Table),
Set: cloneSet(u.Set),
TableRef: cloneTableRef(u.TableRef),
Where: cloneWhere(u.Where),
Returning: cloneReturning(u.Returning),
Load: cloneLoad(u.Load),
EmbeddedHook: cloneEmbeddedHook(u.EmbeddedHook),
ContextualModdable: cloneContextualModdable(u.ContextualModdable),
}
}

func (d *DeleteQuery) Clone() *DeleteQuery {
if d == nil {
return nil
}

return &DeleteQuery{
With: cloneWith(d.With),
Only: d.Only,
Table: cloneTableRef(d.Table),
TableRef: cloneTableRef(d.TableRef),
Where: cloneWhere(d.Where),
Returning: cloneReturning(d.Returning),
Load: cloneLoad(d.Load),
EmbeddedHook: cloneEmbeddedHook(d.EmbeddedHook),
ContextualModdable: cloneContextualModdable(d.ContextualModdable),
}
}

func (i *InsertQuery) Clone() *InsertQuery {
if i == nil {
return nil
}

return &InsertQuery{
With: cloneWith(i.With),
Overriding: i.Overriding,
TableRef: cloneTableRef(i.TableRef),
Values: cloneValues(i.Values),
Conflict: cloneConflict(i.Conflict),
Returning: cloneReturning(i.Returning),
Load: cloneLoad(i.Load),
EmbeddedHook: cloneEmbeddedHook(i.EmbeddedHook),
ContextualModdable: cloneContextualModdable(i.ContextualModdable),
}
}
Loading