Skip to content

feat(python): PostgREST/Supabase query dialect in generated services (parity with TS #36/#56-#60) - #117

Open
cultron wants to merge 1 commit into
mainfrom
feat/postgrest-dialect-python
Open

feat(python): PostgREST/Supabase query dialect in generated services (parity with TS #36/#56-#60)#117
cultron wants to merge 1 commit into
mainfrom
feat/postgrest-dialect-python

Conversation

@cultron

@cultron cultron commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Ports the @apso/crud PostgREST/Supabase dialect to the Python (FastAPI + SQLAlchemy) code generator. Generated services now accept the same PostgREST URL conventions the TypeScript services do. The nestjsx dialect is unchanged.

What changed (generated output)

utils/query.py gains a second parser path alongside the incumbent nestjsx parser, plus a dialect detector. The router reads the X-Crud-Dialect header and shapes the response per dialect. The service exposes both retrieval methods.

  • src/lib/templates/python/utils/query.eta — PostgREST parser, dialect detection, postgrest-aware QueryBuilder
  • src/lib/templates/python/routers/router.eta — header-driven dialect selection + response shaping
  • src/lib/templates/python/services/service.etaget_many_list (bare array) alongside get_many (envelope)
  • src/lib/templates/python/utils/init.eta — exports
  • test/lib/generators/python.test.ts — 9 new generator tests

Operators / behaviors covered

  • select=a,b including alias:col output-key rename (CLI first pivot: new command surface, architecture refactor, tests #57), col::type cast stripping, and rel(cols) embeds (mapped to selectinload eager loads)
  • col=op.value filters: eq / neq / gt / gte / lt / lte / like / ilike / in / is and not.<op>, with the TS operator mapping and *% like/ilike wildcards; is.null / is.true / is.false; in.(1,2,3) with quoted-value support
  • or=(...) / and=(...) logical groups, nested or(...)/and(...) (Plat 1269 #56)
  • order=col.asc|desc[.nullsfirst|nullslast]
  • limit / offset

Dialect detection (mirrors dialect.ts)

  1. X-Crud-Dialect header wins (postgrest|nestjsx, any other value → 400)
  2. Param-shape: select/order or a bare col=op.value → postgrest; fields/filter/join/sort/s/or/per_page → nestjsx; limit/offset/page/cache neutral
  3. Both families present → 400 (refuse to guess)
  4. Otherwise default to nestjsx (incumbent — existing requests byte-identical)

Response shaping

Deferred

Tests

  • Generator suite: 330 passed / 25 suites (NODE_OPTIONS='--experimental-vm-modules' jest), including 9 new PostgREST-dialect tests
  • Semantic battery: the generated query.py was loaded in Python and run against real SQLAlchemy compilation (eq, not/in/is, like/ilike *%, or=()/nested and=(), order + nulls, select alias projection, unknown-column 400, bare-array limit/offset windowing, deferred-op rejection, embedded-filter rejection, dialect detection incl. header override + collision) — all green. Generated query.py, router.py, service.py all parse.

Do not merge — merging publishes the CLI. Left green for review.

🤖 Generated with Claude Code

Port the @apso/crud PostgREST dialect to the Python (FastAPI+SQLAlchemy)
code generator, so generated services accept the same PostgREST URL
conventions the TypeScript services now do (parity with TS #36/#56-#60).

The generated utils/query.py gains a second parser path alongside the
incumbent nestjsx parser:

- select=a,b (+ alias:col rename #57, + rel(cols) embeds as eager loads)
- col=op.value filters: eq/neq/gt/gte/lt/lte/like/ilike/in/is and not.<op>,
  with the TS operator mapping and *->% like/ilike wildcards
- or=(...) / and=(...) logical groups, nested (#56)
- order=col.asc|desc[.nullsfirst|nullslast]
- limit / offset

Dialect detection mirrors dialect.ts: X-Crud-Dialect header wins
(postgrest|nestjsx, else 400); otherwise param-shape (select/order or a
bare col=op.value => postgrest; fields/filter/join/sort/s/or/per_page =>
nestjsx; both families => 400; default nestjsx). The router passes the
header + raw query to the parser.

Response shaping: the postgrest path returns a BARE JSON array (no
{data,count} envelope), even with limit/offset (#58); nestjsx keeps its
envelope. An unknown filter/select/order column on the postgrest path is a
400, not a 500 (#60).

Deferred (as in TS): cs/cd/ov/fts and range operators (#54) are rejected
clearly. Embedded resource filtering (rel.col=op.value) is deferred and
rejected: the Python join path uses selectinload (a separate SELECT) and
cannot push a predicate into a JOIN ON clause; select embeds still load the
relation. nestjsx behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.

1 participant