Skip to content

fix(model): polymorphic associations find underscore-shaped columns - #4457

Merged
bpamiri merged 6 commits into
developfrom
peter/polymorphic-schema-columns
Oct 6, 2026
Merged

bpamiri merged 6 commits into
developfrom
peter/polymorphic-schema-columns

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Replaces #4420, which closed automatically when its base branch (#4398's) was deleted on merge. #4398 is in develop now, so this PR targets develop.

Polymorphic associations find underscore-shaped columns

A polymorphic association declared without foreignKey / foreignType fixed its columns to <name>id / <name>type at registration, before the schema is read. That covers belongsTo(polymorphic = true) and hasMany() / hasOne() with as. An app whose schema uses the underscore shape, as t.references(polymorphic = true) writes it when useUnderscoreReferenceColumns is on (the wheels new default), threw key [<name>id] doesn't exist on first use.

Non-polymorphic defaults already resolve against the schema at join time (#3337). This does the same for polymorphic ones:

  • Registration records which polymorphic column names were derived; names passed as foreignKey / foreignType are never touched.
  • On first use, $resolvePolymorphicColumns() checks the child's real columns. That's this model for a polymorphic belongsTo, and the associated model for hasMany / hasOne with as. It runs from the association methods, the include join and the nested-properties link.
  • <name>id / <name>type win when they exist (including when both shapes exist). Otherwise <name>_id / <name>_type are used when they exist. When neither exists, the legacy default stays, and the existing error path reports it.
  • If the child's columns can't be read (say the child model fails to load), the defaults are kept, a warning is logged, and nothing throws.
  • The result is memoized per association, under a named lock with a double check.

Specs

vendor/wheels/tests/specs/model/polymorphicSchemaColumnsSpec.cfc runs on its own underscore-shaped tables, with models that pass no column names:

  • create through hasMany and read back through belongsTo;
  • hasMany reads and counts filter by the type column;
  • an include join filters by the type column;
  • create and read through hasOne;
  • nested properties set both columns;
  • the resolved names are recorded on every side;
  • both shapes on one table: the legacy names win;
  • a child model that doesn't exist: the defaults are kept and nothing throws;
  • an explicit foreignKey with a derived foreignType, and the reverse: only the derived name resolves.

Every spec starts with the model classes forgotten, so each path resolves the columns itself. The include join and the nested link run as the association's first use, with rows inserted directly rather than through the association. Removing either call site makes its spec fail with key [notableid] doesn't exist; I checked that and then restored the code.

The legacy shape is covered by the existing polymorphic specs, which pass unchanged.

RED before the resolver: the schema cases error with key [notableid] doesn't exist.

GREEN:

  • Lucee 7 + SQLite after merging develop: full suite 7101/7101.
  • RustCFML: 7086/0.
  • Complexity gate, consumer-docs check, guides build and verify-docs on associations.mdx (17/17) pass.

Docs

  • basics/associations.mdx, "Column names": how the columns are found, with explicit names for anything else.
  • .ai/models.md, consumer copy and template.
  • A fixed fragment.

🤖 Generated with Claude Code

bpamiri and others added 6 commits October 5, 2026 12:49
belongsTo(polymorphic = true), and hasMany() / hasOne() with `as`, fix the
type column to <name>type. A new foreignType argument names it, so a schema
with underscore columns (notable_id / notable_type) works with an explicit
foreignKey and foreignType. The default is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
…foreign-type

Signed-off-by: Peter Amiri <peter@alurium.com>

# Conflicts:
#	cli/lucli/templates/app/.ai/models.md
#	docs/consumer-ai/.ai/models.md
…gnType spec

Assert the page has no note before the decoy page note exists, so equal ids
can't change the result. notable_id is a bigInteger, as wide as the parents'
keys on CockroachDB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
A polymorphic association without foreignKey / foreignType fixed its columns
to <name>id / <name>type at registration, before the schema is known, so a
schema written with useUnderscoreReferenceColumns (the wheels new default)
threw key [<name>id] doesn't exist. Derived names now resolve against the
child's real columns on first use, like the non-polymorphic defaults: the
legacy names when they exist, else <name>_id / <name>_type. Explicit names
are untouched; a failed schema read keeps the defaults. Memoized per
association.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
…cit/derived names

Every spec forgets the model classes first, and the include join and nested
link run as the association's first use (rows inserted directly), so each
pins its own call site: removing either call fails its spec. Two mixed cases:
an explicit key with a derived type, and the reverse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
…schema-columns

Signed-off-by: Peter Amiri <peter@alurium.com>

# Conflicts:
#	cli/lucli/templates/app/.ai/models.md
#	docs/consumer-ai/.ai/models.md
#	vendor/wheels/model/associations.cfm
#	vendor/wheels/tests/specs/model/polymorphicForeignTypeSpec.cfc
#	web/sites/guides/src/content/docs/v4-2-0/basics/associations.mdx
@bpamiri

bpamiri commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Source review: APPROVE at 5b305d9, with final approval pending the full matrix and current-head PR CI.

Compared with the previously reviewed resolver in #4420, the production changes are identical. The new specs clear the model cache before each case, exercise the include join and nested link as first use, and cover both mixed explicit/derived column-name combinations. The include case seeds rows directly rather than warming the association through a create call.

No new source blocker. Final verification will check the named schema-resolution specs in every matrix leg, plus the PR's own checks.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

232 180 tests   226 996 ✅  1h 41m 5s ⏱️
 18 464 suites    5 184 💤
     32 files          0 ❌

Results for commit 5b305d9.

@bpamiri

bpamiri commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Final review: APPROVE at 5b305d9.

I read all 32 raw engine/database results from full matrix 37403150148 on this head: 577 bundles discovered per leg, zero failures/errors. polymorphicSchemaColumnsSpec passed all 10 named cases on every leg (320 passes, no targeted skips), including cold include/nested first use and both mixed explicit/derived combinations. RustCFML and the compatibility gate succeeded.

The reviewed resolver is unchanged from the earlier source approval. Current PR CI: 29 passed, 2 skipped (Update docs and Reviewer), 0 failed or pending. No remaining blocker.

@bpamiri
bpamiri merged commit c610da9 into develop Oct 6, 2026
69 checks passed
@bpamiri
bpamiri deleted the peter/polymorphic-schema-columns branch October 6, 2026 03:17
bpamiri added a commit that referenced this pull request Oct 6, 2026
…4459)

The maintainer CLAUDE.md still said polymorphic associations need an
explicit foreignKey against an underscore schema. Since #4457 they
resolve <name>id/<name>type or <name>_id/<name>_type from the schema,
and #4398 added foreignType.

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant