Skip to content

Add R.Loaded to track which relationships have been loaded#671

Merged
stephenafamo merged 4 commits into
stephenafamo:mainfrom
jacobmolby:track-relationship-loading
May 8, 2026
Merged

Add R.Loaded to track which relationships have been loaded#671
stephenafamo merged 4 commits into
stephenafamo:mainfrom
jacobmolby:track-relationship-loading

Conversation

@jacobmolby
Copy link
Copy Markdown
Contributor

@jacobmolby jacobmolby commented May 6, 2026

Why

Today, the only way to inspect whether a generated relationship has been loaded is to look at the value in R. That collapses two distinct states into one:

  • A to-one relation: model.R.Pilot == nil could mean "never loaded" or "loaded, no related row".
  • A to-many relation: len(model.R.Jets) == 0 could mean "never loaded" or "loaded, zero rows" (loaders reset to nil and then append, so the zero-row case is indistinguishable from never-loaded).

Callers have no reliable way to tell these apart, which is a real footgun when the FK is nullable or the related set may
legitimately be empty.

Solution

A new typed struct, nested inside R as R.Loaded, with one bool per relationship.

if pilot.R.Loaded.Airport && pilot.R.Airport == nil {
    // definitively no airport
}

if pilot.R.Loaded.Jets && len(pilot.R.Jets) == 0 {
    // definitively zero jets
}

R.Loaded.X is set to true by:

  • LoadX, Preload.X, and ThenLoad.X after they populate R.X (including the zero-row case).
  • The inverse-side assignment performed during loading (e.g. pilots.LoadJets(...) flips jet.R.Loaded.Pilot on each child).
  • AttachX / InsertX for to-one relations, where the relation is fully known after the call.
  • Factory Build and Create

R.Loaded.X is not changed by AttachX / InsertX for to-many relations, since appending rows does not turn a partial slice into a complete one.

Behaviour change: slice LoadX now resets stale to-one values

In 110_loaders.go.tpl, the per-parent reset loop in both slice-load paths previously ran only for to-many relations - slice LoadX on a to-one relation would leave a stale R.X in place if the new query returned no match for that parent. Single-object LoadX already reset, so the two paths were inconsistent.

We remove the {{if $rel.IsToMany}} guard around that reset, so slice LoadX on a to-one now also clears R.X per parent before the inner match loop. This is required for the new R.Loaded contract to hold - without it, a parent could end up withLoaded.X == true while R.X still points at a row from an earlier load.

Other Notes

  • The relationship alias Loaded is now reserved; generation fails with a clear error if any relationship is aliased that way.
  • Hand-mutating R directly (e.g. jet.R.Pilot = pilot) does not update R.Loaded

@stephenafamo
Copy link
Copy Markdown
Owner

I'm not sure about this, because it reserves a relationship name

@jacobmolby
Copy link
Copy Markdown
Contributor Author

I can make the "Loaded" name configurable in the bobgen config, so users can change it themselves, rather than aliasing their own relations in case of conflict?

@stephenafamo
Copy link
Copy Markdown
Owner

Alright, I like that idea 👍🏾

@jacobmolby
Copy link
Copy Markdown
Contributor Author

It's updated now :)

@stephenafamo stephenafamo merged commit 4adc544 into stephenafamo:main May 8, 2026
8 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.

2 participants