Skip to content

feat: add belongs-to-many pivot models - #353

Merged
elpete merged 5 commits into
nextfrom
feat/belongs-to-many-pivot-model
Aug 24, 2026
Merged

feat: add belongs-to-many pivot models#353
elpete merged 5 commits into
nextfrom
feat/belongs-to-many-pivot-model

Conversation

@elpete

@elpete elpete commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Review

This extracts and expands the pivot-table behavior that was previously bundled into #331. It is the first PR in the stack; #331 will contain only relationship create() support after this PR lands.

Eloquent's belongs-to-many model is the reference point: related models always carry an intermediate model containing both pivot keys, withPivot() selects additional columns, as() renames the accessor, using() selects a custom model, and the relationship owns pivot-specific query and write helpers. Reference: https://laravel.com/docs/12.x/eloquent-relationships#retrieving-intermediate-table-columns

Quick semantics and choices

  • Every BelongsToMany result receives a loaded pivot relationship containing both pivot keys. withPivot() adds declared columns.
  • as() renames that loaded relationship, so normal Quick access such as getSubscription() works.
  • The default Pivot@quick model is read-only. Its schema is assembled at runtime, so allowing save() would imply persistence guarantees Quick cannot safely make.
  • using( "PostTag" ) opts into a declared Pivot entity. It supports normal Quick properties, aliases, casts, methods, lifecycle hooks, and explicit save(); it must extend quick.models.Relationships.Pivot.
  • Pivot models retain their parent and related entities through getPivotParent() and getPivotRelated(), without introducing cascading persistence.
  • Pivot keys cannot be overwritten by extra attachment/update data.
  • withTimestamps() accepts explicit created/modified column names because Quick applications do not share one universal timestamp convention.
  • Multi-ID attach() and sync() intentionally apply one pivot-attribute struct to every row. Per-ID attribute maps are not introduced in this PR.

API

  • withPivot()
  • as()
  • using()
  • withTimestamps()
  • wherePivot(), orWherePivot()
  • wherePivotIn(), wherePivotNotIn()
  • wherePivotBetween(), wherePivotNotBetween()
  • wherePivotNull(), wherePivotNotNull()
  • orderByPivot(), orderByPivotDesc()
  • withPivotValue()
  • pivot attributes on attach() and sync()
  • updateExistingPivot()

Test-first evidence

Before implementation, seven public relationship-flow specs errored because withPivot(), using(), and withTimestamps() did not exist. The tests exercise lazy and eager loading, the default and custom accessors, a custom Pivot cast and method, explicit custom-Pivot persistence, the read-only default model, pivot constraints/order, attachment attributes, updates, defaults, and timestamps.

Validation

  • Focused belongs-to-many suite: 11 passed, 0 failed, 0 errors
  • Full suite on Lucee 6: 566 passed, 0 failed, 0 errors, 3 skipped
  • Formatter completed
  • git diff --check passed

Supports #84.

@elpete

elpete commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

CI follow-up: the first matrix exposed three portability details in the new pivot hydration path—Adobe property declaration ordering, Adobe full-null handling for an unset custom pivot mapping, and BoxLang representing the declared-but-unset mapping as null. Those paths now use engine-neutral component ordering and an explicit empty-string default. The final matrix is green across all 27 checks.

@elpete
elpete merged commit 56103b9 into next Aug 24, 2026
27 checks passed
@elpete
elpete deleted the feat/belongs-to-many-pivot-model branch August 24, 2026 19:26
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