Skip to content

test smoke CRUD, Python/Go smoke and migrate guard, table override in Python/Go (supersedes #119) - #150

Merged
cultron merged 4 commits into
mainfrom
feat/smoke-crud-python-go
Oct 5, 2026
Merged

cultron merged 4 commits into
mainfrom
feat/smoke-crud-python-go

Conversation

@cultron

@cultron cultron commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Four commits for three changes. Supersedes #119 (its commit is included with authorship kept, plus one fix on top).

1. Per-entity table override in Python and Go (supersedes #119)

2. apso test smoke create/read/delete

src/lib/verify/crud-plan.ts builds one row per entity from the .apsorc (required = not nullable, no default, not a user primary key; text/string/varchar -> "smoke" cut to length, int types -> 1, decimal/numeric/float -> 1.5, boolean -> true, date/timestamp(tz)/datetime -> 2024-01-01T00:00:00.000Z, enum -> first value, uuid -> random uuid, json/jsonb -> {}, is_email -> an email). Required ManyToOne parents come from the parsed relationship map (both ManyToOne and OneToMany declarations, to_name respected via getRelationshipIdField), and entities are ordered parents first. Cycles, unsupported required types, and children of skipped entities become skip <Entity> steps with the reason, not failures.

The smoke run is now: GET list per entity, then POST (expect 200/201 and an id), GET /<route>/<id> (200), and finally DELETE in reverse order (200/204). One Step per call, same as before. Auth-guarded projects still only check /health.

3. Python and Go

  • apso test smoke for python/go starts the generated server (.venv/bin/python -m uvicorn app.main:app, or a go build ./cmd binary so killing it can't orphan the server) and runs the same HTTP checks against DATABASE_URL (environment first, then .env). It does not create tables or invent a database: tables come from the project's own DATABASE_SYNC / migrations. With no DATABASE_URL, or nothing listening at its host:port, the check is skipped with a message ("Smoke test skipped.").
  • Route per language follows each generator: TS /<Plural>, Python /api/<plural lowercased>, Go /api/<kebab-plural>.
  • apso migrate generate / apso migrate verify fail fast for Python and Go with "supports TypeScript services only for now" plus the native tool to use (Alembic; goose). The baseline/run/drift flow needs a scratch database to build the deployed schema on; TypeScript has PGlite, Python and Go do not, and running a baseline against the user's database is destructive. Not straightforward, so not wired.
  • scripts/scaffold-smoke.sh go leg: opt-in SMOKE_DATABASE_URL=<empty Postgres> runs apso test smoke after the compile. Nothing added to the python leg: a fresh Python scaffold cannot pass the smoke test today (template bugs below). CI is unchanged (no Postgres service there, so the go step does not run in CI).

Verified (all on this branch, macOS, Node 22, Python 3.12, Go 1.25, local Postgres 17)

  • Gates: npm run lint 0 errors (562 warnings, unchanged), npx tsc --noEmit -p . clean, full jest suite 390 passed / 0 failed. The intermediate commit for part 2 also typechecks and passes the verify tests on its own.
  • Part 1: new generator test asserts ForeignKey("order_record.id") and ForeignKey("user_profile.id"). In a real generated Python project the child model emitted ForeignKey("order_record.id"), and the Python smoke run below created rows in order_record.
  • Part 2, TypeScript, real project: bin/run init --name smoke-app --language typescript --skip-platform, then an .apsorc (v2) with Customer (text with length 5, is_email, enum, boolean, integer with default, nullable text), Order (table: order_record, decimal, timestamptz, nullable json, required ManyToOne Customer) and LineItem (integer, date, required ManyToOne Order). bin/run generate, npm install, npm run build, bin/run test smoke --skip-build: all 14 steps ok (3 lists, POST/GET for Customer, Order, LineItem in that order, DELETE LineItem, Order, Customer).
  • Broken route: renamed the LineItem controller path in dist/ and reran. GET /LineItems and POST /LineItems FAIL with 404, the command exits 1, and the other entities still pass.
  • Unit tests (test/lib/verify/crud-plan.test.ts): sample values per type, required-only payload, parents-first order via both ManyToOne and OneToMany declarations, to_name FK naming, nullable parents ignored, cycles / unsupported types / children of skipped entities skipped with reasons, per-language routes, .env parsing.
  • scripts/scaffold-smoke.sh run locally: typescript leg PASS (smoke now shows POST/GET/DELETE /SmokeWidgets, then migrate generate + verify), go leg with SMOKE_DATABASE_URL set to an empty local database PASS (build, start, list, POST/GET/DELETE /api/smoke-widgets), python leg (PYTHON=python3.12) PASS unchanged.
  • Go, real project, same schema minus the enum and json fields (see Go bugs below), local Postgres with DATABASE_SYNC=true: all steps ok, including the required ManyToOne chain on /api/orders and /api/line-items.
  • Python, real project, local Postgres, DATABASE_SYNC=true, after the two manual template workarounds listed below: same schema without relationships, all 13 steps ok. With relationships, POST /api/orders fails with 500 (a real generator bug, below), which is the smoke test doing its job.
  • Skip paths: Python project with DATABASE_URL at a closed port, and with no .env: one skipped step with the reason, "Smoke test skipped.", exit 0.
  • apso migrate verify / generate in Python and Go projects: exit 2 with the TypeScript-only message.

Found while verifying (not fixed here)

Each of these makes a fresh Python or Go service fail at runtime. None is caught today because the python/go legs only compile.

  • Python template (apsoai/service-template-python):
    • app/main.py never includes autogen_router, so no entity route is served and create_all sees no models. Fresh scaffold smoke: every entity route 404s.
    • pyproject.toml needs sqlalchemy[asyncio]: SQLAlchemy 2.1 no longer pulls in greenlet, so the app crashes on import.
    • package.json pins @electric-sql/pglite-socket@^0.3.0, which does not exist (latest is 0.2.11), so npm run db / apso dev with PGlite cannot install. Same in the Go template.
  • Python generator (this repo):
    • Enum columns use create_type=False, so DATABASE_SYNC=true fails with type "customer_tier_enum" does not exist.
    • Any ManyToOne makes POST and list return 500: the response schema has customer: Optional[Any] and pydantic cannot serialize the SQLAlchemy object (PydanticSerializationError).
  • Go generator (this repo):
    • Enum fields do not compile: models and DTOs reference enums.TierEnum, but enums.go defines CustomerTierEnum.
    • A nullable json field does not compile: services mix map[string]interface{} and *map[string]interface{}.
  • TypeScript template .apsorc ships "version": 1, so top-level relationships are ignored until a user changes it to 2.

To get the green Python run I added app.include_router(autogen_router) to main.py, installed greenlet, and (for the run with relationships and an enum) created the enum type by hand. To get the green Go run I dropped the enum and json fields.

Not done

  • No npm publish, no merge. Note merging to main releases @apso/cli.

…#107)

Parity with #98/#106 (TypeScript). The Python and Go generators derived
the table name as snakeCase(entity.name) and never read the per-entity
`table` override, so an entity named `Order` with "table": "order_record"
generated the SQL reserved word `order`.

- Both generators now emit `tableName = entity.table || snakeCase(name)`.
- Python template uses it for `__tablename__`, the self-referencing
  ManyToMany ForeignKey, join-table names, and index names, so every
  reference to this entity's own table follows the override consistently.
- Go template uses it in `TableName()`.

Without an override tableName === snakeCase(name), so output is unchanged
for the common case. Added override + fallback tests to both suites.

Full suite: 318 passed, 0 failed.

fixes #107
…table

PR #119 moves __tablename__ to the per-entity table override, but child
models still wrote ForeignKey("<parent name lowercased>.id"). With an
override on the parent, or any multi-word parent (UserProfile ->
user_profile, FK said userprofile), the FK named a table that does not
exist. Pass every entity's table name into the model template and use it.
apso test smoke only called GET /health and each list route. It now
builds a payload per entity from the .apsorc (required fields only:
not nullable, no default), creates entities parents first so required
ManyToOne foreign keys (<camelCase(to_name or parent)>Id) can be set
from the parent's id, reads each row back by id, and deletes children
before parents. Entities it cannot fill (unsupported required types,
cycles of required relationships, children of skipped entities) get a
skip step with the reason instead of failing.

The planner lives in src/lib/verify/crud-plan.ts with unit tests for
the payload values and the ordering.
apso test smoke for Python and Go projects starts the generated server
(uvicorn from .venv, or a go build ./cmd binary so stopping it cannot
orphan the server) and runs the same HTTP checks against DATABASE_URL
from the environment or .env. It never creates a database or tables;
with no DATABASE_URL, or nothing listening at its host and port, the
check is skipped with a message and reports "Smoke test skipped.".
Routes follow each generator: /api/<plural> (Python) and
/api/<kebab-plural> (Go).

apso migrate generate and verify fail fast for Python and Go and name
the native tool (Alembic, goose). The baseline, run and drift flow
needs a scratch database; TypeScript has PGlite, the others do not.

scaffold-smoke.sh: the go leg runs apso test smoke when
SMOKE_DATABASE_URL points at an empty Postgres (opt in, off in CI).
@cultron
cultron merged commit f7ed4a8 into main Oct 5, 2026
4 checks passed
@cultron
cultron deleted the feat/smoke-crud-python-go branch October 5, 2026 01:06
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