Skip to content

Statically link postgres calls for tree shaking - #10

Closed
sundbp wants to merge 1 commit into
jolt-lang:mainfrom
sundbp:avoid-resolve
Closed

Statically link postgres calls for tree shaking#10
sundbp wants to merge 1 commit into
jolt-lang:mainfrom
sundbp:avoid-resolve

Conversation

@sundbp

@sundbp sundbp commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem

jdbc.core dynamically loaded the PostgreSQL driver and resolved its functions by name:

(require '[db.pg])
(deref (resolve (symbol "db.pg" n)))

A reachable call to clojure.core/resolve prevents Jolt from constructing a sound whole-program call graph. As a result, jolt build --tree-shake skipped tree shaking for any application that included jdbc.core, including SQLite-only applications.

Runtime require is also unsuitable as the only dependency edge for an AOT binary: Jolt assembles the transitive static require graph at build time, and the resulting executable has no source roots from which to load db.pg later.

Change

  • Statically require db.pg through the pg namespace alias.
  • Replace the runtime require/resolve dispatch with direct calls to:
    • pg/connect
    • pg/close
    • pg/exec
    • pg/all
  • Remove the pgfn helper and its declaration.

This gives Jolt a static dependency edge to the PostgreSQL implementation and allows tree shaking to proceed.

SQLite applications still do not require libpq at runtime. The native library remains optional, and Jolt's FFI bindings resolve libpq symbols lazily when a PostgreSQL operation is first invoked.

Why there are no test changes

The public JDBC API and its runtime behavior are unchanged; this is a build-time reachability and linking change.

The existing suite already exercises:

  • loading jdbc.core;
  • SQLite connections and queries;
  • CRUD and transaction behavior;
  • PostgreSQL placeholder rewriting;
  • PostgreSQL integration when JOLT_TEST_PG_URI is provided.

A new unit test would either duplicate those behavioral tests or assert implementation details such as the absence of resolve. The relevant regression can only be demonstrated meaningfully by running an actual tree-shaken build, so verification uses jolt build --tree-shake rather than a source-text assertion.

Verification

  • jolt -M:test
    • all existing checks passed
  • Built a standalone SQLite probe with jolt build --tree-shake
    • tree-shake kept 339 of 763 defs
    • dropping compiler image (no runtime eval)
  • Ran the resulting executable
    • returned {:name "ada"}
  • The probe declared an intentionally nonexistent optional libpq library
    • the SQLite build and runtime scenario still succeeded

Replace jdbc.core runtime require/resolve dispatch with a static db.pg alias. This gives Jolt a sound AOT dependency edge while optional libpq symbols remain lazily resolved.
yogthos added a commit that referenced this pull request Aug 11, 2026
Adopted from #10 by sundbp, extended to the java.sql shim and resolved against
main, which had moved underneath it.

jdbc.core reached the postgres driver through (resolve (symbol "db.pg" n)) behind
a runtime require. A reachable call to resolve stops jolt from building a sound
whole-program call graph, so jolt build --tree-shake skipped shaking entirely for
any program that loaded jdbc.core, sqlite-only ones included. A runtime require is
also the wrong edge for an AOT binary, which has no source roots to load db.pg
from later.

The reason that indirection existed was that a compile-time db.pg/foo reference
used to be read as a host class. That is no longer so, and the namespace alias
works, so both callers now require db.pg statically and call pg/connect, pg/close,
pg/exec, pg/all and pg/all-raw directly.

db.jdbc-shim had grown its own copy of the same pgfn trick, which matters more
than the jdbc.core one: jdbc.core is on its way out when the switchover lands,
while the shim is what stays. Fixing only the file in the PR would have carried
the defect into the new architecture.

A sqlite-only app still does not need libpq. Loading db.pg only declares the
bindings; ffi/defcfn resolves a symbol on first call. Checked against a project
whose deps.edn points :jolt/native at a libpq that does not exist: it builds, runs
and returns rows.

Verified the actual claim rather than the diff. Against main, jolt build
--tree-shake reports "tree-shake skipped (reachable code resolves vars at
runtime)". With this, it reports "tree-shake kept 336 of 764 defs (core 237/659)"
and the resulting binary runs.

Co-authored-by: Yogthos <yogthos@gmail.com>
Co-authored-by: sundbp <sundbp@users.noreply.github.com>
@yogthos

yogthos commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Adopted in #11, now merged as 9955782. Thanks for this, and for the diagnosis — the tree-shake reasoning was the useful part and it was right.

Two things meant landing it as a separate commit rather than merging this one directly. main had moved underneath it: pg-placeholders now lives in db.pg and is applied inside db.pg/run, so pg-eval no longer rewrites, and execute! returns affected rows on postgres, which is what the conflict was.

The more substantive one is that db.jdbc-shim had since grown its own copy of the same pgfn trick. That copy matters more than the jdbc.core one, because jdbc.core is on its way out when the clojure.jdbc switchover lands while the shim is what stays, so adopting this as written would have fixed the file being deleted and left the defect in the file that survives. Both are static now.

I also checked the two claims directly rather than the diff, since both are build-time properties no unit test covers. Before, on main, jolt build --tree-shake reports tree-shake skipped (reachable code resolves vars at runtime); after, tree-shake kept 336 of 764 defs (core 237/659) and the binary runs. And for the sqlite-only case, a project pointing :jolt/native at a libpq that does not exist builds, runs and returns rows, which confirms the lazy-symbol-resolution reasoning in your description.

You are credited as co-author on the commit. Closing this in favour of #11.

@yogthos yogthos closed this Aug 11, 2026
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