Skip to content

fix(cli): honor per-entity table override in TS generator (#98) - #106

Merged
cultron merged 1 commit into
mainfrom
fix/98-honor-table-override
Jul 29, 2026
Merged

cultron merged 1 commit into
mainfrom
fix/98-honor-table-override

Conversation

@cultron

@cultron cultron commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #98: the .apsorc per-entity table override was silently ignored, so an entity named Order with "table": "order_record" still generated the SQL reserved word order.

Root cause

The override was dropped at two layers:

  1. table wasn't declared on the Entity type, so it was never surfaced from the parsed .apsorc (it existed on the raw object at runtime but was untyped).
  2. The TypeScript generator hardcoded the table name as snakeCase(name) and never read entity.table. The entity template emitted @Entity('<%= it.snakeCasedName %>').

The reporter's "works for other compound-name entities" observation was a red herring: it only appeared honored when the override happened to equal snakeCase(name) (e.g. OrderItem → order_item). For Order → order_record the mismatch made the bug visible.

Fix

  • Add table?: string to the Entity type.
  • Generator emits tableName = entity.table || snakeCase(name) and the template uses it in @Entity(...).
  • migrate/sandbox.ts honors the same override so local migration tests match the generated entity's table name.

Test

Added table name override (cli#98) to the TS generator suite:

  • override honored → @Entity('order_record'), never @Entity('order')
  • no override → default @Entity('order') preserved
  • compound name without override → @Entity('order_item') (regression guard)

npm run test: 208 passed / 0 failed.

Scope note

The Python and Go generators derive table names the same way (snakeCase(name)) and share this latent bug. I kept this PR to the reported TypeScript/TypeORM target to stay focused and low-risk; the same one-line pattern would give Python/Go parity as a follow-up.

Note

Merging to main publishes @apso/cli (merge = release) — leaving the merge to Matt.

Closes #98

🤖 Generated with Claude Code

The `.apsorc` per-entity `table` override was dropped entirely: the
TypeScript generator always emitted `@Entity('<snake_case(name)>')` and
never read `entity.table`. It "worked" for some entities only when their
override happened to equal snake_case(name); for an entity named `Order`
with `"table": "order_record"` it generated the reserved word `order`.

- Add `table?: string` to the `Entity` type (the field existed in
  .apsorc at runtime but was untyped and never surfaced).
- Generator: emit `tableName = entity.table || snakeCase(name)` and use
  it in the `@Entity(...)` decorator.
- Migrate sandbox: honor the same override so local migration tests use
  the same table name as the generated entity.
- Tests: override is honored; default snake_case still applies with no
  override; compound names still snake_case correctly.

Note: Python/Go generators derive table names the same way and have the
same latent bug; left out of this PR to keep it focused on the reported
TypeScript/TypeORM target. Worth a parity follow-up.

Closes #98

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cultron cultron left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Approve (peer review, apso-overnight-1)

Correctness: the diff matches the write-up exactly — table?: string added to the Entity type, generator computes tableName: entity.table || snakeCase(name), template switches @Entity('<%= it.snakeCasedName %>') → @Entity('<%= it.tableName %>'), and migrate/sandbox.ts honors the same override so local migration tests agree with the generated entity.

Coverage: 3 tests — override honored (order_record, never order), no-override fallback (order), compound-name regression (order_item). The last two are the important ones because they pin that the default path is unchanged.

Release-safety: for entities without a table override, entity.table is undefined → snakeCase(name), byte-identical to before, so existing schemas are unaffected; only the (previously-broken) override case changes. Safe to publish.

Missed cases: Python and Go derive table names the same way and share the latent bug — correctly scoped out as a follow-up in the PR. Fine to land TS first.

@cultron
cultron merged commit a7711d5 into main Jul 29, 2026
1 of 2 checks passed
cultron added a commit that referenced this pull request Aug 18, 2026
…#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
cultron added a commit that referenced this pull request Oct 5, 2026
… Python/Go (supersedes #119) (#150)

* fix(cli): honor per-entity table override in Python and Go generators (#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

* fix(python): point ManyToOne and join-table FKs at the parent's real 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.

* feat(test smoke): create, read and delete one row per entity

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.

* feat(test smoke, migrate): Python and Go support where it is honest

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).
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.

CLI ignores per-entity table override; entity named Order generates table order (SQL reserved word)

1 participant