Skip to content

feat(psql): multiple FROM/USING sources, join chains, and table functions#690

Merged
stephenafamo merged 1 commit into
stephenafamo:mainfrom
atzedus:feat/multiple-from-using-psql
May 21, 2026
Merged

feat(psql): multiple FROM/USING sources, join chains, and table functions#690
stephenafamo merged 1 commit into
stephenafamo:mainfrom
atzedus:feat/multiple-from-using-psql

Conversation

@atzedus
Copy link
Copy Markdown
Contributor

@atzedus atzedus commented May 21, 2026

This is a reworked version resulting from #689

Extends the PostgreSQL query builder for UPDATE ... FROM and DELETE ... USING so multiple comma-separated from_item sources are supported, with inline and standalone joins, table functions, and matching bobgen-psql codegen.

UPDATE / DELETE

  • Additional FROM / USING sources live in UpdateQuery.FromItems and DeleteQuery.UsingItems (not the embedded TableRef on the query struct).
  • The update/delete target table is set with um.Table(...) / dm.From(...); extra sources use um.From(...) / dm.Using(...).
  • Each um.From(...) or dm.Using(...) call appends one comma-separated from_item (repeated calls build a, b, (c JOIN d)).
  • um.From(table, joins...) and dm.Using(table, joins...) accept optional JoinChain arguments (InnerJoin, LeftJoin, CrossJoin, etc.) on that source.
  • Standalone join mods (um.InnerJoin, dm.LeftJoin, …) attach to the last from_item when FromItems / UsingItems is non-empty.
  • When a from_item has joins and more items follow, the writer parenthesizes it (PostgreSQL: comma binds looser than JOIN).

Table functions

  • um.FromFunction, dm.UsingFunction, and sm.FromFunction return bob.Expression (single function or ROWS FROM (...) for multiple).
  • Pass the expression into um.From / dm.Using / sm.From; aliasing and table modifiers stay on the From / Using chain (.As(...), .Only(), etc.).

SELECT (related)

  • sm.FromFunction no longer returns FromChain or applies FROM by itself — use sm.From(sm.FromFunction(...)).
  • sm.From uses AppendTableRef (replace semantics); fixes stale alias when replacing FROM without a new alias.
  • Join helpers (.On(), .Using(), .Natural(), …) return JoinChain for fluent chaining instead of bob.Mod.
  • CrossJoin is unified under JoinChain (no separate CrossJoinChain).

Codegen

  • bobgen-psql parses multiple UPDATE ... FROM / DELETE ... USING items and emits AppendTableRef(...) per item; column resolution considers every source.

Breaking changes

  • UPDATE/DELETE extra sources moved from embedded clause.TableRef to FromItems / UsingItems; use um.Table / dm.From for the target table.
  • um.FromFunction / sm.FromFunction return bob.Expression and must be wrapped in From / Using.
  • um / dm join helpers return JoinChain[*UpdateQuery] / JoinChain[*DeleteQuery]; .On(), .Using(), etc. return JoinChain instead of bob.Mod.

Examples

// Multiple FROM sources
psql.Update(
    um.Table("employees"),
    um.SetCol("sales_count").To("sales_count + 1"),
    um.From("accounts"),
    um.From("departments"),
)
// UPDATE employees SET "sales_count" = sales_count + 1 FROM accounts, departments

// Inline join on a from_item
psql.Update(
    um.Table("employees"),
    um.SetCol("sales_count").To("sales_count + 1"),
    um.From("accounts",
        um.InnerJoin("departments").OnEQ(
            psql.Quote("accounts", "dept_id"),
            psql.Quote("departments", "id"),
        ),
    ),
)
// UPDATE employees SET "sales_count" = sales_count + 1 FROM accounts
//   INNER JOIN departments ON (accounts.dept_id = departments.id)

// Standalone join on the last from_item (parentheses when needed)
psql.Update(
    um.Table("employees"),
    um.SetCol("sales_count").To("sales_count + 1"),
    um.From("accounts"),
    um.From("departments"),
    um.InnerJoin("regions").OnEQ(
        psql.Quote("departments", "id"),
        psql.Quote("regions", "dept_id"),
    ),
)
// UPDATE employees SET "sales_count" = sales_count + 1 FROM accounts, (departments
//   INNER JOIN regions ON (departments.id = regions.dept_id))

// Table function
psql.Update(
    um.Table("employees"),
    um.SetCol("sales_count").To("sales_count + 1"),
    um.From(um.FromFunction(psql.F("generate_series", 1, 3)())),
)
// UPDATE employees SET "sales_count" = sales_count + 1 FROM generate_series(1, 3)

// Multiple USING sources
psql.Delete(
    dm.From("employees"),
    dm.Using("accounts"),
    dm.Using("departments", dm.CrossJoin("regions")),
    dm.Where(psql.Quote("employees", "id").EQ(psql.Arg(1))),
)
// DELETE FROM employees USING accounts, (departments CROSS JOIN regions)
//   WHERE (employees.id = $1)

// SELECT: FromFunction via sm.From
psql.Select(
    sm.Columns("p"),
    sm.From(sm.FromFunction(psql.F("generate_series", 1, 3)())),
)
// SELECT p FROM generate_series(1, 3)

@stephenafamo
Copy link
Copy Markdown
Owner

This is a fantastic PR. Thank you.

Seems there are a few lint issues to fix though

…e typing

Support variadic FROM/USING with inline joins, table functions, JoinChain[Q Joinable],
and AppendJoin on the last from_item for UPDATE/DELETE queries.
@atzedus atzedus force-pushed the feat/multiple-from-using-psql branch from e9f7070 to f91d49b Compare May 21, 2026 11:41
@atzedus
Copy link
Copy Markdown
Contributor Author

atzedus commented May 21, 2026

Seems there are a few lint issues to fix though

Done.

I think this will be my last PR for a while, now we just wait for the release :)

@stephenafamo stephenafamo merged commit eaf2ac4 into stephenafamo:main May 21, 2026
8 checks passed
@stephenafamo
Copy link
Copy Markdown
Owner

Released v0.44.0

@atzedus atzedus deleted the feat/multiple-from-using-psql branch May 21, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants