Contributing & Debugging
Debugging PormG Internals from a Consuming App
PormG uses an internal no-op macro, @pormg_debug, as a compile-time debug hook. In the published package it expands to nothing. This section explains how to activate real breakpoints when you need to investigate a bug that occurs inside PormG while running your own application.
Why @pormg_debug is a no-op by default
Julia macros are expanded at parse time, before the code runs. When PormG is loaded as a registered package, its .ji precompiled cache already contains every @pormg_debug call expanded to nothing. Redefining the macro at runtime cannot retroactively change those expansions.
The solution is to switch your app to use PormG from source, which forces Julia to re-parse every file and apply the new macro definition.
Step-by-step: activating breakpoints from your app
Requirements: Revise.jl and Infiltrator.jl must be available in your local Julia environment (they are not required by PormG itself).
# In your global environment (run once):
] add Revise Infiltrator1. Switch your app to the local PormG source
Open your app's Julia REPL and run:
] dev C:\path\to\PormG-new-features # Windows
] dev /path/to/PormG-new-features # Linux/macOSThis rewrites your app's Manifest.toml to point at the PormG source folder instead of the cached package. Julia will now compile PormG from source on every session.
2. Load Revise and Infiltrator before PormG
Order matters — Revise must be tracking PormG before it is loaded:
using Revise # must come first
using Infiltrator
using PormG # now loaded from source, tracked by Revise3. Re-wire @pormg_debug to real breakpoints
PormG.eval(:(macro pormg_debug(ex); :(Infiltrator.@infiltrate($(esc(ex)))); end))4. Mark the exact line you want to break on
Open the relevant PormG source file (e.g. src/querybuilder/execution_read.jl) and change the call site from:
@pormg_debug false # condition false → never firesto:
@pormg_debug true # condition true → always firesSave the file. Revise detects the change and re-parses the function with the new macro definition. The next call to that code path will drop you into the Infiltrator REPL.
5. Reproduce the bug in your app
# Example: investigating a query build issue on Result joins
using MyApp # or load your models directly
q = M.Result.objects
q.filter("driverid__nationality" => "Brazilian", "positionorder" => 1)
q.values("raceid__year", "raceid__name", "driverid__surname")
df = q |> DataFrame # breakpoint fires inside PormG hereInfiltrator drops you into an interactive REPL at the marked line, where you can inspect local variables, call functions, and step through the logic.
6. Restore your app to the registered PormG
When you are done debugging, switch back to the published version:
] free PormGThe @pormg_debug call sites in PormG source
All debug hook placements in the source use the pattern:
@pormg_debug false # change to `true` or a real condition to fireThe false condition means the hook is permanently silent by default. When re-wired to Infiltrator (steps above), change false to true or any expression that reflects the specific scenario you are investigating:
@pormg_debug variable_name == :unexpected_valueHow to contribute
- Fork the repository on GitHub and clone your fork locally.
- Create a feature branch:
git checkout -b my-feature - Make your changes, add tests under
test/integration/ortest/unit/. - Run the unit tests (no database required):
This is what CI runs, and thejulia -O0 --project=. -e 'using Pkg; Pkg.test()'-ematters: handing the test script straight tojulia --project=.does not work. TheLibPQ,Postgres,SQLiteandMySQLdrivers are weak dependencies, so the package environment never installs them;Pkg.test()resolves them from the test target instead.-O0is optional but worth it: the suite spends most of its time compiling code that mostly runs once, and skipping LLVM optimization makes the whole run about 40% faster.Pkg.test()passes the flag on to the process that runs the tests. - Run the integration tests (requires a live PostgreSQL, SQLite or MySQL database). Use
--project=test/integration— that environment carries the drivers:
That runs PostgreSQL through LibPQ, the default driver. Postgres.jl is in use too, so a change that reaches either driver's extension, or how results are decoded, runs the suite again through it, after the first run, never at the same time (the two reseed the same fixture):julia -t auto --project=test/integration test/integration/runtests.jl
One file at a time — unit or integration — uses the same environment:PORMG_POSTGRES_DRIVER=Postgres julia -t auto --project=test/integration test/integration/runtests.jl
First use in a fresh clone needs it instantiated once:julia --project=test/integration test/unit/test_order_by_nulls.jl
MySQL runs against a local MySQL 8.4 (a Dockerjulia --project=test/integration -e 'using Pkg; Pkg.instantiate()'mysql:8.4container is enough) and atest/integration/db_my/connection.ymlusingadapter: MySQL. The full suite runs there as on the other engines, and MySQL's own files run on their own, after it:PORMG_DB=db_my julia -t auto --project=test/integration test/integration/runtests.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_backend.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_reads.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_writes.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_bulk.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_migrations.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_alters.jl PORMG_DB=db_my julia -t auto --project=test/integration test/integration/test_mysql_inspectdb.jl - Submit a pull request against
main.
Testing conventions
- Every
@testsetblock must be commented: explain what is tested, the expected SQL, and why the behaviour matters. - Use the Formula 1 dataset models (
M.Driver,M.Result,M.Race, etc.) for all user-facing examples. GenericUser/Postexamples are not accepted in this codebase. - Use
show_query=:sqlto capture generated SQL in tests for assertion. - An integration test of an optional feature (a row of the backend capability table) asks
requires(:feature)fromtest/integration/common_setup.jl, never which engine it runs on. It returnstruewhen this run's backend has the feature. Otherwise it records the skip with its reason, and the test asserts theBackendCapabilityErrorinstead:if requires(:arrays) # … the feature, against the live database … else @test_throws PormG.BackendCapabilityError … end - Where a test differs by engine for any other reason, it names each engine and never falls through an
else, because a backend added later would inherit thatelsewithout a decision. An engine's own text (a plan, an error's fields, a placeholder) or how the test drives the engine is written asby_engine(postgres = …, sqlite = …, mysql = …). It raises an error for an engine with no row. A testset that exercises one engine's own machinery (its catalog, its server sessions) is guarded withonly_on(:postgres, "why"), which records the skip likerequiresdoes. A value is never keyed by engine: it is checked against an oracle on every engine.test/unit/test_engine_branches.jlcounts the by-name branches (isa PormGPostgres,PORMG_DB_FOLDER == …) left intest/integration/, so a new one fails until it is reviewed. - Avoid leaving
@pormg_debug truein committed code — always reset tofalse.