Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7fee358
feat(gen): migrate to gen-contract v4.0.1 / gen-sdk v2.0.0, adopt Sdk…
nikita-volkov Jul 11, 2026
a5b4530
feat(gen): rewrite compile.dhall/Gen.dhall as Interpret.dhall/package…
nikita-volkov Jul 11, 2026
b13e427
refactor(gen): rename gen/ to src/, move tests/Exhaustive.dhall to de…
nikita-volkov Jul 11, 2026
2c9073f
refactor(gen): update path references from gen/ to src/, tests/Exhaus…
nikita-volkov Jul 11, 2026
a30ac32
fix(gen): update gen/ path references in the pytest harness to src/
nikita-volkov Jul 11, 2026
ffea69f
docs(gen): add Task 7 CHANGELOG entry, fix stale Config.dhall comment
nikita-volkov Jul 11, 2026
9c33a23
refactor(gen): drop dead Run type-alias bindings in Query/Result/Resu…
nikita-volkov Jul 11, 2026
15c2836
The plan
nikita-volkov Jul 11, 2026
81ea82c
Revert "The plan"
nikita-volkov Jul 11, 2026
7b3ef57
Fix
nikita-volkov Jul 12, 2026
7625550
Drop the plans
nikita-volkov Jul 12, 2026
bd42d46
Add plans
nikita-volkov Jul 12, 2026
c30e212
python.gen: rename emitSync config field to sync (no behavior change)
nikita-volkov Jul 12, 2026
be51390
python.gen: make sync/async mutually exclusive at unified output paths
nikita-volkov Jul 12, 2026
cf6b165
python.gen: add python-sync fixture artifact, drop emitSync from main…
nikita-volkov Jul 12, 2026
fe5f82b
python.gen: add tests/golden_sync, regenerate both golden trees for e…
nikita-volkov Jul 12, 2026
3008b70
python.gen: split sync-surface tests onto their own golden fixture
nikita-volkov Jul 12, 2026
5bab734
python.gen: fix config-variant sync detection for unified output paths
nikita-volkov Jul 12, 2026
2892802
python.gen: document the exclusive sync/async config change
nikita-volkov Jul 12, 2026
a0b527e
Add demos to fixtures task
nikita-volkov Jul 12, 2026
82bd4ad
Drop the task doc
nikita-volkov Jul 12, 2026
270eb44
Redistribute rows per statement and fix custom type codecs
nikita-volkov Jul 12, 2026
d94fa34
python.gen: rename demos/ to fixtures/
nikita-volkov Jul 12, 2026
7b876a3
Merge remote-tracking branch 'origin/update-architecture' into review…
slavashvets Jul 12, 2026
5147204
fix(gen): restore fail-loud custom type resolution
slavashvets Jul 12, 2026
50f74fa
fix(gen): isolate generated statement namespaces
slavashvets Jul 12, 2026
e46dc96
refactor(gen): share async and sync query modules
slavashvets Jul 12, 2026
375ba6f
test(gen): prove psycopg adapter-backed rows
slavashvets Jul 12, 2026
bac8f95
refactor(gen): register generated postgres value types
slavashvets Jul 12, 2026
3647a9c
refactor(gen): construct rows with psycopg row factories
slavashvets Jul 13, 2026
e7d1e6b
style(gen): emit ruff-clean python
slavashvets Jul 13, 2026
5ac6d49
test(gen): lock takeover-ready output contracts
slavashvets Jul 13, 2026
1a5de19
docs(gen): document takeover-ready clients
slavashvets Jul 13, 2026
df3831d
fix(gen): reject generated python name collisions
slavashvets Jul 13, 2026
ae2869c
docs(gen): align takeover acceptance guidance
slavashvets Jul 13, 2026
55d14db
perf(gen): slim dhall normalization state
slavashvets Jul 13, 2026
4b89b1b
fix(fixtures): pass name mappings to exhaustive config
slavashvets Jul 13, 2026
a1ac9f8
fix(generator): preserve project order under Ruff
slavashvets Jul 13, 2026
cfcd0ae
chore(deps): raise psycopg floor and refresh tooling
slavashvets Jul 13, 2026
e92a498
Migrate to new contract removing deps on Text/equal (#17)
nikita-volkov Jul 16, 2026
07634be
chore(deps): refresh release workflow actions
slavashvets Jul 16, 2026
bc459ba
chore: prepare default branch rename to main
slavashvets Jul 16, 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
4 changes: 2 additions & 2 deletions .github/scripts/build-contract-shell.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Wraps the Dhall-generated package (from tests/Exhaustive.dhall) in a minimal
# Wraps the Dhall-generated package (from fixtures/Exhaustive.dhall) in a minimal
# consumer shell, mirroring the hand-written tests/golden/ shell (pyproject.toml
# + py.typed) so basedpyright strict runs against the same layout a real
# consumer would import, per the full_package pattern in tests/conftest.py.
Expand Down Expand Up @@ -31,7 +31,7 @@ build-backend = "hatchling.build"
name = "${pkg_name//_/-}"
version = "0.0.0"
requires-python = ">=3.12"
dependencies = ["psycopg>=3.2"]
dependencies = ["psycopg>=3.3.4,<4"]

[tool.hatch.build.targets.wheel]
packages = ["src/$pkg_name"]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

Expand Down
70 changes: 35 additions & 35 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [master]
branches: [main]
pull_request:
workflow_call:
inputs:
Expand Down Expand Up @@ -47,16 +47,13 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref || github.sha }}

# A single-artifact pgn generate peaks at ~6.5 GB RSS on this runner
# (memory goes to normalizing the generator closure, near-independent
# of the query count), which flirts with the 7.8 GiB RAM + 3 GiB stock
# swap and got the VM killed by the memory watchdog mid-suite; extra
# swap lets the GC spill instead. /swapfile is taken by the image's
# stock swap, so the extra file goes on the /mnt ephemeral disk.
# pgn generation has exceeded the hosted runner's stock memory on this
# suite. Keep extra swap as evaluator and GC headroom. /swapfile is taken
# by the image's stock swap, so the extra file goes on /mnt.
- name: Add swap headroom for pgn generate
shell: bash
run: |
Expand All @@ -77,13 +74,16 @@ jobs:
set -euo pipefail

version="$(mise x -- pgn --version)"
if [[ "$version" != "0.9.1" ]]; then
echo "::error::Expected pgn 0.9.1 (mise.toml pin), got '$version'."
if [[ "$version" != "0.12.0" ]]; then
echo "::error::Expected pgn 0.12.0 (mise.toml pin), got '$version'."
exit 1
fi

- name: Sync the harness venv
run: mise x -- uv sync
run: mise run install

- name: Lint the harness and committed generated package
run: mise run lint

# Hosted runners cannot fit a full 7-artifact pgn generate (~31 GB peak
# RSS measured locally); keep only the "python" artifact for this CI
Expand All @@ -94,7 +94,7 @@ jobs:
run: |
set -euo pipefail

python3 - <<'EOF'
mise exec -- python - <<'EOF'
from pathlib import Path

p = Path("tests/fixture-project/project1.pgn.yaml")
Expand All @@ -117,31 +117,16 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref || github.sha }}

# The `as Source` import mode on gen/Deps only changes how pgn loads the
# remote packages (unnormalized, to save RAM), not what they evaluate to.
# The pinned action below bundles a dhall fork that predates the mode, so
# strip it here; the evaluation is semantically identical either way. The
# sha256 pins go with it: an as-Source pin hashes the import's source,
# not the normalized expression a plain import verifies against, so after
# the strip the pins would be checked against the wrong algorithm.
- name: Strip `as Source` and its source-hash pins for the action's dhall
shell: bash
run: |
set -euo pipefail
sed -i -e 's/^[[:space:]]*as Source$//' -e 's/^[[:space:]]*sha256:[0-9a-f]\{64\}$//' gen/Deps/*.dhall

# A plain, standard-Dhall evaluator cannot run this: gen/Interpreters/Project.dhall's
# buildLookup and gen-sdk's own Fixtures.Exhaustive both use Text/equal, a builtin
# from pgn's forked Dhall, absent from the upstream dhall-lang Prelude. This action
# bundles that same fork.
# Use the pinned directory-tree action for the same reproducible contract
# evaluation path used by release validation.
- name: Generate output from Dhall
uses: nikita-volkov/dhall-directory-tree.github-action@60a18dc647d6daea805263ea0fed7bb8011f3bcd # v2
with:
dhall_file: tests/Exhaustive.dhall
dhall_file: fixtures/Exhaustive.dhall
output_dir: contract-output

- name: Assert the compile did not fail
Expand All @@ -166,12 +151,27 @@ jobs:
- name: Install mise-managed tools
uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0

- name: Create a venv and install psycopg + basedpyright
- name: Create a venv and install contract tools
run: |
set -euo pipefail

mise exec -- uv venv contract-shell/.venv
mise exec -- uv pip install --python contract-shell/.venv/bin/python \
"psycopg[binary]>=3.3.4,<4" \
"basedpyright==1.39.9" \
"ruff==0.15.20"

- name: Ruff check the generated package
shell: bash
run: |
set -euo pipefail

mise x -- uv venv contract-shell/.venv
mise x -- uv pip install --python contract-shell/.venv/bin/python "psycopg[binary]>=3.2" basedpyright
mise exec -- contract-shell/.venv/bin/ruff check \
--config "$PWD/pyproject.toml" \
contract-shell/src
mise exec -- contract-shell/.venv/bin/ruff format --check \
--config "$PWD/pyproject.toml" \
contract-shell/src

- name: basedpyright strict on the generated package
shell: bash
Expand All @@ -189,7 +189,7 @@ jobs:
}
JSON

output="$(contract-shell/.venv/bin/basedpyright --project contract-pyrightconfig.json --outputjson)" || true
output="$(mise exec -- contract-shell/.venv/bin/basedpyright --project contract-pyrightconfig.json --outputjson)" || true
echo "$output" | jq .

files=$(jq '.summary.filesAnalyzed' <<<"$output")
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
fi

- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.ref }}
fetch-depth: 0
Expand Down Expand Up @@ -77,16 +77,16 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.ref }}
fetch-depth: 0

- name: Resolve Dhall
id: resolve
uses: nikita-volkov/dhall-resolve.github-action@7caaf1fdb40ac864bc02e37575f577ee084713a8 # v3
uses: nikita-volkov/dhall-resolve.github-action@b7a346816b48a0f58282855a56e19b2a2c77f35d # v3
with:
file: gen/Gen.dhall
file: src/package.dhall
minify: true

- name: Prepare changelog for release
Expand Down Expand Up @@ -124,7 +124,7 @@ jobs:
echo "commit-sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: v${{ inputs.version }}
name: "v${{ inputs.version }}"
Expand Down Expand Up @@ -158,7 +158,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.ref }}
fetch-depth: 0
Expand Down Expand Up @@ -257,7 +257,7 @@ jobs:
cmp "$bundled" release-asset/resolved.dhall

- name: Upload the built distributions as workflow artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pgenie-python-gen-dist
path: wheel/dist/*
Expand All @@ -279,7 +279,7 @@ jobs:

steps:
- name: Download the built distributions
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pgenie-python-gen-dist
path: dist
Expand Down
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ already says.

## Dhall

`gen/` pins its remote imports by sha256 (`gen/Deps/*.dhall`). Bump those
`src/` pins its remote imports by sha256 (`src/Deps/*.dhall`). Bump those
deliberately, one at a time, and re-run the harness before committing a pin
change.

Never replace a pin with a local filesystem path (`../gen-contract/...`,
`../gen-sdk/...`) even temporarily for convenience; it only works on a
machine with those sibling repos checked out and breaks CI. Always use a
`https://raw.githubusercontent.com/pgenie-io/<repo>/<tag>/...` import with
its `sha256`.
148 changes: 103 additions & 45 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,105 @@
# Upcoming

- The test harness now runs every pgn subprocess in its own process group under
an RSS watchdog: a thread polls `ps -o rss=` every 2 s and, on breach of
`PGN_MAX_RSS_GB` (default 40 GB), kills the whole group and fails the test with
the observed RSS. A single-artifact generate peaks ~35 GB on the reference
machine; an unbounded run once hit ~80 GB and had to be emergency-killed, so
the budget keeps a runaway generate from taking down the host.
- Emitted packages gained a surface-agnostic `_generated/_core.py` that owns the
shared names (the `JsonValue` alias, `NoRowError`, a new `DecodeError`, and the
`require_array` decode guard) with no I/O. Both `_runtime.py` modules are now
I/O-only and re-export `JsonValue`/`NoRowError`/`require_array` from `_core` so
off-contract `from ._runtime import ...` keeps working; `_rows.py`, the
statement modules, and the facades import the shared names from `_core`
directly.
- The release wheel now ships its GPL compliance files inside the artifact:
the build fetches the GPLv3 text into `COPYING` (sha256-pinned) and bundles
the committed `wheel/NOTICE` describing the composition; both land in
`dist-info/licenses` and the release job asserts their presence in the
wheel and the sdist. The repository LICENSE (MIT) is no longer copied into
the wheel, where it misstated the artifact's license.
- Emitted files now carry REUSE-style SPDX header lines
(`SPDX-FileCopyrightText`, `SPDX-License-Identifier: MIT-0`) right after
the `@generated` marker: license scanners in consuming projects see a
standard permissive id for the generated code instead of guessing its
provenance.
- Added a `pgenie-python-gen` wheel channel (`wheel/`): the release build bundles
the resolved generator as an installable package with a `path`/`url`/`vendor`
CLI; publication to PyPI ships wired but disabled.
- Keyword escaping (`PyIdent.dhall`) no longer needs the fork-only
`Text/equal` builtin; it's rewritten against a `Text/replace`-based marker
trick, since pgn's embedded `Text/replace` doesn't match a needle spanning
a concatenation boundary, which is what the java.gen-style delimiter trick
relied on.
- The generator config is now fully optional, folded through a single
defaults record in `compile.dhall` (`packageName` from the project name,
`emitSync` off, `onUnsupported` `Fail`). A project can omit `config:`
entirely, pass `config: {}`, or set any subset of the keys.
- gen-sdk pinned to `v0.10.2`.
- Fixed single-field composite param binding: it rendered as `(x.field)`,
parentheses around a bare expression, not a one-element Python tuple.
Now renders `(x.field,)`. Covered by a dedicated fixture composite
(`tag_value`) exercised as both a param and a result column.
- Added `onUnsupported: Fail | Skip`. `Fail` (default) is the existing
loud-abort behavior. `Skip` drops the smallest self-consistent unit (a
statement or a custom type, cascading to anything that references it) and
keeps generating the rest.
- Bumped the pins to gen-contract v5.0.0 and gen-sdk v3.0.0 (requires `pgn`
v0.12.0). Replaced the hand-rolled `buildLookup` custom-kind resolver and its
fixed-point removal cascade with gen-sdk's `CustomTypes` module: references
now resolve by the contract's `CustomTypeRef.index` and `onUnsupported: Skip`
computes survivorship in a single left-fold over the topologically-sorted
`customTypes`. This retires the generator's last `Text/equal` use, so no live
`Text/equal` call remains anywhere in `src/`.

- Removed the `queryNameMappings`/`customTypeNameMappings` rename-mapping
config and all generation-time Python namespace-collision detection
(project-wide facade/module, per-query, per-custom-type, and per-type
module-internal audits). `pgn` 0.11.0 dropped the `Text/equal` builtin these
relied on to compare two runtime `Text` values, and there is no
`Text/replace`-based way to reconstruct that decision. A colliding schema is
no longer caught at `pgn generate`. Collisions that remain visible in the
generated tree generally fail the package's `basedpyright --strict` gate
(`reportRedeclaration` or `reportInvalidTypeForm`), but a module-path
collision can overwrite an earlier file before the checker sees it.
[pgenie-io/pgenie#75](https://github.com/pgenie-io/pgenie/issues/75) asks pgn
to guarantee unique custom-type identities at the source.

- Finalized one additive package surface. Async functions remain at the package
root for every configuration. `emitSync: true` adds `<package>.sync`, a sync
runtime, and adjacent sync functions in the same canonical statement modules.
SQL, Row classes, custom types, core errors, and registration stay shared, so
async and sync facades expose exact model identities.

- Moved PostgreSQL conversion to psycopg's class-aware adapters. Generated enums
are pure `StrEnum` classes, composites and statement rows are frozen slotted
dataclasses, and `args_row` constructs each canonical Row positionally through
`BaseRowFactory`. `EnumInfo`/`register_enum` and
`CompositeInfo`/`register_composite` register generated classes in
dependency-first order. Statement SQL remains one `LiteralString`; generated
output has no query decoders, model codecs, casts, or bytes SQL.

- Completed custom-type coverage for scalar enums, enum arrays through rank 2,
scalar composite load and dump, rank-1 composite arrays, nested scalar
composites, nullable members, and sanitized identifiers. Unsupported enum
ranks, composite ranks, custom-array fields inside composites, and missing or
unsupported custom types fail loudly.

- Resolved custom-type references by their contract-supplied
`CustomTypeRef.index` (gen-contract v5) rather than by name comparison, so a
reference carries a stable schema-qualified identity (`pgSchema`/`pgName`)
directly. Same-unqualified-name types across schemas no longer collapse
during reference resolution. Generated class and module names still come
from the unqualified contract name, so their Python names must remain unique.
Natural project indexes provide deterministic custom-import deduplication and
ordering. Reserved helper and keyword conflicts are escaped while SQL names
remain unchanged.

- Kept `onUnsupported: Fail | Skip`. `Fail` aborts generation with the nested
report. `Skip` preserves warnings and computes survivorship in a single
left-fold over the topologically-sorted `customTypes`, removing an unsupported
custom type, its dependent custom types and statements, and all affected type,
registration, facade, Row, and statement entries. The surviving package
remains strict-importable.

- Established the generated tree as a greenfield layout with no compatibility
layer or promise for internal generated paths. Every generated Python file now
begins with the exact marker
`# @generated by python.gen (pGenie); regeneration overwrites manual changes.`
followed by REUSE copyright and MIT-0 SPDX lines. Takeover contract tests pin
the marker and greenfield layout. Consumers must disable regeneration and
replace the marker before treating the files as owned Python.

- Added Ruff check and format gates, authored and SQL line-length gates, raw
output checks with no postformat step, public identity checks, import-boundary
checks, adapter AST checks, and async/sync PostgreSQL round trips. Final
evidence: H1 CONFIRM with psycopg 3.3.4, consumer `psycopg>=3.3.4,<4`,
class-aware adapters, pure models, canonical `args_row` Rows, and no query
decoders, model codecs, casts, or bytes SQL. H2 CONFIRM: 25 Python files /
1467 lines / 11 statement files / 801 statement lines / 11 SQL. H3 CONFIRM:
Ruff 0/0, authored long0, SQL long0, raw output/no postformat. Tests: 50
passed, 0 skipped; pgn v0.12.0; strict basedpyright 0/0.

- Migrated the generator to gen-contract v4.0.1 and gen-sdk v2.0.0 using
`Sdk.Sigs`. The implementation moved from `gen/` into `src/`, the working-tree
entry point is `src/package.dhall`, and the contract fixture is
`fixtures/Exhaustive.dhall`. Remote imports in `src/Deps/` are ordinary
sha256-pinned imports.

- Preserved the pgn subprocess RSS watchdog. Each process runs in its own process
group, is polled every 2 seconds, and is killed on a
`PGN_MAX_RSS_GB` breach, with a 40 GB default. Heavy fixture generation remains
serial.

- Fixed one-field composite binding. Generated dumpers construct field-ordered
tuples with `dataclasses.fields` and `getattr`, preserving one-field arity.
The fixture exercises the type as both a parameter and a result.

- Kept `PyIdent.dhall` independent of fork-only text equality by using
`Lude.Text.replaceIfOneOf`'s bounded, sentinel-wrapped `Text/replace`
construction. Keyword fields and parameters gain a trailing underscore only
in Python, while raw database names remain stable.

- Preserved the release and license flow. The release job resolves
`src/package.dhall` into the `resolved.dhall` release asset, then byte-checks
and bundles that same asset into the `pgenie-python-gen` wheel. Repository
source remains MIT, emitted Python is MIT-0, and the combined resolved artifact
and wheel are GPL-3.0-or-later because they inline gen-sdk. The wheel includes
the sha256-pinned GPL text and `wheel/NOTICE`; PyPI publication remains
disabled behind its explicit gate.
Loading
Loading