Skip to content

fix: number Preload and generated join aliases per query - #785

Open
xrn wants to merge 2 commits into
stephenafamo:mainfrom
xrn:feature/preload-stable-aliases
Open

xrn wants to merge 2 commits into
stephenafamo:mainfrom
xrn:feature/preload-stable-aliases

Conversation

@xrn

@xrn xrn commented Oct 21, 2026

Copy link
Copy Markdown

Description

Preload and the generated joins through another table (such as SelectJoins.Videos.InnerJoin.Tags) took the number in their table aliases from the process-wide bob.NextUniqueInt(). The same query was ... AS "users_10001" on one build and "users_10002" on the next, so drivers that cache prepared statements by their SQL, such as pgx, could never reuse the statement (#741).

The number now comes from the query, through a new (*bob.Load).NextUniqueInt(). Every query starts at 10001, so the aliases look exactly as they do today in a freshly started process, and the same query always gives the same SQL.

No breaking change. The Preloader type keeps its signature: a preloader works out its aliases when its mod is applied to the query, which is when the query's counter is available. Hand-written preloaders and code generated by the current version keep compiling. The only behaviour change is for code that calls a Preloader function directly and uses the returned mapper mod before applying the returned mod; nothing in bob does that.

For the generated joins, the join types now require the query to have NextUniqueInt(). Those types are internal to the generated package, and every query type they are used with already has it.

Measured with pgx v5.7.5

Same Preload query, 5000 executions on one connection, median of 3 runs:

before after
statements pgx prepared 5000 1
prepared statements held by the server 512 1
native pgx, PostgreSQL 16, µs/query 572 200
native pgx, PostgreSQL 18, µs/query 653 196
database/sql + pgx/stdlib, PostgreSQL 16, µs/query 835 282
pg_stat_statements entries on PostgreSQL 18 (1000 runs) 1000 1

It also fixes a failure that showed up while measuring: because the global counter only grows, a long-running process eventually gives aliases with 6+ digits, and with long table and column names the "alias.column" name of a preloaded column passes PostgreSQL's 63-byte limit. The query then fails with no destination for column ... although the same query worked when the process started.

Relation to #783

#783 fixes the same issue by passing an alias allocator through a new variadic parameter on Preloader, which is a breaking change. This PR gets the same result without it. I ran the #741-related scenarios from #783's tests, both its orm tests and its generated join tests, against this branch (adapted to the alias format): all pass. This PR is +178 / −16.

#783 also fixes two unrelated Preload bugs, both present on main: PreloadAs on a relationship through another table gives every side the same alias, and to_where columns are qualified with the parent table instead of the joined one. They are left out here to keep this PR to one problem; they would fit well in their own PR.

Related issue

Fixes #741

Checklist

  • Tests pass locally: go test -race ./... including the generated-code suites against PostgreSQL, MySQL and SQLite. The new tests fail without the change.
  • CHANGELOG.md is updated.
  • Documentation is updated: not needed, the aliases look the same as before.

Prepared with Claude Code; the measurements above were run locally.

RafRabenda and others added 2 commits October 6, 2026 12:34
Preload took the number in the alias of a joined table from the
process-wide bob.NextUniqueInt(), so the same query had different SQL
every time it was built. Drivers that cache prepared statements by
their SQL, such as pgx, could never reuse the statement of a query that
preloads, and on PostgreSQL 18 every execution added an entry to
pg_stat_statements. The aliases also got longer the longer the process
ran, until the "alias.column" name of a preloaded column passed
PostgreSQL's limit of 63 bytes and the query failed.

The number now comes from the query, through the new
(*bob.Load).NextUniqueInt. Since the alias depends on the query, a
preloader is only built when its mod is applied to the query.

Fixes stephenafamo#741

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Generated joins through another table, such as
SelectJoins.Videos.InnerJoin.Tags, took the number in the alias of the
table in between from the process-wide bob.NextUniqueInt(), so the same
query had different SQL every time it was built, the same problem as
Preload had. They now take it from the query, like Preload.

The join types now require the query to have NextUniqueInt(). They are
internal to the generated package, and every query type they are used
with has it through bob.Load.

Refs stephenafamo#741

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@xrn

xrn commented Oct 21, 2026

Copy link
Copy Markdown
Author

@stephenafamo sorry for bothering you again - but I would appreciate your review one more time

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.

NextUniqueInt in Preload does not play nice with pgx's statement caching

2 participants