Skip to content

feat: add query relation#1463

Open
LinboLen wants to merge 15 commits into
goravel:masterfrom
LinboLen:feat/relation_query
Open

feat: add query relation#1463
LinboLen wants to merge 15 commits into
goravel:masterfrom
LinboLen:feat/relation_query

Conversation

@LinboLen

@LinboLen LinboLen commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a relation declaration system and relation-aware query APIs for Has / WhereHas, morph existence checks, aggregate subselects, and framework-managed eager loading across direct, nested, polymorphic, many-to-many, and through relations.
  • Add Relation(...) and Related(...) ORM builders so applications can read and write relationships through a typed API, including association writes, pivot-table operations, OfMany helpers, and morph map resolution.
  • Expand ORM and integration coverage across supported drivers to verify relation loading, relation existence filtering, pivot writes, through relations, and the new Relations() model contract.

Closes #963

Why

This PR fills in the missing ORM relationship workflow that users expect from Laravel-style APIs: declaring relations in one place, querying parents by related records, eager loading nested relations, and mutating associations without dropping down to raw GORM behavior. The new contracts make relationship behavior explicit at the framework layer instead of relying on scattered struct-tag conventions and custom query code.

type User struct {
	ID    uint
	Name  string
	Books []*Book `gorm:"-"`
	Roles []*Role `gorm:"-"`
}

func (u *User) Relations() map[string]orm.Relation {
	return map[string]orm.Relation{
		"Books": orm.HasMany{Related: &Book{}},
		"Roles": orm.Many2Many{Related: &Role{}, Table: "role_user"},
	}
}

var users []User
err := facades.Orm().Query().
	Has("Books", ">=", 2).
	With("Books").
	WithCount("Roles").
	Get(&users)

The broader relation rework also enables relationship writers and polymorphic helpers to share the same metadata pipeline, which is why this branch includes eager-loader internals, pivot query support, morph maps, and new integration tests. That combination is what makes features like WhereHas, HasMorph, With("Books.Author"), Relation(parent, "Roles"), and through-relation queries behave consistently across drivers.

@LinboLen LinboLen requested a review from a team as a code owner May 4, 2026 01:37
@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.62041% with 1321 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.51%. Comparing base (950dc14) to head (e338e9a).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
database/gorm/eager_loader.go 22.48% 538 Missing and 10 partials ⚠️
database/gorm/relation_writes.go 39.54% 430 Missing and 50 partials ⚠️
database/gorm/queries_relationships.go 80.03% 88 Missing and 17 partials ⚠️
database/gorm/relation.go 79.88% 44 Missing and 29 partials ⚠️
database/gorm/new_relation.go 75.37% 20 Missing and 13 partials ⚠️
database/gorm/pivot_query.go 69.56% 20 Missing and 1 partial ⚠️
database/gorm/query.go 55.17% 11 Missing and 2 partials ⚠️
database/gorm/eager_load_parse.go 89.28% 8 Missing and 4 partials ⚠️
database/orm/orm.go 0.00% 12 Missing ⚠️
database/orm/morphmap/morphmap.go 86.48% 5 Missing and 5 partials ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1463      +/-   ##
==========================================
- Coverage   69.19%   68.51%   -0.69%     
==========================================
  Files         370      385      +15     
  Lines       29338    32519    +3181     
==========================================
+ Hits        20300    22279    +1979     
- Misses       8106     9156    +1050     
- Partials      932     1084     +152     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

LinboLen added 7 commits May 11, 2026 07:30
Introduce contracts and regenerate mocks for the unified relation rework:
Relation/RelationWriter/PivotQuery, ModelWithRelations,
RelationCallback/MorphRelationCallback/PivotCallback. Add the corresponding
OrmRelation* / OrmEagerLoad* errors and remove the obsolete Association
contract.

(cherry picked from commit 1a39fa5)
Introduce a morph map under database/orm/morphmap to register and resolve
polymorphic class aliases, plus the contracts/database/orm/morph.go and
database/orm/morph.go entry points used by relation queries to translate
between *_type column values and registered model types.

(cherry picked from commit 06b7cb8)
Replace the old Preload-based With() pipeline with an eager-load engine that
parses dotted paths, batches per-relation queries, and hydrates results back
onto the parent model. Add OfMany / LatestOfMany / OldestOfMany aggregate
selectors usable inside With() callbacks. Update Cursor() to run eager loads
per-row and Load() to forward variadic args to With().

(cherry picked from commit 65dedf6)
Add the read-side relation engine: relation.go resolves Relations() definitions
into typed Has-One/Has-Many/Belongs-To/MorphMany/etc. relations with shared
key inference; new_relation.go exposes the Related() helper for ad-hoc relation
queries; queries_relationships.go provides Has, OrHas, DoesntHave, HasMorph and
WhereRelation existence/absence builders driven by Conditions.relations.

(cherry picked from commit 611d2ca)
Add the relation write API (Save/Push/Associate/Dissociate/Attach/Detach/Sync/
Toggle) implemented over the unified relation resolver, plus a PivotQuery
builder for filtering and updating pivot rows including pivot-defined
created_at/updated_at columns. Introduce contracts.db.SyncResult to report
attached/detached/updated ids per Sync call.

(cherry picked from commit ee0fcde)
…ework

Add tests covering With/Has/HasMorph/Load/PivotQuery/Sync flows against the
real database drivers, refresh tests/mock_config.go and tests go.mod/go.sum
for the new dependencies, and document the many-to-many API in
docs/orm-many-to-many.md.

(cherry picked from commit 185c0a8)
(cherry picked from commit 9f6456e)
@LinboLen LinboLen force-pushed the feat/relation_query branch from f5a4bed to da8db93 Compare May 10, 2026 23:30
@LinboLen LinboLen changed the title wip feat: add query relation feat: add query relation May 10, 2026
@hwbrzzl

hwbrzzl commented May 16, 2026

Copy link
Copy Markdown
Contributor

Hey @LinboLen Is the PR ready to be reviewed, please? And FYI, CI failed.

@LinboLen

Copy link
Copy Markdown
Contributor Author

is tested pass on my local dev environment. currently is finished state. I will fix the ci failed. this pr not use gorm associate any more. use Related for relation query, Relation for relation modify.

Comment thread database/gorm/new_relation.go
Remove GORM relation tags from test model struct fields and implement
Relations() methods instead. This aligns with the new relation system
that forbids GORM tags and requires explicit relation declarations.

Changes:
- Add `gorm:"-"` tags to all relation fields
- Implement Relations() for User, Role, Address, and Book models
- Use contractsorm relation types (HasOne, HasMany, BelongsTo, Many2Many, MorphOne, MorphMany)

Fixes CI test failures in TestWithSuite.
@LinboLen

Copy link
Copy Markdown
Contributor Author

fixed all test case. and removed association code in query. the codecov is low. adding more test case

@LinboLen LinboLen force-pushed the feat/relation_query branch from a2128fa to c01e6cf Compare May 20, 2026 07:59
@LinboLen LinboLen force-pushed the feat/relation_query branch from c01e6cf to eda8bab Compare May 20, 2026 08:01
@LinboLen

LinboLen commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

some of api are real database read and write. already covered in another test case. so the codecov is down for now and can't add this coverage test

@hwbrzzl

hwbrzzl commented May 28, 2026

Copy link
Copy Markdown
Contributor

So sorry, I missed the messages. I will review the PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds Goravel’s metadata-driven query relation system and custom eager loading support, replacing GORM association/preload behavior with framework-managed relation declarations and loaders.

Changes:

  • Adds relation read/write APIs, relation descriptors, eager-load parsing/loading, morph map support, pivot query support, and one-of-many helpers.
  • Updates ORM contracts, mocks, errors, and test models to use declarative Relations().
  • Adds extensive unit/integration tests for relation resolution, SQL generation, eager loading, relation writers, morph maps, and With behavior.

Reviewed changes

Copilot reviewed 57 out of 59 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/with_test.go Adds integration coverage for With, Without, WithOnly, nested eager loads, callbacks, columns, and chunking.
tests/models.go Migrates test models to declarative Relations() and ignores relation fields for GORM.
tests/mock_config.go Adds mock config support for eager-load chunk size.
tests/go.mod Bumps indirect dst dependency.
tests/go.sum Updates checksum entries for dst.
testing/mock/mock.go Removes obsolete ORM association mock factory.
mocks/database/schema/ColumnType.go Removes generated mock no longer needed.
mocks/database/orm/RelationCallback.go Adds generated mock for relation callbacks.
mocks/database/orm/Relation.go Adds generated mock for relation contracts.
mocks/database/orm/PivotQuery.go Adds generated mock for pivot query callbacks.
mocks/database/orm/PivotCallback.go Adds generated mock for pivot callbacks.
mocks/database/orm/Orm.go Regenerates ORM mock with Related and Relation.
mocks/database/orm/MorphRelationCallback.go Adds generated mock for morph relation callbacks.
mocks/database/orm/ModelWithRelations.go Adds generated mock for relation-declaring models.
mocks/database/orm/ModelWithMorphClass.go Adds generated mock for morph-class models.
mocks/database/orm/Association.go Removes obsolete association mock.
errors/list.go Adds centralized ORM relation/eager-load/morph errors.
database/orm/orm.go Adds facade-level Related and Relation methods.
database/orm/morphmap/morphmap.go Adds process-wide morph alias registry.
database/orm/morphmap/morphmap_test.go Tests morph map registration and lookup behavior.
database/orm/morph.go Adds public morph map helper functions.
database/orm/model.go Removes exported GORM association constant.
database/gorm/row.go Applies eager loads after row scanning.
database/gorm/row_test.go Adds row error behavior tests.
database/gorm/relation_writer.go Adds relation writer delegating to relation write operations.
database/gorm/relation_writer_test.go Tests relation writer binding and delegation paths.
database/gorm/relation_test.go Tests relation descriptor resolution and forbidden tag handling.
database/gorm/relation_sql_test.go Pins generated SQL for relation queries and subqueries.
database/gorm/relation_sql_capture_test.go Adds opt-in SQL capture helper test.
database/gorm/query.go Wires relation query APIs, eager loads, aggregates, and removes association-specific behavior.
database/gorm/queries_relationships_test.go Tests relationship query queueing and helper parsing.
database/gorm/pivot_query.go Adds pivot query callback implementation.
database/gorm/pivot_query_test.go Tests pivot query SQL and descriptor wiring.
database/gorm/one_of_many.go Adds one-of-many eager-load query rewrite.
database/gorm/one_of_many_test.go Tests one-of-many joins and shortcut APIs.
database/gorm/new_relation.go Adds Related query builders for each relation kind.
database/gorm/new_relation_test.go Tests Related query behavior and morph resolution.
database/gorm/event_extra_test.go Adds supplemental event behavior tests.
database/gorm/eager_loader_test.go Tests eager-load helpers, assignment, and pivot hydration helpers.
database/gorm/eager_load_parse.go Adds parser for With argument shapes.
database/gorm/eager_load_parse_test.go Tests eager-load argument parsing and chunking helpers.
database/gorm/conditions.go Extends query conditions with relation/eager-load state.
database/gorm/build_relations_test.go Tests building relation existence and aggregate subqueries.
contracts/database/orm/relation_writer.go Adds relation write-side contract.
contracts/database/orm/relation_test.go Tests relation kind constants and implementations.
contracts/database/orm/orm.go Extends ORM/query contracts for relation APIs and eager-load shapes.
contracts/database/orm/morph.go Adds morph-class contract.
contracts/database/db/db.go Adds sync result contract for pivot operations.

Comment thread database/gorm/eager_loader.go Outdated
Comment on lines +436 to +439
return r.freshSession().
Table(desc.pivotTable).
Select(selectArgs[0], selectArgs[1:]...).
Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(pivotParentCol)), chunk)
Comment thread database/gorm/eager_loader.go Outdated
Comment on lines +479 to +482
// Build pivot data map: key = relatedID, value = map of pivot column values to hydrate.
var pivotDataByRelatedID map[string]map[string]any
if pivotPlan != nil {
pivotDataByRelatedID = make(map[string]map[string]any, len(pivotRows))
Comment thread database/gorm/eager_loader.go Outdated
Comment on lines +572 to +576
return r.freshSession().
Table(desc.pivotTable).
Select(selectArgs[0], selectArgs[1:]...).
Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(pivotParentCol)), chunk).
Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue)
Comment thread database/gorm/eager_loader.go Outdated
Comment on lines +616 to +620
// Build pivot data map: key = relatedID, value = map of pivot column values to hydrate.
var pivotDataByRelatedID map[string]map[string]any
if pivotPlan != nil {
pivotDataByRelatedID = make(map[string]map[string]any, len(pivotRows))
for _, p := range pivotRows {
Comment thread database/gorm/eager_loader.go Outdated
Comment on lines +986 to +990
v := r.config.GetInt("database.eager_load_chunk_size", defaultEagerLoadChunkSize)
if v == 0 {
return defaultEagerLoadChunkSize
}
return v
Comment thread database/gorm/new_relation.go

@hwbrzzl hwbrzzl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the amazing PR. But it's so big that it is hard to be reviewed. I'm not sure how much the core logic is. Is it possible to split it into several PRs via AI? I just reviewed the contracts and tests for now, I left some questions. I'm glad to have a deep discussion on them with you.


type Query interface {
// Association gets an association instance by name.
Association(association string) Association

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will Relation replace this function? It's a break change.

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.

there are no need Association any more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can both of them be kept for now? We don't want to have such a break change in the next major version.

Comment thread tests/models.go
return &UserFactory{}
}

func (r *User) Relations() map[string]contractsorm.Relation {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this function required in the new implementation?

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.

yes it's required for define relation. no need gorm relation annotation

Comment thread tests/models.go
House *House `gorm:"polymorphic:Houseable"`
Phones []*Phone `gorm:"polymorphic:Phoneable"`
Roles []*Role `gorm:"many2many:role_user"`
Address *Address `gorm:"-"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is gorm:"-" required when defining the relation?

@LinboLen LinboLen May 29, 2026

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.

Yes, gorm:"-" IS required when defining relations in the new system.

  1. The new relation system uses Relations() method - Relations are now defined programmatically via the Relations() method that returns a map of relation definitions, not
    through struct tags.
  2. Without gorm:"-", GORM will try to persist these fields - If you don't add gorm:"-", GORM will treat relation fields (like Books []*Book, Address *Address) as regular
    database columns and try to:
    - Create columns for them in migrations
    - Serialize/deserialize them during queries
    - This will cause errors since these are complex types that can't be stored directly
  3. The tag tells GORM to ignore these fields - The gorm:"-" tag instructs GORM to skip these fields entirely during its normal operations. The framework's relation system
    then handles loading these fields separately through the Relations() definitions.
  4. This is a breaking change from the old system - Previously, you'd use tags like:
    - gorm:"polymorphic:Houseable"
    - gorm:"many2many:role_user"

Now you use:
- gorm:"-" on the struct field
- Define the relation in Relations() method

Example from the codebase:

type User struct {
Model
Name string
Address *Address gorm:"-" // ← Required!
Books []*Book gorm:"-" // ← Required!
}

func (r *User) Relations() map[string]contractsorm.Relation {
return map[string]contractsorm.Relation{
"Address": contractsorm.HasOne{Related: &Address{}},
"Books": contractsorm.HasMany{Related: &Book{}},
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, I got it. but I think it's a bit redundant when defining a relation. Can the GORM tag be removed directly, or use another new tag to replace it? For example, The relations are defined in the model currently. Can we ignore the tag based on the relation names?

Comment thread tests/queries_relationships_test.go Outdated
for driver, query := range s.queries {
s.Run(driver, func() {
user := &User{Name: "rel_bt_user"}
s.Nil(query.Query().Create(&user))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

&user == &&User{Name: "rel_bt_user"}, is it expected?

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.

fixed

Comment thread tests/queries_relationships_test.go
)
}

func (s *QueriesRelationshipsTestSuite) TestSQL_OrHas() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you add some specific tests for Or** functions?

Comment thread tests/with_test.go
Comment thread tests/with_test.go
Comment thread tests/query_test.go
Comment on lines -71 to -73
Address: &Address{
Name: "association_find_address",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can't user create the relation like this when using the new logic? It's a break change.

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.

yes. implement with laravel style. not gorm style

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Em, most of users are using this feature. The impact is enormous. We should find a solution to adapt it.

The OR logic means all users should match:
- alice: doesn't have target_role (matches 2nd condition)
- bob: doesn't have target_book (matches 1st condition)
- carol: doesn't have either (matches both conditions)
@LinboLen

Copy link
Copy Markdown
Contributor Author

建议不要拆分,理由:

  1. 核心代码只有140行左右
  2. 大部分是删除旧代码(5000行)和测试(800行)
  3. 这是一个必须整体工作的功能特性

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.

3 participants