Skip to content

Fix post-merge review findings on has-one-of-many - #334

Merged
tmgbedu merged 3 commits into
mainfrom
fix/has-one-of-many-review
Oct 31, 2026
Merged

tmgbedu merged 3 commits into
mainfrom
fix/has-one-of-many-review

Conversation

@tmgbedu

@tmgbedu tmgbedu commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #331, addressing the post-merge review (#331 (comment)).

Changes

  • M1 NULL ordering: the of-many subquery adds alias.column IS NOT NULL, giving MAX/MIN semantics on SQLite, MySQL and PostgreSQL. A parent whose children are all NULL gets None.
  • M2 pk default: of_many(column=None) resolves to the related model's __primary_key__ (custom pks work); when the column is the pk it is ordered once with no NULL filter.
  • m1: @relationship raises TypeError when the method returns an existing descriptor (return self.statuses) or a builder, leaving the shared descriptor intact.
  • m3: aggregate typed as Literal["max", "min"] (runtime ValueError kept).
  • m4: TypeError when the parent HasMany has no explicit foreign_key and no attribute name to derive one.
  • m5: model_reference covered via related_model().
  • m6: tests for NULLs, custom pk, subclass owner, self-referencing table, plus MySQL and PostgreSQL to_sql snapshots (no live server needed).
  • m7: ADR documents MySQL error 1093 on update/delete through the relation.
  • Renamed relationships/relationship.py → relationships/RelationshipMethod.py; fastapi_startkit.masoniteorm.relationship and fastapi_startkit.masoniteorm.relationships.relationship still import.
  • ADR 050 → Accepted, with decisions recorded and a caveat that a relation named latest/oldest overrides Model.latest()/oldest().

Unchanged pending a user decision: both self and @classmethod forms, the created_at default for latest_of_many/oldest_of_many, no inner ofMany constraints.

Validation

  • Full suite: 3647 passed, coverage 92.64% (HasOneOfMany.py and RelationshipMethod.py at 100%)
  • ruff check / format clean
  • basedpyright: no new errors (the single error is the pre-existing unresolved langchain import in ai/ai.py)

🤖 Generated with Claude Code

- Ignore NULL columns in the of-many subquery so latest/oldest follow
  MAX/MIN semantics on every database
- of_many() defaults to the related primary key and orders by it once
- Type the aggregate as Literal["max", "min"]
- Reject @relationship methods returning an existing descriptor or a
  builder, and HasMany relations without a resolvable foreign key
- Rename relationships/relationship.py to RelationshipMethod.py while
  keeping the public relationship imports
- Cover NULLs, custom primary keys, subclasses, self-referencing tables
  and MySQL/PostgreSQL SQL
- Mark ADR 050 accepted and document the MySQL 1093 and naming caveats

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@tmgbedu tmgbedu 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.

Review of #334 (has-one-of-many post-merge fixes)

Verdict: approve with minor follow-ups. No blockers.

I checked out fix/has-one-of-many-review and ran uv run pytest --ignore=tests/masoniteorm/postgres --cov: 3647 passed, total coverage 92.64%, with HasOneOfMany.py and RelationshipMethod.py at 100%. The MySQL and Postgres SQL snapshot tests also pass without a database server, and ruff check is clean.

What I checked

  • M1 (NULL ordering): I generated SQL with the Postgres and MySQL grammars for the eager, where_has and oldest_of_many queries. Each correlated subquery has <alias>.<column> IS NOT NULL before the ORDER BY <column>, <pk>, so NULL rows are ignored the way MAX/MIN ignore them. The new SQLite test covers a parent whose children all have NULL dates (it gets None), for both directions.
  • M2 (custom primary key): with __primary_key__ = "ticket_id", of_many() and of_many(aggregate="min") order only by ticket_id, once, with no NULL filter. An explicit latest_of_many("ticket_id") produces the same SQL. Postgres and MySQL both look right.
  • m1: returning self.statuses, a builder, or another @relationship result raises TypeError, and the shared descriptor keeps its name.
  • m3, m4, m5, m6, m7: all addressed. The Literal aggregate type, the foreign-key check, the related_model() test, the new tests (NULLs, custom pk, subclass, self-referencing eager and where_has) and the MySQL error 1093 caveat are all in place.
  • Repo rules: ADR 050 and its index entry are updated, and the PR adds no explanatory comments or docstrings.

Minor

  1. The old module path no longer imports, but the ADR says it does. relationships/relationship.py shipped in v0.71.0. After the rename, from fastapi_startkit.masoniteorm.relationships.relationship import relationship (or import RelationshipMethod from it) raises ModuleNotFoundError. from fastapi_startkit.masoniteorm.relationships import relationship still works. Either add a small compatibility module at the old path, or change the ADR wording ("still exported as … relationships.relationship") to say the old module path was removed.
  2. The ADR dropped a caveat that still applies. The earlier text said global scopes on the related model also run on the correlated subquery's builder, and scopes that hard-code the table name instead of using the builder's alias aren't handled. The code hasn't changed there, so please put that note back under Caveats.

Nit

  • In one_of_many, the return value of query.where_column(...) is discarded, and the code relies on the builder changing in place. That is correct, but chaining the calls like the rest of the method would read more clearly.

Not flagged, as agreed: the self/@classmethod forms, the created_at default, and inner ofMany constraints.

No code was pushed and nothing was merged.

@codecov

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

tmgbedu and others added 2 commits October 31, 2026 02:37
- Re-export relationship and RelationshipMethod from
  relationships/relationship.py so the old module path imports again;
  the package imports through it so its attribute stays the function
- Restore the ADR caveat about global scopes that hard-code the table
  name inside the correlated subquery
- Chain where_column in one_of_many

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmgbedu
tmgbedu merged commit dc0fa42 into main Oct 31, 2026
10 checks passed
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