From 2bf0b1b1a6fcc106480a798a30223f690fd24d42 Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:21:20 +0800 Subject: [PATCH 01/11] feat(orm): add relation, relation writer, and pivot query contracts 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 1a39fa53e8018b5bb94afc5e3afb618de0c802f8) --- contracts/database/orm/orm.go | 92 +- contracts/database/orm/relation.go | 446 +++ contracts/database/orm/relation_writer.go | 94 + database/orm/orm.go | 25 + errors/list.go | 56 +- mocks/database/orm/Association.go | 344 -- mocks/database/orm/ModelWithMorphClass.go | 77 + mocks/database/orm/ModelWithRelations.go | 82 + mocks/database/orm/MorphRelationCallback.go | 84 + mocks/database/orm/Orm.go | 98 + mocks/database/orm/PivotCallback.go | 83 + mocks/database/orm/PivotQuery.go | 288 ++ mocks/database/orm/Query.go | 3686 ++++++++++++++----- mocks/database/orm/QueryWithRelations.go | 1406 +++++++ mocks/database/orm/Relation.go | 80 + mocks/database/orm/RelationCallback.go | 83 + mocks/database/orm/RelationWriter.go | 1216 ++++++ 17 files changed, 6868 insertions(+), 1372 deletions(-) create mode 100644 contracts/database/orm/relation.go create mode 100644 contracts/database/orm/relation_writer.go delete mode 100644 mocks/database/orm/Association.go create mode 100644 mocks/database/orm/ModelWithMorphClass.go create mode 100644 mocks/database/orm/ModelWithRelations.go create mode 100644 mocks/database/orm/MorphRelationCallback.go create mode 100644 mocks/database/orm/PivotCallback.go create mode 100644 mocks/database/orm/PivotQuery.go create mode 100644 mocks/database/orm/QueryWithRelations.go create mode 100644 mocks/database/orm/Relation.go create mode 100644 mocks/database/orm/RelationCallback.go create mode 100644 mocks/database/orm/RelationWriter.go diff --git a/contracts/database/orm/orm.go b/contracts/database/orm/orm.go index 11580a947..5d54dbd15 100644 --- a/contracts/database/orm/orm.go +++ b/contracts/database/orm/orm.go @@ -21,6 +21,27 @@ type Orm interface { DatabaseName() string // Name gets the current connection name. Name() string + // Related returns a Query pre-scoped to the related rows for the given parent and relation + // name. parent must be a non-nil pointer to a struct. The returned Query is a fresh chain — + // call any of Where / OrderBy / Get / First / Count / etc. on it. + // + // Per-kind shape: + // - HasOne / HasMany: Query().Model(related).Where("", parent.) + // - BelongsTo: Query().Model(related).Where("", parent.) + // - MorphOne / MorphMany: HasMany shape + Where("", desc.morphValue) + // - MorphTo: type resolved from morph map; Query().Model().Where("", parent.) + // - Many2Many / MorphToMany / MorphedByMany: Query().Table(related).Joins("INNER JOIN ON ...").Where(".", parent.) + // - HasOneThrough / HasManyThrough: Query().Table(related).Joins("INNER JOIN ON ...").Where(".", parent.) + // + // Mirrors fedaco's model.NewRelation('foo') for the read path. Write operations live on + // RelationWriter (see Orm.Relation) — they are not chained off this Query. + Related(parent any, relation string) Query + // Relation returns a RelationWriter bound to (parent, name) for FK-safe write operations. + // All write methods (Save / Create / UpdateOrCreate / Attach / Sync / Detach / Toggle / + // Associate / Dissociate / etc.) are reached via this builder rather than as flat methods on + // Orm — the (parent, name) pair binds once. + Relation(parent any, name string) RelationWriter + // Observe registers an observer with the Orm. Observe(model any, observer Observer) // Query gets a new query builder instance. @@ -36,8 +57,18 @@ type Orm interface { } type Query interface { - // Association gets an association instance by name. - Association(association string) Association + // QueryWithRelations exposes the QueriesRelationships surface (Has / WhereHas / WithCount / + // HasMorph / etc.). Embedding it into Query lets users chain relationship queries with the + // rest of the builder: q.Where(...).Has("Books", ">=", 3).Get(&users). + QueryWithRelations + // Related returns a Query pre-scoped to the related rows for parent.name. Mirrors Orm.Related + // but lives on Query so it can be used inside a Transaction callback. parent must be a + // non-nil pointer to a struct. + Related(parent any, name string) Query + // Relation returns a RelationWriter bound to (parent, name) for FK-safe write operations. + // Mirrors Orm.Relation but lives on Query so writes inside a Transaction callback honor the + // transaction. + Relation(parent any, name string) RelationWriter // Begin begins a new transaction // DEPRECATED Use BeginTransaction instead. Begin() (Query, error) @@ -101,14 +132,31 @@ type Query interface { Join(query string, args ...any) Query // Limit the number of records returned. Limit(limit int) Query - // Load loads a relationship for the model. + // Load loads a relationship for the model. args may be a callback or other + // shapes accepted by With (e.g. "Books:id,name" column pruning is supported by + // embedding the column list in relation; a callback may be passed via args). Load(dest any, relation string, args ...any) error // LoadMissing loads a relationship for the model that is not already loaded. + // args follow the same shapes as Load. LoadMissing(dest any, relation string, args ...any) error // LockForUpdate locks the selected rows in the table for updating. LockForUpdate() Query // Model sets the model instance to be queried. Model(value any) Query + // OfMany configures a HasOne or MorphOne relation to return the row whose value of column + // matches the given SQL aggregate ("MAX" / "MIN") within each parent. Intended for use + // inside a With(...) callback so the rewrite is local to a single relation: + // + // q.With("LatestImage", func(q orm.Query) orm.Query { + // return q.OfMany("created_at", "MAX") + // }) + OfMany(column, aggregate string) Query + // LatestOfMany is shorthand for OfMany(column, "MAX") with column defaulting to "id" when + // empty. Mirrors fedaco's latestOfMany(). + LatestOfMany(column ...string) Query + // OldestOfMany is shorthand for OfMany(column, "MIN") with column defaulting to "id" when + // empty. Mirrors fedaco's oldestOfMany(). + OldestOfMany(column ...string) Query // Offset specifies the number of records to skip before starting to return the records. Offset(offset int) Query // Omit specifies columns that should be omitted from the query. @@ -221,10 +269,29 @@ type Query interface { WithoutEvents() Query // WithoutGlobalScopes disables all global scopes for the query. WithoutGlobalScopes(names ...string) Query + // Without removes the given relations from the eager-load list set by With. + // Mirrors fedaco's without(). + Without(relations ...string) Query // WithTrashed allows soft deleted models to be included in the results. WithTrashed() Query - // With returns a new query instance with the given relationships eager loaded. - With(query string, args ...any) Query + // With eagerly loads the given relationships using Goravel's own loader (does not + // delegate to GORM Preload). Accepts the union of fedaco's with(...) shapes: + // + // q.With("Books") + // q.With("Books", cb) // string + callback + // q.With("Books", "Roles", "Address") // multiple strings + // q.With("Books:id,name") // column pruning + // q.With(map[string]orm.RelationCallback{"Books": cb}) // map of name -> callback + // q.With([]any{"Books", map[string]orm.RelationCallback{"Roles": cb}}) + // q.With("Books.Author") // nested + // q.With("Books.Author", cb) // nested + callback + // + // Supports HasOne, HasMany, BelongsTo, BelongsToMany, MorphOne, MorphMany, + // HasOneThrough and HasManyThrough. + With(args ...any) Query + // WithOnly clears the eager-load list set by With, then adds the given + // relations. Mirrors fedaco's withOnly(). + WithOnly(args ...any) Query } type QueryWithContext interface { @@ -235,21 +302,6 @@ type QueryWithObserver interface { Observe(model any, observer Observer) } -type Association interface { - // Find finds records that match given conditions. - Find(out any, conds ...any) error - // Append appending a model to the association. - Append(values ...any) error - // Replace replaces the association with the given value. - Replace(values ...any) error - // Delete deletes the given value from the association. - Delete(values ...any) error - // Clear clears the association. - Clear() error - // Count returns the number of records in the association. - Count() int64 -} - type ModelWithConnection interface { // Connection gets the connection name for the model. Connection() string diff --git a/contracts/database/orm/relation.go b/contracts/database/orm/relation.go new file mode 100644 index 000000000..4b8cca5a0 --- /dev/null +++ b/contracts/database/orm/relation.go @@ -0,0 +1,446 @@ +package orm + +// RelationCallback is the signature of a closure used to scope a relationship existence query. +// Mirrors the (q) => void closure shape from the QueriesRelationships mixin. The returned Query +// is the inner subquery used for has / whereHas / withCount / etc. +type RelationCallback func(query Query) Query + +// MorphRelationCallback is the per-type variant of RelationCallback used by the *HasMorph family, +// matching the `function ($query, $type)` callback for whereHasMorph. The second argument is the +// morph type currently being scoped (the related model's morph class - the table name in GORM's +// polymorphic convention). +type MorphRelationCallback func(query Query, morphType string) Query + +// PivotQuery is a small builder surface for scoping the pivot-table SQL emitted by Sync / Attach +// duplicate-skip / Detach / UpdateExistingPivot. It is intentionally narrower than the full Query +// interface — only the WHERE-style methods that make sense on a single-table pivot read/write are +// exposed. Mirrors fedaco's `wherePivot` / `wherePivotIn` / `wherePivotNull` accumulators on +// BelongsToMany. +// +// NOTE: PivotQuery filters only SELECT / UPDATE / DELETE on the pivot table — equality conditions +// added here are NOT auto-injected into INSERT rows on Attach. Callers that need the conditions +// to appear on inserted rows should pass them through Attach attrs. +type PivotQuery interface { + // Where adds a `column op value` clause to the pivot query. operator defaults to "=" when + // only two args are passed (column, value). + Where(column string, args ...any) PivotQuery + // WhereIn adds a `column IN (...)` clause to the pivot query. + WhereIn(column string, values []any) PivotQuery + // WhereNotIn adds a `column NOT IN (...)` clause to the pivot query. + WhereNotIn(column string, values []any) PivotQuery + // WhereNull adds a `column IS NULL` clause to the pivot query. + WhereNull(column string) PivotQuery + // WhereNotNull adds a `column IS NOT NULL` clause to the pivot query. + WhereNotNull(column string) PivotQuery +} + +// PivotCallback scopes pivot-table reads (and the corresponding diff-driven writes) for a +// BelongsToMany relation. Declared via Many2Many / MorphToMany / MorphedByMany's OnPivotQuery +// field; applied automatically to Sync / Detach / UpdateExistingPivot and Attach's duplicate- +// detection SELECT. +type PivotCallback func(query PivotQuery) PivotQuery + +// QueryWithRelations is the Go port of the QueriesRelationships mixin from Laravel and the 1:1 +// TypeScript port in fedaco at libs/fedaco/src/fedaco/mixins/queries-relationships.ts. +// +// Where the upstream framework has first-class Relation objects with getRelationExistenceQuery +// methods, GORM models its relationships through struct-tag metadata. The bridge here surfaces +// relationship-existence and aggregate-subselect queries on top of that metadata. Callers can +// write, for example: +// +// users := []User{} +// query.Query().Has("Books", ">=", 3).WithCount("Roles").Get(&users) +// +// Existence-style methods (Has / OrHas / DoesntHave / WhereHas / ...) accept a variadic args +// slice that may carry, in any order: +// - a RelationCallback or func(Query) Query to scope the inner subquery +// - a string operator (e.g. ">=", "<", ">", "=") - defaults to ">=" +// - an int count - defaults to 1 +// +// Morph-style methods take an additional types []any of model instances; the morph value used in +// the type column is derived from each model's GORM-resolved table name (e.g. *User -> "users"). +// +// QueryWithRelations is embedded into Query, so all of these methods are also reachable directly +// off Query without a type assertion. +type QueryWithRelations interface { + // Has adds a relationship count / exists condition to the query. + // Defaults to operator ">=" and count 1. + Has(relation string, args ...any) Query + // OrHas adds a relationship count / exists condition to the query with an "or" conjunction. + OrHas(relation string, args ...any) Query + // DoesntHave adds a relationship absence condition - equivalent to Has(rel, "<", 1). + DoesntHave(relation string, args ...any) Query + // OrDoesntHave adds a relationship absence condition with an "or" conjunction. + OrDoesntHave(relation string, args ...any) Query + // WhereHas adds a relationship count / exists condition to the query with where clauses. + // Identical semantics to Has but conventionally used with a callback first arg. + WhereHas(relation string, args ...any) Query + // OrWhereHas adds a relationship count / exists condition to the query with where clauses + // and an "or" conjunction. + OrWhereHas(relation string, args ...any) Query + // WhereDoesntHave adds a relationship absence condition to the query with where clauses. + WhereDoesntHave(relation string, args ...any) Query + // OrWhereDoesntHave adds a relationship absence condition to the query with where clauses + // and an "or" conjunction. + OrWhereDoesntHave(relation string, args ...any) Query + + // HasMorph adds a polymorphic relationship count / exists condition to the query. + // types is a slice of model instances (e.g. []any{&Post{}, &Video{}}); the morph value + // used in the type column is derived from each model's table name. + // + // Note: auto-discovery of distinct morph values via `types = ['*']` is not supported. + // An explicit list of model instances is required. + HasMorph(relation string, types []any, args ...any) Query + // OrHasMorph adds a polymorphic relationship count / exists condition with an "or" conjunction. + OrHasMorph(relation string, types []any, args ...any) Query + // DoesntHaveMorph adds a polymorphic relationship absence condition. + DoesntHaveMorph(relation string, types []any, args ...any) Query + // OrDoesntHaveMorph adds a polymorphic relationship absence condition with an "or" conjunction. + OrDoesntHaveMorph(relation string, types []any, args ...any) Query + // WhereHasMorph adds a polymorphic relationship count / exists condition to the query with + // where clauses. Callbacks may be MorphRelationCallback for per-type scoping. + WhereHasMorph(relation string, types []any, args ...any) Query + // OrWhereHasMorph adds a polymorphic relationship count / exists condition with where clauses + // and an "or" conjunction. + OrWhereHasMorph(relation string, types []any, args ...any) Query + // WhereDoesntHaveMorph adds a polymorphic relationship absence condition with where clauses. + WhereDoesntHaveMorph(relation string, types []any, args ...any) Query + // OrWhereDoesntHaveMorph adds a polymorphic relationship absence condition with where clauses + // and an "or" conjunction. + OrWhereDoesntHaveMorph(relation string, types []any, args ...any) Query + + // WithAggregate adds a sub-select query to include an aggregate value for a relationship. + // fn must be one of: count, max, min, sum, avg, exists. + WithAggregate(relation, column, fn string, args ...any) Query + // WithCount adds sub-select queries to count the relations. Each entry may be either a + // string ("Books") or a RelationCount struct for scoped/aliased counts. + WithCount(relations ...any) Query + // WithMax adds sub-select queries to include the max of the relation's column. + WithMax(relation, column string, args ...any) Query + // WithMin adds sub-select queries to include the min of the relation's column. + WithMin(relation, column string, args ...any) Query + // WithSum adds sub-select queries to include the sum of the relation's column. + WithSum(relation, column string, args ...any) Query + // WithAvg adds sub-select queries to include the average of the relation's column. + WithAvg(relation, column string, args ...any) Query + // WithExists adds sub-select queries to include the existence of related models. The result + // is emitted as `CASE WHEN EXISTS (...) THEN 1 ELSE 0 END` for cross-dialect portability + // (SQL Server has no boolean literal), but the dest field may be either `bool` or an integer + // type - Go's database/sql layer converts 0/1 ints to bool automatically. + WithExists(relations ...string) Query +} + +// RelationCount is an entry accepted by WithCount that pairs a relation name with an optional +// scope callback and result alias. Equivalent to the array-keyed `withCount(['posts as p_count' => +// fn ...])` idiom in Laravel, expressed as a Go struct: +// +// q.WithCount(orm.RelationCount{Name: "Books", Alias: "book_total", Callback: func(q) q.Where(...)}) +type RelationCount struct { + // Name is the relation method/field name on the parent model (e.g. "Books"). + Name string + // Alias overrides the default `_count` column alias when non-empty. + Alias string + // Callback scopes the inner count subquery, mirroring the upstream array-keyed callback shape. + Callback RelationCallback +} + +// RelationKind names a relationship flavour for diagnostic / error-message use only. The +// per-kind structs below (HasOne, HasMany, ...) are the actual user-facing declaration types; +// the RelationKind constants exist purely so error messages can refer to a kind by name. +type RelationKind string + +const ( + KindHasOne RelationKind = "hasOne" + KindHasMany RelationKind = "hasMany" + KindBelongsTo RelationKind = "belongsTo" + KindMany2Many RelationKind = "many2Many" + KindMorphOne RelationKind = "morphOne" + KindMorphMany RelationKind = "morphMany" + KindMorphTo RelationKind = "morphTo" + KindMorphToMany RelationKind = "morphToMany" + KindMorphedByMany RelationKind = "morphedByMany" + KindHasOneThrough RelationKind = "hasOneThrough" + KindHasManyThrough RelationKind = "hasManyThrough" +) + +// Relation is the sealed interface implemented by every per-kind relation declaration struct +// (HasOne, HasMany, BelongsTo, Many2Many, MorphOne, MorphMany, MorphTo, MorphToMany, +// MorphedByMany, HasOneThrough, HasManyThrough). The relation() method is unexported so external +// packages cannot define new kinds — the resolver type-switches on the closed set defined here. +// +// Models declare their relationships in a single map: +// +// func (User) Relations() map[string]orm.Relation { +// return map[string]orm.Relation{ +// "Books": orm.HasMany{Related: &Book{}}, +// "Roles": orm.Many2Many{Related: &Role{}, Table: "user_roles"}, +// "Houses": orm.MorphMany{Related: &House{}, Name: "houseable"}, +// "Posts": orm.HasManyThrough{Related: &Post{}, Through: &Account{}}, +// } +// } +// +// All relation fields on the model struct must be tagged `gorm:"-"` so GORM doesn't try to +// auto-resolve them from struct tags. +type Relation interface { + // Kind returns the relation flavour for diagnostics (error messages, logging). The resolver + // itself dispatches by Go type, not by the Kind value. + Kind() RelationKind +} + +// HasOne declares a one-to-one relation where the related row holds a foreign key referencing +// this model. +// +// Defaults: ForeignKey = singular(parentTable) + "_id"; LocalKey = "id". +type HasOne struct { + // Related is a sample instance of the related model (e.g. &Profile{}). + Related any + // ForeignKey is the column on the related table referencing the parent. Optional. + ForeignKey string + // LocalKey is the column on the parent referenced by ForeignKey. Optional, defaults to "id". + LocalKey string + // OnQuery is a default scope applied to every query built for this relation (eager loads, + // existence checks, aggregates, Related). Applied before any caller-supplied callback. + OnQuery RelationCallback +} + +func (HasOne) Kind() RelationKind { return KindHasOne } + +// HasMany declares a one-to-many relation — the multi-result variant of HasOne. +// +// Defaults: ForeignKey = singular(parentTable) + "_id"; LocalKey = "id". +type HasMany struct { + Related any + ForeignKey string + LocalKey string + OnQuery RelationCallback +} + +func (HasMany) Kind() RelationKind { return KindHasMany } + +// BelongsTo declares the inverse of HasOne / HasMany — this model holds a foreign key +// referencing the related row. +// +// Defaults: ForeignKey = singular(relatedTable) + "_id"; OwnerKey = "id". +type BelongsTo struct { + Related any + // ForeignKey is the column on the parent table referencing the related row. Optional. + ForeignKey string + // OwnerKey is the column on the related table referenced by ForeignKey. Optional, "id". + OwnerKey string + OnQuery RelationCallback +} + +func (BelongsTo) Kind() RelationKind { return KindBelongsTo } + +// Many2Many declares a many-to-many relation through a pivot table. +// +// Defaults: +// +// Table = alphabetical singular pair (e.g. "post_tag") +// ForeignPivotKey = singular(parentTable) + "_id" +// RelatedPivotKey = singular(relatedTable) + "_id" +// ParentKey = "id" +// RelatedKey = "id" +type Many2Many struct { + Related any + // Table is the pivot table name. Optional. + Table string + // ForeignPivotKey is the pivot column referencing the parent. Optional. + ForeignPivotKey string + // RelatedPivotKey is the pivot column referencing the related. Optional. + RelatedPivotKey string + // ParentKey is the column on the parent referenced by ForeignPivotKey. Optional, "id". + ParentKey string + // RelatedKey is the column on the related referenced by RelatedPivotKey. Optional, "id". + RelatedKey string + // PivotField is the name of the struct field on the related model that the eager loader will + // hydrate with pivot column values (e.g. "Pivot", "UserPivot"). Optional — defaults to + // "Pivot". The field's Go type drives both the pivot SELECT list (every db-tagged column on + // the struct) and the hydration target. When the related model has no field by this name, + // no Pivot hydration happens — the relation still works for joining, just doesn't surface + // pivot columns. Use a non-default name when one related model serves multiple m2m relations + // with different pivot schemas (e.g. Role with both UserPivot and GroupPivot fields). + PivotField string + // PivotTimestamps enables auto-stamping of the pivot table's created_at / updated_at columns + // on Attach / Sync / Save (and updated_at on UpdateExistingPivot), using default column names. + // Most users don't need to set this flag explicitly — see "Detection priority" below. + // + // Detection priority for pivot timestamps (highest first): + // + // 1. Pivot struct field with `gorm:"autoCreateTime"` / `gorm:"autoUpdateTime"` tag. Works + // for any field name; column name is taken from the struct's GORM schema. + // 2. Pivot struct field named CreatedAt / UpdatedAt of type time.Time (Go/GORM convention). + // 3. PivotTimestamps: true. Fallback for when no Pivot struct is declared (or its struct + // has no timestamp fields) but the underlying table still has created_at / updated_at + // columns you want auto-filled. Uses default column names. + // + // Customize column names via `gorm:"column:..."` on the Pivot struct field. There is + // intentionally no relation-level override — the Pivot struct is the single source of truth + // for column metadata. + PivotTimestamps bool + OnQuery RelationCallback + // OnPivotQuery scopes pivot-table SELECT / UPDATE / DELETE for Sync / Detach / + // UpdateExistingPivot operations on this relation. Equality conditions added here are NOT + // auto-injected into Attach INSERT rows — pass them via Attach attrs if needed. + OnPivotQuery PivotCallback + // Touches, when true, causes Sync / Attach / Detach / Toggle / UpdateExistingPivot on this + // relation to bump the parent's `updated_at` after the pivot write completes (and only when + // the operation actually changed pivot rows). Mirrors fedaco's `touchIfTouching`. Silently + // no-ops when the parent's schema doesn't carry an updated_at field. + Touches bool +} + +func (Many2Many) Kind() RelationKind { return KindMany2Many } + +// MorphOne declares a one-to-one polymorphic relation — the related row holds _id and +// _type referencing one of several possible parent kinds. +// +// Defaults: TypeColumn = Name + "_type"; IDColumn = Name + "_id"; LocalKey = "id". +type MorphOne struct { + Related any + // Name is the polymorphic name (e.g. "imageable", "taggable"). Required. + Name string + // TypeColumn is the polymorphic type column on the related table. Optional. + TypeColumn string + // IDColumn is the polymorphic id column on the related table. Optional. + IDColumn string + // LocalKey is the column on the parent referenced by IDColumn. Optional, "id". + LocalKey string + OnQuery RelationCallback +} + +func (MorphOne) Kind() RelationKind { return KindMorphOne } + +// MorphMany is the multi-result variant of MorphOne. +type MorphMany struct { + Related any + Name string + TypeColumn string + IDColumn string + LocalKey string + OnQuery RelationCallback +} + +func (MorphMany) Kind() RelationKind { return KindMorphMany } + +// MorphTo declares the inverse polymorphic side: this model holds _id + _type and +// resolves to one of several parent kinds via the morph map registry. There is no Related — the +// concrete type is determined per row from the type column. +// +// Defaults: TypeColumn = Name + "_type"; IDColumn = Name + "_id"; OwnerKey = "id". +type MorphTo struct { + // Name is the polymorphic name. Required. + Name string + // TypeColumn is the polymorphic type column on this table. Optional. + TypeColumn string + // IDColumn is the polymorphic id column on this table. Optional. + IDColumn string + // OwnerKey is the column on each related table referenced by IDColumn. Optional, "id". + OwnerKey string + OnQuery RelationCallback +} + +func (MorphTo) Kind() RelationKind { return KindMorphTo } + +// MorphToMany declares a polymorphic many-to-many — through a pivot that carries +// _id + _type plus a related FK. +// +// Defaults: +// +// Table = pluralize(Name) (e.g. "taggables") +// TypeColumn = Name + "_type" +// ForeignPivotKey = Name + "_id" +// RelatedPivotKey = singular(relatedTable) + "_id" +// ParentKey = "id" +// RelatedKey = "id" +type MorphToMany struct { + Related any + Name string + Table string + TypeColumn string + ForeignPivotKey string + RelatedPivotKey string + ParentKey string + RelatedKey string + // PivotField — see Many2Many.PivotField. + PivotField string + // PivotTimestamps — see Many2Many.PivotTimestamps. + PivotTimestamps bool + OnQuery RelationCallback + // OnPivotQuery — see Many2Many.OnPivotQuery. + OnPivotQuery PivotCallback + // Touches — see Many2Many.Touches. + Touches bool +} + +func (MorphToMany) Kind() RelationKind { return KindMorphToMany } + +// MorphedByMany is the inverse side of MorphToMany — the morph value pins on the related rather +// than the parent. Field semantics and defaults match MorphToMany. +type MorphedByMany struct { + Related any + Name string + Table string + TypeColumn string + ForeignPivotKey string + RelatedPivotKey string + ParentKey string + RelatedKey string + // PivotField — see Many2Many.PivotField. + PivotField string + // PivotTimestamps — see Many2Many.PivotTimestamps. + PivotTimestamps bool + OnQuery RelationCallback + // OnPivotQuery — see Many2Many.OnPivotQuery. + OnPivotQuery PivotCallback + // Touches — see Many2Many.Touches. + Touches bool +} + +func (MorphedByMany) Kind() RelationKind { return KindMorphedByMany } + +// HasOneThrough declares a relation reached through an intermediate ("through") table. +// +// Defaults: +// +// FirstKey = singular(parentTable) + "_id" +// SecondKey = singular(throughTable) + "_id" +// LocalKey = "id" +// SecondLocalKey = "id" +type HasOneThrough struct { + Related any + // Through is the intermediate model. + Through any + // FirstKey is the FK on the through table pointing at parent. Optional. + FirstKey string + // SecondKey is the FK on the related table pointing at through. Optional. + SecondKey string + // LocalKey is the PK on the parent referenced by FirstKey. Optional, "id". + LocalKey string + // SecondLocalKey is the PK on the through table referenced by SecondKey. Optional, "id". + SecondLocalKey string + OnQuery RelationCallback +} + +func (HasOneThrough) Kind() RelationKind { return KindHasOneThrough } + +// HasManyThrough is the multi-result variant of HasOneThrough. +type HasManyThrough struct { + Related any + Through any + FirstKey string + SecondKey string + LocalKey string + SecondLocalKey string + OnQuery RelationCallback +} + +func (HasManyThrough) Kind() RelationKind { return KindHasManyThrough } + +// ModelWithRelations is implemented by every model that declares relationships. The single map +// returned by Relations() is the only place relations are declared. GORM relation struct tags +// (`foreignKey:`, `references:`, `many2many:`, `polymorphic:`) are forbidden — fields that hold +// related rows must be tagged `gorm:"-"`. +type ModelWithRelations interface { + Relations() map[string]Relation +} diff --git a/contracts/database/orm/relation_writer.go b/contracts/database/orm/relation_writer.go new file mode 100644 index 000000000..6fb50c50f --- /dev/null +++ b/contracts/database/orm/relation_writer.go @@ -0,0 +1,94 @@ +package orm + +import ( + "github.com/goravel/framework/contracts/database/db" +) + +// RelationWriter is the write-side builder for a single (parent, relation) pair, returned by +// Query.Relation / Orm.Relation. All write operations on a relation flow through this interface; +// flat methods like Orm.Save(parent, relation, child) intentionally do not exist. +// +// RelationWriter has NO Where / OrderBy / chain methods — fedaco-style "where(...).updateOrCreate(...)" +// composition is not supported because Goravel's relation system is metadata-driven (declared via +// ModelWithRelations.Relations()), not method-driven. Search criteria for the find-then-write +// methods (FirstOrNew/FirstOrCreate/UpdateOrCreate/FindOrNew) are passed via the attrs map, which +// is combined with the relation's foreign-key scope. +// +// For chained reads on a relation, use Query.Related(parent, name) instead — it returns a regular +// Query already scoped by the relation's foreign key, supporting the full Where/OrderBy/Get chain. +type RelationWriter interface { + // Save inserts or updates child as a member of the relation. Sets child's foreign key (and + // morph_type for MorphOne/MorphMany) from parent's local key, then persists child. + // Supported kinds: HasOne, HasMany, MorphOne, MorphMany, Many2Many, MorphToMany. + Save(child any) error + // SaveMany is the slice form of Save. children must be a slice or pointer-to-slice. + SaveMany(children any) error + // SaveWithPivot is Save with caller-supplied pivot column values for BelongsToMany kinds. + // On HasOneOrMany kinds attrs is ignored (no pivot row). + SaveWithPivot(child any, attrs map[string]any) error + // SaveManyWithPivot is the slice form of SaveWithPivot. attrsPerChild is keyed by the related + // PK of each child; an entry may be nil to attach without extra columns. + SaveManyWithPivot(children any, attrsPerChild map[any]map[string]any) error + + // Create persists a new related row. For HasOneOrMany kinds the framework pre-sets FK (and + // morph type) on dest from parent, then inserts. For BelongsToMany kinds inserts dest first, + // then writes a pivot row. + Create(dest any) error + // CreateMany is the slice form of Create. + CreateMany(dests any) error + + // FindOrNew finds the related row with primary key id. If absent, fills dest with a new + // instance of the related model and pre-sets FK (and morph type) — does NOT persist. + FindOrNew(id any, dest any) error + // FirstOrNew finds the first related row matching attrs. If absent, fills dest with a new + // instance carrying attrs+values and pre-set FK — does NOT persist. + FirstOrNew(attrs, values map[string]any, dest any) error + // FirstOrCreate is FirstOrNew that persists when no matching row exists. For BelongsToMany + // kinds also writes a pivot row. + FirstOrCreate(attrs, values map[string]any, dest any) error + // UpdateOrCreate finds the first related row matching attrs (or creates one), then overlays + // values onto it and persists. For BelongsToMany kinds also writes a pivot row when freshly + // created. Always saves dest. + UpdateOrCreate(attrs, values map[string]any, dest any) error + + // Associate sets parent's foreign key (and morph_type for MorphTo) to point at owner, then + // persists parent. Supported kinds: BelongsTo, MorphTo. owner must be a non-nil pointer to a + // struct. + Associate(owner any) error + // Dissociate clears parent's foreign key (and morph_type for MorphTo) and persists parent. + // Supported kinds: BelongsTo, MorphTo. + Dissociate() error + + // Attach inserts pivot rows linking parent to each id in ids. For polymorphic pivots the + // morph_type column is filled from the parent's morph value. Skips ids that already have a + // pivot row. Supported kinds: Many2Many, MorphToMany, MorphedByMany. + Attach(ids []any) error + // AttachWithPivot is Attach with per-row pivot column values. The map key is the related id; + // the map value is the column-name-to-value map applied to that pivot row. + AttachWithPivot(idsWithAttrs map[any]map[string]any) error + // Detach removes pivot rows linking parent to the given ids. With nil ids, removes all pivot + // rows for parent (and morph type, for polymorphic). Returns the number of rows removed. + Detach(ids ...any) (int64, error) + + // Sync replaces parent's pivot rows so they exactly match ids: detaches missing entries, + // attaches new ones, leaves existing untouched. + Sync(ids []any) (*db.SyncResult, error) + // SyncWithPivot is Sync with per-ID pivot column values. The map key is the related id; the + // map value is the column-name-to-value map applied to that pivot row. For existing pivot + // rows with non-empty attrs, updates the pivot columns (reported in SyncResult.Updated). + SyncWithPivot(idsWithAttrs map[any]map[string]any) (*db.SyncResult, error) + // SyncWithPivotValues is a convenience wrapper that applies the same pivot column values to + // all ids. + SyncWithPivotValues(ids []any, pivotValues map[string]any) (*db.SyncResult, error) + // SyncWithoutDetaching is Sync minus the detach step — adds missing entries only. + SyncWithoutDetaching(ids []any) (*db.SyncResult, error) + // SyncWithoutDetachingWithPivot is SyncWithPivot minus the detach step. + SyncWithoutDetachingWithPivot(idsWithAttrs map[any]map[string]any) (*db.SyncResult, error) + // Toggle attaches missing entries and detaches existing ones. + Toggle(ids []any) (*db.SyncResult, error) + // ToggleWithPivot is Toggle with per-ID pivot column values for newly attached rows. + ToggleWithPivot(idsWithAttrs map[any]map[string]any) (*db.SyncResult, error) + + // UpdateExistingPivot updates pivot columns for an already-attached id. + UpdateExistingPivot(id any, attrs map[string]any) (int64, error) +} diff --git a/database/orm/orm.go b/database/orm/orm.go index 9e8b4ff81..9b7fba31a 100644 --- a/database/orm/orm.go +++ b/database/orm/orm.go @@ -137,6 +137,31 @@ func (r *Orm) Query() contractsorm.Query { return r.query } +// Related returns a Query pre-scoped to the related rows for parent.relation. parent must be +// a non-nil pointer to a struct. See contractsorm.Orm.Related for the per-kind shape. +func (r *Orm) Related(parent any, relation string) contractsorm.Query { + q := r.Query() + gq, ok := q.(*gorm.Query) + if !ok { + // Implementation invariant: r.query is always a *gorm.Query in this driver. + // If a future driver implements contractsorm.Orm differently, that driver provides its + // own Related; this branch should never run in practice. + _ = q + return nil + } + return gq.Related(parent, relation) +} + +// Relation returns a RelationWriter bound to (parent, name) for FK-safe write operations. +// See contractsorm.Orm.Relation for usage. +func (r *Orm) Relation(parent any, name string) contractsorm.RelationWriter { + gq, ok := r.Query().(*gorm.Query) + if !ok { + return nil + } + return gq.Relation(parent, name) +} + func (r *Orm) SetQuery(query contractsorm.Query) { r.query = query } diff --git a/errors/list.go b/errors/list.go index 496c85c91..2c50d4059 100644 --- a/errors/list.go +++ b/errors/list.go @@ -158,25 +158,43 @@ var ( MigrationResetFailed = New("migration reset failed: %v") MigrationRollbackFailed = New("migration rollback failed: %v") - OrmDriverNotSupported = New("invalid driver: %s, only support mysql, postgres, sqlite and sqlserver") - OrmFailedToGenerateDNS = New("failed to generate DSN, please check the database configuration") - OrmFactoryMissingAttributes = New("failed to get raw attributes") - OrmFactoryMissingMethod = New("%s does not find factory method") - OrmInitConnection = New("init %s connection error: %v") - OrmMissingWhereClause = New("WHERE conditions required") - OrmNoDialectorsFound = New("no dialectors found") - OrmQueryAssociationsConflict = New("cannot set orm.Associations and other fields at the same time") - OrmQueryConditionRequired = New("query condition is required") - OrmQueryEmptyId = New("id can't be empty") - OrmQueryEmptyRelation = New("relation can't be empty") - OrmQueryInvalidModel = New("invalid model %s") - OrmQueryInvalidParameter = New("parameter error, please check the document") - OrmQueryModelNotPointer = New("model must be pointer") - OrmQuerySelectAndOmitsConflict = New("cannot set Select and Omits at the same time") - OrmRecordNotFound = New("record not found") - OrmDeletedAtColumnNotFound = New("deleted at column not found") - OrmJsonContainsInvalidBinding = New("invalid value for JSON contains: %v") - OrmJsonColumnUpdateInvalid = New("invalid value for JSON column update: %v") + OrmDriverNotSupported = New("invalid driver: %s, only support mysql, postgres, sqlite and sqlserver") + OrmFailedToGenerateDNS = New("failed to generate DSN, please check the database configuration") + OrmFactoryMissingAttributes = New("failed to get raw attributes") + OrmFactoryMissingMethod = New("%s does not find factory method") + OrmInitConnection = New("init %s connection error: %v") + OrmMissingWhereClause = New("WHERE conditions required") + OrmNoDialectorsFound = New("no dialectors found") + OrmQueryAssociationsConflict = New("cannot set orm.Associations and other fields at the same time") + OrmQueryConditionRequired = New("query condition is required") + OrmQueryEmptyId = New("id can't be empty") + OrmQueryEmptyRelation = New("relation can't be empty") + OrmQueryInvalidModel = New("invalid model %s") + OrmQueryInvalidParameter = New("parameter error, please check the document") + OrmQueryModelNotPointer = New("model must be pointer") + OrmQuerySelectAndOmitsConflict = New("cannot set Select and Omits at the same time") + OrmRecordNotFound = New("record not found") + OrmDeletedAtColumnNotFound = New("deleted at column not found") + OrmJsonContainsInvalidBinding = New("invalid value for JSON contains: %v") + OrmJsonColumnUpdateInvalid = New("invalid value for JSON column update: %v") + OrmRelationNotFound = New("relation %q not found on model %s") + OrmRelationUnsupported = New("relation %q on model %s has unsupported kind %q") + OrmRelationInvalidArgument = New("invalid argument %T for relation query, expected callback, operator or count") + OrmRelationInvalidAggregate = New("invalid aggregate function %q, expected one of count, max, min, sum, avg, exists") + OrmRelationMorphTypesEmpty = New("hasMorph requires at least one morph type") + OrmRelationThroughNotConfigured = New("through relation %q must be declared via the Relations() method on model %s") + OrmMorphRelationNotConfigured = New("polymorphic relation %q must be declared via the Relations() method on model %s") + OrmMorphRelationKindUnknown = New("relation %q on model %s has unknown kind %q") + OrmMorphRelationMissingField = New("relation %q on model %s is missing required field %q") + OrmMorphTypeUnknown = New("polymorphic type %q is not registered in the morph map; call orm.MorphMap or implement MorphClass() on the target model") + OrmPolymorphicTagForbidden = New("polymorphic GORM tag on field %q of model %s is forbidden; declare the relation via the Relations() method instead") + OrmRelationTagForbidden = New("GORM relation tag on field %q of model %s is forbidden; declare the relation via the Relations() method instead, and tag the field with `gorm:\"-\"`") + OrmRelationParentNotPointer = New("Related: parent must be a non-nil pointer to a struct, got %T") + OrmRelationKindNotSupported = New("operation %q is not supported on relation %q (kind %q)") + OrmEagerLoadInvalidArgument = New("invalid argument %T passed to With; expected string, []string, []any, map[string]orm.RelationCallback, or string + callback") + OrmEagerLoadCannotAssign = New("cannot assign eager-loaded rows to field %q on model %s; field must be *Model, []*Model or []Model") + OrmEagerLoadEmptyRelation = New("With received an empty relation name") + OrmRelationPivotFieldNotStruct = New("eager-load: related model %s has %s field of kind %s; pivot hydration target must be a struct") PackageConfigKeyExists = New("config key '%s' already exists,using ReplaceConfig instead if you want to update it") PackageFacadeNotFound = New("facade %s not found") diff --git a/mocks/database/orm/Association.go b/mocks/database/orm/Association.go deleted file mode 100644 index 8c46e27c4..000000000 --- a/mocks/database/orm/Association.go +++ /dev/null @@ -1,344 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package orm - -import mock "github.com/stretchr/testify/mock" - -// Association is an autogenerated mock type for the Association type -type Association struct { - mock.Mock -} - -type Association_Expecter struct { - mock *mock.Mock -} - -func (_m *Association) EXPECT() *Association_Expecter { - return &Association_Expecter{mock: &_m.Mock} -} - -// Append provides a mock function with given fields: values -func (_m *Association) Append(values ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, values...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Append") - } - - var r0 error - if rf, ok := ret.Get(0).(func(...interface{}) error); ok { - r0 = rf(values...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Association_Append_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Append' -type Association_Append_Call struct { - *mock.Call -} - -// Append is a helper method to define mock.On call -// - values ...interface{} -func (_e *Association_Expecter) Append(values ...interface{}) *Association_Append_Call { - return &Association_Append_Call{Call: _e.mock.On("Append", - append([]interface{}{}, values...)...)} -} - -func (_c *Association_Append_Call) Run(run func(values ...interface{})) *Association_Append_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-0) - for i, a := range args[0:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(variadicArgs...) - }) - return _c -} - -func (_c *Association_Append_Call) Return(_a0 error) *Association_Append_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Association_Append_Call) RunAndReturn(run func(...interface{}) error) *Association_Append_Call { - _c.Call.Return(run) - return _c -} - -// Clear provides a mock function with no fields -func (_m *Association) Clear() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Clear") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Association_Clear_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clear' -type Association_Clear_Call struct { - *mock.Call -} - -// Clear is a helper method to define mock.On call -func (_e *Association_Expecter) Clear() *Association_Clear_Call { - return &Association_Clear_Call{Call: _e.mock.On("Clear")} -} - -func (_c *Association_Clear_Call) Run(run func()) *Association_Clear_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *Association_Clear_Call) Return(_a0 error) *Association_Clear_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Association_Clear_Call) RunAndReturn(run func() error) *Association_Clear_Call { - _c.Call.Return(run) - return _c -} - -// Count provides a mock function with no fields -func (_m *Association) Count() int64 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Count") - } - - var r0 int64 - if rf, ok := ret.Get(0).(func() int64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int64) - } - - return r0 -} - -// Association_Count_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Count' -type Association_Count_Call struct { - *mock.Call -} - -// Count is a helper method to define mock.On call -func (_e *Association_Expecter) Count() *Association_Count_Call { - return &Association_Count_Call{Call: _e.mock.On("Count")} -} - -func (_c *Association_Count_Call) Run(run func()) *Association_Count_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *Association_Count_Call) Return(_a0 int64) *Association_Count_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Association_Count_Call) RunAndReturn(run func() int64) *Association_Count_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: values -func (_m *Association) Delete(values ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, values...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(...interface{}) error); ok { - r0 = rf(values...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Association_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type Association_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - values ...interface{} -func (_e *Association_Expecter) Delete(values ...interface{}) *Association_Delete_Call { - return &Association_Delete_Call{Call: _e.mock.On("Delete", - append([]interface{}{}, values...)...)} -} - -func (_c *Association_Delete_Call) Run(run func(values ...interface{})) *Association_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-0) - for i, a := range args[0:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(variadicArgs...) - }) - return _c -} - -func (_c *Association_Delete_Call) Return(_a0 error) *Association_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Association_Delete_Call) RunAndReturn(run func(...interface{}) error) *Association_Delete_Call { - _c.Call.Return(run) - return _c -} - -// Find provides a mock function with given fields: out, conds -func (_m *Association) Find(out interface{}, conds ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, out) - _ca = append(_ca, conds...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Find") - } - - var r0 error - if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) error); ok { - r0 = rf(out, conds...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Association_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' -type Association_Find_Call struct { - *mock.Call -} - -// Find is a helper method to define mock.On call -// - out interface{} -// - conds ...interface{} -func (_e *Association_Expecter) Find(out interface{}, conds ...interface{}) *Association_Find_Call { - return &Association_Find_Call{Call: _e.mock.On("Find", - append([]interface{}{out}, conds...)...)} -} - -func (_c *Association_Find_Call) Run(run func(out interface{}, conds ...interface{})) *Association_Find_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(args[0].(interface{}), variadicArgs...) - }) - return _c -} - -func (_c *Association_Find_Call) Return(_a0 error) *Association_Find_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Association_Find_Call) RunAndReturn(run func(interface{}, ...interface{}) error) *Association_Find_Call { - _c.Call.Return(run) - return _c -} - -// Replace provides a mock function with given fields: values -func (_m *Association) Replace(values ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, values...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Replace") - } - - var r0 error - if rf, ok := ret.Get(0).(func(...interface{}) error); ok { - r0 = rf(values...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Association_Replace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Replace' -type Association_Replace_Call struct { - *mock.Call -} - -// Replace is a helper method to define mock.On call -// - values ...interface{} -func (_e *Association_Expecter) Replace(values ...interface{}) *Association_Replace_Call { - return &Association_Replace_Call{Call: _e.mock.On("Replace", - append([]interface{}{}, values...)...)} -} - -func (_c *Association_Replace_Call) Run(run func(values ...interface{})) *Association_Replace_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-0) - for i, a := range args[0:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(variadicArgs...) - }) - return _c -} - -func (_c *Association_Replace_Call) Return(_a0 error) *Association_Replace_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Association_Replace_Call) RunAndReturn(run func(...interface{}) error) *Association_Replace_Call { - _c.Call.Return(run) - return _c -} - -// NewAssociation creates a new instance of Association. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewAssociation(t interface { - mock.TestingT - Cleanup(func()) -}) *Association { - mock := &Association{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/database/orm/ModelWithMorphClass.go b/mocks/database/orm/ModelWithMorphClass.go new file mode 100644 index 000000000..2f6743608 --- /dev/null +++ b/mocks/database/orm/ModelWithMorphClass.go @@ -0,0 +1,77 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import mock "github.com/stretchr/testify/mock" + +// ModelWithMorphClass is an autogenerated mock type for the ModelWithMorphClass type +type ModelWithMorphClass struct { + mock.Mock +} + +type ModelWithMorphClass_Expecter struct { + mock *mock.Mock +} + +func (_m *ModelWithMorphClass) EXPECT() *ModelWithMorphClass_Expecter { + return &ModelWithMorphClass_Expecter{mock: &_m.Mock} +} + +// MorphClass provides a mock function with no fields +func (_m *ModelWithMorphClass) MorphClass() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for MorphClass") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ModelWithMorphClass_MorphClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MorphClass' +type ModelWithMorphClass_MorphClass_Call struct { + *mock.Call +} + +// MorphClass is a helper method to define mock.On call +func (_e *ModelWithMorphClass_Expecter) MorphClass() *ModelWithMorphClass_MorphClass_Call { + return &ModelWithMorphClass_MorphClass_Call{Call: _e.mock.On("MorphClass")} +} + +func (_c *ModelWithMorphClass_MorphClass_Call) Run(run func()) *ModelWithMorphClass_MorphClass_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ModelWithMorphClass_MorphClass_Call) Return(_a0 string) *ModelWithMorphClass_MorphClass_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ModelWithMorphClass_MorphClass_Call) RunAndReturn(run func() string) *ModelWithMorphClass_MorphClass_Call { + _c.Call.Return(run) + return _c +} + +// NewModelWithMorphClass creates a new instance of ModelWithMorphClass. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewModelWithMorphClass(t interface { + mock.TestingT + Cleanup(func()) +}) *ModelWithMorphClass { + mock := &ModelWithMorphClass{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/ModelWithRelations.go b/mocks/database/orm/ModelWithRelations.go new file mode 100644 index 000000000..e1123ae00 --- /dev/null +++ b/mocks/database/orm/ModelWithRelations.go @@ -0,0 +1,82 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// ModelWithRelations is an autogenerated mock type for the ModelWithRelations type +type ModelWithRelations struct { + mock.Mock +} + +type ModelWithRelations_Expecter struct { + mock *mock.Mock +} + +func (_m *ModelWithRelations) EXPECT() *ModelWithRelations_Expecter { + return &ModelWithRelations_Expecter{mock: &_m.Mock} +} + +// Relations provides a mock function with no fields +func (_m *ModelWithRelations) Relations() map[string]orm.Relation { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Relations") + } + + var r0 map[string]orm.Relation + if rf, ok := ret.Get(0).(func() map[string]orm.Relation); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]orm.Relation) + } + } + + return r0 +} + +// ModelWithRelations_Relations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Relations' +type ModelWithRelations_Relations_Call struct { + *mock.Call +} + +// Relations is a helper method to define mock.On call +func (_e *ModelWithRelations_Expecter) Relations() *ModelWithRelations_Relations_Call { + return &ModelWithRelations_Relations_Call{Call: _e.mock.On("Relations")} +} + +func (_c *ModelWithRelations_Relations_Call) Run(run func()) *ModelWithRelations_Relations_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ModelWithRelations_Relations_Call) Return(_a0 map[string]orm.Relation) *ModelWithRelations_Relations_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ModelWithRelations_Relations_Call) RunAndReturn(run func() map[string]orm.Relation) *ModelWithRelations_Relations_Call { + _c.Call.Return(run) + return _c +} + +// NewModelWithRelations creates a new instance of ModelWithRelations. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewModelWithRelations(t interface { + mock.TestingT + Cleanup(func()) +}) *ModelWithRelations { + mock := &ModelWithRelations{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/MorphRelationCallback.go b/mocks/database/orm/MorphRelationCallback.go new file mode 100644 index 000000000..f10d5f00f --- /dev/null +++ b/mocks/database/orm/MorphRelationCallback.go @@ -0,0 +1,84 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// MorphRelationCallback is an autogenerated mock type for the MorphRelationCallback type +type MorphRelationCallback struct { + mock.Mock +} + +type MorphRelationCallback_Expecter struct { + mock *mock.Mock +} + +func (_m *MorphRelationCallback) EXPECT() *MorphRelationCallback_Expecter { + return &MorphRelationCallback_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: query, morphType +func (_m *MorphRelationCallback) Execute(query orm.Query, morphType string) orm.Query { + ret := _m.Called(query, morphType) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(orm.Query, string) orm.Query); ok { + r0 = rf(query, morphType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// MorphRelationCallback_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type MorphRelationCallback_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - query orm.Query +// - morphType string +func (_e *MorphRelationCallback_Expecter) Execute(query interface{}, morphType interface{}) *MorphRelationCallback_Execute_Call { + return &MorphRelationCallback_Execute_Call{Call: _e.mock.On("Execute", query, morphType)} +} + +func (_c *MorphRelationCallback_Execute_Call) Run(run func(query orm.Query, morphType string)) *MorphRelationCallback_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(orm.Query), args[1].(string)) + }) + return _c +} + +func (_c *MorphRelationCallback_Execute_Call) Return(_a0 orm.Query) *MorphRelationCallback_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MorphRelationCallback_Execute_Call) RunAndReturn(run func(orm.Query, string) orm.Query) *MorphRelationCallback_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewMorphRelationCallback creates a new instance of MorphRelationCallback. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMorphRelationCallback(t interface { + mock.TestingT + Cleanup(func()) +}) *MorphRelationCallback { + mock := &MorphRelationCallback{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/Orm.go b/mocks/database/orm/Orm.go index b52605b33..690340393 100644 --- a/mocks/database/orm/Orm.go +++ b/mocks/database/orm/Orm.go @@ -426,6 +426,104 @@ func (_c *Orm_Query_Call) RunAndReturn(run func() orm.Query) *Orm_Query_Call { return _c } +// Related provides a mock function with given fields: parent, relation +func (_m *Orm) Related(parent interface{}, relation string) orm.Query { + ret := _m.Called(parent, relation) + + if len(ret) == 0 { + panic("no return value specified for Related") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(interface{}, string) orm.Query); ok { + r0 = rf(parent, relation) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Orm_Related_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Related' +type Orm_Related_Call struct { + *mock.Call +} + +// Related is a helper method to define mock.On call +// - parent interface{} +// - relation string +func (_e *Orm_Expecter) Related(parent interface{}, relation interface{}) *Orm_Related_Call { + return &Orm_Related_Call{Call: _e.mock.On("Related", parent, relation)} +} + +func (_c *Orm_Related_Call) Run(run func(parent interface{}, relation string)) *Orm_Related_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(string)) + }) + return _c +} + +func (_c *Orm_Related_Call) Return(_a0 orm.Query) *Orm_Related_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Orm_Related_Call) RunAndReturn(run func(interface{}, string) orm.Query) *Orm_Related_Call { + _c.Call.Return(run) + return _c +} + +// Relation provides a mock function with given fields: parent, name +func (_m *Orm) Relation(parent interface{}, name string) orm.RelationWriter { + ret := _m.Called(parent, name) + + if len(ret) == 0 { + panic("no return value specified for Relation") + } + + var r0 orm.RelationWriter + if rf, ok := ret.Get(0).(func(interface{}, string) orm.RelationWriter); ok { + r0 = rf(parent, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.RelationWriter) + } + } + + return r0 +} + +// Orm_Relation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Relation' +type Orm_Relation_Call struct { + *mock.Call +} + +// Relation is a helper method to define mock.On call +// - parent interface{} +// - name string +func (_e *Orm_Expecter) Relation(parent interface{}, name interface{}) *Orm_Relation_Call { + return &Orm_Relation_Call{Call: _e.mock.On("Relation", parent, name)} +} + +func (_c *Orm_Relation_Call) Run(run func(parent interface{}, name string)) *Orm_Relation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(string)) + }) + return _c +} + +func (_c *Orm_Relation_Call) Return(_a0 orm.RelationWriter) *Orm_Relation_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Orm_Relation_Call) RunAndReturn(run func(interface{}, string) orm.RelationWriter) *Orm_Relation_Call { + _c.Call.Return(run) + return _c +} + // SetQuery provides a mock function with given fields: query func (_m *Orm) SetQuery(query orm.Query) { _m.Called(query) diff --git a/mocks/database/orm/PivotCallback.go b/mocks/database/orm/PivotCallback.go new file mode 100644 index 000000000..f8fb5e5b1 --- /dev/null +++ b/mocks/database/orm/PivotCallback.go @@ -0,0 +1,83 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// PivotCallback is an autogenerated mock type for the PivotCallback type +type PivotCallback struct { + mock.Mock +} + +type PivotCallback_Expecter struct { + mock *mock.Mock +} + +func (_m *PivotCallback) EXPECT() *PivotCallback_Expecter { + return &PivotCallback_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: query +func (_m *PivotCallback) Execute(query orm.PivotQuery) orm.PivotQuery { + ret := _m.Called(query) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 orm.PivotQuery + if rf, ok := ret.Get(0).(func(orm.PivotQuery) orm.PivotQuery); ok { + r0 = rf(query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.PivotQuery) + } + } + + return r0 +} + +// PivotCallback_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type PivotCallback_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - query orm.PivotQuery +func (_e *PivotCallback_Expecter) Execute(query interface{}) *PivotCallback_Execute_Call { + return &PivotCallback_Execute_Call{Call: _e.mock.On("Execute", query)} +} + +func (_c *PivotCallback_Execute_Call) Run(run func(query orm.PivotQuery)) *PivotCallback_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(orm.PivotQuery)) + }) + return _c +} + +func (_c *PivotCallback_Execute_Call) Return(_a0 orm.PivotQuery) *PivotCallback_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PivotCallback_Execute_Call) RunAndReturn(run func(orm.PivotQuery) orm.PivotQuery) *PivotCallback_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewPivotCallback creates a new instance of PivotCallback. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPivotCallback(t interface { + mock.TestingT + Cleanup(func()) +}) *PivotCallback { + mock := &PivotCallback{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/PivotQuery.go b/mocks/database/orm/PivotQuery.go new file mode 100644 index 000000000..a895458a8 --- /dev/null +++ b/mocks/database/orm/PivotQuery.go @@ -0,0 +1,288 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// PivotQuery is an autogenerated mock type for the PivotQuery type +type PivotQuery struct { + mock.Mock +} + +type PivotQuery_Expecter struct { + mock *mock.Mock +} + +func (_m *PivotQuery) EXPECT() *PivotQuery_Expecter { + return &PivotQuery_Expecter{mock: &_m.Mock} +} + +// Where provides a mock function with given fields: column, args +func (_m *PivotQuery) Where(column string, args ...interface{}) orm.PivotQuery { + var _ca []interface{} + _ca = append(_ca, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Where") + } + + var r0 orm.PivotQuery + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.PivotQuery); ok { + r0 = rf(column, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.PivotQuery) + } + } + + return r0 +} + +// PivotQuery_Where_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Where' +type PivotQuery_Where_Call struct { + *mock.Call +} + +// Where is a helper method to define mock.On call +// - column string +// - args ...interface{} +func (_e *PivotQuery_Expecter) Where(column interface{}, args ...interface{}) *PivotQuery_Where_Call { + return &PivotQuery_Where_Call{Call: _e.mock.On("Where", + append([]interface{}{column}, args...)...)} +} + +func (_c *PivotQuery_Where_Call) Run(run func(column string, args ...interface{})) *PivotQuery_Where_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *PivotQuery_Where_Call) Return(_a0 orm.PivotQuery) *PivotQuery_Where_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PivotQuery_Where_Call) RunAndReturn(run func(string, ...interface{}) orm.PivotQuery) *PivotQuery_Where_Call { + _c.Call.Return(run) + return _c +} + +// WhereIn provides a mock function with given fields: column, values +func (_m *PivotQuery) WhereIn(column string, values []interface{}) orm.PivotQuery { + ret := _m.Called(column, values) + + if len(ret) == 0 { + panic("no return value specified for WhereIn") + } + + var r0 orm.PivotQuery + if rf, ok := ret.Get(0).(func(string, []interface{}) orm.PivotQuery); ok { + r0 = rf(column, values) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.PivotQuery) + } + } + + return r0 +} + +// PivotQuery_WhereIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereIn' +type PivotQuery_WhereIn_Call struct { + *mock.Call +} + +// WhereIn is a helper method to define mock.On call +// - column string +// - values []interface{} +func (_e *PivotQuery_Expecter) WhereIn(column interface{}, values interface{}) *PivotQuery_WhereIn_Call { + return &PivotQuery_WhereIn_Call{Call: _e.mock.On("WhereIn", column, values)} +} + +func (_c *PivotQuery_WhereIn_Call) Run(run func(column string, values []interface{})) *PivotQuery_WhereIn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]interface{})) + }) + return _c +} + +func (_c *PivotQuery_WhereIn_Call) Return(_a0 orm.PivotQuery) *PivotQuery_WhereIn_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PivotQuery_WhereIn_Call) RunAndReturn(run func(string, []interface{}) orm.PivotQuery) *PivotQuery_WhereIn_Call { + _c.Call.Return(run) + return _c +} + +// WhereNotIn provides a mock function with given fields: column, values +func (_m *PivotQuery) WhereNotIn(column string, values []interface{}) orm.PivotQuery { + ret := _m.Called(column, values) + + if len(ret) == 0 { + panic("no return value specified for WhereNotIn") + } + + var r0 orm.PivotQuery + if rf, ok := ret.Get(0).(func(string, []interface{}) orm.PivotQuery); ok { + r0 = rf(column, values) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.PivotQuery) + } + } + + return r0 +} + +// PivotQuery_WhereNotIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotIn' +type PivotQuery_WhereNotIn_Call struct { + *mock.Call +} + +// WhereNotIn is a helper method to define mock.On call +// - column string +// - values []interface{} +func (_e *PivotQuery_Expecter) WhereNotIn(column interface{}, values interface{}) *PivotQuery_WhereNotIn_Call { + return &PivotQuery_WhereNotIn_Call{Call: _e.mock.On("WhereNotIn", column, values)} +} + +func (_c *PivotQuery_WhereNotIn_Call) Run(run func(column string, values []interface{})) *PivotQuery_WhereNotIn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]interface{})) + }) + return _c +} + +func (_c *PivotQuery_WhereNotIn_Call) Return(_a0 orm.PivotQuery) *PivotQuery_WhereNotIn_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PivotQuery_WhereNotIn_Call) RunAndReturn(run func(string, []interface{}) orm.PivotQuery) *PivotQuery_WhereNotIn_Call { + _c.Call.Return(run) + return _c +} + +// WhereNotNull provides a mock function with given fields: column +func (_m *PivotQuery) WhereNotNull(column string) orm.PivotQuery { + ret := _m.Called(column) + + if len(ret) == 0 { + panic("no return value specified for WhereNotNull") + } + + var r0 orm.PivotQuery + if rf, ok := ret.Get(0).(func(string) orm.PivotQuery); ok { + r0 = rf(column) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.PivotQuery) + } + } + + return r0 +} + +// PivotQuery_WhereNotNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotNull' +type PivotQuery_WhereNotNull_Call struct { + *mock.Call +} + +// WhereNotNull is a helper method to define mock.On call +// - column string +func (_e *PivotQuery_Expecter) WhereNotNull(column interface{}) *PivotQuery_WhereNotNull_Call { + return &PivotQuery_WhereNotNull_Call{Call: _e.mock.On("WhereNotNull", column)} +} + +func (_c *PivotQuery_WhereNotNull_Call) Run(run func(column string)) *PivotQuery_WhereNotNull_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *PivotQuery_WhereNotNull_Call) Return(_a0 orm.PivotQuery) *PivotQuery_WhereNotNull_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PivotQuery_WhereNotNull_Call) RunAndReturn(run func(string) orm.PivotQuery) *PivotQuery_WhereNotNull_Call { + _c.Call.Return(run) + return _c +} + +// WhereNull provides a mock function with given fields: column +func (_m *PivotQuery) WhereNull(column string) orm.PivotQuery { + ret := _m.Called(column) + + if len(ret) == 0 { + panic("no return value specified for WhereNull") + } + + var r0 orm.PivotQuery + if rf, ok := ret.Get(0).(func(string) orm.PivotQuery); ok { + r0 = rf(column) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.PivotQuery) + } + } + + return r0 +} + +// PivotQuery_WhereNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNull' +type PivotQuery_WhereNull_Call struct { + *mock.Call +} + +// WhereNull is a helper method to define mock.On call +// - column string +func (_e *PivotQuery_Expecter) WhereNull(column interface{}) *PivotQuery_WhereNull_Call { + return &PivotQuery_WhereNull_Call{Call: _e.mock.On("WhereNull", column)} +} + +func (_c *PivotQuery_WhereNull_Call) Run(run func(column string)) *PivotQuery_WhereNull_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *PivotQuery_WhereNull_Call) Return(_a0 orm.PivotQuery) *PivotQuery_WhereNull_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *PivotQuery_WhereNull_Call) RunAndReturn(run func(string) orm.PivotQuery) *PivotQuery_WhereNull_Call { + _c.Call.Return(run) + return _c +} + +// NewPivotQuery creates a new instance of PivotQuery. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPivotQuery(t interface { + mock.TestingT + Cleanup(func()) +}) *PivotQuery { + mock := &PivotQuery{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/Query.go b/mocks/database/orm/Query.go index a49b3b2b5..99042551f 100644 --- a/mocks/database/orm/Query.go +++ b/mocks/database/orm/Query.go @@ -26,54 +26,6 @@ func (_m *Query) EXPECT() *Query_Expecter { return &Query_Expecter{mock: &_m.Mock} } -// Association provides a mock function with given fields: association -func (_m *Query) Association(association string) orm.Association { - ret := _m.Called(association) - - if len(ret) == 0 { - panic("no return value specified for Association") - } - - var r0 orm.Association - if rf, ok := ret.Get(0).(func(string) orm.Association); ok { - r0 = rf(association) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(orm.Association) - } - } - - return r0 -} - -// Query_Association_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Association' -type Query_Association_Call struct { - *mock.Call -} - -// Association is a helper method to define mock.On call -// - association string -func (_e *Query_Expecter) Association(association interface{}) *Query_Association_Call { - return &Query_Association_Call{Call: _e.mock.On("Association", association)} -} - -func (_c *Query_Association_Call) Run(run func(association string)) *Query_Association_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *Query_Association_Call) Return(_a0 orm.Association) *Query_Association_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *Query_Association_Call) RunAndReturn(run func(string) orm.Association) *Query_Association_Call { - _c.Call.Return(run) - return _c -} - // Avg provides a mock function with given fields: column, dest func (_m *Query) Avg(column string, dest interface{}) error { ret := _m.Called(column, dest) @@ -660,6 +612,125 @@ func (_c *Query_Distinct_Call) RunAndReturn(run func(...string) orm.Query) *Quer return _c } +// DoesntHave provides a mock function with given fields: relation, args +func (_m *Query) DoesntHave(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DoesntHave") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_DoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DoesntHave' +type Query_DoesntHave_Call struct { + *mock.Call +} + +// DoesntHave is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *Query_Expecter) DoesntHave(relation interface{}, args ...interface{}) *Query_DoesntHave_Call { + return &Query_DoesntHave_Call{Call: _e.mock.On("DoesntHave", + append([]interface{}{relation}, args...)...)} +} + +func (_c *Query_DoesntHave_Call) Run(run func(relation string, args ...interface{})) *Query_DoesntHave_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Query_DoesntHave_Call) Return(_a0 orm.Query) *Query_DoesntHave_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_DoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_DoesntHave_Call { + _c.Call.Return(run) + return _c +} + +// DoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *Query) DoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DoesntHaveMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_DoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DoesntHaveMorph' +type Query_DoesntHaveMorph_Call struct { + *mock.Call +} + +// DoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) DoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *Query_DoesntHaveMorph_Call { + return &Query_DoesntHaveMorph_Call{Call: _e.mock.On("DoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *Query_DoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_DoesntHaveMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *Query_DoesntHaveMorph_Call) Return(_a0 orm.Query) *Query_DoesntHaveMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_DoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_DoesntHaveMorph_Call { + _c.Call.Return(run) + return _c +} + // Driver provides a mock function with no fields func (_m *Query) Driver() string { ret := _m.Called() @@ -1419,6 +1490,125 @@ func (_c *Query_GroupBy_Call) RunAndReturn(run func(...string) orm.Query) *Query return _c } +// Has provides a mock function with given fields: relation, args +func (_m *Query) Has(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Has") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Has_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Has' +type Query_Has_Call struct { + *mock.Call +} + +// Has is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *Query_Expecter) Has(relation interface{}, args ...interface{}) *Query_Has_Call { + return &Query_Has_Call{Call: _e.mock.On("Has", + append([]interface{}{relation}, args...)...)} +} + +func (_c *Query_Has_Call) Run(run func(relation string, args ...interface{})) *Query_Has_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Query_Has_Call) Return(_a0 orm.Query) *Query_Has_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Has_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_Has_Call { + _c.Call.Return(run) + return _c +} + +// HasMorph provides a mock function with given fields: relation, types, args +func (_m *Query) HasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for HasMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_HasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasMorph' +type Query_HasMorph_Call struct { + *mock.Call +} + +// HasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) HasMorph(relation interface{}, types interface{}, args ...interface{}) *Query_HasMorph_Call { + return &Query_HasMorph_Call{Call: _e.mock.On("HasMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *Query_HasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_HasMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *Query_HasMorph_Call) Return(_a0 orm.Query) *Query_HasMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_HasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_HasMorph_Call { + _c.Call.Return(run) + return _c +} + // Having provides a mock function with given fields: query, args func (_m *Query) Having(query interface{}, args ...interface{}) orm.Query { var _ca []interface{} @@ -1629,17 +1819,23 @@ func (_c *Query_Join_Call) RunAndReturn(run func(string, ...interface{}) orm.Que return _c } -// Limit provides a mock function with given fields: limit -func (_m *Query) Limit(limit int) orm.Query { - ret := _m.Called(limit) +// LatestOfMany provides a mock function with given fields: column +func (_m *Query) LatestOfMany(column ...string) orm.Query { + _va := make([]interface{}, len(column)) + for _i := range column { + _va[_i] = column[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for Limit") + panic("no return value specified for LatestOfMany") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(int) orm.Query); ok { - r0 = rf(limit) + if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { + r0 = rf(column...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -1649,49 +1845,104 @@ func (_m *Query) Limit(limit int) orm.Query { return r0 } -// Query_Limit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Limit' -type Query_Limit_Call struct { +// Query_LatestOfMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestOfMany' +type Query_LatestOfMany_Call struct { *mock.Call } -// Limit is a helper method to define mock.On call -// - limit int -func (_e *Query_Expecter) Limit(limit interface{}) *Query_Limit_Call { - return &Query_Limit_Call{Call: _e.mock.On("Limit", limit)} +// LatestOfMany is a helper method to define mock.On call +// - column ...string +func (_e *Query_Expecter) LatestOfMany(column ...interface{}) *Query_LatestOfMany_Call { + return &Query_LatestOfMany_Call{Call: _e.mock.On("LatestOfMany", + append([]interface{}{}, column...)...)} } -func (_c *Query_Limit_Call) Run(run func(limit int)) *Query_Limit_Call { +func (_c *Query_LatestOfMany_Call) Run(run func(column ...string)) *Query_LatestOfMany_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(int)) + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) }) return _c } -func (_c *Query_Limit_Call) Return(_a0 orm.Query) *Query_Limit_Call { +func (_c *Query_LatestOfMany_Call) Return(_a0 orm.Query) *Query_LatestOfMany_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Limit_Call) RunAndReturn(run func(int) orm.Query) *Query_Limit_Call { +func (_c *Query_LatestOfMany_Call) RunAndReturn(run func(...string) orm.Query) *Query_LatestOfMany_Call { _c.Call.Return(run) return _c } -// Load provides a mock function with given fields: dest, relation, args -func (_m *Query) Load(dest interface{}, relation string, args ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, dest, relation) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) +// Limit provides a mock function with given fields: limit +func (_m *Query) Limit(limit int) orm.Query { + ret := _m.Called(limit) if len(ret) == 0 { - panic("no return value specified for Load") + panic("no return value specified for Limit") } - var r0 error - if rf, ok := ret.Get(0).(func(interface{}, string, ...interface{}) error); ok { - r0 = rf(dest, relation, args...) - } else { + var r0 orm.Query + if rf, ok := ret.Get(0).(func(int) orm.Query); ok { + r0 = rf(limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Limit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Limit' +type Query_Limit_Call struct { + *mock.Call +} + +// Limit is a helper method to define mock.On call +// - limit int +func (_e *Query_Expecter) Limit(limit interface{}) *Query_Limit_Call { + return &Query_Limit_Call{Call: _e.mock.On("Limit", limit)} +} + +func (_c *Query_Limit_Call) Run(run func(limit int)) *Query_Limit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int)) + }) + return _c +} + +func (_c *Query_Limit_Call) Return(_a0 orm.Query) *Query_Limit_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Limit_Call) RunAndReturn(run func(int) orm.Query) *Query_Limit_Call { + _c.Call.Return(run) + return _c +} + +// Load provides a mock function with given fields: dest, relation, args +func (_m *Query) Load(dest interface{}, relation string, args ...interface{}) error { + var _ca []interface{} + _ca = append(_ca, dest, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Load") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, string, ...interface{}) error); ok { + r0 = rf(dest, relation, args...) + } else { r0 = ret.Error(0) } @@ -1982,6 +2233,55 @@ func (_c *Query_Model_Call) RunAndReturn(run func(interface{}) orm.Query) *Query return _c } +// OfMany provides a mock function with given fields: column, aggregate +func (_m *Query) OfMany(column string, aggregate string) orm.Query { + ret := _m.Called(column, aggregate) + + if len(ret) == 0 { + panic("no return value specified for OfMany") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, string) orm.Query); ok { + r0 = rf(column, aggregate) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_OfMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OfMany' +type Query_OfMany_Call struct { + *mock.Call +} + +// OfMany is a helper method to define mock.On call +// - column string +// - aggregate string +func (_e *Query_Expecter) OfMany(column interface{}, aggregate interface{}) *Query_OfMany_Call { + return &Query_OfMany_Call{Call: _e.mock.On("OfMany", column, aggregate)} +} + +func (_c *Query_OfMany_Call) Run(run func(column string, aggregate string)) *Query_OfMany_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *Query_OfMany_Call) Return(_a0 orm.Query) *Query_OfMany_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_OfMany_Call) RunAndReturn(run func(string, string) orm.Query) *Query_OfMany_Call { + _c.Call.Return(run) + return _c +} + // Offset provides a mock function with given fields: offset func (_m *Query) Offset(offset int) orm.Query { ret := _m.Called(offset) @@ -2030,6 +2330,67 @@ func (_c *Query_Offset_Call) RunAndReturn(run func(int) orm.Query) *Query_Offset return _c } +// OldestOfMany provides a mock function with given fields: column +func (_m *Query) OldestOfMany(column ...string) orm.Query { + _va := make([]interface{}, len(column)) + for _i := range column { + _va[_i] = column[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OldestOfMany") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { + r0 = rf(column...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_OldestOfMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OldestOfMany' +type Query_OldestOfMany_Call struct { + *mock.Call +} + +// OldestOfMany is a helper method to define mock.On call +// - column ...string +func (_e *Query_Expecter) OldestOfMany(column ...interface{}) *Query_OldestOfMany_Call { + return &Query_OldestOfMany_Call{Call: _e.mock.On("OldestOfMany", + append([]interface{}{}, column...)...)} +} + +func (_c *Query_OldestOfMany_Call) Run(run func(column ...string)) *Query_OldestOfMany_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Query_OldestOfMany_Call) Return(_a0 orm.Query) *Query_OldestOfMany_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_OldestOfMany_Call) RunAndReturn(run func(...string) orm.Query) *Query_OldestOfMany_Call { + _c.Call.Return(run) + return _c +} + // Omit provides a mock function with given fields: columns func (_m *Query) Omit(columns ...string) orm.Query { _va := make([]interface{}, len(columns)) @@ -2091,20 +2452,20 @@ func (_c *Query_Omit_Call) RunAndReturn(run func(...string) orm.Query) *Query_Om return _c } -// OrWhere provides a mock function with given fields: query, args -func (_m *Query) OrWhere(query interface{}, args ...interface{}) orm.Query { +// OrDoesntHave provides a mock function with given fields: relation, args +func (_m *Query) OrDoesntHave(relation string, args ...interface{}) orm.Query { var _ca []interface{} - _ca = append(_ca, query) + _ca = append(_ca, relation) _ca = append(_ca, args...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhere") + panic("no return value specified for OrDoesntHave") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) orm.Query); ok { - r0 = rf(query, args...) + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2114,20 +2475,20 @@ func (_m *Query) OrWhere(query interface{}, args ...interface{}) orm.Query { return r0 } -// Query_OrWhere_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhere' -type Query_OrWhere_Call struct { +// Query_OrDoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrDoesntHave' +type Query_OrDoesntHave_Call struct { *mock.Call } -// OrWhere is a helper method to define mock.On call -// - query interface{} +// OrDoesntHave is a helper method to define mock.On call +// - relation string // - args ...interface{} -func (_e *Query_Expecter) OrWhere(query interface{}, args ...interface{}) *Query_OrWhere_Call { - return &Query_OrWhere_Call{Call: _e.mock.On("OrWhere", - append([]interface{}{query}, args...)...)} +func (_e *Query_Expecter) OrDoesntHave(relation interface{}, args ...interface{}) *Query_OrDoesntHave_Call { + return &Query_OrDoesntHave_Call{Call: _e.mock.On("OrDoesntHave", + append([]interface{}{relation}, args...)...)} } -func (_c *Query_OrWhere_Call) Run(run func(query interface{}, args ...interface{})) *Query_OrWhere_Call { +func (_c *Query_OrDoesntHave_Call) Run(run func(relation string, args ...interface{})) *Query_OrDoesntHave_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]interface{}, len(args)-1) for i, a := range args[1:] { @@ -2135,32 +2496,35 @@ func (_c *Query_OrWhere_Call) Run(run func(query interface{}, args ...interface{ variadicArgs[i] = a.(interface{}) } } - run(args[0].(interface{}), variadicArgs...) + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_OrWhere_Call) Return(_a0 orm.Query) *Query_OrWhere_Call { +func (_c *Query_OrDoesntHave_Call) Return(_a0 orm.Query) *Query_OrDoesntHave_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhere_Call) RunAndReturn(run func(interface{}, ...interface{}) orm.Query) *Query_OrWhere_Call { +func (_c *Query_OrDoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_OrDoesntHave_Call { _c.Call.Return(run) return _c } -// OrWhereBetween provides a mock function with given fields: column, x, y -func (_m *Query) OrWhereBetween(column string, x interface{}, y interface{}) orm.Query { - ret := _m.Called(column, x, y) +// OrDoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *Query) OrDoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereBetween") + panic("no return value specified for OrDoesntHaveMorph") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) orm.Query); ok { - r0 = rf(column, x, y) + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2170,47 +2534,57 @@ func (_m *Query) OrWhereBetween(column string, x interface{}, y interface{}) orm return r0 } -// Query_OrWhereBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereBetween' -type Query_OrWhereBetween_Call struct { +// Query_OrDoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrDoesntHaveMorph' +type Query_OrDoesntHaveMorph_Call struct { *mock.Call } -// OrWhereBetween is a helper method to define mock.On call -// - column string -// - x interface{} -// - y interface{} -func (_e *Query_Expecter) OrWhereBetween(column interface{}, x interface{}, y interface{}) *Query_OrWhereBetween_Call { - return &Query_OrWhereBetween_Call{Call: _e.mock.On("OrWhereBetween", column, x, y)} +// OrDoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) OrDoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *Query_OrDoesntHaveMorph_Call { + return &Query_OrDoesntHaveMorph_Call{Call: _e.mock.On("OrDoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} } -func (_c *Query_OrWhereBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_OrWhereBetween_Call { +func (_c *Query_OrDoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_OrDoesntHaveMorph_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{}), args[2].(interface{})) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) }) return _c } -func (_c *Query_OrWhereBetween_Call) Return(_a0 orm.Query) *Query_OrWhereBetween_Call { +func (_c *Query_OrDoesntHaveMorph_Call) Return(_a0 orm.Query) *Query_OrDoesntHaveMorph_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_OrWhereBetween_Call { +func (_c *Query_OrDoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_OrDoesntHaveMorph_Call { _c.Call.Return(run) return _c } -// OrWhereIn provides a mock function with given fields: column, values -func (_m *Query) OrWhereIn(column string, values []interface{}) orm.Query { - ret := _m.Called(column, values) +// OrHas provides a mock function with given fields: relation, args +func (_m *Query) OrHas(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereIn") + panic("no return value specified for OrHas") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, []interface{}) orm.Query); ok { - r0 = rf(column, values) + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2220,46 +2594,56 @@ func (_m *Query) OrWhereIn(column string, values []interface{}) orm.Query { return r0 } -// Query_OrWhereIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereIn' -type Query_OrWhereIn_Call struct { +// Query_OrHas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrHas' +type Query_OrHas_Call struct { *mock.Call } -// OrWhereIn is a helper method to define mock.On call -// - column string -// - values []interface{} -func (_e *Query_Expecter) OrWhereIn(column interface{}, values interface{}) *Query_OrWhereIn_Call { - return &Query_OrWhereIn_Call{Call: _e.mock.On("OrWhereIn", column, values)} +// OrHas is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *Query_Expecter) OrHas(relation interface{}, args ...interface{}) *Query_OrHas_Call { + return &Query_OrHas_Call{Call: _e.mock.On("OrHas", + append([]interface{}{relation}, args...)...)} } -func (_c *Query_OrWhereIn_Call) Run(run func(column string, values []interface{})) *Query_OrWhereIn_Call { +func (_c *Query_OrHas_Call) Run(run func(relation string, args ...interface{})) *Query_OrHas_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].([]interface{})) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_OrWhereIn_Call) Return(_a0 orm.Query) *Query_OrWhereIn_Call { +func (_c *Query_OrHas_Call) Return(_a0 orm.Query) *Query_OrHas_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_OrWhereIn_Call { +func (_c *Query_OrHas_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_OrHas_Call { _c.Call.Return(run) return _c } -// OrWhereJsonContains provides a mock function with given fields: column, value -func (_m *Query) OrWhereJsonContains(column string, value interface{}) orm.Query { - ret := _m.Called(column, value) +// OrHasMorph provides a mock function with given fields: relation, types, args +func (_m *Query) OrHasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereJsonContains") + panic("no return value specified for OrHasMorph") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { - r0 = rf(column, value) + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2269,46 +2653,57 @@ func (_m *Query) OrWhereJsonContains(column string, value interface{}) orm.Query return r0 } -// Query_OrWhereJsonContains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonContains' -type Query_OrWhereJsonContains_Call struct { +// Query_OrHasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrHasMorph' +type Query_OrHasMorph_Call struct { *mock.Call } -// OrWhereJsonContains is a helper method to define mock.On call -// - column string -// - value interface{} -func (_e *Query_Expecter) OrWhereJsonContains(column interface{}, value interface{}) *Query_OrWhereJsonContains_Call { - return &Query_OrWhereJsonContains_Call{Call: _e.mock.On("OrWhereJsonContains", column, value)} +// OrHasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) OrHasMorph(relation interface{}, types interface{}, args ...interface{}) *Query_OrHasMorph_Call { + return &Query_OrHasMorph_Call{Call: _e.mock.On("OrHasMorph", + append([]interface{}{relation, types}, args...)...)} } -func (_c *Query_OrWhereJsonContains_Call) Run(run func(column string, value interface{})) *Query_OrWhereJsonContains_Call { +func (_c *Query_OrHasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_OrHasMorph_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{})) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) }) return _c } -func (_c *Query_OrWhereJsonContains_Call) Return(_a0 orm.Query) *Query_OrWhereJsonContains_Call { +func (_c *Query_OrHasMorph_Call) Return(_a0 orm.Query) *Query_OrHasMorph_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereJsonContains_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_OrWhereJsonContains_Call { +func (_c *Query_OrHasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_OrHasMorph_Call { _c.Call.Return(run) return _c } -// OrWhereJsonContainsKey provides a mock function with given fields: column -func (_m *Query) OrWhereJsonContainsKey(column string) orm.Query { - ret := _m.Called(column) +// OrWhere provides a mock function with given fields: query, args +func (_m *Query) OrWhere(query interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereJsonContainsKey") + panic("no return value specified for OrWhere") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) orm.Query); ok { + r0 = rf(query, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2318,45 +2713,53 @@ func (_m *Query) OrWhereJsonContainsKey(column string) orm.Query { return r0 } -// Query_OrWhereJsonContainsKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonContainsKey' -type Query_OrWhereJsonContainsKey_Call struct { +// Query_OrWhere_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhere' +type Query_OrWhere_Call struct { *mock.Call } -// OrWhereJsonContainsKey is a helper method to define mock.On call -// - column string -func (_e *Query_Expecter) OrWhereJsonContainsKey(column interface{}) *Query_OrWhereJsonContainsKey_Call { - return &Query_OrWhereJsonContainsKey_Call{Call: _e.mock.On("OrWhereJsonContainsKey", column)} +// OrWhere is a helper method to define mock.On call +// - query interface{} +// - args ...interface{} +func (_e *Query_Expecter) OrWhere(query interface{}, args ...interface{}) *Query_OrWhere_Call { + return &Query_OrWhere_Call{Call: _e.mock.On("OrWhere", + append([]interface{}{query}, args...)...)} } -func (_c *Query_OrWhereJsonContainsKey_Call) Run(run func(column string)) *Query_OrWhereJsonContainsKey_Call { +func (_c *Query_OrWhere_Call) Run(run func(query interface{}, args ...interface{})) *Query_OrWhere_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(interface{}), variadicArgs...) }) return _c } -func (_c *Query_OrWhereJsonContainsKey_Call) Return(_a0 orm.Query) *Query_OrWhereJsonContainsKey_Call { +func (_c *Query_OrWhere_Call) Return(_a0 orm.Query) *Query_OrWhere_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereJsonContainsKey_Call) RunAndReturn(run func(string) orm.Query) *Query_OrWhereJsonContainsKey_Call { +func (_c *Query_OrWhere_Call) RunAndReturn(run func(interface{}, ...interface{}) orm.Query) *Query_OrWhere_Call { _c.Call.Return(run) return _c } -// OrWhereJsonDoesntContain provides a mock function with given fields: column, value -func (_m *Query) OrWhereJsonDoesntContain(column string, value interface{}) orm.Query { - ret := _m.Called(column, value) +// OrWhereBetween provides a mock function with given fields: column, x, y +func (_m *Query) OrWhereBetween(column string, x interface{}, y interface{}) orm.Query { + ret := _m.Called(column, x, y) if len(ret) == 0 { - panic("no return value specified for OrWhereJsonDoesntContain") + panic("no return value specified for OrWhereBetween") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { - r0 = rf(column, value) + if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) orm.Query); ok { + r0 = rf(column, x, y) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2366,46 +2769,50 @@ func (_m *Query) OrWhereJsonDoesntContain(column string, value interface{}) orm. return r0 } -// Query_OrWhereJsonDoesntContain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonDoesntContain' -type Query_OrWhereJsonDoesntContain_Call struct { +// Query_OrWhereBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereBetween' +type Query_OrWhereBetween_Call struct { *mock.Call } -// OrWhereJsonDoesntContain is a helper method to define mock.On call +// OrWhereBetween is a helper method to define mock.On call // - column string -// - value interface{} -func (_e *Query_Expecter) OrWhereJsonDoesntContain(column interface{}, value interface{}) *Query_OrWhereJsonDoesntContain_Call { - return &Query_OrWhereJsonDoesntContain_Call{Call: _e.mock.On("OrWhereJsonDoesntContain", column, value)} +// - x interface{} +// - y interface{} +func (_e *Query_Expecter) OrWhereBetween(column interface{}, x interface{}, y interface{}) *Query_OrWhereBetween_Call { + return &Query_OrWhereBetween_Call{Call: _e.mock.On("OrWhereBetween", column, x, y)} } -func (_c *Query_OrWhereJsonDoesntContain_Call) Run(run func(column string, value interface{})) *Query_OrWhereJsonDoesntContain_Call { +func (_c *Query_OrWhereBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_OrWhereBetween_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{})) + run(args[0].(string), args[1].(interface{}), args[2].(interface{})) }) return _c } -func (_c *Query_OrWhereJsonDoesntContain_Call) Return(_a0 orm.Query) *Query_OrWhereJsonDoesntContain_Call { +func (_c *Query_OrWhereBetween_Call) Return(_a0 orm.Query) *Query_OrWhereBetween_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereJsonDoesntContain_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_OrWhereJsonDoesntContain_Call { +func (_c *Query_OrWhereBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_OrWhereBetween_Call { _c.Call.Return(run) return _c } -// OrWhereJsonDoesntContainKey provides a mock function with given fields: column -func (_m *Query) OrWhereJsonDoesntContainKey(column string) orm.Query { - ret := _m.Called(column) +// OrWhereDoesntHave provides a mock function with given fields: relation, args +func (_m *Query) OrWhereDoesntHave(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereJsonDoesntContainKey") + panic("no return value specified for OrWhereDoesntHave") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2415,45 +2822,56 @@ func (_m *Query) OrWhereJsonDoesntContainKey(column string) orm.Query { return r0 } -// Query_OrWhereJsonDoesntContainKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonDoesntContainKey' -type Query_OrWhereJsonDoesntContainKey_Call struct { +// Query_OrWhereDoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereDoesntHave' +type Query_OrWhereDoesntHave_Call struct { *mock.Call } -// OrWhereJsonDoesntContainKey is a helper method to define mock.On call -// - column string -func (_e *Query_Expecter) OrWhereJsonDoesntContainKey(column interface{}) *Query_OrWhereJsonDoesntContainKey_Call { - return &Query_OrWhereJsonDoesntContainKey_Call{Call: _e.mock.On("OrWhereJsonDoesntContainKey", column)} +// OrWhereDoesntHave is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *Query_Expecter) OrWhereDoesntHave(relation interface{}, args ...interface{}) *Query_OrWhereDoesntHave_Call { + return &Query_OrWhereDoesntHave_Call{Call: _e.mock.On("OrWhereDoesntHave", + append([]interface{}{relation}, args...)...)} } -func (_c *Query_OrWhereJsonDoesntContainKey_Call) Run(run func(column string)) *Query_OrWhereJsonDoesntContainKey_Call { +func (_c *Query_OrWhereDoesntHave_Call) Run(run func(relation string, args ...interface{})) *Query_OrWhereDoesntHave_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_OrWhereJsonDoesntContainKey_Call) Return(_a0 orm.Query) *Query_OrWhereJsonDoesntContainKey_Call { +func (_c *Query_OrWhereDoesntHave_Call) Return(_a0 orm.Query) *Query_OrWhereDoesntHave_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereJsonDoesntContainKey_Call) RunAndReturn(run func(string) orm.Query) *Query_OrWhereJsonDoesntContainKey_Call { +func (_c *Query_OrWhereDoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_OrWhereDoesntHave_Call { _c.Call.Return(run) return _c } -// OrWhereJsonLength provides a mock function with given fields: column, length -func (_m *Query) OrWhereJsonLength(column string, length int) orm.Query { - ret := _m.Called(column, length) +// OrWhereDoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *Query) OrWhereDoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereJsonLength") + panic("no return value specified for OrWhereDoesntHaveMorph") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, int) orm.Query); ok { - r0 = rf(column, length) + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2463,46 +2881,57 @@ func (_m *Query) OrWhereJsonLength(column string, length int) orm.Query { return r0 } -// Query_OrWhereJsonLength_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonLength' -type Query_OrWhereJsonLength_Call struct { +// Query_OrWhereDoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereDoesntHaveMorph' +type Query_OrWhereDoesntHaveMorph_Call struct { *mock.Call } -// OrWhereJsonLength is a helper method to define mock.On call -// - column string -// - length int -func (_e *Query_Expecter) OrWhereJsonLength(column interface{}, length interface{}) *Query_OrWhereJsonLength_Call { - return &Query_OrWhereJsonLength_Call{Call: _e.mock.On("OrWhereJsonLength", column, length)} +// OrWhereDoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) OrWhereDoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *Query_OrWhereDoesntHaveMorph_Call { + return &Query_OrWhereDoesntHaveMorph_Call{Call: _e.mock.On("OrWhereDoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} } -func (_c *Query_OrWhereJsonLength_Call) Run(run func(column string, length int)) *Query_OrWhereJsonLength_Call { +func (_c *Query_OrWhereDoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_OrWhereDoesntHaveMorph_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(int)) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) }) return _c } -func (_c *Query_OrWhereJsonLength_Call) Return(_a0 orm.Query) *Query_OrWhereJsonLength_Call { +func (_c *Query_OrWhereDoesntHaveMorph_Call) Return(_a0 orm.Query) *Query_OrWhereDoesntHaveMorph_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereJsonLength_Call) RunAndReturn(run func(string, int) orm.Query) *Query_OrWhereJsonLength_Call { +func (_c *Query_OrWhereDoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_OrWhereDoesntHaveMorph_Call { _c.Call.Return(run) return _c } -// OrWhereNotBetween provides a mock function with given fields: column, x, y -func (_m *Query) OrWhereNotBetween(column string, x interface{}, y interface{}) orm.Query { - ret := _m.Called(column, x, y) +// OrWhereHas provides a mock function with given fields: relation, args +func (_m *Query) OrWhereHas(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereNotBetween") + panic("no return value specified for OrWhereHas") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) orm.Query); ok { - r0 = rf(column, x, y) + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2512,47 +2941,56 @@ func (_m *Query) OrWhereNotBetween(column string, x interface{}, y interface{}) return r0 } -// Query_OrWhereNotBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereNotBetween' -type Query_OrWhereNotBetween_Call struct { +// Query_OrWhereHas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereHas' +type Query_OrWhereHas_Call struct { *mock.Call } -// OrWhereNotBetween is a helper method to define mock.On call -// - column string -// - x interface{} -// - y interface{} -func (_e *Query_Expecter) OrWhereNotBetween(column interface{}, x interface{}, y interface{}) *Query_OrWhereNotBetween_Call { - return &Query_OrWhereNotBetween_Call{Call: _e.mock.On("OrWhereNotBetween", column, x, y)} +// OrWhereHas is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *Query_Expecter) OrWhereHas(relation interface{}, args ...interface{}) *Query_OrWhereHas_Call { + return &Query_OrWhereHas_Call{Call: _e.mock.On("OrWhereHas", + append([]interface{}{relation}, args...)...)} } -func (_c *Query_OrWhereNotBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_OrWhereNotBetween_Call { +func (_c *Query_OrWhereHas_Call) Run(run func(relation string, args ...interface{})) *Query_OrWhereHas_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{}), args[2].(interface{})) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_OrWhereNotBetween_Call) Return(_a0 orm.Query) *Query_OrWhereNotBetween_Call { +func (_c *Query_OrWhereHas_Call) Return(_a0 orm.Query) *Query_OrWhereHas_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereNotBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_OrWhereNotBetween_Call { +func (_c *Query_OrWhereHas_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_OrWhereHas_Call { _c.Call.Return(run) return _c } -// OrWhereNotIn provides a mock function with given fields: column, values -func (_m *Query) OrWhereNotIn(column string, values []interface{}) orm.Query { - ret := _m.Called(column, values) +// OrWhereHasMorph provides a mock function with given fields: relation, types, args +func (_m *Query) OrWhereHasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for OrWhereNotIn") + panic("no return value specified for OrWhereHasMorph") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, []interface{}) orm.Query); ok { - r0 = rf(column, values) + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2562,46 +3000,54 @@ func (_m *Query) OrWhereNotIn(column string, values []interface{}) orm.Query { return r0 } -// Query_OrWhereNotIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereNotIn' -type Query_OrWhereNotIn_Call struct { +// Query_OrWhereHasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereHasMorph' +type Query_OrWhereHasMorph_Call struct { *mock.Call } -// OrWhereNotIn is a helper method to define mock.On call -// - column string -// - values []interface{} -func (_e *Query_Expecter) OrWhereNotIn(column interface{}, values interface{}) *Query_OrWhereNotIn_Call { - return &Query_OrWhereNotIn_Call{Call: _e.mock.On("OrWhereNotIn", column, values)} +// OrWhereHasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) OrWhereHasMorph(relation interface{}, types interface{}, args ...interface{}) *Query_OrWhereHasMorph_Call { + return &Query_OrWhereHasMorph_Call{Call: _e.mock.On("OrWhereHasMorph", + append([]interface{}{relation, types}, args...)...)} } -func (_c *Query_OrWhereNotIn_Call) Run(run func(column string, values []interface{})) *Query_OrWhereNotIn_Call { +func (_c *Query_OrWhereHasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_OrWhereHasMorph_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].([]interface{})) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) }) return _c } -func (_c *Query_OrWhereNotIn_Call) Return(_a0 orm.Query) *Query_OrWhereNotIn_Call { +func (_c *Query_OrWhereHasMorph_Call) Return(_a0 orm.Query) *Query_OrWhereHasMorph_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereNotIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_OrWhereNotIn_Call { +func (_c *Query_OrWhereHasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_OrWhereHasMorph_Call { _c.Call.Return(run) return _c } -// OrWhereNull provides a mock function with given fields: column -func (_m *Query) OrWhereNull(column string) orm.Query { - ret := _m.Called(column) +// OrWhereIn provides a mock function with given fields: column, values +func (_m *Query) OrWhereIn(column string, values []interface{}) orm.Query { + ret := _m.Called(column, values) if len(ret) == 0 { - panic("no return value specified for OrWhereNull") + panic("no return value specified for OrWhereIn") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(string, []interface{}) orm.Query); ok { + r0 = rf(column, values) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2611,45 +3057,46 @@ func (_m *Query) OrWhereNull(column string) orm.Query { return r0 } -// Query_OrWhereNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereNull' -type Query_OrWhereNull_Call struct { +// Query_OrWhereIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereIn' +type Query_OrWhereIn_Call struct { *mock.Call } -// OrWhereNull is a helper method to define mock.On call +// OrWhereIn is a helper method to define mock.On call // - column string -func (_e *Query_Expecter) OrWhereNull(column interface{}) *Query_OrWhereNull_Call { - return &Query_OrWhereNull_Call{Call: _e.mock.On("OrWhereNull", column)} +// - values []interface{} +func (_e *Query_Expecter) OrWhereIn(column interface{}, values interface{}) *Query_OrWhereIn_Call { + return &Query_OrWhereIn_Call{Call: _e.mock.On("OrWhereIn", column, values)} } -func (_c *Query_OrWhereNull_Call) Run(run func(column string)) *Query_OrWhereNull_Call { +func (_c *Query_OrWhereIn_Call) Run(run func(column string, values []interface{})) *Query_OrWhereIn_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + run(args[0].(string), args[1].([]interface{})) }) return _c } -func (_c *Query_OrWhereNull_Call) Return(_a0 orm.Query) *Query_OrWhereNull_Call { +func (_c *Query_OrWhereIn_Call) Return(_a0 orm.Query) *Query_OrWhereIn_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrWhereNull_Call) RunAndReturn(run func(string) orm.Query) *Query_OrWhereNull_Call { +func (_c *Query_OrWhereIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_OrWhereIn_Call { _c.Call.Return(run) return _c } -// Order provides a mock function with given fields: value -func (_m *Query) Order(value interface{}) orm.Query { - ret := _m.Called(value) +// OrWhereJsonContains provides a mock function with given fields: column, value +func (_m *Query) OrWhereJsonContains(column string, value interface{}) orm.Query { + ret := _m.Called(column, value) if len(ret) == 0 { - panic("no return value specified for Order") + panic("no return value specified for OrWhereJsonContains") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(interface{}) orm.Query); ok { - r0 = rf(value) + if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { + r0 = rf(column, value) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2659,52 +3106,46 @@ func (_m *Query) Order(value interface{}) orm.Query { return r0 } -// Query_Order_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Order' -type Query_Order_Call struct { +// Query_OrWhereJsonContains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonContains' +type Query_OrWhereJsonContains_Call struct { *mock.Call } -// Order is a helper method to define mock.On call +// OrWhereJsonContains is a helper method to define mock.On call +// - column string // - value interface{} -func (_e *Query_Expecter) Order(value interface{}) *Query_Order_Call { - return &Query_Order_Call{Call: _e.mock.On("Order", value)} +func (_e *Query_Expecter) OrWhereJsonContains(column interface{}, value interface{}) *Query_OrWhereJsonContains_Call { + return &Query_OrWhereJsonContains_Call{Call: _e.mock.On("OrWhereJsonContains", column, value)} } -func (_c *Query_Order_Call) Run(run func(value interface{})) *Query_Order_Call { +func (_c *Query_OrWhereJsonContains_Call) Run(run func(column string, value interface{})) *Query_OrWhereJsonContains_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{})) + run(args[0].(string), args[1].(interface{})) }) return _c } -func (_c *Query_Order_Call) Return(_a0 orm.Query) *Query_Order_Call { +func (_c *Query_OrWhereJsonContains_Call) Return(_a0 orm.Query) *Query_OrWhereJsonContains_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Order_Call) RunAndReturn(run func(interface{}) orm.Query) *Query_Order_Call { +func (_c *Query_OrWhereJsonContains_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_OrWhereJsonContains_Call { _c.Call.Return(run) return _c } -// OrderBy provides a mock function with given fields: column, direction -func (_m *Query) OrderBy(column string, direction ...string) orm.Query { - _va := make([]interface{}, len(direction)) - for _i := range direction { - _va[_i] = direction[_i] - } - var _ca []interface{} - _ca = append(_ca, column) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) +// OrWhereJsonContainsKey provides a mock function with given fields: column +func (_m *Query) OrWhereJsonContainsKey(column string) orm.Query { + ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for OrderBy") + panic("no return value specified for OrWhereJsonContainsKey") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, ...string) orm.Query); ok { - r0 = rf(column, direction...) + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(column) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2714,53 +3155,45 @@ func (_m *Query) OrderBy(column string, direction ...string) orm.Query { return r0 } -// Query_OrderBy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderBy' -type Query_OrderBy_Call struct { +// Query_OrWhereJsonContainsKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonContainsKey' +type Query_OrWhereJsonContainsKey_Call struct { *mock.Call } -// OrderBy is a helper method to define mock.On call +// OrWhereJsonContainsKey is a helper method to define mock.On call // - column string -// - direction ...string -func (_e *Query_Expecter) OrderBy(column interface{}, direction ...interface{}) *Query_OrderBy_Call { - return &Query_OrderBy_Call{Call: _e.mock.On("OrderBy", - append([]interface{}{column}, direction...)...)} +func (_e *Query_Expecter) OrWhereJsonContainsKey(column interface{}) *Query_OrWhereJsonContainsKey_Call { + return &Query_OrWhereJsonContainsKey_Call{Call: _e.mock.On("OrWhereJsonContainsKey", column)} } -func (_c *Query_OrderBy_Call) Run(run func(column string, direction ...string)) *Query_OrderBy_Call { +func (_c *Query_OrWhereJsonContainsKey_Call) Run(run func(column string)) *Query_OrWhereJsonContainsKey_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]string, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(string) - } - } - run(args[0].(string), variadicArgs...) + run(args[0].(string)) }) return _c } -func (_c *Query_OrderBy_Call) Return(_a0 orm.Query) *Query_OrderBy_Call { +func (_c *Query_OrWhereJsonContainsKey_Call) Return(_a0 orm.Query) *Query_OrWhereJsonContainsKey_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrderBy_Call) RunAndReturn(run func(string, ...string) orm.Query) *Query_OrderBy_Call { +func (_c *Query_OrWhereJsonContainsKey_Call) RunAndReturn(run func(string) orm.Query) *Query_OrWhereJsonContainsKey_Call { _c.Call.Return(run) return _c } -// OrderByDesc provides a mock function with given fields: column -func (_m *Query) OrderByDesc(column string) orm.Query { - ret := _m.Called(column) +// OrWhereJsonDoesntContain provides a mock function with given fields: column, value +func (_m *Query) OrWhereJsonDoesntContain(column string, value interface{}) orm.Query { + ret := _m.Called(column, value) if len(ret) == 0 { - panic("no return value specified for OrderByDesc") + panic("no return value specified for OrWhereJsonDoesntContain") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { + r0 = rf(column, value) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2770,45 +3203,46 @@ func (_m *Query) OrderByDesc(column string) orm.Query { return r0 } -// Query_OrderByDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderByDesc' -type Query_OrderByDesc_Call struct { +// Query_OrWhereJsonDoesntContain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonDoesntContain' +type Query_OrWhereJsonDoesntContain_Call struct { *mock.Call } -// OrderByDesc is a helper method to define mock.On call +// OrWhereJsonDoesntContain is a helper method to define mock.On call // - column string -func (_e *Query_Expecter) OrderByDesc(column interface{}) *Query_OrderByDesc_Call { - return &Query_OrderByDesc_Call{Call: _e.mock.On("OrderByDesc", column)} +// - value interface{} +func (_e *Query_Expecter) OrWhereJsonDoesntContain(column interface{}, value interface{}) *Query_OrWhereJsonDoesntContain_Call { + return &Query_OrWhereJsonDoesntContain_Call{Call: _e.mock.On("OrWhereJsonDoesntContain", column, value)} } -func (_c *Query_OrderByDesc_Call) Run(run func(column string)) *Query_OrderByDesc_Call { +func (_c *Query_OrWhereJsonDoesntContain_Call) Run(run func(column string, value interface{})) *Query_OrWhereJsonDoesntContain_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + run(args[0].(string), args[1].(interface{})) }) return _c } -func (_c *Query_OrderByDesc_Call) Return(_a0 orm.Query) *Query_OrderByDesc_Call { +func (_c *Query_OrWhereJsonDoesntContain_Call) Return(_a0 orm.Query) *Query_OrWhereJsonDoesntContain_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrderByDesc_Call) RunAndReturn(run func(string) orm.Query) *Query_OrderByDesc_Call { +func (_c *Query_OrWhereJsonDoesntContain_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_OrWhereJsonDoesntContain_Call { _c.Call.Return(run) return _c } -// OrderByRaw provides a mock function with given fields: raw -func (_m *Query) OrderByRaw(raw string) orm.Query { - ret := _m.Called(raw) +// OrWhereJsonDoesntContainKey provides a mock function with given fields: column +func (_m *Query) OrWhereJsonDoesntContainKey(column string) orm.Query { + ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for OrderByRaw") + panic("no return value specified for OrWhereJsonDoesntContainKey") } var r0 orm.Query if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(raw) + r0 = rf(column) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2818,144 +3252,144 @@ func (_m *Query) OrderByRaw(raw string) orm.Query { return r0 } -// Query_OrderByRaw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderByRaw' -type Query_OrderByRaw_Call struct { +// Query_OrWhereJsonDoesntContainKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonDoesntContainKey' +type Query_OrWhereJsonDoesntContainKey_Call struct { *mock.Call } -// OrderByRaw is a helper method to define mock.On call -// - raw string -func (_e *Query_Expecter) OrderByRaw(raw interface{}) *Query_OrderByRaw_Call { - return &Query_OrderByRaw_Call{Call: _e.mock.On("OrderByRaw", raw)} +// OrWhereJsonDoesntContainKey is a helper method to define mock.On call +// - column string +func (_e *Query_Expecter) OrWhereJsonDoesntContainKey(column interface{}) *Query_OrWhereJsonDoesntContainKey_Call { + return &Query_OrWhereJsonDoesntContainKey_Call{Call: _e.mock.On("OrWhereJsonDoesntContainKey", column)} } -func (_c *Query_OrderByRaw_Call) Run(run func(raw string)) *Query_OrderByRaw_Call { +func (_c *Query_OrWhereJsonDoesntContainKey_Call) Run(run func(column string)) *Query_OrWhereJsonDoesntContainKey_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string)) }) return _c } -func (_c *Query_OrderByRaw_Call) Return(_a0 orm.Query) *Query_OrderByRaw_Call { +func (_c *Query_OrWhereJsonDoesntContainKey_Call) Return(_a0 orm.Query) *Query_OrWhereJsonDoesntContainKey_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_OrderByRaw_Call) RunAndReturn(run func(string) orm.Query) *Query_OrderByRaw_Call { +func (_c *Query_OrWhereJsonDoesntContainKey_Call) RunAndReturn(run func(string) orm.Query) *Query_OrWhereJsonDoesntContainKey_Call { _c.Call.Return(run) return _c } -// Paginate provides a mock function with given fields: page, limit, dest, total -func (_m *Query) Paginate(page int, limit int, dest interface{}, total *int64) error { - ret := _m.Called(page, limit, dest, total) +// OrWhereJsonLength provides a mock function with given fields: column, length +func (_m *Query) OrWhereJsonLength(column string, length int) orm.Query { + ret := _m.Called(column, length) if len(ret) == 0 { - panic("no return value specified for Paginate") + panic("no return value specified for OrWhereJsonLength") } - var r0 error - if rf, ok := ret.Get(0).(func(int, int, interface{}, *int64) error); ok { - r0 = rf(page, limit, dest, total) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, int) orm.Query); ok { + r0 = rf(column, length) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } } return r0 } -// Query_Paginate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Paginate' -type Query_Paginate_Call struct { +// Query_OrWhereJsonLength_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereJsonLength' +type Query_OrWhereJsonLength_Call struct { *mock.Call } -// Paginate is a helper method to define mock.On call -// - page int -// - limit int -// - dest interface{} -// - total *int64 -func (_e *Query_Expecter) Paginate(page interface{}, limit interface{}, dest interface{}, total interface{}) *Query_Paginate_Call { - return &Query_Paginate_Call{Call: _e.mock.On("Paginate", page, limit, dest, total)} +// OrWhereJsonLength is a helper method to define mock.On call +// - column string +// - length int +func (_e *Query_Expecter) OrWhereJsonLength(column interface{}, length interface{}) *Query_OrWhereJsonLength_Call { + return &Query_OrWhereJsonLength_Call{Call: _e.mock.On("OrWhereJsonLength", column, length)} } -func (_c *Query_Paginate_Call) Run(run func(page int, limit int, dest interface{}, total *int64)) *Query_Paginate_Call { +func (_c *Query_OrWhereJsonLength_Call) Run(run func(column string, length int)) *Query_OrWhereJsonLength_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(int), args[1].(int), args[2].(interface{}), args[3].(*int64)) + run(args[0].(string), args[1].(int)) }) return _c } -func (_c *Query_Paginate_Call) Return(_a0 error) *Query_Paginate_Call { +func (_c *Query_OrWhereJsonLength_Call) Return(_a0 orm.Query) *Query_OrWhereJsonLength_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Paginate_Call) RunAndReturn(run func(int, int, interface{}, *int64) error) *Query_Paginate_Call { +func (_c *Query_OrWhereJsonLength_Call) RunAndReturn(run func(string, int) orm.Query) *Query_OrWhereJsonLength_Call { _c.Call.Return(run) return _c } -// Pluck provides a mock function with given fields: column, dest -func (_m *Query) Pluck(column string, dest interface{}) error { - ret := _m.Called(column, dest) +// OrWhereNotBetween provides a mock function with given fields: column, x, y +func (_m *Query) OrWhereNotBetween(column string, x interface{}, y interface{}) orm.Query { + ret := _m.Called(column, x, y) if len(ret) == 0 { - panic("no return value specified for Pluck") + panic("no return value specified for OrWhereNotBetween") } - var r0 error - if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { - r0 = rf(column, dest) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) orm.Query); ok { + r0 = rf(column, x, y) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } } return r0 } -// Query_Pluck_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pluck' -type Query_Pluck_Call struct { +// Query_OrWhereNotBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereNotBetween' +type Query_OrWhereNotBetween_Call struct { *mock.Call } -// Pluck is a helper method to define mock.On call +// OrWhereNotBetween is a helper method to define mock.On call // - column string -// - dest interface{} -func (_e *Query_Expecter) Pluck(column interface{}, dest interface{}) *Query_Pluck_Call { - return &Query_Pluck_Call{Call: _e.mock.On("Pluck", column, dest)} +// - x interface{} +// - y interface{} +func (_e *Query_Expecter) OrWhereNotBetween(column interface{}, x interface{}, y interface{}) *Query_OrWhereNotBetween_Call { + return &Query_OrWhereNotBetween_Call{Call: _e.mock.On("OrWhereNotBetween", column, x, y)} } -func (_c *Query_Pluck_Call) Run(run func(column string, dest interface{})) *Query_Pluck_Call { +func (_c *Query_OrWhereNotBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_OrWhereNotBetween_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{})) + run(args[0].(string), args[1].(interface{}), args[2].(interface{})) }) return _c } -func (_c *Query_Pluck_Call) Return(_a0 error) *Query_Pluck_Call { +func (_c *Query_OrWhereNotBetween_Call) Return(_a0 orm.Query) *Query_OrWhereNotBetween_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Pluck_Call) RunAndReturn(run func(string, interface{}) error) *Query_Pluck_Call { +func (_c *Query_OrWhereNotBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_OrWhereNotBetween_Call { _c.Call.Return(run) return _c } -// Raw provides a mock function with given fields: _a0, values -func (_m *Query) Raw(_a0 string, values ...interface{}) orm.Query { - var _ca []interface{} - _ca = append(_ca, _a0) - _ca = append(_ca, values...) - ret := _m.Called(_ca...) +// OrWhereNotIn provides a mock function with given fields: column, values +func (_m *Query) OrWhereNotIn(column string, values []interface{}) orm.Query { + ret := _m.Called(column, values) if len(ret) == 0 { - panic("no return value specified for Raw") + panic("no return value specified for OrWhereNotIn") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { - r0 = rf(_a0, values...) + if rf, ok := ret.Get(0).(func(string, []interface{}) orm.Query); ok { + r0 = rf(column, values) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -2965,165 +3399,1301 @@ func (_m *Query) Raw(_a0 string, values ...interface{}) orm.Query { return r0 } -// Query_Raw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Raw' -type Query_Raw_Call struct { +// Query_OrWhereNotIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereNotIn' +type Query_OrWhereNotIn_Call struct { *mock.Call } -// Raw is a helper method to define mock.On call -// - _a0 string -// - values ...interface{} -func (_e *Query_Expecter) Raw(_a0 interface{}, values ...interface{}) *Query_Raw_Call { - return &Query_Raw_Call{Call: _e.mock.On("Raw", - append([]interface{}{_a0}, values...)...)} +// OrWhereNotIn is a helper method to define mock.On call +// - column string +// - values []interface{} +func (_e *Query_Expecter) OrWhereNotIn(column interface{}, values interface{}) *Query_OrWhereNotIn_Call { + return &Query_OrWhereNotIn_Call{Call: _e.mock.On("OrWhereNotIn", column, values)} } -func (_c *Query_Raw_Call) Run(run func(_a0 string, values ...interface{})) *Query_Raw_Call { +func (_c *Query_OrWhereNotIn_Call) Run(run func(column string, values []interface{})) *Query_OrWhereNotIn_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(args[0].(string), variadicArgs...) + run(args[0].(string), args[1].([]interface{})) }) return _c } -func (_c *Query_Raw_Call) Return(_a0 orm.Query) *Query_Raw_Call { +func (_c *Query_OrWhereNotIn_Call) Return(_a0 orm.Query) *Query_OrWhereNotIn_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Raw_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_Raw_Call { +func (_c *Query_OrWhereNotIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_OrWhereNotIn_Call { _c.Call.Return(run) return _c } -// Restore provides a mock function with given fields: model -func (_m *Query) Restore(model ...interface{}) (*db.Result, error) { - var _ca []interface{} - _ca = append(_ca, model...) - ret := _m.Called(_ca...) +// OrWhereNull provides a mock function with given fields: column +func (_m *Query) OrWhereNull(column string) orm.Query { + ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for Restore") + panic("no return value specified for OrWhereNull") } - var r0 *db.Result - var r1 error - if rf, ok := ret.Get(0).(func(...interface{}) (*db.Result, error)); ok { - return rf(model...) - } - if rf, ok := ret.Get(0).(func(...interface{}) *db.Result); ok { - r0 = rf(model...) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(column) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*db.Result) + r0 = ret.Get(0).(orm.Query) } } - if rf, ok := ret.Get(1).(func(...interface{}) error); ok { - r1 = rf(model...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } -// Query_Restore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Restore' -type Query_Restore_Call struct { +// Query_OrWhereNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereNull' +type Query_OrWhereNull_Call struct { *mock.Call } -// Restore is a helper method to define mock.On call -// - model ...interface{} -func (_e *Query_Expecter) Restore(model ...interface{}) *Query_Restore_Call { - return &Query_Restore_Call{Call: _e.mock.On("Restore", - append([]interface{}{}, model...)...)} -} +// OrWhereNull is a helper method to define mock.On call +// - column string +func (_e *Query_Expecter) OrWhereNull(column interface{}) *Query_OrWhereNull_Call { + return &Query_OrWhereNull_Call{Call: _e.mock.On("OrWhereNull", column)} +} + +func (_c *Query_OrWhereNull_Call) Run(run func(column string)) *Query_OrWhereNull_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Query_OrWhereNull_Call) Return(_a0 orm.Query) *Query_OrWhereNull_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_OrWhereNull_Call) RunAndReturn(run func(string) orm.Query) *Query_OrWhereNull_Call { + _c.Call.Return(run) + return _c +} + +// Order provides a mock function with given fields: value +func (_m *Query) Order(value interface{}) orm.Query { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for Order") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(interface{}) orm.Query); ok { + r0 = rf(value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Order_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Order' +type Query_Order_Call struct { + *mock.Call +} + +// Order is a helper method to define mock.On call +// - value interface{} +func (_e *Query_Expecter) Order(value interface{}) *Query_Order_Call { + return &Query_Order_Call{Call: _e.mock.On("Order", value)} +} + +func (_c *Query_Order_Call) Run(run func(value interface{})) *Query_Order_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *Query_Order_Call) Return(_a0 orm.Query) *Query_Order_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Order_Call) RunAndReturn(run func(interface{}) orm.Query) *Query_Order_Call { + _c.Call.Return(run) + return _c +} + +// OrderBy provides a mock function with given fields: column, direction +func (_m *Query) OrderBy(column string, direction ...string) orm.Query { + _va := make([]interface{}, len(direction)) + for _i := range direction { + _va[_i] = direction[_i] + } + var _ca []interface{} + _ca = append(_ca, column) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrderBy") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...string) orm.Query); ok { + r0 = rf(column, direction...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_OrderBy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderBy' +type Query_OrderBy_Call struct { + *mock.Call +} + +// OrderBy is a helper method to define mock.On call +// - column string +// - direction ...string +func (_e *Query_Expecter) OrderBy(column interface{}, direction ...interface{}) *Query_OrderBy_Call { + return &Query_OrderBy_Call{Call: _e.mock.On("OrderBy", + append([]interface{}{column}, direction...)...)} +} + +func (_c *Query_OrderBy_Call) Run(run func(column string, direction ...string)) *Query_OrderBy_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Query_OrderBy_Call) Return(_a0 orm.Query) *Query_OrderBy_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_OrderBy_Call) RunAndReturn(run func(string, ...string) orm.Query) *Query_OrderBy_Call { + _c.Call.Return(run) + return _c +} + +// OrderByDesc provides a mock function with given fields: column +func (_m *Query) OrderByDesc(column string) orm.Query { + ret := _m.Called(column) + + if len(ret) == 0 { + panic("no return value specified for OrderByDesc") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(column) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_OrderByDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderByDesc' +type Query_OrderByDesc_Call struct { + *mock.Call +} + +// OrderByDesc is a helper method to define mock.On call +// - column string +func (_e *Query_Expecter) OrderByDesc(column interface{}) *Query_OrderByDesc_Call { + return &Query_OrderByDesc_Call{Call: _e.mock.On("OrderByDesc", column)} +} + +func (_c *Query_OrderByDesc_Call) Run(run func(column string)) *Query_OrderByDesc_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Query_OrderByDesc_Call) Return(_a0 orm.Query) *Query_OrderByDesc_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_OrderByDesc_Call) RunAndReturn(run func(string) orm.Query) *Query_OrderByDesc_Call { + _c.Call.Return(run) + return _c +} + +// OrderByRaw provides a mock function with given fields: raw +func (_m *Query) OrderByRaw(raw string) orm.Query { + ret := _m.Called(raw) + + if len(ret) == 0 { + panic("no return value specified for OrderByRaw") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(raw) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_OrderByRaw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderByRaw' +type Query_OrderByRaw_Call struct { + *mock.Call +} + +// OrderByRaw is a helper method to define mock.On call +// - raw string +func (_e *Query_Expecter) OrderByRaw(raw interface{}) *Query_OrderByRaw_Call { + return &Query_OrderByRaw_Call{Call: _e.mock.On("OrderByRaw", raw)} +} + +func (_c *Query_OrderByRaw_Call) Run(run func(raw string)) *Query_OrderByRaw_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Query_OrderByRaw_Call) Return(_a0 orm.Query) *Query_OrderByRaw_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_OrderByRaw_Call) RunAndReturn(run func(string) orm.Query) *Query_OrderByRaw_Call { + _c.Call.Return(run) + return _c +} + +// Paginate provides a mock function with given fields: page, limit, dest, total +func (_m *Query) Paginate(page int, limit int, dest interface{}, total *int64) error { + ret := _m.Called(page, limit, dest, total) + + if len(ret) == 0 { + panic("no return value specified for Paginate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int, int, interface{}, *int64) error); ok { + r0 = rf(page, limit, dest, total) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_Paginate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Paginate' +type Query_Paginate_Call struct { + *mock.Call +} + +// Paginate is a helper method to define mock.On call +// - page int +// - limit int +// - dest interface{} +// - total *int64 +func (_e *Query_Expecter) Paginate(page interface{}, limit interface{}, dest interface{}, total interface{}) *Query_Paginate_Call { + return &Query_Paginate_Call{Call: _e.mock.On("Paginate", page, limit, dest, total)} +} + +func (_c *Query_Paginate_Call) Run(run func(page int, limit int, dest interface{}, total *int64)) *Query_Paginate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(int), args[2].(interface{}), args[3].(*int64)) + }) + return _c +} + +func (_c *Query_Paginate_Call) Return(_a0 error) *Query_Paginate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Paginate_Call) RunAndReturn(run func(int, int, interface{}, *int64) error) *Query_Paginate_Call { + _c.Call.Return(run) + return _c +} + +// Pluck provides a mock function with given fields: column, dest +func (_m *Query) Pluck(column string, dest interface{}) error { + ret := _m.Called(column, dest) + + if len(ret) == 0 { + panic("no return value specified for Pluck") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { + r0 = rf(column, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_Pluck_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pluck' +type Query_Pluck_Call struct { + *mock.Call +} + +// Pluck is a helper method to define mock.On call +// - column string +// - dest interface{} +func (_e *Query_Expecter) Pluck(column interface{}, dest interface{}) *Query_Pluck_Call { + return &Query_Pluck_Call{Call: _e.mock.On("Pluck", column, dest)} +} + +func (_c *Query_Pluck_Call) Run(run func(column string, dest interface{})) *Query_Pluck_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(interface{})) + }) + return _c +} + +func (_c *Query_Pluck_Call) Return(_a0 error) *Query_Pluck_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Pluck_Call) RunAndReturn(run func(string, interface{}) error) *Query_Pluck_Call { + _c.Call.Return(run) + return _c +} + +// Raw provides a mock function with given fields: _a0, values +func (_m *Query) Raw(_a0 string, values ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, _a0) + _ca = append(_ca, values...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Raw") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(_a0, values...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Raw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Raw' +type Query_Raw_Call struct { + *mock.Call +} + +// Raw is a helper method to define mock.On call +// - _a0 string +// - values ...interface{} +func (_e *Query_Expecter) Raw(_a0 interface{}, values ...interface{}) *Query_Raw_Call { + return &Query_Raw_Call{Call: _e.mock.On("Raw", + append([]interface{}{_a0}, values...)...)} +} + +func (_c *Query_Raw_Call) Run(run func(_a0 string, values ...interface{})) *Query_Raw_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Query_Raw_Call) Return(_a0 orm.Query) *Query_Raw_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Raw_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_Raw_Call { + _c.Call.Return(run) + return _c +} + +// Related provides a mock function with given fields: parent, name +func (_m *Query) Related(parent interface{}, name string) orm.Query { + ret := _m.Called(parent, name) + + if len(ret) == 0 { + panic("no return value specified for Related") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(interface{}, string) orm.Query); ok { + r0 = rf(parent, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Related_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Related' +type Query_Related_Call struct { + *mock.Call +} + +// Related is a helper method to define mock.On call +// - parent interface{} +// - name string +func (_e *Query_Expecter) Related(parent interface{}, name interface{}) *Query_Related_Call { + return &Query_Related_Call{Call: _e.mock.On("Related", parent, name)} +} + +func (_c *Query_Related_Call) Run(run func(parent interface{}, name string)) *Query_Related_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(string)) + }) + return _c +} + +func (_c *Query_Related_Call) Return(_a0 orm.Query) *Query_Related_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Related_Call) RunAndReturn(run func(interface{}, string) orm.Query) *Query_Related_Call { + _c.Call.Return(run) + return _c +} + +// Relation provides a mock function with given fields: parent, name +func (_m *Query) Relation(parent interface{}, name string) orm.RelationWriter { + ret := _m.Called(parent, name) + + if len(ret) == 0 { + panic("no return value specified for Relation") + } + + var r0 orm.RelationWriter + if rf, ok := ret.Get(0).(func(interface{}, string) orm.RelationWriter); ok { + r0 = rf(parent, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.RelationWriter) + } + } + + return r0 +} + +// Query_Relation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Relation' +type Query_Relation_Call struct { + *mock.Call +} + +// Relation is a helper method to define mock.On call +// - parent interface{} +// - name string +func (_e *Query_Expecter) Relation(parent interface{}, name interface{}) *Query_Relation_Call { + return &Query_Relation_Call{Call: _e.mock.On("Relation", parent, name)} +} + +func (_c *Query_Relation_Call) Run(run func(parent interface{}, name string)) *Query_Relation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(string)) + }) + return _c +} + +func (_c *Query_Relation_Call) Return(_a0 orm.RelationWriter) *Query_Relation_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Relation_Call) RunAndReturn(run func(interface{}, string) orm.RelationWriter) *Query_Relation_Call { + _c.Call.Return(run) + return _c +} + +// Restore provides a mock function with given fields: model +func (_m *Query) Restore(model ...interface{}) (*db.Result, error) { + var _ca []interface{} + _ca = append(_ca, model...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Restore") + } + + var r0 *db.Result + var r1 error + if rf, ok := ret.Get(0).(func(...interface{}) (*db.Result, error)); ok { + return rf(model...) + } + if rf, ok := ret.Get(0).(func(...interface{}) *db.Result); ok { + r0 = rf(model...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.Result) + } + } + + if rf, ok := ret.Get(1).(func(...interface{}) error); ok { + r1 = rf(model...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Query_Restore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Restore' +type Query_Restore_Call struct { + *mock.Call +} + +// Restore is a helper method to define mock.On call +// - model ...interface{} +func (_e *Query_Expecter) Restore(model ...interface{}) *Query_Restore_Call { + return &Query_Restore_Call{Call: _e.mock.On("Restore", + append([]interface{}{}, model...)...)} +} + +func (_c *Query_Restore_Call) Run(run func(model ...interface{})) *Query_Restore_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Query_Restore_Call) Return(_a0 *db.Result, _a1 error) *Query_Restore_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Query_Restore_Call) RunAndReturn(run func(...interface{}) (*db.Result, error)) *Query_Restore_Call { + _c.Call.Return(run) + return _c +} + +// Rollback provides a mock function with no fields +func (_m *Query) Rollback() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Rollback") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_Rollback_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rollback' +type Query_Rollback_Call struct { + *mock.Call +} + +// Rollback is a helper method to define mock.On call +func (_e *Query_Expecter) Rollback() *Query_Rollback_Call { + return &Query_Rollback_Call{Call: _e.mock.On("Rollback")} +} + +func (_c *Query_Rollback_Call) Run(run func()) *Query_Rollback_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Query_Rollback_Call) Return(_a0 error) *Query_Rollback_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Rollback_Call) RunAndReturn(run func() error) *Query_Rollback_Call { + _c.Call.Return(run) + return _c +} + +// Save provides a mock function with given fields: value +func (_m *Query) Save(value interface{}) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' +type Query_Save_Call struct { + *mock.Call +} + +// Save is a helper method to define mock.On call +// - value interface{} +func (_e *Query_Expecter) Save(value interface{}) *Query_Save_Call { + return &Query_Save_Call{Call: _e.mock.On("Save", value)} +} + +func (_c *Query_Save_Call) Run(run func(value interface{})) *Query_Save_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *Query_Save_Call) Return(_a0 error) *Query_Save_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Save_Call) RunAndReturn(run func(interface{}) error) *Query_Save_Call { + _c.Call.Return(run) + return _c +} + +// SaveQuietly provides a mock function with given fields: value +func (_m *Query) SaveQuietly(value interface{}) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for SaveQuietly") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_SaveQuietly_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveQuietly' +type Query_SaveQuietly_Call struct { + *mock.Call +} + +// SaveQuietly is a helper method to define mock.On call +// - value interface{} +func (_e *Query_Expecter) SaveQuietly(value interface{}) *Query_SaveQuietly_Call { + return &Query_SaveQuietly_Call{Call: _e.mock.On("SaveQuietly", value)} +} + +func (_c *Query_SaveQuietly_Call) Run(run func(value interface{})) *Query_SaveQuietly_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *Query_SaveQuietly_Call) Return(_a0 error) *Query_SaveQuietly_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_SaveQuietly_Call) RunAndReturn(run func(interface{}) error) *Query_SaveQuietly_Call { + _c.Call.Return(run) + return _c +} + +// Scan provides a mock function with given fields: dest +func (_m *Query) Scan(dest interface{}) error { + ret := _m.Called(dest) + + if len(ret) == 0 { + panic("no return value specified for Scan") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_Scan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scan' +type Query_Scan_Call struct { + *mock.Call +} + +// Scan is a helper method to define mock.On call +// - dest interface{} +func (_e *Query_Expecter) Scan(dest interface{}) *Query_Scan_Call { + return &Query_Scan_Call{Call: _e.mock.On("Scan", dest)} +} + +func (_c *Query_Scan_Call) Run(run func(dest interface{})) *Query_Scan_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *Query_Scan_Call) Return(_a0 error) *Query_Scan_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Scan_Call) RunAndReturn(run func(interface{}) error) *Query_Scan_Call { + _c.Call.Return(run) + return _c +} + +// Scopes provides a mock function with given fields: funcs +func (_m *Query) Scopes(funcs ...func(orm.Query) orm.Query) orm.Query { + _va := make([]interface{}, len(funcs)) + for _i := range funcs { + _va[_i] = funcs[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Scopes") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(...func(orm.Query) orm.Query) orm.Query); ok { + r0 = rf(funcs...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Scopes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scopes' +type Query_Scopes_Call struct { + *mock.Call +} + +// Scopes is a helper method to define mock.On call +// - funcs ...func(orm.Query) orm.Query +func (_e *Query_Expecter) Scopes(funcs ...interface{}) *Query_Scopes_Call { + return &Query_Scopes_Call{Call: _e.mock.On("Scopes", + append([]interface{}{}, funcs...)...)} +} + +func (_c *Query_Scopes_Call) Run(run func(funcs ...func(orm.Query) orm.Query)) *Query_Scopes_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]func(orm.Query) orm.Query, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(func(orm.Query) orm.Query) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Query_Scopes_Call) Return(_a0 orm.Query) *Query_Scopes_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Scopes_Call) RunAndReturn(run func(...func(orm.Query) orm.Query) orm.Query) *Query_Scopes_Call { + _c.Call.Return(run) + return _c +} + +// Select provides a mock function with given fields: columns +func (_m *Query) Select(columns ...string) orm.Query { + _va := make([]interface{}, len(columns)) + for _i := range columns { + _va[_i] = columns[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Select") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { + r0 = rf(columns...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Select_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Select' +type Query_Select_Call struct { + *mock.Call +} + +// Select is a helper method to define mock.On call +// - columns ...string +func (_e *Query_Expecter) Select(columns ...interface{}) *Query_Select_Call { + return &Query_Select_Call{Call: _e.mock.On("Select", + append([]interface{}{}, columns...)...)} +} + +func (_c *Query_Select_Call) Run(run func(columns ...string)) *Query_Select_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Query_Select_Call) Return(_a0 orm.Query) *Query_Select_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Select_Call) RunAndReturn(run func(...string) orm.Query) *Query_Select_Call { + _c.Call.Return(run) + return _c +} + +// SelectRaw provides a mock function with given fields: query, args +func (_m *Query) SelectRaw(query interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for SelectRaw") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) orm.Query); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_SelectRaw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectRaw' +type Query_SelectRaw_Call struct { + *mock.Call +} + +// SelectRaw is a helper method to define mock.On call +// - query interface{} +// - args ...interface{} +func (_e *Query_Expecter) SelectRaw(query interface{}, args ...interface{}) *Query_SelectRaw_Call { + return &Query_SelectRaw_Call{Call: _e.mock.On("SelectRaw", + append([]interface{}{query}, args...)...)} +} + +func (_c *Query_SelectRaw_Call) Run(run func(query interface{}, args ...interface{})) *Query_SelectRaw_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(interface{}), variadicArgs...) + }) + return _c +} + +func (_c *Query_SelectRaw_Call) Return(_a0 orm.Query) *Query_SelectRaw_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_SelectRaw_Call) RunAndReturn(run func(interface{}, ...interface{}) orm.Query) *Query_SelectRaw_Call { + _c.Call.Return(run) + return _c +} + +// SharedLock provides a mock function with no fields +func (_m *Query) SharedLock() orm.Query { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SharedLock") + } -func (_c *Query_Restore_Call) Run(run func(model ...interface{})) *Query_Restore_Call { + var r0 orm.Query + if rf, ok := ret.Get(0).(func() orm.Query); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_SharedLock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SharedLock' +type Query_SharedLock_Call struct { + *mock.Call +} + +// SharedLock is a helper method to define mock.On call +func (_e *Query_Expecter) SharedLock() *Query_SharedLock_Call { + return &Query_SharedLock_Call{Call: _e.mock.On("SharedLock")} +} + +func (_c *Query_SharedLock_Call) Run(run func()) *Query_SharedLock_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-0) - for i, a := range args[0:] { + run() + }) + return _c +} + +func (_c *Query_SharedLock_Call) Return(_a0 orm.Query) *Query_SharedLock_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_SharedLock_Call) RunAndReturn(run func() orm.Query) *Query_SharedLock_Call { + _c.Call.Return(run) + return _c +} + +// Sum provides a mock function with given fields: column, dest +func (_m *Query) Sum(column string, dest interface{}) error { + ret := _m.Called(column, dest) + + if len(ret) == 0 { + panic("no return value specified for Sum") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { + r0 = rf(column, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Query_Sum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sum' +type Query_Sum_Call struct { + *mock.Call +} + +// Sum is a helper method to define mock.On call +// - column string +// - dest interface{} +func (_e *Query_Expecter) Sum(column interface{}, dest interface{}) *Query_Sum_Call { + return &Query_Sum_Call{Call: _e.mock.On("Sum", column, dest)} +} + +func (_c *Query_Sum_Call) Run(run func(column string, dest interface{})) *Query_Sum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(interface{})) + }) + return _c +} + +func (_c *Query_Sum_Call) Return(_a0 error) *Query_Sum_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Sum_Call) RunAndReturn(run func(string, interface{}) error) *Query_Sum_Call { + _c.Call.Return(run) + return _c +} + +// Table provides a mock function with given fields: name, args +func (_m *Query) Table(name string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, name) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Table") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(name, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Table_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Table' +type Query_Table_Call struct { + *mock.Call +} + +// Table is a helper method to define mock.On call +// - name string +// - args ...interface{} +func (_e *Query_Expecter) Table(name interface{}, args ...interface{}) *Query_Table_Call { + return &Query_Table_Call{Call: _e.mock.On("Table", + append([]interface{}{name}, args...)...)} +} + +func (_c *Query_Table_Call) Run(run func(name string, args ...interface{})) *Query_Table_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { if a != nil { variadicArgs[i] = a.(interface{}) } } - run(variadicArgs...) + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_Restore_Call) Return(_a0 *db.Result, _a1 error) *Query_Restore_Call { - _c.Call.Return(_a0, _a1) +func (_c *Query_Table_Call) Return(_a0 orm.Query) *Query_Table_Call { + _c.Call.Return(_a0) return _c } -func (_c *Query_Restore_Call) RunAndReturn(run func(...interface{}) (*db.Result, error)) *Query_Restore_Call { +func (_c *Query_Table_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_Table_Call { _c.Call.Return(run) return _c } -// Rollback provides a mock function with no fields -func (_m *Query) Rollback() error { +// ToRawSql provides a mock function with no fields +func (_m *Query) ToRawSql() orm.ToSql { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for Rollback") + panic("no return value specified for ToRawSql") } - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { + var r0 orm.ToSql + if rf, ok := ret.Get(0).(func() orm.ToSql); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.ToSql) + } + } + + return r0 +} + +// Query_ToRawSql_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToRawSql' +type Query_ToRawSql_Call struct { + *mock.Call +} + +// ToRawSql is a helper method to define mock.On call +func (_e *Query_Expecter) ToRawSql() *Query_ToRawSql_Call { + return &Query_ToRawSql_Call{Call: _e.mock.On("ToRawSql")} +} + +func (_c *Query_ToRawSql_Call) Run(run func()) *Query_ToRawSql_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Query_ToRawSql_Call) Return(_a0 orm.ToSql) *Query_ToRawSql_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_ToRawSql_Call) RunAndReturn(run func() orm.ToSql) *Query_ToRawSql_Call { + _c.Call.Return(run) + return _c +} + +// ToSql provides a mock function with no fields +func (_m *Query) ToSql() orm.ToSql { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ToSql") + } + + var r0 orm.ToSql + if rf, ok := ret.Get(0).(func() orm.ToSql); ok { r0 = rf() } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.ToSql) + } + } + + return r0 +} + +// Query_ToSql_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToSql' +type Query_ToSql_Call struct { + *mock.Call +} + +// ToSql is a helper method to define mock.On call +func (_e *Query_Expecter) ToSql() *Query_ToSql_Call { + return &Query_ToSql_Call{Call: _e.mock.On("ToSql")} +} + +func (_c *Query_ToSql_Call) Run(run func()) *Query_ToSql_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Query_ToSql_Call) Return(_a0 orm.ToSql) *Query_ToSql_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_ToSql_Call) RunAndReturn(run func() orm.ToSql) *Query_ToSql_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: column, value +func (_m *Query) Update(column interface{}, value ...interface{}) (*db.Result, error) { + var _ca []interface{} + _ca = append(_ca, column) + _ca = append(_ca, value...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *db.Result + var r1 error + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) (*db.Result, error)); ok { + return rf(column, value...) + } + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *db.Result); ok { + r0 = rf(column, value...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.Result) + } + } + + if rf, ok := ret.Get(1).(func(interface{}, ...interface{}) error); ok { + r1 = rf(column, value...) + } else { + r1 = ret.Error(1) } - return r0 + return r0, r1 } -// Query_Rollback_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rollback' -type Query_Rollback_Call struct { +// Query_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type Query_Update_Call struct { *mock.Call } -// Rollback is a helper method to define mock.On call -func (_e *Query_Expecter) Rollback() *Query_Rollback_Call { - return &Query_Rollback_Call{Call: _e.mock.On("Rollback")} +// Update is a helper method to define mock.On call +// - column interface{} +// - value ...interface{} +func (_e *Query_Expecter) Update(column interface{}, value ...interface{}) *Query_Update_Call { + return &Query_Update_Call{Call: _e.mock.On("Update", + append([]interface{}{column}, value...)...)} } -func (_c *Query_Rollback_Call) Run(run func()) *Query_Rollback_Call { +func (_c *Query_Update_Call) Run(run func(column interface{}, value ...interface{})) *Query_Update_Call { _c.Call.Run(func(args mock.Arguments) { - run() + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(interface{}), variadicArgs...) }) return _c } -func (_c *Query_Rollback_Call) Return(_a0 error) *Query_Rollback_Call { - _c.Call.Return(_a0) +func (_c *Query_Update_Call) Return(_a0 *db.Result, _a1 error) *Query_Update_Call { + _c.Call.Return(_a0, _a1) return _c } -func (_c *Query_Rollback_Call) RunAndReturn(run func() error) *Query_Rollback_Call { +func (_c *Query_Update_Call) RunAndReturn(run func(interface{}, ...interface{}) (*db.Result, error)) *Query_Update_Call { _c.Call.Return(run) return _c } -// Save provides a mock function with given fields: value -func (_m *Query) Save(value interface{}) error { - ret := _m.Called(value) +// UpdateOrCreate provides a mock function with given fields: dest, attributes, values +func (_m *Query) UpdateOrCreate(dest interface{}, attributes interface{}, values interface{}) error { + ret := _m.Called(dest, attributes, values) if len(ret) == 0 { - panic("no return value specified for Save") + panic("no return value specified for UpdateOrCreate") } var r0 error - if rf, ok := ret.Get(0).(func(interface{}) error); ok { - r0 = rf(value) + if rf, ok := ret.Get(0).(func(interface{}, interface{}, interface{}) error); ok { + r0 = rf(dest, attributes, values) } else { r0 = ret.Error(0) } @@ -3131,143 +4701,168 @@ func (_m *Query) Save(value interface{}) error { return r0 } -// Query_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' -type Query_Save_Call struct { +// Query_UpdateOrCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateOrCreate' +type Query_UpdateOrCreate_Call struct { *mock.Call } -// Save is a helper method to define mock.On call -// - value interface{} -func (_e *Query_Expecter) Save(value interface{}) *Query_Save_Call { - return &Query_Save_Call{Call: _e.mock.On("Save", value)} +// UpdateOrCreate is a helper method to define mock.On call +// - dest interface{} +// - attributes interface{} +// - values interface{} +func (_e *Query_Expecter) UpdateOrCreate(dest interface{}, attributes interface{}, values interface{}) *Query_UpdateOrCreate_Call { + return &Query_UpdateOrCreate_Call{Call: _e.mock.On("UpdateOrCreate", dest, attributes, values)} } -func (_c *Query_Save_Call) Run(run func(value interface{})) *Query_Save_Call { +func (_c *Query_UpdateOrCreate_Call) Run(run func(dest interface{}, attributes interface{}, values interface{})) *Query_UpdateOrCreate_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{})) + run(args[0].(interface{}), args[1].(interface{}), args[2].(interface{})) }) return _c } -func (_c *Query_Save_Call) Return(_a0 error) *Query_Save_Call { +func (_c *Query_UpdateOrCreate_Call) Return(_a0 error) *Query_UpdateOrCreate_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Save_Call) RunAndReturn(run func(interface{}) error) *Query_Save_Call { +func (_c *Query_UpdateOrCreate_Call) RunAndReturn(run func(interface{}, interface{}, interface{}) error) *Query_UpdateOrCreate_Call { _c.Call.Return(run) return _c } -// SaveQuietly provides a mock function with given fields: value -func (_m *Query) SaveQuietly(value interface{}) error { - ret := _m.Called(value) +// Where provides a mock function with given fields: query, args +func (_m *Query) Where(query interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for SaveQuietly") + panic("no return value specified for Where") } - var r0 error - if rf, ok := ret.Get(0).(func(interface{}) error); ok { - r0 = rf(value) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) orm.Query); ok { + r0 = rf(query, args...) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } } return r0 } -// Query_SaveQuietly_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveQuietly' -type Query_SaveQuietly_Call struct { +// Query_Where_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Where' +type Query_Where_Call struct { *mock.Call } -// SaveQuietly is a helper method to define mock.On call -// - value interface{} -func (_e *Query_Expecter) SaveQuietly(value interface{}) *Query_SaveQuietly_Call { - return &Query_SaveQuietly_Call{Call: _e.mock.On("SaveQuietly", value)} +// Where is a helper method to define mock.On call +// - query interface{} +// - args ...interface{} +func (_e *Query_Expecter) Where(query interface{}, args ...interface{}) *Query_Where_Call { + return &Query_Where_Call{Call: _e.mock.On("Where", + append([]interface{}{query}, args...)...)} } -func (_c *Query_SaveQuietly_Call) Run(run func(value interface{})) *Query_SaveQuietly_Call { +func (_c *Query_Where_Call) Run(run func(query interface{}, args ...interface{})) *Query_Where_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{})) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(interface{}), variadicArgs...) }) return _c } -func (_c *Query_SaveQuietly_Call) Return(_a0 error) *Query_SaveQuietly_Call { +func (_c *Query_Where_Call) Return(_a0 orm.Query) *Query_Where_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_SaveQuietly_Call) RunAndReturn(run func(interface{}) error) *Query_SaveQuietly_Call { +func (_c *Query_Where_Call) RunAndReturn(run func(interface{}, ...interface{}) orm.Query) *Query_Where_Call { _c.Call.Return(run) return _c } -// Scan provides a mock function with given fields: dest -func (_m *Query) Scan(dest interface{}) error { - ret := _m.Called(dest) +// WhereAll provides a mock function with given fields: columns, args +func (_m *Query) WhereAll(columns []string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, columns) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for Scan") + panic("no return value specified for WhereAll") } - var r0 error - if rf, ok := ret.Get(0).(func(interface{}) error); ok { - r0 = rf(dest) + var r0 orm.Query + if rf, ok := ret.Get(0).(func([]string, ...interface{}) orm.Query); ok { + r0 = rf(columns, args...) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } } return r0 } -// Query_Scan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scan' -type Query_Scan_Call struct { +// Query_WhereAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereAll' +type Query_WhereAll_Call struct { *mock.Call } -// Scan is a helper method to define mock.On call -// - dest interface{} -func (_e *Query_Expecter) Scan(dest interface{}) *Query_Scan_Call { - return &Query_Scan_Call{Call: _e.mock.On("Scan", dest)} +// WhereAll is a helper method to define mock.On call +// - columns []string +// - args ...interface{} +func (_e *Query_Expecter) WhereAll(columns interface{}, args ...interface{}) *Query_WhereAll_Call { + return &Query_WhereAll_Call{Call: _e.mock.On("WhereAll", + append([]interface{}{columns}, args...)...)} } -func (_c *Query_Scan_Call) Run(run func(dest interface{})) *Query_Scan_Call { +func (_c *Query_WhereAll_Call) Run(run func(columns []string, args ...interface{})) *Query_WhereAll_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{})) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].([]string), variadicArgs...) }) return _c } -func (_c *Query_Scan_Call) Return(_a0 error) *Query_Scan_Call { +func (_c *Query_WhereAll_Call) Return(_a0 orm.Query) *Query_WhereAll_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Scan_Call) RunAndReturn(run func(interface{}) error) *Query_Scan_Call { +func (_c *Query_WhereAll_Call) RunAndReturn(run func([]string, ...interface{}) orm.Query) *Query_WhereAll_Call { _c.Call.Return(run) return _c } -// Scopes provides a mock function with given fields: funcs -func (_m *Query) Scopes(funcs ...func(orm.Query) orm.Query) orm.Query { - _va := make([]interface{}, len(funcs)) - for _i := range funcs { - _va[_i] = funcs[_i] - } +// WhereAny provides a mock function with given fields: columns, args +func (_m *Query) WhereAny(columns []string, args ...interface{}) orm.Query { var _ca []interface{} - _ca = append(_ca, _va...) + _ca = append(_ca, columns) + _ca = append(_ca, args...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for Scopes") + panic("no return value specified for WhereAny") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(...func(orm.Query) orm.Query) orm.Query); ok { - r0 = rf(funcs...) + if rf, ok := ret.Get(0).(func([]string, ...interface{}) orm.Query); ok { + r0 = rf(columns, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3277,58 +4872,53 @@ func (_m *Query) Scopes(funcs ...func(orm.Query) orm.Query) orm.Query { return r0 } -// Query_Scopes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scopes' -type Query_Scopes_Call struct { +// Query_WhereAny_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereAny' +type Query_WhereAny_Call struct { *mock.Call } -// Scopes is a helper method to define mock.On call -// - funcs ...func(orm.Query) orm.Query -func (_e *Query_Expecter) Scopes(funcs ...interface{}) *Query_Scopes_Call { - return &Query_Scopes_Call{Call: _e.mock.On("Scopes", - append([]interface{}{}, funcs...)...)} +// WhereAny is a helper method to define mock.On call +// - columns []string +// - args ...interface{} +func (_e *Query_Expecter) WhereAny(columns interface{}, args ...interface{}) *Query_WhereAny_Call { + return &Query_WhereAny_Call{Call: _e.mock.On("WhereAny", + append([]interface{}{columns}, args...)...)} } -func (_c *Query_Scopes_Call) Run(run func(funcs ...func(orm.Query) orm.Query)) *Query_Scopes_Call { +func (_c *Query_WhereAny_Call) Run(run func(columns []string, args ...interface{})) *Query_WhereAny_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]func(orm.Query) orm.Query, len(args)-0) - for i, a := range args[0:] { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { if a != nil { - variadicArgs[i] = a.(func(orm.Query) orm.Query) + variadicArgs[i] = a.(interface{}) } } - run(variadicArgs...) + run(args[0].([]string), variadicArgs...) }) return _c } -func (_c *Query_Scopes_Call) Return(_a0 orm.Query) *Query_Scopes_Call { +func (_c *Query_WhereAny_Call) Return(_a0 orm.Query) *Query_WhereAny_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Scopes_Call) RunAndReturn(run func(...func(orm.Query) orm.Query) orm.Query) *Query_Scopes_Call { +func (_c *Query_WhereAny_Call) RunAndReturn(run func([]string, ...interface{}) orm.Query) *Query_WhereAny_Call { _c.Call.Return(run) return _c } -// Select provides a mock function with given fields: columns -func (_m *Query) Select(columns ...string) orm.Query { - _va := make([]interface{}, len(columns)) - for _i := range columns { - _va[_i] = columns[_i] - } - var _ca []interface{} - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) +// WhereBetween provides a mock function with given fields: column, x, y +func (_m *Query) WhereBetween(column string, x interface{}, y interface{}) orm.Query { + ret := _m.Called(column, x, y) if len(ret) == 0 { - panic("no return value specified for Select") + panic("no return value specified for WhereBetween") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { - r0 = rf(columns...) + if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) orm.Query); ok { + r0 = rf(column, x, y) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3338,55 +4928,50 @@ func (_m *Query) Select(columns ...string) orm.Query { return r0 } -// Query_Select_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Select' -type Query_Select_Call struct { +// Query_WhereBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereBetween' +type Query_WhereBetween_Call struct { *mock.Call } -// Select is a helper method to define mock.On call -// - columns ...string -func (_e *Query_Expecter) Select(columns ...interface{}) *Query_Select_Call { - return &Query_Select_Call{Call: _e.mock.On("Select", - append([]interface{}{}, columns...)...)} +// WhereBetween is a helper method to define mock.On call +// - column string +// - x interface{} +// - y interface{} +func (_e *Query_Expecter) WhereBetween(column interface{}, x interface{}, y interface{}) *Query_WhereBetween_Call { + return &Query_WhereBetween_Call{Call: _e.mock.On("WhereBetween", column, x, y)} } -func (_c *Query_Select_Call) Run(run func(columns ...string)) *Query_Select_Call { +func (_c *Query_WhereBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_WhereBetween_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]string, len(args)-0) - for i, a := range args[0:] { - if a != nil { - variadicArgs[i] = a.(string) - } - } - run(variadicArgs...) + run(args[0].(string), args[1].(interface{}), args[2].(interface{})) }) return _c } -func (_c *Query_Select_Call) Return(_a0 orm.Query) *Query_Select_Call { +func (_c *Query_WhereBetween_Call) Return(_a0 orm.Query) *Query_WhereBetween_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Select_Call) RunAndReturn(run func(...string) orm.Query) *Query_Select_Call { +func (_c *Query_WhereBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_WhereBetween_Call { _c.Call.Return(run) return _c } -// SelectRaw provides a mock function with given fields: query, args -func (_m *Query) SelectRaw(query interface{}, args ...interface{}) orm.Query { +// WhereDoesntHave provides a mock function with given fields: relation, args +func (_m *Query) WhereDoesntHave(relation string, args ...interface{}) orm.Query { var _ca []interface{} - _ca = append(_ca, query) + _ca = append(_ca, relation) _ca = append(_ca, args...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for SelectRaw") + panic("no return value specified for WhereDoesntHave") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) orm.Query); ok { - r0 = rf(query, args...) + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3396,20 +4981,20 @@ func (_m *Query) SelectRaw(query interface{}, args ...interface{}) orm.Query { return r0 } -// Query_SelectRaw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectRaw' -type Query_SelectRaw_Call struct { +// Query_WhereDoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereDoesntHave' +type Query_WhereDoesntHave_Call struct { *mock.Call } -// SelectRaw is a helper method to define mock.On call -// - query interface{} +// WhereDoesntHave is a helper method to define mock.On call +// - relation string // - args ...interface{} -func (_e *Query_Expecter) SelectRaw(query interface{}, args ...interface{}) *Query_SelectRaw_Call { - return &Query_SelectRaw_Call{Call: _e.mock.On("SelectRaw", - append([]interface{}{query}, args...)...)} +func (_e *Query_Expecter) WhereDoesntHave(relation interface{}, args ...interface{}) *Query_WhereDoesntHave_Call { + return &Query_WhereDoesntHave_Call{Call: _e.mock.On("WhereDoesntHave", + append([]interface{}{relation}, args...)...)} } -func (_c *Query_SelectRaw_Call) Run(run func(query interface{}, args ...interface{})) *Query_SelectRaw_Call { +func (_c *Query_WhereDoesntHave_Call) Run(run func(relation string, args ...interface{})) *Query_WhereDoesntHave_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]interface{}, len(args)-1) for i, a := range args[1:] { @@ -3417,32 +5002,35 @@ func (_c *Query_SelectRaw_Call) Run(run func(query interface{}, args ...interfac variadicArgs[i] = a.(interface{}) } } - run(args[0].(interface{}), variadicArgs...) + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_SelectRaw_Call) Return(_a0 orm.Query) *Query_SelectRaw_Call { +func (_c *Query_WhereDoesntHave_Call) Return(_a0 orm.Query) *Query_WhereDoesntHave_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_SelectRaw_Call) RunAndReturn(run func(interface{}, ...interface{}) orm.Query) *Query_SelectRaw_Call { +func (_c *Query_WhereDoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_WhereDoesntHave_Call { _c.Call.Return(run) return _c } -// SharedLock provides a mock function with no fields -func (_m *Query) SharedLock() orm.Query { - ret := _m.Called() +// WhereDoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *Query) WhereDoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for SharedLock") + panic("no return value specified for WhereDoesntHaveMorph") } var r0 orm.Query - if rf, ok := ret.Get(0).(func() orm.Query); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3452,94 +5040,116 @@ func (_m *Query) SharedLock() orm.Query { return r0 } -// Query_SharedLock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SharedLock' -type Query_SharedLock_Call struct { +// Query_WhereDoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereDoesntHaveMorph' +type Query_WhereDoesntHaveMorph_Call struct { *mock.Call } -// SharedLock is a helper method to define mock.On call -func (_e *Query_Expecter) SharedLock() *Query_SharedLock_Call { - return &Query_SharedLock_Call{Call: _e.mock.On("SharedLock")} +// WhereDoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *Query_Expecter) WhereDoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *Query_WhereDoesntHaveMorph_Call { + return &Query_WhereDoesntHaveMorph_Call{Call: _e.mock.On("WhereDoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} } -func (_c *Query_SharedLock_Call) Run(run func()) *Query_SharedLock_Call { +func (_c *Query_WhereDoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_WhereDoesntHaveMorph_Call { _c.Call.Run(func(args mock.Arguments) { - run() + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) }) return _c } -func (_c *Query_SharedLock_Call) Return(_a0 orm.Query) *Query_SharedLock_Call { +func (_c *Query_WhereDoesntHaveMorph_Call) Return(_a0 orm.Query) *Query_WhereDoesntHaveMorph_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_SharedLock_Call) RunAndReturn(run func() orm.Query) *Query_SharedLock_Call { +func (_c *Query_WhereDoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_WhereDoesntHaveMorph_Call { _c.Call.Return(run) return _c } -// Sum provides a mock function with given fields: column, dest -func (_m *Query) Sum(column string, dest interface{}) error { - ret := _m.Called(column, dest) +// WhereHas provides a mock function with given fields: relation, args +func (_m *Query) WhereHas(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for Sum") + panic("no return value specified for WhereHas") } - var r0 error - if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { - r0 = rf(column, dest) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } } return r0 } -// Query_Sum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sum' -type Query_Sum_Call struct { +// Query_WhereHas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereHas' +type Query_WhereHas_Call struct { *mock.Call } -// Sum is a helper method to define mock.On call -// - column string -// - dest interface{} -func (_e *Query_Expecter) Sum(column interface{}, dest interface{}) *Query_Sum_Call { - return &Query_Sum_Call{Call: _e.mock.On("Sum", column, dest)} +// WhereHas is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *Query_Expecter) WhereHas(relation interface{}, args ...interface{}) *Query_WhereHas_Call { + return &Query_WhereHas_Call{Call: _e.mock.On("WhereHas", + append([]interface{}{relation}, args...)...)} } -func (_c *Query_Sum_Call) Run(run func(column string, dest interface{})) *Query_Sum_Call { +func (_c *Query_WhereHas_Call) Run(run func(relation string, args ...interface{})) *Query_WhereHas_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{})) + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) }) return _c } -func (_c *Query_Sum_Call) Return(_a0 error) *Query_Sum_Call { +func (_c *Query_WhereHas_Call) Return(_a0 orm.Query) *Query_WhereHas_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Sum_Call) RunAndReturn(run func(string, interface{}) error) *Query_Sum_Call { +func (_c *Query_WhereHas_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_WhereHas_Call { _c.Call.Return(run) return _c } -// Table provides a mock function with given fields: name, args -func (_m *Query) Table(name string, args ...interface{}) orm.Query { +// WhereHasMorph provides a mock function with given fields: relation, types, args +func (_m *Query) WhereHasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { var _ca []interface{} - _ca = append(_ca, name) + _ca = append(_ca, relation, types) _ca = append(_ca, args...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for Table") + panic("no return value specified for WhereHasMorph") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { - r0 = rf(name, args...) + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3549,267 +5159,249 @@ func (_m *Query) Table(name string, args ...interface{}) orm.Query { return r0 } -// Query_Table_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Table' -type Query_Table_Call struct { +// Query_WhereHasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereHasMorph' +type Query_WhereHasMorph_Call struct { *mock.Call } -// Table is a helper method to define mock.On call -// - name string +// WhereHasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} // - args ...interface{} -func (_e *Query_Expecter) Table(name interface{}, args ...interface{}) *Query_Table_Call { - return &Query_Table_Call{Call: _e.mock.On("Table", - append([]interface{}{name}, args...)...)} +func (_e *Query_Expecter) WhereHasMorph(relation interface{}, types interface{}, args ...interface{}) *Query_WhereHasMorph_Call { + return &Query_WhereHasMorph_Call{Call: _e.mock.On("WhereHasMorph", + append([]interface{}{relation, types}, args...)...)} } -func (_c *Query_Table_Call) Run(run func(name string, args ...interface{})) *Query_Table_Call { +func (_c *Query_WhereHasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *Query_WhereHasMorph_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { if a != nil { variadicArgs[i] = a.(interface{}) } } - run(args[0].(string), variadicArgs...) + run(args[0].(string), args[1].([]interface{}), variadicArgs...) }) return _c } -func (_c *Query_Table_Call) Return(_a0 orm.Query) *Query_Table_Call { +func (_c *Query_WhereHasMorph_Call) Return(_a0 orm.Query) *Query_WhereHasMorph_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Table_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_Table_Call { +func (_c *Query_WhereHasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *Query_WhereHasMorph_Call { _c.Call.Return(run) return _c } -// ToRawSql provides a mock function with no fields -func (_m *Query) ToRawSql() orm.ToSql { - ret := _m.Called() +// WhereIn provides a mock function with given fields: column, values +func (_m *Query) WhereIn(column string, values []interface{}) orm.Query { + ret := _m.Called(column, values) if len(ret) == 0 { - panic("no return value specified for ToRawSql") + panic("no return value specified for WhereIn") } - var r0 orm.ToSql - if rf, ok := ret.Get(0).(func() orm.ToSql); ok { - r0 = rf() + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}) orm.Query); ok { + r0 = rf(column, values) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(orm.ToSql) + r0 = ret.Get(0).(orm.Query) } } return r0 } -// Query_ToRawSql_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToRawSql' -type Query_ToRawSql_Call struct { +// Query_WhereIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereIn' +type Query_WhereIn_Call struct { *mock.Call } -// ToRawSql is a helper method to define mock.On call -func (_e *Query_Expecter) ToRawSql() *Query_ToRawSql_Call { - return &Query_ToRawSql_Call{Call: _e.mock.On("ToRawSql")} +// WhereIn is a helper method to define mock.On call +// - column string +// - values []interface{} +func (_e *Query_Expecter) WhereIn(column interface{}, values interface{}) *Query_WhereIn_Call { + return &Query_WhereIn_Call{Call: _e.mock.On("WhereIn", column, values)} } -func (_c *Query_ToRawSql_Call) Run(run func()) *Query_ToRawSql_Call { +func (_c *Query_WhereIn_Call) Run(run func(column string, values []interface{})) *Query_WhereIn_Call { _c.Call.Run(func(args mock.Arguments) { - run() + run(args[0].(string), args[1].([]interface{})) }) return _c } -func (_c *Query_ToRawSql_Call) Return(_a0 orm.ToSql) *Query_ToRawSql_Call { +func (_c *Query_WhereIn_Call) Return(_a0 orm.Query) *Query_WhereIn_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_ToRawSql_Call) RunAndReturn(run func() orm.ToSql) *Query_ToRawSql_Call { +func (_c *Query_WhereIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_WhereIn_Call { _c.Call.Return(run) return _c } -// ToSql provides a mock function with no fields -func (_m *Query) ToSql() orm.ToSql { - ret := _m.Called() +// WhereJsonContains provides a mock function with given fields: column, value +func (_m *Query) WhereJsonContains(column string, value interface{}) orm.Query { + ret := _m.Called(column, value) if len(ret) == 0 { - panic("no return value specified for ToSql") + panic("no return value specified for WhereJsonContains") } - var r0 orm.ToSql - if rf, ok := ret.Get(0).(func() orm.ToSql); ok { - r0 = rf() + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { + r0 = rf(column, value) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(orm.ToSql) + r0 = ret.Get(0).(orm.Query) } } return r0 } -// Query_ToSql_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToSql' -type Query_ToSql_Call struct { +// Query_WhereJsonContains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonContains' +type Query_WhereJsonContains_Call struct { *mock.Call } -// ToSql is a helper method to define mock.On call -func (_e *Query_Expecter) ToSql() *Query_ToSql_Call { - return &Query_ToSql_Call{Call: _e.mock.On("ToSql")} +// WhereJsonContains is a helper method to define mock.On call +// - column string +// - value interface{} +func (_e *Query_Expecter) WhereJsonContains(column interface{}, value interface{}) *Query_WhereJsonContains_Call { + return &Query_WhereJsonContains_Call{Call: _e.mock.On("WhereJsonContains", column, value)} } -func (_c *Query_ToSql_Call) Run(run func()) *Query_ToSql_Call { +func (_c *Query_WhereJsonContains_Call) Run(run func(column string, value interface{})) *Query_WhereJsonContains_Call { _c.Call.Run(func(args mock.Arguments) { - run() + run(args[0].(string), args[1].(interface{})) }) return _c } -func (_c *Query_ToSql_Call) Return(_a0 orm.ToSql) *Query_ToSql_Call { +func (_c *Query_WhereJsonContains_Call) Return(_a0 orm.Query) *Query_WhereJsonContains_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_ToSql_Call) RunAndReturn(run func() orm.ToSql) *Query_ToSql_Call { +func (_c *Query_WhereJsonContains_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_WhereJsonContains_Call { _c.Call.Return(run) return _c } -// Update provides a mock function with given fields: column, value -func (_m *Query) Update(column interface{}, value ...interface{}) (*db.Result, error) { - var _ca []interface{} - _ca = append(_ca, column) - _ca = append(_ca, value...) - ret := _m.Called(_ca...) +// WhereJsonContainsKey provides a mock function with given fields: column +func (_m *Query) WhereJsonContainsKey(column string) orm.Query { + ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for Update") + panic("no return value specified for WhereJsonContainsKey") } - var r0 *db.Result - var r1 error - if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) (*db.Result, error)); ok { - return rf(column, value...) - } - if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *db.Result); ok { - r0 = rf(column, value...) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(column) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*db.Result) + r0 = ret.Get(0).(orm.Query) } } - if rf, ok := ret.Get(1).(func(interface{}, ...interface{}) error); ok { - r1 = rf(column, value...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } -// Query_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type Query_Update_Call struct { +// Query_WhereJsonContainsKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonContainsKey' +type Query_WhereJsonContainsKey_Call struct { *mock.Call } -// Update is a helper method to define mock.On call -// - column interface{} -// - value ...interface{} -func (_e *Query_Expecter) Update(column interface{}, value ...interface{}) *Query_Update_Call { - return &Query_Update_Call{Call: _e.mock.On("Update", - append([]interface{}{column}, value...)...)} +// WhereJsonContainsKey is a helper method to define mock.On call +// - column string +func (_e *Query_Expecter) WhereJsonContainsKey(column interface{}) *Query_WhereJsonContainsKey_Call { + return &Query_WhereJsonContainsKey_Call{Call: _e.mock.On("WhereJsonContainsKey", column)} } -func (_c *Query_Update_Call) Run(run func(column interface{}, value ...interface{})) *Query_Update_Call { +func (_c *Query_WhereJsonContainsKey_Call) Run(run func(column string)) *Query_WhereJsonContainsKey_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(args[0].(interface{}), variadicArgs...) + run(args[0].(string)) }) return _c } -func (_c *Query_Update_Call) Return(_a0 *db.Result, _a1 error) *Query_Update_Call { - _c.Call.Return(_a0, _a1) +func (_c *Query_WhereJsonContainsKey_Call) Return(_a0 orm.Query) *Query_WhereJsonContainsKey_Call { + _c.Call.Return(_a0) return _c } -func (_c *Query_Update_Call) RunAndReturn(run func(interface{}, ...interface{}) (*db.Result, error)) *Query_Update_Call { +func (_c *Query_WhereJsonContainsKey_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereJsonContainsKey_Call { _c.Call.Return(run) return _c } -// UpdateOrCreate provides a mock function with given fields: dest, attributes, values -func (_m *Query) UpdateOrCreate(dest interface{}, attributes interface{}, values interface{}) error { - ret := _m.Called(dest, attributes, values) +// WhereJsonDoesntContain provides a mock function with given fields: column, value +func (_m *Query) WhereJsonDoesntContain(column string, value interface{}) orm.Query { + ret := _m.Called(column, value) if len(ret) == 0 { - panic("no return value specified for UpdateOrCreate") + panic("no return value specified for WhereJsonDoesntContain") } - var r0 error - if rf, ok := ret.Get(0).(func(interface{}, interface{}, interface{}) error); ok { - r0 = rf(dest, attributes, values) + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { + r0 = rf(column, value) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } } return r0 } -// Query_UpdateOrCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateOrCreate' -type Query_UpdateOrCreate_Call struct { +// Query_WhereJsonDoesntContain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonDoesntContain' +type Query_WhereJsonDoesntContain_Call struct { *mock.Call } -// UpdateOrCreate is a helper method to define mock.On call -// - dest interface{} -// - attributes interface{} -// - values interface{} -func (_e *Query_Expecter) UpdateOrCreate(dest interface{}, attributes interface{}, values interface{}) *Query_UpdateOrCreate_Call { - return &Query_UpdateOrCreate_Call{Call: _e.mock.On("UpdateOrCreate", dest, attributes, values)} +// WhereJsonDoesntContain is a helper method to define mock.On call +// - column string +// - value interface{} +func (_e *Query_Expecter) WhereJsonDoesntContain(column interface{}, value interface{}) *Query_WhereJsonDoesntContain_Call { + return &Query_WhereJsonDoesntContain_Call{Call: _e.mock.On("WhereJsonDoesntContain", column, value)} } -func (_c *Query_UpdateOrCreate_Call) Run(run func(dest interface{}, attributes interface{}, values interface{})) *Query_UpdateOrCreate_Call { +func (_c *Query_WhereJsonDoesntContain_Call) Run(run func(column string, value interface{})) *Query_WhereJsonDoesntContain_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{}), args[1].(interface{}), args[2].(interface{})) + run(args[0].(string), args[1].(interface{})) }) return _c } -func (_c *Query_UpdateOrCreate_Call) Return(_a0 error) *Query_UpdateOrCreate_Call { +func (_c *Query_WhereJsonDoesntContain_Call) Return(_a0 orm.Query) *Query_WhereJsonDoesntContain_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_UpdateOrCreate_Call) RunAndReturn(run func(interface{}, interface{}, interface{}) error) *Query_UpdateOrCreate_Call { +func (_c *Query_WhereJsonDoesntContain_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_WhereJsonDoesntContain_Call { _c.Call.Return(run) return _c } -// Where provides a mock function with given fields: query, args -func (_m *Query) Where(query interface{}, args ...interface{}) orm.Query { - var _ca []interface{} - _ca = append(_ca, query) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) +// WhereJsonDoesntContainKey provides a mock function with given fields: column +func (_m *Query) WhereJsonDoesntContainKey(column string) orm.Query { + ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for Where") + panic("no return value specified for WhereJsonDoesntContainKey") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) orm.Query); ok { - r0 = rf(query, args...) + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(column) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3819,56 +5411,45 @@ func (_m *Query) Where(query interface{}, args ...interface{}) orm.Query { return r0 } -// Query_Where_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Where' -type Query_Where_Call struct { +// Query_WhereJsonDoesntContainKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonDoesntContainKey' +type Query_WhereJsonDoesntContainKey_Call struct { *mock.Call } -// Where is a helper method to define mock.On call -// - query interface{} -// - args ...interface{} -func (_e *Query_Expecter) Where(query interface{}, args ...interface{}) *Query_Where_Call { - return &Query_Where_Call{Call: _e.mock.On("Where", - append([]interface{}{query}, args...)...)} +// WhereJsonDoesntContainKey is a helper method to define mock.On call +// - column string +func (_e *Query_Expecter) WhereJsonDoesntContainKey(column interface{}) *Query_WhereJsonDoesntContainKey_Call { + return &Query_WhereJsonDoesntContainKey_Call{Call: _e.mock.On("WhereJsonDoesntContainKey", column)} } -func (_c *Query_Where_Call) Run(run func(query interface{}, args ...interface{})) *Query_Where_Call { +func (_c *Query_WhereJsonDoesntContainKey_Call) Run(run func(column string)) *Query_WhereJsonDoesntContainKey_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(args[0].(interface{}), variadicArgs...) + run(args[0].(string)) }) return _c } -func (_c *Query_Where_Call) Return(_a0 orm.Query) *Query_Where_Call { +func (_c *Query_WhereJsonDoesntContainKey_Call) Return(_a0 orm.Query) *Query_WhereJsonDoesntContainKey_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_Where_Call) RunAndReturn(run func(interface{}, ...interface{}) orm.Query) *Query_Where_Call { +func (_c *Query_WhereJsonDoesntContainKey_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereJsonDoesntContainKey_Call { _c.Call.Return(run) return _c } -// WhereAll provides a mock function with given fields: columns, args -func (_m *Query) WhereAll(columns []string, args ...interface{}) orm.Query { - var _ca []interface{} - _ca = append(_ca, columns) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) +// WhereJsonLength provides a mock function with given fields: column, length +func (_m *Query) WhereJsonLength(column string, length int) orm.Query { + ret := _m.Called(column, length) if len(ret) == 0 { - panic("no return value specified for WhereAll") + panic("no return value specified for WhereJsonLength") } var r0 orm.Query - if rf, ok := ret.Get(0).(func([]string, ...interface{}) orm.Query); ok { - r0 = rf(columns, args...) + if rf, ok := ret.Get(0).(func(string, int) orm.Query); ok { + r0 = rf(column, length) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -3878,51 +5459,44 @@ func (_m *Query) WhereAll(columns []string, args ...interface{}) orm.Query { return r0 } -// Query_WhereAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereAll' -type Query_WhereAll_Call struct { +// Query_WhereJsonLength_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonLength' +type Query_WhereJsonLength_Call struct { *mock.Call } -// WhereAll is a helper method to define mock.On call -// - columns []string -// - args ...interface{} -func (_e *Query_Expecter) WhereAll(columns interface{}, args ...interface{}) *Query_WhereAll_Call { - return &Query_WhereAll_Call{Call: _e.mock.On("WhereAll", - append([]interface{}{columns}, args...)...)} +// WhereJsonLength is a helper method to define mock.On call +// - column string +// - length int +func (_e *Query_Expecter) WhereJsonLength(column interface{}, length interface{}) *Query_WhereJsonLength_Call { + return &Query_WhereJsonLength_Call{Call: _e.mock.On("WhereJsonLength", column, length)} } -func (_c *Query_WhereAll_Call) Run(run func(columns []string, args ...interface{})) *Query_WhereAll_Call { +func (_c *Query_WhereJsonLength_Call) Run(run func(column string, length int)) *Query_WhereJsonLength_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(interface{}) - } - } - run(args[0].([]string), variadicArgs...) + run(args[0].(string), args[1].(int)) }) return _c } -func (_c *Query_WhereAll_Call) Return(_a0 orm.Query) *Query_WhereAll_Call { +func (_c *Query_WhereJsonLength_Call) Return(_a0 orm.Query) *Query_WhereJsonLength_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereAll_Call) RunAndReturn(run func([]string, ...interface{}) orm.Query) *Query_WhereAll_Call { +func (_c *Query_WhereJsonLength_Call) RunAndReturn(run func(string, int) orm.Query) *Query_WhereJsonLength_Call { _c.Call.Return(run) return _c } -// WhereAny provides a mock function with given fields: columns, args -func (_m *Query) WhereAny(columns []string, args ...interface{}) orm.Query { +// WhereNone provides a mock function with given fields: columns, args +func (_m *Query) WhereNone(columns []string, args ...interface{}) orm.Query { var _ca []interface{} _ca = append(_ca, columns) _ca = append(_ca, args...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereAny") + panic("no return value specified for WhereNone") } var r0 orm.Query @@ -3937,20 +5511,20 @@ func (_m *Query) WhereAny(columns []string, args ...interface{}) orm.Query { return r0 } -// Query_WhereAny_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereAny' -type Query_WhereAny_Call struct { +// Query_WhereNone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNone' +type Query_WhereNone_Call struct { *mock.Call } -// WhereAny is a helper method to define mock.On call +// WhereNone is a helper method to define mock.On call // - columns []string // - args ...interface{} -func (_e *Query_Expecter) WhereAny(columns interface{}, args ...interface{}) *Query_WhereAny_Call { - return &Query_WhereAny_Call{Call: _e.mock.On("WhereAny", +func (_e *Query_Expecter) WhereNone(columns interface{}, args ...interface{}) *Query_WhereNone_Call { + return &Query_WhereNone_Call{Call: _e.mock.On("WhereNone", append([]interface{}{columns}, args...)...)} } -func (_c *Query_WhereAny_Call) Run(run func(columns []string, args ...interface{})) *Query_WhereAny_Call { +func (_c *Query_WhereNone_Call) Run(run func(columns []string, args ...interface{})) *Query_WhereNone_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]interface{}, len(args)-1) for i, a := range args[1:] { @@ -3963,22 +5537,22 @@ func (_c *Query_WhereAny_Call) Run(run func(columns []string, args ...interface{ return _c } -func (_c *Query_WhereAny_Call) Return(_a0 orm.Query) *Query_WhereAny_Call { +func (_c *Query_WhereNone_Call) Return(_a0 orm.Query) *Query_WhereNone_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereAny_Call) RunAndReturn(run func([]string, ...interface{}) orm.Query) *Query_WhereAny_Call { +func (_c *Query_WhereNone_Call) RunAndReturn(run func([]string, ...interface{}) orm.Query) *Query_WhereNone_Call { _c.Call.Return(run) return _c } -// WhereBetween provides a mock function with given fields: column, x, y -func (_m *Query) WhereBetween(column string, x interface{}, y interface{}) orm.Query { +// WhereNotBetween provides a mock function with given fields: column, x, y +func (_m *Query) WhereNotBetween(column string, x interface{}, y interface{}) orm.Query { ret := _m.Called(column, x, y) if len(ret) == 0 { - panic("no return value specified for WhereBetween") + panic("no return value specified for WhereNotBetween") } var r0 orm.Query @@ -3993,42 +5567,42 @@ func (_m *Query) WhereBetween(column string, x interface{}, y interface{}) orm.Q return r0 } -// Query_WhereBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereBetween' -type Query_WhereBetween_Call struct { +// Query_WhereNotBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotBetween' +type Query_WhereNotBetween_Call struct { *mock.Call } -// WhereBetween is a helper method to define mock.On call +// WhereNotBetween is a helper method to define mock.On call // - column string // - x interface{} // - y interface{} -func (_e *Query_Expecter) WhereBetween(column interface{}, x interface{}, y interface{}) *Query_WhereBetween_Call { - return &Query_WhereBetween_Call{Call: _e.mock.On("WhereBetween", column, x, y)} +func (_e *Query_Expecter) WhereNotBetween(column interface{}, x interface{}, y interface{}) *Query_WhereNotBetween_Call { + return &Query_WhereNotBetween_Call{Call: _e.mock.On("WhereNotBetween", column, x, y)} } -func (_c *Query_WhereBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_WhereBetween_Call { +func (_c *Query_WhereNotBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_WhereNotBetween_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(interface{}), args[2].(interface{})) }) return _c } -func (_c *Query_WhereBetween_Call) Return(_a0 orm.Query) *Query_WhereBetween_Call { +func (_c *Query_WhereNotBetween_Call) Return(_a0 orm.Query) *Query_WhereNotBetween_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_WhereBetween_Call { +func (_c *Query_WhereNotBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_WhereNotBetween_Call { _c.Call.Return(run) return _c } -// WhereIn provides a mock function with given fields: column, values -func (_m *Query) WhereIn(column string, values []interface{}) orm.Query { +// WhereNotIn provides a mock function with given fields: column, values +func (_m *Query) WhereNotIn(column string, values []interface{}) orm.Query { ret := _m.Called(column, values) if len(ret) == 0 { - panic("no return value specified for WhereIn") + panic("no return value specified for WhereNotIn") } var r0 orm.Query @@ -4043,46 +5617,46 @@ func (_m *Query) WhereIn(column string, values []interface{}) orm.Query { return r0 } -// Query_WhereIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereIn' -type Query_WhereIn_Call struct { +// Query_WhereNotIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotIn' +type Query_WhereNotIn_Call struct { *mock.Call } -// WhereIn is a helper method to define mock.On call +// WhereNotIn is a helper method to define mock.On call // - column string // - values []interface{} -func (_e *Query_Expecter) WhereIn(column interface{}, values interface{}) *Query_WhereIn_Call { - return &Query_WhereIn_Call{Call: _e.mock.On("WhereIn", column, values)} +func (_e *Query_Expecter) WhereNotIn(column interface{}, values interface{}) *Query_WhereNotIn_Call { + return &Query_WhereNotIn_Call{Call: _e.mock.On("WhereNotIn", column, values)} } -func (_c *Query_WhereIn_Call) Run(run func(column string, values []interface{})) *Query_WhereIn_Call { +func (_c *Query_WhereNotIn_Call) Run(run func(column string, values []interface{})) *Query_WhereNotIn_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].([]interface{})) }) return _c } -func (_c *Query_WhereIn_Call) Return(_a0 orm.Query) *Query_WhereIn_Call { +func (_c *Query_WhereNotIn_Call) Return(_a0 orm.Query) *Query_WhereNotIn_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_WhereIn_Call { +func (_c *Query_WhereNotIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_WhereNotIn_Call { _c.Call.Return(run) return _c } -// WhereJsonContains provides a mock function with given fields: column, value -func (_m *Query) WhereJsonContains(column string, value interface{}) orm.Query { - ret := _m.Called(column, value) +// WhereNotNull provides a mock function with given fields: column +func (_m *Query) WhereNotNull(column string) orm.Query { + ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for WhereJsonContains") + panic("no return value specified for WhereNotNull") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { - r0 = rf(column, value) + if rf, ok := ret.Get(0).(func(string) orm.Query); ok { + r0 = rf(column) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4092,41 +5666,40 @@ func (_m *Query) WhereJsonContains(column string, value interface{}) orm.Query { return r0 } -// Query_WhereJsonContains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonContains' -type Query_WhereJsonContains_Call struct { +// Query_WhereNotNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotNull' +type Query_WhereNotNull_Call struct { *mock.Call } -// WhereJsonContains is a helper method to define mock.On call +// WhereNotNull is a helper method to define mock.On call // - column string -// - value interface{} -func (_e *Query_Expecter) WhereJsonContains(column interface{}, value interface{}) *Query_WhereJsonContains_Call { - return &Query_WhereJsonContains_Call{Call: _e.mock.On("WhereJsonContains", column, value)} +func (_e *Query_Expecter) WhereNotNull(column interface{}) *Query_WhereNotNull_Call { + return &Query_WhereNotNull_Call{Call: _e.mock.On("WhereNotNull", column)} } -func (_c *Query_WhereJsonContains_Call) Run(run func(column string, value interface{})) *Query_WhereJsonContains_Call { +func (_c *Query_WhereNotNull_Call) Run(run func(column string)) *Query_WhereNotNull_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{})) + run(args[0].(string)) }) return _c } -func (_c *Query_WhereJsonContains_Call) Return(_a0 orm.Query) *Query_WhereJsonContains_Call { +func (_c *Query_WhereNotNull_Call) Return(_a0 orm.Query) *Query_WhereNotNull_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereJsonContains_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_WhereJsonContains_Call { +func (_c *Query_WhereNotNull_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereNotNull_Call { _c.Call.Return(run) return _c } -// WhereJsonContainsKey provides a mock function with given fields: column -func (_m *Query) WhereJsonContainsKey(column string) orm.Query { +// WhereNull provides a mock function with given fields: column +func (_m *Query) WhereNull(column string) orm.Query { ret := _m.Called(column) if len(ret) == 0 { - panic("no return value specified for WhereJsonContainsKey") + panic("no return value specified for WhereNull") } var r0 orm.Query @@ -4141,45 +5714,47 @@ func (_m *Query) WhereJsonContainsKey(column string) orm.Query { return r0 } -// Query_WhereJsonContainsKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonContainsKey' -type Query_WhereJsonContainsKey_Call struct { +// Query_WhereNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNull' +type Query_WhereNull_Call struct { *mock.Call } -// WhereJsonContainsKey is a helper method to define mock.On call +// WhereNull is a helper method to define mock.On call // - column string -func (_e *Query_Expecter) WhereJsonContainsKey(column interface{}) *Query_WhereJsonContainsKey_Call { - return &Query_WhereJsonContainsKey_Call{Call: _e.mock.On("WhereJsonContainsKey", column)} +func (_e *Query_Expecter) WhereNull(column interface{}) *Query_WhereNull_Call { + return &Query_WhereNull_Call{Call: _e.mock.On("WhereNull", column)} } -func (_c *Query_WhereJsonContainsKey_Call) Run(run func(column string)) *Query_WhereJsonContainsKey_Call { +func (_c *Query_WhereNull_Call) Run(run func(column string)) *Query_WhereNull_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string)) }) return _c } -func (_c *Query_WhereJsonContainsKey_Call) Return(_a0 orm.Query) *Query_WhereJsonContainsKey_Call { +func (_c *Query_WhereNull_Call) Return(_a0 orm.Query) *Query_WhereNull_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereJsonContainsKey_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereJsonContainsKey_Call { +func (_c *Query_WhereNull_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereNull_Call { _c.Call.Return(run) return _c } -// WhereJsonDoesntContain provides a mock function with given fields: column, value -func (_m *Query) WhereJsonDoesntContain(column string, value interface{}) orm.Query { - ret := _m.Called(column, value) +// With provides a mock function with given fields: args +func (_m *Query) With(args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereJsonDoesntContain") + panic("no return value specified for With") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}) orm.Query); ok { - r0 = rf(column, value) + if rf, ok := ret.Get(0).(func(...interface{}) orm.Query); ok { + r0 = rf(args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4189,46 +5764,55 @@ func (_m *Query) WhereJsonDoesntContain(column string, value interface{}) orm.Qu return r0 } -// Query_WhereJsonDoesntContain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonDoesntContain' -type Query_WhereJsonDoesntContain_Call struct { +// Query_With_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'With' +type Query_With_Call struct { *mock.Call } -// WhereJsonDoesntContain is a helper method to define mock.On call -// - column string -// - value interface{} -func (_e *Query_Expecter) WhereJsonDoesntContain(column interface{}, value interface{}) *Query_WhereJsonDoesntContain_Call { - return &Query_WhereJsonDoesntContain_Call{Call: _e.mock.On("WhereJsonDoesntContain", column, value)} +// With is a helper method to define mock.On call +// - args ...interface{} +func (_e *Query_Expecter) With(args ...interface{}) *Query_With_Call { + return &Query_With_Call{Call: _e.mock.On("With", + append([]interface{}{}, args...)...)} } -func (_c *Query_WhereJsonDoesntContain_Call) Run(run func(column string, value interface{})) *Query_WhereJsonDoesntContain_Call { +func (_c *Query_With_Call) Run(run func(args ...interface{})) *Query_With_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{})) + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(variadicArgs...) }) return _c } -func (_c *Query_WhereJsonDoesntContain_Call) Return(_a0 orm.Query) *Query_WhereJsonDoesntContain_Call { +func (_c *Query_With_Call) Return(_a0 orm.Query) *Query_With_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereJsonDoesntContain_Call) RunAndReturn(run func(string, interface{}) orm.Query) *Query_WhereJsonDoesntContain_Call { +func (_c *Query_With_Call) RunAndReturn(run func(...interface{}) orm.Query) *Query_With_Call { _c.Call.Return(run) return _c } -// WhereJsonDoesntContainKey provides a mock function with given fields: column -func (_m *Query) WhereJsonDoesntContainKey(column string) orm.Query { - ret := _m.Called(column) +// WithAggregate provides a mock function with given fields: relation, column, fn, args +func (_m *Query) WithAggregate(relation string, column string, fn string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column, fn) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereJsonDoesntContainKey") + panic("no return value specified for WithAggregate") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(string, string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, fn, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4238,45 +5822,58 @@ func (_m *Query) WhereJsonDoesntContainKey(column string) orm.Query { return r0 } -// Query_WhereJsonDoesntContainKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonDoesntContainKey' -type Query_WhereJsonDoesntContainKey_Call struct { +// Query_WithAggregate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithAggregate' +type Query_WithAggregate_Call struct { *mock.Call } -// WhereJsonDoesntContainKey is a helper method to define mock.On call +// WithAggregate is a helper method to define mock.On call +// - relation string // - column string -func (_e *Query_Expecter) WhereJsonDoesntContainKey(column interface{}) *Query_WhereJsonDoesntContainKey_Call { - return &Query_WhereJsonDoesntContainKey_Call{Call: _e.mock.On("WhereJsonDoesntContainKey", column)} +// - fn string +// - args ...interface{} +func (_e *Query_Expecter) WithAggregate(relation interface{}, column interface{}, fn interface{}, args ...interface{}) *Query_WithAggregate_Call { + return &Query_WithAggregate_Call{Call: _e.mock.On("WithAggregate", + append([]interface{}{relation, column, fn}, args...)...)} } -func (_c *Query_WhereJsonDoesntContainKey_Call) Run(run func(column string)) *Query_WhereJsonDoesntContainKey_Call { +func (_c *Query_WithAggregate_Call) Run(run func(relation string, column string, fn string, args ...interface{})) *Query_WithAggregate_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), args[2].(string), variadicArgs...) }) return _c } -func (_c *Query_WhereJsonDoesntContainKey_Call) Return(_a0 orm.Query) *Query_WhereJsonDoesntContainKey_Call { +func (_c *Query_WithAggregate_Call) Return(_a0 orm.Query) *Query_WithAggregate_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereJsonDoesntContainKey_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereJsonDoesntContainKey_Call { +func (_c *Query_WithAggregate_Call) RunAndReturn(run func(string, string, string, ...interface{}) orm.Query) *Query_WithAggregate_Call { _c.Call.Return(run) return _c } -// WhereJsonLength provides a mock function with given fields: column, length -func (_m *Query) WhereJsonLength(column string, length int) orm.Query { - ret := _m.Called(column, length) +// WithAvg provides a mock function with given fields: relation, column, args +func (_m *Query) WithAvg(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereJsonLength") + panic("no return value specified for WithAvg") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, int) orm.Query); ok { - r0 = rf(column, length) + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4286,49 +5883,56 @@ func (_m *Query) WhereJsonLength(column string, length int) orm.Query { return r0 } -// Query_WhereJsonLength_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereJsonLength' -type Query_WhereJsonLength_Call struct { +// Query_WithAvg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithAvg' +type Query_WithAvg_Call struct { *mock.Call } -// WhereJsonLength is a helper method to define mock.On call +// WithAvg is a helper method to define mock.On call +// - relation string // - column string -// - length int -func (_e *Query_Expecter) WhereJsonLength(column interface{}, length interface{}) *Query_WhereJsonLength_Call { - return &Query_WhereJsonLength_Call{Call: _e.mock.On("WhereJsonLength", column, length)} +// - args ...interface{} +func (_e *Query_Expecter) WithAvg(relation interface{}, column interface{}, args ...interface{}) *Query_WithAvg_Call { + return &Query_WithAvg_Call{Call: _e.mock.On("WithAvg", + append([]interface{}{relation, column}, args...)...)} } -func (_c *Query_WhereJsonLength_Call) Run(run func(column string, length int)) *Query_WhereJsonLength_Call { +func (_c *Query_WithAvg_Call) Run(run func(relation string, column string, args ...interface{})) *Query_WithAvg_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(int)) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) }) return _c } -func (_c *Query_WhereJsonLength_Call) Return(_a0 orm.Query) *Query_WhereJsonLength_Call { +func (_c *Query_WithAvg_Call) Return(_a0 orm.Query) *Query_WithAvg_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereJsonLength_Call) RunAndReturn(run func(string, int) orm.Query) *Query_WhereJsonLength_Call { +func (_c *Query_WithAvg_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *Query_WithAvg_Call { _c.Call.Return(run) return _c } -// WhereNone provides a mock function with given fields: columns, args -func (_m *Query) WhereNone(columns []string, args ...interface{}) orm.Query { +// WithCount provides a mock function with given fields: relations +func (_m *Query) WithCount(relations ...interface{}) orm.Query { var _ca []interface{} - _ca = append(_ca, columns) - _ca = append(_ca, args...) + _ca = append(_ca, relations...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereNone") + panic("no return value specified for WithCount") } var r0 orm.Query - if rf, ok := ret.Get(0).(func([]string, ...interface{}) orm.Query); ok { - r0 = rf(columns, args...) + if rf, ok := ret.Get(0).(func(...interface{}) orm.Query); ok { + r0 = rf(relations...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4338,53 +5942,58 @@ func (_m *Query) WhereNone(columns []string, args ...interface{}) orm.Query { return r0 } -// Query_WhereNone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNone' -type Query_WhereNone_Call struct { +// Query_WithCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithCount' +type Query_WithCount_Call struct { *mock.Call } -// WhereNone is a helper method to define mock.On call -// - columns []string -// - args ...interface{} -func (_e *Query_Expecter) WhereNone(columns interface{}, args ...interface{}) *Query_WhereNone_Call { - return &Query_WhereNone_Call{Call: _e.mock.On("WhereNone", - append([]interface{}{columns}, args...)...)} +// WithCount is a helper method to define mock.On call +// - relations ...interface{} +func (_e *Query_Expecter) WithCount(relations ...interface{}) *Query_WithCount_Call { + return &Query_WithCount_Call{Call: _e.mock.On("WithCount", + append([]interface{}{}, relations...)...)} } -func (_c *Query_WhereNone_Call) Run(run func(columns []string, args ...interface{})) *Query_WhereNone_Call { +func (_c *Query_WithCount_Call) Run(run func(relations ...interface{})) *Query_WithCount_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { if a != nil { variadicArgs[i] = a.(interface{}) } } - run(args[0].([]string), variadicArgs...) + run(variadicArgs...) }) return _c } -func (_c *Query_WhereNone_Call) Return(_a0 orm.Query) *Query_WhereNone_Call { +func (_c *Query_WithCount_Call) Return(_a0 orm.Query) *Query_WithCount_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereNone_Call) RunAndReturn(run func([]string, ...interface{}) orm.Query) *Query_WhereNone_Call { +func (_c *Query_WithCount_Call) RunAndReturn(run func(...interface{}) orm.Query) *Query_WithCount_Call { _c.Call.Return(run) return _c } -// WhereNotBetween provides a mock function with given fields: column, x, y -func (_m *Query) WhereNotBetween(column string, x interface{}, y interface{}) orm.Query { - ret := _m.Called(column, x, y) +// WithExists provides a mock function with given fields: relations +func (_m *Query) WithExists(relations ...string) orm.Query { + _va := make([]interface{}, len(relations)) + for _i := range relations { + _va[_i] = relations[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereNotBetween") + panic("no return value specified for WithExists") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) orm.Query); ok { - r0 = rf(column, x, y) + if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { + r0 = rf(relations...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4394,47 +6003,55 @@ func (_m *Query) WhereNotBetween(column string, x interface{}, y interface{}) or return r0 } -// Query_WhereNotBetween_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotBetween' -type Query_WhereNotBetween_Call struct { +// Query_WithExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithExists' +type Query_WithExists_Call struct { *mock.Call } -// WhereNotBetween is a helper method to define mock.On call -// - column string -// - x interface{} -// - y interface{} -func (_e *Query_Expecter) WhereNotBetween(column interface{}, x interface{}, y interface{}) *Query_WhereNotBetween_Call { - return &Query_WhereNotBetween_Call{Call: _e.mock.On("WhereNotBetween", column, x, y)} +// WithExists is a helper method to define mock.On call +// - relations ...string +func (_e *Query_Expecter) WithExists(relations ...interface{}) *Query_WithExists_Call { + return &Query_WithExists_Call{Call: _e.mock.On("WithExists", + append([]interface{}{}, relations...)...)} } -func (_c *Query_WhereNotBetween_Call) Run(run func(column string, x interface{}, y interface{})) *Query_WhereNotBetween_Call { +func (_c *Query_WithExists_Call) Run(run func(relations ...string)) *Query_WithExists_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(interface{}), args[2].(interface{})) + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) }) return _c } -func (_c *Query_WhereNotBetween_Call) Return(_a0 orm.Query) *Query_WhereNotBetween_Call { +func (_c *Query_WithExists_Call) Return(_a0 orm.Query) *Query_WithExists_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereNotBetween_Call) RunAndReturn(run func(string, interface{}, interface{}) orm.Query) *Query_WhereNotBetween_Call { +func (_c *Query_WithExists_Call) RunAndReturn(run func(...string) orm.Query) *Query_WithExists_Call { _c.Call.Return(run) return _c } -// WhereNotIn provides a mock function with given fields: column, values -func (_m *Query) WhereNotIn(column string, values []interface{}) orm.Query { - ret := _m.Called(column, values) +// WithMax provides a mock function with given fields: relation, column, args +func (_m *Query) WithMax(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereNotIn") + panic("no return value specified for WithMax") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, []interface{}) orm.Query); ok { - r0 = rf(column, values) + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4444,46 +6061,57 @@ func (_m *Query) WhereNotIn(column string, values []interface{}) orm.Query { return r0 } -// Query_WhereNotIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotIn' -type Query_WhereNotIn_Call struct { +// Query_WithMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithMax' +type Query_WithMax_Call struct { *mock.Call } -// WhereNotIn is a helper method to define mock.On call +// WithMax is a helper method to define mock.On call +// - relation string // - column string -// - values []interface{} -func (_e *Query_Expecter) WhereNotIn(column interface{}, values interface{}) *Query_WhereNotIn_Call { - return &Query_WhereNotIn_Call{Call: _e.mock.On("WhereNotIn", column, values)} +// - args ...interface{} +func (_e *Query_Expecter) WithMax(relation interface{}, column interface{}, args ...interface{}) *Query_WithMax_Call { + return &Query_WithMax_Call{Call: _e.mock.On("WithMax", + append([]interface{}{relation, column}, args...)...)} } -func (_c *Query_WhereNotIn_Call) Run(run func(column string, values []interface{})) *Query_WhereNotIn_Call { +func (_c *Query_WithMax_Call) Run(run func(relation string, column string, args ...interface{})) *Query_WithMax_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].([]interface{})) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) }) return _c } -func (_c *Query_WhereNotIn_Call) Return(_a0 orm.Query) *Query_WhereNotIn_Call { +func (_c *Query_WithMax_Call) Return(_a0 orm.Query) *Query_WithMax_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereNotIn_Call) RunAndReturn(run func(string, []interface{}) orm.Query) *Query_WhereNotIn_Call { +func (_c *Query_WithMax_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *Query_WithMax_Call { _c.Call.Return(run) return _c } -// WhereNotNull provides a mock function with given fields: column -func (_m *Query) WhereNotNull(column string) orm.Query { - ret := _m.Called(column) +// WithMin provides a mock function with given fields: relation, column, args +func (_m *Query) WithMin(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereNotNull") + panic("no return value specified for WithMin") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4493,45 +6121,56 @@ func (_m *Query) WhereNotNull(column string) orm.Query { return r0 } -// Query_WhereNotNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNotNull' -type Query_WhereNotNull_Call struct { +// Query_WithMin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithMin' +type Query_WithMin_Call struct { *mock.Call } -// WhereNotNull is a helper method to define mock.On call +// WithMin is a helper method to define mock.On call +// - relation string // - column string -func (_e *Query_Expecter) WhereNotNull(column interface{}) *Query_WhereNotNull_Call { - return &Query_WhereNotNull_Call{Call: _e.mock.On("WhereNotNull", column)} +// - args ...interface{} +func (_e *Query_Expecter) WithMin(relation interface{}, column interface{}, args ...interface{}) *Query_WithMin_Call { + return &Query_WithMin_Call{Call: _e.mock.On("WithMin", + append([]interface{}{relation, column}, args...)...)} } -func (_c *Query_WhereNotNull_Call) Run(run func(column string)) *Query_WhereNotNull_Call { +func (_c *Query_WithMin_Call) Run(run func(relation string, column string, args ...interface{})) *Query_WithMin_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) }) return _c } -func (_c *Query_WhereNotNull_Call) Return(_a0 orm.Query) *Query_WhereNotNull_Call { +func (_c *Query_WithMin_Call) Return(_a0 orm.Query) *Query_WithMin_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereNotNull_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereNotNull_Call { +func (_c *Query_WithMin_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *Query_WithMin_Call { _c.Call.Return(run) return _c } -// WhereNull provides a mock function with given fields: column -func (_m *Query) WhereNull(column string) orm.Query { - ret := _m.Called(column) +// WithOnly provides a mock function with given fields: args +func (_m *Query) WithOnly(args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, args...) + ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for WhereNull") + panic("no return value specified for WithOnly") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string) orm.Query); ok { - r0 = rf(column) + if rf, ok := ret.Get(0).(func(...interface{}) orm.Query); ok { + r0 = rf(args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4541,48 +6180,55 @@ func (_m *Query) WhereNull(column string) orm.Query { return r0 } -// Query_WhereNull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereNull' -type Query_WhereNull_Call struct { +// Query_WithOnly_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithOnly' +type Query_WithOnly_Call struct { *mock.Call } -// WhereNull is a helper method to define mock.On call -// - column string -func (_e *Query_Expecter) WhereNull(column interface{}) *Query_WhereNull_Call { - return &Query_WhereNull_Call{Call: _e.mock.On("WhereNull", column)} +// WithOnly is a helper method to define mock.On call +// - args ...interface{} +func (_e *Query_Expecter) WithOnly(args ...interface{}) *Query_WithOnly_Call { + return &Query_WithOnly_Call{Call: _e.mock.On("WithOnly", + append([]interface{}{}, args...)...)} } -func (_c *Query_WhereNull_Call) Run(run func(column string)) *Query_WhereNull_Call { +func (_c *Query_WithOnly_Call) Run(run func(args ...interface{})) *Query_WithOnly_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(variadicArgs...) }) return _c } -func (_c *Query_WhereNull_Call) Return(_a0 orm.Query) *Query_WhereNull_Call { +func (_c *Query_WithOnly_Call) Return(_a0 orm.Query) *Query_WithOnly_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_WhereNull_Call) RunAndReturn(run func(string) orm.Query) *Query_WhereNull_Call { +func (_c *Query_WithOnly_Call) RunAndReturn(run func(...interface{}) orm.Query) *Query_WithOnly_Call { _c.Call.Return(run) return _c } -// With provides a mock function with given fields: query, args -func (_m *Query) With(query string, args ...interface{}) orm.Query { +// WithSum provides a mock function with given fields: relation, column, args +func (_m *Query) WithSum(relation string, column string, args ...interface{}) orm.Query { var _ca []interface{} - _ca = append(_ca, query) + _ca = append(_ca, relation, column) _ca = append(_ca, args...) ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for With") + panic("no return value specified for WithSum") } var r0 orm.Query - if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { - r0 = rf(query, args...) + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(orm.Query) @@ -4592,38 +6238,39 @@ func (_m *Query) With(query string, args ...interface{}) orm.Query { return r0 } -// Query_With_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'With' -type Query_With_Call struct { +// Query_WithSum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithSum' +type Query_WithSum_Call struct { *mock.Call } -// With is a helper method to define mock.On call -// - query string +// WithSum is a helper method to define mock.On call +// - relation string +// - column string // - args ...interface{} -func (_e *Query_Expecter) With(query interface{}, args ...interface{}) *Query_With_Call { - return &Query_With_Call{Call: _e.mock.On("With", - append([]interface{}{query}, args...)...)} +func (_e *Query_Expecter) WithSum(relation interface{}, column interface{}, args ...interface{}) *Query_WithSum_Call { + return &Query_WithSum_Call{Call: _e.mock.On("WithSum", + append([]interface{}{relation, column}, args...)...)} } -func (_c *Query_With_Call) Run(run func(query string, args ...interface{})) *Query_With_Call { +func (_c *Query_WithSum_Call) Run(run func(relation string, column string, args ...interface{})) *Query_WithSum_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]interface{}, len(args)-1) - for i, a := range args[1:] { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { if a != nil { variadicArgs[i] = a.(interface{}) } } - run(args[0].(string), variadicArgs...) + run(args[0].(string), args[1].(string), variadicArgs...) }) return _c } -func (_c *Query_With_Call) Return(_a0 orm.Query) *Query_With_Call { +func (_c *Query_WithSum_Call) Return(_a0 orm.Query) *Query_WithSum_Call { _c.Call.Return(_a0) return _c } -func (_c *Query_With_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *Query_With_Call { +func (_c *Query_WithSum_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *Query_WithSum_Call { _c.Call.Return(run) return _c } @@ -4675,6 +6322,67 @@ func (_c *Query_WithTrashed_Call) RunAndReturn(run func() orm.Query) *Query_With return _c } +// Without provides a mock function with given fields: relations +func (_m *Query) Without(relations ...string) orm.Query { + _va := make([]interface{}, len(relations)) + for _i := range relations { + _va[_i] = relations[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Without") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { + r0 = rf(relations...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// Query_Without_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Without' +type Query_Without_Call struct { + *mock.Call +} + +// Without is a helper method to define mock.On call +// - relations ...string +func (_e *Query_Expecter) Without(relations ...interface{}) *Query_Without_Call { + return &Query_Without_Call{Call: _e.mock.On("Without", + append([]interface{}{}, relations...)...)} +} + +func (_c *Query_Without_Call) Run(run func(relations ...string)) *Query_Without_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *Query_Without_Call) Return(_a0 orm.Query) *Query_Without_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Query_Without_Call) RunAndReturn(run func(...string) orm.Query) *Query_Without_Call { + _c.Call.Return(run) + return _c +} + // WithoutEvents provides a mock function with no fields func (_m *Query) WithoutEvents() orm.Query { ret := _m.Called() diff --git a/mocks/database/orm/QueryWithRelations.go b/mocks/database/orm/QueryWithRelations.go new file mode 100644 index 000000000..2909fd3ce --- /dev/null +++ b/mocks/database/orm/QueryWithRelations.go @@ -0,0 +1,1406 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// QueryWithRelations is an autogenerated mock type for the QueryWithRelations type +type QueryWithRelations struct { + mock.Mock +} + +type QueryWithRelations_Expecter struct { + mock *mock.Mock +} + +func (_m *QueryWithRelations) EXPECT() *QueryWithRelations_Expecter { + return &QueryWithRelations_Expecter{mock: &_m.Mock} +} + +// DoesntHave provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) DoesntHave(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DoesntHave") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_DoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DoesntHave' +type QueryWithRelations_DoesntHave_Call struct { + *mock.Call +} + +// DoesntHave is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) DoesntHave(relation interface{}, args ...interface{}) *QueryWithRelations_DoesntHave_Call { + return &QueryWithRelations_DoesntHave_Call{Call: _e.mock.On("DoesntHave", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_DoesntHave_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_DoesntHave_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_DoesntHave_Call) Return(_a0 orm.Query) *QueryWithRelations_DoesntHave_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_DoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_DoesntHave_Call { + _c.Call.Return(run) + return _c +} + +// DoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) DoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DoesntHaveMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_DoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DoesntHaveMorph' +type QueryWithRelations_DoesntHaveMorph_Call struct { + *mock.Call +} + +// DoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) DoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_DoesntHaveMorph_Call { + return &QueryWithRelations_DoesntHaveMorph_Call{Call: _e.mock.On("DoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_DoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_DoesntHaveMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_DoesntHaveMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_DoesntHaveMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_DoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_DoesntHaveMorph_Call { + _c.Call.Return(run) + return _c +} + +// Has provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) Has(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Has") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_Has_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Has' +type QueryWithRelations_Has_Call struct { + *mock.Call +} + +// Has is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) Has(relation interface{}, args ...interface{}) *QueryWithRelations_Has_Call { + return &QueryWithRelations_Has_Call{Call: _e.mock.On("Has", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_Has_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_Has_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_Has_Call) Return(_a0 orm.Query) *QueryWithRelations_Has_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_Has_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_Has_Call { + _c.Call.Return(run) + return _c +} + +// HasMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) HasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for HasMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_HasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasMorph' +type QueryWithRelations_HasMorph_Call struct { + *mock.Call +} + +// HasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) HasMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_HasMorph_Call { + return &QueryWithRelations_HasMorph_Call{Call: _e.mock.On("HasMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_HasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_HasMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_HasMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_HasMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_HasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_HasMorph_Call { + _c.Call.Return(run) + return _c +} + +// OrDoesntHave provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) OrDoesntHave(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrDoesntHave") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrDoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrDoesntHave' +type QueryWithRelations_OrDoesntHave_Call struct { + *mock.Call +} + +// OrDoesntHave is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrDoesntHave(relation interface{}, args ...interface{}) *QueryWithRelations_OrDoesntHave_Call { + return &QueryWithRelations_OrDoesntHave_Call{Call: _e.mock.On("OrDoesntHave", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_OrDoesntHave_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_OrDoesntHave_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrDoesntHave_Call) Return(_a0 orm.Query) *QueryWithRelations_OrDoesntHave_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrDoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_OrDoesntHave_Call { + _c.Call.Return(run) + return _c +} + +// OrDoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) OrDoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrDoesntHaveMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrDoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrDoesntHaveMorph' +type QueryWithRelations_OrDoesntHaveMorph_Call struct { + *mock.Call +} + +// OrDoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrDoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_OrDoesntHaveMorph_Call { + return &QueryWithRelations_OrDoesntHaveMorph_Call{Call: _e.mock.On("OrDoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_OrDoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_OrDoesntHaveMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrDoesntHaveMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_OrDoesntHaveMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrDoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_OrDoesntHaveMorph_Call { + _c.Call.Return(run) + return _c +} + +// OrHas provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) OrHas(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrHas") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrHas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrHas' +type QueryWithRelations_OrHas_Call struct { + *mock.Call +} + +// OrHas is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrHas(relation interface{}, args ...interface{}) *QueryWithRelations_OrHas_Call { + return &QueryWithRelations_OrHas_Call{Call: _e.mock.On("OrHas", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_OrHas_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_OrHas_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrHas_Call) Return(_a0 orm.Query) *QueryWithRelations_OrHas_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrHas_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_OrHas_Call { + _c.Call.Return(run) + return _c +} + +// OrHasMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) OrHasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrHasMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrHasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrHasMorph' +type QueryWithRelations_OrHasMorph_Call struct { + *mock.Call +} + +// OrHasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrHasMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_OrHasMorph_Call { + return &QueryWithRelations_OrHasMorph_Call{Call: _e.mock.On("OrHasMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_OrHasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_OrHasMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrHasMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_OrHasMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrHasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_OrHasMorph_Call { + _c.Call.Return(run) + return _c +} + +// OrWhereDoesntHave provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) OrWhereDoesntHave(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrWhereDoesntHave") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrWhereDoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereDoesntHave' +type QueryWithRelations_OrWhereDoesntHave_Call struct { + *mock.Call +} + +// OrWhereDoesntHave is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrWhereDoesntHave(relation interface{}, args ...interface{}) *QueryWithRelations_OrWhereDoesntHave_Call { + return &QueryWithRelations_OrWhereDoesntHave_Call{Call: _e.mock.On("OrWhereDoesntHave", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_OrWhereDoesntHave_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_OrWhereDoesntHave_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrWhereDoesntHave_Call) Return(_a0 orm.Query) *QueryWithRelations_OrWhereDoesntHave_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrWhereDoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_OrWhereDoesntHave_Call { + _c.Call.Return(run) + return _c +} + +// OrWhereDoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) OrWhereDoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrWhereDoesntHaveMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrWhereDoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereDoesntHaveMorph' +type QueryWithRelations_OrWhereDoesntHaveMorph_Call struct { + *mock.Call +} + +// OrWhereDoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrWhereDoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_OrWhereDoesntHaveMorph_Call { + return &QueryWithRelations_OrWhereDoesntHaveMorph_Call{Call: _e.mock.On("OrWhereDoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_OrWhereDoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_OrWhereDoesntHaveMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrWhereDoesntHaveMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_OrWhereDoesntHaveMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrWhereDoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_OrWhereDoesntHaveMorph_Call { + _c.Call.Return(run) + return _c +} + +// OrWhereHas provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) OrWhereHas(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrWhereHas") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrWhereHas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereHas' +type QueryWithRelations_OrWhereHas_Call struct { + *mock.Call +} + +// OrWhereHas is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrWhereHas(relation interface{}, args ...interface{}) *QueryWithRelations_OrWhereHas_Call { + return &QueryWithRelations_OrWhereHas_Call{Call: _e.mock.On("OrWhereHas", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_OrWhereHas_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_OrWhereHas_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrWhereHas_Call) Return(_a0 orm.Query) *QueryWithRelations_OrWhereHas_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrWhereHas_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_OrWhereHas_Call { + _c.Call.Return(run) + return _c +} + +// OrWhereHasMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) OrWhereHasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OrWhereHasMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_OrWhereHasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrWhereHasMorph' +type QueryWithRelations_OrWhereHasMorph_Call struct { + *mock.Call +} + +// OrWhereHasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) OrWhereHasMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_OrWhereHasMorph_Call { + return &QueryWithRelations_OrWhereHasMorph_Call{Call: _e.mock.On("OrWhereHasMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_OrWhereHasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_OrWhereHasMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_OrWhereHasMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_OrWhereHasMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_OrWhereHasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_OrWhereHasMorph_Call { + _c.Call.Return(run) + return _c +} + +// WhereDoesntHave provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) WhereDoesntHave(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WhereDoesntHave") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WhereDoesntHave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereDoesntHave' +type QueryWithRelations_WhereDoesntHave_Call struct { + *mock.Call +} + +// WhereDoesntHave is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WhereDoesntHave(relation interface{}, args ...interface{}) *QueryWithRelations_WhereDoesntHave_Call { + return &QueryWithRelations_WhereDoesntHave_Call{Call: _e.mock.On("WhereDoesntHave", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_WhereDoesntHave_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_WhereDoesntHave_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WhereDoesntHave_Call) Return(_a0 orm.Query) *QueryWithRelations_WhereDoesntHave_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WhereDoesntHave_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_WhereDoesntHave_Call { + _c.Call.Return(run) + return _c +} + +// WhereDoesntHaveMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) WhereDoesntHaveMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WhereDoesntHaveMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WhereDoesntHaveMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereDoesntHaveMorph' +type QueryWithRelations_WhereDoesntHaveMorph_Call struct { + *mock.Call +} + +// WhereDoesntHaveMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WhereDoesntHaveMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_WhereDoesntHaveMorph_Call { + return &QueryWithRelations_WhereDoesntHaveMorph_Call{Call: _e.mock.On("WhereDoesntHaveMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_WhereDoesntHaveMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_WhereDoesntHaveMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WhereDoesntHaveMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_WhereDoesntHaveMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WhereDoesntHaveMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_WhereDoesntHaveMorph_Call { + _c.Call.Return(run) + return _c +} + +// WhereHas provides a mock function with given fields: relation, args +func (_m *QueryWithRelations) WhereHas(relation string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WhereHas") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, ...interface{}) orm.Query); ok { + r0 = rf(relation, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WhereHas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereHas' +type QueryWithRelations_WhereHas_Call struct { + *mock.Call +} + +// WhereHas is a helper method to define mock.On call +// - relation string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WhereHas(relation interface{}, args ...interface{}) *QueryWithRelations_WhereHas_Call { + return &QueryWithRelations_WhereHas_Call{Call: _e.mock.On("WhereHas", + append([]interface{}{relation}, args...)...)} +} + +func (_c *QueryWithRelations_WhereHas_Call) Run(run func(relation string, args ...interface{})) *QueryWithRelations_WhereHas_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WhereHas_Call) Return(_a0 orm.Query) *QueryWithRelations_WhereHas_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WhereHas_Call) RunAndReturn(run func(string, ...interface{}) orm.Query) *QueryWithRelations_WhereHas_Call { + _c.Call.Return(run) + return _c +} + +// WhereHasMorph provides a mock function with given fields: relation, types, args +func (_m *QueryWithRelations) WhereHasMorph(relation string, types []interface{}, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, types) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WhereHasMorph") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, []interface{}, ...interface{}) orm.Query); ok { + r0 = rf(relation, types, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WhereHasMorph_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WhereHasMorph' +type QueryWithRelations_WhereHasMorph_Call struct { + *mock.Call +} + +// WhereHasMorph is a helper method to define mock.On call +// - relation string +// - types []interface{} +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WhereHasMorph(relation interface{}, types interface{}, args ...interface{}) *QueryWithRelations_WhereHasMorph_Call { + return &QueryWithRelations_WhereHasMorph_Call{Call: _e.mock.On("WhereHasMorph", + append([]interface{}{relation, types}, args...)...)} +} + +func (_c *QueryWithRelations_WhereHasMorph_Call) Run(run func(relation string, types []interface{}, args ...interface{})) *QueryWithRelations_WhereHasMorph_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].([]interface{}), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WhereHasMorph_Call) Return(_a0 orm.Query) *QueryWithRelations_WhereHasMorph_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WhereHasMorph_Call) RunAndReturn(run func(string, []interface{}, ...interface{}) orm.Query) *QueryWithRelations_WhereHasMorph_Call { + _c.Call.Return(run) + return _c +} + +// WithAggregate provides a mock function with given fields: relation, column, fn, args +func (_m *QueryWithRelations) WithAggregate(relation string, column string, fn string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column, fn) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithAggregate") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, fn, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithAggregate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithAggregate' +type QueryWithRelations_WithAggregate_Call struct { + *mock.Call +} + +// WithAggregate is a helper method to define mock.On call +// - relation string +// - column string +// - fn string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WithAggregate(relation interface{}, column interface{}, fn interface{}, args ...interface{}) *QueryWithRelations_WithAggregate_Call { + return &QueryWithRelations_WithAggregate_Call{Call: _e.mock.On("WithAggregate", + append([]interface{}{relation, column, fn}, args...)...)} +} + +func (_c *QueryWithRelations_WithAggregate_Call) Run(run func(relation string, column string, fn string, args ...interface{})) *QueryWithRelations_WithAggregate_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), args[2].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithAggregate_Call) Return(_a0 orm.Query) *QueryWithRelations_WithAggregate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithAggregate_Call) RunAndReturn(run func(string, string, string, ...interface{}) orm.Query) *QueryWithRelations_WithAggregate_Call { + _c.Call.Return(run) + return _c +} + +// WithAvg provides a mock function with given fields: relation, column, args +func (_m *QueryWithRelations) WithAvg(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithAvg") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithAvg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithAvg' +type QueryWithRelations_WithAvg_Call struct { + *mock.Call +} + +// WithAvg is a helper method to define mock.On call +// - relation string +// - column string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WithAvg(relation interface{}, column interface{}, args ...interface{}) *QueryWithRelations_WithAvg_Call { + return &QueryWithRelations_WithAvg_Call{Call: _e.mock.On("WithAvg", + append([]interface{}{relation, column}, args...)...)} +} + +func (_c *QueryWithRelations_WithAvg_Call) Run(run func(relation string, column string, args ...interface{})) *QueryWithRelations_WithAvg_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithAvg_Call) Return(_a0 orm.Query) *QueryWithRelations_WithAvg_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithAvg_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *QueryWithRelations_WithAvg_Call { + _c.Call.Return(run) + return _c +} + +// WithCount provides a mock function with given fields: relations +func (_m *QueryWithRelations) WithCount(relations ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relations...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithCount") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(...interface{}) orm.Query); ok { + r0 = rf(relations...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithCount' +type QueryWithRelations_WithCount_Call struct { + *mock.Call +} + +// WithCount is a helper method to define mock.On call +// - relations ...interface{} +func (_e *QueryWithRelations_Expecter) WithCount(relations ...interface{}) *QueryWithRelations_WithCount_Call { + return &QueryWithRelations_WithCount_Call{Call: _e.mock.On("WithCount", + append([]interface{}{}, relations...)...)} +} + +func (_c *QueryWithRelations_WithCount_Call) Run(run func(relations ...interface{})) *QueryWithRelations_WithCount_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithCount_Call) Return(_a0 orm.Query) *QueryWithRelations_WithCount_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithCount_Call) RunAndReturn(run func(...interface{}) orm.Query) *QueryWithRelations_WithCount_Call { + _c.Call.Return(run) + return _c +} + +// WithExists provides a mock function with given fields: relations +func (_m *QueryWithRelations) WithExists(relations ...string) orm.Query { + _va := make([]interface{}, len(relations)) + for _i := range relations { + _va[_i] = relations[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithExists") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(...string) orm.Query); ok { + r0 = rf(relations...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithExists' +type QueryWithRelations_WithExists_Call struct { + *mock.Call +} + +// WithExists is a helper method to define mock.On call +// - relations ...string +func (_e *QueryWithRelations_Expecter) WithExists(relations ...interface{}) *QueryWithRelations_WithExists_Call { + return &QueryWithRelations_WithExists_Call{Call: _e.mock.On("WithExists", + append([]interface{}{}, relations...)...)} +} + +func (_c *QueryWithRelations_WithExists_Call) Run(run func(relations ...string)) *QueryWithRelations_WithExists_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithExists_Call) Return(_a0 orm.Query) *QueryWithRelations_WithExists_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithExists_Call) RunAndReturn(run func(...string) orm.Query) *QueryWithRelations_WithExists_Call { + _c.Call.Return(run) + return _c +} + +// WithMax provides a mock function with given fields: relation, column, args +func (_m *QueryWithRelations) WithMax(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithMax") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithMax' +type QueryWithRelations_WithMax_Call struct { + *mock.Call +} + +// WithMax is a helper method to define mock.On call +// - relation string +// - column string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WithMax(relation interface{}, column interface{}, args ...interface{}) *QueryWithRelations_WithMax_Call { + return &QueryWithRelations_WithMax_Call{Call: _e.mock.On("WithMax", + append([]interface{}{relation, column}, args...)...)} +} + +func (_c *QueryWithRelations_WithMax_Call) Run(run func(relation string, column string, args ...interface{})) *QueryWithRelations_WithMax_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithMax_Call) Return(_a0 orm.Query) *QueryWithRelations_WithMax_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithMax_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *QueryWithRelations_WithMax_Call { + _c.Call.Return(run) + return _c +} + +// WithMin provides a mock function with given fields: relation, column, args +func (_m *QueryWithRelations) WithMin(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithMin") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithMin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithMin' +type QueryWithRelations_WithMin_Call struct { + *mock.Call +} + +// WithMin is a helper method to define mock.On call +// - relation string +// - column string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WithMin(relation interface{}, column interface{}, args ...interface{}) *QueryWithRelations_WithMin_Call { + return &QueryWithRelations_WithMin_Call{Call: _e.mock.On("WithMin", + append([]interface{}{relation, column}, args...)...)} +} + +func (_c *QueryWithRelations_WithMin_Call) Run(run func(relation string, column string, args ...interface{})) *QueryWithRelations_WithMin_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithMin_Call) Return(_a0 orm.Query) *QueryWithRelations_WithMin_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithMin_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *QueryWithRelations_WithMin_Call { + _c.Call.Return(run) + return _c +} + +// WithSum provides a mock function with given fields: relation, column, args +func (_m *QueryWithRelations) WithSum(relation string, column string, args ...interface{}) orm.Query { + var _ca []interface{} + _ca = append(_ca, relation, column) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for WithSum") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) orm.Query); ok { + r0 = rf(relation, column, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// QueryWithRelations_WithSum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithSum' +type QueryWithRelations_WithSum_Call struct { + *mock.Call +} + +// WithSum is a helper method to define mock.On call +// - relation string +// - column string +// - args ...interface{} +func (_e *QueryWithRelations_Expecter) WithSum(relation interface{}, column interface{}, args ...interface{}) *QueryWithRelations_WithSum_Call { + return &QueryWithRelations_WithSum_Call{Call: _e.mock.On("WithSum", + append([]interface{}{relation, column}, args...)...)} +} + +func (_c *QueryWithRelations_WithSum_Call) Run(run func(relation string, column string, args ...interface{})) *QueryWithRelations_WithSum_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *QueryWithRelations_WithSum_Call) Return(_a0 orm.Query) *QueryWithRelations_WithSum_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *QueryWithRelations_WithSum_Call) RunAndReturn(run func(string, string, ...interface{}) orm.Query) *QueryWithRelations_WithSum_Call { + _c.Call.Return(run) + return _c +} + +// NewQueryWithRelations creates a new instance of QueryWithRelations. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewQueryWithRelations(t interface { + mock.TestingT + Cleanup(func()) +}) *QueryWithRelations { + mock := &QueryWithRelations{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/Relation.go b/mocks/database/orm/Relation.go new file mode 100644 index 000000000..6c653951f --- /dev/null +++ b/mocks/database/orm/Relation.go @@ -0,0 +1,80 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// Relation is an autogenerated mock type for the Relation type +type Relation struct { + mock.Mock +} + +type Relation_Expecter struct { + mock *mock.Mock +} + +func (_m *Relation) EXPECT() *Relation_Expecter { + return &Relation_Expecter{mock: &_m.Mock} +} + +// Kind provides a mock function with no fields +func (_m *Relation) Kind() orm.RelationKind { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Kind") + } + + var r0 orm.RelationKind + if rf, ok := ret.Get(0).(func() orm.RelationKind); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(orm.RelationKind) + } + + return r0 +} + +// Relation_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind' +type Relation_Kind_Call struct { + *mock.Call +} + +// Kind is a helper method to define mock.On call +func (_e *Relation_Expecter) Kind() *Relation_Kind_Call { + return &Relation_Kind_Call{Call: _e.mock.On("Kind")} +} + +func (_c *Relation_Kind_Call) Run(run func()) *Relation_Kind_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Relation_Kind_Call) Return(_a0 orm.RelationKind) *Relation_Kind_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Relation_Kind_Call) RunAndReturn(run func() orm.RelationKind) *Relation_Kind_Call { + _c.Call.Return(run) + return _c +} + +// NewRelation creates a new instance of Relation. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRelation(t interface { + mock.TestingT + Cleanup(func()) +}) *Relation { + mock := &Relation{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/RelationCallback.go b/mocks/database/orm/RelationCallback.go new file mode 100644 index 000000000..7d10e6a6f --- /dev/null +++ b/mocks/database/orm/RelationCallback.go @@ -0,0 +1,83 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + orm "github.com/goravel/framework/contracts/database/orm" + mock "github.com/stretchr/testify/mock" +) + +// RelationCallback is an autogenerated mock type for the RelationCallback type +type RelationCallback struct { + mock.Mock +} + +type RelationCallback_Expecter struct { + mock *mock.Mock +} + +func (_m *RelationCallback) EXPECT() *RelationCallback_Expecter { + return &RelationCallback_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: query +func (_m *RelationCallback) Execute(query orm.Query) orm.Query { + ret := _m.Called(query) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 orm.Query + if rf, ok := ret.Get(0).(func(orm.Query) orm.Query); ok { + r0 = rf(query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Query) + } + } + + return r0 +} + +// RelationCallback_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type RelationCallback_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - query orm.Query +func (_e *RelationCallback_Expecter) Execute(query interface{}) *RelationCallback_Execute_Call { + return &RelationCallback_Execute_Call{Call: _e.mock.On("Execute", query)} +} + +func (_c *RelationCallback_Execute_Call) Run(run func(query orm.Query)) *RelationCallback_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(orm.Query)) + }) + return _c +} + +func (_c *RelationCallback_Execute_Call) Return(_a0 orm.Query) *RelationCallback_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationCallback_Execute_Call) RunAndReturn(run func(orm.Query) orm.Query) *RelationCallback_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewRelationCallback creates a new instance of RelationCallback. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRelationCallback(t interface { + mock.TestingT + Cleanup(func()) +}) *RelationCallback { + mock := &RelationCallback{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/database/orm/RelationWriter.go b/mocks/database/orm/RelationWriter.go new file mode 100644 index 000000000..7a12d05ff --- /dev/null +++ b/mocks/database/orm/RelationWriter.go @@ -0,0 +1,1216 @@ +// Code generated by mockery. DO NOT EDIT. + +package orm + +import ( + db "github.com/goravel/framework/contracts/database/db" + mock "github.com/stretchr/testify/mock" +) + +// RelationWriter is an autogenerated mock type for the RelationWriter type +type RelationWriter struct { + mock.Mock +} + +type RelationWriter_Expecter struct { + mock *mock.Mock +} + +func (_m *RelationWriter) EXPECT() *RelationWriter_Expecter { + return &RelationWriter_Expecter{mock: &_m.Mock} +} + +// Associate provides a mock function with given fields: owner +func (_m *RelationWriter) Associate(owner interface{}) error { + ret := _m.Called(owner) + + if len(ret) == 0 { + panic("no return value specified for Associate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(owner) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate' +type RelationWriter_Associate_Call struct { + *mock.Call +} + +// Associate is a helper method to define mock.On call +// - owner interface{} +func (_e *RelationWriter_Expecter) Associate(owner interface{}) *RelationWriter_Associate_Call { + return &RelationWriter_Associate_Call{Call: _e.mock.On("Associate", owner)} +} + +func (_c *RelationWriter_Associate_Call) Run(run func(owner interface{})) *RelationWriter_Associate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_Associate_Call) Return(_a0 error) *RelationWriter_Associate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_Associate_Call) RunAndReturn(run func(interface{}) error) *RelationWriter_Associate_Call { + _c.Call.Return(run) + return _c +} + +// Attach provides a mock function with given fields: ids +func (_m *RelationWriter) Attach(ids []interface{}) error { + ret := _m.Called(ids) + + if len(ret) == 0 { + panic("no return value specified for Attach") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]interface{}) error); ok { + r0 = rf(ids) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_Attach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Attach' +type RelationWriter_Attach_Call struct { + *mock.Call +} + +// Attach is a helper method to define mock.On call +// - ids []interface{} +func (_e *RelationWriter_Expecter) Attach(ids interface{}) *RelationWriter_Attach_Call { + return &RelationWriter_Attach_Call{Call: _e.mock.On("Attach", ids)} +} + +func (_c *RelationWriter_Attach_Call) Run(run func(ids []interface{})) *RelationWriter_Attach_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]interface{})) + }) + return _c +} + +func (_c *RelationWriter_Attach_Call) Return(_a0 error) *RelationWriter_Attach_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_Attach_Call) RunAndReturn(run func([]interface{}) error) *RelationWriter_Attach_Call { + _c.Call.Return(run) + return _c +} + +// AttachWithPivot provides a mock function with given fields: idsWithAttrs +func (_m *RelationWriter) AttachWithPivot(idsWithAttrs map[interface{}]map[string]interface{}) error { + ret := _m.Called(idsWithAttrs) + + if len(ret) == 0 { + panic("no return value specified for AttachWithPivot") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) error); ok { + r0 = rf(idsWithAttrs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_AttachWithPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AttachWithPivot' +type RelationWriter_AttachWithPivot_Call struct { + *mock.Call +} + +// AttachWithPivot is a helper method to define mock.On call +// - idsWithAttrs map[interface{}]map[string]interface{} +func (_e *RelationWriter_Expecter) AttachWithPivot(idsWithAttrs interface{}) *RelationWriter_AttachWithPivot_Call { + return &RelationWriter_AttachWithPivot_Call{Call: _e.mock.On("AttachWithPivot", idsWithAttrs)} +} + +func (_c *RelationWriter_AttachWithPivot_Call) Run(run func(idsWithAttrs map[interface{}]map[string]interface{})) *RelationWriter_AttachWithPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[interface{}]map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_AttachWithPivot_Call) Return(_a0 error) *RelationWriter_AttachWithPivot_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_AttachWithPivot_Call) RunAndReturn(run func(map[interface{}]map[string]interface{}) error) *RelationWriter_AttachWithPivot_Call { + _c.Call.Return(run) + return _c +} + +// Create provides a mock function with given fields: dest +func (_m *RelationWriter) Create(dest interface{}) error { + ret := _m.Called(dest) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type RelationWriter_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - dest interface{} +func (_e *RelationWriter_Expecter) Create(dest interface{}) *RelationWriter_Create_Call { + return &RelationWriter_Create_Call{Call: _e.mock.On("Create", dest)} +} + +func (_c *RelationWriter_Create_Call) Run(run func(dest interface{})) *RelationWriter_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_Create_Call) Return(_a0 error) *RelationWriter_Create_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_Create_Call) RunAndReturn(run func(interface{}) error) *RelationWriter_Create_Call { + _c.Call.Return(run) + return _c +} + +// CreateMany provides a mock function with given fields: dests +func (_m *RelationWriter) CreateMany(dests interface{}) error { + ret := _m.Called(dests) + + if len(ret) == 0 { + panic("no return value specified for CreateMany") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(dests) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_CreateMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMany' +type RelationWriter_CreateMany_Call struct { + *mock.Call +} + +// CreateMany is a helper method to define mock.On call +// - dests interface{} +func (_e *RelationWriter_Expecter) CreateMany(dests interface{}) *RelationWriter_CreateMany_Call { + return &RelationWriter_CreateMany_Call{Call: _e.mock.On("CreateMany", dests)} +} + +func (_c *RelationWriter_CreateMany_Call) Run(run func(dests interface{})) *RelationWriter_CreateMany_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_CreateMany_Call) Return(_a0 error) *RelationWriter_CreateMany_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_CreateMany_Call) RunAndReturn(run func(interface{}) error) *RelationWriter_CreateMany_Call { + _c.Call.Return(run) + return _c +} + +// Detach provides a mock function with given fields: ids +func (_m *RelationWriter) Detach(ids ...interface{}) (int64, error) { + var _ca []interface{} + _ca = append(_ca, ids...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Detach") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(...interface{}) (int64, error)); ok { + return rf(ids...) + } + if rf, ok := ret.Get(0).(func(...interface{}) int64); ok { + r0 = rf(ids...) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(...interface{}) error); ok { + r1 = rf(ids...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_Detach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Detach' +type RelationWriter_Detach_Call struct { + *mock.Call +} + +// Detach is a helper method to define mock.On call +// - ids ...interface{} +func (_e *RelationWriter_Expecter) Detach(ids ...interface{}) *RelationWriter_Detach_Call { + return &RelationWriter_Detach_Call{Call: _e.mock.On("Detach", + append([]interface{}{}, ids...)...)} +} + +func (_c *RelationWriter_Detach_Call) Run(run func(ids ...interface{})) *RelationWriter_Detach_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *RelationWriter_Detach_Call) Return(_a0 int64, _a1 error) *RelationWriter_Detach_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_Detach_Call) RunAndReturn(run func(...interface{}) (int64, error)) *RelationWriter_Detach_Call { + _c.Call.Return(run) + return _c +} + +// Dissociate provides a mock function with no fields +func (_m *RelationWriter) Dissociate() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Dissociate") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate' +type RelationWriter_Dissociate_Call struct { + *mock.Call +} + +// Dissociate is a helper method to define mock.On call +func (_e *RelationWriter_Expecter) Dissociate() *RelationWriter_Dissociate_Call { + return &RelationWriter_Dissociate_Call{Call: _e.mock.On("Dissociate")} +} + +func (_c *RelationWriter_Dissociate_Call) Run(run func()) *RelationWriter_Dissociate_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *RelationWriter_Dissociate_Call) Return(_a0 error) *RelationWriter_Dissociate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_Dissociate_Call) RunAndReturn(run func() error) *RelationWriter_Dissociate_Call { + _c.Call.Return(run) + return _c +} + +// FindOrNew provides a mock function with given fields: id, dest +func (_m *RelationWriter) FindOrNew(id interface{}, dest interface{}) error { + ret := _m.Called(id, dest) + + if len(ret) == 0 { + panic("no return value specified for FindOrNew") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, interface{}) error); ok { + r0 = rf(id, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_FindOrNew_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindOrNew' +type RelationWriter_FindOrNew_Call struct { + *mock.Call +} + +// FindOrNew is a helper method to define mock.On call +// - id interface{} +// - dest interface{} +func (_e *RelationWriter_Expecter) FindOrNew(id interface{}, dest interface{}) *RelationWriter_FindOrNew_Call { + return &RelationWriter_FindOrNew_Call{Call: _e.mock.On("FindOrNew", id, dest)} +} + +func (_c *RelationWriter_FindOrNew_Call) Run(run func(id interface{}, dest interface{})) *RelationWriter_FindOrNew_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_FindOrNew_Call) Return(_a0 error) *RelationWriter_FindOrNew_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_FindOrNew_Call) RunAndReturn(run func(interface{}, interface{}) error) *RelationWriter_FindOrNew_Call { + _c.Call.Return(run) + return _c +} + +// FirstOrCreate provides a mock function with given fields: attrs, values, dest +func (_m *RelationWriter) FirstOrCreate(attrs map[string]interface{}, values map[string]interface{}, dest interface{}) error { + ret := _m.Called(attrs, values, dest) + + if len(ret) == 0 { + panic("no return value specified for FirstOrCreate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]interface{}, map[string]interface{}, interface{}) error); ok { + r0 = rf(attrs, values, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_FirstOrCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FirstOrCreate' +type RelationWriter_FirstOrCreate_Call struct { + *mock.Call +} + +// FirstOrCreate is a helper method to define mock.On call +// - attrs map[string]interface{} +// - values map[string]interface{} +// - dest interface{} +func (_e *RelationWriter_Expecter) FirstOrCreate(attrs interface{}, values interface{}, dest interface{}) *RelationWriter_FirstOrCreate_Call { + return &RelationWriter_FirstOrCreate_Call{Call: _e.mock.On("FirstOrCreate", attrs, values, dest)} +} + +func (_c *RelationWriter_FirstOrCreate_Call) Run(run func(attrs map[string]interface{}, values map[string]interface{}, dest interface{})) *RelationWriter_FirstOrCreate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{}), args[1].(map[string]interface{}), args[2].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_FirstOrCreate_Call) Return(_a0 error) *RelationWriter_FirstOrCreate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_FirstOrCreate_Call) RunAndReturn(run func(map[string]interface{}, map[string]interface{}, interface{}) error) *RelationWriter_FirstOrCreate_Call { + _c.Call.Return(run) + return _c +} + +// FirstOrNew provides a mock function with given fields: attrs, values, dest +func (_m *RelationWriter) FirstOrNew(attrs map[string]interface{}, values map[string]interface{}, dest interface{}) error { + ret := _m.Called(attrs, values, dest) + + if len(ret) == 0 { + panic("no return value specified for FirstOrNew") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]interface{}, map[string]interface{}, interface{}) error); ok { + r0 = rf(attrs, values, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_FirstOrNew_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FirstOrNew' +type RelationWriter_FirstOrNew_Call struct { + *mock.Call +} + +// FirstOrNew is a helper method to define mock.On call +// - attrs map[string]interface{} +// - values map[string]interface{} +// - dest interface{} +func (_e *RelationWriter_Expecter) FirstOrNew(attrs interface{}, values interface{}, dest interface{}) *RelationWriter_FirstOrNew_Call { + return &RelationWriter_FirstOrNew_Call{Call: _e.mock.On("FirstOrNew", attrs, values, dest)} +} + +func (_c *RelationWriter_FirstOrNew_Call) Run(run func(attrs map[string]interface{}, values map[string]interface{}, dest interface{})) *RelationWriter_FirstOrNew_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{}), args[1].(map[string]interface{}), args[2].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_FirstOrNew_Call) Return(_a0 error) *RelationWriter_FirstOrNew_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_FirstOrNew_Call) RunAndReturn(run func(map[string]interface{}, map[string]interface{}, interface{}) error) *RelationWriter_FirstOrNew_Call { + _c.Call.Return(run) + return _c +} + +// Save provides a mock function with given fields: child +func (_m *RelationWriter) Save(child interface{}) error { + ret := _m.Called(child) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(child) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' +type RelationWriter_Save_Call struct { + *mock.Call +} + +// Save is a helper method to define mock.On call +// - child interface{} +func (_e *RelationWriter_Expecter) Save(child interface{}) *RelationWriter_Save_Call { + return &RelationWriter_Save_Call{Call: _e.mock.On("Save", child)} +} + +func (_c *RelationWriter_Save_Call) Run(run func(child interface{})) *RelationWriter_Save_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_Save_Call) Return(_a0 error) *RelationWriter_Save_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_Save_Call) RunAndReturn(run func(interface{}) error) *RelationWriter_Save_Call { + _c.Call.Return(run) + return _c +} + +// SaveMany provides a mock function with given fields: children +func (_m *RelationWriter) SaveMany(children interface{}) error { + ret := _m.Called(children) + + if len(ret) == 0 { + panic("no return value specified for SaveMany") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(children) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_SaveMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveMany' +type RelationWriter_SaveMany_Call struct { + *mock.Call +} + +// SaveMany is a helper method to define mock.On call +// - children interface{} +func (_e *RelationWriter_Expecter) SaveMany(children interface{}) *RelationWriter_SaveMany_Call { + return &RelationWriter_SaveMany_Call{Call: _e.mock.On("SaveMany", children)} +} + +func (_c *RelationWriter_SaveMany_Call) Run(run func(children interface{})) *RelationWriter_SaveMany_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_SaveMany_Call) Return(_a0 error) *RelationWriter_SaveMany_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_SaveMany_Call) RunAndReturn(run func(interface{}) error) *RelationWriter_SaveMany_Call { + _c.Call.Return(run) + return _c +} + +// SaveManyWithPivot provides a mock function with given fields: children, attrsPerChild +func (_m *RelationWriter) SaveManyWithPivot(children interface{}, attrsPerChild map[interface{}]map[string]interface{}) error { + ret := _m.Called(children, attrsPerChild) + + if len(ret) == 0 { + panic("no return value specified for SaveManyWithPivot") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, map[interface{}]map[string]interface{}) error); ok { + r0 = rf(children, attrsPerChild) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_SaveManyWithPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveManyWithPivot' +type RelationWriter_SaveManyWithPivot_Call struct { + *mock.Call +} + +// SaveManyWithPivot is a helper method to define mock.On call +// - children interface{} +// - attrsPerChild map[interface{}]map[string]interface{} +func (_e *RelationWriter_Expecter) SaveManyWithPivot(children interface{}, attrsPerChild interface{}) *RelationWriter_SaveManyWithPivot_Call { + return &RelationWriter_SaveManyWithPivot_Call{Call: _e.mock.On("SaveManyWithPivot", children, attrsPerChild)} +} + +func (_c *RelationWriter_SaveManyWithPivot_Call) Run(run func(children interface{}, attrsPerChild map[interface{}]map[string]interface{})) *RelationWriter_SaveManyWithPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(map[interface{}]map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_SaveManyWithPivot_Call) Return(_a0 error) *RelationWriter_SaveManyWithPivot_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_SaveManyWithPivot_Call) RunAndReturn(run func(interface{}, map[interface{}]map[string]interface{}) error) *RelationWriter_SaveManyWithPivot_Call { + _c.Call.Return(run) + return _c +} + +// SaveWithPivot provides a mock function with given fields: child, attrs +func (_m *RelationWriter) SaveWithPivot(child interface{}, attrs map[string]interface{}) error { + ret := _m.Called(child, attrs) + + if len(ret) == 0 { + panic("no return value specified for SaveWithPivot") + } + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, map[string]interface{}) error); ok { + r0 = rf(child, attrs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_SaveWithPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveWithPivot' +type RelationWriter_SaveWithPivot_Call struct { + *mock.Call +} + +// SaveWithPivot is a helper method to define mock.On call +// - child interface{} +// - attrs map[string]interface{} +func (_e *RelationWriter_Expecter) SaveWithPivot(child interface{}, attrs interface{}) *RelationWriter_SaveWithPivot_Call { + return &RelationWriter_SaveWithPivot_Call{Call: _e.mock.On("SaveWithPivot", child, attrs)} +} + +func (_c *RelationWriter_SaveWithPivot_Call) Run(run func(child interface{}, attrs map[string]interface{})) *RelationWriter_SaveWithPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_SaveWithPivot_Call) Return(_a0 error) *RelationWriter_SaveWithPivot_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_SaveWithPivot_Call) RunAndReturn(run func(interface{}, map[string]interface{}) error) *RelationWriter_SaveWithPivot_Call { + _c.Call.Return(run) + return _c +} + +// Sync provides a mock function with given fields: ids +func (_m *RelationWriter) Sync(ids []interface{}) (*db.SyncResult, error) { + ret := _m.Called(ids) + + if len(ret) == 0 { + panic("no return value specified for Sync") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func([]interface{}) (*db.SyncResult, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]interface{}) *db.SyncResult); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func([]interface{}) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_Sync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sync' +type RelationWriter_Sync_Call struct { + *mock.Call +} + +// Sync is a helper method to define mock.On call +// - ids []interface{} +func (_e *RelationWriter_Expecter) Sync(ids interface{}) *RelationWriter_Sync_Call { + return &RelationWriter_Sync_Call{Call: _e.mock.On("Sync", ids)} +} + +func (_c *RelationWriter_Sync_Call) Run(run func(ids []interface{})) *RelationWriter_Sync_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]interface{})) + }) + return _c +} + +func (_c *RelationWriter_Sync_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_Sync_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_Sync_Call) RunAndReturn(run func([]interface{}) (*db.SyncResult, error)) *RelationWriter_Sync_Call { + _c.Call.Return(run) + return _c +} + +// SyncWithPivot provides a mock function with given fields: idsWithAttrs +func (_m *RelationWriter) SyncWithPivot(idsWithAttrs map[interface{}]map[string]interface{}) (*db.SyncResult, error) { + ret := _m.Called(idsWithAttrs) + + if len(ret) == 0 { + panic("no return value specified for SyncWithPivot") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) (*db.SyncResult, error)); ok { + return rf(idsWithAttrs) + } + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) *db.SyncResult); ok { + r0 = rf(idsWithAttrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func(map[interface{}]map[string]interface{}) error); ok { + r1 = rf(idsWithAttrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_SyncWithPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SyncWithPivot' +type RelationWriter_SyncWithPivot_Call struct { + *mock.Call +} + +// SyncWithPivot is a helper method to define mock.On call +// - idsWithAttrs map[interface{}]map[string]interface{} +func (_e *RelationWriter_Expecter) SyncWithPivot(idsWithAttrs interface{}) *RelationWriter_SyncWithPivot_Call { + return &RelationWriter_SyncWithPivot_Call{Call: _e.mock.On("SyncWithPivot", idsWithAttrs)} +} + +func (_c *RelationWriter_SyncWithPivot_Call) Run(run func(idsWithAttrs map[interface{}]map[string]interface{})) *RelationWriter_SyncWithPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[interface{}]map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_SyncWithPivot_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_SyncWithPivot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_SyncWithPivot_Call) RunAndReturn(run func(map[interface{}]map[string]interface{}) (*db.SyncResult, error)) *RelationWriter_SyncWithPivot_Call { + _c.Call.Return(run) + return _c +} + +// SyncWithPivotValues provides a mock function with given fields: ids, pivotValues +func (_m *RelationWriter) SyncWithPivotValues(ids []interface{}, pivotValues map[string]interface{}) (*db.SyncResult, error) { + ret := _m.Called(ids, pivotValues) + + if len(ret) == 0 { + panic("no return value specified for SyncWithPivotValues") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func([]interface{}, map[string]interface{}) (*db.SyncResult, error)); ok { + return rf(ids, pivotValues) + } + if rf, ok := ret.Get(0).(func([]interface{}, map[string]interface{}) *db.SyncResult); ok { + r0 = rf(ids, pivotValues) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func([]interface{}, map[string]interface{}) error); ok { + r1 = rf(ids, pivotValues) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_SyncWithPivotValues_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SyncWithPivotValues' +type RelationWriter_SyncWithPivotValues_Call struct { + *mock.Call +} + +// SyncWithPivotValues is a helper method to define mock.On call +// - ids []interface{} +// - pivotValues map[string]interface{} +func (_e *RelationWriter_Expecter) SyncWithPivotValues(ids interface{}, pivotValues interface{}) *RelationWriter_SyncWithPivotValues_Call { + return &RelationWriter_SyncWithPivotValues_Call{Call: _e.mock.On("SyncWithPivotValues", ids, pivotValues)} +} + +func (_c *RelationWriter_SyncWithPivotValues_Call) Run(run func(ids []interface{}, pivotValues map[string]interface{})) *RelationWriter_SyncWithPivotValues_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]interface{}), args[1].(map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_SyncWithPivotValues_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_SyncWithPivotValues_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_SyncWithPivotValues_Call) RunAndReturn(run func([]interface{}, map[string]interface{}) (*db.SyncResult, error)) *RelationWriter_SyncWithPivotValues_Call { + _c.Call.Return(run) + return _c +} + +// SyncWithoutDetaching provides a mock function with given fields: ids +func (_m *RelationWriter) SyncWithoutDetaching(ids []interface{}) (*db.SyncResult, error) { + ret := _m.Called(ids) + + if len(ret) == 0 { + panic("no return value specified for SyncWithoutDetaching") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func([]interface{}) (*db.SyncResult, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]interface{}) *db.SyncResult); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func([]interface{}) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_SyncWithoutDetaching_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SyncWithoutDetaching' +type RelationWriter_SyncWithoutDetaching_Call struct { + *mock.Call +} + +// SyncWithoutDetaching is a helper method to define mock.On call +// - ids []interface{} +func (_e *RelationWriter_Expecter) SyncWithoutDetaching(ids interface{}) *RelationWriter_SyncWithoutDetaching_Call { + return &RelationWriter_SyncWithoutDetaching_Call{Call: _e.mock.On("SyncWithoutDetaching", ids)} +} + +func (_c *RelationWriter_SyncWithoutDetaching_Call) Run(run func(ids []interface{})) *RelationWriter_SyncWithoutDetaching_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]interface{})) + }) + return _c +} + +func (_c *RelationWriter_SyncWithoutDetaching_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_SyncWithoutDetaching_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_SyncWithoutDetaching_Call) RunAndReturn(run func([]interface{}) (*db.SyncResult, error)) *RelationWriter_SyncWithoutDetaching_Call { + _c.Call.Return(run) + return _c +} + +// SyncWithoutDetachingWithPivot provides a mock function with given fields: idsWithAttrs +func (_m *RelationWriter) SyncWithoutDetachingWithPivot(idsWithAttrs map[interface{}]map[string]interface{}) (*db.SyncResult, error) { + ret := _m.Called(idsWithAttrs) + + if len(ret) == 0 { + panic("no return value specified for SyncWithoutDetachingWithPivot") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) (*db.SyncResult, error)); ok { + return rf(idsWithAttrs) + } + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) *db.SyncResult); ok { + r0 = rf(idsWithAttrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func(map[interface{}]map[string]interface{}) error); ok { + r1 = rf(idsWithAttrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_SyncWithoutDetachingWithPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SyncWithoutDetachingWithPivot' +type RelationWriter_SyncWithoutDetachingWithPivot_Call struct { + *mock.Call +} + +// SyncWithoutDetachingWithPivot is a helper method to define mock.On call +// - idsWithAttrs map[interface{}]map[string]interface{} +func (_e *RelationWriter_Expecter) SyncWithoutDetachingWithPivot(idsWithAttrs interface{}) *RelationWriter_SyncWithoutDetachingWithPivot_Call { + return &RelationWriter_SyncWithoutDetachingWithPivot_Call{Call: _e.mock.On("SyncWithoutDetachingWithPivot", idsWithAttrs)} +} + +func (_c *RelationWriter_SyncWithoutDetachingWithPivot_Call) Run(run func(idsWithAttrs map[interface{}]map[string]interface{})) *RelationWriter_SyncWithoutDetachingWithPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[interface{}]map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_SyncWithoutDetachingWithPivot_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_SyncWithoutDetachingWithPivot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_SyncWithoutDetachingWithPivot_Call) RunAndReturn(run func(map[interface{}]map[string]interface{}) (*db.SyncResult, error)) *RelationWriter_SyncWithoutDetachingWithPivot_Call { + _c.Call.Return(run) + return _c +} + +// Toggle provides a mock function with given fields: ids +func (_m *RelationWriter) Toggle(ids []interface{}) (*db.SyncResult, error) { + ret := _m.Called(ids) + + if len(ret) == 0 { + panic("no return value specified for Toggle") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func([]interface{}) (*db.SyncResult, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]interface{}) *db.SyncResult); ok { + r0 = rf(ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func([]interface{}) error); ok { + r1 = rf(ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_Toggle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Toggle' +type RelationWriter_Toggle_Call struct { + *mock.Call +} + +// Toggle is a helper method to define mock.On call +// - ids []interface{} +func (_e *RelationWriter_Expecter) Toggle(ids interface{}) *RelationWriter_Toggle_Call { + return &RelationWriter_Toggle_Call{Call: _e.mock.On("Toggle", ids)} +} + +func (_c *RelationWriter_Toggle_Call) Run(run func(ids []interface{})) *RelationWriter_Toggle_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]interface{})) + }) + return _c +} + +func (_c *RelationWriter_Toggle_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_Toggle_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_Toggle_Call) RunAndReturn(run func([]interface{}) (*db.SyncResult, error)) *RelationWriter_Toggle_Call { + _c.Call.Return(run) + return _c +} + +// ToggleWithPivot provides a mock function with given fields: idsWithAttrs +func (_m *RelationWriter) ToggleWithPivot(idsWithAttrs map[interface{}]map[string]interface{}) (*db.SyncResult, error) { + ret := _m.Called(idsWithAttrs) + + if len(ret) == 0 { + panic("no return value specified for ToggleWithPivot") + } + + var r0 *db.SyncResult + var r1 error + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) (*db.SyncResult, error)); ok { + return rf(idsWithAttrs) + } + if rf, ok := ret.Get(0).(func(map[interface{}]map[string]interface{}) *db.SyncResult); ok { + r0 = rf(idsWithAttrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.SyncResult) + } + } + + if rf, ok := ret.Get(1).(func(map[interface{}]map[string]interface{}) error); ok { + r1 = rf(idsWithAttrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_ToggleWithPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToggleWithPivot' +type RelationWriter_ToggleWithPivot_Call struct { + *mock.Call +} + +// ToggleWithPivot is a helper method to define mock.On call +// - idsWithAttrs map[interface{}]map[string]interface{} +func (_e *RelationWriter_Expecter) ToggleWithPivot(idsWithAttrs interface{}) *RelationWriter_ToggleWithPivot_Call { + return &RelationWriter_ToggleWithPivot_Call{Call: _e.mock.On("ToggleWithPivot", idsWithAttrs)} +} + +func (_c *RelationWriter_ToggleWithPivot_Call) Run(run func(idsWithAttrs map[interface{}]map[string]interface{})) *RelationWriter_ToggleWithPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[interface{}]map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_ToggleWithPivot_Call) Return(_a0 *db.SyncResult, _a1 error) *RelationWriter_ToggleWithPivot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_ToggleWithPivot_Call) RunAndReturn(run func(map[interface{}]map[string]interface{}) (*db.SyncResult, error)) *RelationWriter_ToggleWithPivot_Call { + _c.Call.Return(run) + return _c +} + +// UpdateExistingPivot provides a mock function with given fields: id, attrs +func (_m *RelationWriter) UpdateExistingPivot(id interface{}, attrs map[string]interface{}) (int64, error) { + ret := _m.Called(id, attrs) + + if len(ret) == 0 { + panic("no return value specified for UpdateExistingPivot") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(interface{}, map[string]interface{}) (int64, error)); ok { + return rf(id, attrs) + } + if rf, ok := ret.Get(0).(func(interface{}, map[string]interface{}) int64); ok { + r0 = rf(id, attrs) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(interface{}, map[string]interface{}) error); ok { + r1 = rf(id, attrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RelationWriter_UpdateExistingPivot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateExistingPivot' +type RelationWriter_UpdateExistingPivot_Call struct { + *mock.Call +} + +// UpdateExistingPivot is a helper method to define mock.On call +// - id interface{} +// - attrs map[string]interface{} +func (_e *RelationWriter_Expecter) UpdateExistingPivot(id interface{}, attrs interface{}) *RelationWriter_UpdateExistingPivot_Call { + return &RelationWriter_UpdateExistingPivot_Call{Call: _e.mock.On("UpdateExistingPivot", id, attrs)} +} + +func (_c *RelationWriter_UpdateExistingPivot_Call) Run(run func(id interface{}, attrs map[string]interface{})) *RelationWriter_UpdateExistingPivot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(interface{}), args[1].(map[string]interface{})) + }) + return _c +} + +func (_c *RelationWriter_UpdateExistingPivot_Call) Return(_a0 int64, _a1 error) *RelationWriter_UpdateExistingPivot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RelationWriter_UpdateExistingPivot_Call) RunAndReturn(run func(interface{}, map[string]interface{}) (int64, error)) *RelationWriter_UpdateExistingPivot_Call { + _c.Call.Return(run) + return _c +} + +// UpdateOrCreate provides a mock function with given fields: attrs, values, dest +func (_m *RelationWriter) UpdateOrCreate(attrs map[string]interface{}, values map[string]interface{}, dest interface{}) error { + ret := _m.Called(attrs, values, dest) + + if len(ret) == 0 { + panic("no return value specified for UpdateOrCreate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]interface{}, map[string]interface{}, interface{}) error); ok { + r0 = rf(attrs, values, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RelationWriter_UpdateOrCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateOrCreate' +type RelationWriter_UpdateOrCreate_Call struct { + *mock.Call +} + +// UpdateOrCreate is a helper method to define mock.On call +// - attrs map[string]interface{} +// - values map[string]interface{} +// - dest interface{} +func (_e *RelationWriter_Expecter) UpdateOrCreate(attrs interface{}, values interface{}, dest interface{}) *RelationWriter_UpdateOrCreate_Call { + return &RelationWriter_UpdateOrCreate_Call{Call: _e.mock.On("UpdateOrCreate", attrs, values, dest)} +} + +func (_c *RelationWriter_UpdateOrCreate_Call) Run(run func(attrs map[string]interface{}, values map[string]interface{}, dest interface{})) *RelationWriter_UpdateOrCreate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{}), args[1].(map[string]interface{}), args[2].(interface{})) + }) + return _c +} + +func (_c *RelationWriter_UpdateOrCreate_Call) Return(_a0 error) *RelationWriter_UpdateOrCreate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RelationWriter_UpdateOrCreate_Call) RunAndReturn(run func(map[string]interface{}, map[string]interface{}, interface{}) error) *RelationWriter_UpdateOrCreate_Call { + _c.Call.Return(run) + return _c +} + +// NewRelationWriter creates a new instance of RelationWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRelationWriter(t interface { + mock.TestingT + Cleanup(func()) +}) *RelationWriter { + mock := &RelationWriter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 7c7c51de789e2c66e97726a989c9b820070dc7d8 Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:22:15 +0800 Subject: [PATCH 02/11] feat(orm): add morph map registry for polymorphic class resolution 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 06b7cb857620664c495e27cfd44bfeeda967f23b) --- contracts/database/orm/morph.go | 17 ++ database/orm/morph.go | 35 ++++ database/orm/morphmap/morphmap.go | 148 +++++++++++++++++ database/orm/morphmap/morphmap_test.go | 221 +++++++++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 contracts/database/orm/morph.go create mode 100644 database/orm/morph.go create mode 100644 database/orm/morphmap/morphmap.go create mode 100644 database/orm/morphmap/morphmap_test.go diff --git a/contracts/database/orm/morph.go b/contracts/database/orm/morph.go new file mode 100644 index 000000000..08b15de52 --- /dev/null +++ b/contracts/database/orm/morph.go @@ -0,0 +1,17 @@ +package orm + +// ModelWithMorphClass lets a model override the value written to and matched against polymorphic +// `*_type` columns. The override takes precedence over both the global morph map (registered via +// orm.MorphMap) and GORM's default of using the parent's table name. +// +// A model that wants to be aliased as e.g. "post" in polymorphic relations declares: +// +// func (Post) MorphClass() string { return "post" } +// +// This is the recommended primary mechanism for aliasing morph types because it co-locates the +// alias with the model definition. The global morph map is provided as a fallback for models the +// caller cannot modify (e.g. third-party types) or for teams that prefer a single boot-time +// registration. +type ModelWithMorphClass interface { + MorphClass() string +} diff --git a/database/orm/morph.go b/database/orm/morph.go new file mode 100644 index 000000000..1583bb3c4 --- /dev/null +++ b/database/orm/morph.go @@ -0,0 +1,35 @@ +package orm + +import ( + "github.com/goravel/framework/database/orm/morphmap" +) + +// MorphMap registers polymorphic aliases. Each entry maps an alias (the value stored in a +// `*_type` column) to a sample model instance from which the registry derives the underlying Go +// type. +// +// Subsequent calls merge with previously registered entries; later writes win on conflict. Pass +// false in the optional merge argument to replace the registry instead of merging. +// +// orm.MorphMap(map[string]any{ +// "post": &Post{}, +// "video": &Video{}, +// }) +// +// Models that implement orm.ModelWithMorphClass take precedence over the registry. +func MorphMap(entries map[string]any, merge ...bool) { + morphmap.Register(entries, merge...) +} + +// MorphedModel returns a fresh pointer to a new instance of the model registered under alias, or +// nil if no model is registered. Used by the MorphTo loader to allocate the right Go type for a +// row whose `*_type` column contains alias. +func MorphedModel(alias string) any { + return morphmap.Find(alias) +} + +// MorphAlias returns the alias registered for the given model's underlying type. Useful at insert +// time when writing the value of a `*_type` column from a Go value. +func MorphAlias(model any) (string, bool) { + return morphmap.AliasOf(model) +} diff --git a/database/orm/morphmap/morphmap.go b/database/orm/morphmap/morphmap.go new file mode 100644 index 000000000..ce732f263 --- /dev/null +++ b/database/orm/morphmap/morphmap.go @@ -0,0 +1,148 @@ +// Package morphmap holds the process-wide registry of polymorphic aliases shared by the orm +// and gorm packages. It mirrors fedaco's static Relation._morphMap (libs/fedaco/src/fedaco/ +// relations/relation.ts:31) while remaining safe for concurrent reads from many query goroutines. +// +// Most app code interacts with the registry via the wrappers in package orm +// (orm.MorphMap, orm.MorphedModel, orm.MorphAlias). The lower-level gorm wrapper imports this +// package directly to resolve morph values during query construction without introducing a +// circular dependency on package orm. +package morphmap + +import ( + "reflect" + "sync" + + contractsorm "github.com/goravel/framework/contracts/database/orm" +) + +var ( + mu sync.RWMutex + aliasToType = map[string]reflect.Type{} + typeToAlias = map[reflect.Type]string{} +) + +// Register stores a set of alias-to-sample-model bindings. Subsequent calls merge with previously +// registered entries; later writes win on conflict. Pass false in the optional merge argument to +// replace the registry instead of merging. +func Register(entries map[string]any, merge ...bool) { + doMerge := true + if len(merge) > 0 { + doMerge = merge[0] + } + mu.Lock() + defer mu.Unlock() + if !doMerge { + aliasToType = map[string]reflect.Type{} + typeToAlias = map[reflect.Type]string{} + } + for alias, sample := range entries { + typ := indirectType(reflect.TypeOf(sample)) + if typ == nil { + continue + } + // Drop any previous bindings on either side so re-registration leaves a single canonical + // mapping in both directions. + if oldType, ok := aliasToType[alias]; ok { + delete(typeToAlias, oldType) + } + if oldAlias, ok := typeToAlias[typ]; ok { + delete(aliasToType, oldAlias) + } + aliasToType[alias] = typ + typeToAlias[typ] = alias + } +} + +// Find returns a fresh pointer to a new instance of the model registered under alias, or nil if +// no model is registered. +func Find(alias string) any { + mu.RLock() + typ, ok := aliasToType[alias] + mu.RUnlock() + if !ok { + return nil + } + return reflect.New(typ).Interface() +} + +// AliasOf returns the alias registered for the given model's underlying type. +func AliasOf(model any) (string, bool) { + typ := indirectType(reflect.TypeOf(model)) + if typ == nil { + return "", false + } + mu.RLock() + defer mu.RUnlock() + alias, ok := typeToAlias[typ] + return alias, ok +} + +// All returns a snapshot copy of the alias-to-type registry. +func All() map[string]reflect.Type { + mu.RLock() + defer mu.RUnlock() + out := make(map[string]reflect.Type, len(aliasToType)) + for alias, typ := range aliasToType { + out[alias] = typ + } + return out +} + +// Reset clears all entries. Intended for tests. +func Reset() { + mu.Lock() + defer mu.Unlock() + aliasToType = map[string]reflect.Type{} + typeToAlias = map[reflect.Type]string{} +} + +// MorphValue resolves the morph alias for a model. Resolution order: +// 1. model.MorphClass() if the model (or its pointer) implements ModelWithMorphClass +// 2. global morph map (registered via Register) +// +// Returns "" and false if neither resolves. The caller is then expected to fall back to GORM's +// `polymorphicValue:` tag or the parent's table name. +func MorphValue(model any) (string, bool) { + if alias, ok := tryMorphClass(model); ok && alias != "" { + return alias, true + } + if alias, ok := AliasOf(model); ok { + return alias, true + } + return "", false +} + +// tryMorphClass invokes MorphClass() on model whether it has a value-receiver method or a +// pointer-receiver method, and whether the caller passed a value or a pointer. +func tryMorphClass(model any) (string, bool) { + if m, ok := model.(contractsorm.ModelWithMorphClass); ok { + return m.MorphClass(), true + } + rv := reflect.ValueOf(model) + switch rv.Kind() { + case reflect.Pointer: + if rv.IsNil() { + return "", false + } + if m, ok := rv.Elem().Interface().(contractsorm.ModelWithMorphClass); ok { + return m.MorphClass(), true + } + case reflect.Struct: + ptr := reflect.New(rv.Type()) + ptr.Elem().Set(rv) + if m, ok := ptr.Interface().(contractsorm.ModelWithMorphClass); ok { + return m.MorphClass(), true + } + } + return "", false +} + +func indirectType(t reflect.Type) reflect.Type { + if t == nil { + return nil + } + if t.Kind() == reflect.Pointer { + return t.Elem() + } + return t +} diff --git a/database/orm/morphmap/morphmap_test.go b/database/orm/morphmap/morphmap_test.go new file mode 100644 index 000000000..db06f3d0b --- /dev/null +++ b/database/orm/morphmap/morphmap_test.go @@ -0,0 +1,221 @@ +package morphmap + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +type morphmapPost struct { + ID uint +} + +type morphmapVideo struct { + ID uint +} + +type morphmapUser struct{} + +func (morphmapUser) MorphClass() string { return "user" } + +type morphmapTag struct{} + +func (*morphmapTag) MorphClass() string { return "tag" } + +type morphmapEmpty struct{} + +func (morphmapEmpty) MorphClass() string { return "" } + +func TestRegister_AndLookup(t *testing.T) { + tests := []struct { + name string + setup func() + alias string + wantType reflect.Type + wantNil bool + modelLook any + wantAlias string + wantOk bool + }{ + { + name: "registered alias yields fresh pointer to model type", + setup: func() { + Reset() + Register(map[string]any{"post": &morphmapPost{}}) + }, + alias: "post", + wantType: reflect.TypeOf(&morphmapPost{}), + modelLook: &morphmapPost{}, + wantAlias: "post", + wantOk: true, + }, + { + name: "value-type sample is normalised to elem type", + setup: func() { + Reset() + Register(map[string]any{"video": morphmapVideo{}}) + }, + alias: "video", + wantType: reflect.TypeOf(&morphmapVideo{}), + modelLook: &morphmapVideo{}, + wantAlias: "video", + wantOk: true, + }, + { + name: "unregistered alias returns nil", + setup: Reset, + alias: "missing", + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + got := Find(tt.alias) + if tt.wantNil { + assert.Nil(t, got) + return + } + assert.Equal(t, tt.wantType, reflect.TypeOf(got)) + alias, ok := AliasOf(tt.modelLook) + assert.Equal(t, tt.wantOk, ok) + assert.Equal(t, tt.wantAlias, alias) + }) + } +} + +func TestRegister_MergeAndReplace(t *testing.T) { + Reset() + Register(map[string]any{"post": &morphmapPost{}}) + Register(map[string]any{"video": &morphmapVideo{}}) // merge + assert.NotNil(t, Find("post")) + assert.NotNil(t, Find("video")) + + Register(map[string]any{"user": &morphmapUser{}}, false) // replace + assert.Nil(t, Find("post")) + assert.Nil(t, Find("video")) + assert.NotNil(t, Find("user")) +} + +func TestRegister_LaterWriteWins(t *testing.T) { + Reset() + Register(map[string]any{"post": &morphmapPost{}}) + Register(map[string]any{"post": &morphmapVideo{}}) // overwrite under same alias + got := Find("post") + assert.Equal(t, reflect.TypeOf(&morphmapVideo{}), reflect.TypeOf(got)) + + // AliasOf should also rebind: Post no longer maps to "post"; Video does. + _, ok := AliasOf(&morphmapPost{}) + assert.False(t, ok) + + alias, ok := AliasOf(&morphmapVideo{}) + assert.True(t, ok) + assert.Equal(t, "post", alias) +} + +func TestRegister_RebindOnConflictingType(t *testing.T) { + // Same type registered under two different aliases — only the later alias survives. + Reset() + Register(map[string]any{"first": &morphmapPost{}}) + Register(map[string]any{"second": &morphmapPost{}}) + + assert.Nil(t, Find("first")) + got := Find("second") + assert.Equal(t, reflect.TypeOf(&morphmapPost{}), reflect.TypeOf(got)) + + alias, ok := AliasOf(&morphmapPost{}) + assert.True(t, ok) + assert.Equal(t, "second", alias) +} + +func TestMorphValue_PriorityOrder(t *testing.T) { + tests := []struct { + name string + setup func() + input any + wantValue string + wantOk bool + }{ + { + name: "MorphClass method takes precedence over registry", + setup: func() { + Reset() + Register(map[string]any{"registered_user": &morphmapUser{}}) + }, + input: &morphmapUser{}, + wantValue: "user", // from MorphClass(), not "registered_user" + wantOk: true, + }, + { + name: "MorphClass works on pointer-receiver method via value caller", + setup: func() { + Reset() + }, + input: morphmapTag{}, // value, but MorphClass has pointer receiver + wantValue: "tag", + wantOk: true, + }, + { + name: "MorphClass works on pointer caller too", + setup: func() { + Reset() + }, + input: &morphmapTag{}, + wantValue: "tag", + wantOk: true, + }, + { + name: "empty MorphClass falls through to registry", + setup: func() { + Reset() + Register(map[string]any{"fallback": &morphmapEmpty{}}) + }, + input: &morphmapEmpty{}, + wantValue: "fallback", + wantOk: true, + }, + { + name: "registry hit when no MorphClass", + setup: func() { + Reset() + Register(map[string]any{"post": &morphmapPost{}}) + }, + input: &morphmapPost{}, + wantValue: "post", + wantOk: true, + }, + { + name: "no MorphClass and no registry entry yields not-found", + setup: Reset, + input: &morphmapPost{}, + wantOk: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + value, ok := MorphValue(tt.input) + assert.Equal(t, tt.wantOk, ok) + assert.Equal(t, tt.wantValue, value) + }) + } +} + +func TestAll_Snapshot(t *testing.T) { + Reset() + Register(map[string]any{ + "post": &morphmapPost{}, + "video": &morphmapVideo{}, + }) + snap := All() + assert.Equal(t, 2, len(snap)) + assert.Equal(t, reflect.TypeOf(morphmapPost{}), snap["post"]) + assert.Equal(t, reflect.TypeOf(morphmapVideo{}), snap["video"]) + + // Mutating the snapshot must not affect the registry. + delete(snap, "post") + assert.NotNil(t, Find("post")) +} From 4513622295795ed1ca00b8adedb5d910a4d7685b Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:23:05 +0800 Subject: [PATCH 03/11] feat(orm): add eager loader, OfMany aggregates, and rewire With/Load 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 65dedf6659705a6132ebb60082232d4c3218319f) --- database/gorm/conditions.go | 39 +- database/gorm/eager_load_parse.go | 215 +++++ database/gorm/eager_load_parse_test.go | 243 +++++ database/gorm/eager_loader.go | 1164 ++++++++++++++++++++++++ database/gorm/eager_loader_test.go | 439 +++++++++ database/gorm/one_of_many.go | 72 ++ database/gorm/one_of_many_test.go | 117 +++ database/gorm/query.go | 89 +- database/gorm/row.go | 19 +- 9 files changed, 2329 insertions(+), 68 deletions(-) create mode 100644 database/gorm/eager_load_parse.go create mode 100644 database/gorm/eager_load_parse_test.go create mode 100644 database/gorm/eager_loader.go create mode 100644 database/gorm/eager_loader_test.go create mode 100644 database/gorm/one_of_many.go create mode 100644 database/gorm/one_of_many_test.go diff --git a/database/gorm/conditions.go b/database/gorm/conditions.go index 3b619a654..99943c8f1 100644 --- a/database/gorm/conditions.go +++ b/database/gorm/conditions.go @@ -19,8 +19,11 @@ type Conditions struct { scopes []func(contractsorm.Query) contractsorm.Query selectColumns []string selectRaw *Select + selectSubs []selectSub where []contractsdriver.Where - with []With + eagerLoad []eagerLoadEntry + relations []relationExistence + oneOfMany *oneOfManyConfig distinct bool lockForUpdate bool sharedLock bool @@ -29,6 +32,14 @@ type Conditions struct { withTrashed bool } +// oneOfManyConfig captures the column + aggregate for OfMany / LatestOfMany / OldestOfMany when +// they are called inside a With() eager-load callback. It's read by runRelatedQuery, which +// rewrites the inner query into an INNER JOIN over a per-parent aggregate subquery. +type oneOfManyConfig struct { + column string + aggregate string // "MAX" | "MIN" | other SQL aggregate +} + type Select struct { query any args []any @@ -39,7 +50,27 @@ type Table struct { args []any } -type With struct { - query string - args []any +// selectSub describes a deferred sub-select aggregate (WithCount / WithMax / etc.). +// The relation is resolved at buildConditions() time, when the parent model is known. +type selectSub struct { + relation string + column string + function string // count | max | min | sum | avg | exists + alias string + callback contractsorm.RelationCallback +} + +// relationExistence describes a deferred relationship existence/absence condition. +// Building is deferred so the parent model can be resolved from conditions.model or conditions.dest +// (the latter is set by Find/First/Get when the user passes a dest). +type relationExistence struct { + relation string + operator string + count int + conjunction string // "and" | "or" + callback contractsorm.RelationCallback + + // morph specifics (zero-valued for non-morph queries) + morphTypes []any + morphCallback contractsorm.MorphRelationCallback } diff --git a/database/gorm/eager_load_parse.go b/database/gorm/eager_load_parse.go new file mode 100644 index 000000000..4d1cf3973 --- /dev/null +++ b/database/gorm/eager_load_parse.go @@ -0,0 +1,215 @@ +package gorm + +import ( + "strings" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +// eagerLoadEntry is the normalised representation of one entry on the eager-load list. It is the +// Go equivalent of fedaco's Record, but uses a slice in the caller +// to preserve insertion order (Go maps don't). +type eagerLoadEntry struct { + relation string // e.g. "Books" or "Books.Author" + columns []string // pruned column list parsed from "Books:id,name"; nil = SELECT * + callback contractsorm.RelationCallback // nil for the synthetic noop entries that _addNestedWiths inserts +} + +// parseEagerLoad normalises the variadic args accepted by Query.With into an ordered +// slice of eagerLoadEntry. Mirrors the union of fedaco's _parseWithRelations, +// _addNestedWiths and _createSelectWithConstraint, expressed as Go runtime type-dispatch since Go +// doesn't have TypeScript-style overloads. +// +// Accepted shapes (any of which may also appear inside a single []any): +// - "Books" +// - "Books:id,name" (column-pruned) +// - "Books.Author" (nested; auto-fills "Books" as a noop entry) +// - "Books", callback (string + callback as the only two args) +// - "Books", "Roles", "Address" (multiple strings) +// - map[string]contractsorm.RelationCallback{...} (relation -> callback) +// - []string{"Books", "Roles"} +// - []any{"Books", map[string]contractsorm.RelationCallback{"Roles": cb}} +func parseEagerLoad(args []any) ([]eagerLoadEntry, error) { + // Special-case the (string, callback) two-arg form so q.With("Books", cb) binds the + // callback to the string rather than treating cb as a freestanding entry. + if len(args) == 2 { + if name, ok := args[0].(string); ok { + if cb, ok := toRelationCallback(args[1]); ok { + return appendEagerLoadEntry(nil, name, cb) + } + } + } + + var out []eagerLoadEntry + for _, arg := range args { + var err error + out, err = mergeEagerLoadArg(out, arg) + if err != nil { + return nil, err + } + } + return out, nil +} + +// mergeEagerLoadArg dispatches a single arg into out, handling all accepted shapes recursively. +func mergeEagerLoadArg(out []eagerLoadEntry, arg any) ([]eagerLoadEntry, error) { + switch v := arg.(type) { + case nil: + return out, nil + case string: + return appendEagerLoadEntry(out, v, nil) + case []string: + var err error + for _, s := range v { + out, err = appendEagerLoadEntry(out, s, nil) + if err != nil { + return nil, err + } + } + return out, nil + case []any: + var err error + for _, item := range v { + out, err = mergeEagerLoadArg(out, item) + if err != nil { + return nil, err + } + } + return out, nil + case map[string]contractsorm.RelationCallback: + return appendEagerLoadMap(out, v) + case map[string]func(contractsorm.Query) contractsorm.Query: + converted := make(map[string]contractsorm.RelationCallback, len(v)) + for k, fn := range v { + converted[k] = contractsorm.RelationCallback(fn) + } + return appendEagerLoadMap(out, converted) + default: + return nil, errors.OrmEagerLoadInvalidArgument.Args(v) + } +} + +func appendEagerLoadMap(out []eagerLoadEntry, m map[string]contractsorm.RelationCallback) ([]eagerLoadEntry, error) { + var err error + for name, cb := range m { + out, err = appendEagerLoadEntry(out, name, cb) + if err != nil { + return nil, err + } + } + return out, nil +} + +// appendEagerLoadEntry adds one (relation, callback) pair to out, applying _addNestedWiths and +// _createSelectWithConstraint semantics: +// - "A.B.C" inserts noop entries for each missing prefix (A, A.B), then a real entry for A.B.C +// - "Books:id,name" splits into name="Books" + columns=[id, name] +// - duplicate relations: the later write replaces the earlier (last-wins) while preserving the +// position of the earlier entry, matching fedaco's overwrite-in-place behaviour for +// Record +func appendEagerLoadEntry(out []eagerLoadEntry, raw string, cb contractsorm.RelationCallback) ([]eagerLoadEntry, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, errors.OrmEagerLoadEmptyRelation + } + + name, columns := splitRelationSelect(raw) + if name == "" { + return nil, errors.OrmEagerLoadEmptyRelation + } + + // Walk dot-segments and ensure every prefix has an entry. Only the leaf carries the real + // callback / columns; intermediate prefixes get a synthetic placeholder. + segments := strings.Split(name, ".") + progress := "" + for i, seg := range segments { + if seg == "" { + return nil, errors.OrmEagerLoadEmptyRelation + } + if i == 0 { + progress = seg + } else { + progress = progress + "." + seg + } + isLeaf := i == len(segments)-1 + entry := eagerLoadEntry{relation: progress} + if isLeaf { + entry.columns = columns + entry.callback = cb + } + out = upsertEagerLoadEntry(out, entry, isLeaf) + } + return out, nil +} + +// upsertEagerLoadEntry inserts entry into out, or — when an entry with the same relation already +// exists — overwrites it in place. The isLeaf flag prevents synthetic prefix placeholders from +// clobbering an existing real entry: if "Books" was already added with a callback, walking +// through "Books.Author" should not erase Books's callback when re-touching the prefix. +func upsertEagerLoadEntry(out []eagerLoadEntry, entry eagerLoadEntry, isLeaf bool) []eagerLoadEntry { + for i, existing := range out { + if existing.relation == entry.relation { + if isLeaf { + out[i] = entry + } + return out + } + } + return append(out, entry) +} + +// splitRelationSelect splits "Books:id,name" into ("Books", ["id", "name"]). A bare relation name +// without a colon yields (name, nil). Whitespace inside the column list is trimmed. +func splitRelationSelect(raw string) (string, []string) { + prefix, rest, ok := strings.Cut(raw, ":") + if !ok { + return raw, nil + } + name := strings.TrimSpace(prefix) + if rest == "" { + return name, nil + } + parts := strings.Split(rest, ",") + cols := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + cols = append(cols, p) + } + } + if len(cols) == 0 { + return name, nil + } + return name, cols +} + +// toRelationCallback accepts the two callable shapes a user is likely to pass and converts to a +// canonical contractsorm.RelationCallback. Returns ok=false for anything else. +func toRelationCallback(v any) (contractsorm.RelationCallback, bool) { + switch fn := v.(type) { + case nil: + return nil, true + case contractsorm.RelationCallback: + return fn, true + case func(contractsorm.Query) contractsorm.Query: + return contractsorm.RelationCallback(fn), true + } + return nil, false +} + +// directNestedEntries returns the entries from list whose relation is strictly nested under +// parent (i.e. starts with "parent."), with the "parent." prefix stripped. Used when recursing +// into a child query: "Books.Author" under parent "Books" becomes "Author". +func directNestedEntries(list []eagerLoadEntry, parent string) []eagerLoadEntry { + prefix := parent + "." + var out []eagerLoadEntry + for _, e := range list { + if strings.HasPrefix(e.relation, prefix) { + child := e + child.relation = strings.TrimPrefix(e.relation, prefix) + out = append(out, child) + } + } + return out +} diff --git a/database/gorm/eager_load_parse_test.go b/database/gorm/eager_load_parse_test.go new file mode 100644 index 000000000..60779d026 --- /dev/null +++ b/database/gorm/eager_load_parse_test.go @@ -0,0 +1,243 @@ +package gorm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +func TestParseEagerLoad(t *testing.T) { + cb := contractsorm.RelationCallback(func(q contractsorm.Query) contractsorm.Query { return q }) + + type expected struct { + relation string + columns []string + callbackSet bool + } + + cases := []struct { + name string + args []any + want []expected + }{ + { + name: "single string", + args: []any{"Books"}, + want: []expected{{relation: "Books"}}, + }, + { + name: "string + callback", + args: []any{"Books", cb}, + want: []expected{{relation: "Books", callbackSet: true}}, + }, + { + name: "multiple strings", + args: []any{"Books", "Roles", "Address"}, + want: []expected{ + {relation: "Books"}, + {relation: "Roles"}, + {relation: "Address"}, + }, + }, + { + name: "column pruning", + args: []any{"Books:id,name"}, + want: []expected{{relation: "Books", columns: []string{"id", "name"}}}, + }, + { + name: "column pruning with whitespace", + args: []any{"Books: id , name "}, + want: []expected{{relation: "Books", columns: []string{"id", "name"}}}, + }, + { + name: "map with callback", + args: []any{map[string]contractsorm.RelationCallback{"Books": cb}}, + want: []expected{{relation: "Books", callbackSet: true}}, + }, + { + name: "map with nil callback", + args: []any{map[string]contractsorm.RelationCallback{"Roles": nil}}, + want: []expected{{relation: "Roles"}}, + }, + { + name: "func map literal", + args: []any{map[string]func(contractsorm.Query) contractsorm.Query{"Roles": func(q contractsorm.Query) contractsorm.Query { return q }}}, + want: []expected{{relation: "Roles", callbackSet: true}}, + }, + { + name: "[]string", + args: []any{[]string{"Books", "Roles"}}, + want: []expected{ + {relation: "Books"}, + {relation: "Roles"}, + }, + }, + { + name: "[]any mix", + args: []any{[]any{"Books", map[string]contractsorm.RelationCallback{"Roles": cb}}}, + want: []expected{ + {relation: "Books"}, + {relation: "Roles", callbackSet: true}, + }, + }, + { + name: "nested dot", + args: []any{"Books.Author"}, + want: []expected{ + {relation: "Books"}, + {relation: "Books.Author"}, + }, + }, + { + name: "nested dot with callback on leaf", + args: []any{"Books.Author", cb}, + want: []expected{ + {relation: "Books"}, + {relation: "Books.Author", callbackSet: true}, + }, + }, + { + name: "duplicate later wins", + args: []any{"Books", map[string]contractsorm.RelationCallback{"Books": cb}}, + want: []expected{{relation: "Books", callbackSet: true}}, + }, + { + name: "synthetic prefix does not clobber leaf already set", + args: []any{"Books", "Books.Author"}, + want: []expected{ + {relation: "Books"}, + {relation: "Books.Author"}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := parseEagerLoad(tc.args) + assert.NoError(t, err) + assert.Len(t, got, len(tc.want), "entry count") + for i := 0; i < len(tc.want) && i < len(got); i++ { + assert.Equal(t, tc.want[i].relation, got[i].relation, "relation[%d]", i) + assert.Equal(t, tc.want[i].columns, got[i].columns, "columns[%d]", i) + assert.Equal(t, tc.want[i].callbackSet, got[i].callback != nil, "callback set[%d]", i) + } + }) + } +} + +func TestParseEagerLoadErrors(t *testing.T) { + t.Run("unsupported type", func(t *testing.T) { + _, err := parseEagerLoad([]any{123}) + assert.True(t, errors.Is(err, errors.OrmEagerLoadInvalidArgument)) + }) + + t.Run("empty string", func(t *testing.T) { + _, err := parseEagerLoad([]any{""}) + assert.True(t, errors.Is(err, errors.OrmEagerLoadEmptyRelation)) + }) + + t.Run("dot path with empty segment", func(t *testing.T) { + _, err := parseEagerLoad([]any{"Books..Author"}) + assert.True(t, errors.Is(err, errors.OrmEagerLoadEmptyRelation)) + }) + + t.Run("nil values are skipped", func(t *testing.T) { + got, err := parseEagerLoad([]any{nil, "Books", nil}) + assert.NoError(t, err) + assert.Len(t, got, 1) + assert.Equal(t, "Books", got[0].relation) + }) +} + +func TestSplitRelationSelect(t *testing.T) { + cases := []struct { + raw string + name string + columns []string + }{ + {"Books", "Books", nil}, + {"Books:id", "Books", []string{"id"}}, + {"Books:id,name", "Books", []string{"id", "name"}}, + {"Books: id , name ", "Books", []string{"id", "name"}}, + {"Books:", "Books", nil}, + {"Books:,,", "Books", nil}, + } + for _, tc := range cases { + t.Run(tc.raw, func(t *testing.T) { + name, cols := splitRelationSelect(tc.raw) + assert.Equal(t, tc.name, name) + assert.Equal(t, tc.columns, cols) + }) + } +} + +func TestDirectNestedEntries(t *testing.T) { + list, err := parseEagerLoad([]any{"Books", "Books.Author", "Books.Reviews", "Roles"}) + assert.NoError(t, err) + + got := directNestedEntries(list, "Books") + assert.Len(t, got, 2) + assert.Equal(t, "Author", got[0].relation) + assert.Equal(t, "Reviews", got[1].relation) +} + +func TestChunkKeys(t *testing.T) { + cases := []struct { + name string + keys []any + size int + want [][]any + }{ + { + name: "size 0 disables chunking", + keys: []any{1, 2, 3}, + size: 0, + want: [][]any{{1, 2, 3}}, + }, + { + name: "negative size disables chunking", + keys: []any{1, 2, 3, 4, 5}, + size: -1, + want: [][]any{{1, 2, 3, 4, 5}}, + }, + { + name: "len <= size returns single chunk", + keys: []any{1, 2, 3}, + size: 5, + want: [][]any{{1, 2, 3}}, + }, + { + name: "exact multiple", + keys: []any{1, 2, 3, 4}, + size: 2, + want: [][]any{{1, 2}, {3, 4}}, + }, + { + name: "non-exact: last chunk shorter", + keys: []any{1, 2, 3, 4, 5}, + size: 2, + want: [][]any{{1, 2}, {3, 4}, {5}}, + }, + { + name: "size 1 yields one item per chunk", + keys: []any{1, 2, 3}, + size: 1, + want: [][]any{{1}, {2}, {3}}, + }, + { + name: "empty input", + keys: []any{}, + size: 10, + want: [][]any{{}}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := chunkKeys(tc.keys, tc.size) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/database/gorm/eager_loader.go b/database/gorm/eager_loader.go new file mode 100644 index 000000000..77b424525 --- /dev/null +++ b/database/gorm/eager_loader.go @@ -0,0 +1,1164 @@ +package gorm + +import ( + "context" + "fmt" + "reflect" + "slices" + "strings" + + "github.com/samber/lo" + gormio "gorm.io/gorm" + gormschema "gorm.io/gorm/schema" + + "github.com/goravel/framework/database/orm/morphmap" + "github.com/goravel/framework/errors" +) + +// defaultEagerLoadChunkSize is the WHERE IN list size at which we split a single eager-load +// query into multiple round-trips. 1000 covers the strictest mainstream limits: +// Oracle's hard cap of 1000 expressions and SQLite's default SQLITE_MAX_VARIABLE_NUMBER of 999. +// PostgreSQL/MySQL/SQL Server have higher limits but their planners also slow down dramatically +// past a few thousand entries, so chunking is a net win even where it isn't strictly required. +// +// The size can be overridden per-app via the `database.eager_load_chunk_size` config key. A value +// <= 0 disables chunking entirely (single IN clause regardless of length). +const defaultEagerLoadChunkSize = 1000 + +// applyEagerLoads runs all queued With entries against the just-loaded dest. It must be +// called by terminal methods (Get / Find / First / FirstOrFail / FirstOr / Cursor) after the main +// query has populated dest, and only when conditions.eagerLoad is non-empty. +func (r *Query) applyEagerLoads(dest any) error { + if len(r.conditions.eagerLoad) == 0 { + return nil + } + parents, err := collectEagerParents(dest) + if err != nil { + return err + } + entries := r.conditions.eagerLoad + r.conditions.eagerLoad = nil + if len(parents) == 0 { + return nil + } + return r.runEagerLoads(parents, entries) +} + +// runEagerLoads iterates the eager-load entries and dispatches each top-level relation to its +// kind-specific loader. Nested entries (those whose name contains a dot) are handled by the +// trickle-down recursion inside each loader, mirroring fedaco's eagerLoadRelations. +func (r *Query) runEagerLoads(parents []reflect.Value, entries []eagerLoadEntry) error { + if len(parents) == 0 || len(entries) == 0 { + return nil + } + parentModel := newSampleModel(parents[0]) + for _, entry := range entries { + if strings.Contains(entry.relation, ".") { + continue + } + nested := directNestedEntries(entries, entry.relation) + if err := r.loadOneRelation(parents, parentModel, entry, nested); err != nil { + return err + } + } + return nil +} + +func (r *Query) loadOneRelation(parents []reflect.Value, parentModel any, entry eagerLoadEntry, nested []eagerLoadEntry) error { + desc, err := resolveRelation(r.instance, parentModel, entry.relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany: + return r.loadHasOneOrMany(parents, parentModel, desc, entry, nested, desc.kind == relKindHasMany) + case relKindBelongsTo: + return r.loadBelongsTo(parents, parentModel, desc, entry, nested) + case relKindMany2Many: + return r.loadMany2Many(parents, parentModel, desc, entry, nested) + case relKindMorphOne, relKindMorphMany: + return r.loadMorph(parents, parentModel, desc, entry, nested, desc.kind == relKindMorphMany) + case relKindMorphTo: + return r.loadMorphTo(parents, parentModel, desc, entry, nested) + case relKindMorphToMany: + return r.loadMorphToMany(parents, parentModel, desc, entry, nested) + case relKindHasOneThrough, relKindHasManyThrough: + return r.loadThrough(parents, parentModel, desc, entry, nested, desc.kind == relKindHasManyThrough) + } + return errors.OrmRelationUnsupported.Args(entry.relation, reflect.TypeOf(parentModel).String(), fmt.Sprintf("kind=%d", desc.kind)) +} + +// --------------------------------------------------------------------------- +// Per-kind loaders +// --------------------------------------------------------------------------- + +func (r *Query) loadHasOneOrMany(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry, isMany bool) error { + if len(desc.references) == 0 { + return errors.OrmRelationUnsupported.Args(entry.relation, "", "no references") + } + ref := desc.references[0] + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + parentField, ok := parentSchema.FieldsByDBName[ref.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+ref.primaryColumn) + } + + keys := extractKeys(r, parents, parentField) + if len(keys) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, isMany, nested) + } + + rows, err := r.runChunkedRelatedQuery(keys, desc, entry, []string{ref.foreignColumn}, func(chunk []any) *gormio.DB { + return r.freshSession().Table(desc.relatedTable).Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.relatedTable), quoteIdent(ref.foreignColumn)), chunk) + }) + if err != nil { + return err + } + + relatedSchema, err := parseGormSchema(r.instance, desc.relatedModel) + if err != nil { + return err + } + fkField, ok := relatedSchema.FieldsByDBName[ref.foreignColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no related FK field for "+ref.foreignColumn) + } + + dict := make(map[string][]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := fkField.ValueOf(r.ctx, row.Elem()) + dict[dictKey(val)] = append(dict[dictKey(val)], row) + } + + if err := r.assignToParents(parents, parentField, entry.relation, dict, isMany); err != nil { + return err + } + return r.recurseNested(rows, nested) +} + +func (r *Query) loadBelongsTo(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry) error { + if len(desc.references) == 0 { + return errors.OrmRelationUnsupported.Args(entry.relation, "", "no references") + } + ref := desc.references[0] + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + // For BelongsTo: ref.foreignTable=parent, ref.foreignColumn=FK on parent; + // ref.primaryTable=related, ref.primaryColumn=PK on related. + fkField, ok := parentSchema.FieldsByDBName[ref.foreignColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent FK field for "+ref.foreignColumn) + } + + keys := extractKeys(r, parents, fkField) + if len(keys) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, false, nested) + } + + rows, err := r.runChunkedRelatedQuery(keys, desc, entry, []string{ref.primaryColumn}, func(chunk []any) *gormio.DB { + return r.freshSession().Table(desc.relatedTable).Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.relatedTable), quoteIdent(ref.primaryColumn)), chunk) + }) + if err != nil { + return err + } + + relatedSchema, err := parseGormSchema(r.instance, desc.relatedModel) + if err != nil { + return err + } + pkField, ok := relatedSchema.FieldsByDBName[ref.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no related PK field for "+ref.primaryColumn) + } + + dict := make(map[string][]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := pkField.ValueOf(r.ctx, row.Elem()) + dict[dictKey(val)] = append(dict[dictKey(val)], row) + } + + if err := r.assignToParents(parents, fkField, entry.relation, dict, false); err != nil { + return err + } + return r.recurseNested(rows, nested) +} + +func (r *Query) loadMorph(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry, isMany bool) error { + if len(desc.references) == 0 { + return errors.OrmRelationUnsupported.Args(entry.relation, "", "no references") + } + ref := desc.references[0] + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + parentField, ok := parentSchema.FieldsByDBName[ref.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+ref.primaryColumn) + } + + keys := extractKeys(r, parents, parentField) + if len(keys) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, isMany, nested) + } + + rows, err := r.runChunkedRelatedQuery(keys, desc, entry, []string{ref.foreignColumn, desc.morphTypeColumn}, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(desc.relatedTable). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.relatedTable), quoteIdent(ref.foreignColumn)), chunk). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.relatedTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + }) + if err != nil { + return err + } + + relatedSchema, err := parseGormSchema(r.instance, desc.relatedModel) + if err != nil { + return err + } + fkField, ok := relatedSchema.FieldsByDBName[ref.foreignColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no related FK field for "+ref.foreignColumn) + } + + dict := make(map[string][]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := fkField.ValueOf(r.ctx, row.Elem()) + dict[dictKey(val)] = append(dict[dictKey(val)], row) + } + + if err := r.assignToParents(parents, parentField, entry.relation, dict, isMany); err != nil { + return err + } + return r.recurseNested(rows, nested) +} + +// loadMorphTo eager-loads the inverse polymorphic relation. Unlike the outbound MorphOne / +// MorphMany loaders, the related Go type is unknown at descriptor build time and discovered per +// row from the value of the *_type column. Parents are bucketed by morph type, and each bucket +// runs an IN query against its resolved table. +func (r *Query) loadMorphTo(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry) error { + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + typeField, ok := parentSchema.FieldsByDBName[desc.morphTypeColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+desc.morphTypeColumn) + } + idField, ok := parentSchema.FieldsByDBName[desc.morphIDColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+desc.morphIDColumn) + } + + // Bucket parents by morph type, deduplicating IDs per bucket. + type bucket struct { + keys []any + seenKeys map[string]struct{} + } + buckets := make(map[string]*bucket) + parentMorphKey := make([]string, len(parents)) // morph type per parent + parentBucketKey := make([]string, len(parents)) // dictKey of id per parent + for i, parent := range parents { + typeVal, typeZero := typeField.ValueOf(r.ctx, parent) + idVal, idZero := idField.ValueOf(r.ctx, parent) + if typeZero || idZero { + continue + } + morphType, _ := typeVal.(string) + if morphType == "" { + morphType = fmt.Sprint(typeVal) + } + k := dictKey(idVal) + b, exists := buckets[morphType] + if !exists { + b = &bucket{seenKeys: map[string]struct{}{}} + buckets[morphType] = b + } + if _, dup := b.seenKeys[k]; !dup { + b.seenKeys[k] = struct{}{} + b.keys = append(b.keys, idVal) + } + parentMorphKey[i] = morphType + parentBucketKey[i] = k + } + + if len(buckets) == 0 { + // No parents pointed at anything; clear the relation field on each. + for _, parent := range parents { + if err := setRelationField(parent, entry.relation, nil); err != nil { + return err + } + } + return nil + } + + // For each bucket: resolve type, run IN query, build a per-bucket id->row dict. + type resolvedBucket struct { + dict map[string]reflect.Value // parent's dictKey(idVal) -> *RelatedModel + rows []reflect.Value + } + resolved := make(map[string]resolvedBucket, len(buckets)) + allRows := make([]reflect.Value, 0) + + for morphType, b := range buckets { + sample := morphmap.Find(morphType) + if sample == nil { + return errors.OrmMorphTypeUnknown.Args(morphType) + } + relatedTable, terr := tableNameFor(r.instance, sample) + if terr != nil { + return terr + } + // Make a per-bucket descriptor so runChunkedRelatedQuery's user-callback gets a + // related-shaped query. + bucketDesc := &relationDescriptor{ + kind: relKindBelongsTo, // BelongsTo-shaped: WHERE related. IN ? + parentTable: desc.parentTable, + relatedTable: relatedTable, + relatedModel: sample, + onQuery: desc.onQuery, // propagate so the default scope applies per bucket + } + ownerKey := desc.morphOwnerKey + if ownerKey == "" { + ownerKey = "id" + } + + rows, qerr := r.runChunkedRelatedQuery(b.keys, bucketDesc, entry, []string{ownerKey}, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(relatedTable). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(relatedTable), quoteIdent(ownerKey)), chunk) + }) + if qerr != nil { + return qerr + } + allRows = append(allRows, rows...) + + relatedSchema, perr := parseGormSchema(r.instance, sample) + if perr != nil { + return perr + } + ownerField, ok := relatedSchema.FieldsByDBName[ownerKey] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no owner key field for "+ownerKey) + } + dict := make(map[string]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := ownerField.ValueOf(r.ctx, row.Elem()) + dict[dictKey(val)] = row + } + resolved[morphType] = resolvedBucket{dict: dict, rows: rows} + } + + // Assign each parent the row it pointed to. + for i, parent := range parents { + morphType := parentMorphKey[i] + idKey := parentBucketKey[i] + if morphType == "" || idKey == "" { + if err := setRelationField(parent, entry.relation, nil); err != nil { + return err + } + continue + } + bucketResult, ok := resolved[morphType] + if !ok { + if err := setRelationField(parent, entry.relation, nil); err != nil { + return err + } + continue + } + row, ok := bucketResult.dict[idKey] + if !ok { + if err := setRelationField(parent, entry.relation, nil); err != nil { + return err + } + continue + } + if err := setRelationField(parent, entry.relation, []reflect.Value{row}); err != nil { + return err + } + } + + return r.recurseNested(allRows, nested) +} + +// loadMany2Many eager-loads a regular many-to-many relation through a pivot table whose schema +// is described by GORM's parsed metadata. +func (r *Query) loadMany2Many(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry) error { + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + parentField, ok := parentSchema.FieldsByDBName[desc.pivotParentRef.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+desc.pivotParentRef.primaryColumn) + } + + keys := extractKeys(r, parents, parentField) + if len(keys) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, true, nested) + } + + pivotParentCol := desc.pivotParentRef.foreignColumn + pivotRelatedCol := desc.pivotRelatedRef.foreignColumn + + // Check if the related model has a Pivot field — if yes, we'll SELECT extra pivot columns + // using the desc.pivotUsing struct schema. struct-only: when desc.pivotUsing is nil, no pivot + // hydration happens regardless of whether the related model has a Pivot field. + relatedSchema, err := parseGormSchema(r.instance, desc.relatedModel) + if err != nil { + return err + } + pivotPlan, err := preparePivotHydration(r, desc) + if err != nil { + return err + } + + // Build the pivot SELECT list: always include the two FK columns, plus the columns reported + // by the Using-struct hydration plan when present. + pivotSelectCols := []string{pivotParentCol, pivotRelatedCol} + if pivotPlan != nil { + pivotSelectCols = append(pivotSelectCols, pivotPlan.extraColumns...) + } + + // Convert []string to []interface{} for GORM's Select signature. + selectArgs := make([]interface{}, len(pivotSelectCols)) + for i, col := range pivotSelectCols { + selectArgs[i] = col + } + + pivotRows, err := r.chunkedFindMaps(keys, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(desc.pivotTable). + Select(selectArgs[0], selectArgs[1:]...). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(pivotParentCol)), chunk) + }) + if err != nil { + return err + } + if len(pivotRows) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, true, nested) + } + + relatedKeysSet := make(map[string]any, len(pivotRows)) + for _, p := range pivotRows { + k := dictKey(p[pivotRelatedCol]) + if _, exists := relatedKeysSet[k]; !exists { + relatedKeysSet[k] = p[pivotRelatedCol] + } + } + relatedKeys := make([]any, 0, len(relatedKeysSet)) + for _, v := range relatedKeysSet { + relatedKeys = append(relatedKeys, v) + } + + rows, err := r.runChunkedRelatedQuery(relatedKeys, desc, entry, []string{desc.pivotRelatedRef.primaryColumn}, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(desc.relatedTable). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.relatedTable), quoteIdent(desc.pivotRelatedRef.primaryColumn)), chunk) + }) + if err != nil { + return err + } + + relatedPKField, ok := relatedSchema.FieldsByDBName[desc.pivotRelatedRef.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no related PK field for "+desc.pivotRelatedRef.primaryColumn) + } + relatedByID := make(map[string]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := relatedPKField.ValueOf(r.ctx, row.Elem()) + relatedByID[dictKey(val)] = row + } + + // 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 { + relatedKey := dictKey(p[pivotRelatedCol]) + data := make(map[string]any, len(pivotPlan.extraColumns)) + for _, col := range pivotPlan.extraColumns { + if val, ok := p[col]; ok { + data[col] = val + } + } + pivotDataByRelatedID[relatedKey] = data + } + } + + dict := make(map[string][]reflect.Value, len(parents)) + for _, p := range pivotRows { + parentKey := dictKey(p[pivotParentCol]) + relatedKey := dictKey(p[pivotRelatedCol]) + if rel, ok := relatedByID[relatedKey]; ok { + dict[parentKey] = append(dict[parentKey], rel) + } + } + + if err := r.assignToParents(parents, parentField, entry.relation, dict, true); err != nil { + return err + } + + // Hydrate Pivot field on each related row if we have pivot data. + if pivotPlan != nil && len(pivotDataByRelatedID) > 0 { + for _, row := range rows { + val, _ := relatedPKField.ValueOf(r.ctx, row.Elem()) + relatedKey := dictKey(val) + if data, ok := pivotDataByRelatedID[relatedKey]; ok { + if err := writePivotField(r.ctx, row, data, pivotPlan); err != nil { + return err + } + } + } + } + + return r.recurseNested(rows, nested) +} + +// loadMorphToMany eager-loads polymorphic many-to-many. Mirrors loadMany2Many with one extra +// pivot WHERE that pins the morph_type column to desc.morphValue. Both MorphToMany (forward) and +// MorphedByMany (inverse) share this code path; the difference between them is captured in +// desc.morphValue at descriptor-build time. +func (r *Query) loadMorphToMany(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry) error { + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + parentField, ok := parentSchema.FieldsByDBName[desc.pivotParentRef.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+desc.pivotParentRef.primaryColumn) + } + + keys := extractKeys(r, parents, parentField) + if len(keys) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, true, nested) + } + + pivotParentCol := desc.pivotParentRef.foreignColumn + pivotRelatedCol := desc.pivotRelatedRef.foreignColumn + + // Check if the related model has a Pivot field — if yes, we'll SELECT extra pivot columns + // using the desc.pivotUsing struct schema. struct-only: when desc.pivotUsing is nil, no pivot + // hydration happens regardless of whether the related model has a Pivot field. + relatedSchema, err := parseGormSchema(r.instance, desc.relatedModel) + if err != nil { + return err + } + pivotPlan, err := preparePivotHydration(r, desc) + if err != nil { + return err + } + + // Build the pivot SELECT list: always include the two FK columns, plus the columns reported + // by the Using-struct hydration plan when present. + pivotSelectCols := []string{pivotParentCol, pivotRelatedCol} + if pivotPlan != nil { + pivotSelectCols = append(pivotSelectCols, pivotPlan.extraColumns...) + } + + // Convert []string to []interface{} for GORM's Select signature. + selectArgs := make([]interface{}, len(pivotSelectCols)) + for i, col := range pivotSelectCols { + selectArgs[i] = col + } + + pivotRows, err := r.chunkedFindMaps(keys, func(chunk []any) *gormio.DB { + 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) + }) + if err != nil { + return err + } + if len(pivotRows) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, true, nested) + } + + relatedKeysSet := make(map[string]any, len(pivotRows)) + for _, p := range pivotRows { + k := dictKey(p[pivotRelatedCol]) + if _, exists := relatedKeysSet[k]; !exists { + relatedKeysSet[k] = p[pivotRelatedCol] + } + } + relatedKeys := make([]any, 0, len(relatedKeysSet)) + for _, v := range relatedKeysSet { + relatedKeys = append(relatedKeys, v) + } + + rows, err := r.runChunkedRelatedQuery(relatedKeys, desc, entry, []string{desc.pivotRelatedRef.primaryColumn}, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(desc.relatedTable). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.relatedTable), quoteIdent(desc.pivotRelatedRef.primaryColumn)), chunk) + }) + if err != nil { + return err + } + + relatedPKField, ok := relatedSchema.FieldsByDBName[desc.pivotRelatedRef.primaryColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no related PK field for "+desc.pivotRelatedRef.primaryColumn) + } + relatedByID := make(map[string]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := relatedPKField.ValueOf(r.ctx, row.Elem()) + relatedByID[dictKey(val)] = row + } + + // 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 { + relatedKey := dictKey(p[pivotRelatedCol]) + data := make(map[string]any, len(pivotPlan.extraColumns)) + for _, col := range pivotPlan.extraColumns { + if val, ok := p[col]; ok { + data[col] = val + } + } + pivotDataByRelatedID[relatedKey] = data + } + } + + dict := make(map[string][]reflect.Value, len(parents)) + for _, p := range pivotRows { + parentKey := dictKey(p[pivotParentCol]) + relatedKey := dictKey(p[pivotRelatedCol]) + if rel, ok := relatedByID[relatedKey]; ok { + dict[parentKey] = append(dict[parentKey], rel) + } + } + + if err := r.assignToParents(parents, parentField, entry.relation, dict, true); err != nil { + return err + } + + // Hydrate Pivot field on each related row if we have pivot data. + if pivotPlan != nil && len(pivotDataByRelatedID) > 0 { + for _, row := range rows { + val, _ := relatedPKField.ValueOf(r.ctx, row.Elem()) + relatedKey := dictKey(val) + if data, ok := pivotDataByRelatedID[relatedKey]; ok { + if err := writePivotField(r.ctx, row, data, pivotPlan); err != nil { + return err + } + } + } + } + + return r.recurseNested(rows, nested) +} + +func (r *Query) loadThrough(parents []reflect.Value, parentModel any, desc *relationDescriptor, entry eagerLoadEntry, nested []eagerLoadEntry, isMany bool) error { + parentSchema, err := parseGormSchema(r.instance, parentModel) + if err != nil { + return err + } + parentField, ok := parentSchema.FieldsByDBName[desc.localKey] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, parentSchema.Name, "no parent field for "+desc.localKey) + } + + keys := extractKeys(r, parents, parentField) + if len(keys) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, isMany, nested) + } + + throughRows, err := r.chunkedFindMaps(keys, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(desc.throughTable). + Select(desc.firstKey, desc.secondLocalKey). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.throughTable), quoteIdent(desc.firstKey)), chunk) + }) + if err != nil { + return err + } + if len(throughRows) == 0 { + return r.maybeRecurseEmpty(parents, entry.relation, isMany, nested) + } + + secondKeysSet := make(map[string]any, len(throughRows)) + for _, t := range throughRows { + k := dictKey(t[desc.secondLocalKey]) + if _, exists := secondKeysSet[k]; !exists { + secondKeysSet[k] = t[desc.secondLocalKey] + } + } + secondKeys := make([]any, 0, len(secondKeysSet)) + for _, v := range secondKeysSet { + secondKeys = append(secondKeys, v) + } + + rows, err := r.runChunkedRelatedQuery(secondKeys, desc, entry, []string{desc.secondKey}, func(chunk []any) *gormio.DB { + return r.freshSession(). + Table(desc.relatedTable). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.relatedTable), quoteIdent(desc.secondKey)), chunk) + }) + if err != nil { + return err + } + + relatedSchema, err := parseGormSchema(r.instance, desc.relatedModel) + if err != nil { + return err + } + secondField, ok := relatedSchema.FieldsByDBName[desc.secondKey] + if !ok { + return errors.OrmRelationUnsupported.Args(entry.relation, relatedSchema.Name, "no related field for "+desc.secondKey) + } + relatedByThrough := make(map[string][]reflect.Value, len(rows)) + for _, row := range rows { + val, _ := secondField.ValueOf(r.ctx, row.Elem()) + k := dictKey(val) + relatedByThrough[k] = append(relatedByThrough[k], row) + } + + dict := make(map[string][]reflect.Value, len(parents)) + for _, t := range throughRows { + parentKey := dictKey(t[desc.firstKey]) + secondKey := dictKey(t[desc.secondLocalKey]) + if rels, ok := relatedByThrough[secondKey]; ok { + dict[parentKey] = append(dict[parentKey], rels...) + } + } + + if err := r.assignToParents(parents, parentField, entry.relation, dict, isMany); err != nil { + return err + } + return r.recurseNested(rows, nested) +} + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +// runRelatedQuery applies the user's callback and column pruning to the inner builder, executes +// it, and returns the result rows as []reflect.Value where each value is a *RelatedModel. +// +// requiredCols are columns the loader needs back (FK columns, PK columns) to build dictionaries; +// they are appended to the user's prune list when not already present so the caller does not have +// to remember to include them. +func (r *Query) runRelatedQuery(inner *gormio.DB, desc *relationDescriptor, entry eagerLoadEntry, requiredCols []string) ([]reflect.Value, error) { + // Apply the per-relation default scope first so callers can layer extra constraints on top + // via With("Books", func(q) { ... }). + if desc.onQuery != nil { + wrapper := r.wrap(inner) + wrapped := desc.onQuery(wrapper) + if w, ok := wrapped.(*Query); ok { + inner = w.buildConditions().instance + } + } + var oneOfMany *oneOfManyConfig + if entry.callback != nil { + wrapper := r.wrap(inner) + wrapped := entry.callback(wrapper) + if w, ok := wrapped.(*Query); ok { + oneOfMany = w.conditions.oneOfMany + inner = w.buildConditions().instance + } + } + if oneOfMany != nil { + inner = r.applyOneOfManyJoin(inner, desc, oneOfMany) + } + if len(entry.columns) > 0 { + cols := append([]string(nil), entry.columns...) + for _, req := range requiredCols { + if !slices.ContainsFunc(cols, func(c string) bool { + if c == req { + return true + } + _, suffix, ok := strings.Cut(c, ".") + return ok && suffix == req + }) { + cols = append(cols, req) + } + } + inner = inner.Select(cols) + } + + relatedType := reflect.TypeOf(desc.relatedModel) + if relatedType.Kind() == reflect.Pointer { + relatedType = relatedType.Elem() + } + sliceType := reflect.SliceOf(reflect.PointerTo(relatedType)) + slicePtr := reflect.New(sliceType) + if err := inner.Find(slicePtr.Interface()).Error; err != nil { + return nil, err + } + slice := slicePtr.Elem() + out := make([]reflect.Value, 0, slice.Len()) + for i := 0; i < slice.Len(); i++ { + out = append(out, slice.Index(i)) + } + return out, nil +} + +// runChunkedRelatedQuery runs runRelatedQuery once per chunk of keys and concatenates rows. Each +// chunk gets a freshly built inner query from buildInner so the user's callback / column pruning +// is applied per-chunk. +// +// Note: when entry.callback installs a LIMIT, that LIMIT applies *per chunk*, not globally — +// same semantics as Eloquent's chunkById iteration and unavoidable for any chunked IN approach. +func (r *Query) runChunkedRelatedQuery(keys []any, desc *relationDescriptor, entry eagerLoadEntry, requiredCols []string, buildInner func(chunk []any) *gormio.DB) ([]reflect.Value, error) { + chunks := chunkKeys(keys, r.chunkSize()) + var all []reflect.Value + for _, chunk := range chunks { + rows, err := r.runRelatedQuery(buildInner(chunk), desc, entry, requiredCols) + if err != nil { + return nil, err + } + all = append(all, rows...) + } + return all, nil +} + +// chunkedFindMaps runs the pivot / through intermediate query in chunks of keys and accumulates +// results into a single []map[string]any. Used by loadMany2Many and loadThrough for the lookup +// queries that don't go through runRelatedQuery. +func (r *Query) chunkedFindMaps(keys []any, buildQuery func(chunk []any) *gormio.DB) ([]map[string]any, error) { + chunks := chunkKeys(keys, r.chunkSize()) + var all []map[string]any + for _, chunk := range chunks { + var rows []map[string]any + if err := buildQuery(chunk).Find(&rows).Error; err != nil { + return nil, err + } + all = append(all, rows...) + } + return all, nil +} + +// assignToParents writes the dictionary entries onto each parent's relation field using +// setRelationField. When isMany is false and a parent has multiple matches, only the first one is +// assigned (HasOne / BelongsTo / MorphOne / HasOneThrough cases). +func (r *Query) assignToParents(parents []reflect.Value, parentField *gormschema.Field, relation string, dict map[string][]reflect.Value, isMany bool) error { + for _, parent := range parents { + val, zero := parentField.ValueOf(r.ctx, parent) + if zero { + if !isMany { + continue + } + if err := setRelationField(parent, relation, nil); err != nil { + return err + } + continue + } + match := dict[dictKey(val)] + if !isMany && len(match) > 1 { + match = match[:1] + } + if err := setRelationField(parent, relation, match); err != nil { + return err + } + } + return nil +} + +func (r *Query) recurseNested(rows []reflect.Value, nested []eagerLoadEntry) error { + if len(rows) == 0 || len(nested) == 0 { + return nil + } + nestedParents := make([]reflect.Value, 0, len(rows)) + for _, row := range rows { + nestedParents = append(nestedParents, row.Elem()) + } + return r.runEagerLoads(nestedParents, nested) +} + +// maybeRecurseEmpty is the no-op fast path: when there are no parent keys to load against, leave +// each parent's relation field at its zero value (or empty slice for many) and skip nested. +func (r *Query) maybeRecurseEmpty(parents []reflect.Value, relation string, isMany bool, _ []eagerLoadEntry) error { + if !isMany { + return nil + } + for _, parent := range parents { + if err := setRelationField(parent, relation, nil); err != nil { + return err + } + } + return nil +} + +// --------------------------------------------------------------------------- +// Reflect / extraction helpers +// --------------------------------------------------------------------------- + +// collectEagerParents extracts the addressable struct values from dest. dest may be *Struct, +// *[]Struct, or *[]*Struct; each form yields a flat slice of struct values whose fields can be +// mutated. +func collectEagerParents(dest any) ([]reflect.Value, error) { + if dest == nil { + return nil, nil + } + rv := reflect.ValueOf(dest) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return nil, nil + } + elem := rv.Elem() + switch elem.Kind() { + case reflect.Struct: + return []reflect.Value{elem}, nil + case reflect.Slice: + out := make([]reflect.Value, 0, elem.Len()) + for i := 0; i < elem.Len(); i++ { + item := elem.Index(i) + if item.Kind() == reflect.Pointer { + if item.IsNil() { + continue + } + item = item.Elem() + } + if item.Kind() != reflect.Struct { + continue + } + out = append(out, item) + } + return out, nil + } + return nil, nil +} + +// newSampleModel returns a fresh pointer-to-struct of the same type as parent. resolveRelation +// expects an addressable model instance (it parses the schema and inspects its tags), and we +// don't want to hand it one of our actual parent rows (which may carry mutated fields). +func newSampleModel(parent reflect.Value) any { + t := parent.Type() + return reflect.New(t).Interface() +} + +func parseGormSchema(db *gormio.DB, model any) (*gormschema.Schema, error) { + stmt := &gormio.Statement{DB: db} + if err := stmt.Parse(model); err != nil { + return nil, err + } + return stmt.Schema, nil +} + +// extractKeys pulls the unique non-zero values of field from the parent slice. +func extractKeys(r *Query, parents []reflect.Value, field *gormschema.Field) []any { + seen := make(map[string]struct{}, len(parents)) + out := make([]any, 0, len(parents)) + for _, parent := range parents { + val, zero := field.ValueOf(r.ctx, parent) + if zero { + continue + } + k := dictKey(val) + if _, dup := seen[k]; dup { + continue + } + seen[k] = struct{}{} + out = append(out, val) + } + return out +} + +// dictKey reduces any value to a canonical string for use as a map key, paving over the type +// mismatch between Go field types (uint, int64, string) and database-layer scan types +// (often int64 or []byte). Mirrors fedaco's _getDictionaryKey. +func dictKey(v any) string { + switch x := v.(type) { + case nil: + return "" + case []byte: + return string(x) + case string: + return x + } + return fmt.Sprint(v) +} + +// chunkSize returns the eager-load IN-clause chunk size, falling back to the default when the +// config value is unset or invalid. A non-positive value disables chunking. +func (r *Query) chunkSize() int { + if r.config == nil { + return defaultEagerLoadChunkSize + } + v := r.config.GetInt("database.eager_load_chunk_size", defaultEagerLoadChunkSize) + if v == 0 { + return defaultEagerLoadChunkSize + } + return v +} + +// chunkKeys splits keys into batches of at most size. Returns the input unchanged in a single +// batch when size <= 0 or len(keys) <= size, which lets callers stay on the cheap single-query +// path for typical workloads. +func chunkKeys(keys []any, size int) [][]any { + if size <= 0 || len(keys) <= size { + return [][]any{keys} + } + return lo.Chunk(keys, size) +} + +// setRelationField writes loaded rows back to parent's relation field. Supports *Model, +// []*Model, []Model, and `any` (interface) field shapes — the last is used by MorphTo, where the +// concrete loaded type is determined per row from the morph map. +func setRelationField(parent reflect.Value, fieldName string, rows []reflect.Value) error { + field := parent.FieldByName(fieldName) + if !field.IsValid() { + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + } + if !field.CanSet() { + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + } + + switch field.Kind() { + case reflect.Interface: + if len(rows) == 0 { + field.Set(reflect.Zero(field.Type())) + return nil + } + row := rows[0] + if row.Type().Implements(field.Type()) { + field.Set(row) + return nil + } + // `any` (empty interface) — anything implements it. + if field.Type().NumMethod() == 0 { + field.Set(row) + return nil + } + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + + case reflect.Pointer: + if len(rows) == 0 { + field.Set(reflect.Zero(field.Type())) + return nil + } + row := rows[0] + if row.Type() == field.Type() { + field.Set(row) + return nil + } + if row.Kind() == reflect.Pointer && row.Type().Elem() == field.Type().Elem() { + field.Set(row) + return nil + } + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + + case reflect.Slice: + elemType := field.Type().Elem() + out := reflect.MakeSlice(field.Type(), 0, len(rows)) + for _, row := range rows { + switch elemType.Kind() { + case reflect.Pointer: + if row.Type() == elemType { + out = reflect.Append(out, row) + continue + } + if row.Kind() == reflect.Pointer && row.Type().Elem() == elemType.Elem() { + out = reflect.Append(out, row) + continue + } + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + case reflect.Struct: + if row.Kind() == reflect.Pointer && row.Type().Elem() == elemType { + out = reflect.Append(out, row.Elem()) + continue + } + if row.Type() == elemType { + out = reflect.Append(out, row) + continue + } + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + default: + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) + } + } + field.Set(out) + return nil + } + return errors.OrmEagerLoadCannotAssign.Args(fieldName, parent.Type().String()) +} + +// pivotHydrationPlan precomputes everything writePivotField needs to copy a row of pivot column +// values into the configured pivot field on each eager-loaded related model. It is built once +// per loadMany2Many / loadMorphToMany invocation by preparePivotHydration. nil means "no Pivot +// hydration" (related model has no field by desc.pivotField). +type pivotHydrationPlan struct { + // fieldName is the struct field on the related model that we hydrate (typically "Pivot"). + fieldName string + // extraColumns is the pivot SELECT list contributed by the pivot struct (every db-tagged + // field's column name), not including the two FK columns which loadMany2Many always selects. + extraColumns []string + // fieldByColumn maps each db column name to the *gormschema.Field that owns it on the pivot + // struct. Used by writePivotField to set struct fields from the SELECT row. + fieldByColumn map[string]*gormschema.Field +} + +// preparePivotHydration inspects the related model for a field named desc.pivotField. When found, +// returns a plan that drives the pivot SELECT list and field-by-field hydration. Returns nil (no +// error) when the related model has no field by that name — pivot data still flows through the +// SELECT but nothing is surfaced. Returns an error when the field exists but isn't a struct +// (catches typos like `Pivot string`). +func preparePivotHydration(r *Query, desc *relationDescriptor) (*pivotHydrationPlan, error) { + relatedType := reflect.TypeOf(desc.relatedModel) + if relatedType.Kind() == reflect.Pointer { + relatedType = relatedType.Elem() + } + if relatedType.Kind() != reflect.Struct { + return nil, nil + } + pivotStructField, ok := relatedType.FieldByName(desc.pivotField) + if !ok { + // No field by this name — silently skip hydration; the relation still works for joining. + return nil, nil + } + if pivotStructField.Type.Kind() != reflect.Struct { + return nil, errors.OrmRelationPivotFieldNotStruct.Args( + relatedType.String(), desc.pivotField, pivotStructField.Type.Kind().String(), + ) + } + // Parse the field type's GORM schema by instantiating a zero value of it. + usingSchema, err := parseGormSchema(r.instance, reflect.New(pivotStructField.Type).Interface()) + if err != nil { + return nil, err + } + cols := make([]string, 0, len(usingSchema.Fields)) + byCol := make(map[string]*gormschema.Field, len(usingSchema.Fields)) + for _, f := range usingSchema.Fields { + if f.DBName == "" { + continue + } + cols = append(cols, f.DBName) + byCol[f.DBName] = f + } + return &pivotHydrationPlan{ + fieldName: desc.pivotField, + extraColumns: cols, + fieldByColumn: byCol, + }, nil +} + +// writePivotField copies the column values in data into rv's pivot struct field (named +// plan.fieldName), using plan.fieldByColumn to resolve column names to *gormschema.Fields on the +// pivot struct. Caller guarantees plan is non-nil. +func writePivotField(ctx context.Context, rv reflect.Value, data map[string]any, plan *pivotHydrationPlan) error { + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + pivotField := rv.FieldByName(plan.fieldName) + if !pivotField.IsValid() || !pivotField.CanSet() { + return nil + } + for col, val := range data { + f, ok := plan.fieldByColumn[col] + if !ok { + continue + } + if err := f.Set(ctx, pivotField, val); err != nil { + return err + } + } + return nil +} diff --git a/database/gorm/eager_loader_test.go b/database/gorm/eager_loader_test.go new file mode 100644 index 000000000..dfaf432f5 --- /dev/null +++ b/database/gorm/eager_loader_test.go @@ -0,0 +1,439 @@ +package gorm + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + gormschema "gorm.io/gorm/schema" + + contractsdatabase "github.com/goravel/framework/contracts/database" + "github.com/goravel/framework/errors" +) + +// --- Pure helpers ---------------------------------------------------------- + +func TestDictKey(t *testing.T) { + cases := []struct { + name string + input any + want string + }{ + {"nil", nil, ""}, + {"string", "abc", "abc"}, + {"bytes", []byte("abc"), "abc"}, + {"int", 42, "42"}, + {"int64", int64(42), "42"}, + {"uint", uint(7), "7"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, dictKey(tc.input)) + }) + } +} + +func TestChunkSize(t *testing.T) { + q := newRelQuery(t) + // nil config -> default + assert.Equal(t, defaultEagerLoadChunkSize, q.chunkSize()) +} + +// --- Reflect helpers ------------------------------------------------------ + +func TestCollectEagerParentsStructPtr(t *testing.T) { + u := &relUser{ID: 1} + out, err := collectEagerParents(u) + assert.NoError(t, err) + assert.Len(t, out, 1) +} + +func TestCollectEagerParentsSlice(t *testing.T) { + users := []relUser{{ID: 1}, {ID: 2}} + out, err := collectEagerParents(&users) + assert.NoError(t, err) + assert.Len(t, out, 2) +} + +func TestCollectEagerParentsSliceOfPtr(t *testing.T) { + users := []*relUser{{ID: 1}, nil, {ID: 2}} + out, err := collectEagerParents(&users) + assert.NoError(t, err) + assert.Len(t, out, 2) +} + +func TestCollectEagerParentsNil(t *testing.T) { + out, err := collectEagerParents(nil) + assert.NoError(t, err) + assert.Nil(t, out) + + var p *relUser + out, err = collectEagerParents(p) + assert.NoError(t, err) + assert.Nil(t, out) +} + +func TestCollectEagerParentsNotPointer(t *testing.T) { + out, err := collectEagerParents(relUser{}) + assert.NoError(t, err) + assert.Nil(t, out) +} + +func TestCollectEagerParentsUnsupportedKind(t *testing.T) { + v := 7 + out, err := collectEagerParents(&v) + assert.NoError(t, err) + assert.Nil(t, out) +} + +func TestNewSampleModel(t *testing.T) { + u := relUser{ID: 1} + rv := reflect.ValueOf(u) + got := newSampleModel(rv) + rt := reflect.TypeOf(got) + assert.Equal(t, reflect.Pointer, rt.Kind()) + assert.Equal(t, "relUser", rt.Elem().Name()) + // Should be a fresh zero instance (not the original). + assert.Equal(t, uint(0), got.(*relUser).ID) +} + +func TestParseGormSchema(t *testing.T) { + db := newStubGormDB(t) + s, err := parseGormSchema(db, &relUser{}) + assert.NoError(t, err) + assert.Equal(t, "rel_users", s.Table) + + _, err = parseGormSchema(db, "bad-model") + assert.Error(t, err) +} + +func TestExtractKeysDeduplicatesAndSkipsZero(t *testing.T) { + db := newStubGormDB(t) + s, err := parseGormSchema(db, &relUser{}) + assert.NoError(t, err) + idField := s.FieldsByDBName["id"] + assert.NotNil(t, idField) + + q := NewQuery(context.Background(), nil, contractsdatabase.Config{}, db, nil, nil, nil, &Conditions{}) + + parents := []reflect.Value{ + reflect.ValueOf(relUser{ID: 1}), + reflect.ValueOf(relUser{ID: 2}), + reflect.ValueOf(relUser{ID: 1}), // dup + reflect.ValueOf(relUser{ID: 0}), // zero - skipped + } + keys := extractKeys(q, parents, idField) + assert.Len(t, keys, 2) +} + +// --- setRelationField ------------------------------------------------------ + +type withPtrRel struct { + ID uint + Profile *relProfile +} + +type withSlicePtrRel struct { + ID uint + Books []*relBook +} + +type withSliceStructRel struct { + ID uint + Books []relBook +} + +func TestSetRelationField_PtrAssignment(t *testing.T) { + parent := withPtrRel{} + rv := reflect.ValueOf(&parent).Elem() + row := reflect.ValueOf(&relProfile{Bio: "x"}) + err := setRelationField(rv, "Profile", []reflect.Value{row}) + assert.NoError(t, err) + assert.Equal(t, "x", parent.Profile.Bio) +} + +func TestSetRelationField_PtrEmptyClearsField(t *testing.T) { + parent := withPtrRel{Profile: &relProfile{Bio: "stale"}} + rv := reflect.ValueOf(&parent).Elem() + err := setRelationField(rv, "Profile", nil) + assert.NoError(t, err) + assert.Nil(t, parent.Profile) +} + +func TestSetRelationField_SliceOfPtrs(t *testing.T) { + parent := withSlicePtrRel{} + rv := reflect.ValueOf(&parent).Elem() + rows := []reflect.Value{ + reflect.ValueOf(&relBook{Title: "a"}), + reflect.ValueOf(&relBook{Title: "b"}), + } + err := setRelationField(rv, "Books", rows) + assert.NoError(t, err) + assert.Len(t, parent.Books, 2) +} + +func TestSetRelationField_SliceOfStructs(t *testing.T) { + parent := withSliceStructRel{} + rv := reflect.ValueOf(&parent).Elem() + rows := []reflect.Value{ + reflect.ValueOf(&relBook{Title: "a"}), + reflect.ValueOf(&relBook{Title: "b"}), + } + err := setRelationField(rv, "Books", rows) + assert.NoError(t, err) + assert.Len(t, parent.Books, 2) + assert.Equal(t, "a", parent.Books[0].Title) +} + +func TestSetRelationField_UnknownField(t *testing.T) { + parent := withPtrRel{} + rv := reflect.ValueOf(&parent).Elem() + err := setRelationField(rv, "Missing", nil) + assert.True(t, errors.Is(err, errors.OrmEagerLoadCannotAssign)) +} + +// withInterfaceRel exercises the MorphTo field shape: an `any` field that the loader fills with +// a *RelatedModel value chosen at runtime via the morph map. +type withInterfaceRel struct { + ID uint + Imageable any +} + +func TestSetRelationField_InterfaceAssignment(t *testing.T) { + parent := withInterfaceRel{} + rv := reflect.ValueOf(&parent).Elem() + row := reflect.ValueOf(&relBook{Title: "x"}) + err := setRelationField(rv, "Imageable", []reflect.Value{row}) + assert.NoError(t, err) + got, ok := parent.Imageable.(*relBook) + assert.True(t, ok) + assert.Equal(t, "x", got.Title) +} + +func TestSetRelationField_InterfaceEmptyClearsField(t *testing.T) { + parent := withInterfaceRel{Imageable: &relBook{Title: "stale"}} + rv := reflect.ValueOf(&parent).Elem() + err := setRelationField(rv, "Imageable", nil) + assert.NoError(t, err) + assert.Nil(t, parent.Imageable) +} + +// --- runEagerLoads no-op paths -------------------------------------------- + +func TestRunEagerLoadsNoParents(t *testing.T) { + q := newRelQuery(t) + err := q.runEagerLoads(nil, []eagerLoadEntry{{relation: "Books"}}) + assert.NoError(t, err) +} + +func TestRunEagerLoadsNoEntries(t *testing.T) { + q := newRelQuery(t) + parents := []reflect.Value{reflect.ValueOf(relUser{ID: 1})} + err := q.runEagerLoads(parents, nil) + assert.NoError(t, err) +} + +func TestApplyEagerLoadsNothingQueued(t *testing.T) { + q := newRelQuery(t) + users := &[]relUser{} + err := q.applyEagerLoads(users) + assert.NoError(t, err) +} + +func TestApplyEagerLoadsEmptyDest(t *testing.T) { + q := newRelQuery(t) + q.conditions.eagerLoad = []eagerLoadEntry{{relation: "Books"}} + users := &[]relUser{} + err := q.applyEagerLoads(users) + assert.NoError(t, err) +} + +func TestRecurseNestedNoop(t *testing.T) { + q := newRelQuery(t) + err := q.recurseNested(nil, []eagerLoadEntry{{relation: "X"}}) + assert.NoError(t, err) + err = q.recurseNested([]reflect.Value{reflect.ValueOf(relUser{})}, nil) + assert.NoError(t, err) +} + +func TestMaybeRecurseEmpty_NotMany(t *testing.T) { + q := newRelQuery(t) + err := q.maybeRecurseEmpty(nil, "X", false, nil) + assert.NoError(t, err) +} + +func TestMaybeRecurseEmpty_ManyAssignsEmptySlices(t *testing.T) { + q := newRelQuery(t) + u1 := &withSlicePtrRel{ID: 1, Books: []*relBook{{Title: "a"}}} + u2 := &withSlicePtrRel{ID: 2} + parents := []reflect.Value{reflect.ValueOf(u1).Elem(), reflect.ValueOf(u2).Elem()} + err := q.maybeRecurseEmpty(parents, "Books", true, nil) + assert.NoError(t, err) + assert.Empty(t, u1.Books) + assert.Empty(t, u2.Books) +} + +// --- Phase D: Pivot column hydration tests -------------------------------- + +// roleUserPivot is a sample custom Pivot model used by the struct-only Pivot tests below. The +// gorm tags lock column names so the test data (keyed by db column) maps deterministically into +// struct fields. +type roleUserPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + Priority string `gorm:"column:priority"` + Notes string `gorm:"column:notes"` +} + +type roleWithPivot struct { + ID uint + Name string + Pivot roleUserPivot `gorm:"-"` +} + +type roleWithoutPivot struct { + ID uint + Name string +} + +// roleWithCustomPivotField has the pivot data on a non-default field name — exercises +// PivotField configuration. +type roleWithCustomPivotField struct { + ID uint + Name string + UserPivot roleUserPivot `gorm:"-"` +} + +// roleWithBadPivot has a Pivot field that isn't a struct — exercises the field-not-struct guard. +type roleWithBadPivot struct { + ID uint + Name string + Pivot string `gorm:"-"` +} + +func TestWritePivotField_HydratesStructField(t *testing.T) { + role := &roleWithPivot{ID: 1, Name: "admin"} + plan := mustPivotPlan(t, "Pivot", &roleUserPivot{}) + err := writePivotField(t.Context(), reflect.ValueOf(role), map[string]any{ + "priority": "high", + "notes": "test", + }, plan) + assert.NoError(t, err) + assert.Equal(t, "high", role.Pivot.Priority) + assert.Equal(t, "test", role.Pivot.Notes) +} + +func TestWritePivotField_TypedColumns(t *testing.T) { + role := &roleWithPivot{ID: 1} + plan := mustPivotPlan(t, "Pivot", &roleUserPivot{}) + err := writePivotField(t.Context(), reflect.ValueOf(role), map[string]any{ + "user_id": uint(7), + "role_id": uint(99), + }, plan) + assert.NoError(t, err) + assert.Equal(t, uint(7), role.Pivot.UserID) + assert.Equal(t, uint(99), role.Pivot.RoleID) +} + +func TestWritePivotField_CustomFieldName(t *testing.T) { + role := &roleWithCustomPivotField{ID: 1} + plan := mustPivotPlan(t, "UserPivot", &roleUserPivot{}) + err := writePivotField(t.Context(), reflect.ValueOf(role), map[string]any{ + "user_id": uint(7), + "priority": "high", + }, plan) + assert.NoError(t, err) + assert.Equal(t, uint(7), role.UserPivot.UserID) + assert.Equal(t, "high", role.UserPivot.Priority) +} + +func TestWritePivotField_NoPivotField_ReturnsNil(t *testing.T) { + role := &roleWithoutPivot{ID: 1, Name: "admin"} + plan := mustPivotPlan(t, "Pivot", &roleUserPivot{}) + err := writePivotField(t.Context(), reflect.ValueOf(role), map[string]any{"priority": "high"}, plan) + assert.NoError(t, err, "writePivotField must silently skip when configured field is absent") +} + +func TestWritePivotField_UnknownColumn_Skipped(t *testing.T) { + role := &roleWithPivot{ID: 1} + plan := mustPivotPlan(t, "Pivot", &roleUserPivot{}) + err := writePivotField(t.Context(), reflect.ValueOf(role), map[string]any{ + "priority": "high", + "unknown_xy": "ignored", + }, plan) + assert.NoError(t, err) + assert.Equal(t, "high", role.Pivot.Priority) +} + +func TestPreparePivotHydration_NoFieldOnRelated_ReturnsNil(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + relatedModel: &roleWithoutPivot{}, + pivotField: "Pivot", + } + plan, err := preparePivotHydration(q, desc) + assert.NoError(t, err) + assert.Nil(t, plan, "no field by configured name means nothing to hydrate") +} + +func TestPreparePivotHydration_FieldNotStruct_Errors(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + relatedModel: &roleWithBadPivot{}, + pivotField: "Pivot", + } + _, err := preparePivotHydration(q, desc) + assert.True(t, errors.Is(err, errors.OrmRelationPivotFieldNotStruct)) +} + +func TestPreparePivotHydration_DefaultPivot_FromFieldType(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + relatedModel: &roleWithPivot{}, + pivotField: "Pivot", + } + plan, err := preparePivotHydration(q, desc) + assert.NoError(t, err) + assert.NotNil(t, plan) + assert.Equal(t, "Pivot", plan.fieldName) + // Selected columns include every db-tagged field on roleUserPivot. + assert.ElementsMatch(t, []string{"user_id", "role_id", "priority", "notes"}, plan.extraColumns) +} + +func TestPreparePivotHydration_CustomFieldName_FromFieldType(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + relatedModel: &roleWithCustomPivotField{}, + pivotField: "UserPivot", + } + plan, err := preparePivotHydration(q, desc) + assert.NoError(t, err) + assert.NotNil(t, plan) + assert.Equal(t, "UserPivot", plan.fieldName) +} + +// mustPivotPlan builds a pivotHydrationPlan for use in writePivotField tests, bypassing +// preparePivotHydration so we can exercise writePivotField independently. +func mustPivotPlan(t *testing.T, fieldName string, pivotProto any) *pivotHydrationPlan { + t.Helper() + q := newRelQueryWith(t, &relUser{}) + usingSchema, err := parseGormSchema(q.instance, pivotProto) + if err != nil { + t.Fatalf("parse pivot schema: %v", err) + } + cols := make([]string, 0, len(usingSchema.Fields)) + byCol := make(map[string]*gormschema.Field, len(usingSchema.Fields)) + for _, f := range usingSchema.Fields { + if f.DBName == "" { + continue + } + cols = append(cols, f.DBName) + byCol[f.DBName] = f + } + return &pivotHydrationPlan{ + fieldName: fieldName, + extraColumns: cols, + fieldByColumn: byCol, + } +} diff --git a/database/gorm/one_of_many.go b/database/gorm/one_of_many.go new file mode 100644 index 000000000..fe40dbe99 --- /dev/null +++ b/database/gorm/one_of_many.go @@ -0,0 +1,72 @@ +package gorm + +import ( + "fmt" + "strings" + + gormio "gorm.io/gorm" +) + +// applyOneOfManyJoin rewrites the inner eager-load query to keep only the row whose value of +// cfg.column equals the per-parent aggregate of cfg.column. The rewrite is an INNER JOIN against +// a grouped subquery; e.g. for MorphOne with cfg.aggregate = "MAX": +// +// INNER JOIN ( +// SELECT MAX(created_at) AS aggregate, imageable_id, imageable_type +// FROM images +// GROUP BY imageable_id, imageable_type +// ) sub ON images.created_at = sub.aggregate +// AND images.imageable_id = sub.imageable_id +// AND images.imageable_type = sub.imageable_type +// +// HasOne uses just the foreign-key column in the GROUP BY / JOIN. Mirrors fedaco's +// mixinCanBeOneOfMany at libs/fedaco/src/fedaco/relations/concerns/can-be-one-of-many.ts:81-145. +func (r *Query) applyOneOfManyJoin(inner *gormio.DB, desc *relationDescriptor, cfg *oneOfManyConfig) *gormio.DB { + if cfg == nil { + return inner + } + switch desc.kind { + case relKindHasOne, relKindMorphOne: + // supported + default: + // Ignore on unsupported kinds rather than erroring so a generic with-callback that calls + // LatestOfMany doesn't break unrelated relations downstream. Document elsewhere that + // OfMany is only effective on HasOne / MorphOne. + return inner + } + + if len(desc.references) == 0 { + return inner + } + ref := desc.references[0] + relatedTable := desc.relatedTable + const alias = "goravel_one_of_many" + + // Group by the foreign key (and morph type column for MorphOne). + groupCols := []string{quoteIdent(relatedTable) + "." + quoteIdent(ref.foreignColumn)} + selectCols := []string{ + fmt.Sprintf("%s(%s.%s) AS aggregate", cfg.aggregate, quoteIdent(relatedTable), quoteIdent(cfg.column)), + quoteIdent(relatedTable) + "." + quoteIdent(ref.foreignColumn), + } + if desc.kind == relKindMorphOne { + groupCols = append(groupCols, quoteIdent(relatedTable)+"."+quoteIdent(desc.morphTypeColumn)) + selectCols = append(selectCols, quoteIdent(relatedTable)+"."+quoteIdent(desc.morphTypeColumn)) + } + + sub := r.freshSession(). + Table(relatedTable). + Select(selectCols). + Group(strings.Join(groupCols, ", ")) + + on := []string{ + fmt.Sprintf("%s.%s = %s.aggregate", quoteIdent(relatedTable), quoteIdent(cfg.column), alias), + fmt.Sprintf("%s.%s = %s.%s", quoteIdent(relatedTable), quoteIdent(ref.foreignColumn), alias, quoteIdent(ref.foreignColumn)), + } + if desc.kind == relKindMorphOne { + on = append(on, fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(relatedTable), quoteIdent(desc.morphTypeColumn), + alias, quoteIdent(desc.morphTypeColumn))) + } + + return inner.Joins(fmt.Sprintf("INNER JOIN (?) %s ON %s", alias, strings.Join(on, " AND ")), sub) +} diff --git a/database/gorm/one_of_many_test.go b/database/gorm/one_of_many_test.go new file mode 100644 index 000000000..0620cdcb4 --- /dev/null +++ b/database/gorm/one_of_many_test.go @@ -0,0 +1,117 @@ +package gorm + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" +) + +func TestApplyOneOfManyJoin_HasOne(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + kind: relKindHasOne, + relatedTable: "books", + references: []referenceKey{{ + primaryTable: "users", + primaryColumn: "id", + foreignTable: "books", + foreignColumn: "user_id", + }}, + } + cfg := &oneOfManyConfig{column: "published_at", aggregate: "MAX"} + inner := q.freshSession().Table("books") + + got := q.applyOneOfManyJoin(inner, desc, cfg) + stmt := got.Session(&gormio.Session{DryRun: true}).Find(&[]map[string]any{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "INNER JOIN (") + assert.Contains(t, sql, "MAX(") + assert.Contains(t, strings.ToLower(sql), `published_at`) + assert.Contains(t, sql, `user_id`) +} + +func TestApplyOneOfManyJoin_MorphOne_IncludesTypeColumn(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + kind: relKindMorphOne, + relatedTable: "logos", + morphTypeColumn: "logoable_type", + morphIDColumn: "logoable_id", + references: []referenceKey{{ + primaryTable: "users", + primaryColumn: "id", + foreignTable: "logos", + foreignColumn: "logoable_id", + }}, + } + cfg := &oneOfManyConfig{column: "id", aggregate: "MIN"} + inner := q.freshSession().Table("logos") + + got := q.applyOneOfManyJoin(inner, desc, cfg) + stmt := got.Session(&gormio.Session{DryRun: true}).Find(&[]map[string]any{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "MIN(") + assert.Contains(t, sql, "logoable_id") + assert.Contains(t, sql, "logoable_type") // morph type included in GROUP BY / JOIN +} + +func TestApplyOneOfManyJoin_UnsupportedKind_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + kind: relKindHasMany, + relatedTable: "books", + references: []referenceKey{{ + primaryTable: "users", + primaryColumn: "id", + foreignTable: "books", + foreignColumn: "user_id", + }}, + } + cfg := &oneOfManyConfig{column: "id", aggregate: "MAX"} + inner := q.freshSession().Table("books") + + got := q.applyOneOfManyJoin(inner, desc, cfg) + stmt := got.Session(&gormio.Session{DryRun: true}).Find(&[]map[string]any{}) + sql := stmt.Statement.SQL.String() + assert.NotContains(t, strings.ToUpper(sql), "INNER JOIN (") +} + +func TestApplyOneOfManyJoin_NilCfg_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{ + kind: relKindHasOne, + relatedTable: "books", + references: []referenceKey{{ + primaryTable: "users", + primaryColumn: "id", + foreignTable: "books", + foreignColumn: "user_id", + }}, + } + inner := q.freshSession().Table("books") + + got := q.applyOneOfManyJoin(inner, desc, nil) + stmt := got.Session(&gormio.Session{DryRun: true}).Find(&[]map[string]any{}) + sql := stmt.Statement.SQL.String() + assert.NotContains(t, strings.ToUpper(sql), "INNER JOIN (") +} + +func TestQuery_OfManyShortcuts(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + + latest := q.LatestOfMany().(*Query) + assert.NotNil(t, latest.conditions.oneOfMany) + assert.Equal(t, "MAX", latest.conditions.oneOfMany.aggregate) + assert.Equal(t, "id", latest.conditions.oneOfMany.column) + + oldest := q.OldestOfMany("created_at").(*Query) + assert.NotNil(t, oldest.conditions.oneOfMany) + assert.Equal(t, "MIN", oldest.conditions.oneOfMany.aggregate) + assert.Equal(t, "created_at", oldest.conditions.oneOfMany.column) + + custom := q.OfMany("score", "AVG").(*Query) + assert.Equal(t, "AVG", custom.conditions.oneOfMany.aggregate) + assert.Equal(t, "score", custom.conditions.oneOfMany.column) +} diff --git a/database/gorm/query.go b/database/gorm/query.go index 9bb020634..2acd263bf 100644 --- a/database/gorm/query.go +++ b/database/gorm/query.go @@ -94,12 +94,6 @@ func BuildQuery(ctx context.Context, config config.Config, connection string, lo return NewQuery(ctx, config, pool.Writers[0], gorm, driver.Grammar(), log, modelToObserver, nil), pool.Writers[0], nil } -func (r *Query) Association(association string) contractsorm.Association { - query := r.buildConditions() - - return query.instance.Association(association) -} - // DEPRECATED Use BeginTransaction instead. func (r *Query) Begin() (contractsorm.Query, error) { return r.BeginTransaction() @@ -156,9 +150,7 @@ func (r *Query) Create(value any) error { } func (r *Query) Cursor() chan contractsdb.Row { - with := r.conditions.with query := r.addGlobalScopes().buildConditions() - r.conditions.with = with cursorChan := make(chan contractsdb.Row) go func() { @@ -464,6 +456,34 @@ func (r *Query) Limit(limit int) contractsorm.Query { return r.setConditions(conditions) } +func (r *Query) OfMany(column, aggregate string) contractsorm.Query { + if column == "" { + column = "id" + } + if aggregate == "" { + aggregate = "MAX" + } + conditions := r.conditions + conditions.oneOfMany = &oneOfManyConfig{column: column, aggregate: aggregate} + return r.setConditions(conditions) +} + +func (r *Query) LatestOfMany(column ...string) contractsorm.Query { + col := "" + if len(column) > 0 { + col = column[0] + } + return r.OfMany(col, "MAX") +} + +func (r *Query) OldestOfMany(column ...string) contractsorm.Query { + col := "" + if len(column) > 0 { + col = column[0] + } + return r.OfMany(col, "MIN") +} + func (r *Query) Load(model any, relation string, args ...any) error { if relation == "" { return errors.OrmQueryEmptyRelation @@ -479,7 +499,7 @@ func (r *Query) Load(model any, relation string, args ...any) error { } copyDest := copyStruct(model) - err := r.With(relation, args...).Find(model) + err := r.With(append([]any{relation}, args...)...).Find(model) relationRoot := relation if dotIndex := strings.Index(relation, "."); dotIndex > 0 { @@ -1152,16 +1172,6 @@ func (r *Query) WhereNull(column string) contractsorm.Query { return r.Where(fmt.Sprintf("%s IS NULL", column)) } -func (r *Query) With(query string, args ...any) contractsorm.Query { - conditions := r.conditions - conditions.with = deep.Append(r.conditions.with, With{ - query: query, - args: args, - }) - - return r.setConditions(conditions) -} - func (r *Query) WithoutEvents() contractsorm.Query { conditions := r.conditions conditions.withoutEvents = true @@ -1260,11 +1270,12 @@ func (r *Query) buildConditions() *Query { db = query.buildOmit(db) db = query.buildScopes(db) db = query.buildSelectColumns(db) + db = query.buildSelectSubAggregates(db) db = query.buildSharedLock(db) db = query.buildTable(db) - db = query.buildWith(db) db = query.buildWithTrashed(db) db = query.buildWhere(db) + db = query.buildRelations(db) return query.new(db) } @@ -1537,41 +1548,6 @@ func (r *Query) buildWherePlaceholder(query string, args ...any) string { return query } -func (r *Query) buildWith(db *gormio.DB) *gormio.DB { - if len(r.conditions.with) == 0 { - return db - } - - for _, item := range r.conditions.with { - isSet := false - if len(item.args) == 1 { - if arg, ok := item.args[0].(func(contractsorm.Query) contractsorm.Query); ok { - newArgs := []any{ - func(tx *gormio.DB) *gormio.DB { - queryImpl := NewQuery(r.ctx, r.config, r.dbConfig, tx, r.grammar, r.log, r.modelToObserver, nil) - query := arg(queryImpl) - queryImpl = query.(*Query) - queryImpl = queryImpl.buildConditions() - - return queryImpl.instance - }, - } - - db = db.Preload(item.query, newArgs...) - isSet = true - } - } - - if !isSet { - db = db.Preload(item.query, item.args...) - } - } - - r.conditions.with = nil - - return db -} - func (r *Query) buildWithTrashed(db *gormio.DB) *gormio.DB { if !r.conditions.withTrashed { return db @@ -1834,6 +1810,9 @@ func (r *Query) restoring(dest any) error { } func (r *Query) retrieved(dest any) error { + if err := r.applyEagerLoads(dest); err != nil { + return err + } if isSlice(dest) { return nil } diff --git a/database/gorm/row.go b/database/gorm/row.go index b9b954c72..071b6bb61 100644 --- a/database/gorm/row.go +++ b/database/gorm/row.go @@ -20,15 +20,16 @@ func (r *Row) Scan(value any) error { return err } - for _, item := range r.query.conditions.with { - // Need to new a query, avoid to clear the conditions - query := r.query.new(r.query.instance) - // The new query must be cleared - query.clearConditions() - if err := query.Load(value, item.query, item.args...); err != nil { - return err - } + if len(r.query.conditions.eagerLoad) == 0 { + return nil } - return nil + // Per-row eager loading for the Cursor() path. applyEagerLoads consumes its own slice + // (sets it to nil after running), so we copy into a fresh query so subsequent rows in the + // cursor still see the queued entries. + query := r.query.new(r.query.instance) + query.clearConditions() + query.conditions.eagerLoad = append([]eagerLoadEntry(nil), r.query.conditions.eagerLoad...) + + return query.applyEagerLoads(value) } From 8607ed4f254d70caa853c60e620496abb8f6b7eb Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:24:04 +0800 Subject: [PATCH 04/11] feat(orm): add relation resolver and Has/HasMorph existence queries 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 611d2ca5a8c5ba383746122926147969a95ab290) --- database/gorm/build_relations_test.go | 298 +++++++ database/gorm/new_relation.go | 243 +++++ database/gorm/new_relation_test.go | 198 +++++ database/gorm/queries_relationships.go | 927 ++++++++++++++++++++ database/gorm/queries_relationships_test.go | 419 +++++++++ database/gorm/relation.go | 675 ++++++++++++++ database/gorm/relation_test.go | 440 ++++++++++ 7 files changed, 3200 insertions(+) create mode 100644 database/gorm/build_relations_test.go create mode 100644 database/gorm/new_relation.go create mode 100644 database/gorm/new_relation_test.go create mode 100644 database/gorm/queries_relationships.go create mode 100644 database/gorm/queries_relationships_test.go create mode 100644 database/gorm/relation.go create mode 100644 database/gorm/relation_test.go diff --git a/database/gorm/build_relations_test.go b/database/gorm/build_relations_test.go new file mode 100644 index 000000000..9ae779dd6 --- /dev/null +++ b/database/gorm/build_relations_test.go @@ -0,0 +1,298 @@ +package gorm + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" + + contractsdatabase "github.com/goravel/framework/contracts/database" + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +// newRelQueryWith returns a Query whose conditions.model is preset, ready for build/compile tests. +func newRelQueryWith(t *testing.T, model any) *Query { + t.Helper() + db := newStubGormDB(t) + conditions := Conditions{model: model} + return NewQuery(context.Background(), nil, contractsdatabase.Config{}, db, nil, nil, nil, &conditions) +} + +func dryRunSQL(t *testing.T, db *gormio.DB) string { + t.Helper() + stmt := db.Session(&gormio.Session{DryRun: true}).Find(&relUser{}) + return stmt.Statement.SQL.String() +} + +// --- buildRelations ------------------------------------------------------- + +func TestBuildRelations_NoOps(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + got := q.buildRelations(q.instance) + assert.Same(t, q.instance, got) +} + +func TestBuildRelations_NoParent(t *testing.T) { + q := newRelQuery(t) + q.conditions.relations = []relationExistence{{relation: "Books", operator: ">=", count: 1, conjunction: "and"}} + got := q.buildRelations(q.instance) + assert.True(t, errors.Is(got.Error, errors.OrmQueryEmptyRelation)) +} + +func TestBuildRelations_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{relation: "Books", operator: ">=", count: 1, conjunction: "and"}} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "EXISTS") + assert.Contains(t, sql, "rel_books") +} + +func TestBuildRelations_DoesntHaveBuildsNotExists(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{relation: "Books", operator: "<", count: 1, conjunction: "and"}} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "NOT EXISTS") +} + +func TestBuildRelations_CountComparison(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{relation: "Books", operator: ">", count: 3, conjunction: "and"}} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + // when not the EXISTS-eligible shape, build a (?) > ? clause instead + assert.Contains(t, sql, "COUNT(*)") +} + +func TestBuildRelations_ResolveError(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{relation: "Missing", operator: ">=", count: 1, conjunction: "and"}} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + assert.True(t, errors.Is(out.Error, errors.OrmRelationNotFound)) +} + +func TestBuildRelations_OrConjunction(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{ + {relation: "Books", operator: ">=", count: 1, conjunction: "and"}, + {relation: "Roles", operator: ">=", count: 1, conjunction: "or"}, + } + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "OR") +} + +// --- compileExistenceSubquery (covers HasOne/HasMany/BelongsTo/M2M/Morph/Through SQL) --- + +func TestCompileExistenceSubquery_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relBook{}) + sql := stmt.Statement.SQL.String() + assert.True(t, strings.Contains(sql, "rel_books")) +} + +func TestCompileExistenceSubquery_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + desc, err := resolveRelation(q.instance, &relBook{}, "Author") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + assert.NotNil(t, inner) +} + +func TestCompileExistenceSubquery_Many2Many(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relRole{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "rel_user_roles") +} + +func TestCompileExistenceSubquery_Morph(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Houses") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + assert.NotNil(t, inner) +} + +func TestCompileExistenceSubquery_Through(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + desc, err := resolveRelation(q.instance, &relCountry{}, "Posts") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relPost{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "rel_users") // through table +} + +func TestCompileExistenceSubquery_WithCallback(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + cb := contractsorm.RelationCallback(func(qq contractsorm.Query) contractsorm.Query { + return qq.Where("title = ?", "x") + }) + inner := q.compileExistenceSubquery(desc, cb) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relBook{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "title") +} + +func TestCompileExistenceSubquery_NestedRelation(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books.Author") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + assert.NotNil(t, inner) +} + +// --- compileAggregateSubquery --------------------------------------------- + +func TestCompileAggregateSubquery_Count(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + sub := selectSub{relation: "Books", column: "*", function: "count"} + inner := q.compileAggregateSubquery(desc, sub) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relBook{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "COUNT(*)") +} + +func TestCompileAggregateSubquery_Sum(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + sub := selectSub{relation: "Books", column: "id", function: "sum"} + inner := q.compileAggregateSubquery(desc, sub) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relBook{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "SUM(") +} + +func TestCompileAggregateSubquery_Exists(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + sub := selectSub{relation: "Books", column: "*", function: "exists"} + inner := q.compileAggregateSubquery(desc, sub) + assert.NotNil(t, inner) +} + +// --- buildSelectSubAggregates --------------------------------------------- + +func TestBuildSelectSubAggregates_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + got := q.buildSelectSubAggregates(q.instance) + assert.Same(t, q.instance, got) +} + +func TestBuildSelectSubAggregates_NoParent(t *testing.T) { + q := newRelQuery(t) + q.conditions.selectSubs = []selectSub{{relation: "Books", column: "*", function: "count", alias: "books_count"}} + got := q.buildSelectSubAggregates(q.instance) + assert.True(t, errors.Is(got.Error, errors.OrmQueryEmptyRelation)) +} + +func TestBuildSelectSubAggregates_Count(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.selectSubs = []selectSub{{relation: "Books", column: "*", function: "count", alias: "books_count"}} + out := q.buildSelectSubAggregates(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "books_count") +} + +func TestBuildSelectSubAggregates_Exists(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.selectSubs = []selectSub{{relation: "Books", column: "*", function: "exists", alias: "has_books"}} + out := q.buildSelectSubAggregates(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "CASE WHEN EXISTS") + assert.Contains(t, sql, "has_books") +} + +func TestBuildSelectSubAggregates_BadRelationRecordsError(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.selectSubs = []selectSub{{relation: "Missing", column: "*", function: "count"}} + out := q.buildSelectSubAggregates(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + assert.Error(t, out.Error) +} + +// --- applyMorphExistence -------------------------------------------------- + +func TestApplyMorphExistence_BuildsSQL(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{ + relation: "Houses", + operator: ">=", + count: 1, + conjunction: "and", + morphTypes: []any{&relUser{}}, + }} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "houseable_type") +} + +func TestApplyMorphExistence_NonMorphRelationRecordsError(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{ + relation: "Books", // hasMany, not morph + operator: ">=", + count: 1, + conjunction: "and", + morphTypes: []any{&relUser{}}, + }} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + assert.Error(t, out.Error) +} + +func TestApplyMorphExistence_OrConjunction(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{ + relation: "Houses", + operator: ">=", + count: 1, + conjunction: "or", + morphTypes: []any{&relUser{}}, + }} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + assert.NotNil(t, out) +} + +func TestApplyMorphExistence_DoesntHave(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{ + relation: "Houses", + operator: "<", + count: 1, + conjunction: "and", + morphTypes: []any{&relUser{}}, + }} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "NOT EXISTS") +} + +func TestApplyMorphExistence_CountComparison(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + q.conditions.relations = []relationExistence{{ + relation: "Houses", + operator: ">", + count: 3, + conjunction: "and", + morphTypes: []any{&relUser{}}, + }} + out := q.buildRelations(q.instance.Session(&gormio.Session{}).Model(&relUser{})) + sql := dryRunSQL(t, out) + assert.Contains(t, sql, "COUNT(*)") +} diff --git a/database/gorm/new_relation.go b/database/gorm/new_relation.go new file mode 100644 index 000000000..4c2091e4b --- /dev/null +++ b/database/gorm/new_relation.go @@ -0,0 +1,243 @@ +package gorm + +import ( + "fmt" + "reflect" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/orm/morphmap" + "github.com/goravel/framework/errors" +) + +// Related is the public Query-level entry point. The Orm-level entry (Orm.Related) delegates +// here. Returns a fresh Query pre-scoped to the related rows for parent.relation. +func (r *Query) Related(parent any, relation string) contractsorm.Query { + return r.newRelationQuery(parent, relation) +} + +// newRelationQuery returns a fresh Query pre-scoped to the related rows for parent.relation. +// Mirrors fedaco's model.NewRelation('foo') for the read path. Caller can chain Where / OrderBy +// / Get / etc. on the returned Query. Write operations live on RelationWriter (see Query.Relation). +// +// Public entry: Orm.Related delegates here on a fresh-session *Query. +func (r *Query) newRelationQuery(parent any, relation string) contractsorm.Query { + if !isValidParent(parent) { + return r.guardedQuery(errors.OrmRelationParentNotPointer.Args(parent)) + } + + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return r.guardedQuery(err) + } + + var built contractsorm.Query + switch desc.kind { + case relKindHasOne, relKindHasMany: + built = r.newHasOneOrManyQuery(parent, desc) + case relKindBelongsTo: + built = r.newBelongsToQuery(parent, desc) + case relKindMorphOne, relKindMorphMany: + built = r.newMorphOneOrManyQuery(parent, desc) + case relKindMorphTo: + built = r.newMorphToQuery(parent, desc) + case relKindMany2Many: + built = r.newMany2ManyQuery(parent, desc, false) + case relKindMorphToMany: + built = r.newMany2ManyQuery(parent, desc, true) + case relKindHasOneThrough, relKindHasManyThrough: + built = r.newThroughQuery(parent, desc) + default: + return r.guardedQuery(errors.OrmRelationUnsupported.Args(relation, fmt.Sprintf("%T", parent), fmt.Sprintf("kind=%d", desc.kind))) + } + // Apply the relation's default scope on top of the per-kind constraints. Caller still gets + // a chainable Query and can layer further conditions via Where / OrderBy / etc. + if desc.onQuery != nil { + built = desc.onQuery(built) + } + return built +} + +// --- per-kind builders ----------------------------------------------------- + +// newHasOneOrManyQuery: SELECT * FROM WHERE . = parent.. +func (r *Query) newHasOneOrManyQuery(parent any, desc *relationDescriptor) contractsorm.Query { + if len(desc.references) == 0 { + return r.guardedQuery(errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, "no references")) + } + ref := desc.references[0] + parentVal, err := readParentColumn(r, parent, ref.primaryColumn) + if err != nil { + return r.guardedQuery(err) + } + return r.relatedQuery(desc.relatedModel).Where(ref.foreignColumn, parentVal) +} + +// newBelongsToQuery: SELECT * FROM WHERE . = parent.. +func (r *Query) newBelongsToQuery(parent any, desc *relationDescriptor) contractsorm.Query { + if len(desc.references) == 0 { + return r.guardedQuery(errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, "no references")) + } + ref := desc.references[0] + parentVal, err := readParentColumn(r, parent, ref.foreignColumn) + if err != nil { + return r.guardedQuery(err) + } + return r.relatedQuery(desc.relatedModel).Where(ref.primaryColumn, parentVal) +} + +// newMorphOneOrManyQuery: HasMany shape + WHERE . = desc.morphValue. +func (r *Query) newMorphOneOrManyQuery(parent any, desc *relationDescriptor) contractsorm.Query { + if len(desc.references) == 0 { + return r.guardedQuery(errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, "no references")) + } + ref := desc.references[0] + parentVal, err := readParentColumn(r, parent, ref.primaryColumn) + if err != nil { + return r.guardedQuery(err) + } + return r.relatedQuery(desc.relatedModel). + Where(ref.foreignColumn, parentVal). + Where(desc.morphTypeColumn, desc.morphValue) +} + +// newMorphToQuery resolves the related model per-row from the parent's via the morph +// map, then issues SELECT * FROM WHERE . = parent.. +// +// If the parent's *_type is empty or unregistered, the returned Query is guarded so subsequent +// Get / First yields no rows without a database round-trip. +func (r *Query) newMorphToQuery(parent any, desc *relationDescriptor) contractsorm.Query { + morphType, err := readParentColumn(r, parent, desc.morphTypeColumn) + if err != nil { + return r.guardedQuery(err) + } + morphID, err := readParentColumn(r, parent, desc.morphIDColumn) + if err != nil { + return r.guardedQuery(err) + } + typeStr := morphTypeToString(morphType) + if typeStr == "" { + return r.zeroRowQuery() + } + sample := morphmap.Find(typeStr) + if sample == nil { + return r.guardedQuery(errors.OrmMorphTypeUnknown.Args(typeStr)) + } + ownerKey := desc.morphOwnerKey + if ownerKey == "" { + ownerKey = "id" + } + return r.relatedQuery(sample).Where(ownerKey, morphID) +} + +// newMany2ManyQuery: SELECT .* FROM INNER JOIN +// +// ON . = . +// WHERE . = parent. [AND . = morphValue] +// +// Pivot columns are not surfaced — call sites that need them should add Select / a future +// WithPivot helper. +func (r *Query) newMany2ManyQuery(parent any, desc *relationDescriptor, isMorph bool) contractsorm.Query { + parentVal, err := readParentColumn(r, parent, desc.pivotParentRef.primaryColumn) + if err != nil { + return r.guardedQuery(err) + } + relatedTable := desc.relatedTable + pivotTable := desc.pivotTable + + q := r.relatedQuery(desc.relatedModel). + Join(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + quoteIdent(pivotTable), + quoteIdent(pivotTable), quoteIdent(desc.pivotRelatedRef.foreignColumn), + quoteIdent(relatedTable), quoteIdent(desc.pivotRelatedRef.primaryColumn))). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn)), parentVal) + if isMorph { + q = q.Where(fmt.Sprintf("%s.%s = ?", quoteIdent(pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + } + return q +} + +// newThroughQuery: +// +// SELECT .* FROM +// INNER JOIN ON . = . +// WHERE . = parent. +func (r *Query) newThroughQuery(parent any, desc *relationDescriptor) contractsorm.Query { + parentVal, err := readParentColumn(r, parent, desc.localKey) + if err != nil { + return r.guardedQuery(err) + } + return r.relatedQuery(desc.relatedModel). + Join(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + quoteIdent(desc.throughTable), + quoteIdent(desc.relatedTable), quoteIdent(desc.secondKey), + quoteIdent(desc.throughTable), quoteIdent(desc.secondLocalKey))). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.throughTable), quoteIdent(desc.firstKey)), parentVal) +} + +// --- helpers --------------------------------------------------------------- + +// relatedQuery returns a fresh Query bound to the given related model, sharing connection / +// config / context with the receiver. Subsequent unqualified Where("col", v) calls default to +// the related table's column space. +func (r *Query) relatedQuery(related any) contractsorm.Query { + q := r.wrap(r.freshSession()) + return q.Model(related) +} + +// guardedQuery returns a Query guarded so subsequent terminals (Get/First/Count/...) immediately +// surface err to the caller. +func (r *Query) guardedQuery(err error) contractsorm.Query { + q := r.wrap(r.freshSession()) + _ = q.instance.AddError(err) + return q +} + +// zeroRowQuery returns a Query whose Get/First yields no rows without a database round-trip — +// the WHERE evaluates to a guaranteed-false condition. +func (r *Query) zeroRowQuery() contractsorm.Query { + return r.wrap(r.freshSession()).Where("1 = 0") +} + +// readParentColumn reads the value of the given DB column from parent via GORM's parsed schema. +func readParentColumn(r *Query, parent any, dbColumn string) (any, error) { + parentSchema, err := parseGormSchema(r.instance, parent) + if err != nil { + return nil, err + } + field, ok := parentSchema.FieldsByDBName[dbColumn] + if !ok { + return nil, errors.OrmRelationUnsupported.Args("", parentSchema.Name, "no parent field for "+dbColumn) + } + rv := reflect.ValueOf(parent) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + val, _ := field.ValueOf(r.ctx, rv) + return val, nil +} + +// morphTypeToString coerces a morph_type column value (which the driver may return as string, +// []byte, or sql.NullString) into a plain string. +func morphTypeToString(v any) string { + switch x := v.(type) { + case nil: + return "" + case string: + return x + case []byte: + return string(x) + } + return fmt.Sprint(v) +} + +// isValidParent returns true if v is a non-nil pointer to a struct. +func isValidParent(v any) bool { + if v == nil { + return false + } + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return false + } + return rv.Elem().Kind() == reflect.Struct +} diff --git a/database/gorm/new_relation_test.go b/database/gorm/new_relation_test.go new file mode 100644 index 000000000..ef50885bc --- /dev/null +++ b/database/gorm/new_relation_test.go @@ -0,0 +1,198 @@ +package gorm + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/orm/morphmap" + "github.com/goravel/framework/errors" +) + +// dryRunFind wraps the inner *gormio.DB pulled out of a Goravel Query, runs Find in DryRun mode, +// and returns the resulting SQL string. Used to verify the WHERE / JOIN shape produced by +// Related per kind. +func newRelationSQL(t *testing.T, q contractsorm.Query, dest any) string { + t.Helper() + gq, ok := q.(*Query) + if !ok { + t.Fatalf("expected *gorm.Query, got %T", q) + } + stmt := gq.buildConditions().instance.Session(&gormio.Session{DryRun: true}).Find(dest) + return stmt.Statement.SQL.String() +} + +func TestRelated_HasMany_Where(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 7}, "Books") + sql := newRelationSQL(t, rel, &[]relBook{}) + assert.Contains(t, sql, "rel_books") + assert.Contains(t, sql, "user_id") +} + +func TestRelated_BelongsTo_Where(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + rel := q.Related(&relBook{AuthorID: 5}, "Author") + sql := newRelationSQL(t, rel, &[]relUser{}) + assert.Contains(t, sql, "rel_users") + // BelongsTo: WHERE related. = parent. + assert.Contains(t, strings.ToLower(sql), "id") +} + +func TestRelated_MorphMany_AddsTypeFilter(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 9}, "Houses") + sql := newRelationSQL(t, rel, &[]relHouse{}) + assert.Contains(t, sql, "houseable_id") + assert.Contains(t, sql, "houseable_type") +} + +func TestRelated_MorphOne_AddsTypeFilter(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 9}, "Logo") + sql := newRelationSQL(t, rel, &relLogo{}) + assert.Contains(t, sql, "logoable_id") + assert.Contains(t, sql, "logoable_type") +} + +func TestRelated_HasManyThrough(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + rel := q.Related(&relCountry{ID: 1}, "Posts") + sql := newRelationSQL(t, rel, &[]relPost{}) + assert.Contains(t, sql, "rel_posts") + assert.Contains(t, sql, "INNER JOIN") + assert.Contains(t, sql, "rel_users") +} + +func TestRelated_NotPointer(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(relUser{ID: 1}, "Books") // value, not pointer + gq, ok := rel.(*Query) + assert.True(t, ok) + assert.True(t, errors.Is(gq.instance.Error, errors.OrmRelationParentNotPointer)) +} + +func TestRelated_NilParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(nil, "Books") + gq, ok := rel.(*Query) + assert.True(t, ok) + assert.True(t, errors.Is(gq.instance.Error, errors.OrmRelationParentNotPointer)) +} + +func TestRelated_RelationNotFound(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 1}, "DoesNotExist") + gq, ok := rel.(*Query) + assert.True(t, ok) + assert.True(t, errors.Is(gq.instance.Error, errors.OrmRelationNotFound)) +} + +// --- MorphTo -------------------------------------------------------------- + +// morphParentLikePost is a sample model registered in the morph map for MorphTo tests. +type morphParentLikePost struct { + ID uint + Title string +} + +func (morphParentLikePost) TableName() string { return "morph_parent_like_posts" } + +func TestRelated_MorphTo_ResolvedViaMorphMap(t *testing.T) { + morphmap.Reset() + defer morphmap.Reset() + morphmap.Register(map[string]any{"post": &morphParentLikePost{}}) + + q := newRelQueryWith(t, &morphImage{}) + rel := q.Related(&morphImage{ID: 1, ImageableID: 42, ImageableType: "post"}, "Imageable") + sql := newRelationSQL(t, rel, &morphParentLikePost{}) + assert.Contains(t, sql, "morph_parent_like_posts") + assert.Contains(t, sql, "id") +} + +func TestRelated_MorphTo_UnregisteredType(t *testing.T) { + morphmap.Reset() + defer morphmap.Reset() + + q := newRelQueryWith(t, &morphImage{}) + rel := q.Related(&morphImage{ID: 1, ImageableID: 42, ImageableType: "unknown"}, "Imageable") + gq, ok := rel.(*Query) + assert.True(t, ok) + assert.True(t, errors.Is(gq.instance.Error, errors.OrmMorphTypeUnknown)) +} + +func TestRelated_MorphTo_EmptyType_YieldsZeroRowQuery(t *testing.T) { + morphmap.Reset() + defer morphmap.Reset() + + q := newRelQueryWith(t, &morphImage{}) + rel := q.Related(&morphImage{ID: 1, ImageableID: 0, ImageableType: ""}, "Imageable") + gq, ok := rel.(*Query) + assert.True(t, ok) + // Apply conditions and run Find against an arbitrary table to verify the WHERE renders. + stmt := gq.buildConditions().instance.Table("any_table").Session(&gormio.Session{DryRun: true}).Find(&[]map[string]any{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "1 = 0") +} + +// --- MorphToMany --------------------------------------------------------- + +func TestRelated_MorphToMany_AddsPivotTypeFilter(t *testing.T) { + q := newRelQueryWith(t, &morphPost{}) + rel := q.Related(&morphPost{ID: 3}, "Tags") + sql := newRelationSQL(t, rel, &[]morphTag{}) + assert.Contains(t, sql, "INNER JOIN") + assert.Contains(t, sql, "taggables") + assert.Contains(t, sql, "taggable_type") +} + +// --- OnQuery hook ---------------------------------------------------------- + +// scopedUser declares a HasMany whose OnQuery scope filters out unpublished books. Every code +// path that builds a query for this relation (Related, eager load, existence) must apply +// the scope. +type scopedUser struct { + ID uint + Books []*scopedBook `gorm:"-"` +} + +func (scopedUser) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Books": contractsorm.HasMany{ + Related: &scopedBook{}, + ForeignKey: "user_id", + OnQuery: func(q contractsorm.Query) contractsorm.Query { + return q.Where("published", true) + }, + }, + } +} + +type scopedBook struct { + ID uint + UserID uint + Title string + Published bool +} + +func TestRelated_OnQuery_AppliedToReturnedQuery(t *testing.T) { + q := newRelQueryWith(t, &scopedUser{}) + rel := q.Related(&scopedUser{ID: 5}, "Books") + sql := newRelationSQL(t, rel, &[]scopedBook{}) + // The generated WHERE must include both the FK constraint and the OnQuery's published=true. + assert.Contains(t, sql, "user_id") + assert.Contains(t, sql, "published") +} + +func TestCompileExistenceSubquery_OnQuery_AppliedInExistenceCheck(t *testing.T) { + q := newRelQueryWith(t, &scopedUser{}) + desc, err := resolveRelation(q.instance, &scopedUser{}, "Books") + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&[]scopedBook{}) + sql := stmt.Statement.SQL.String() + assert.Contains(t, sql, "published") +} diff --git a/database/gorm/queries_relationships.go b/database/gorm/queries_relationships.go new file mode 100644 index 000000000..511998691 --- /dev/null +++ b/database/gorm/queries_relationships.go @@ -0,0 +1,927 @@ +// Package gorm contains the GORM-backed implementation of the framework's database/orm contracts. +// +// Where the upstream framework has first-class Relation objects with `getRelationExistenceQuery` / +// `getRelationExistenceCountQuery` methods, GORM models its relationships through struct-tag +// metadata. The two are bridged here by relation.go's resolver: it inspects gorm.Schema and a +// model's optional ModelWithThroughRelations declaration, then produces a relationDescriptor +// that knows how to emit a correlated EXISTS / count subquery for any of HasOne, HasMany, +// BelongsTo, BelongsToMany, MorphOne, MorphMany, HasOneThrough or HasManyThrough. +package gorm + +import ( + "fmt" + "strings" + + gormio "gorm.io/gorm" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/deep" + "github.com/goravel/framework/support/str" +) + +// --------------------------------------------------------------------------- +// Public API: existence (has / whereHas / doesntHave / orWhereDoesntHave / ...) +// +// Each method appends a relationExistence to conditions.relations; the actual subquery is built +// when buildConditions runs (so the parent model from .Model() / dest can be resolved). +// --------------------------------------------------------------------------- + +// Has adds a relationship count / exists condition to the query. +// +// args may include any combination of a RelationCallback (or func(Query) Query) for scoping the +// inner subquery, a string operator (defaults to ">="), and an int count (defaults to 1). For +// nested relations dot-notation is honoured: `Has("Books.Author")` is shorthand for +// `WhereHas("Books", q -> q.Has("Author"))`. +func (r *Query) Has(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "and", false) +} + +// OrHas adds a relationship count / exists condition to the query with an "or" conjunction. +func (r *Query) OrHas(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "or", false) +} + +// DoesntHave adds a relationship absence condition to the query. +// Equivalent to Has(rel, "<", 1). +func (r *Query) DoesntHave(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "and", true) +} + +// OrDoesntHave adds a relationship absence condition with an "or" conjunction. +func (r *Query) OrDoesntHave(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "or", true) +} + +// WhereHas adds a relationship count / exists condition to the query with where clauses. +// Functionally identical to Has - the rename merely makes call sites that always pass a callback +// read more naturally. +func (r *Query) WhereHas(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "and", false) +} + +// OrWhereHas adds a relationship count / exists condition with where clauses and an "or" +// conjunction. +func (r *Query) OrWhereHas(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "or", false) +} + +// WhereDoesntHave adds a relationship absence condition to the query with where clauses. +func (r *Query) WhereDoesntHave(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "and", true) +} + +// OrWhereDoesntHave adds a relationship absence condition with where clauses and an "or" +// conjunction. +func (r *Query) OrWhereDoesntHave(relation string, args ...any) contractsorm.Query { + return r.queueRelationExistence(relation, args, "or", true) +} + +// HasMorph adds a polymorphic relationship count / exists condition to the query. +// types is a slice of model instances; the morph value used in the polymorphic type column is +// derived from each model's GORM-resolved table name. +// +// Note: auto-discovery of distinct morph values via `types = ['*']` is not supported; an explicit +// list of model instances is required. +func (r *Query) HasMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "and", false) +} + +// OrHasMorph adds a polymorphic relationship count / exists condition with an "or" conjunction. +func (r *Query) OrHasMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "or", false) +} + +// DoesntHaveMorph adds a polymorphic relationship absence condition. +func (r *Query) DoesntHaveMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "and", true) +} + +// OrDoesntHaveMorph adds a polymorphic relationship absence condition with an "or" conjunction. +func (r *Query) OrDoesntHaveMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "or", true) +} + +// WhereHasMorph adds a polymorphic existence condition with where clauses; callbacks may be +// MorphRelationCallback for per-type scoping. +func (r *Query) WhereHasMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "and", false) +} + +// OrWhereHasMorph adds a polymorphic existence condition with where clauses and an "or" +// conjunction. +func (r *Query) OrWhereHasMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "or", false) +} + +// WhereDoesntHaveMorph adds a polymorphic absence condition with where clauses. +func (r *Query) WhereDoesntHaveMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "and", true) +} + +// OrWhereDoesntHaveMorph adds a polymorphic absence condition with where clauses and an "or" +// conjunction. +func (r *Query) OrWhereDoesntHaveMorph(relation string, types []any, args ...any) contractsorm.Query { + return r.queueMorphExistence(relation, types, args, "or", true) +} + +// --------------------------------------------------------------------------- +// Public API: aggregate sub-selects (withCount / withMax / withSum / ...) +// +// Each method appends a selectSub to conditions.selectSubs; the actual sub-select column is +// emitted when buildConditions runs (so the parent model can be resolved and the alias derived). +// --------------------------------------------------------------------------- + +// WithAggregate adds a sub-select to include an aggregate value for a relationship. +// fn must be one of: count, max, min, sum, avg, exists. +func (r *Query) WithAggregate(relation, column, fn string, args ...any) contractsorm.Query { + if !validAggregateFn(fn) { + query := r.new(r.instance.Session(&gormio.Session{})) + _ = query.instance.AddError(errors.OrmRelationInvalidAggregate.Args(fn)) + return query + } + cb, _, _, err := parseRelationArgs(args) + if err != nil { + query := r.new(r.instance.Session(&gormio.Session{})) + _ = query.instance.AddError(err) + return query + } + conditions := r.conditions + conditions.selectSubs = deep.Append(conditions.selectSubs, selectSub{ + relation: relation, + column: column, + function: fn, + alias: aggregateAlias(relation, fn, column), + callback: cb, + }) + return r.setConditions(conditions) +} + +// WithCount adds sub-select queries to count the relations. Each entry may be either: +// - a string ("Books") - emits `(SELECT COUNT(*) FROM ...) AS books_count` +// - a contractsorm.RelationCount struct for scoped counts and/or custom alias +func (r *Query) WithCount(relations ...any) contractsorm.Query { + current := r + for _, raw := range relations { + switch v := raw.(type) { + case string: + next, ok := current.WithAggregate(v, "*", "count").(*Query) + if !ok { + return current + } + current = next + case contractsorm.RelationCount: + args := []any{} + if v.Callback != nil { + args = append(args, v.Callback) + } + next, ok := current.WithAggregate(v.Name, "*", "count", args...).(*Query) + if !ok { + return current + } + if v.Alias != "" { + if n := len(next.conditions.selectSubs); n > 0 { + next.conditions.selectSubs[n-1].alias = v.Alias + } + } + current = next + default: + impl := r.new(r.instance.Session(&gormio.Session{})) + _ = impl.instance.AddError(errors.OrmRelationInvalidArgument.Args(raw)) + return impl + } + } + return current +} + +// WithMax adds sub-select queries to include the max of the relation's column. +func (r *Query) WithMax(relation, column string, args ...any) contractsorm.Query { + return r.WithAggregate(relation, column, "max", args...) +} + +// WithMin adds sub-select queries to include the min of the relation's column. +func (r *Query) WithMin(relation, column string, args ...any) contractsorm.Query { + return r.WithAggregate(relation, column, "min", args...) +} + +// WithSum adds sub-select queries to include the sum of the relation's column. +func (r *Query) WithSum(relation, column string, args ...any) contractsorm.Query { + return r.WithAggregate(relation, column, "sum", args...) +} + +// WithAvg adds sub-select queries to include the average of the relation's column. +func (r *Query) WithAvg(relation, column string, args ...any) contractsorm.Query { + return r.WithAggregate(relation, column, "avg", args...) +} + +// WithExists adds sub-select queries to include the existence of related models. The result is +// emitted as `CASE WHEN EXISTS (...) THEN 1 ELSE 0 END` for cross-dialect portability (SQL Server +// has no boolean literal). The dest field may be either `bool` or an integer type - Go's +// database/sql layer converts 0/1 ints to bool automatically. +func (r *Query) WithExists(relations ...string) contractsorm.Query { + current := r + for _, rel := range relations { + next, ok := current.WithAggregate(rel, "*", "exists").(*Query) + if !ok { + return current + } + current = next + } + return current +} + +// --------------------------------------------------------------------------- +// Public API: eager loading (With / Without / WithOnly) +// +// These methods build up conditions.eagerLoad. The actual loader runs after the main query +// returns; see eager_loader.go for the execution side. +// --------------------------------------------------------------------------- + +// With eagerly loads the given relationships using Goravel's own loader. Accepts the +// union of fedaco's with(...) shapes; see the orm.Query interface comment for the full grammar. +func (r *Query) With(args ...any) contractsorm.Query { + entries, err := parseEagerLoad(args) + if err != nil { + query := r.new(r.instance.Session(&gormio.Session{})) + _ = query.instance.AddError(err) + return query + } + conditions := r.conditions + for _, e := range entries { + conditions.eagerLoad = upsertEagerLoadEntry(conditions.eagerLoad, e, e.callback != nil || e.columns != nil) + } + return r.setConditions(conditions) +} + +// Without removes the named relations from the eager-load list. Mirrors fedaco's +// without(). Names must match exactly (including dot-paths, e.g. "Books.Author"). +func (r *Query) Without(relations ...string) contractsorm.Query { + if len(relations) == 0 || len(r.conditions.eagerLoad) == 0 { + return r + } + drop := make(map[string]struct{}, len(relations)) + for _, n := range relations { + drop[n] = struct{}{} + } + conditions := r.conditions + filtered := make([]eagerLoadEntry, 0, len(conditions.eagerLoad)) + for _, e := range conditions.eagerLoad { + if _, omit := drop[e.relation]; omit { + continue + } + filtered = append(filtered, e) + } + conditions.eagerLoad = filtered + return r.setConditions(conditions) +} + +// WithOnly clears the eager-load list, then adds the given relations. Mirrors fedaco's +// withOnly(). Useful when a default-scoped query has eager loads you want to override. +func (r *Query) WithOnly(args ...any) contractsorm.Query { + conditions := r.conditions + conditions.eagerLoad = nil + return r.setConditions(conditions).With(args...) +} + +// --------------------------------------------------------------------------- +// Internal: queueing +// --------------------------------------------------------------------------- + +func (r *Query) queueRelationExistence(relation string, args []any, conjunction string, doesntHave bool) contractsorm.Query { + cb, op, count, err := parseRelationArgs(args) + if err != nil { + query := r.new(r.instance.Session(&gormio.Session{})) + _ = query.instance.AddError(err) + return query + } + if doesntHave { + op = "<" + count = 1 + } + conditions := r.conditions + conditions.relations = deep.Append(conditions.relations, relationExistence{ + relation: relation, + operator: op, + count: count, + conjunction: conjunction, + callback: cb, + }) + return r.setConditions(conditions) +} + +func (r *Query) queueMorphExistence(relation string, types []any, args []any, conjunction string, doesntHave bool) contractsorm.Query { + if len(types) == 0 { + query := r.new(r.instance.Session(&gormio.Session{})) + _ = query.instance.AddError(errors.OrmRelationMorphTypesEmpty) + return query + } + cb, mcb, op, count, err := parseMorphRelationArgs(args) + if err != nil { + query := r.new(r.instance.Session(&gormio.Session{})) + _ = query.instance.AddError(err) + return query + } + if doesntHave { + op = "<" + count = 1 + } + conditions := r.conditions + conditions.relations = deep.Append(conditions.relations, relationExistence{ + relation: relation, + operator: op, + count: count, + conjunction: conjunction, + callback: cb, + morphTypes: types, + morphCallback: mcb, + }) + return r.setConditions(conditions) +} + +// --------------------------------------------------------------------------- +// Internal: build phase (called from buildConditions) +// --------------------------------------------------------------------------- + +// buildRelations compiles all queued relation existence conditions into the outer GORM query. +// Resolves each relation against the parent model now that one of conditions.model / conditions.dest +// is set. +func (r *Query) buildRelations(db *gormio.DB) *gormio.DB { + if len(r.conditions.relations) == 0 { + return db + } + parent := r.parentModel() + if parent == nil { + _ = db.AddError(errors.OrmQueryEmptyRelation) + return db + } + + for _, item := range r.conditions.relations { + if len(item.morphTypes) > 0 { + db = r.applyMorphExistence(db, parent, item) + } else { + db = r.applyExistence(db, parent, item) + } + } + r.conditions.relations = nil + return db +} + +// buildSelectSubAggregates compiles WithCount / WithSum / WithExists / etc. as sub-select +// columns. GORM's Select() overwrites prior selections, so we coalesce the parent's existing +// columns (or a default `.*`) with all sub-select expressions and emit a single +// Select() containing the full list of column expressions plus the inner subqueries as bindings. +func (r *Query) buildSelectSubAggregates(db *gormio.DB) *gormio.DB { + if len(r.conditions.selectSubs) == 0 { + return db + } + parent := r.parentModel() + if parent == nil { + _ = db.AddError(errors.OrmQueryEmptyRelation) + return db + } + parentTable := r.parentTable(parent) + + // Start with the columns the user already requested (via prior .Select() calls). If they + // didn't request anything, we project the parent's wildcard so the row can still be scanned + // into the dest model. + existing := append([]string{}, db.Statement.Selects...) + if len(existing) == 0 { + existing = []string{fmt.Sprintf("%s.*", quoteIdent(parentTable))} + } + + var subExprs []string + var subArgs []any + for _, sub := range r.conditions.selectSubs { + desc, err := resolveRelation(r.instance, parent, sub.relation) + if err != nil { + _ = db.AddError(err) + continue + } + inner := r.compileAggregateSubquery(desc, sub) + if inner == nil { + continue + } + alias := sub.alias + if alias == "" { + alias = aggregateAlias(sub.relation, sub.function, sub.column) + } + if sub.function == "exists" { + // Use CASE WHEN EXISTS instead of bare `EXISTS (...) AS col`: PostgreSQL returns + // EXISTS as a native bool which won't scan into integer struct fields, and SQL Server + // rejects EXISTS as a column expression entirely. CASE WHEN yields a portable 0/1 int + // across SQLite / MySQL / PostgreSQL / SQL Server. + subExprs = append(subExprs, fmt.Sprintf("CASE WHEN EXISTS (?) THEN 1 ELSE 0 END AS %s", quoteIdent(alias))) + } else { + subExprs = append(subExprs, fmt.Sprintf("(?) AS %s", quoteIdent(alias))) + } + subArgs = append(subArgs, inner) + } + if len(subExprs) == 0 { + r.conditions.selectSubs = nil + return db + } + full := strings.Join(append(existing, subExprs...), ", ") + db = db.Select(full, subArgs...) + r.conditions.selectSubs = nil + return db +} + +// --------------------------------------------------------------------------- +// Internal: existence subquery construction +// --------------------------------------------------------------------------- + +func (r *Query) applyExistence(db *gormio.DB, parent any, item relationExistence) *gormio.DB { + desc, err := resolveRelation(r.instance, parent, item.relation) + if err != nil { + _ = db.AddError(err) + return db + } + inner := r.compileExistenceSubquery(desc, item.callback) + if inner == nil { + return db + } + return r.attachHasWhere(db, inner, item.operator, item.count, item.conjunction) +} + +func (r *Query) applyMorphExistence(db *gormio.DB, parent any, item relationExistence) *gormio.DB { + desc, err := resolveRelation(r.instance, parent, item.relation) + if err != nil { + _ = db.AddError(err) + return db + } + switch desc.kind { + case relKindMorphOne, relKindMorphMany: + return r.applyOutboundMorphExistence(db, desc, item) + case relKindMorphTo: + return r.applyMorphToExistence(db, desc, item) + default: + _ = db.AddError(errors.OrmRelationUnsupported.Args(item.relation, fmt.Sprintf("%T", parent), "morph (must be polymorphic relation)")) + return db + } +} + +// applyOutboundMorphExistence builds the morph-existence clauses for outbound MorphOne / +// MorphMany. The morph_type column lives on the *related* table (e.g. houses.houseable_type), so +// for each requested type we build a correlated EXISTS subquery whose inner WHERE pins +// houses.houseable_type to that type's morph value. Multiple types are joined with OR. +func (r *Query) applyOutboundMorphExistence(db *gormio.DB, desc *relationDescriptor, item relationExistence) *gormio.DB { + sub := r.freshSession() + first := true + for _, typeModel := range item.morphTypes { + morphValue, terr := tableNameFor(r.instance, typeModel) + if terr != nil { + _ = db.AddError(terr) + continue + } + morphValue = resolveMorphValue(typeModel, morphValue) + + var perTypeCallback contractsorm.RelationCallback + if item.morphCallback != nil { + cb := item.morphCallback + captured := morphValue + perTypeCallback = func(q contractsorm.Query) contractsorm.Query { + return cb(q, captured) + } + } else { + perTypeCallback = item.callback + } + inner := r.compileMorphExistenceSubquery(desc, morphValue, perTypeCallback) + if inner == nil { + continue + } + + var clauseSQL string + var clauseArgs []any + if shouldUseExists(item.operator, item.count) { + negate := item.operator == "<" && item.count == 1 + if negate { + clauseSQL = "NOT EXISTS (?)" + } else { + clauseSQL = "EXISTS (?)" + } + clauseArgs = []any{inner} + } else { + countInner := inner.Select("COUNT(*)") + clauseSQL = fmt.Sprintf("(?) %s ?", item.operator) + clauseArgs = []any{countInner, item.count} + } + + if first { + sub = sub.Where(clauseSQL, clauseArgs...) + first = false + } else { + sub = sub.Or(clauseSQL, clauseArgs...) + } + } + if first { + return db + } + + if item.conjunction == "or" { + return db.Or(sub) + } + return db.Where(sub) +} + +// applyMorphToExistence builds the inverse-polymorphic existence clauses. Mirrors fedaco's +// hasMorph at libs/fedaco/src/fedaco/mixins/queries-relationships.ts:320-378: for each requested +// type, we synthesise a BelongsTo-shaped subquery against that type's table, and AND it with a +// type filter on the parent's morph_type column. The per-type clauses are OR-ed together. +// +// Generated SQL pattern: +// +// WHERE ( +// (parents.imageable_type = 'post' AND ((SELECT count(*) FROM posts WHERE posts.id = parents.imageable_id) >= N)) +// OR (parents.imageable_type = 'video' AND ((SELECT count(*) FROM videos WHERE videos.id = parents.imageable_id) >= N)) +// ) +func (r *Query) applyMorphToExistence(db *gormio.DB, desc *relationDescriptor, item relationExistence) *gormio.DB { + sub := r.freshSession() + first := true + ownerKey := desc.morphOwnerKey + if ownerKey == "" { + ownerKey = "id" + } + + for _, typeModel := range item.morphTypes { + relatedTable, terr := tableNameFor(r.instance, typeModel) + if terr != nil { + _ = db.AddError(terr) + continue + } + morphValue := resolveMorphValue(typeModel, relatedTable) + + // Per-type callback resolution (same shape as outbound morph existence). + var perTypeCallback contractsorm.RelationCallback + if item.morphCallback != nil { + cb := item.morphCallback + captured := morphValue + perTypeCallback = func(q contractsorm.Query) contractsorm.Query { + return cb(q, captured) + } + } else { + perTypeCallback = item.callback + } + + // Build the BelongsTo-shaped inner: SELECT * FROM WHERE + // . = .. + inner := r.freshSession().Table(relatedTable).Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(relatedTable), quoteIdent(ownerKey), + quoteIdent(desc.parentTable), quoteIdent(desc.morphIDColumn))) + if desc.onQuery != nil { + wrapper := r.wrap(inner) + result := desc.onQuery(wrapper) + if w, ok := result.(*Query); ok { + inner = w.buildConditions().instance + } + } + if perTypeCallback != nil { + wrapper := r.wrap(inner) + result := perTypeCallback(wrapper) + if w, ok := result.(*Query); ok { + inner = w.buildConditions().instance + } + } + + // Type filter applied at the outer level (parent table). + typeClause := fmt.Sprintf("%s.%s = ?", quoteIdent(desc.parentTable), quoteIdent(desc.morphTypeColumn)) + typeArgs := []any{morphValue} + + var perType *gormio.DB + if shouldUseExists(item.operator, item.count) { + negate := item.operator == "<" && item.count == 1 + if negate { + perType = r.freshSession().Where(typeClause, typeArgs...).Where("NOT EXISTS (?)", inner) + } else { + perType = r.freshSession().Where(typeClause, typeArgs...).Where("EXISTS (?)", inner) + } + } else { + countInner := inner.Select("COUNT(*)") + perType = r.freshSession().Where(typeClause, typeArgs...).Where(fmt.Sprintf("(?) %s ?", item.operator), countInner, item.count) + } + + if first { + sub = sub.Where(perType) + first = false + } else { + sub = sub.Or(perType) + } + } + if first { + return db + } + + if item.conjunction == "or" { + return db.Or(sub) + } + return db.Where(sub) +} + +// compileExistenceSubquery returns a *gormio.DB representing the inner SELECT correlated to the +// parent table. The returned DB is intended to be passed as a value bound to a "?" placeholder +// in the outer query (GORM will inline it as a subquery and merge bindings). +func (r *Query) compileExistenceSubquery(desc *relationDescriptor, callback contractsorm.RelationCallback) *gormio.DB { + inner := r.freshSession().Table(desc.relatedTable) + + switch desc.kind { + case relKindHasOne, relKindHasMany: + for _, ref := range desc.references { + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(ref.foreignTable), quoteIdent(ref.foreignColumn), + quoteIdent(ref.primaryTable), quoteIdent(ref.primaryColumn))) + } + case relKindBelongsTo: + for _, ref := range desc.references { + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(ref.primaryTable), quoteIdent(ref.primaryColumn), + quoteIdent(ref.foreignTable), quoteIdent(ref.foreignColumn))) + } + case relKindMany2Many: + inner = inner.Joins(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + quoteIdent(desc.pivotTable), + quoteIdent(desc.pivotTable), quoteIdent(desc.pivotRelatedRef.foreignColumn), + quoteIdent(desc.relatedTable), quoteIdent(desc.pivotRelatedRef.primaryColumn))) + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(desc.pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn), + quoteIdent(desc.pivotParentRef.primaryTable), quoteIdent(desc.pivotParentRef.primaryColumn))) + case relKindMorphToMany: + inner = inner.Joins(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + quoteIdent(desc.pivotTable), + quoteIdent(desc.pivotTable), quoteIdent(desc.pivotRelatedRef.foreignColumn), + quoteIdent(desc.relatedTable), quoteIdent(desc.pivotRelatedRef.primaryColumn))) + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(desc.pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn), + quoteIdent(desc.pivotParentRef.primaryTable), quoteIdent(desc.pivotParentRef.primaryColumn))) + inner = inner.Where(fmt.Sprintf("%s.%s = ?", + quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + case relKindMorphOne, relKindMorphMany: + for _, ref := range desc.references { + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(ref.foreignTable), quoteIdent(ref.foreignColumn), + quoteIdent(ref.primaryTable), quoteIdent(ref.primaryColumn))) + } + inner = inner.Where(fmt.Sprintf("%s.%s = ?", + quoteIdent(desc.relatedTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + case relKindHasOneThrough, relKindHasManyThrough: + inner = inner.Joins(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + quoteIdent(desc.throughTable), + quoteIdent(desc.relatedTable), quoteIdent(desc.secondKey), + quoteIdent(desc.throughTable), quoteIdent(desc.secondLocalKey))) + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(desc.throughTable), quoteIdent(desc.firstKey), + quoteIdent(desc.parentTable), quoteIdent(desc.localKey))) + default: + _ = inner.AddError(errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, fmt.Sprintf("kind=%d", desc.kind))) + return inner + } + + // Apply the relation's default scope before the caller's callback so the user's WhereHas + // callback can layer on top of the always-on filter. + if desc.onQuery != nil { + wrapper := r.wrap(inner) + wrapped := desc.onQuery(wrapper) + if w, ok := wrapped.(*Query); ok { + inner = w.buildConditions().instance + } + } + + if callback != nil { + wrapper := r.wrap(inner) + wrapped := callback(wrapper) + if w, ok := wrapped.(*Query); ok { + inner = w.buildConditions().instance + } + } + + if desc.nested != nil { + nested := r.compileExistenceSubquery(desc.nested, nil) + if nested != nil { + inner = inner.Where("EXISTS (?)", nested) + } + } + + return inner.Select("1") +} + +// compileMorphExistenceSubquery is the morph-specific variant. The morph_type column is included +// in the *outer* group, not the inner subquery, because it lives on the parent's side. +func (r *Query) compileMorphExistenceSubquery(desc *relationDescriptor, morphValue string, callback contractsorm.RelationCallback) *gormio.DB { + inner := r.freshSession().Table(desc.relatedTable) + for _, ref := range desc.references { + inner = inner.Where(fmt.Sprintf("%s.%s = %s.%s", + quoteIdent(ref.foreignTable), quoteIdent(ref.foreignColumn), + quoteIdent(ref.primaryTable), quoteIdent(ref.primaryColumn))) + } + inner = inner.Where(fmt.Sprintf("%s.%s = ?", + quoteIdent(desc.relatedTable), quoteIdent(desc.morphTypeColumn)), morphValue) + if desc.onQuery != nil { + wrapper := r.wrap(inner) + wrapped := desc.onQuery(wrapper) + if w, ok := wrapped.(*Query); ok { + inner = w.buildConditions().instance + } + } + if callback != nil { + wrapper := r.wrap(inner) + wrapped := callback(wrapper) + if w, ok := wrapped.(*Query); ok { + inner = w.buildConditions().instance + } + } + return inner.Select("1") +} + +// compileAggregateSubquery returns a *gormio.DB whose compiled SQL is the inner SELECT for an +// aggregate. The select expression is dictated by sub.function. +func (r *Query) compileAggregateSubquery(desc *relationDescriptor, sub selectSub) *gormio.DB { + inner := r.compileExistenceSubquery(desc, sub.callback) + if inner == nil { + return nil + } + var selectExpr string + switch sub.function { + case "count": + selectExpr = "COUNT(*)" + case "exists": + selectExpr = "1" + default: + col := "*" + if sub.column != "*" && sub.column != "" { + col = fmt.Sprintf("%s.%s", quoteIdent(desc.relatedTable), quoteIdent(sub.column)) + } + selectExpr = fmt.Sprintf("%s(%s)", strings.ToUpper(sub.function), col) + } + return inner.Select(selectExpr) +} + +// attachHasWhere appends the inner subquery to the outer query as either WHERE [NOT] EXISTS +// (preferred, when operator/count match the EXISTS optimisation) or as a (SELECT count(*)) op ? +// comparison. +func (r *Query) attachHasWhere(db *gormio.DB, inner *gormio.DB, operator string, count int, conjunction string) *gormio.DB { + if shouldUseExists(operator, count) { + negate := operator == "<" && count == 1 + var clause string + if negate { + clause = "NOT EXISTS (?)" + } else { + clause = "EXISTS (?)" + } + if conjunction == "or" { + return db.Or(clause, inner) + } + return db.Where(clause, inner) + } + + countInner := inner.Select("COUNT(*)") + clause := fmt.Sprintf("(?) %s ?", operator) + if conjunction == "or" { + return db.Or(clause, countInner, count) + } + return db.Where(clause, countInner, count) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +func (r *Query) freshSession() *gormio.DB { + return r.instance.Session(&gormio.Session{NewDB: true, Initialized: true}) +} + +// wrap turns a fresh GORM DB into a Goravel Query wrapper so user callbacks can use the +// familiar Where/OrWhere/etc. surface. +func (r *Query) wrap(db *gormio.DB) *Query { + return NewQuery(r.ctx, r.config, r.dbConfig, db, r.grammar, r.log, r.modelToObserver, nil) +} + +func (r *Query) parentModel() any { + if r.conditions.model != nil { + return r.conditions.model + } + if r.conditions.dest != nil { + if m, err := modelToStruct(r.conditions.dest); err == nil && m != nil { + return m + } + } + if r.instance != nil && r.instance.Statement != nil { + if r.instance.Statement.Model != nil { + return r.instance.Statement.Model + } + } + return nil +} + +func (r *Query) parentTable(parent any) string { + if t, err := tableNameFor(r.instance, parent); err == nil { + return t + } + return "" +} + +// parseRelationArgs unpacks variadic args of unknown order/length into (callback, op, count). +// +// Acceptable shapes (any combination): +// - (callback) +// - (callback, op) +// - (callback, op, count) +// - (op) +// - (op, count) +// +// Defaults: op = ">=", count = 1. +func parseRelationArgs(args []any) (contractsorm.RelationCallback, string, int, error) { + op := ">=" + count := 1 + var cb contractsorm.RelationCallback + + for _, arg := range args { + switch v := arg.(type) { + case nil: + continue + case contractsorm.RelationCallback: + cb = v + case func(contractsorm.Query) contractsorm.Query: + cb = contractsorm.RelationCallback(v) + case string: + op = v + case int: + count = v + case int64: + count = int(v) + default: + return nil, op, count, errors.OrmRelationInvalidArgument.Args(v) + } + } + return cb, op, count, nil +} + +// parseMorphRelationArgs is a variant that also accepts MorphRelationCallback. +func parseMorphRelationArgs(args []any) (contractsorm.RelationCallback, contractsorm.MorphRelationCallback, string, int, error) { + op := ">=" + count := 1 + var cb contractsorm.RelationCallback + var mcb contractsorm.MorphRelationCallback + + for _, arg := range args { + switch v := arg.(type) { + case nil: + continue + case contractsorm.MorphRelationCallback: + mcb = v + case func(contractsorm.Query, string) contractsorm.Query: + mcb = contractsorm.MorphRelationCallback(v) + case contractsorm.RelationCallback: + cb = v + case func(contractsorm.Query) contractsorm.Query: + cb = contractsorm.RelationCallback(v) + case string: + op = v + case int: + count = v + case int64: + count = int(v) + default: + return nil, nil, op, count, errors.OrmRelationInvalidArgument.Args(v) + } + } + return cb, mcb, op, count, nil +} + +// shouldUseExists is the standard optimisation: comparisons of the form ">= 1" or "< 1" can be +// emitted as cheaper EXISTS / NOT EXISTS instead of a full COUNT(*) sub-select. +func shouldUseExists(op string, count int) bool { + return (op == ">=" || op == "<") && count == 1 +} + +func validAggregateFn(fn string) bool { + switch fn { + case "count", "max", "min", "sum", "avg", "exists": + return true + } + return false +} + +func aggregateAlias(relation, fn, column string) string { + rel := str.Of(strings.ReplaceAll(relation, ".", "_")).Snake().String() + if column == "*" || column == "" { + return fmt.Sprintf("%s_%s", rel, fn) + } + return fmt.Sprintf("%s_%s_%s", rel, fn, column) +} + +// quoteIdent applies a portable identifier quote. We use backticks - GORM rewrites identifiers +// per dialect during compilation when the value passes through `clause.Column`, but raw SQL +// fragments (which is what we emit for correlation) are left alone. Backticks work on MySQL and +// SQLite by default; PostgreSQL and SQL Server only accept double quotes. To stay portable we +// emit the bare identifier and let the dialects accept it - all tested dialects parse unquoted +// identifiers when the names are simple snake_case. +func quoteIdent(name string) string { + if name == "" { + return "" + } + if strings.ContainsAny(name, " (.") { + return name + } + return name +} + +// Sanity: ensure *Query satisfies contractsorm.QueryWithRelations at compile time. +var _ contractsorm.QueryWithRelations = (*Query)(nil) diff --git a/database/gorm/queries_relationships_test.go b/database/gorm/queries_relationships_test.go new file mode 100644 index 000000000..6ba4dce29 --- /dev/null +++ b/database/gorm/queries_relationships_test.go @@ -0,0 +1,419 @@ +package gorm + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + contractsdatabase "github.com/goravel/framework/contracts/database" + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +// newRelQuery builds a Query backed by a stub gorm.DB. The DB is non-functional but is enough for +// the queueing methods (Has / WhereHas / WithCount / With / ...) which only mutate the +// Conditions value and never execute SQL. +func newRelQuery(t *testing.T) *Query { + t.Helper() + db := newStubGormDB(t) + conditions := Conditions{} + return NewQuery(context.Background(), nil, contractsdatabase.Config{}, db, nil, nil, nil, &conditions) +} + +func toQuery(q contractsorm.Query) *Query { + return q.(*Query) +} + +// --- Existence queueing ---------------------------------------------------- + +func TestHasQueueing(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.Has("Books")) + assert.Len(t, got.conditions.relations, 1) + rel := got.conditions.relations[0] + assert.Equal(t, "Books", rel.relation) + assert.Equal(t, ">=", rel.operator) + assert.Equal(t, 1, rel.count) + assert.Equal(t, "and", rel.conjunction) + assert.Nil(t, rel.callback) +} + +func TestHasWithCallbackOpAndCount(t *testing.T) { + q := newRelQuery(t) + cb := contractsorm.RelationCallback(func(query contractsorm.Query) contractsorm.Query { return query }) + got := toQuery(q.Has("Books", cb, ">", 3)) + assert.Len(t, got.conditions.relations, 1) + rel := got.conditions.relations[0] + assert.Equal(t, ">", rel.operator) + assert.Equal(t, 3, rel.count) + assert.NotNil(t, rel.callback) +} + +func TestOrHasQueueing(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.OrHas("Books")) + assert.Equal(t, "or", got.conditions.relations[0].conjunction) +} + +func TestDoesntHaveQueueing(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.DoesntHave("Books")) + rel := got.conditions.relations[0] + assert.Equal(t, "<", rel.operator) + assert.Equal(t, 1, rel.count) + assert.Equal(t, "and", rel.conjunction) +} + +func TestOrDoesntHaveQueueing(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.OrDoesntHave("Books")) + assert.Equal(t, "or", got.conditions.relations[0].conjunction) + assert.Equal(t, "<", got.conditions.relations[0].operator) +} + +func TestWhereHasFamilyQueueing(t *testing.T) { + cb := contractsorm.RelationCallback(func(q contractsorm.Query) contractsorm.Query { return q }) + tests := []struct { + name string + invoke func(*Query) contractsorm.Query + conjunction string + operator string + }{ + {"WhereHas", func(q *Query) contractsorm.Query { return q.WhereHas("Books", cb) }, "and", ">="}, + {"OrWhereHas", func(q *Query) contractsorm.Query { return q.OrWhereHas("Books", cb) }, "or", ">="}, + {"WhereDoesntHave", func(q *Query) contractsorm.Query { return q.WhereDoesntHave("Books", cb) }, "and", "<"}, + {"OrWhereDoesntHave", func(q *Query) contractsorm.Query { return q.OrWhereDoesntHave("Books", cb) }, "or", "<"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + q := newRelQuery(t) + got := toQuery(tc.invoke(q)) + assert.Len(t, got.conditions.relations, 1) + assert.Equal(t, tc.conjunction, got.conditions.relations[0].conjunction) + assert.Equal(t, tc.operator, got.conditions.relations[0].operator) + assert.NotNil(t, got.conditions.relations[0].callback) + }) + } +} + +func TestHasInvalidArgErrorPropagated(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.Has("Books", 1.23)) + // invalid arg path returns a fresh query carrying an error on its instance. + assert.Error(t, got.instance.Error) +} + +// --- Morph queueing -------------------------------------------------------- + +func TestHasMorphQueueing(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.HasMorph("Houseable", []any{&relUser{}})) + assert.Len(t, got.conditions.relations, 1) + rel := got.conditions.relations[0] + assert.Equal(t, "Houseable", rel.relation) + assert.Len(t, rel.morphTypes, 1) +} + +func TestMorphFamilyQueueing(t *testing.T) { + mcb := contractsorm.MorphRelationCallback(func(q contractsorm.Query, _ string) contractsorm.Query { return q }) + tests := []struct { + name string + invoke func(*Query) contractsorm.Query + conjunction string + operator string + }{ + {"OrHasMorph", func(q *Query) contractsorm.Query { return q.OrHasMorph("X", []any{&relUser{}}) }, "or", ">="}, + {"DoesntHaveMorph", func(q *Query) contractsorm.Query { return q.DoesntHaveMorph("X", []any{&relUser{}}) }, "and", "<"}, + {"OrDoesntHaveMorph", func(q *Query) contractsorm.Query { return q.OrDoesntHaveMorph("X", []any{&relUser{}}) }, "or", "<"}, + {"WhereHasMorph", func(q *Query) contractsorm.Query { return q.WhereHasMorph("X", []any{&relUser{}}, mcb) }, "and", ">="}, + {"OrWhereHasMorph", func(q *Query) contractsorm.Query { return q.OrWhereHasMorph("X", []any{&relUser{}}, mcb) }, "or", ">="}, + {"WhereDoesntHaveMorph", func(q *Query) contractsorm.Query { return q.WhereDoesntHaveMorph("X", []any{&relUser{}}) }, "and", "<"}, + {"OrWhereDoesntHaveMorph", func(q *Query) contractsorm.Query { return q.OrWhereDoesntHaveMorph("X", []any{&relUser{}}) }, "or", "<"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + q := newRelQuery(t) + got := toQuery(tc.invoke(q)) + assert.Len(t, got.conditions.relations, 1) + assert.Equal(t, tc.conjunction, got.conditions.relations[0].conjunction) + assert.Equal(t, tc.operator, got.conditions.relations[0].operator) + }) + } +} + +func TestHasMorphEmptyTypesError(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.HasMorph("X", nil)) + assert.True(t, errors.Is(got.instance.Error, errors.OrmRelationMorphTypesEmpty)) +} + +func TestHasMorphInvalidArgError(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.HasMorph("X", []any{&relUser{}}, 3.14)) + assert.Error(t, got.instance.Error) +} + +// --- Aggregate / WithCount / WithExists queueing -------------------------- + +func TestWithAggregate(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.WithAggregate("Books", "id", "max")) + assert.Len(t, got.conditions.selectSubs, 1) + sub := got.conditions.selectSubs[0] + assert.Equal(t, "Books", sub.relation) + assert.Equal(t, "id", sub.column) + assert.Equal(t, "max", sub.function) + assert.Equal(t, "books_max_id", sub.alias) +} + +func TestWithAggregateInvalidFn(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.WithAggregate("Books", "*", "median")) + assert.True(t, errors.Is(got.instance.Error, errors.OrmRelationInvalidAggregate)) +} + +func TestWithAggregateInvalidArg(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.WithAggregate("Books", "*", "count", 3.14)) + assert.True(t, errors.Is(got.instance.Error, errors.OrmRelationInvalidArgument)) +} + +func TestWithCountStringAndStruct(t *testing.T) { + q := newRelQuery(t) + cb := contractsorm.RelationCallback(func(q contractsorm.Query) contractsorm.Query { return q }) + got := toQuery(q.WithCount("Books", contractsorm.RelationCount{Name: "Roles", Alias: "rcount", Callback: cb})) + assert.Len(t, got.conditions.selectSubs, 2) + assert.Equal(t, "books_count", got.conditions.selectSubs[0].alias) + assert.Equal(t, "rcount", got.conditions.selectSubs[1].alias) + assert.NotNil(t, got.conditions.selectSubs[1].callback) +} + +func TestWithCountInvalidArg(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.WithCount(123)) + assert.True(t, errors.Is(got.instance.Error, errors.OrmRelationInvalidArgument)) +} + +func TestWithMaxMinSumAvg(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.WithMax("Books", "id")) + got = toQuery(got.WithMin("Books", "id")) + got = toQuery(got.WithSum("Books", "id")) + got = toQuery(got.WithAvg("Books", "id")) + assert.Len(t, got.conditions.selectSubs, 4) + assert.Equal(t, "max", got.conditions.selectSubs[0].function) + assert.Equal(t, "min", got.conditions.selectSubs[1].function) + assert.Equal(t, "sum", got.conditions.selectSubs[2].function) + assert.Equal(t, "avg", got.conditions.selectSubs[3].function) +} + +func TestWithExists(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.WithExists("Books", "Roles")) + assert.Len(t, got.conditions.selectSubs, 2) + assert.Equal(t, "exists", got.conditions.selectSubs[0].function) + assert.Equal(t, "exists", got.conditions.selectSubs[1].function) + assert.Equal(t, "books_exists", got.conditions.selectSubs[0].alias) +} + +// --- Eager-load queueing --------------------------------------------------- + +func TestWithQueueing(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.With("Books", "Roles")) + assert.Len(t, got.conditions.eagerLoad, 2) + assert.Equal(t, "Books", got.conditions.eagerLoad[0].relation) + assert.Equal(t, "Roles", got.conditions.eagerLoad[1].relation) +} + +func TestWithInvalidArg(t *testing.T) { + q := newRelQuery(t) + got := toQuery(q.With(3.14)) + assert.True(t, errors.Is(got.instance.Error, errors.OrmEagerLoadInvalidArgument)) +} + +func TestWithout(t *testing.T) { + q := newRelQuery(t) + q1 := toQuery(q.With("Books", "Roles", "Logo")) + q2 := toQuery(q1.Without("Roles")) + assert.Len(t, q2.conditions.eagerLoad, 2) + for _, e := range q2.conditions.eagerLoad { + assert.NotEqual(t, "Roles", e.relation) + } +} + +func TestWithoutNoOps(t *testing.T) { + q := newRelQuery(t) + // no eager loads queued -> returns same query + q1 := q.Without("Books") + assert.Same(t, q, q1.(*Query)) + + // no relation names -> returns same query + q2 := toQuery(q.With("Books")) + q3 := q2.Without() + assert.Same(t, q2, q3.(*Query)) +} + +func TestWithOnly(t *testing.T) { + q := newRelQuery(t) + q1 := toQuery(q.With("Books", "Roles")) + q2 := toQuery(q1.WithOnly("Logo")) + assert.Len(t, q2.conditions.eagerLoad, 1) + assert.Equal(t, "Logo", q2.conditions.eagerLoad[0].relation) +} + +// --- Pure helpers --------------------------------------------------------- + +func TestParseRelationArgsAllShapes(t *testing.T) { + cb := contractsorm.RelationCallback(func(q contractsorm.Query) contractsorm.Query { return q }) + cases := []struct { + name string + args []any + op string + count int + hasCb bool + }{ + {"empty", nil, ">=", 1, false}, + {"callback", []any{cb}, ">=", 1, true}, + {"callback as func", []any{func(q contractsorm.Query) contractsorm.Query { return q }}, ">=", 1, true}, + {"op only", []any{">"}, ">", 1, false}, + {"op + count", []any{">", 5}, ">", 5, false}, + {"int64 count", []any{int64(2)}, ">=", 2, false}, + {"nil arg ignored", []any{nil, "<"}, "<", 1, false}, + {"callback + op + count", []any{cb, ">=", 3}, ">=", 3, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + gotCb, op, count, err := parseRelationArgs(tc.args) + assert.NoError(t, err) + assert.Equal(t, tc.op, op) + assert.Equal(t, tc.count, count) + assert.Equal(t, tc.hasCb, gotCb != nil) + }) + } +} + +func TestParseRelationArgsInvalid(t *testing.T) { + _, _, _, err := parseRelationArgs([]any{3.14}) + assert.True(t, errors.Is(err, errors.OrmRelationInvalidArgument)) +} + +func TestParseMorphRelationArgs(t *testing.T) { + cb := contractsorm.RelationCallback(func(q contractsorm.Query) contractsorm.Query { return q }) + mcb := contractsorm.MorphRelationCallback(func(q contractsorm.Query, _ string) contractsorm.Query { return q }) + + gotCb, gotMcb, op, count, err := parseMorphRelationArgs([]any{cb, ">", 3}) + assert.NoError(t, err) + assert.NotNil(t, gotCb) + assert.Nil(t, gotMcb) + assert.Equal(t, ">", op) + assert.Equal(t, 3, count) + + gotCb, gotMcb, _, _, err = parseMorphRelationArgs([]any{mcb}) + assert.NoError(t, err) + assert.Nil(t, gotCb) + assert.NotNil(t, gotMcb) + + gotCb, gotMcb, _, _, err = parseMorphRelationArgs([]any{func(q contractsorm.Query, _ string) contractsorm.Query { return q }}) + assert.NoError(t, err) + assert.Nil(t, gotCb) + assert.NotNil(t, gotMcb) + + gotCb, _, _, _, err = parseMorphRelationArgs([]any{func(q contractsorm.Query) contractsorm.Query { return q }}) + assert.NoError(t, err) + assert.NotNil(t, gotCb) + + _, _, _, _, err = parseMorphRelationArgs([]any{nil, int64(7), "="}) + assert.NoError(t, err) + + _, _, _, _, err = parseMorphRelationArgs([]any{3.14}) + assert.True(t, errors.Is(err, errors.OrmRelationInvalidArgument)) +} + +func TestShouldUseExists(t *testing.T) { + cases := []struct { + op string + count int + want bool + }{ + {">=", 1, true}, + {"<", 1, true}, + {">=", 2, false}, + {"<", 2, false}, + {"=", 1, false}, + {">", 1, false}, + } + for _, tc := range cases { + assert.Equal(t, tc.want, shouldUseExists(tc.op, tc.count), "op=%s count=%d", tc.op, tc.count) + } +} + +func TestValidAggregateFn(t *testing.T) { + for _, fn := range []string{"count", "max", "min", "sum", "avg", "exists"} { + assert.True(t, validAggregateFn(fn), fn) + } + for _, fn := range []string{"", "median", "stddev"} { + assert.False(t, validAggregateFn(fn), fn) + } +} + +func TestAggregateAlias(t *testing.T) { + cases := []struct { + relation string + fn string + col string + want string + }{ + {"Books", "count", "*", "books_count"}, + {"Books", "count", "", "books_count"}, + {"Books.Author", "count", "*", "books_author_count"}, + {"BooksAuthor", "max", "id", "books_author_max_id"}, + {"books_author", "sum", "price", "books_author_sum_price"}, + } + for _, tc := range cases { + assert.Equal(t, tc.want, aggregateAlias(tc.relation, tc.fn, tc.col)) + } +} + +func TestQuoteIdent(t *testing.T) { + assert.Equal(t, "", quoteIdent("")) + assert.Equal(t, "users", quoteIdent("users")) + // expressions left alone (contain space, parens, dot) + assert.Equal(t, "users.id", quoteIdent("users.id")) + assert.Equal(t, "COUNT(*)", quoteIdent("COUNT(*)")) +} + +func TestParentTable(t *testing.T) { + q := newRelQuery(t) + tbl := q.parentTable(&relUser{}) + assert.Equal(t, "rel_users", tbl) + // invalid model returns "" + assert.Equal(t, "", q.parentTable("not-a-model")) +} + +func TestParentModelFromConditions(t *testing.T) { + q := newRelQuery(t) + q.conditions.model = &relUser{} + assert.NotNil(t, q.parentModel()) + + q2 := newRelQuery(t) + q2.conditions.dest = &[]relUser{} + assert.NotNil(t, q2.parentModel()) + + q3 := newRelQuery(t) + assert.Nil(t, q3.parentModel()) +} + +func TestFreshSession(t *testing.T) { + q := newRelQuery(t) + s := q.freshSession() + assert.NotNil(t, s) +} + +func TestWrapReturnsQuery(t *testing.T) { + q := newRelQuery(t) + wrapped := q.wrap(q.instance) + assert.NotNil(t, wrapped) + assert.NotNil(t, wrapped.instance) +} diff --git a/database/gorm/relation.go b/database/gorm/relation.go new file mode 100644 index 000000000..79057dd81 --- /dev/null +++ b/database/gorm/relation.go @@ -0,0 +1,675 @@ +package gorm + +import ( + "cmp" + "reflect" + "strings" + "time" + + gormio "gorm.io/gorm" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/orm/morphmap" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/str" +) + +// relationKind enumerates every relationship flavour the resolver can describe. +// It is a superset of GORM's RelationshipType because it also covers the inverse polymorphic +// (MorphTo) and the through relations declared via ModelWithThroughRelations. +type relationKind int + +const ( + relKindHasOne relationKind = iota + relKindHasMany + relKindBelongsTo + relKindMany2Many + relKindMorphOne + relKindMorphMany + relKindMorphTo + relKindMorphToMany + relKindHasOneThrough + relKindHasManyThrough +) + +// referenceKey describes one column-pair from a GORM Reference, with each side already qualified +// by table name. PrimaryKey/ForeignKey naming follows GORM's convention. +type referenceKey struct { + primaryTable string + primaryColumn string + foreignTable string + foreignColumn string +} + +// relationDescriptor is the resolver's normalised view of a relationship. It lets the +// queries-relationships builder construct correlated subqueries without ever calling back into +// GORM's relation internals. +type relationDescriptor struct { + name string + kind relationKind + parentTable string + relatedTable string + relatedModel any + references []referenceKey + + // many-to-many specifics + pivotTable string + pivotParentRef referenceKey + pivotRelatedRef referenceKey + // pivotField is the name of the field on the related model that the eager loader hydrates + // with pivot column values. Sourced from Many2Many.PivotField (or the morph variants); + // defaults to "Pivot". When the related model has no field by this name, no Pivot hydration + // happens. The field's Go type drives both the SELECT list and hydration target. + pivotField string + // pivotCreatedAtColumn is the pivot-table column to auto-stamp with the current time on + // INSERT. Empty string means "don't auto-stamp on INSERT". Resolved by the descriptor builder + // from (priority): Pivot struct's autoCreateTime field → Pivot struct's CreatedAt field → + // PivotTimestamps: true fallback (defaults to "created_at"). + pivotCreatedAtColumn string + // pivotUpdatedAtColumn is the pivot-table column to auto-stamp with the current time on + // INSERT and UPDATE. Empty string means "don't auto-stamp on INSERT/UPDATE". Same resolution + // rules as pivotCreatedAtColumn but using autoUpdateTime / UpdatedAt. + pivotUpdatedAtColumn string + + // relatedKeyType is the Go type of the related model's PK field — used by castKey to normalise + // SyncResult ids back to the related model's native key type, irrespective of what the caller + // passed (string, int, uint, etc.) and what GORM scanned from the pivot table (often int64). + // nil for kinds that don't have a related-side pivot key (HasOne family, BelongsTo, etc.). + relatedKeyType reflect.Type + + // polymorphic specifics + morphTypeColumn string // e.g. "imageable_type" — on parent table for MorphTo, on pivot for MorphToMany + morphIDColumn string // e.g. "imageable_id" — on parent table for MorphTo, on pivot for MorphToMany + morphValue string // e.g. "post" — used in WHERE *_type = ? filters + morphOwnerKey string // PK on each related model for MorphTo (defaults to "id") + morphInverse bool // true for MorphedByMany — flips morph value source from parent to related + + // through specifics + throughTable string + throughModel any + firstKey string // FK on through pointing at parent + secondKey string // FK on related pointing at through + localKey string // PK on parent + secondLocalKey string // PK on through + + // onQuery is the per-relation default scope from Relation.OnQuery. Applied by every code + // path that builds an inner query for this relation (eager loaders, existence builders, + // Related), *before* any caller-supplied callback. + onQuery contractsorm.RelationCallback + + // onPivotQuery is the per-relation default scope for pivot-table SELECT / UPDATE / DELETE, + // from Many2Many.OnPivotQuery / MorphToMany.OnPivotQuery / MorphedByMany.OnPivotQuery. + // Applied by existingPivotIDs, allPivotIDs, DetachRelation, UpdateExistingPivotRelation. + onPivotQuery contractsorm.PivotCallback + + // touches, when true, makes Sync / Attach / Detach / Toggle / UpdateExistingPivot bump the + // parent's updated_at after the pivot write succeeds (and only when pivot rows actually + // changed). Source: Many2Many.Touches / MorphToMany.Touches / MorphedByMany.Touches. + touches bool + + // next link for nested resolution (e.g. "Books.Author") + nested *relationDescriptor +} + +// resolveRelation walks a (possibly dotted) relation path and returns a chain of descriptors +// rooted at the given parent model. The returned descriptor's nested field points at the next +// hop, so callers can recurse to build subqueries for "User.Books.Author"-style queries. +// +// All relations are declared via the parent's Relations() method (ModelWithRelations). GORM +// relation tags (`foreignKey`, `references`, `many2many`, `polymorphic`) are forbidden — if +// detected the resolver returns OrmRelationTagForbidden pointing the user at Relations(). +func resolveRelation(db *gormio.DB, parent any, relation string) (*relationDescriptor, error) { + if relation == "" { + return nil, errors.OrmQueryEmptyRelation + } + + head, tail, _ := strings.Cut(relation, ".") + + // Parse the parent's schema using GORM's cache (avoids reparsing on every call). + stmt := &gormio.Statement{DB: db} + if err := stmt.Parse(parent); err != nil { + return nil, err + } + parentSchema := stmt.Schema + parentTable := parentSchema.Table + + // Detect forbidden GORM relation tags. If GORM populated a Relationships entry for the + // requested name, the user has a conflicting tag — error out with a pointer to Relations(). + if _, hasGormRel := parentSchema.Relationships.Relations[head]; hasGormRel { + return nil, errors.OrmRelationTagForbidden.Args(head, parentSchema.Name) + } + + desc, err := descriptorFromRelations(db, parent, parentTable, head) + if err != nil { + return nil, err + } + desc.name = head + + if tail != "" { + // Recurse using the *related* model as the new parent. + nestedParent := desc.relatedModel + if nestedParent == nil { + return nil, errors.OrmRelationUnsupported.Args(head, parentSchema.Name, "no related model") + } + nested, err := resolveRelation(db, nestedParent, tail) + if err != nil { + return nil, err + } + desc.nested = nested + } + return desc, nil +} + +// descriptorFromRelations resolves a relation declared via the parent's Relations() method. +// Handles all 11 kinds. Returns OrmRelationNotFound when the parent doesn't implement the +// interface or the relation name isn't in its map. +// +// Dispatch is by Go type, not by a discriminator field — each per-kind struct in +// contracts/database/orm satisfies the sealed Relation interface and lands in its own case here. +// The kinds form four families (mirroring fedaco's class hierarchy in +// /workbench/fedaco/libs/fedaco/src/fedaco/relations) which share resolver logic: +// +// - HasOneOrMany family (HasOne, HasMany, MorphOne, MorphMany): FK lives on the related side. +// - BelongsTo family (BelongsTo, MorphTo): FK lives on the parent. +// - BelongsToMany family (Many2Many, MorphToMany, MorphedByMany): joined via a pivot table. +// - HasManyThrough family (HasOneThrough, HasManyThrough): joined via an intermediate table. +// +// Family-level shared behaviour (Save/Associate/Attach/etc.) lives downstream in relation_writes.go +// and is dispatched by the internal relationKind groups; this function's job is to map the +// user-facing struct to the descriptor those downstream paths consume. +// +// Accepts Relations() declared with either a value receiver (`func (Foo) Relations()`) or a +// pointer receiver (`func (*Foo) Relations()`). Models often mix the two — e.g. value receivers +// for pure-metadata methods and pointer receivers for GORM lifecycle hooks — so the framework +// looks up the interface on whichever form is addressable. +func descriptorFromRelations(db *gormio.DB, parent any, parentTable, name string) (*relationDescriptor, error) { + relations, ok := tryGetRelations(parent) + if !ok { + return nil, errors.OrmRelationNotFound.Args(name, reflect.TypeOf(parent).String()) + } + rel, ok := relations[name] + if !ok { + return nil, errors.OrmRelationNotFound.Args(name, reflect.TypeOf(parent).String()) + } + + var ( + desc *relationDescriptor + err error + onQuery contractsorm.RelationCallback + ) + switch r := rel.(type) { + case contractsorm.HasOne: + desc, err = descriptorFromHasOneOrMany(db, parent, parentTable, name, r.Related, r.ForeignKey, r.LocalKey, relKindHasOne) + onQuery = r.OnQuery + case contractsorm.HasMany: + desc, err = descriptorFromHasOneOrMany(db, parent, parentTable, name, r.Related, r.ForeignKey, r.LocalKey, relKindHasMany) + onQuery = r.OnQuery + case contractsorm.BelongsTo: + desc, err = descriptorFromBelongsTo(db, parent, parentTable, name, r) + onQuery = r.OnQuery + case contractsorm.Many2Many: + desc, err = descriptorFromMany2Many(db, parent, parentTable, name, r) + onQuery = r.OnQuery + if desc != nil { + desc.onPivotQuery = r.OnPivotQuery + desc.touches = r.Touches + } + case contractsorm.MorphOne: + desc, err = descriptorFromMorphOneOrMany(db, parent, parentTable, name, r.Related, r.Name, r.TypeColumn, r.IDColumn, r.LocalKey, relKindMorphOne) + onQuery = r.OnQuery + case contractsorm.MorphMany: + desc, err = descriptorFromMorphOneOrMany(db, parent, parentTable, name, r.Related, r.Name, r.TypeColumn, r.IDColumn, r.LocalKey, relKindMorphMany) + onQuery = r.OnQuery + case contractsorm.MorphTo: + desc, err = descriptorFromMorphTo(parent, parentTable, name, r) + onQuery = r.OnQuery + case contractsorm.MorphToMany: + desc, err = descriptorFromMorphToMany(db, parent, parentTable, name, r, false) + onQuery = r.OnQuery + if desc != nil { + desc.onPivotQuery = r.OnPivotQuery + desc.touches = r.Touches + } + case contractsorm.MorphedByMany: + desc, err = descriptorFromMorphedByMany(db, parent, parentTable, name, r) + onQuery = r.OnQuery + if desc != nil { + desc.onPivotQuery = r.OnPivotQuery + desc.touches = r.Touches + } + case contractsorm.HasOneThrough: + desc, err = descriptorFromThrough(db, parent, parentTable, name, r.Related, r.Through, r.FirstKey, r.SecondKey, r.LocalKey, r.SecondLocalKey, relKindHasOneThrough) + onQuery = r.OnQuery + case contractsorm.HasManyThrough: + desc, err = descriptorFromThrough(db, parent, parentTable, name, r.Related, r.Through, r.FirstKey, r.SecondKey, r.LocalKey, r.SecondLocalKey, relKindHasManyThrough) + onQuery = r.OnQuery + default: + return nil, errors.OrmMorphRelationKindUnknown.Args(name, reflect.TypeOf(parent).String(), reflect.TypeOf(rel).String()) + } + if err != nil { + return nil, err + } + // Carry the per-relation default-scope hook into the descriptor; every consumer (eager + // loader, existence builder, Related) applies it before any caller callback. + desc.onQuery = onQuery + return desc, nil +} + +// descriptorFromHasOneOrMany handles the HasOneOrMany family's non-polymorphic members +// (HasOne, HasMany). The polymorphic members (MorphOne, MorphMany) share the same FK-on-related +// shape but add a type-column filter, so they go through descriptorFromMorphOneOrMany instead. +func descriptorFromHasOneOrMany(db *gormio.DB, parent any, parentTable, name string, related any, foreignKey, localKey string, kind relationKind) (*relationDescriptor, error) { + if related == nil { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Related") + } + relatedTable, err := tableNameFor(db, related) + if err != nil { + return nil, err + } + fk := cmp.Or(foreignKey, str.Of(parentTable).Singular().String()+"_id") + lk := cmp.Or(localKey, "id") + return &relationDescriptor{ + kind: kind, + parentTable: parentTable, + relatedTable: relatedTable, + relatedModel: related, + references: []referenceKey{{ + primaryTable: parentTable, + primaryColumn: lk, + foreignTable: relatedTable, + foreignColumn: fk, + }}, + }, nil +} + +func descriptorFromBelongsTo(db *gormio.DB, parent any, parentTable, name string, rel contractsorm.BelongsTo) (*relationDescriptor, error) { + if rel.Related == nil { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Related") + } + relatedTable, err := tableNameFor(db, rel.Related) + if err != nil { + return nil, err + } + fk := cmp.Or(rel.ForeignKey, str.Of(relatedTable).Singular().String()+"_id") + owner := cmp.Or(rel.OwnerKey, "id") + return &relationDescriptor{ + kind: relKindBelongsTo, + parentTable: parentTable, + relatedTable: relatedTable, + relatedModel: rel.Related, + references: []referenceKey{{ + primaryTable: relatedTable, + primaryColumn: owner, + foreignTable: parentTable, + foreignColumn: fk, + }}, + }, nil +} + +func descriptorFromMany2Many(db *gormio.DB, parent any, parentTable, name string, rel contractsorm.Many2Many) (*relationDescriptor, error) { + if rel.Related == nil { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Related") + } + relatedTable, err := tableNameFor(db, rel.Related) + if err != nil { + return nil, err + } + parentSingular := str.Of(parentTable).Singular().String() + relatedSingular := str.Of(relatedTable).Singular().String() + pivotTable := cmp.Or(rel.Table, alphabeticalPivotName(parentSingular, relatedSingular)) + foreignPivotKey := cmp.Or(rel.ForeignPivotKey, parentSingular+"_id") + relatedPivotKey := cmp.Or(rel.RelatedPivotKey, relatedSingular+"_id") + parentKey := cmp.Or(rel.ParentKey, "id") + relatedKey := cmp.Or(rel.RelatedKey, "id") + + relatedKeyType, err := relatedKeyFieldType(db, rel.Related, relatedKey) + if err != nil { + return nil, err + } + pivotField := cmp.Or(rel.PivotField, "Pivot") + createdAtCol, updatedAtCol, err := resolvePivotTimestamps(db, rel.Related, pivotField, rel.PivotTimestamps) + if err != nil { + return nil, err + } + + return &relationDescriptor{ + kind: relKindMany2Many, + parentTable: parentTable, + relatedTable: relatedTable, + relatedModel: rel.Related, + pivotTable: pivotTable, + pivotParentRef: referenceKey{ + primaryTable: parentTable, + primaryColumn: parentKey, + foreignTable: pivotTable, + foreignColumn: foreignPivotKey, + }, + pivotRelatedRef: referenceKey{ + primaryTable: relatedTable, + primaryColumn: relatedKey, + foreignTable: pivotTable, + foreignColumn: relatedPivotKey, + }, + pivotField: pivotField, + pivotCreatedAtColumn: createdAtCol, + pivotUpdatedAtColumn: updatedAtCol, + relatedKeyType: relatedKeyType, + }, nil +} + +func descriptorFromMorphOneOrMany(db *gormio.DB, parent any, parentTable, name string, related any, morphName, typeCol, idCol, localKey string, kind relationKind) (*relationDescriptor, error) { + if related == nil { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Related") + } + if morphName == "" { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Name") + } + relatedTable, err := tableNameFor(db, related) + if err != nil { + return nil, err + } + typeColumn := cmp.Or(typeCol, morphName+"_type") + idColumn := cmp.Or(idCol, morphName+"_id") + lk := cmp.Or(localKey, "id") + + return &relationDescriptor{ + kind: kind, + parentTable: parentTable, + relatedTable: relatedTable, + relatedModel: related, + morphTypeColumn: typeColumn, + morphIDColumn: idColumn, + morphValue: resolveMorphValue(parent, parentTable), + references: []referenceKey{{ + primaryTable: parentTable, + primaryColumn: lk, + foreignTable: relatedTable, + foreignColumn: idColumn, + }}, + }, nil +} + +func descriptorFromMorphTo(parent any, parentTable, name string, rel contractsorm.MorphTo) (*relationDescriptor, error) { + if rel.Name == "" { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Name") + } + return &relationDescriptor{ + kind: relKindMorphTo, + parentTable: parentTable, + morphTypeColumn: cmp.Or(rel.TypeColumn, rel.Name+"_type"), + morphIDColumn: cmp.Or(rel.IDColumn, rel.Name+"_id"), + morphOwnerKey: cmp.Or(rel.OwnerKey, "id"), + }, nil +} + +// descriptorFromMorphToMany covers MorphToMany. It's separated from MorphedByMany so the +// morph-value derivation source can differ (parent vs. related) without re-reading the kind via +// reflection. +func descriptorFromMorphToMany(db *gormio.DB, parent any, parentTable, name string, rel contractsorm.MorphToMany, inverse bool) (*relationDescriptor, error) { + return buildMorphPivotDescriptor(db, parent, parentTable, name, + rel.Related, rel.Name, rel.Table, rel.TypeColumn, + rel.ForeignPivotKey, rel.RelatedPivotKey, rel.ParentKey, rel.RelatedKey, + rel.PivotField, rel.PivotTimestamps, + inverse, + ) +} + +func descriptorFromMorphedByMany(db *gormio.DB, parent any, parentTable, name string, rel contractsorm.MorphedByMany) (*relationDescriptor, error) { + return buildMorphPivotDescriptor(db, parent, parentTable, name, + rel.Related, rel.Name, rel.Table, rel.TypeColumn, + rel.ForeignPivotKey, rel.RelatedPivotKey, rel.ParentKey, rel.RelatedKey, + rel.PivotField, rel.PivotTimestamps, + true, + ) +} + +func buildMorphPivotDescriptor(db *gormio.DB, parent any, parentTable, name string, related any, morphName, table, typeCol, foreignPivot, relatedPivot, parentKey, relatedKey string, pivotField string, pivotTimestamps bool, inverse bool) (*relationDescriptor, error) { + if related == nil { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Related") + } + if morphName == "" { + return nil, errors.OrmMorphRelationMissingField.Args(name, reflect.TypeOf(parent).String(), "Name") + } + relatedTable, err := tableNameFor(db, related) + if err != nil { + return nil, err + } + + pivotTable := cmp.Or(table, str.Of(morphName).Plural().String()) + morphTypeColumn := cmp.Or(typeCol, morphName+"_type") + morphIDColumn := cmp.Or(foreignPivot, morphName+"_id") + relatedPivotKey := cmp.Or(relatedPivot, str.Of(relatedTable).Singular().String()+"_id") + pk := cmp.Or(parentKey, "id") + rk := cmp.Or(relatedKey, "id") + + morphValue := resolveMorphValue(parent, parentTable) + if inverse { + morphValue = resolveMorphValue(related, relatedTable) + } + + relatedKeyType, err := relatedKeyFieldType(db, related, rk) + if err != nil { + return nil, err + } + pivotFieldName := cmp.Or(pivotField, "Pivot") + createdAtCol, updatedAtCol, err := resolvePivotTimestamps(db, related, pivotFieldName, pivotTimestamps) + if err != nil { + return nil, err + } + + return &relationDescriptor{ + kind: relKindMorphToMany, + parentTable: parentTable, + relatedTable: relatedTable, + relatedModel: related, + pivotTable: pivotTable, + morphTypeColumn: morphTypeColumn, + morphIDColumn: morphIDColumn, + morphValue: morphValue, + morphInverse: inverse, + pivotParentRef: referenceKey{ + primaryTable: parentTable, + primaryColumn: pk, + foreignTable: pivotTable, + foreignColumn: morphIDColumn, + }, + pivotRelatedRef: referenceKey{ + primaryTable: relatedTable, + primaryColumn: rk, + foreignTable: pivotTable, + foreignColumn: relatedPivotKey, + }, + pivotField: pivotFieldName, + pivotCreatedAtColumn: createdAtCol, + pivotUpdatedAtColumn: updatedAtCol, + relatedKeyType: relatedKeyType, + }, nil +} + +func descriptorFromThrough(db *gormio.DB, parent any, parentTable, name string, related, through any, firstKey, secondKey, localKey, secondLocalKey string, kind relationKind) (*relationDescriptor, error) { + if related == nil { + return nil, errors.OrmRelationThroughNotConfigured.Args(name, reflect.TypeOf(parent).String()) + } + if through == nil { + return nil, errors.OrmRelationThroughNotConfigured.Args(name, reflect.TypeOf(parent).String()) + } + relatedTable, err := tableNameFor(db, related) + if err != nil { + return nil, err + } + throughTable, err := tableNameFor(db, through) + if err != nil { + return nil, err + } + return &relationDescriptor{ + kind: kind, + parentTable: parentTable, + relatedTable: relatedTable, + relatedModel: related, + throughTable: throughTable, + throughModel: through, + firstKey: cmp.Or(firstKey, str.Of(parentTable).Singular().String()+"_id"), + secondKey: cmp.Or(secondKey, str.Of(throughTable).Singular().String()+"_id"), + localKey: cmp.Or(localKey, "id"), + secondLocalKey: cmp.Or(secondLocalKey, "id"), + }, nil +} + +// tableNameFor returns the GORM-resolved table name for any model instance. +func tableNameFor(db *gormio.DB, model any) (string, error) { + stmt := &gormio.Statement{DB: db} + if err := stmt.Parse(model); err != nil { + return "", err + } + return stmt.Schema.Table, nil +} + +// relatedKeyFieldType returns the Go type of the related model's PK field (the column referenced +// by the pivot's RelatedPivotKey). Used by SyncResult to normalise ids back to the related model's +// native key type via castKey. Returns nil (no error) if the column is not a recognised field on +// the related schema — castKey then leaves ids untouched. +func relatedKeyFieldType(db *gormio.DB, related any, columnName string) (reflect.Type, error) { + schema, err := parseGormSchema(db, related) + if err != nil { + return nil, err + } + if field, ok := schema.FieldsByDBName[columnName]; ok { + return field.FieldType, nil + } + return nil, nil +} + +// resolvePivotTimestamps decides which pivot-table columns should be auto-stamped with the +// current time on INSERT (created) and INSERT/UPDATE (updated). Detection priority: +// +// 1. Pivot struct field with `gorm:"autoCreateTime"` / `gorm:"autoUpdateTime"` tag — column +// name from the field's GORM schema (respects `gorm:"column:..."`). +// 2. Pivot struct field named CreatedAt / UpdatedAt of type time.Time (GORM convention). +// 3. fallbackEnabled (relation-level PivotTimestamps: true) — defaults to "created_at" / +// "updated_at" for whichever column the pivot struct didn't already provide. +// +// Empty string for either column means "don't auto-stamp on that op". The pivot struct does not +// need to declare both — declaring only CreatedAt is fine and disables update-side stamping. +func resolvePivotTimestamps(db *gormio.DB, relatedModel any, pivotFieldName string, fallbackEnabled bool) (createdCol, updatedCol string, err error) { + pivotStructType, ok := pivotFieldStructType(relatedModel, pivotFieldName) + if ok { + schema, schemaErr := parseGormSchema(db, reflect.New(pivotStructType).Interface()) + if schemaErr != nil { + return "", "", schemaErr + } + // Priority 1: explicit GORM tags on any field. + for _, f := range schema.Fields { + if f.AutoCreateTime != 0 && createdCol == "" { + createdCol = f.DBName + } + if f.AutoUpdateTime != 0 && updatedCol == "" { + updatedCol = f.DBName + } + } + // Priority 2: convention — fields named CreatedAt / UpdatedAt of type time.Time. + if createdCol == "" { + if f, found := schema.FieldsByName["CreatedAt"]; found && f.FieldType == reflect.TypeFor[time.Time]() { + createdCol = f.DBName + } + } + if updatedCol == "" { + if f, found := schema.FieldsByName["UpdatedAt"]; found && f.FieldType == reflect.TypeFor[time.Time]() { + updatedCol = f.DBName + } + } + } + // Priority 3: relation-level fallback. Only fills columns the pivot struct didn't provide. + if fallbackEnabled { + if createdCol == "" { + createdCol = "created_at" + } + if updatedCol == "" { + updatedCol = "updated_at" + } + } + return createdCol, updatedCol, nil +} + +// pivotFieldStructType reflects relatedModel for a struct field named pivotFieldName and returns +// its underlying struct type. Returns ok=false when the related model has no such field, or when +// the field exists but isn't a struct (the eager loader will surface the mismatched-kind error +// later via OrmRelationPivotFieldNotStruct; here we silently fall back to no struct-driven config). +func pivotFieldStructType(relatedModel any, pivotFieldName string) (reflect.Type, bool) { + relatedType := reflect.TypeOf(relatedModel) + if relatedType.Kind() == reflect.Pointer { + relatedType = relatedType.Elem() + } + if relatedType.Kind() != reflect.Struct { + return nil, false + } + field, ok := relatedType.FieldByName(pivotFieldName) + if !ok || field.Type.Kind() != reflect.Struct { + return nil, false + } + return field.Type, true +} + +// alphabeticalPivotName returns the Eloquent-convention default pivot table for a Many2Many +// relation: the two singular table names sorted alphabetically and joined by "_". E.g. +// (post, tag) -> "post_tag", (user, role) -> "role_user". +func alphabeticalPivotName(a, b string) string { + if a < b { + return a + "_" + b + } + return b + "_" + a +} + +// resolveMorphValue picks the value to use for a polymorphic *_type column. The model-level +// MorphClass() method takes precedence, then the global morph map (registered via orm.MorphMap), +// then GORM's parsed PrimaryValue (which is either a `polymorphicValue:` tag or the parent's +// table name). +func resolveMorphValue(parent any, gormDefault string) string { + if v, ok := morphmap.MorphValue(parent); ok { + return v + } + return gormDefault +} + +// resolveMorphAlias returns the morph alias for model from MorphClass() / morph map only — +// without falling back to the table name. Used by Associate when we want to know whether the +// owner has an explicit registered alias before defaulting to its table. +func resolveMorphAlias(model any) (string, bool) { + return morphmap.MorphValue(model) +} + +// tryGetRelations returns the parent model's Relations() map regardless of whether the method is +// declared with a value receiver (`func (Foo) Relations()`) or a pointer receiver (`func (*Foo) +// Relations()`). Returns ok=false if neither form satisfies ModelWithRelations. +// +// Mirrors the dual-receiver detection in morphmap.tryMorphClass; both helpers exist because Go +// doesn't pick a receiver style for users — and real-world models freely mix value and pointer +// receivers across methods on the same struct. +func tryGetRelations(parent any) (map[string]contractsorm.Relation, bool) { + // Direct interface satisfaction — covers the common case where parent is *Foo and Relations + // has a pointer receiver, *or* parent is *Foo and Relations has a value receiver (since + // pointer-to-T satisfies any value-receiver interface T). + if m, ok := parent.(contractsorm.ModelWithRelations); ok { + return m.Relations(), true + } + rv := reflect.ValueOf(parent) + switch rv.Kind() { + case reflect.Pointer: + if rv.IsNil() { + return nil, false + } + // Try the dereferenced value — covers value-receiver methods when parent is a pointer. + // (Already handled above, but keep the branch for completeness; falls through silently.) + if m, ok := rv.Elem().Interface().(contractsorm.ModelWithRelations); ok { + return m.Relations(), true + } + case reflect.Struct: + // parent is a value but Relations is on the pointer receiver — wrap in a fresh + // addressable pointer so the method set includes the pointer-receiver methods. + ptr := reflect.New(rv.Type()) + ptr.Elem().Set(rv) + if m, ok := ptr.Interface().(contractsorm.ModelWithRelations); ok { + return m.Relations(), true + } + } + return nil, false +} diff --git a/database/gorm/relation_test.go b/database/gorm/relation_test.go new file mode 100644 index 000000000..8570a5ae0 --- /dev/null +++ b/database/gorm/relation_test.go @@ -0,0 +1,440 @@ +package gorm + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" + "gorm.io/gorm/callbacks" + "gorm.io/gorm/clause" + gormschema "gorm.io/gorm/schema" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +// stubDialector is a no-op dialector that lets us spin up a *gormio.DB without an actual +// connection. It registers the standard callbacks so DryRun-mode SQL can still be built. +type stubDialector struct{} + +func (stubDialector) Name() string { return "stub" } +func (stubDialector) Initialize(db *gormio.DB) error { + callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{}) + return nil +} +func (stubDialector) Migrator(db *gormio.DB) gormio.Migrator { return nil } +func (stubDialector) DataTypeOf(*gormschema.Field) string { return "TEXT" } +func (stubDialector) DefaultValueOf(*gormschema.Field) clause.Expression { return clause.Expr{} } +func (stubDialector) BindVarTo(writer clause.Writer, _ *gormio.Statement, _ any) { + _ = writer.WriteByte('?') +} +func (stubDialector) QuoteTo(writer clause.Writer, str string) { + _, _ = writer.WriteString(`"` + str + `"`) +} +func (stubDialector) Explain(sql string, _ ...any) string { return sql } + +func newStubGormDB(t *testing.T) *gormio.DB { + t.Helper() + db, err := gormio.Open(stubDialector{}, &gormio.Config{}) + if err != nil { + t.Fatalf("open stub gorm: %v", err) + } + return db +} + +// --- Test fixtures --------------------------------------------------------- + +type relUser struct { + ID uint + Name string + Books []*relBook `gorm:"-"` + Profile *relProfile `gorm:"-"` + Roles []*relRole `gorm:"-"` + Houses []*relHouse `gorm:"-"` + Logo *relLogo `gorm:"-"` +} + +func (relUser) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Books": contractsorm.HasMany{Related: &relBook{}, ForeignKey: "user_id"}, + "Profile": contractsorm.HasOne{Related: &relProfile{}, ForeignKey: "user_id"}, + "Roles": contractsorm.Many2Many{Related: &relRole{}, Table: "rel_user_roles"}, + "Houses": contractsorm.MorphMany{Related: &relHouse{}, Name: "houseable"}, + "Logo": contractsorm.MorphOne{Related: &relLogo{}, Name: "logoable"}, + } +} + +type relBook struct { + ID uint + Title string + UserID uint + AuthorID uint + Author *relUser `gorm:"-"` +} + +func (relBook) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Author": contractsorm.BelongsTo{Related: &relUser{}, ForeignKey: "author_id"}, + } +} + +type relProfile struct { + ID uint + Bio string + UserID uint +} + +type relRole struct { + ID uint + Name string +} + +type relHouse struct { + ID uint + Address string + HouseableID uint + HouseableType string +} + +type relLogo struct { + ID uint + URL string + LogoableID uint + LogoableType string +} + +// relCountry / relPost via relUser participate in a HasManyThrough setup. +type relCountry struct { + ID uint + Name string +} + +func (relCountry) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Posts": contractsorm.HasManyThrough{ + Related: &relPost{}, + Through: &relUser{}, + }, + "FirstPost": contractsorm.HasOneThrough{ + Related: &relPost{}, + Through: &relUser{}, + }, + "NoRelated": contractsorm.HasManyThrough{}, + "BadKind": unknownRelation{}, + } +} + +// unknownRelation satisfies contractsorm.Relation but isn't one of the known per-kind structs. +// Used to exercise the resolver's default branch (OrmMorphRelationKindUnknown) — a defensive +// path that triggers if someone hand-rolls a Relation impl outside the standard set. +type unknownRelation struct{} + +func (unknownRelation) Kind() contractsorm.RelationKind { return "weird" } + +type relPost struct { + ID uint + Title string + UserID uint +} + +// --- Pure helpers ---------------------------------------------------------- + +// --- Schema-dependent helpers --------------------------------------------- + +func TestTableNameFor(t *testing.T) { + db := newStubGormDB(t) + name, err := tableNameFor(db, &relUser{}) + assert.NoError(t, err) + assert.Equal(t, "rel_users", name) + + // Invalid (non-struct) model surfaces parse error. + _, err = tableNameFor(db, "not-a-model") + assert.Error(t, err) +} + +// --- resolveRelation across all kinds ------------------------------------- + +func TestResolveRelation_Empty(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &relUser{}, "") + assert.True(t, errors.Is(err, errors.OrmQueryEmptyRelation)) +} + +func TestResolveRelation_NotFound(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &relUser{}, "Missing") + assert.True(t, errors.Is(err, errors.OrmRelationNotFound)) +} + +func TestResolveRelation_HasMany(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Books") + assert.NoError(t, err) + assert.Equal(t, relKindHasMany, desc.kind) + assert.Equal(t, "rel_users", desc.parentTable) + assert.Equal(t, "rel_books", desc.relatedTable) + assert.NotEmpty(t, desc.references) +} + +func TestResolveRelation_HasOne(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Profile") + assert.NoError(t, err) + assert.Equal(t, relKindHasOne, desc.kind) + assert.Equal(t, "rel_profiles", desc.relatedTable) +} + +func TestResolveRelation_BelongsTo(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relBook{}, "Author") + assert.NoError(t, err) + assert.Equal(t, relKindBelongsTo, desc.kind) + assert.Equal(t, "rel_users", desc.relatedTable) +} + +func TestResolveRelation_Many2Many(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Roles") + assert.NoError(t, err) + assert.Equal(t, relKindMany2Many, desc.kind) + assert.Equal(t, "rel_user_roles", desc.pivotTable) + assert.Equal(t, "rel_users", desc.pivotParentRef.primaryTable) + assert.Equal(t, "rel_roles", desc.pivotRelatedRef.primaryTable) +} + +func TestResolveRelation_MorphMany(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Houses") + assert.NoError(t, err) + assert.Equal(t, relKindMorphMany, desc.kind) + assert.Equal(t, "houseable_type", desc.morphTypeColumn) + assert.Equal(t, "houseable_id", desc.morphIDColumn) + assert.NotEmpty(t, desc.references) +} + +func TestResolveRelation_MorphOne(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Logo") + assert.NoError(t, err) + assert.Equal(t, relKindMorphOne, desc.kind) + assert.Equal(t, "logoable_type", desc.morphTypeColumn) +} + +func TestResolveRelation_Nested(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Books.Author") + assert.NoError(t, err) + assert.Equal(t, "Books", desc.name) + assert.NotNil(t, desc.nested) + assert.Equal(t, "Author", desc.nested.name) + assert.Equal(t, relKindBelongsTo, desc.nested.kind) +} + +func TestResolveRelation_HasManyThrough(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relCountry{}, "Posts") + assert.NoError(t, err) + assert.Equal(t, relKindHasManyThrough, desc.kind) + assert.Equal(t, "rel_posts", desc.relatedTable) + assert.Equal(t, "rel_users", desc.throughTable) + // Through default keys come from naming conventions: + // firstKey = singular(parentTable) + "_id" + // secondKey = singular(throughTable) + "_id" + // localKey / secondLocalKey default to "id". + assert.Equal(t, "rel_country_id", desc.firstKey) + assert.Equal(t, "rel_user_id", desc.secondKey) + assert.Equal(t, "id", desc.localKey) + assert.Equal(t, "id", desc.secondLocalKey) +} + +func TestResolveRelation_HasOneThrough(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relCountry{}, "FirstPost") + assert.NoError(t, err) + assert.Equal(t, relKindHasOneThrough, desc.kind) +} + +func TestResolveRelation_ThroughNotConfigured(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &relCountry{}, "NoRelated") + assert.True(t, errors.Is(err, errors.OrmRelationThroughNotConfigured)) +} + +func TestResolveRelation_ThroughBadKind(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &relCountry{}, "BadKind") + assert.True(t, errors.Is(err, errors.OrmMorphRelationKindUnknown)) +} + +func TestResolveRelation_ThroughNotImplemented(t *testing.T) { + db := newStubGormDB(t) + // relUser does NOT implement ModelWithThroughRelations. + _, err := resolveRelation(db, &relUser{}, "Anything") + assert.True(t, errors.Is(err, errors.OrmRelationNotFound)) +} + +// Sanity: relatedModel is a fresh pointer to the related struct type. +func TestResolveRelation_RelatedModelType(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &relUser{}, "Books") + assert.NoError(t, err) + rt := reflect.TypeOf(desc.relatedModel) + assert.Equal(t, reflect.Pointer, rt.Kind()) + assert.Equal(t, "relBook", rt.Elem().Name()) +} + +// --- Morph relation fixtures --- + +type morphImage struct { + ID uint + URL string + ImageableID uint + ImageableType string + Imageable any `gorm:"-"` +} + +func (morphImage) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Imageable": contractsorm.MorphTo{Name: "imageable"}, + } +} + +type morphPost struct { + ID uint + Title string + Tags []*morphTag `gorm:"-"` +} + +func (morphPost) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Tags": contractsorm.MorphToMany{Related: &morphTag{}, Name: "taggable"}, + } +} + +type morphTag struct { + ID uint + Name string + Posts []*morphPost `gorm:"-"` +} + +func (morphTag) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Posts": contractsorm.MorphedByMany{Related: &morphPost{}, Name: "taggable"}, + } +} + +type morphBadKind struct{} + +func (morphBadKind) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "X": unknownRelation{}, + } +} + +type morphMissingRelated struct{} + +func (morphMissingRelated) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "X": contractsorm.MorphMany{Name: "imageable"}, + } +} + +// --- Morph relation resolution tests --- + +func TestResolveRelation_MorphTo(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &morphImage{}, "Imageable") + assert.NoError(t, err) + assert.Equal(t, relKindMorphTo, desc.kind) + assert.Equal(t, "imageable_type", desc.morphTypeColumn) + assert.Equal(t, "imageable_id", desc.morphIDColumn) + assert.Equal(t, "id", desc.morphOwnerKey) + // MorphTo has no single related model; it's resolved per-row. + assert.Nil(t, desc.relatedModel) +} + +func TestResolveRelation_MorphToMany(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &morphPost{}, "Tags") + assert.NoError(t, err) + assert.Equal(t, relKindMorphToMany, desc.kind) + assert.Equal(t, "taggables", desc.pivotTable) + assert.Equal(t, "taggable_type", desc.morphTypeColumn) + assert.Equal(t, "taggable_id", desc.morphIDColumn) + assert.False(t, desc.morphInverse) + // morphValue defaults to the parent's table name when no MorphClass / morph map override. + assert.Equal(t, "morph_posts", desc.morphValue) +} + +func TestResolveRelation_MorphedByMany(t *testing.T) { + db := newStubGormDB(t) + desc, err := resolveRelation(db, &morphTag{}, "Posts") + assert.NoError(t, err) + assert.Equal(t, relKindMorphToMany, desc.kind) + assert.True(t, desc.morphInverse) + // For inverse, the morph value pins on the related's morph value. + assert.Equal(t, "morph_posts", desc.morphValue) +} + +func TestResolveRelation_MorphBadKind(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &morphBadKind{}, "X") + assert.True(t, errors.Is(err, errors.OrmMorphRelationKindUnknown)) +} + +func TestResolveRelation_MorphMissingRelated(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &morphMissingRelated{}, "X") + assert.True(t, errors.Is(err, errors.OrmMorphRelationMissingField)) +} + +// --- Forbidden GORM relation tags --- + +type forbiddenPolymorphicParent struct { + ID uint + Houses []*forbiddenPolymorphicChild `gorm:"polymorphic:Houseable"` +} + +type forbiddenPolymorphicChild struct { + ID uint + HouseableID uint + HouseableType string +} + +type forbiddenForeignKeyParent struct { + ID uint + Books []*forbiddenForeignKeyChild `gorm:"foreignKey:ParentID"` +} + +type forbiddenForeignKeyChild struct { + ID uint + ParentID uint +} + +type forbiddenMany2ManyParent struct { + ID uint + Roles []*forbiddenMany2ManyChild `gorm:"many2many:parent_roles"` +} + +type forbiddenMany2ManyChild struct { + ID uint +} + +func TestResolveRelation_ForbidsPolymorphicTag(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &forbiddenPolymorphicParent{}, "Houses") + assert.True(t, errors.Is(err, errors.OrmRelationTagForbidden)) +} + +func TestResolveRelation_ForbidsForeignKeyTag(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &forbiddenForeignKeyParent{}, "Books") + assert.True(t, errors.Is(err, errors.OrmRelationTagForbidden)) +} + +func TestResolveRelation_ForbidsMany2ManyTag(t *testing.T) { + db := newStubGormDB(t) + _, err := resolveRelation(db, &forbiddenMany2ManyParent{}, "Roles") + assert.True(t, errors.Is(err, errors.OrmRelationTagForbidden)) +} From f3298109a8864da75861ca1c86e888385b7656af Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:25:08 +0800 Subject: [PATCH 05/11] feat(orm): add relation writer and pivot query for many-to-many 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 ee0fcde67825097e9c916354ee0f755be5365664) --- contracts/database/db/db.go | 9 + database/gorm/pivot_query.go | 88 ++ database/gorm/pivot_query_test.go | 309 ++++++ database/gorm/relation_writer.go | 114 +++ database/gorm/relation_writes.go | 1314 +++++++++++++++++++++++++ database/gorm/relation_writes_test.go | 622 ++++++++++++ 6 files changed, 2456 insertions(+) create mode 100644 database/gorm/pivot_query.go create mode 100644 database/gorm/pivot_query_test.go create mode 100644 database/gorm/relation_writer.go create mode 100644 database/gorm/relation_writes.go create mode 100644 database/gorm/relation_writes_test.go diff --git a/contracts/database/db/db.go b/contracts/database/db/db.go index c45249b60..afef1a74d 100644 --- a/contracts/database/db/db.go +++ b/contracts/database/db/db.go @@ -215,6 +215,15 @@ type Result struct { RowsAffected int64 } +// SyncResult reports the per-id outcome of a Sync / SyncWithoutDetaching / Toggle operation on +// a many-to-many (or polymorphic many-to-many) pivot table. Each slice carries the related ids +// in the order they were processed. +type SyncResult struct { + Attached []any + Detached []any + Updated []any +} + type Builder interface { CommonBuilder Beginx() (*sqlx.Tx, error) diff --git a/database/gorm/pivot_query.go b/database/gorm/pivot_query.go new file mode 100644 index 000000000..841973ad3 --- /dev/null +++ b/database/gorm/pivot_query.go @@ -0,0 +1,88 @@ +package gorm + +import ( + "fmt" + + gormio "gorm.io/gorm" + + contractsorm "github.com/goravel/framework/contracts/database/orm" +) + +// pivotQuery is the gorm-backed implementation of contractsorm.PivotQuery handed to user-supplied +// PivotCallback closures. It wraps a *gormio.DB chain so each Where* call appends a clause; the +// final *gormio.DB is read back via .db() and chained onto whatever query the caller is about to +// execute (SELECT for existingPivotIDs/allPivotIDs, DELETE for DetachRelation, UPDATE for +// UpdateExistingPivotRelation). +// +// Identifier qualification: the pivot table is always present in the surrounding query, so the +// callback is expected to pass bare column names (e.g. "active") — we prefix them with the pivot +// table name to avoid ambiguity when the surrounding query JOINs the related table. +type pivotQuery struct { + db *gormio.DB + tableName string +} + +func newPivotQuery(db *gormio.DB, tableName string) *pivotQuery { + return &pivotQuery{db: db, tableName: tableName} +} + +// qualified returns "." with the project's quoteIdent treatment, mirroring +// how relation_writes.go formats its hand-written WHERE clauses. +func (p *pivotQuery) qualified(column string) string { + return fmt.Sprintf("%s.%s", quoteIdent(p.tableName), quoteIdent(column)) +} + +func (p *pivotQuery) Where(column string, args ...any) contractsorm.PivotQuery { + switch len(args) { + case 1: + // (column, value) — operator defaults to "=". + p.db = p.db.Where(fmt.Sprintf("%s = ?", p.qualified(column)), args[0]) + case 2: + // (column, operator, value). + op, ok := args[0].(string) + if !ok { + // Defensive fallback: treat both args as values for an "=" + AND join. This shouldn't + // happen with normal usage but avoids a silent panic on bad input. + p.db = p.db.Where(fmt.Sprintf("%s = ?", p.qualified(column)), args[0]). + Where(fmt.Sprintf("%s = ?", p.qualified(column)), args[1]) + return p + } + p.db = p.db.Where(fmt.Sprintf("%s %s ?", p.qualified(column), op), args[1]) + default: + // 0 args or >2 args — no-op. The narrower interface should prevent this in practice. + } + return p +} + +func (p *pivotQuery) WhereIn(column string, values []any) contractsorm.PivotQuery { + p.db = p.db.Where(fmt.Sprintf("%s IN ?", p.qualified(column)), values) + return p +} + +func (p *pivotQuery) WhereNotIn(column string, values []any) contractsorm.PivotQuery { + p.db = p.db.Where(fmt.Sprintf("%s NOT IN ?", p.qualified(column)), values) + return p +} + +func (p *pivotQuery) WhereNull(column string) contractsorm.PivotQuery { + p.db = p.db.Where(fmt.Sprintf("%s IS NULL", p.qualified(column))) + return p +} + +func (p *pivotQuery) WhereNotNull(column string) contractsorm.PivotQuery { + p.db = p.db.Where(fmt.Sprintf("%s IS NOT NULL", p.qualified(column))) + return p +} + +// applyOnPivotQuery threads the descriptor's OnPivotQuery callback through q. No-op if the +// callback is nil. Used by every pivot-table read/update/delete code path so the per-relation +// scope is honoured uniformly. INSERT paths (AttachRelation / AttachWithPivotRelation) skip this +// — see the doc on contractsorm.PivotQuery for rationale. +func applyOnPivotQuery(q *gormio.DB, desc *relationDescriptor) *gormio.DB { + if desc.onPivotQuery == nil { + return q + } + pq := newPivotQuery(q, desc.pivotTable) + desc.onPivotQuery(pq) + return pq.db +} diff --git a/database/gorm/pivot_query_test.go b/database/gorm/pivot_query_test.go new file mode 100644 index 000000000..f54f8cb0c --- /dev/null +++ b/database/gorm/pivot_query_test.go @@ -0,0 +1,309 @@ +package gorm + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" + + contractsorm "github.com/goravel/framework/contracts/database/orm" +) + +// dryRunPivotSQL renders the SQL emitted by a SELECT 1 FROM ... query that pq has +// already mutated, so test cases can assert on exact WHERE-clause shape. +func dryRunPivotSQL(t *testing.T, db *gormio.DB, table string) string { + t.Helper() + stmt := db.Session(&gormio.Session{DryRun: true}).Table(table).Find(&[]map[string]any{}) + return stmt.Statement.SQL.String() +} + +func TestPivotQuery_Where_TwoArgs_DefaultsToEqual(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.Where("active", 1) + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.active = ?") +} + +func TestPivotQuery_Where_ThreeArgs_UsesOperator(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.Where("priority", ">=", 5) + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.priority >= ?") +} + +func TestPivotQuery_WhereIn(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.WhereIn("scope", []any{"team", "global"}) + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.scope IN (?,?)") +} + +func TestPivotQuery_WhereNotIn(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.WhereNotIn("scope", []any{"archived"}) + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.scope NOT IN (?)") +} + +func TestPivotQuery_WhereNull(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.WhereNull("deleted_at") + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.deleted_at IS NULL") +} + +func TestPivotQuery_WhereNotNull(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.WhereNotNull("expires_at") + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.expires_at IS NOT NULL") +} + +func TestPivotQuery_Chained_AccumulatesAllClauses(t *testing.T) { + db := newStubGormDB(t) + pq := newPivotQuery(db.Table("rel_user_roles"), "rel_user_roles") + pq.Where("active", 1). + WhereIn("scope", []any{"team", "global"}). + WhereNull("deleted_at") + sql := dryRunPivotSQL(t, pq.db, "rel_user_roles") + // All three predicates should be ANDed into the final query. + assert.Contains(t, sql, "rel_user_roles.active = ?") + assert.Contains(t, sql, "rel_user_roles.scope IN (?,?)") + assert.Contains(t, sql, "rel_user_roles.deleted_at IS NULL") + // Naive count: three "AND" or three predicates joined. + assert.Equal(t, 2, strings.Count(sql, " AND "), "three predicates should be joined by two ANDs") +} + +func TestApplyOnPivotQuery_NilCallback_NoOp(t *testing.T) { + db := newStubGormDB(t) + desc := &relationDescriptor{pivotTable: "rel_user_roles", onPivotQuery: nil} + q := db.Table("rel_user_roles") + out := applyOnPivotQuery(q, desc) + assert.Same(t, q, out, "nil callback must return the original query unchanged") +} + +func TestApplyOnPivotQuery_AppliesCallback(t *testing.T) { + db := newStubGormDB(t) + desc := &relationDescriptor{ + pivotTable: "rel_user_roles", + onPivotQuery: func(q contractsorm.PivotQuery) contractsorm.PivotQuery { + return q.Where("active", 1) + }, + } + q := db.Table("rel_user_roles") + out := applyOnPivotQuery(q, desc) + sql := dryRunPivotSQL(t, out, "rel_user_roles") + assert.Contains(t, sql, "rel_user_roles.active = ?") +} + +// Descriptor wiring: OnPivotQuery declared on Many2Many / MorphToMany / MorphedByMany must land +// on the relationDescriptor. +type pivotWireUser struct { + ID uint + Roles []*relRole `gorm:"-"` +} + +func (pivotWireUser) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Roles": contractsorm.Many2Many{ + Related: &relRole{}, + Table: "pivot_wire_user_roles", + OnPivotQuery: func(q contractsorm.PivotQuery) contractsorm.PivotQuery { + return q.Where("active", 1) + }, + }, + } +} + +func TestDescriptor_OnPivotQuery_Many2Many(t *testing.T) { + q := newRelQueryWith(t, &pivotWireUser{}) + desc, err := resolveRelation(q.instance, &pivotWireUser{}, "Roles") + assert.NoError(t, err) + assert.NotNil(t, desc.onPivotQuery, "Many2Many.OnPivotQuery must land on descriptor") + + // Verify the callback actually runs. + pq := newPivotQuery(newStubGormDB(t).Table("pivot_wire_user_roles"), "pivot_wire_user_roles") + desc.onPivotQuery(pq) + sql := dryRunPivotSQL(t, pq.db, "pivot_wire_user_roles") + assert.Contains(t, sql, "pivot_wire_user_roles.active = ?") +} + +type pivotWireMorphPost struct { + ID uint + Tags []*morphTag `gorm:"-"` +} + +func (pivotWireMorphPost) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Tags": contractsorm.MorphToMany{ + Related: &morphTag{}, + Name: "taggable", + OnPivotQuery: func(q contractsorm.PivotQuery) contractsorm.PivotQuery { + return q.WhereNull("deleted_at") + }, + }, + } +} + +func TestDescriptor_OnPivotQuery_MorphToMany(t *testing.T) { + q := newRelQueryWith(t, &pivotWireMorphPost{}) + desc, err := resolveRelation(q.instance, &pivotWireMorphPost{}, "Tags") + assert.NoError(t, err) + assert.NotNil(t, desc.onPivotQuery, "MorphToMany.OnPivotQuery must land on descriptor") +} + +type pivotWireTag struct { + ID uint + Posts []*morphPost `gorm:"-"` +} + +func (pivotWireTag) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Posts": contractsorm.MorphedByMany{ + Related: &morphPost{}, + Name: "taggable", + OnPivotQuery: func(q contractsorm.PivotQuery) contractsorm.PivotQuery { + return q.Where("verified", 1) + }, + }, + } +} + +func TestDescriptor_OnPivotQuery_MorphedByMany(t *testing.T) { + q := newRelQueryWith(t, &pivotWireTag{}) + desc, err := resolveRelation(q.instance, &pivotWireTag{}, "Posts") + assert.NoError(t, err) + assert.NotNil(t, desc.onPivotQuery, "MorphedByMany.OnPivotQuery must land on descriptor") +} + +// Touches wiring — the relation declarations carry Touches: true through to the descriptor. + +type touchesUser struct { + ID uint + Roles []*relRole `gorm:"-"` +} + +func (touchesUser) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Roles": contractsorm.Many2Many{Related: &relRole{}, Table: "touches_user_roles", Touches: true}, + } +} + +func TestDescriptor_Touches_Many2Many(t *testing.T) { + q := newRelQueryWith(t, &touchesUser{}) + desc, err := resolveRelation(q.instance, &touchesUser{}, "Roles") + assert.NoError(t, err) + assert.True(t, desc.touches) +} + +type touchesPost struct { + ID uint + Tags []*morphTag `gorm:"-"` +} + +func (touchesPost) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Tags": contractsorm.MorphToMany{Related: &morphTag{}, Name: "taggable", Touches: true}, + } +} + +func TestDescriptor_Touches_MorphToMany(t *testing.T) { + q := newRelQueryWith(t, &touchesPost{}) + desc, err := resolveRelation(q.instance, &touchesPost{}, "Tags") + assert.NoError(t, err) + assert.True(t, desc.touches) +} + +type touchesTag struct { + ID uint + Posts []*morphPost `gorm:"-"` +} + +func (touchesTag) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Posts": contractsorm.MorphedByMany{Related: &morphPost{}, Name: "taggable", Touches: true}, + } +} + +func TestDescriptor_Touches_MorphedByMany(t *testing.T) { + q := newRelQueryWith(t, &touchesTag{}) + desc, err := resolveRelation(q.instance, &touchesTag{}, "Posts") + assert.NoError(t, err) + assert.True(t, desc.touches) +} + +// Default: when Touches is omitted, descriptor.touches is false. Sanity guard against accidental +// always-on behavior. +func TestDescriptor_Touches_DefaultsFalse(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + assert.False(t, desc.touches) +} + +func TestTouchIfTouching_DescTouchesFalse_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc := &relationDescriptor{touches: false} + // parent ptr is nil-safe here because the function returns before dereferencing. + err := q.touchIfTouching(desc, &relUser{ID: 7}, uint(7)) + assert.NoError(t, err) +} + +func TestTouchIfTouching_NoUpdatedAtField_NoOp(t *testing.T) { + // relRole has no UpdatedAt field — touchIfTouching must silently skip even when touches=true. + q := newRelQueryWith(t, &relRole{}) + desc := &relationDescriptor{touches: true} + err := q.touchIfTouching(desc, &relRole{ID: 1}, uint(1)) + assert.NoError(t, err) +} + +// PivotField wiring — defaults to "Pivot" when omitted, takes the user-specified value otherwise. + +type pivotFieldUser struct { + ID uint + Roles []*relRole `gorm:"-"` +} + +func (pivotFieldUser) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Roles": contractsorm.Many2Many{Related: &relRole{}, Table: "pf_user_roles"}, + "AuditedRoles": contractsorm.Many2Many{Related: &relRole{}, Table: "pf_audit_roles", PivotField: "AuditPivot"}, + "TaggedPosts": contractsorm.MorphToMany{Related: &morphTag{}, Name: "taggable", PivotField: "TagPivot"}, + "InversedTagged": contractsorm.MorphedByMany{Related: &morphPost{}, Name: "taggable", PivotField: "InversePivot"}, + } +} + +func TestDescriptor_PivotField_DefaultsToPivot(t *testing.T) { + q := newRelQueryWith(t, &pivotFieldUser{}) + desc, err := resolveRelation(q.instance, &pivotFieldUser{}, "Roles") + assert.NoError(t, err) + assert.Equal(t, "Pivot", desc.pivotField) +} + +func TestDescriptor_PivotField_CustomMany2Many(t *testing.T) { + q := newRelQueryWith(t, &pivotFieldUser{}) + desc, err := resolveRelation(q.instance, &pivotFieldUser{}, "AuditedRoles") + assert.NoError(t, err) + assert.Equal(t, "AuditPivot", desc.pivotField) +} + +func TestDescriptor_PivotField_CustomMorphToMany(t *testing.T) { + q := newRelQueryWith(t, &pivotFieldUser{}) + desc, err := resolveRelation(q.instance, &pivotFieldUser{}, "TaggedPosts") + assert.NoError(t, err) + assert.Equal(t, "TagPivot", desc.pivotField) +} + +func TestDescriptor_PivotField_CustomMorphedByMany(t *testing.T) { + q := newRelQueryWith(t, &pivotFieldUser{}) + desc, err := resolveRelation(q.instance, &pivotFieldUser{}, "InversedTagged") + assert.NoError(t, err) + assert.Equal(t, "InversePivot", desc.pivotField) +} diff --git a/database/gorm/relation_writer.go b/database/gorm/relation_writer.go new file mode 100644 index 000000000..8c8c4b9d6 --- /dev/null +++ b/database/gorm/relation_writer.go @@ -0,0 +1,114 @@ +package gorm + +import ( + dbcontract "github.com/goravel/framework/contracts/database/db" + contractsorm "github.com/goravel/framework/contracts/database/orm" +) + +// relationWriter binds (parent, name) to a Query session and forwards each write to the +// matching *Relation-suffixed method on *Query. It implements contractsorm.RelationWriter so +// callers can use a single chained entry — Query.Relation(parent, name) — for all writes. +type relationWriter struct { + q *Query + parent any + name string +} + +// Relation returns a RelationWriter bound to the given parent and relation name. The returned +// builder forwards all write operations to the receiver's session, so calls inside a Transaction +// callback honor the transaction. +func (r *Query) Relation(parent any, name string) contractsorm.RelationWriter { + return &relationWriter{q: r, parent: parent, name: name} +} + +func (w *relationWriter) Save(child any) error { + return w.q.SaveRelation(w.parent, w.name, child) +} + +func (w *relationWriter) SaveMany(children any) error { + return w.q.SaveManyRelation(w.parent, w.name, children) +} + +func (w *relationWriter) SaveWithPivot(child any, attrs map[string]any) error { + return w.q.SaveRelationWithPivot(w.parent, w.name, child, attrs) +} + +func (w *relationWriter) SaveManyWithPivot(children any, attrsPerChild map[any]map[string]any) error { + return w.q.SaveManyRelationWithPivot(w.parent, w.name, children, attrsPerChild) +} + +func (w *relationWriter) Create(dest any) error { + return w.q.CreateRelation(w.parent, w.name, dest) +} + +func (w *relationWriter) CreateMany(dests any) error { + return w.q.CreateManyRelation(w.parent, w.name, dests) +} + +func (w *relationWriter) FindOrNew(id any, dest any) error { + return w.q.FindOrNewRelation(w.parent, w.name, id, dest) +} + +func (w *relationWriter) FirstOrNew(attrs, values map[string]any, dest any) error { + return w.q.FirstOrNewRelation(w.parent, w.name, attrs, values, dest) +} + +func (w *relationWriter) FirstOrCreate(attrs, values map[string]any, dest any) error { + return w.q.FirstOrCreateRelation(w.parent, w.name, attrs, values, dest) +} + +func (w *relationWriter) UpdateOrCreate(attrs, values map[string]any, dest any) error { + return w.q.UpdateOrCreateRelation(w.parent, w.name, attrs, values, dest) +} + +func (w *relationWriter) Associate(owner any) error { + return w.q.AssociateRelation(w.parent, w.name, owner) +} + +func (w *relationWriter) Dissociate() error { + return w.q.DissociateRelation(w.parent, w.name) +} + +func (w *relationWriter) Attach(ids []any) error { + return w.q.AttachRelation(w.parent, w.name, ids) +} + +func (w *relationWriter) AttachWithPivot(idsWithAttrs map[any]map[string]any) error { + return w.q.AttachWithPivotRelation(w.parent, w.name, idsWithAttrs) +} + +func (w *relationWriter) Detach(ids ...any) (int64, error) { + return w.q.DetachRelation(w.parent, w.name, ids) +} + +func (w *relationWriter) Sync(ids []any) (*dbcontract.SyncResult, error) { + return w.q.SyncRelation(w.parent, w.name, ids) +} + +func (w *relationWriter) SyncWithPivot(idsWithAttrs map[any]map[string]any) (*dbcontract.SyncResult, error) { + return w.q.SyncRelationWithPivot(w.parent, w.name, idsWithAttrs) +} + +func (w *relationWriter) SyncWithPivotValues(ids []any, pivotValues map[string]any) (*dbcontract.SyncResult, error) { + return w.q.SyncRelationWithPivotValues(w.parent, w.name, ids, pivotValues) +} + +func (w *relationWriter) SyncWithoutDetaching(ids []any) (*dbcontract.SyncResult, error) { + return w.q.SyncWithoutDetachingRelation(w.parent, w.name, ids) +} + +func (w *relationWriter) SyncWithoutDetachingWithPivot(idsWithAttrs map[any]map[string]any) (*dbcontract.SyncResult, error) { + return w.q.SyncWithoutDetachingRelationWithPivot(w.parent, w.name, idsWithAttrs) +} + +func (w *relationWriter) Toggle(ids []any) (*dbcontract.SyncResult, error) { + return w.q.ToggleRelation(w.parent, w.name, ids) +} + +func (w *relationWriter) ToggleWithPivot(idsWithAttrs map[any]map[string]any) (*dbcontract.SyncResult, error) { + return w.q.ToggleRelationWithPivot(w.parent, w.name, idsWithAttrs) +} + +func (w *relationWriter) UpdateExistingPivot(id any, attrs map[string]any) (int64, error) { + return w.q.UpdateExistingPivotRelation(w.parent, w.name, id, attrs) +} diff --git a/database/gorm/relation_writes.go b/database/gorm/relation_writes.go new file mode 100644 index 000000000..3ca56c5c6 --- /dev/null +++ b/database/gorm/relation_writes.go @@ -0,0 +1,1314 @@ +package gorm + +import ( + "fmt" + "reflect" + "strconv" + "time" + + dbcontract "github.com/goravel/framework/contracts/database/db" + "github.com/goravel/framework/errors" +) + +// SaveRelation inserts or updates child as a member of parent's relation. Sets child's foreign +// key (and morph_type for MorphOne/MorphMany) from parent's local key, then persists child via +// Query.Save. For BelongsToMany kinds (Many2Many, MorphToMany, MorphedByMany) persists child first, +// then writes a pivot row linking parent and child. +// +// Public Query-level helper used by Orm.Save. Named "SaveRelation" to avoid clashing with the +// existing single-arg Query.Save(value any) which persists a model directly. +// +// Supported kinds: HasOne, HasMany, MorphOne, MorphMany, Many2Many, MorphToMany, MorphedByMany. +// Other kinds error with OrmRelationKindNotSupported. +func (r *Query) SaveRelation(parent any, relation string, child any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(child) { + return errors.OrmRelationParentNotPointer.Args(child) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + if err := r.setRelationFKOnChild(parent, child, desc); err != nil { + return err + } + return r.wrap(r.freshSession()).Save(child) + case relKindMany2Many, relKindMorphToMany: + // Persist child first, then attach via pivot. + if err := r.wrap(r.freshSession()).Save(child); err != nil { + return err + } + childPK, err := readParentColumn(r, child, desc.pivotRelatedRef.primaryColumn) + if err != nil { + return err + } + return r.AttachRelation(parent, relation, []any{childPK}) + default: + return errors.OrmRelationKindNotSupported.Args("Save", relation, kindName(desc.kind)) + } +} + +// SaveManyRelation is the slice form of SaveRelation. children must be a slice or pointer-to- +// slice of either pointer-to-struct or struct elements. Iterates and bails on first error. +func (r *Query) SaveManyRelation(parent any, relation string, children any) error { + rv := reflect.ValueOf(children) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Slice { + return errors.OrmRelationKindNotSupported.Args("SaveMany", relation, fmt.Sprintf("children=%T (must be slice)", children)) + } + for i := 0; i < rv.Len(); i++ { + item := rv.Index(i) + var elem any + switch item.Kind() { + case reflect.Pointer: + elem = item.Interface() + case reflect.Struct: + if !item.CanAddr() { + ptr := reflect.New(item.Type()) + ptr.Elem().Set(item) + elem = ptr.Interface() + } else { + elem = item.Addr().Interface() + } + default: + return errors.OrmRelationKindNotSupported.Args("SaveMany", relation, fmt.Sprintf("children element=%s", item.Kind())) + } + if err := r.SaveRelation(parent, relation, elem); err != nil { + return err + } + } + return nil +} + +// SaveRelationWithPivot is SaveRelation with caller-supplied pivot column values for the +// BelongsToMany family. On HasOneOrMany kinds attrs is ignored (no pivot row). +func (r *Query) SaveRelationWithPivot(parent any, relation string, child any, attrs map[string]any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(child) { + return errors.OrmRelationParentNotPointer.Args(child) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + // No pivot — just delegate to SaveRelation. + return r.SaveRelation(parent, relation, child) + case relKindMany2Many, relKindMorphToMany: + // Persist child first, then attach via pivot with attrs. + if err := r.wrap(r.freshSession()).Save(child); err != nil { + return err + } + childPK, err := readParentColumn(r, child, desc.pivotRelatedRef.primaryColumn) + if err != nil { + return err + } + return r.AttachWithPivotRelation(parent, relation, map[any]map[string]any{childPK: attrs}) + default: + return errors.OrmRelationKindNotSupported.Args("SaveWithPivot", relation, kindName(desc.kind)) + } +} + +// SaveManyRelationWithPivot is the slice form of SaveRelationWithPivot. attrsPerChild is keyed by +// the related PK of each child; an entry may be nil to attach without extra columns. +func (r *Query) SaveManyRelationWithPivot(parent any, relation string, children any, attrsPerChild map[any]map[string]any) error { + rv := reflect.ValueOf(children) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Slice { + return errors.OrmRelationKindNotSupported.Args("SaveManyWithPivot", relation, fmt.Sprintf("children=%T (must be slice)", children)) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + for i := 0; i < rv.Len(); i++ { + item := rv.Index(i) + var elem any + switch item.Kind() { + case reflect.Pointer: + elem = item.Interface() + case reflect.Struct: + if !item.CanAddr() { + ptr := reflect.New(item.Type()) + ptr.Elem().Set(item) + elem = ptr.Interface() + } else { + elem = item.Addr().Interface() + } + default: + return errors.OrmRelationKindNotSupported.Args("SaveManyWithPivot", relation, fmt.Sprintf("children element=%s", item.Kind())) + } + // Read child's PK to look up attrs. + childPK, err := readParentColumn(r, elem, desc.pivotRelatedRef.primaryColumn) + if err != nil { + return err + } + attrs := attrsPerChild[childPK] + if err := r.SaveRelationWithPivot(parent, relation, elem, attrs); err != nil { + return err + } + } + return nil +} + +// AssociateRelation sets parent's foreign key (and morph_type for MorphTo) to point at owner, +// then persists parent. Supported kinds: BelongsTo, MorphTo. owner must be a non-nil pointer to +// a struct. +// +// Public Query-level helper used by Orm.Associate. +func (r *Query) AssociateRelation(parent any, relation string, owner any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(owner) { + return errors.OrmRelationParentNotPointer.Args(owner) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindBelongsTo: + return r.applyAssociate(parent, owner, desc, false) + case relKindMorphTo: + return r.applyAssociate(parent, owner, desc, true) + default: + return errors.OrmRelationKindNotSupported.Args("Associate", relation, kindName(desc.kind)) + } +} + +// DissociateRelation clears parent's foreign key (and morph_type for MorphTo) and persists +// parent. Supported kinds: BelongsTo, MorphTo. +func (r *Query) DissociateRelation(parent any, relation string) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindBelongsTo: + return r.applyDissociate(parent, desc, false) + case relKindMorphTo: + return r.applyDissociate(parent, desc, true) + default: + return errors.OrmRelationKindNotSupported.Args("Dissociate", relation, kindName(desc.kind)) + } +} + +// applyAssociate writes owner's PK into parent's FK column (and the morph_type column for +// MorphTo, resolved from the morph map / MorphClass()), then persists parent. +func (r *Query) applyAssociate(parent, owner any, desc *relationDescriptor, isMorph bool) error { + if err := r.mutateAssociate(parent, owner, desc, isMorph); err != nil { + return err + } + return r.wrap(r.freshSession()).Save(parent) +} + +// mutateAssociate is the pure-mutation half of applyAssociate. Writes owner's PK into parent's +// FK column (and the morph_type column for MorphTo). No persistence. +func (r *Query) mutateAssociate(parent, owner any, desc *relationDescriptor, isMorph bool) error { + parentSchema, err := parseGormSchema(r.instance, parent) + if err != nil { + return err + } + parentRV := reflect.ValueOf(parent).Elem() + + var fkColumn string + if isMorph { + fkColumn = desc.morphIDColumn + } else { + if len(desc.references) == 0 { + return errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, "no references") + } + fkColumn = desc.references[0].foreignColumn + } + fkField, ok := parentSchema.FieldsByDBName[fkColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(desc.name, parentSchema.Name, "no FK field "+fkColumn) + } + + ownerPKColumn := "id" + if !isMorph && len(desc.references) > 0 { + ownerPKColumn = desc.references[0].primaryColumn + } else if isMorph && desc.morphOwnerKey != "" { + ownerPKColumn = desc.morphOwnerKey + } + ownerPK, err := readParentColumn(r, owner, ownerPKColumn) + if err != nil { + return err + } + if err := fkField.Set(r.ctx, parentRV, ownerPK); err != nil { + return err + } + + if isMorph { + typeField, ok := parentSchema.FieldsByDBName[desc.morphTypeColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(desc.name, parentSchema.Name, "no morph type field "+desc.morphTypeColumn) + } + alias, ok := resolveMorphAlias(owner) + if !ok { + tbl, terr := tableNameFor(r.instance, owner) + if terr != nil { + return terr + } + alias = tbl + } + if err := typeField.Set(r.ctx, parentRV, alias); err != nil { + return err + } + } + return nil +} + +// applyDissociate sets parent's FK to the zero value (and morph_type to "" for MorphTo), then +// persists parent. +func (r *Query) applyDissociate(parent any, desc *relationDescriptor, isMorph bool) error { + if err := r.mutateDissociate(parent, desc, isMorph); err != nil { + return err + } + return r.wrap(r.freshSession()).Save(parent) +} + +// mutateDissociate is the pure-mutation half of applyDissociate. +func (r *Query) mutateDissociate(parent any, desc *relationDescriptor, isMorph bool) error { + parentSchema, err := parseGormSchema(r.instance, parent) + if err != nil { + return err + } + parentRV := reflect.ValueOf(parent).Elem() + + var fkColumn string + if isMorph { + fkColumn = desc.morphIDColumn + } else { + if len(desc.references) == 0 { + return errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, "no references") + } + fkColumn = desc.references[0].foreignColumn + } + fkField, ok := parentSchema.FieldsByDBName[fkColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(desc.name, parentSchema.Name, "no FK field "+fkColumn) + } + zero := reflect.Zero(fkField.FieldType).Interface() + if err := fkField.Set(r.ctx, parentRV, zero); err != nil { + return err + } + + if isMorph { + typeField, ok := parentSchema.FieldsByDBName[desc.morphTypeColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(desc.name, parentSchema.Name, "no morph type field "+desc.morphTypeColumn) + } + zeroType := reflect.Zero(typeField.FieldType).Interface() + if err := typeField.Set(r.ctx, parentRV, zeroType); err != nil { + return err + } + } + return nil +} + +// CreateRelation persists a new related row. For HasOneOrMany kinds (HasOne, HasMany, MorphOne, +// MorphMany) the framework first sets the FK (and morph type column) on dest from parent, then +// inserts. dest must be a non-nil pointer to a struct of the related type. +// +// Public Query-level helper used by Orm.Create. +func (r *Query) CreateRelation(parent any, relation string, dest any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(dest) { + return errors.OrmRelationParentNotPointer.Args(dest) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + if err := r.setRelationFKOnChild(parent, dest, desc); err != nil { + return err + } + return r.wrap(r.freshSession()).Create(dest) + case relKindMany2Many, relKindMorphToMany: + // Create dest first, then attach via pivot. + if err := r.wrap(r.freshSession()).Create(dest); err != nil { + return err + } + childPK, err := readParentColumn(r, dest, desc.pivotRelatedRef.primaryColumn) + if err != nil { + return err + } + return r.AttachRelation(parent, relation, []any{childPK}) + default: + return errors.OrmRelationKindNotSupported.Args("Create", relation, kindName(desc.kind)) + } +} + +// CreateManyRelation is the slice form of CreateRelation. dests must be a slice or a pointer to a +// slice; iterates and calls CreateRelation per element, bailing on the first error. +func (r *Query) CreateManyRelation(parent any, relation string, dests any) error { + rv := reflect.ValueOf(dests) + if rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + if rv.Kind() != reflect.Slice { + return errors.OrmRelationUnsupported.Args(relation, fmt.Sprintf("%T", parent), "CreateMany requires a slice") + } + for i := 0; i < rv.Len(); i++ { + elem := rv.Index(i) + if elem.Kind() != reflect.Pointer { + elem = elem.Addr() + } + if err := r.CreateRelation(parent, relation, elem.Interface()); err != nil { + return err + } + } + return nil +} + +// FindOrNewRelation finds the related row with primary key id. If absent, fills dest with a new +// instance of the related model and pre-sets the FK (and morph type) — but does NOT persist. +// dest must be a pointer to a struct. +func (r *Query) FindOrNewRelation(parent any, relation string, id any, dest any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(dest) { + return errors.OrmRelationParentNotPointer.Args(dest) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + q := r.newRelationQuery(parent, relation) + if err := q.Find(dest, id); err != nil { + return err + } + // Check if Find actually populated dest by inspecting the PK field. + schema, err := parseGormSchema(r.instance, dest) + if err != nil { + return err + } + if len(schema.PrimaryFields) == 0 { + return errors.OrmRelationUnsupported.Args(relation, schema.Name, "no primary key") + } + pkField := schema.PrimaryFields[0] + rv := reflect.ValueOf(dest).Elem() + pkVal, isZero := pkField.ValueOf(r.ctx, rv) + _ = pkVal + if isZero { + // Not found — set FK on the zero-valued dest. + return r.setRelationFKOnChild(parent, dest, desc) + } + return nil + default: + return errors.OrmRelationKindNotSupported.Args("FindOrNew", relation, kindName(desc.kind)) + } +} + +// FirstOrNewRelation finds the first related row matching attrs. If absent, fills dest with a new +// instance carrying attrs+values and pre-set FK — does NOT persist. +func (r *Query) FirstOrNewRelation(parent any, relation string, attrs map[string]any, values map[string]any, dest any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(dest) { + return errors.OrmRelationParentNotPointer.Args(dest) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + q := r.newRelationQuery(parent, relation) + for col, val := range attrs { + q = q.Where(col, val) + } + if err := q.First(dest); err != nil { + // Not found — overlay attrs+values and set FK. + if err := r.applyAttrMap(dest, attrs); err != nil { + return err + } + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + return r.setRelationFKOnChild(parent, dest, desc) + } + return nil + default: + return errors.OrmRelationKindNotSupported.Args("FirstOrNew", relation, kindName(desc.kind)) + } +} + +// FirstOrCreateRelation is FirstOrNewRelation that persists when no matching row exists. +func (r *Query) FirstOrCreateRelation(parent any, relation string, attrs map[string]any, values map[string]any, dest any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(dest) { + return errors.OrmRelationParentNotPointer.Args(dest) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + q := r.newRelationQuery(parent, relation) + for col, val := range attrs { + q = q.Where(col, val) + } + if err := q.First(dest); err != nil { + // Not found — overlay attrs+values, set FK, persist. + if err := r.applyAttrMap(dest, attrs); err != nil { + return err + } + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + if err := r.setRelationFKOnChild(parent, dest, desc); err != nil { + return err + } + return r.wrap(r.freshSession()).Create(dest) + } + return nil + case relKindMany2Many, relKindMorphToMany: + // For m2m: search the related table directly (no FK constraint), create+attach if missing. + q := r.freshSession().Model(desc.relatedModel) + for col, val := range attrs { + q = q.Where(col, val) + } + if err := r.wrap(q).First(dest); err != nil { + // Not found — overlay attrs+values, create, attach. + if err := r.applyAttrMap(dest, attrs); err != nil { + return err + } + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + if err := r.wrap(r.freshSession()).Create(dest); err != nil { + return err + } + childPK, err := readParentColumn(r, dest, desc.pivotRelatedRef.primaryColumn) + if err != nil { + return err + } + return r.AttachRelation(parent, relation, []any{childPK}) + } + return nil + default: + return errors.OrmRelationKindNotSupported.Args("FirstOrCreate", relation, kindName(desc.kind)) + } +} + +// UpdateOrCreateRelation finds the first related row matching attrs (or creates one), then overlays +// values onto it and persists. Always saves dest. +func (r *Query) UpdateOrCreateRelation(parent any, relation string, attrs map[string]any, values map[string]any, dest any) error { + if !isValidParent(parent) { + return errors.OrmRelationParentNotPointer.Args(parent) + } + if !isValidParent(dest) { + return errors.OrmRelationParentNotPointer.Args(dest) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return err + } + switch desc.kind { + case relKindHasOne, relKindHasMany, relKindMorphOne, relKindMorphMany: + // FirstOrNew logic. + q := r.newRelationQuery(parent, relation) + for col, val := range attrs { + q = q.Where(col, val) + } + if err := q.First(dest); err != nil { + // Not found — overlay attrs+values and set FK. + if err := r.applyAttrMap(dest, attrs); err != nil { + return err + } + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + if err := r.setRelationFKOnChild(parent, dest, desc); err != nil { + return err + } + } else { + // Found — overlay values only. + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + } + return r.wrap(r.freshSession()).Save(dest) + case relKindMany2Many, relKindMorphToMany: + // For m2m: search the related table directly, create+attach if missing, otherwise update. + q := r.freshSession().Model(desc.relatedModel) + for col, val := range attrs { + q = q.Where(col, val) + } + var freshlyCreated bool + if err := r.wrap(q).First(dest); err != nil { + // Not found — overlay attrs+values, create. + if err := r.applyAttrMap(dest, attrs); err != nil { + return err + } + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + if err := r.wrap(r.freshSession()).Create(dest); err != nil { + return err + } + freshlyCreated = true + } else { + // Found — overlay values only. + if err := r.applyAttrMap(dest, values); err != nil { + return err + } + if err := r.wrap(r.freshSession()).Save(dest); err != nil { + return err + } + } + // Attach if freshly created. + if freshlyCreated { + childPK, err := readParentColumn(r, dest, desc.pivotRelatedRef.primaryColumn) + if err != nil { + return err + } + return r.AttachRelation(parent, relation, []any{childPK}) + } + return nil + default: + return errors.OrmRelationKindNotSupported.Args("UpdateOrCreate", relation, kindName(desc.kind)) + } +} + +// AttachRelation inserts pivot rows linking parent to each id in ids. Skips ids that already +// have a pivot row. Supported kinds: Many2Many, MorphToMany, MorphedByMany. +// +// Public Query-level helper used by Orm.Attach. +func (r *Query) AttachRelation(parent any, relation string, ids []any) error { + desc, parentVal, err := r.resolvePivot(parent, relation, "Attach") + if err != nil { + return err + } + affected, err := r.doAttach(desc, parentVal, ids, nil) + if err != nil { + return err + } + if affected > 0 { + return r.touchIfTouching(desc, parent, parentVal) + } + return nil +} + +// AttachWithPivotRelation is Attach with per-row pivot column values. +func (r *Query) AttachWithPivotRelation(parent any, relation string, idsWithAttrs map[any]map[string]any) error { + desc, parentVal, err := r.resolvePivot(parent, relation, "AttachWithPivot") + if err != nil { + return err + } + affected, err := r.doAttachWithPivot(desc, parentVal, idsWithAttrs) + if err != nil { + return err + } + if affected > 0 { + return r.touchIfTouching(desc, parent, parentVal) + } + return nil +} + +// doAttach is the shared work-horse for AttachRelation / AttachWithPivotRelation. It does not +// trigger touchIfTouching — callers (the public methods, and syncCore) own when to touch. Returns +// the number of newly-inserted pivot rows; zero when every id was already attached. +// +// idsWithAttrs is optional: when non-nil, attrs from this map are merged into each new row; +// when nil, ids alone drive the insert. +func (r *Query) doAttach(desc *relationDescriptor, parentVal any, ids []any, idsWithAttrs map[any]map[string]any) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + existing, err := r.existingPivotIDs(desc, parentVal, ids) + if err != nil { + return 0, err + } + rows := make([]map[string]any, 0, len(ids)) + for _, id := range ids { + if _, dup := existing[dictKey(id)]; dup { + continue + } + var attrs map[string]any + if idsWithAttrs != nil { + attrs = idsWithAttrs[id] + } + rows = append(rows, r.basePivotRow(desc, parentVal, id, attrs)) + } + if len(rows) == 0 { + return 0, nil + } + if err := r.freshSession().Table(desc.pivotTable).Create(rows).Error; err != nil { + return 0, err + } + return int64(len(rows)), nil +} + +// doAttachWithPivot is doAttach for the with-attrs entry shape (map keyed by id). Equivalent in +// outcome to doAttach with the same attrs; kept separate to avoid materialising an ids slice +// just to satisfy the doAttach signature when callers already have a map. +func (r *Query) doAttachWithPivot(desc *relationDescriptor, parentVal any, idsWithAttrs map[any]map[string]any) (int64, error) { + if len(idsWithAttrs) == 0 { + return 0, nil + } + ids := make([]any, 0, len(idsWithAttrs)) + for id := range idsWithAttrs { + ids = append(ids, id) + } + existing, err := r.existingPivotIDs(desc, parentVal, ids) + if err != nil { + return 0, err + } + rows := make([]map[string]any, 0, len(ids)) + for id, attrs := range idsWithAttrs { + if _, dup := existing[dictKey(id)]; dup { + continue + } + rows = append(rows, r.basePivotRow(desc, parentVal, id, attrs)) + } + if len(rows) == 0 { + return 0, nil + } + if err := r.freshSession().Table(desc.pivotTable).Create(rows).Error; err != nil { + return 0, err + } + return int64(len(rows)), nil +} + +// DetachRelation removes pivot rows linking parent to the given ids. With nil/empty ids, removes +// all pivot rows for parent (and morph type, for polymorphic). Returns the number of rows +// removed. +func (r *Query) DetachRelation(parent any, relation string, ids []any) (int64, error) { + desc, parentVal, err := r.resolvePivot(parent, relation, "Detach") + if err != nil { + return 0, err + } + affected, err := r.doDetach(desc, parentVal, ids) + if err != nil { + return 0, err + } + if affected > 0 { + if err := r.touchIfTouching(desc, parent, parentVal); err != nil { + return affected, err + } + } + return affected, nil +} + +// doDetach is the no-touch worker behind DetachRelation. Returns rows affected by the DELETE. +func (r *Query) doDetach(desc *relationDescriptor, parentVal any, ids []any) (int64, error) { + q := r.freshSession().Table(desc.pivotTable). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn)), parentVal) + if desc.kind == relKindMorphToMany { + q = q.Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + } + if len(ids) > 0 { + q = q.Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotRelatedRef.foreignColumn)), ids) + } + q = applyOnPivotQuery(q, desc) + res := q.Delete(nil) + return res.RowsAffected, res.Error +} + +// resolvePivot is the shared front-half for all pivot operations: validates parent, resolves the +// descriptor, asserts a pivot-friendly kind, and reads the parent's PK that anchors every pivot +// row. Returns the descriptor + parent's PK value. +func (r *Query) resolvePivot(parent any, relation, op string) (*relationDescriptor, any, error) { + if !isValidParent(parent) { + return nil, nil, errors.OrmRelationParentNotPointer.Args(parent) + } + desc, err := resolveRelation(r.instance, parent, relation) + if err != nil { + return nil, nil, err + } + if desc.kind != relKindMany2Many && desc.kind != relKindMorphToMany { + return nil, nil, errors.OrmRelationKindNotSupported.Args(op, relation, kindName(desc.kind)) + } + parentVal, err := readParentColumn(r, parent, desc.pivotParentRef.primaryColumn) + if err != nil { + return nil, nil, err + } + return desc, parentVal, nil +} + +// basePivotRow builds the column map for one pivot INSERT row. Always includes the parent FK and +// the related FK; for MorphToMany also includes the morph_type column. When the descriptor's +// resolved created_at / updated_at columns are non-empty, stamps those columns with time.Now(). +// Caller-supplied attrs are merged on top — the caller wins on column-name conflicts. +func (r *Query) basePivotRow(desc *relationDescriptor, parentVal, relatedID any, attrs map[string]any) map[string]any { + row := map[string]any{ + desc.pivotParentRef.foreignColumn: parentVal, + desc.pivotRelatedRef.foreignColumn: relatedID, + } + if desc.kind == relKindMorphToMany { + row[desc.morphTypeColumn] = desc.morphValue + } + now := time.Now() + if desc.pivotCreatedAtColumn != "" { + row[desc.pivotCreatedAtColumn] = now + } + if desc.pivotUpdatedAtColumn != "" { + row[desc.pivotUpdatedAtColumn] = now + } + for k, v := range attrs { + row[k] = v + } + return row +} + +// existingPivotIDs returns the set of already-attached related ids among ids. Used by Attach to +// skip duplicates. +func (r *Query) existingPivotIDs(desc *relationDescriptor, parentVal any, ids []any) (map[string]struct{}, error) { + q := r.freshSession(). + Table(desc.pivotTable). + Select(desc.pivotRelatedRef.foreignColumn). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn)), parentVal). + Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotRelatedRef.foreignColumn)), ids) + if desc.kind == relKindMorphToMany { + q = q.Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + } + q = applyOnPivotQuery(q, desc) + var rows []map[string]any + if err := q.Find(&rows).Error; err != nil { + return nil, err + } + out := make(map[string]struct{}, len(rows)) + for _, row := range rows { + out[dictKey(row[desc.pivotRelatedRef.foreignColumn])] = struct{}{} + } + return out, nil +} + +// allPivotIDs returns the set of all currently-attached related ids for parent. Used by Sync / +// Toggle to compute the diff. +func (r *Query) allPivotIDs(desc *relationDescriptor, parentVal any) ([]any, error) { + q := r.freshSession(). + Table(desc.pivotTable). + Select(desc.pivotRelatedRef.foreignColumn). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn)), parentVal) + if desc.kind == relKindMorphToMany { + q = q.Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + } + q = applyOnPivotQuery(q, desc) + var rows []map[string]any + if err := q.Find(&rows).Error; err != nil { + return nil, err + } + out := make([]any, 0, len(rows)) + for _, row := range rows { + out = append(out, row[desc.pivotRelatedRef.foreignColumn]) + } + return out, nil +} + +// SyncRelation replaces parent's pivot rows so they exactly match ids: detaches missing entries, +// attaches new ones, leaves existing untouched. Returns the per-id outcome. +// +// Public Query-level helper used by Orm.Sync. +func (r *Query) SyncRelation(parent any, relation string, ids []any) (*dbcontract.SyncResult, error) { + return r.syncCore(parent, relation, ids, true /*detach*/, false /*toggle*/, "Sync") +} + +// SyncWithoutDetachingRelation is SyncRelation minus the detach step. +func (r *Query) SyncWithoutDetachingRelation(parent any, relation string, ids []any) (*dbcontract.SyncResult, error) { + return r.syncCore(parent, relation, ids, false /*detach*/, false /*toggle*/, "SyncWithoutDetaching") +} + +// ToggleRelation attaches missing entries and detaches existing ones. +func (r *Query) ToggleRelation(parent any, relation string, ids []any) (*dbcontract.SyncResult, error) { + return r.syncCore(parent, relation, ids, false, true /*toggle*/, "Toggle") +} + +// syncCore is the shared engine for Sync / SyncWithoutDetaching / Toggle. +func (r *Query) syncCore(parent any, relation string, ids []any, detachMissing bool, toggle bool, op string) (*dbcontract.SyncResult, error) { + desc, parentVal, err := r.resolvePivot(parent, relation, op) + if err != nil { + return nil, err + } + current, err := r.allPivotIDs(desc, parentVal) + if err != nil { + return nil, err + } + currentSet := make(map[string]any, len(current)) + for _, id := range current { + currentSet[dictKey(id)] = id + } + wantSet := make(map[string]any, len(ids)) + for _, id := range ids { + wantSet[dictKey(id)] = id + } + + out := &dbcontract.SyncResult{} + switch { + case toggle: + // Anything in `ids` that exists -> detach; anything that doesn't -> attach. + var attachIDs, detachIDs []any + for k, v := range wantSet { + if _, exists := currentSet[k]; exists { + detachIDs = append(detachIDs, v) + } else { + attachIDs = append(attachIDs, v) + } + } + if len(attachIDs) > 0 { + if _, err := r.doAttach(desc, parentVal, attachIDs, nil); err != nil { + return nil, err + } + } + if len(detachIDs) > 0 { + if _, err := r.doDetach(desc, parentVal, detachIDs); err != nil { + return nil, err + } + } + out.Attached = castKeys(attachIDs, desc.relatedKeyType) + out.Detached = castKeys(detachIDs, desc.relatedKeyType) + default: + // Attach anything in `wantSet` that isn't yet attached. + var attachIDs []any + for k, v := range wantSet { + if _, exists := currentSet[k]; !exists { + attachIDs = append(attachIDs, v) + } + } + if len(attachIDs) > 0 { + if _, err := r.doAttach(desc, parentVal, attachIDs, nil); err != nil { + return nil, err + } + } + out.Attached = castKeys(attachIDs, desc.relatedKeyType) + + if detachMissing { + // Detach anything in `currentSet` that isn't in `wantSet`. + var detachIDs []any + for k, v := range currentSet { + if _, keep := wantSet[k]; !keep { + detachIDs = append(detachIDs, v) + } + } + if len(detachIDs) > 0 { + if _, err := r.doDetach(desc, parentVal, detachIDs); err != nil { + return nil, err + } + } + out.Detached = castKeys(detachIDs, desc.relatedKeyType) + } + } + + if syncResultChanged(out) { + if err := r.touchIfTouching(desc, parent, parentVal); err != nil { + return nil, err + } + } + return out, nil +} + +// SyncRelationWithPivot is SyncRelation with per-ID pivot column values. The map key is the +// related id; the map value is the column-name-to-value map applied to that pivot row. For +// existing pivot rows with non-empty attrs, updates the pivot columns (reported in +// SyncResult.Updated). Mirrors fedaco's sync(map). +func (r *Query) SyncRelationWithPivot(parent any, relation string, idsWithAttrs map[any]map[string]any) (*dbcontract.SyncResult, error) { + return r.syncCoreWithPivot(parent, relation, idsWithAttrs, true /*detach*/, false /*toggle*/, "SyncWithPivot") +} + +// SyncRelationWithPivotValues applies the same pivot column values to all ids. Mirrors fedaco's +// syncWithPivotValues. +func (r *Query) SyncRelationWithPivotValues(parent any, relation string, ids []any, pivotValues map[string]any) (*dbcontract.SyncResult, error) { + idsWithAttrs := make(map[any]map[string]any, len(ids)) + for _, id := range ids { + idsWithAttrs[id] = pivotValues + } + return r.syncCoreWithPivot(parent, relation, idsWithAttrs, true /*detach*/, false /*toggle*/, "SyncWithPivotValues") +} + +// SyncWithoutDetachingRelationWithPivot is SyncRelationWithPivot minus the detach step. +func (r *Query) SyncWithoutDetachingRelationWithPivot(parent any, relation string, idsWithAttrs map[any]map[string]any) (*dbcontract.SyncResult, error) { + return r.syncCoreWithPivot(parent, relation, idsWithAttrs, false /*detach*/, false /*toggle*/, "SyncWithoutDetachingWithPivot") +} + +// ToggleRelationWithPivot is ToggleRelation with per-ID pivot column values for newly attached rows. +func (r *Query) ToggleRelationWithPivot(parent any, relation string, idsWithAttrs map[any]map[string]any) (*dbcontract.SyncResult, error) { + return r.syncCoreWithPivot(parent, relation, idsWithAttrs, false, true /*toggle*/, "ToggleWithPivot") +} + +// syncCoreWithPivot is the shared engine for SyncWithPivot / SyncWithPivotValues / +// SyncWithoutDetachingWithPivot / ToggleWithPivot. Similar to syncCore but accepts a map of IDs +// to pivot attributes and updates existing pivot rows when attrs are non-empty. +func (r *Query) syncCoreWithPivot(parent any, relation string, idsWithAttrs map[any]map[string]any, detachMissing bool, toggle bool, op string) (*dbcontract.SyncResult, error) { + desc, parentVal, err := r.resolvePivot(parent, relation, op) + if err != nil { + return nil, err + } + current, err := r.allPivotIDs(desc, parentVal) + if err != nil { + return nil, err + } + currentSet := make(map[string]any, len(current)) + for _, id := range current { + currentSet[dictKey(id)] = id + } + wantSet := make(map[string]any, len(idsWithAttrs)) + for id := range idsWithAttrs { + wantSet[dictKey(id)] = id + } + + out := &dbcontract.SyncResult{} + switch { + case toggle: + // Anything in `idsWithAttrs` that exists -> detach; anything that doesn't -> attach with attrs. + var detachIDs []any + attachMap := make(map[any]map[string]any) + for k, v := range wantSet { + if _, exists := currentSet[k]; exists { + detachIDs = append(detachIDs, v) + } else { + attachMap[v] = idsWithAttrs[v] + } + } + if len(attachMap) > 0 { + if _, err := r.doAttachWithPivot(desc, parentVal, attachMap); err != nil { + return nil, err + } + for id := range attachMap { + out.Attached = append(out.Attached, id) + } + } + if len(detachIDs) > 0 { + if _, err := r.doDetach(desc, parentVal, detachIDs); err != nil { + return nil, err + } + } + out.Attached = castKeys(out.Attached, desc.relatedKeyType) + out.Detached = castKeys(detachIDs, desc.relatedKeyType) + default: + // Attach anything in `wantSet` that isn't yet attached; update existing if attrs non-empty. + attachMap := make(map[any]map[string]any) + var updateIDs []any + for k, v := range wantSet { + if _, exists := currentSet[k]; !exists { + attachMap[v] = idsWithAttrs[v] + } else { + // Already attached — if attrs non-empty, update the pivot row. + attrs := idsWithAttrs[v] + if len(attrs) > 0 { + if _, err := r.doUpdateExistingPivot(desc, parentVal, v, attrs); err != nil { + return nil, err + } + updateIDs = append(updateIDs, v) + } + } + } + if len(attachMap) > 0 { + if _, err := r.doAttachWithPivot(desc, parentVal, attachMap); err != nil { + return nil, err + } + for id := range attachMap { + out.Attached = append(out.Attached, id) + } + } + out.Attached = castKeys(out.Attached, desc.relatedKeyType) + out.Updated = castKeys(updateIDs, desc.relatedKeyType) + + if detachMissing { + // Detach anything in `currentSet` that isn't in `wantSet`. + var detachIDs []any + for k, v := range currentSet { + if _, keep := wantSet[k]; !keep { + detachIDs = append(detachIDs, v) + } + } + if len(detachIDs) > 0 { + if _, err := r.doDetach(desc, parentVal, detachIDs); err != nil { + return nil, err + } + } + out.Detached = castKeys(detachIDs, desc.relatedKeyType) + } + } + + if syncResultChanged(out) { + if err := r.touchIfTouching(desc, parent, parentVal); err != nil { + return nil, err + } + } + return out, nil +} + +// syncResultChanged reports whether out indicates any actual pivot-table mutation. Used by +// syncCore / syncCoreWithPivot to decide whether to call touchIfTouching at the end. +func syncResultChanged(out *dbcontract.SyncResult) bool { + return len(out.Attached) > 0 || len(out.Detached) > 0 || len(out.Updated) > 0 +} + +// UpdateExistingPivotRelation updates pivot columns for an already-attached id. When +// pivotTimestamps is enabled and attrs doesn't already set updated_at, injects time.Now() into +// the update map. No-op (returns 0) if no matching pivot row exists. +func (r *Query) UpdateExistingPivotRelation(parent any, relation string, id any, attrs map[string]any) (int64, error) { + desc, parentVal, err := r.resolvePivot(parent, relation, "UpdateExistingPivot") + if err != nil { + return 0, err + } + affected, err := r.doUpdateExistingPivot(desc, parentVal, id, attrs) + if err != nil { + return 0, err + } + if affected > 0 { + if err := r.touchIfTouching(desc, parent, parentVal); err != nil { + return affected, err + } + } + return affected, nil +} + +// doUpdateExistingPivot is the no-touch worker behind UpdateExistingPivotRelation. Returns the +// number of pivot rows actually updated. When the descriptor has a resolved updated_at column +// and attrs doesn't already set it, injects time.Now() into the UPDATE map. +func (r *Query) doUpdateExistingPivot(desc *relationDescriptor, parentVal any, id any, attrs map[string]any) (int64, error) { + if len(attrs) == 0 && desc.pivotUpdatedAtColumn == "" { + return 0, nil + } + updateMap := make(map[string]any, len(attrs)+1) + for k, v := range attrs { + updateMap[k] = v + } + if desc.pivotUpdatedAtColumn != "" { + if _, hasUpdatedAt := updateMap[desc.pivotUpdatedAtColumn]; !hasUpdatedAt { + updateMap[desc.pivotUpdatedAtColumn] = time.Now() + } + } + q := r.freshSession().Table(desc.pivotTable). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotParentRef.foreignColumn)), parentVal). + Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.pivotRelatedRef.foreignColumn)), id) + if desc.kind == relKindMorphToMany { + q = q.Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) + } + q = applyOnPivotQuery(q, desc) + res := q.Updates(updateMap) + return res.RowsAffected, res.Error +} + +// setRelationFKOnChild reads parent's local key, then writes that value into child's FK column +// (and the morph_type column for MorphOne/MorphMany). Mutates child in place; child must be a +// pointer to a struct. +func (r *Query) setRelationFKOnChild(parent, child any, desc *relationDescriptor) error { + if len(desc.references) == 0 { + return errors.OrmRelationUnsupported.Args(desc.name, desc.parentTable, "no references") + } + ref := desc.references[0] + parentVal, err := readParentColumn(r, parent, ref.primaryColumn) + if err != nil { + return err + } + childSchema, err := parseGormSchema(r.instance, child) + if err != nil { + return err + } + fkField, ok := childSchema.FieldsByDBName[ref.foreignColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(desc.name, childSchema.Name, "no FK field "+ref.foreignColumn) + } + rv := reflect.ValueOf(child).Elem() + if err := fkField.Set(r.ctx, rv, parentVal); err != nil { + return err + } + if desc.kind == relKindMorphOne || desc.kind == relKindMorphMany { + typeField, ok := childSchema.FieldsByDBName[desc.morphTypeColumn] + if !ok { + return errors.OrmRelationUnsupported.Args(desc.name, childSchema.Name, "no morph type field "+desc.morphTypeColumn) + } + if err := typeField.Set(r.ctx, rv, desc.morphValue); err != nil { + return err + } + } + return nil +} + +// applyAttrMap overlays attrs onto dest using GORM's parsed schema to map column names to struct +// fields. dest must be a pointer to a struct. Skips columns that don't map to a field. +func (r *Query) applyAttrMap(dest any, attrs map[string]any) error { + if len(attrs) == 0 { + return nil + } + schema, err := parseGormSchema(r.instance, dest) + if err != nil { + return err + } + rv := reflect.ValueOf(dest).Elem() + for col, val := range attrs { + field, ok := schema.FieldsByDBName[col] + if !ok { + continue + } + if err := field.Set(r.ctx, rv, val); err != nil { + return err + } + } + return nil +} + +// touchIfTouching bumps parent's updated_at column when desc.touches is true. Silently no-ops +// when desc.touches is false, when the parent's schema doesn't expose an updated_at field, or +// when the parent has no primary-key column to anchor the WHERE clause. Mirrors fedaco's +// touchIfTouching on BelongsToMany. +// +// Called at the tail end of public Sync / Attach / Detach / Toggle / UpdateExistingPivot methods, +// and only when the operation actually affected pivot rows. The internal doAttach / doDetach / +// doUpdateExistingPivot helpers do NOT touch — sync* paths chain multiple internal calls and +// touch at most once at the end via this helper. +func (r *Query) touchIfTouching(desc *relationDescriptor, parent any, parentVal any) error { + if !desc.touches { + return nil + } + parentSchema, err := parseGormSchema(r.instance, parent) + if err != nil { + return err + } + field, ok := parentSchema.FieldsByDBName["updated_at"] + if !ok { + // Parent doesn't have an updated_at column — silently skip. + return nil + } + if len(parentSchema.PrimaryFields) == 0 { + return nil + } + pkColumn := parentSchema.PrimaryFields[0].DBName + now := time.Now() + res := r.freshSession().Table(parentSchema.Table). + Where(fmt.Sprintf("%s = ?", quoteIdent(pkColumn)), parentVal). + Update(field.DBName, now) + if res.Error != nil { + return res.Error + } + // Mirror the change into the in-memory parent struct so subsequent reads see the bump. + parentRV := reflect.ValueOf(parent).Elem() + return field.Set(r.ctx, parentRV, now) +} + +// kindName returns a human-friendly name for a relationKind, used in error messages. +func kindName(k relationKind) string { + switch k { + case relKindHasOne: + return "hasOne" + case relKindHasMany: + return "hasMany" + case relKindBelongsTo: + return "belongsTo" + case relKindMany2Many: + return "many2Many" + case relKindMorphOne: + return "morphOne" + case relKindMorphMany: + return "morphMany" + case relKindMorphTo: + return "morphTo" + case relKindMorphToMany: + return "morphToMany" + case relKindHasOneThrough: + return "hasOneThrough" + case relKindHasManyThrough: + return "hasManyThrough" + } + return fmt.Sprintf("kind=%d", k) +} + +// castKeys returns a copy of ids with each value normalised to keyType (the related model's PK +// type). Used by Sync* / Toggle* to ensure SyncResult elements carry a stable Go type regardless +// of what the caller passed in or what GORM scanned out of the pivot table. +// +// Mirrors fedaco's _castKeys / _getTypeSwapValue. Returns nil for a nil input slice (preserving +// the "no rows touched" signal). +func castKeys(ids []any, keyType reflect.Type) []any { + if ids == nil { + return nil + } + out := make([]any, len(ids)) + for i, id := range ids { + out[i] = castKey(id, keyType) + } + return out +} + +// castKey converts v to the Go type t, handling the common cross-type cases (int/uint/float +// numeric widening + narrowing, string ↔ numeric). Returns v unchanged when t is nil, when v is +// already the right type, or when conversion isn't safely representable. +func castKey(v any, t reflect.Type) any { + if v == nil || t == nil { + return v + } + rv := reflect.ValueOf(v) + if rv.Type() == t { + return v + } + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return rv.Convert(t).Interface() + case reflect.String: + if i, err := strconv.ParseInt(rv.String(), 10, 64); err == nil { + return reflect.ValueOf(i).Convert(t).Interface() + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return rv.Convert(t).Interface() + case reflect.String: + if u, err := strconv.ParseUint(rv.String(), 10, 64); err == nil { + return reflect.ValueOf(u).Convert(t).Interface() + } + } + case reflect.Float32, reflect.Float64: + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return rv.Convert(t).Interface() + case reflect.String: + if f, err := strconv.ParseFloat(rv.String(), 64); err == nil { + return reflect.ValueOf(f).Convert(t).Interface() + } + } + case reflect.String: + // Numeric / []byte → string. Avoid reflect.Convert here because int→string interprets the + // int as a Unicode code point (e.g. 65 → "A"), not a decimal digit string. + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return fmt.Sprint(v) + case reflect.Slice: + if rv.Type().Elem().Kind() == reflect.Uint8 { + return string(rv.Bytes()) + } + } + } + return v +} diff --git a/database/gorm/relation_writes_test.go b/database/gorm/relation_writes_test.go new file mode 100644 index 000000000..149552d30 --- /dev/null +++ b/database/gorm/relation_writes_test.go @@ -0,0 +1,622 @@ +package gorm + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +// setRelationFKOnChild is the FK-and-morph-type writer that SaveRelation calls before +// persistence. We test it directly because the stub dialector can't run the INSERT step. + +func TestSetRelationFKOnChild_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + + parent := &relUser{ID: 7} + child := &relBook{Title: "x"} + err = q.setRelationFKOnChild(parent, child, desc) + assert.NoError(t, err) + assert.Equal(t, uint(7), child.UserID) +} + +func TestSetRelationFKOnChild_MorphMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Houses") + assert.NoError(t, err) + + parent := &relUser{ID: 9} + child := &relHouse{Address: "x"} + err = q.setRelationFKOnChild(parent, child, desc) + assert.NoError(t, err) + assert.Equal(t, uint(9), child.HouseableID) + assert.Equal(t, "rel_users", child.HouseableType) +} + +func TestSetRelationFKOnChild_MorphOne(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Logo") + assert.NoError(t, err) + + parent := &relUser{ID: 11} + child := &relLogo{URL: "x"} + err = q.setRelationFKOnChild(parent, child, desc) + assert.NoError(t, err) + assert.Equal(t, uint(11), child.LogoableID) + assert.Equal(t, "rel_users", child.LogoableType) +} + +// SaveRelation guard / dispatch tests. These don't reach the INSERT step (they error / return +// early before persistence), so they're safe to run against the stub dialector. + +func TestSaveRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveRelation(relUser{ID: 1}, "Books", &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestSaveRelation_NilChild(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveRelation(&relUser{ID: 1}, "Books", nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestSaveRelation_UnsupportedKind_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + err := q.SaveRelation(&relBook{}, "Author", &relUser{}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSaveRelation_UnsupportedKind_HasManyThrough(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + err := q.SaveRelation(&relCountry{}, "Posts", &relPost{}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSaveRelation_RelationNotFound(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveRelation(&relUser{}, "DoesNotExist", &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationNotFound)) +} + +func TestSaveManyRelation_NonSlice(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveManyRelation(&relUser{ID: 1}, "Books", "not a slice") + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +// Sanity: *Query satisfies the helper signatures the Orm wrapper relies on, and the +// contractsorm.Query interface is still satisfied (the new methods don't break that contract). +var _ interface { + SaveRelation(parent any, relation string, child any) error + SaveManyRelation(parent any, relation string, children any) error + AssociateRelation(parent any, relation string, owner any) error + DissociateRelation(parent any, relation string) error +} = (*Query)(nil) +var _ contractsorm.Query = (*Query)(nil) + +// --- Sync / Toggle / UpdateExistingPivot ---------------------------------- + +func TestSyncRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.SyncRelation(relUser{}, "Roles", []any{1, 2}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestSyncRelation_UnsupportedKind(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.SyncRelation(&relUser{ID: 1}, "Books", []any{1}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSyncWithoutDetachingRelation_UnsupportedKind(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.SyncWithoutDetachingRelation(&relUser{ID: 1}, "Books", []any{1}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestToggleRelation_UnsupportedKind(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.ToggleRelation(&relUser{ID: 1}, "Books", []any{1}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestUpdateExistingPivotRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.UpdateExistingPivotRelation(relUser{}, "Roles", 1, map[string]any{"x": 1}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestUpdateExistingPivotRelation_UnsupportedKind(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.UpdateExistingPivotRelation(&relUser{ID: 1}, "Books", 1, map[string]any{"x": 1}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestUpdateExistingPivotRelation_EmptyAttrs_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rows, err := q.UpdateExistingPivotRelation(&relUser{ID: 1}, "Roles", 1, map[string]any{}) + assert.NoError(t, err) + assert.Equal(t, int64(0), rows) +} + +func TestBasePivotRow_Many2Many(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + + row := q.basePivotRow(desc, uint(7), uint(99), nil) + assert.Equal(t, uint(7), row[desc.pivotParentRef.foreignColumn]) + assert.Equal(t, uint(99), row[desc.pivotRelatedRef.foreignColumn]) + _, hasMorphType := row[desc.morphTypeColumn] + assert.False(t, hasMorphType, "pure Many2Many must not include morph_type") +} + +func TestBasePivotRow_MorphToMany_IncludesType(t *testing.T) { + q := newRelQueryWith(t, &morphPost{}) + desc, err := resolveRelation(q.instance, &morphPost{}, "Tags") + assert.NoError(t, err) + + row := q.basePivotRow(desc, uint(3), uint(11), nil) + assert.Equal(t, uint(3), row["taggable_id"]) + assert.Equal(t, "morph_posts", row["taggable_type"]) // table-name fallback + assert.Equal(t, uint(11), row[desc.pivotRelatedRef.foreignColumn]) +} + +func TestBasePivotRow_AttrsOverlay(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + + row := q.basePivotRow(desc, uint(7), uint(99), map[string]any{ + "priority": "high", + "notes": "x", + }) + assert.Equal(t, "high", row["priority"]) + assert.Equal(t, "x", row["notes"]) + assert.Equal(t, uint(7), row[desc.pivotParentRef.foreignColumn]) +} + +func TestAttachRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.AttachRelation(relUser{}, "Roles", []any{1}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestAttachRelation_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.AttachRelation(&relUser{ID: 1}, "Books", []any{1}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestAttachRelation_EmptyIDs_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.AttachRelation(&relUser{ID: 1}, "Roles", nil) + assert.NoError(t, err) +} + +func TestAttachWithPivotRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.AttachWithPivotRelation(relUser{}, "Roles", map[any]map[string]any{1: {"priority": "high"}}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestAttachWithPivotRelation_EmptyMap_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.AttachWithPivotRelation(&relUser{ID: 1}, "Roles", map[any]map[string]any{}) + assert.NoError(t, err) +} + +func TestDetachRelation_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.DetachRelation(&relUser{ID: 1}, "Books", nil) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestDetachRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.DetachRelation(relUser{}, "Roles", nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestMutateAssociate_BelongsTo_SetsFK(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + desc, err := resolveRelation(q.instance, &relBook{}, "Author") + assert.NoError(t, err) + + parent := &relBook{Title: "x", AuthorID: 0} + owner := &relUser{ID: 42} + err = q.mutateAssociate(parent, owner, desc, false) + assert.NoError(t, err) + assert.Equal(t, uint(42), parent.AuthorID) +} + +func TestMutateDissociate_BelongsTo_ClearsFK(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + desc, err := resolveRelation(q.instance, &relBook{}, "Author") + assert.NoError(t, err) + + parent := &relBook{Title: "x", AuthorID: 99} + err = q.mutateDissociate(parent, desc, false) + assert.NoError(t, err) + assert.Equal(t, uint(0), parent.AuthorID) +} + +func TestAssociateRelation_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + err := q.AssociateRelation(relBook{}, "Author", &relUser{ID: 1}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestAssociateRelation_NilOwner(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + err := q.AssociateRelation(&relBook{}, "Author", nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestAssociateRelation_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.AssociateRelation(&relUser{}, "Books", &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestDissociateRelation_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.DissociateRelation(&relUser{}, "Books") + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +// MorphTo Associate / Dissociate exercise the morph_type column path. Uses the morphImage +// fixture from relation_test.go. + +func TestMutateAssociate_MorphTo_SetsFKAndType(t *testing.T) { + q := newRelQueryWith(t, &morphImage{}) + desc, err := resolveRelation(q.instance, &morphImage{}, "Imageable") + assert.NoError(t, err) + + parent := &morphImage{} + owner := &relUser{ID: 3, Name: "n"} + err = q.mutateAssociate(parent, owner, desc, true) + assert.NoError(t, err) + assert.Equal(t, uint(3), parent.ImageableID) + assert.Equal(t, "rel_users", parent.ImageableType) // table-name fallback when not registered +} + +func TestMutateDissociate_MorphTo_ClearsFKAndType(t *testing.T) { + q := newRelQueryWith(t, &morphImage{}) + desc, err := resolveRelation(q.instance, &morphImage{}, "Imageable") + assert.NoError(t, err) + + parent := &morphImage{ImageableID: 5, ImageableType: "post"} + err = q.mutateDissociate(parent, desc, true) + assert.NoError(t, err) + assert.Equal(t, uint(0), parent.ImageableID) + assert.Equal(t, "", parent.ImageableType) +} + +// Phase A/B tests: HasOneOrMany and BelongsToMany convenience methods + +func TestCreateRelation_UnsupportedKind_HasManyThrough(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + err := q.CreateRelation(&relCountry{ID: 1}, "Posts", &relPost{}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestCreateRelation_UnsupportedKind_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + err := q.CreateRelation(&relBook{ID: 1}, "Author", &relUser{}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestFindOrNewRelation_UnsupportedKind_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + var dest relUser + err := q.FindOrNewRelation(&relBook{ID: 1}, "Author", uint(5), &dest) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestFirstOrNewRelation_UnsupportedKind_Through(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + var dest relPost + err := q.FirstOrNewRelation(&relCountry{ID: 1}, "Posts", map[string]any{"title": "x"}, nil, &dest) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestFirstOrCreateRelation_UnsupportedKind_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + var dest relUser + err := q.FirstOrCreateRelation(&relBook{ID: 1}, "Author", map[string]any{"name": "x"}, nil, &dest) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestUpdateOrCreateRelation_UnsupportedKind_Through(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + var dest relPost + err := q.UpdateOrCreateRelation(&relCountry{ID: 1}, "Posts", map[string]any{"title": "x"}, map[string]any{"content": "y"}, &dest) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +// Phase C tests: PivotTimestamps + +func TestBasePivotRow_Timestamps_IncludesCreatedUpdatedAt(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + + // Enable timestamps by setting the resolved column names directly. + desc.pivotCreatedAtColumn = "created_at" + desc.pivotUpdatedAtColumn = "updated_at" + + row := q.basePivotRow(desc, uint(7), uint(99), nil) + assert.Equal(t, uint(7), row[desc.pivotParentRef.foreignColumn]) + assert.Equal(t, uint(99), row[desc.pivotRelatedRef.foreignColumn]) + + _, hasCreatedAt := row["created_at"] + assert.True(t, hasCreatedAt, "pivot row must include created_at when desc.pivotCreatedAtColumn is set") + + _, hasUpdatedAt := row["updated_at"] + assert.True(t, hasUpdatedAt, "pivot row must include updated_at when desc.pivotUpdatedAtColumn is set") +} + +func TestBasePivotRow_Timestamps_AttrsCanOverride(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + + desc.pivotCreatedAtColumn = "created_at" + desc.pivotUpdatedAtColumn = "updated_at" + + customTime := "2024-01-01 00:00:00" + row := q.basePivotRow(desc, uint(7), uint(99), map[string]any{ + "created_at": customTime, + }) + + assert.Equal(t, customTime, row["created_at"], "caller-supplied attrs must override timestamp") + _, hasUpdatedAt := row["updated_at"] + assert.True(t, hasUpdatedAt, "updated_at should still be set") +} + +func TestBasePivotRow_NoTimestamps_OmitsBoth(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + + // Empty resolved columns mean "don't auto-stamp". + row := q.basePivotRow(desc, uint(7), uint(99), nil) + _, hasCreatedAt := row["created_at"] + _, hasUpdatedAt := row["updated_at"] + assert.False(t, hasCreatedAt) + assert.False(t, hasUpdatedAt) +} + +func TestBasePivotRow_OnlyUpdatedAt_OmitsCreatedAt(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + + // Pivot struct may declare only UpdatedAt (e.g. ledger-style writes); only stamp updated_at. + desc.pivotUpdatedAtColumn = "updated_at" + + row := q.basePivotRow(desc, uint(7), uint(99), nil) + _, hasCreatedAt := row["created_at"] + _, hasUpdatedAt := row["updated_at"] + assert.False(t, hasCreatedAt) + assert.True(t, hasUpdatedAt) +} + +// Phase G tests: SyncWithPivot / SyncWithPivotValues / ToggleWithPivot + +func TestSyncRelationWithPivot_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.SyncRelationWithPivot(&relUser{ID: 1}, "Books", map[any]map[string]any{uint(1): {"priority": "high"}}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSyncRelationWithPivotValues_UnsupportedKind_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + _, err := q.SyncRelationWithPivotValues(&relBook{ID: 1}, "Author", []any{uint(1)}, map[string]any{"priority": "high"}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSyncWithoutDetachingRelationWithPivot_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.SyncWithoutDetachingRelationWithPivot(&relUser{ID: 1}, "Books", map[any]map[string]any{uint(1): {"priority": "high"}}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestToggleRelationWithPivot_UnsupportedKind_HasMany(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _, err := q.ToggleRelationWithPivot(&relUser{ID: 1}, "Books", map[any]map[string]any{uint(1): {"priority": "high"}}) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +// Phase H tests: castKey — normalises SyncResult ids back to the related model's PK type. + +func TestCastKey(t *testing.T) { + uintT := reflect.TypeFor[uint]() + intT := reflect.TypeFor[int]() + int64T := reflect.TypeFor[int64]() + stringT := reflect.TypeFor[string]() + float64T := reflect.TypeFor[float64]() + + cases := []struct { + name string + in any + t reflect.Type + want any + }{ + {"nil value passthrough", nil, uintT, nil}, + {"nil type passthrough", uint(7), nil, uint(7)}, + {"same-type passthrough", uint(7), uintT, uint(7)}, + {"int -> uint", int(7), uintT, uint(7)}, + {"int64 -> uint (gorm scan typical)", int64(42), uintT, uint(42)}, + {"uint -> int", uint(7), intT, int(7)}, + {"uint -> int64", uint(7), int64T, int64(7)}, + {"float -> int", float64(3), intT, int(3)}, + {"string numeric -> uint", "42", uintT, uint(42)}, + {"string numeric -> int (negative)", "-7", intT, int(-7)}, + {"string numeric -> float", "3.14", float64T, float64(3.14)}, + {"int -> string (decimal, not Unicode)", int(65), stringT, "65"}, + {"uint -> string", uint(99), stringT, "99"}, + {"float -> string", float64(3.14), stringT, "3.14"}, + {"[]byte -> string", []byte("abc"), stringT, "abc"}, + {"non-numeric string -> uint passthrough", "abc", uintT, "abc"}, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got := castKey(tt.in, tt.t) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCastKeys(t *testing.T) { + uintT := reflect.TypeFor[uint]() + + t.Run("nil slice passthrough", func(t *testing.T) { + assert.Nil(t, castKeys(nil, uintT)) + }) + t.Run("empty slice", func(t *testing.T) { + assert.Equal(t, []any{}, castKeys([]any{}, uintT)) + }) + t.Run("mixed input types -> uniform uint", func(t *testing.T) { + got := castKeys([]any{int(1), int64(2), "3", uint(4)}, uintT) + assert.Equal(t, []any{uint(1), uint(2), uint(3), uint(4)}, got) + }) + t.Run("nil keyType leaves values untouched", func(t *testing.T) { + got := castKeys([]any{int(1), "2"}, nil) + assert.Equal(t, []any{int(1), "2"}, got) + }) +} + +// relatedKeyType wiring: descriptor must carry the related model's PK type so castKey can use it. +func TestDescriptor_RelatedKeyType_Many2Many(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Roles") + assert.NoError(t, err) + assert.Equal(t, reflect.TypeFor[uint](), desc.relatedKeyType) +} + +func TestDescriptor_RelatedKeyType_MorphToMany(t *testing.T) { + q := newRelQueryWith(t, &morphPost{}) + desc, err := resolveRelation(q.instance, &morphPost{}, "Tags") + assert.NoError(t, err) + assert.Equal(t, reflect.TypeFor[uint](), desc.relatedKeyType) +} + +// Pivot timestamp resolution tests — exercise the priority order between Pivot struct +// autoCreateTime/autoUpdateTime tags, CreatedAt/UpdatedAt convention, and the relation-level +// PivotTimestamps fallback. + +type tsTaggedPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + Stamped time.Time `gorm:"autoCreateTime"` + Edited time.Time `gorm:"autoUpdateTime"` +} + +type tsTaggedRole struct { + ID uint + Name string + Pivot tsTaggedPivot `gorm:"-"` +} + +type tsConventionPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type tsConventionRole struct { + ID uint + Name string + Pivot tsConventionPivot `gorm:"-"` +} + +type tsCreatedOnlyPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + CreatedAt time.Time +} + +type tsCreatedOnlyRole struct { + ID uint + Pivot tsCreatedOnlyPivot `gorm:"-"` +} + +type tsCustomColumnPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + Stamped time.Time `gorm:"autoCreateTime;column:made_on"` + Edited time.Time `gorm:"autoUpdateTime;column:edited_at"` +} + +type tsCustomColumnRole struct { + ID uint + Pivot tsCustomColumnPivot `gorm:"-"` +} + +func TestResolvePivotTimestamps_AutoCreateTimeTag(t *testing.T) { + db := newStubGormDB(t) + created, updated, err := resolvePivotTimestamps(db, &tsTaggedRole{}, "Pivot", false) + assert.NoError(t, err) + assert.Equal(t, "stamped", created) + assert.Equal(t, "edited", updated) +} + +func TestResolvePivotTimestamps_Convention(t *testing.T) { + db := newStubGormDB(t) + created, updated, err := resolvePivotTimestamps(db, &tsConventionRole{}, "Pivot", false) + assert.NoError(t, err) + assert.Equal(t, "created_at", created) + assert.Equal(t, "updated_at", updated) +} + +func TestResolvePivotTimestamps_OnlyCreatedAt(t *testing.T) { + db := newStubGormDB(t) + created, updated, err := resolvePivotTimestamps(db, &tsCreatedOnlyRole{}, "Pivot", false) + assert.NoError(t, err) + assert.Equal(t, "created_at", created) + assert.Equal(t, "", updated, "no UpdatedAt field means don't auto-stamp on update") +} + +func TestResolvePivotTimestamps_CustomColumnTag(t *testing.T) { + db := newStubGormDB(t) + created, updated, err := resolvePivotTimestamps(db, &tsCustomColumnRole{}, "Pivot", false) + assert.NoError(t, err) + assert.Equal(t, "made_on", created) + assert.Equal(t, "edited_at", updated) +} + +func TestResolvePivotTimestamps_NoStruct_FallbackEnabled(t *testing.T) { + db := newStubGormDB(t) + // roleWithoutPivot has no Pivot field — falls through to relation-level PivotTimestamps. + created, updated, err := resolvePivotTimestamps(db, &roleWithoutPivot{}, "Pivot", true) + assert.NoError(t, err) + assert.Equal(t, "created_at", created) + assert.Equal(t, "updated_at", updated) +} + +func TestResolvePivotTimestamps_NoStruct_FallbackDisabled(t *testing.T) { + db := newStubGormDB(t) + created, updated, err := resolvePivotTimestamps(db, &roleWithoutPivot{}, "Pivot", false) + assert.NoError(t, err) + assert.Equal(t, "", created) + assert.Equal(t, "", updated) +} + +func TestResolvePivotTimestamps_StructHasOneCol_FallbackFillsOther(t *testing.T) { + db := newStubGormDB(t) + // Struct provides only CreatedAt; PivotTimestamps: true fills updated_at default. + created, updated, err := resolvePivotTimestamps(db, &tsCreatedOnlyRole{}, "Pivot", true) + assert.NoError(t, err) + assert.Equal(t, "created_at", created, "struct-provided column wins") + assert.Equal(t, "updated_at", updated, "fallback fills the column the struct didn't provide") +} From 18dbdc321491b6d4b1c9cc2220504c44d24c2932 Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:26:02 +0800 Subject: [PATCH 06/11] test(orm): add integration tests and many-to-many docs for relation rework 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 185c0a879cd623c5f7317cdb24008a277c162beb) --- docs/orm-many-to-many.md | 290 +++++++++++++++ tests/go.mod | 2 +- tests/go.sum | 4 +- tests/mock_config.go | 1 + tests/queries_relationships_test.go | 542 ++++++++++++++++++++++++++++ tests/query_test.go | 39 +- tests/with_test.go | 347 ++++++++++++++++++ 7 files changed, 1207 insertions(+), 18 deletions(-) create mode 100644 docs/orm-many-to-many.md create mode 100644 tests/queries_relationships_test.go create mode 100644 tests/with_test.go diff --git a/docs/orm-many-to-many.md b/docs/orm-many-to-many.md new file mode 100644 index 000000000..c135d5e55 --- /dev/null +++ b/docs/orm-many-to-many.md @@ -0,0 +1,290 @@ +# Many-to-Many Relations (BelongsToMany family) + +The BelongsToMany family covers three relation kinds — all share a pivot table: + +| Kind | Use case | +|---|---| +| `Many2Many` | Plain m:n between two models (e.g. User ↔ Role through `user_roles`) | +| `MorphToMany` | Polymorphic m:n where the parent side is morphable (e.g. Post → Tag, Video → Tag through `taggables`) | +| `MorphedByMany` | Inverse of `MorphToMany` (e.g. Tag → Post, Tag → Video) | + +All three accept the same set of pivot configuration fields described below. + +--- + +## Basic declaration + +```go +type User struct { + ID uint + Roles []*Role `gorm:"-"` +} + +func (User) Relations() map[string]orm.Relation { + return map[string]orm.Relation{ + "Roles": orm.Many2Many{Related: &Role{}, Table: "user_roles"}, + } +} +``` + +`Table` is optional — the framework defaults to the alphabetically-sorted singular pair (e.g. `role_user`). + +## Pivot model (custom Pivot struct) + +To surface pivot-table columns on eager-loaded results, declare a Go struct for the pivot row and add a field of that type to the related model: + +```go +type RoleUserPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + Active bool `gorm:"column:active"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type Role struct { + ID uint + Name string + Pivot RoleUserPivot `gorm:"-"` // ← framework hydrates this on eager load +} +``` + +No additional config required — the framework reflects the `Pivot` field type, parses its GORM schema, and uses the resulting `db_name` columns as the pivot SELECT list. + +### Custom pivot field name (`PivotField`) + +If a single related model participates in multiple m:n relations with different pivot schemas, use a different field name per relation: + +```go +type Role struct { + ID uint + Name string + UserPivot RoleUserPivot `gorm:"-"` + GroupPivot RoleGroupPivot `gorm:"-"` +} + +// On User +"Roles": orm.Many2Many{Related: &Role{}, PivotField: "UserPivot"} + +// On Group +"Roles": orm.Many2Many{Related: &Role{}, PivotField: "GroupPivot"} +``` + +`PivotField` defaults to `"Pivot"` when omitted. + +## Auto-stamping pivot timestamps + +The framework auto-fills `created_at` (on INSERT) and `updated_at` (on INSERT/UPDATE) using the following priority order: + +1. **Explicit GORM tag** on a Pivot struct field — works for any field name: + ```go + type RoleUserPivot struct { + Stamped time.Time `gorm:"autoCreateTime"` + Edited time.Time `gorm:"autoUpdateTime"` + } + ``` + +2. **GORM convention** — fields named `CreatedAt` / `UpdatedAt` of type `time.Time`: + ```go + type RoleUserPivot struct { + CreatedAt time.Time + UpdatedAt time.Time + } + ``` + +3. **Relation-level fallback** — set `PivotTimestamps: true` when no Pivot struct is declared (or it has no timestamp fields) but the underlying table still has `created_at` / `updated_at` you want auto-filled: + ```go + "Roles": orm.Many2Many{Related: &Role{}, PivotTimestamps: true} + ``` + +### Customising column names + +Use the GORM `column` tag on the Pivot struct field — there is no relation-level override: + +```go +type RoleUserPivot struct { + Stamped time.Time `gorm:"autoCreateTime;column:made_on"` + Edited time.Time `gorm:"autoUpdateTime;column:edited_at"` +} +``` + +### Partial timestamps + +Declaring only `CreatedAt` (or only `UpdatedAt`) is fine — the framework only auto-stamps what you declare: + +```go +type RoleUserPivot struct { + UserID uint + RoleID uint + CreatedAt time.Time // only created_at gets stamped; updated_at is left untouched +} +``` + +## Filtering pivot operations (`OnPivotQuery`) + +Sync / Attach / Detach / Toggle / UpdateExistingPivot can be scoped to a subset of pivot rows via `OnPivotQuery` — equivalent to fedaco's `wherePivot` / `wherePivotIn` / `wherePivotNull`. + +```go +"ActiveRoles": orm.Many2Many{ + Related: &Role{}, + Table: "user_roles", + OnPivotQuery: func(q orm.PivotQuery) orm.PivotQuery { + return q.Where("active", 1).WhereNull("deleted_at") + }, +} +``` + +The callback applies to **SELECT / UPDATE / DELETE** on the pivot table: + +- `Sync` reads "current" rows through the filter — so it only detaches rows matching the filter. +- `Detach` only deletes rows matching the filter. +- `UpdateExistingPivot` only updates rows matching the filter. +- `Attach`'s duplicate-detection SELECT also goes through the filter (so a row not matching the filter is treated as "not attached"). + +INSERT rows from `Attach` / `AttachWithPivot` are **not** auto-injected with these conditions — pass equality columns through the `attrs` map: + +```go +orm.Attach(&user, "ActiveRoles", []any{1, 2, 3}) +// Will INSERT (user_id, role_id) only — without active=1 unless you add it explicitly: +orm.AttachWithPivot(&user, "ActiveRoles", map[any]map[string]any{ + 1: {"active": 1}, + 2: {"active": 1}, +}) +``` + +### `PivotQuery` interface + +```go +type PivotQuery interface { + Where(column string, args ...any) PivotQuery + WhereIn(column string, values []any) PivotQuery + WhereNotIn(column string, values []any) PivotQuery + WhereNull(column string) PivotQuery + WhereNotNull(column string) PivotQuery +} +``` + +## Bumping parent timestamps (`Touches`) + +Pivot writes don't affect the parent row's `updated_at` by default. Set `Touches: true` to make Sync / Attach / Detach / Toggle / UpdateExistingPivot bump the parent's `updated_at` after the pivot operation succeeds (and only when pivot rows actually changed): + +```go +type Post struct { + ID uint + Title string + UpdatedAt time.Time + Tags []*Tag `gorm:"-"` +} + +func (Post) Relations() map[string]orm.Relation { + return map[string]orm.Relation{ + "Tags": orm.MorphToMany{Related: &Tag{}, Name: "taggable", Touches: true}, + } +} + +orm.Sync(&post, "Tags", []any{1, 2, 3}) +// → UPDATE posts SET updated_at = NOW() WHERE id = ? +``` + +Silently no-ops when: +- The relation isn't `Touches: true`. +- The parent's schema has no `updated_at` field. +- The pivot operation didn't actually attach/detach/update any rows (e.g. `Sync` with the same id list as already attached). + +## Pivot operation API + +All pivot operations live on `Orm` (and on the underlying `Query`): + +| Method | Effect | +|---|---| +| `Attach(parent, rel, ids)` | INSERT pivot rows; skips ids already attached | +| `AttachWithPivot(parent, rel, idsWithAttrs)` | Attach with per-id pivot column values | +| `Detach(parent, rel, ids...)` | DELETE pivot rows; with no ids, detaches all | +| `Sync(parent, rel, ids)` | INSERT missing + DELETE extra; idempotent | +| `SyncWithPivot(parent, rel, idsWithAttrs)` | Sync with per-id pivot values; UPDATEs existing rows when attrs non-empty | +| `SyncWithPivotValues(parent, rel, ids, sharedAttrs)` | Convenience: all ids share the same pivot values | +| `SyncWithoutDetaching(parent, rel, ids)` | INSERT missing only — no detach | +| `SyncWithoutDetachingWithPivot(parent, rel, idsWithAttrs)` | Same, with attrs | +| `Toggle(parent, rel, ids)` | Attach missing, detach existing | +| `ToggleWithPivot(parent, rel, idsWithAttrs)` | Toggle with attrs on the attach side | +| `UpdateExistingPivot(parent, rel, id, attrs)` | UPDATE one already-attached pivot row | + +### `SyncResult` + +`Sync*` and `Toggle*` return `*db.SyncResult`: + +```go +type SyncResult struct { + Attached []any + Detached []any + Updated []any // populated by SyncWithPivot* when an existing row's attrs changed +} +``` + +Element type matches the related model's primary-key Go type (e.g. `uint`, `int64`, `string`) — caller-supplied ids are normalised regardless of input type. + +## Complete example + +```go +import ( + "time" + + "github.com/goravel/framework/contracts/database/orm" +) + +type RoleUserPivot struct { + UserID uint `gorm:"column:user_id"` + RoleID uint `gorm:"column:role_id"` + Active bool `gorm:"column:active"` + Priority int `gorm:"column:priority"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type Role struct { + ID uint + Name string + Pivot RoleUserPivot `gorm:"-"` +} + +type User struct { + ID uint + Name string + UpdatedAt time.Time + Roles []*Role `gorm:"-"` +} + +func (User) Relations() map[string]orm.Relation { + return map[string]orm.Relation{ + "Roles": orm.Many2Many{ + Related: &Role{}, + Table: "user_roles", + OnPivotQuery: func(q orm.PivotQuery) orm.PivotQuery { + return q.Where("active", 1) + }, + Touches: true, + }, + } +} + +// Usage: +user := User{ID: 7} +orm := facades.Orm() + +// Attach roles 1 and 2 with pivot data. +orm.AttachWithPivot(&user, "Roles", map[any]map[string]any{ + uint(1): {"active": 1, "priority": 10}, + uint(2): {"active": 1, "priority": 5}, +}) +// Pivot rows inserted with active=1, priority=N, created_at=NOW(), updated_at=NOW(). +// User.UpdatedAt also bumped (Touches: true). + +// Sync to roles {1, 3}: detaches 2, attaches 3, leaves 1 untouched. +result, _ := orm.Sync(&user, "Roles", []any{uint(1), uint(3)}) +// result.Attached = [3], result.Detached = [2], result.Updated = [] + +// Eager load roles with pivot data. +var u User +orm.Query().With("Roles").First(&u, uint(7)) +// u.Roles[0].Pivot.Active == true, u.Roles[0].Pivot.Priority == 10, etc. +``` diff --git a/tests/go.mod b/tests/go.mod index 8eec54383..a05254ebf 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -40,7 +40,7 @@ require ( github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/console v1.0.5 // indirect - github.com/dave/dst v0.27.3 // indirect + github.com/dave/dst v0.27.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dromara/carbon/v2 v2.6.11 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect diff --git a/tests/go.sum b/tests/go.sum index f3d6c73e2..247662963 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -82,8 +82,8 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= -github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/dst v0.27.4 h1:d+EVnOZmphH+lUEXq9rit4GjsFSKJ3AhfRWf7eobTps= +github.com/dave/dst v0.27.4/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/tests/mock_config.go b/tests/mock_config.go index acbc2bdb2..44fdf753c 100644 --- a/tests/mock_config.go +++ b/tests/mock_config.go @@ -27,6 +27,7 @@ func mockDatabaseConfigWithoutWriteAndRead(mockConfig *mocksconfig.Config, confi mockConfig.EXPECT().GetBool("app.debug").Return(true) mockConfig.EXPECT().GetInt("database.slow_threshold", 200).Return(200) + mockConfig.EXPECT().GetInt("database.eager_load_chunk_size", 1000).Return(1000).Maybe() mockConfig.EXPECT().GetInt("database.pool.max_idle_conns", 10).Return(10) mockConfig.EXPECT().GetInt("database.pool.max_open_conns", 100).Return(100) mockConfig.EXPECT().GetDuration("database.pool.conn_max_idletime", time.Duration(3600)).Return(time.Duration(3600)) diff --git a/tests/queries_relationships_test.go b/tests/queries_relationships_test.go new file mode 100644 index 000000000..33e69d171 --- /dev/null +++ b/tests/queries_relationships_test.go @@ -0,0 +1,542 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/orm" +) + +// QueriesRelationshipsTestSuite covers the QueryWithRelations contract: Has, WhereHas, DoesntHave, +// WithCount, HasMorph and the through-relation variants. It runs against every available driver +// using the same harness as QueryTestSuite. +type QueriesRelationshipsTestSuite struct { + suite.Suite + queries map[string]*TestQuery +} + +func TestQueriesRelationshipsTestSuite(t *testing.T) { + t.Parallel() + suite.Run(t, &QueriesRelationshipsTestSuite{ + queries: make(map[string]*TestQuery), + }) +} + +func (s *QueriesRelationshipsTestSuite) SetupSuite() { + // Run against every available driver. Tests that require docker (mysql / postgres / sqlserver) + // will spin up containers; sqlite is in-process. + s.queries = NewTestQueryBuilder().All("", false) +} + +func (s *QueriesRelationshipsTestSuite) SetupTest() { + for _, query := range s.queries { + query.CreateTable() + } +} + +// rq is a tiny ergonomic wrapper that returns the (already relation-capable) Query for chaining. +// It exists so test bodies remain stable if Query stops embedding QueriesRelationships in the +// future; today it is a passthrough. +func rq(q contractsorm.Query) contractsorm.Query { return q } + +func (s *QueriesRelationshipsTestSuite) TestHas_Existence() { + for driver, query := range s.queries { + s.Run(driver, func() { + alice := &User{Name: "rel_has_alice", Books: []*Book{{Name: "ab1"}, {Name: "ab2"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&alice)) + bob := &User{Name: "rel_has_bob"} + s.Nil(query.Query().Create(&bob)) + carol := &User{Name: "rel_has_carol", Books: []*Book{{Name: "cb1"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&carol)) + + rq := rq(query.Query()) + var users []User + s.Nil(rq.Has("Books"). + Where("name like ?", "rel_has_%").Get(&users)) + names := namesOf(users) + s.ElementsMatch([]string{"rel_has_alice", "rel_has_carol"}, names) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestHas_CountComparison() { + for driver, query := range s.queries { + s.Run(driver, func() { + alice := &User{Name: "rel_hasc_alice", Books: []*Book{{Name: "h1"}, {Name: "h2"}, {Name: "h3"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&alice)) + bob := &User{Name: "rel_hasc_bob", Books: []*Book{{Name: "h4"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&bob)) + + rq := rq(query.Query()) + var users []User + s.Nil(rq.Has("Books", ">=", 2). + Where("name like ?", "rel_hasc_%").Get(&users)) + s.Len(users, 1) + s.Equal("rel_hasc_alice", users[0].Name) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestDoesntHave() { + for driver, query := range s.queries { + s.Run(driver, func() { + withBooks := &User{Name: "rel_dh_with", Books: []*Book{{Name: "dhb"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&withBooks)) + without := &User{Name: "rel_dh_without"} + s.Nil(query.Query().Create(&without)) + + rq := rq(query.Query()) + var users []User + s.Nil(rq.DoesntHave("Books"). + Where("name like ?", "rel_dh_%").Get(&users)) + s.Len(users, 1) + s.Equal("rel_dh_without", users[0].Name) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestWhereHas_Callback() { + for driver, query := range s.queries { + s.Run(driver, func() { + match := &User{Name: "rel_wh_match", Books: []*Book{{Name: "wh_target"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&match)) + other := &User{Name: "rel_wh_other", Books: []*Book{{Name: "wh_other"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&other)) + + rq := rq(query.Query()) + cb := func(q contractsorm.Query) contractsorm.Query { + return q.Where("name = ?", "wh_target") + } + var users []User + s.Nil(rq.WhereHas("Books", cb). + Where("name like ?", "rel_wh_%").Get(&users)) + s.Len(users, 1) + s.Equal("rel_wh_match", users[0].Name) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestHas_BelongsTo() { + for driver, query := range s.queries { + s.Run(driver, func() { + user := &User{Name: "rel_bt_user", Address: &Address{Name: "rel_bt_address"}} + s.Nil(query.Query().Select(orm.Associations).Create(&user)) + + rq := rq(query.Query()) + var addresses []Address + s.Nil(rq.Has("User"). + Where("name = ?", "rel_bt_address").Get(&addresses)) + s.Len(addresses, 1) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestHas_ManyToMany() { + for driver, query := range s.queries { + s.Run(driver, func() { + withRole := &User{Name: "rel_mtm_with", Roles: []*Role{{Name: "rel_mtm_role"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&withRole)) + noRole := &User{Name: "rel_mtm_no"} + s.Nil(query.Query().Create(&noRole)) + + rq := rq(query.Query()) + var users []User + s.Nil(rq.Has("Roles"). + Where("name like ?", "rel_mtm_%").Get(&users)) + names := namesOf(users) + s.Contains(names, "rel_mtm_with") + s.NotContains(names, "rel_mtm_no") + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestHasMorph() { + for driver, query := range s.queries { + s.Run(driver, func() { + withHouse := &User{Name: "rel_hm_with", House: &House{Name: "rel_hm_house"}} + s.Nil(query.Query().Select(orm.Associations).Create(&withHouse)) + noHouse := &User{Name: "rel_hm_no"} + s.Nil(query.Query().Create(&noHouse)) + + rq := rq(query.Query()) + var users []User + s.Nil(rq.HasMorph("House", []any{&User{}}). + Where("name like ?", "rel_hm_%").Get(&users)) + s.Len(users, 1) + s.Equal("rel_hm_with", users[0].Name) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestNestedHas() { + for driver, query := range s.queries { + s.Run(driver, func() { + // User -> Books -> Author. Only carol has a book with an Author. + carol := &User{ + Name: "rel_nested_carol", + Books: []*Book{ + {Name: "carol_book", Author: &Author{Name: "carol_author"}}, + }, + } + s.Nil(query.Query().Select(orm.Associations).Create(&carol)) + dan := &User{Name: "rel_nested_dan", Books: []*Book{{Name: "dan_book"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&dan)) + + rq := rq(query.Query()) + var users []User + s.Nil(rq.Has("Books.Author"). + Where("name like ?", "rel_nested_%").Get(&users)) + names := namesOf(users) + s.Contains(names, "rel_nested_carol") + s.NotContains(names, "rel_nested_dan") + }) + } +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +func namesOf(users []User) []string { + out := make([]string, 0, len(users)) + for _, u := range users { + out = append(out, u.Name) + } + return out +} + +// --------------------------------------------------------------------------- +// HasManyThrough integration: User -> Authors through Books. +// Ported from /libs/fedaco/test/relations/database-relation-has-many-through-integration.spec.ts +// --------------------------------------------------------------------------- + +// userWithThrough re-uses the users table but declares a HasManyThrough relation via the unified +// Relations() entry point. +type userWithThrough struct{} + +func (userWithThrough) TableName() string { return "users" } + +func (userWithThrough) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Authors": contractsorm.HasManyThrough{ + Related: &Author{}, + Through: &Book{}, + FirstKey: "user_id", // FK on Book pointing at User + SecondKey: "book_id", // FK on Author pointing at Book + LocalKey: "id", + SecondLocalKey: "id", + }, + } +} + +func (s *QueriesRelationshipsTestSuite) TestHasManyThrough_WhereHas() { + for driver, query := range s.queries { + s.Run(driver, func() { + // Seed: u1 has a book with author "match"; u2 has a book without an author. + u1 := &User{Name: "rel_th_u1", Books: []*Book{{Name: "th_book1", Author: &Author{Name: "th_author_match"}}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u1)) + u2 := &User{Name: "rel_th_u2", Books: []*Book{{Name: "th_book2"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u2)) + + rq := rq(query.Query().Model(&userWithThrough{})) + cb := func(q contractsorm.Query) contractsorm.Query { + return q.Where("authors.name = ?", "th_author_match") + } + var users []User + s.Nil(rq.WhereHas("Authors", cb). + Where("name like ?", "rel_th_%").Get(&users)) + s.Len(users, 1) + s.Equal("rel_th_u1", users[0].Name) + }) + } +} + +// --------------------------------------------------------------------------- +// SQL-shape assertions ported from libs/fedaco/test/fedaco-builder-relation.spec.ts. +// +// These tests use sqlite + ToRawSql() to compile a deterministic, fully bound SQL string for +// each query, then compare it byte-for-byte. Identifier quoting (backticks) matches sqlite's +// preferred style; the `users.deleted_at IS NULL` tail is added by Goravel's soft-delete scope. +// --------------------------------------------------------------------------- + +func sqliteOnly(s *QueriesRelationshipsTestSuite) *TestQuery { + if q, ok := s.queries[sqliteDriverName()]; ok { + return q + } + s.T().Skip("requires sqlite driver") + return nil +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_Has() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.Has("Books").ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `books` WHERE books.user_id = users.id) AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_DoesntHave() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.DoesntHave("Books").ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `books` WHERE books.user_id = users.id) AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_HasCount() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.Has("Books", ">=", 3).ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE (SELECT COUNT(*) FROM `books` WHERE books.user_id = users.id) >= 3 AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_NestedHas() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.Has("Books.Author").ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `books` WHERE books.user_id = users.id AND EXISTS (SELECT 1 FROM `authors` WHERE authors.book_id = books.id)) AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_WhereHasWithCallback() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + cb := func(q contractsorm.Query) contractsorm.Query { + return q.Where("name = ?", "wh_target") + } + var users []User + sql := rq.WhereHas("Books", cb).ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `books` WHERE books.user_id = users.id AND name = 'wh_target') AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_OrHas() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.Where("name = ?", "x").OrHas("Books").ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE (name = 'x' OR EXISTS (SELECT 1 FROM `books` WHERE books.user_id = users.id)) AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_OrDoesntHave() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.Where("name = ?", "x").OrDoesntHave("Books").ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE (name = 'x' OR NOT EXISTS (SELECT 1 FROM `books` WHERE books.user_id = users.id)) AND `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_WithCount() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.WithCount("Books").ToRawSql().Get(&users) + s.Equal( + "SELECT users.*, (SELECT COUNT(*) FROM `books` WHERE books.user_id = users.id) AS books_count FROM `users` WHERE `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_WithCountAndCallback() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + cb := func(q contractsorm.Query) contractsorm.Query { + return q.Where("name = ?", "active") + } + var users []User + sql := rq.WithCount(contractsorm.RelationCount{Name: "Books", Callback: cb}).ToRawSql().Get(&users) + s.Equal( + "SELECT users.*, (SELECT COUNT(*) FROM `books` WHERE books.user_id = users.id AND name = 'active') AS books_count FROM `users` WHERE `users`.`deleted_at` IS NULL", + sql, + ) +} + +func (s *QueriesRelationshipsTestSuite) TestSQL_WithCountWithAlias() { + q := sqliteOnly(s) + if q == nil { + return + } + rq := rq(q.Query().Model(&User{})) + var users []User + sql := rq.WithCount(contractsorm.RelationCount{Name: "Books", Alias: "book_total"}).ToRawSql().Get(&users) + s.Equal( + "SELECT users.*, (SELECT COUNT(*) FROM `books` WHERE books.user_id = users.id) AS book_total FROM `users` WHERE `users`.`deleted_at` IS NULL", + sql, + ) +} + +func sqliteDriverName() string { + return "SQLite" +} + +// --------------------------------------------------------------------------- +// Aggregate retrieval: WithCount / WithMax / WithMin / WithSum / WithAvg / WithExists +// +// WithAggregate emits a sub-select column with a deterministic alias (see aggregateAlias in +// database/gorm/queries_relationships.go). To read the value back, declare a struct field tagged +// with that alias as its `gorm:"column:..."`. We use a DTO struct here rather than amending User, +// to keep the demonstration self-contained and avoid affecting other suites. +// --------------------------------------------------------------------------- + +// userAggregates is a DTO that maps to the same `users` table but exposes the aggregate alias +// columns. It is populated via Model(&User{}).Get(&[]userAggregates{}) — Model controls the FROM +// table and relation resolution; the DTO controls how rows are scanned. +type userAggregates struct { + ID uint `gorm:"column:id"` + Name string `gorm:"column:name"` + BooksCount int64 `gorm:"column:books_count"` + BooksAuthorCount int64 `gorm:"column:books_author_count"` + PopularBooks int64 `gorm:"column:popular_books"` + BooksMaxID *int64 `gorm:"column:books_max_id"` + BooksMinID *int64 `gorm:"column:books_min_id"` + BooksSumID *int64 `gorm:"column:books_sum_id"` + BooksAvgID *float64 `gorm:"column:books_avg_id"` + BooksExists bool `gorm:"column:books_exists"` +} + +func (s *QueriesRelationshipsTestSuite) TestWithCount_Retrieve() { + for driver, query := range s.queries { + s.Run(driver, func() { + // u1: 2 books, u2: 0 books, u3: 1 book with an author. + u1 := &User{Name: "agg_count_u1", Books: []*Book{{Name: "ab1"}, {Name: "ab2"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u1)) + u2 := &User{Name: "agg_count_u2"} + s.Nil(query.Query().Create(&u2)) + u3 := &User{Name: "agg_count_u3", Books: []*Book{{Name: "ab3", Author: &Author{Name: "Author1"}}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u3)) + + var rows []userAggregates + s.Nil(query.Query().Model(&User{}).Where("name like ?", "agg_count_%").OrderBy("name"). + WithCount("Books"). + WithCount("Books.Author"). + Get(&rows)) + + s.Len(rows, 3) + s.Equal(int64(2), rows[0].BooksCount, "u1 has 2 books") + s.Equal(int64(0), rows[1].BooksCount, "u2 has 0 books") + s.Equal(int64(1), rows[2].BooksCount, "u3 has 1 book") + s.Equal(int64(0), rows[0].BooksAuthorCount, "u1's books have no authors") + s.Equal(int64(1), rows[2].BooksAuthorCount, "u3's book has an author (nested count)") + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestWithCount_CustomAliasAndCallback() { + for driver, query := range s.queries { + s.Run(driver, func() { + // Three books — only two start with "pop_". Custom alias + callback narrows the count. + u := &User{Name: "agg_alias_u", Books: []*Book{{Name: "pop_x"}, {Name: "pop_y"}, {Name: "boring"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + cb := func(q contractsorm.Query) contractsorm.Query { + return q.Where("name like ?", "pop_%") + } + var rows []userAggregates + s.Nil(query.Query().Model(&User{}).Where("name = ?", "agg_alias_u"). + WithCount(contractsorm.RelationCount{Name: "Books", Alias: "popular_books", Callback: cb}). + Get(&rows)) + + s.Len(rows, 1) + s.Equal(int64(2), rows[0].PopularBooks, "callback filters to 2 books named pop_*") + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestWithMaxMinSumAvg_Retrieve() { + for driver, query := range s.queries { + s.Run(driver, func() { + // Three books with consecutive auto-increment IDs (1, 2, 3) on a fresh table. + u := &User{Name: "agg_num_u", Books: []*Book{{Name: "n1"}, {Name: "n2"}, {Name: "n3"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var rows []userAggregates + s.Nil(query.Query().Model(&User{}).Where("name = ?", "agg_num_u"). + WithMax("Books", "id"). + WithMin("Books", "id"). + WithSum("Books", "id"). + WithAvg("Books", "id"). + Get(&rows)) + + s.Len(rows, 1) + s.NotNil(rows[0].BooksMaxID) + s.NotNil(rows[0].BooksMinID) + s.NotNil(rows[0].BooksSumID) + s.NotNil(rows[0].BooksAvgID) + minID := *rows[0].BooksMinID + maxID := *rows[0].BooksMaxID + s.Equal(int64(2), maxID-minID, "3 consecutive IDs => max-min = 2") + s.Equal(minID+(minID+1)+(minID+2), *rows[0].BooksSumID) + s.InDelta(float64(minID)+1.0, *rows[0].BooksAvgID, 0.001) + }) + } +} + +func (s *QueriesRelationshipsTestSuite) TestWithExists_Retrieve() { + for driver, query := range s.queries { + s.Run(driver, func() { + withBooks := &User{Name: "agg_exist_yes", Books: []*Book{{Name: "ex1"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&withBooks)) + withoutBooks := &User{Name: "agg_exist_no"} + s.Nil(query.Query().Create(&withoutBooks)) + + var rows []userAggregates + s.Nil(query.Query().Model(&User{}).Where("name like ?", "agg_exist_%").OrderBy("name"). + WithExists("Books"). + Get(&rows)) + + s.Len(rows, 2) + s.False(rows[0].BooksExists, "agg_exist_no has no books") + s.True(rows[1].BooksExists, "agg_exist_yes has books") + }) + } +} diff --git a/tests/query_test.go b/tests/query_test.go index 5e6d97bd9..100f424c8 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -83,7 +83,7 @@ func (s *QueryTestSuite) TestAssociation() { s.True(user1.ID > 0) var userAddress Address - s.Nil(query.Query().Model(&user1).Association("Address").Find(&userAddress)) + s.Nil(query.Query().Related(&user1, "Address").First(&userAddress)) s.True(userAddress.ID > 0) s.Equal("association_find_address", userAddress.Name) }, @@ -105,7 +105,7 @@ func (s *QueryTestSuite) TestAssociation() { var user1 User s.Nil(query.Query().Find(&user1, user.ID), driver) s.True(user1.ID > 0, driver) - s.Nil(query.Query().Model(&user1).Association("Address").Append(&Address{Name: "association_has_one_append_address1"}), driver) + s.Nil(query.Query().Relation(&user1, "Address").Save(&Address{Name: "association_has_one_append_address1"}), driver) s.Nil(query.Query().Load(&user1, "Address"), driver) s.True(user1.Address.ID > 0, driver) @@ -131,7 +131,7 @@ func (s *QueryTestSuite) TestAssociation() { var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Model(&user1).Association("Books").Append(&Book{Name: "association_has_many_append_address3"})) + s.Nil(query.Query().Relation(&user1, "Books").Save(&Book{Name: "association_has_many_append_address3"})) s.Nil(query.Query().Load(&user1, "Books")) s.Equal(3, len(user1.Books)) @@ -155,7 +155,7 @@ func (s *QueryTestSuite) TestAssociation() { var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Model(&user1).Association("Address").Replace(&Address{Name: "association_has_one_append_address1"})) + s.Nil(query.Query().Relation(&user1, "Address").Save(&Address{Name: "association_has_one_append_address1"})) s.Nil(query.Query().Load(&user1, "Address")) s.True(user1.Address.ID > 0) @@ -181,7 +181,9 @@ func (s *QueryTestSuite) TestAssociation() { var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Model(&user1).Association("Books").Replace(&Book{Name: "association_has_many_replace_address3"})) + // New design: explicit two-step for HasMany replace (old .Replace would fail on NOT NULL FK) + s.Nil(query.Query().Related(&user1, "Books").Delete()) + s.Nil(query.Query().Relation(&user1, "Books").Save(&Book{Name: "association_has_many_replace_address3"})) s.Nil(query.Query().Load(&user1, "Books")) s.Equal(1, len(user1.Books)) @@ -202,15 +204,14 @@ func (s *QueryTestSuite) TestAssociation() { s.True(user.ID > 0) s.True(user.Address.ID > 0) - // No ID when Delete + // No ID when Delete — new design: true delete (not nullify FK) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Model(&user1).Association("Address").Delete(&Address{Name: "association_delete_address"})) + s.Nil(query.Query().Related(&user1, "Address").Where("name", "association_delete_address").Delete()) s.Nil(query.Query().Load(&user1, "Address")) - s.True(user1.Address.ID > 0) - s.Equal("association_delete_address", user1.Address.Name) + s.Nil(user1.Address) // Has ID when Delete var user2 User @@ -218,7 +219,7 @@ func (s *QueryTestSuite) TestAssociation() { s.True(user2.ID > 0) var userAddress Address userAddress.ID = user1.Address.ID - s.Nil(query.Query().Model(&user2).Association("Address").Delete(&userAddress)) + s.Nil(query.Query().Related(&user2, "Address").Where("id", userAddress.ID).Delete()) s.Nil(query.Query().Load(&user2, "Address")) s.Nil(user2.Address) @@ -242,7 +243,7 @@ func (s *QueryTestSuite) TestAssociation() { var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Model(&user1).Association("Address").Clear()) + s.Nil(query.Query().Related(&user1, "Address").Delete()) s.Nil(query.Query().Load(&user1, "Address")) s.Nil(user1.Address) @@ -267,7 +268,9 @@ func (s *QueryTestSuite) TestAssociation() { var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Equal(int64(2), query.Query().Model(&user1).Association("Books").Count()) + count, err := query.Query().Related(&user1, "Books").Count() + s.Nil(err) + s.Equal(int64(2), count) }, }, } @@ -3418,7 +3421,9 @@ func (s *QueryTestSuite) TestLoad() { s.True(user1.ID > 0) s.Nil(user1.Address) s.Equal(0, len(user1.Books)) - s.Nil(query.Query().Load(&user1, "Books", "name = ?", "load_book0")) + s.Nil(query.Query().Load(&user1, "Books", func(query contractsorm.Query) contractsorm.Query { + return query.Where("name = ?", "load_book0") + })) s.True(user1.ID > 0) s.Nil(user1.Address) s.Equal(1, len(user1.Books)) @@ -3531,7 +3536,9 @@ func (s *QueryTestSuite) TestLoadMissing() { description: "don't load when not missing", setup: func(description string) { var user1 User - s.Nil(query.Query().With("Books", "name = ?", "load_missing_book0").Find(&user1, user.ID)) + s.Nil(query.Query().With("Books", func(query contractsorm.Query) contractsorm.Query { + return query.Where("name = ?", "load_missing_book0") + }).Find(&user1, user.ID)) s.True(user1.ID > 0) s.Nil(user1.Address) s.True(len(user1.Books) == 1) @@ -4630,7 +4637,9 @@ func (s *QueryTestSuite) TestWith() { description: "with simple conditions", setup: func(description string) { var user1 User - s.Nil(query.Query().With("Books", "name = ?", "with_book0").Find(&user1, user.ID)) + s.Nil(query.Query().With("Books", func(query contractsorm.Query) contractsorm.Query { + return query.Where("name = ?", "with_book0") + }).Find(&user1, user.ID)) s.True(user1.ID > 0) s.Nil(user1.Address) s.Equal(1, len(user1.Books)) diff --git a/tests/with_test.go b/tests/with_test.go new file mode 100644 index 000000000..ad340dc38 --- /dev/null +++ b/tests/with_test.go @@ -0,0 +1,347 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/orm" +) + +// WithTestSuite covers the With / Without / WithOnly methods, which run Goravel's own eager +// loader (not GORM Preload). Each kind of relationship is exercised against every available +// driver. +type WithTestSuite struct { + suite.Suite + queries map[string]*TestQuery +} + +func TestWithSuite(t *testing.T) { + t.Parallel() + suite.Run(t, &WithTestSuite{ + queries: make(map[string]*TestQuery), + }) +} + +func (s *WithTestSuite) SetupSuite() { + s.queries = NewTestQueryBuilder().All("", false) +} + +func (s *WithTestSuite) SetupTest() { + for _, query := range s.queries { + query.CreateTable() + } +} + +func (s *WithTestSuite) sqlite() *TestQuery { + if q, ok := s.queries["SQLite"]; ok { + return q + } + s.T().Skip("requires sqlite driver") + return nil +} + +// --------------------------------------------------------------------------- +// Per-kind integration tests +// --------------------------------------------------------------------------- + +func (s *WithTestSuite) TestWith_HasMany() { + for driver, query := range s.queries { + s.Run(driver, func() { + alice := &User{Name: "wr_hm_alice", Books: []*Book{{Name: "wr_hm_a1"}, {Name: "wr_hm_a2"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&alice)) + bob := &User{Name: "wr_hm_bob", Books: []*Book{{Name: "wr_hm_b1"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&bob)) + + var users []User + s.Nil(query.Query().Where("name like ?", "wr_hm_%").OrderBy("name"). + With("Books").Get(&users)) + s.Len(users, 2) + s.Equal("wr_hm_alice", users[0].Name) + s.Len(users[0].Books, 2) + s.Equal("wr_hm_bob", users[1].Name) + s.Len(users[1].Books, 1) + }) + } +} + +func (s *WithTestSuite) TestWith_HasOne() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_ho_user", Address: &Address{Name: "wr_ho_addr", Province: "X"}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_ho_user"). + With("Address").First(&loaded)) + s.NotNil(loaded.Address) + s.Equal("wr_ho_addr", loaded.Address.Name) + }) + } +} + +func (s *WithTestSuite) TestWith_BelongsTo() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_bt_user", Address: &Address{Name: "wr_bt_addr"}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var addrs []Address + s.Nil(query.Query().Where("name = ?", "wr_bt_addr"). + With("User").Get(&addrs)) + s.Len(addrs, 1) + s.NotNil(addrs[0].User) + s.Equal("wr_bt_user", addrs[0].User.Name) + }) + } +} + +func (s *WithTestSuite) TestWith_Many2Many() { + for driver, query := range s.queries { + s.Run(driver, func() { + role := &Role{Name: "wr_m2m_role"} + s.Nil(query.Query().Create(&role)) + u := &User{Name: "wr_m2m_user", Roles: []*Role{role}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_m2m_user"). + With("Roles").First(&loaded)) + s.Len(loaded.Roles, 1) + s.Equal("wr_m2m_role", loaded.Roles[0].Name) + }) + } +} + +func (s *WithTestSuite) TestWith_MorphOne() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_mo_user", House: &House{Name: "wr_mo_house"}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_mo_user"). + With("House").First(&loaded)) + s.NotNil(loaded.House) + s.Equal("wr_mo_house", loaded.House.Name) + }) + } +} + +func (s *WithTestSuite) TestWith_MorphMany() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_mm_user", Phones: []*Phone{{Name: "wr_mm_p1"}, {Name: "wr_mm_p2"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_mm_user"). + With("Phones").First(&loaded)) + s.Len(loaded.Phones, 2) + }) + } +} + +func (s *WithTestSuite) TestWith_HasManyThrough() { + for driver, query := range s.queries { + s.Run(driver, func() { + // User -> Books -> Authors (declared via userWithThrough.ThroughRelations()). + u1 := &User{Name: "wr_th_u1", Books: []*Book{{Name: "wr_th_b1", Author: &Author{Name: "wr_th_a1"}}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u1)) + u2 := &User{Name: "wr_th_u2", Books: []*Book{{Name: "wr_th_b2", Author: &Author{Name: "wr_th_a2"}}, {Name: "wr_th_b3", Author: &Author{Name: "wr_th_a3"}}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u2)) + + // Reuse the userAuthorsThrough model (defined below) which carries the through + // declaration but reads from the same users table. + var users []userAuthorsThrough + s.Nil(query.Query().Model(&userAuthorsThrough{}). + Where("name like ?", "wr_th_%").OrderBy("name"). + With("Authors").Get(&users)) + s.Len(users, 2) + s.Len(users[0].Authors, 1) + s.Equal("wr_th_a1", users[0].Authors[0].Name) + s.Len(users[1].Authors, 2) + }) + } +} + +func (s *WithTestSuite) TestWith_Nested() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_n_user", Books: []*Book{{Name: "wr_n_b1", Author: &Author{Name: "wr_n_a1"}}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_n_user"). + With("Books.Author").First(&loaded)) + s.Len(loaded.Books, 1) + s.NotNil(loaded.Books[0].Author) + s.Equal("wr_n_a1", loaded.Books[0].Author.Name) + }) + } +} + +func (s *WithTestSuite) TestWith_Callback() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_cb_user", Books: []*Book{{Name: "wr_cb_keep"}, {Name: "wr_cb_drop"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + cb := func(q contractsorm.Query) contractsorm.Query { + return q.Where("name = ?", "wr_cb_keep") + } + s.Nil(query.Query().Where("name = ?", "wr_cb_user"). + With("Books", cb).First(&loaded)) + s.Len(loaded.Books, 1) + s.Equal("wr_cb_keep", loaded.Books[0].Name) + }) + } +} + +func (s *WithTestSuite) TestWith_Map() { + for driver, query := range s.queries { + s.Run(driver, func() { + role := &Role{Name: "wr_map_role"} + s.Nil(query.Query().Create(&role)) + u := &User{Name: "wr_map_user", Books: []*Book{{Name: "wr_map_book"}}, Roles: []*Role{role}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_map_user"). + With(map[string]contractsorm.RelationCallback{ + "Books": nil, + "Roles": nil, + }).First(&loaded)) + s.Len(loaded.Books, 1) + s.Len(loaded.Roles, 1) + }) + } +} + +func (s *WithTestSuite) TestWith_Columns() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wr_col_user", Books: []*Book{{Name: "wr_col_b1"}}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wr_col_user"). + With("Books:id,name").First(&loaded)) + s.Len(loaded.Books, 1) + s.Equal("wr_col_b1", loaded.Books[0].Name) + // The loader auto-adds the FK column (user_id) so dictionary grouping by FK works, + // even when the user prunes it from the column list. Other unselected columns stay + // at zero value — verified here with CreatedAt which we did not request. + s.Nil(loaded.Books[0].CreatedAt) + }) + } +} + +func (s *WithTestSuite) TestWithout() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wrr_user", Books: []*Book{{Name: "wrr_b"}}, Address: &Address{Name: "wrr_a"}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wrr_user"). + With("Books", "Address"). + Without("Books"). + First(&loaded)) + s.Len(loaded.Books, 0, "Books should not be loaded after Without") + s.NotNil(loaded.Address) + }) + } +} + +func (s *WithTestSuite) TestWithOnly() { + for driver, query := range s.queries { + s.Run(driver, func() { + u := &User{Name: "wro_user", Books: []*Book{{Name: "wro_b"}}, Address: &Address{Name: "wro_a"}} + s.Nil(query.Query().Select(orm.Associations).Create(&u)) + + var loaded User + s.Nil(query.Query().Where("name = ?", "wro_user"). + With("Books", "Address"). + WithOnly("Books"). + First(&loaded)) + s.Len(loaded.Books, 1) + s.Nil(loaded.Address, "Address should not be loaded after WithOnly") + }) + } +} + +// TestWith_ChunkedIN verifies that the loader splits IN clauses into batches when the +// parent count exceeds the chunk size, working around hard limits like Oracle 1000 / SQLite 999. +// We use sqlite (which has the strictest default of 999) and seed > 999 parents to confirm. +func (s *WithTestSuite) TestWith_ChunkedIN() { + q := s.sqlite() + if q == nil { + return + } + const total = 1100 // > SQLite's default SQLITE_MAX_VARIABLE_NUMBER of 999 + + for i := 0; i < total; i++ { + u := &User{Name: fmt.Sprintf("wr_chunk_%04d", i), Books: []*Book{{Name: fmt.Sprintf("wr_chunk_b_%04d", i)}}} + s.Nil(q.Query().Select(orm.Associations).Create(&u)) + } + + var users []User + s.Nil(q.Query().Where("name like ?", "wr_chunk_%"). + With("Books").Get(&users)) + s.Len(users, total, "all parents should be returned") + + loaded := 0 + for _, u := range users { + loaded += len(u.Books) + } + s.Equal(total, loaded, "every parent should have its book loaded across chunked IN queries") +} + +// --------------------------------------------------------------------------- +// SQL-shape assertions (sqlite + ToRawSql) +// --------------------------------------------------------------------------- + +func (s *WithTestSuite) TestSQL_With_HasMany_DoesNotJoinPreload() { + q := s.sqlite() + if q == nil { + return + } + // With defers loading until after the main query runs, so the *outer* SQL must look + // identical to a plain Get — no joins, no GORM preload markers. + var users []User + sql := q.Query().Model(&User{}).With("Books").ToRawSql().Get(&users) + s.Equal( + "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL", + sql, + ) +} + +// --------------------------------------------------------------------------- +// Test-only model: same shape as User but declares HasManyThrough Authors via Books. +// --------------------------------------------------------------------------- + +type userAuthorsThrough struct { + Model + SoftDeletes + Name string + Authors []*Author `gorm:"-"` +} + +func (userAuthorsThrough) TableName() string { return "users" } + +func (userAuthorsThrough) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Authors": contractsorm.HasManyThrough{ + Related: &Author{}, + Through: &Book{}, + FirstKey: "user_id", + SecondKey: "book_id", + LocalKey: "id", + SecondLocalKey: "id", + }, + } +} From da8db93a33d796099aa02a20ced214eab90be777 Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Sun, 10 May 2026 22:27:51 +0800 Subject: [PATCH 07/11] chore: remove unused ColumnType mock (cherry picked from commit 9f6456e0450fa7a579e244fbcd0639a5f75ced83) --- mocks/database/schema/ColumnType.go | 224 ---------------------------- 1 file changed, 224 deletions(-) delete mode 100644 mocks/database/schema/ColumnType.go diff --git a/mocks/database/schema/ColumnType.go b/mocks/database/schema/ColumnType.go deleted file mode 100644 index ccf868d45..000000000 --- a/mocks/database/schema/ColumnType.go +++ /dev/null @@ -1,224 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package schema - -import mock "github.com/stretchr/testify/mock" - -// ColumnType is an autogenerated mock type for the ColumnType type -type ColumnType[K comparable, V interface{}] struct { - mock.Mock -} - -type ColumnType_Expecter[K comparable, V interface{}] struct { - mock *mock.Mock -} - -func (_m *ColumnType[K, V]) EXPECT() *ColumnType_Expecter[K, V] { - return &ColumnType_Expecter[K, V]{mock: &_m.Mock} -} - -// Key provides a mock function with no fields -func (_m *ColumnType[K, V]) Key() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Key") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// ColumnType_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key' -type ColumnType_Key_Call[K comparable, V interface{}] struct { - *mock.Call -} - -// Key is a helper method to define mock.On call -func (_e *ColumnType_Expecter[K, V]) Key() *ColumnType_Key_Call[K, V] { - return &ColumnType_Key_Call[K, V]{Call: _e.mock.On("Key")} -} - -func (_c *ColumnType_Key_Call[K, V]) Run(run func()) *ColumnType_Key_Call[K, V] { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *ColumnType_Key_Call[K, V]) Return(_a0 string) *ColumnType_Key_Call[K, V] { - _c.Call.Return(_a0) - return _c -} - -func (_c *ColumnType_Key_Call[K, V]) RunAndReturn(run func() string) *ColumnType_Key_Call[K, V] { - _c.Call.Return(run) - return _c -} - -// MarshalJSON provides a mock function with no fields -func (_m *ColumnType[K, V]) MarshalJSON() ([]byte, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for MarshalJSON") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func() ([]byte, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() []byte); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ColumnType_MarshalJSON_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarshalJSON' -type ColumnType_MarshalJSON_Call[K comparable, V interface{}] struct { - *mock.Call -} - -// MarshalJSON is a helper method to define mock.On call -func (_e *ColumnType_Expecter[K, V]) MarshalJSON() *ColumnType_MarshalJSON_Call[K, V] { - return &ColumnType_MarshalJSON_Call[K, V]{Call: _e.mock.On("MarshalJSON")} -} - -func (_c *ColumnType_MarshalJSON_Call[K, V]) Run(run func()) *ColumnType_MarshalJSON_Call[K, V] { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *ColumnType_MarshalJSON_Call[K, V]) Return(_a0 []byte, _a1 error) *ColumnType_MarshalJSON_Call[K, V] { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *ColumnType_MarshalJSON_Call[K, V]) RunAndReturn(run func() ([]byte, error)) *ColumnType_MarshalJSON_Call[K, V] { - _c.Call.Return(run) - return _c -} - -// String provides a mock function with no fields -func (_m *ColumnType[K, V]) String() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for String") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// ColumnType_String_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'String' -type ColumnType_String_Call[K comparable, V interface{}] struct { - *mock.Call -} - -// String is a helper method to define mock.On call -func (_e *ColumnType_Expecter[K, V]) String() *ColumnType_String_Call[K, V] { - return &ColumnType_String_Call[K, V]{Call: _e.mock.On("String")} -} - -func (_c *ColumnType_String_Call[K, V]) Run(run func()) *ColumnType_String_Call[K, V] { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *ColumnType_String_Call[K, V]) Return(_a0 string) *ColumnType_String_Call[K, V] { - _c.Call.Return(_a0) - return _c -} - -func (_c *ColumnType_String_Call[K, V]) RunAndReturn(run func() string) *ColumnType_String_Call[K, V] { - _c.Call.Return(run) - return _c -} - -// Value provides a mock function with no fields -func (_m *ColumnType[K, V]) Value() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Value") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// ColumnType_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value' -type ColumnType_Value_Call[K comparable, V interface{}] struct { - *mock.Call -} - -// Value is a helper method to define mock.On call -func (_e *ColumnType_Expecter[K, V]) Value() *ColumnType_Value_Call[K, V] { - return &ColumnType_Value_Call[K, V]{Call: _e.mock.On("Value")} -} - -func (_c *ColumnType_Value_Call[K, V]) Run(run func()) *ColumnType_Value_Call[K, V] { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *ColumnType_Value_Call[K, V]) Return(_a0 string) *ColumnType_Value_Call[K, V] { - _c.Call.Return(_a0) - return _c -} - -func (_c *ColumnType_Value_Call[K, V]) RunAndReturn(run func() string) *ColumnType_Value_Call[K, V] { - _c.Call.Return(run) - return _c -} - -// NewColumnType creates a new instance of ColumnType. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewColumnType[K comparable, V interface{}](t interface { - mock.TestingT - Cleanup(func()) -}) *ColumnType[K, V] { - mock := &ColumnType[K, V]{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} From da72ea398739a9747a25b8be70027f7ee90c681c Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Mon, 11 May 2026 07:51:14 +0800 Subject: [PATCH 08/11] fix: remove dangling OrmAssociation factory referencing deleted mock --- testing/mock/mock.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/mock/mock.go b/testing/mock/mock.go index 078b9e16c..bdcd5fd27 100644 --- a/testing/mock/mock.go +++ b/testing/mock/mock.go @@ -166,10 +166,6 @@ func (r *factory) Orm() *mocksorm.Orm { return mockOrm } -func (r *factory) OrmAssociation() *mocksorm.Association { - return &mocksorm.Association{} -} - func (r *factory) OrmQuery() *mocksorm.Query { return &mocksorm.Query{} } From e03ac4bd4cdb8df7372f25d1d3790805348b2f14 Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Mon, 11 May 2026 07:55:06 +0800 Subject: [PATCH 09/11] refactor(orm): simplify quoteIdent to strip quote characters --- database/gorm/queries_relationships.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/database/gorm/queries_relationships.go b/database/gorm/queries_relationships.go index 511998691..caf14f99f 100644 --- a/database/gorm/queries_relationships.go +++ b/database/gorm/queries_relationships.go @@ -914,13 +914,7 @@ func aggregateAlias(relation, fn, column string) string { // emit the bare identifier and let the dialects accept it - all tested dialects parse unquoted // identifiers when the names are simple snake_case. func quoteIdent(name string) string { - if name == "" { - return "" - } - if strings.ContainsAny(name, " (.") { - return name - } - return name + return strings.NewReplacer("`", "", "'", "", `"`, "").Replace(name) } // Sanity: ensure *Query satisfies contractsorm.QueryWithRelations at compile time. From 9e1cb34071520a943752cfe47f8ba053a03fb71f Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Wed, 20 May 2026 10:30:35 +0800 Subject: [PATCH 10/11] fix(tests): migrate test models to use Relations() method 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. --- tests/models.go | 69 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/tests/models.go b/tests/models.go index ae1d8ce3d..f3c7b0b73 100644 --- a/tests/models.go +++ b/tests/models.go @@ -35,11 +35,11 @@ type User struct { Name string Bio *string Avatar string - Address *Address - Books []*Book - House *House `gorm:"polymorphic:Houseable"` - Phones []*Phone `gorm:"polymorphic:Phoneable"` - Roles []*Role `gorm:"many2many:role_user"` + Address *Address `gorm:"-"` + Books []*Book `gorm:"-"` + House *House `gorm:"-"` + Phones []*Phone `gorm:"-"` + Roles []*Role `gorm:"-"` Ratio float64 age int } @@ -48,6 +48,29 @@ func (r *User) Factory() factory.Factory { return &UserFactory{} } +func (r *User) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Address": contractsorm.HasOne{ + Related: &Address{}, + }, + "Books": contractsorm.HasMany{ + Related: &Book{}, + }, + "House": contractsorm.MorphOne{ + Related: &House{}, + Name: "Houseable", + }, + "Phones": contractsorm.MorphMany{ + Related: &Phone{}, + Name: "Phoneable", + }, + "Roles": contractsorm.Many2Many{ + Related: &Role{}, + Table: "role_user", + }, + } +} + func (r *User) DispatchesEvents() map[contractsorm.EventType]func(contractsorm.Event) error { return map[contractsorm.EventType]func(contractsorm.Event) error{ contractsorm.EventCreating: func(event contractsorm.Event) error { @@ -418,7 +441,16 @@ type Role struct { Model Name string Avatar string - Users []*User `gorm:"many2many:role_user"` + Users []*User `gorm:"-"` +} + +func (r *Role) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "Users": contractsorm.Many2Many{ + Related: &User{}, + Table: "role_user", + }, + } } type Address struct { @@ -426,15 +458,34 @@ type Address struct { UserID uint Name string Province string - User *User + User *User `gorm:"-"` +} + +func (r *Address) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "User": contractsorm.BelongsTo{ + Related: &User{}, + }, + } } type Book struct { Model UserID uint Name string - User *User - Author *Author + User *User `gorm:"-"` + Author *Author `gorm:"-"` +} + +func (r *Book) Relations() map[string]contractsorm.Relation { + return map[string]contractsorm.Relation{ + "User": contractsorm.BelongsTo{ + Related: &User{}, + }, + "Author": contractsorm.HasOne{ + Related: &Author{}, + }, + } } type Author struct { From eda8bab529e52556711169cbd7e779246851860f Mon Sep 17 00:00:00 2001 From: LinBo Len Date: Wed, 20 May 2026 10:40:42 +0800 Subject: [PATCH 11/11] fix(tests): update with_test to use new RelationWriter API --- contracts/database/orm/relation_test.go | 52 +++ database/gorm/eager_loader_test.go | 89 ++++ database/gorm/event_extra_test.go | 39 ++ database/gorm/queries_relationships_test.go | 10 +- database/gorm/query.go | 68 +-- database/gorm/relation_sql_capture_test.go | 158 +++++++ database/gorm/relation_sql_test.go | 194 ++++++++ database/gorm/relation_writer_test.go | 238 ++++++++++ database/gorm/relation_writes_test.go | 122 +++++ database/gorm/row_test.go | 20 + database/orm/model.go | 3 - docs/orm-many-to-many.md | 290 ------------ errors/list.go | 1 - tests/models.go | 4 +- tests/queries_relationships_test.go | 113 +++-- tests/query_test.go | 494 ++++++++++---------- tests/with_test.go | 96 ++-- 17 files changed, 1293 insertions(+), 698 deletions(-) create mode 100644 contracts/database/orm/relation_test.go create mode 100644 database/gorm/event_extra_test.go create mode 100644 database/gorm/relation_sql_capture_test.go create mode 100644 database/gorm/relation_sql_test.go create mode 100644 database/gorm/relation_writer_test.go create mode 100644 database/gorm/row_test.go delete mode 100644 docs/orm-many-to-many.md diff --git a/contracts/database/orm/relation_test.go b/contracts/database/orm/relation_test.go new file mode 100644 index 000000000..f69fdc7c0 --- /dev/null +++ b/contracts/database/orm/relation_test.go @@ -0,0 +1,52 @@ +package orm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestRelation_Kind verifies the Kind() method on every relation type returns the expected +// constant. The resolver dispatches on the concrete type rather than this value, but Kind() is +// used by error messages and diagnostics. +func TestRelation_Kind(t *testing.T) { + tests := []struct { + name string + relation Relation + expected RelationKind + }{ + {"HasOne", HasOne{}, KindHasOne}, + {"HasMany", HasMany{}, KindHasMany}, + {"BelongsTo", BelongsTo{}, KindBelongsTo}, + {"Many2Many", Many2Many{}, KindMany2Many}, + {"MorphOne", MorphOne{}, KindMorphOne}, + {"MorphMany", MorphMany{}, KindMorphMany}, + {"MorphTo", MorphTo{}, KindMorphTo}, + {"MorphToMany", MorphToMany{}, KindMorphToMany}, + {"MorphedByMany", MorphedByMany{}, KindMorphedByMany}, + {"HasOneThrough", HasOneThrough{}, KindHasOneThrough}, + {"HasManyThrough", HasManyThrough{}, KindHasManyThrough}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.relation.Kind()) + }) + } +} + +// TestRelation_KindConstants verifies the named constants have the expected string values. +// These strings appear in error messages, so they're part of the public contract. +func TestRelation_KindConstants(t *testing.T) { + assert.Equal(t, RelationKind("hasOne"), KindHasOne) + assert.Equal(t, RelationKind("hasMany"), KindHasMany) + assert.Equal(t, RelationKind("belongsTo"), KindBelongsTo) + assert.Equal(t, RelationKind("many2Many"), KindMany2Many) + assert.Equal(t, RelationKind("morphOne"), KindMorphOne) + assert.Equal(t, RelationKind("morphMany"), KindMorphMany) + assert.Equal(t, RelationKind("morphTo"), KindMorphTo) + assert.Equal(t, RelationKind("morphToMany"), KindMorphToMany) + assert.Equal(t, RelationKind("morphedByMany"), KindMorphedByMany) + assert.Equal(t, RelationKind("hasOneThrough"), KindHasOneThrough) + assert.Equal(t, RelationKind("hasManyThrough"), KindHasManyThrough) +} diff --git a/database/gorm/eager_loader_test.go b/database/gorm/eager_loader_test.go index dfaf432f5..069ebb18e 100644 --- a/database/gorm/eager_loader_test.go +++ b/database/gorm/eager_loader_test.go @@ -437,3 +437,92 @@ func mustPivotPlan(t *testing.T, fieldName string, pivotProto any) *pivotHydrati fieldByColumn: byCol, } } + +// Additional edge case coverage + +func TestDictKey_AdditionalTypes(t *testing.T) { + assert.Equal(t, "123", dictKey(uint64(123))) + assert.Equal(t, "123.45", dictKey(float64(123.45))) + assert.Equal(t, "true", dictKey(true)) + assert.Equal(t, "false", dictKey(false)) +} + +func TestCollectEagerParents_EdgeCases(t *testing.T) { + // Empty slice + users := []relUser{} + out, err := collectEagerParents(&users) + assert.NoError(t, err) + assert.Empty(t, out) + + // Slice with all nils + nilUsers := []*relUser{nil, nil, nil} + out, err = collectEagerParents(&nilUsers) + assert.NoError(t, err) + assert.Empty(t, out) +} + +func TestSetRelationField_EdgeCases(t *testing.T) { + // Clearing slice field + parent := withSlicePtrRel{Books: []*relBook{{Title: "old"}}} + rv := reflect.ValueOf(&parent).Elem() + err := setRelationField(rv, "Books", nil) + assert.NoError(t, err) + assert.Empty(t, parent.Books) + + // Empty slice of structs + parent2 := withSliceStructRel{Books: []relBook{{Title: "old"}}} + rv2 := reflect.ValueOf(&parent2).Elem() + err = setRelationField(rv2, "Books", []reflect.Value{}) + assert.NoError(t, err) + assert.Empty(t, parent2.Books) +} + +func TestWritePivotField_EdgeCases(t *testing.T) { + // Empty data + role := &roleWithPivot{ID: 1, Name: "admin"} + plan := mustPivotPlan(t, "Pivot", &roleUserPivot{}) + err := writePivotField(t.Context(), reflect.ValueOf(role), map[string]any{}, plan) + assert.NoError(t, err) + assert.Equal(t, "", role.Pivot.Priority) + + // Nil data + role2 := &roleWithPivot{ID: 2, Name: "user"} + err = writePivotField(t.Context(), reflect.ValueOf(role2), nil, plan) + assert.NoError(t, err) +} + +func TestExtractKeys_EmptyInput(t *testing.T) { + db := newStubGormDB(t) + s, err := parseGormSchema(db, &relUser{}) + assert.NoError(t, err) + idField := s.FieldsByDBName["id"] + + q := NewQuery(context.Background(), nil, contractsdatabase.Config{}, db, nil, nil, nil, &Conditions{}) + keys := extractKeys(q, []reflect.Value{}, idField) + assert.Empty(t, keys) +} + +func TestNewSampleModel_WithPointer(t *testing.T) { + u := &relUser{ID: 1} + rv := reflect.ValueOf(u) + got := newSampleModel(rv) + rt := reflect.TypeOf(got) + assert.Equal(t, reflect.Pointer, rt.Kind()) + // When input is a pointer, output is pointer-to-pointer + assert.Equal(t, reflect.Pointer, rt.Elem().Kind()) +} + +func TestApplyEagerLoads_WithNilDest(t *testing.T) { + q := newRelQuery(t) + q.conditions.eagerLoad = []eagerLoadEntry{{relation: "Books"}} + err := q.applyEagerLoads(nil) + assert.NoError(t, err) +} + +func TestRunEagerLoads_InvalidRelation(t *testing.T) { + q := newRelQuery(t) + parents := []reflect.Value{reflect.ValueOf(relUser{ID: 1})} + err := q.runEagerLoads(parents, []eagerLoadEntry{{relation: "NonExistent"}}) + assert.Error(t, err) +} + diff --git a/database/gorm/event_extra_test.go b/database/gorm/event_extra_test.go new file mode 100644 index 000000000..74f0681c9 --- /dev/null +++ b/database/gorm/event_extra_test.go @@ -0,0 +1,39 @@ +package gorm + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" +) + +// TestEvent_Context_NilCtx verifies Context returns the underlying query's context (nil when +// unset). +func TestEvent_Context_NilCtx(t *testing.T) { + q := &Query{instance: &gormio.DB{Statement: &gormio.Statement{Selects: []string{}, Omits: []string{}}}} + e := NewEvent(q, &testEventModel, nil) + assert.Nil(t, e.Context()) +} + +// TestEvent_Context_ReturnsCtx verifies Context returns the configured context. +func TestEvent_Context_ReturnsCtx(t *testing.T) { + type ctxKey int + const k ctxKey = 1 + ctx := context.WithValue(context.Background(), k, "value") + q := &Query{ + ctx: ctx, + instance: &gormio.DB{Statement: &gormio.Statement{Selects: []string{}, Omits: []string{}}}, + } + e := NewEvent(q, &testEventModel, nil) + assert.Equal(t, "value", e.Context().Value(k)) +} + +// TestEvent_IsClean_InverseOfIsDirty verifies IsClean returns !IsDirty. +func TestEvent_IsClean_InverseOfIsDirty(t *testing.T) { + // When dest is empty/nil, IsDirty returns false, so IsClean returns true. + q := &Query{instance: &gormio.DB{Statement: &gormio.Statement{Selects: []string{}, Omits: []string{}}}} + e := NewEvent(q, &testEventModel, nil) + assert.True(t, e.IsClean()) + assert.False(t, e.IsDirty()) +} diff --git a/database/gorm/queries_relationships_test.go b/database/gorm/queries_relationships_test.go index 6ba4dce29..eda41eb51 100644 --- a/database/gorm/queries_relationships_test.go +++ b/database/gorm/queries_relationships_test.go @@ -268,11 +268,11 @@ func TestWithOnly(t *testing.T) { func TestParseRelationArgsAllShapes(t *testing.T) { cb := contractsorm.RelationCallback(func(q contractsorm.Query) contractsorm.Query { return q }) cases := []struct { - name string - args []any - op string - count int - hasCb bool + name string + args []any + op string + count int + hasCb bool }{ {"empty", nil, ">=", 1, false}, {"callback", []any{cb}, ">=", 1, true}, diff --git a/database/gorm/query.go b/database/gorm/query.go index 2acd263bf..45044935c 100644 --- a/database/gorm/query.go +++ b/database/gorm/query.go @@ -12,7 +12,6 @@ import ( "github.com/spf13/cast" gormio "gorm.io/gorm" - "gorm.io/gorm/clause" "github.com/goravel/framework/contracts/config" contractsdatabase "github.com/goravel/framework/contracts/database" @@ -30,8 +29,6 @@ import ( "github.com/goravel/framework/support/str" ) -const Associations = clause.Associations - type Query struct { config config.Config ctx context.Context @@ -806,13 +803,8 @@ func (r *Query) Select(columns ...string) contractsorm.Query { conditions.selectColumns = append(conditions.selectColumns, columns...) conditions.selectColumns = collect.Unique(conditions.selectColumns) - // (*, Associations) is accepted, but * should be removed when there are other columns. - filteredSelectColumns := collect.Of(conditions.selectColumns).Filter(func(item string, _ int) bool { - return item != Associations - }).All() - // * may be added along with other columns automatically, Distinct().Select("name") -> (*, name), * should be removed in this case. - if len(filteredSelectColumns) > 1 { + if len(conditions.selectColumns) > 1 { conditions.selectColumns = collect.Filter(conditions.selectColumns, func(column string, _ int) bool { return column != "*" }) @@ -1571,7 +1563,7 @@ func (r *Query) create(dest any) error { return err } - if err := r.instance.Omit(Associations).Create(dest).Error; err != nil { + if err := r.instance.Create(dest).Error; err != nil { return err } @@ -1731,16 +1723,6 @@ func (r *Query) new(db *gormio.DB) *Query { } func (r *Query) omitCreate(value any) error { - if len(r.instance.Statement.Omits) > 1 { - if slices.Contains(r.instance.Statement.Omits, Associations) { - return errors.OrmQueryAssociationsConflict - } - } - - if len(r.instance.Statement.Omits) == 1 && r.instance.Statement.Omits[0] == Associations { - r.instance.Statement.Selects = []string{} - } - if err := r.saving(value); err != nil { return err } @@ -1748,14 +1730,8 @@ func (r *Query) omitCreate(value any) error { return err } - if len(r.instance.Statement.Omits) == 1 && r.instance.Statement.Omits[0] == Associations { - if err := r.instance.Omit(Associations).Create(value).Error; err != nil { - return err - } - } else { - if err := r.instance.Create(value).Error; err != nil { - return err - } + if err := r.instance.Create(value).Error; err != nil { + return err } if err := r.created(value); err != nil { @@ -1769,10 +1745,6 @@ func (r *Query) omitCreate(value any) error { } func (r *Query) omitSave(value any) error { - if slices.Contains(r.instance.Statement.Omits, Associations) { - return r.instance.Omit(Associations).Save(value).Error - } - return r.instance.Save(value).Error } @@ -1821,7 +1793,7 @@ func (r *Query) retrieved(dest any) error { } func (r *Query) save(value any) error { - return r.instance.Omit(Associations).Save(value).Error + return r.instance.Save(value).Error } func (r *Query) saved(dest any) error { @@ -1841,16 +1813,6 @@ func (r *Query) saving(dest any) error { } func (r *Query) selectCreate(value any) error { - if len(r.instance.Statement.Selects) > 1 { - if slices.Contains(r.instance.Statement.Selects, Associations) { - return errors.OrmQueryAssociationsConflict - } - } - - if len(r.instance.Statement.Selects) == 1 && r.instance.Statement.Selects[0] == Associations { - r.instance.Statement.Selects = []string{} - } - if err := r.saving(value); err != nil { return err } @@ -1873,10 +1835,6 @@ func (r *Query) selectCreate(value any) error { } func (r *Query) selectSave(value any) error { - if slices.Contains(r.instance.Statement.Selects, Associations) { - return r.instance.Session(&gormio.Session{FullSaveAssociations: true}).Save(value).Error - } - if err := r.instance.Save(value).Error; err != nil { return err } @@ -1920,13 +1878,6 @@ func (r *Query) update(values any) (*contractsdb.Result, error) { } if len(r.instance.Statement.Selects) > 0 { - if slices.Contains(r.instance.Statement.Selects, Associations) { - result := r.instance.Session(&gormio.Session{FullSaveAssociations: true}).Updates(values) - return &contractsdb.Result{ - RowsAffected: result.RowsAffected, - }, result.Error - } - result := r.instance.Updates(values) return &contractsdb.Result{ @@ -1935,20 +1886,13 @@ func (r *Query) update(values any) (*contractsdb.Result, error) { } if len(r.instance.Statement.Omits) > 0 { - if slices.Contains(r.instance.Statement.Omits, Associations) { - result := r.instance.Omit(Associations).Updates(values) - - return &contractsdb.Result{ - RowsAffected: result.RowsAffected, - }, result.Error - } result := r.instance.Updates(values) return &contractsdb.Result{ RowsAffected: result.RowsAffected, }, result.Error } - result := r.instance.Omit(Associations).Updates(values) + result := r.instance.Updates(values) return &contractsdb.Result{ RowsAffected: result.RowsAffected, diff --git a/database/gorm/relation_sql_capture_test.go b/database/gorm/relation_sql_capture_test.go new file mode 100644 index 000000000..f697e58ac --- /dev/null +++ b/database/gorm/relation_sql_capture_test.go @@ -0,0 +1,158 @@ +package gorm + +import ( + "fmt" + "os" + "testing" + + gormio "gorm.io/gorm" + + contractsorm "github.com/goravel/framework/contracts/database/orm" +) + +// TestCapture_RelationSQL is a one-shot helper that writes the actual SQL generated for every +// relation type to /tmp/relation_sql.txt. Run with: go test -run TestCapture_RelationSQL. +// Then read /tmp/relation_sql.txt and paste the values into relation_sql_test.go. +func TestCapture_RelationSQL(t *testing.T) { + if os.Getenv("CAPTURE_SQL") != "1" { + t.Skip("set CAPTURE_SQL=1 to run") + } + + f, err := os.Create("/tmp/relation_sql.txt") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := f.Close(); err != nil { + t.Logf("close: %v", err) + } + }() + + cases := []struct { + name string + build func(t *testing.T) (contractsorm.Query, any) + }{ + {"HasOne", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relUser{}) + return q.Related(&relUser{ID: 7}, "Profile"), &relProfile{} + }}, + {"HasMany", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relUser{}) + return q.Related(&relUser{ID: 7}, "Books"), &[]relBook{} + }}, + {"BelongsTo", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relBook{}) + return q.Related(&relBook{AuthorID: 5}, "Author"), &relUser{} + }}, + {"Many2Many", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relUser{}) + return q.Related(&relUser{ID: 7}, "Roles"), &[]relRole{} + }}, + {"MorphOne", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relUser{}) + return q.Related(&relUser{ID: 9}, "Logo"), &relLogo{} + }}, + {"MorphMany", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relUser{}) + return q.Related(&relUser{ID: 9}, "Houses"), &[]relHouse{} + }}, + {"MorphToMany", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &morphPost{}) + return q.Related(&morphPost{ID: 3}, "Tags"), &[]morphTag{} + }}, + {"MorphedByMany", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &morphTag{}) + return q.Related(&morphTag{ID: 1}, "Posts"), &[]morphPost{} + }}, + {"HasManyThrough", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relCountry{}) + return q.Related(&relCountry{ID: 1}, "Posts"), &[]relPost{} + }}, + {"HasOneThrough", func(t *testing.T) (contractsorm.Query, any) { + q := newRelQueryWith(t, &relCountry{}) + return q.Related(&relCountry{ID: 1}, "FirstPost"), &relPost{} + }}, + } + + if _, err := fmt.Fprintln(f, "=== Related SQL ==="); err != nil { + t.Fatal(err) + } + for _, c := range cases { + q, dest := c.build(t) + gq := q.(*Query) + stmt := gq.buildConditions().instance.Session(&gormio.Session{DryRun: true}).Find(dest) + if _, err := fmt.Fprintf(f, "[%s]\n%s\n\n", c.name, stmt.Statement.SQL.String()); err != nil { + t.Fatal(err) + } + } + + existence := []struct { + name string + model any + relation string + dest any + }{ + {"HasOne", &relUser{}, "Profile", &relProfile{}}, + {"HasMany", &relUser{}, "Books", &[]relBook{}}, + {"BelongsTo", &relBook{}, "Author", &[]relUser{}}, + {"Many2Many", &relUser{}, "Roles", &[]relRole{}}, + {"MorphOne", &relUser{}, "Logo", &relLogo{}}, + {"MorphMany", &relUser{}, "Houses", &[]relHouse{}}, + {"MorphToMany", &morphPost{}, "Tags", &[]morphTag{}}, + {"MorphedByMany", &morphTag{}, "Posts", &[]morphPost{}}, + {"HasManyThrough", &relCountry{}, "Posts", &[]relPost{}}, + {"HasOneThrough", &relCountry{}, "FirstPost", &relPost{}}, + } + + if _, err := fmt.Fprintln(f, "=== ExistenceSubquery SQL ==="); err != nil { + t.Fatal(err) + } + for _, c := range existence { + q := newRelQueryWith(t, c.model) + desc, err := resolveRelation(q.instance, c.model, c.relation) + if err != nil { + if _, err := fmt.Fprintf(f, "[%s] ERROR: %v\n\n", c.name, err); err != nil { + t.Fatal(err) + } + continue + } + inner := q.compileExistenceSubquery(desc, nil) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(c.dest) + if _, err := fmt.Fprintf(f, "[%s]\n%s\n\n", c.name, stmt.Statement.SQL.String()); err != nil { + t.Fatal(err) + } + } + + aggregates := []struct { + name string + sub selectSub + }{ + {"Count", selectSub{relation: "Books", column: "*", function: "count"}}, + {"Sum", selectSub{relation: "Books", column: "id", function: "sum"}}, + {"Max", selectSub{relation: "Books", column: "id", function: "max"}}, + {"Min", selectSub{relation: "Books", column: "id", function: "min"}}, + {"Avg", selectSub{relation: "Books", column: "id", function: "avg"}}, + {"Exists", selectSub{relation: "Books", column: "*", function: "exists"}}, + } + + if _, err := fmt.Fprintln(f, "=== AggregateSubquery SQL ==="); err != nil { + t.Fatal(err) + } + for _, c := range aggregates { + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + if err != nil { + if _, err := fmt.Fprintf(f, "[%s] ERROR: %v\n\n", c.name, err); err != nil { + t.Fatal(err) + } + continue + } + inner := q.compileAggregateSubquery(desc, c.sub) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relBook{}) + if _, err := fmt.Fprintf(f, "[%s]\n%s\n\n", c.name, stmt.Statement.SQL.String()); err != nil { + t.Fatal(err) + } + } + + t.Logf("Captured SQL written to /tmp/relation_sql.txt") +} diff --git a/database/gorm/relation_sql_test.go b/database/gorm/relation_sql_test.go new file mode 100644 index 000000000..69c66f557 --- /dev/null +++ b/database/gorm/relation_sql_test.go @@ -0,0 +1,194 @@ +package gorm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + gormio "gorm.io/gorm" +) + +// This file tests the exact SQL emitted by Goravel's relation system for every relation kind. +// SQL strings are captured from the stub dialector (see relation_sql_capture_test.go) and pinned +// here so any change to the query builder is flagged loudly. + +// --------------------------------------------------------------------------- +// Related() SQL — one query method per relation kind +// --------------------------------------------------------------------------- + +func TestRelated_HasOne_SQL(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 7}, "Profile") + sql := newRelationSQL(t, rel, &relProfile{}) + assert.Equal(t, `SELECT * FROM "rel_profiles" WHERE "user_id" = ?`, sql) +} + +func TestRelated_HasMany_SQL_Exact(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 7}, "Books") + sql := newRelationSQL(t, rel, &[]relBook{}) + assert.Equal(t, `SELECT * FROM "rel_books" WHERE "user_id" = ?`, sql) +} + +func TestRelated_BelongsTo_SQL_Exact(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + rel := q.Related(&relBook{AuthorID: 5}, "Author") + sql := newRelationSQL(t, rel, &relUser{}) + assert.Equal(t, `SELECT * FROM "rel_users" WHERE "id" = ?`, sql) +} + +func TestRelated_Many2Many_SQL(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 7}, "Roles") + sql := newRelationSQL(t, rel, &[]relRole{}) + assert.Equal(t, `SELECT "rel_roles"."id","rel_roles"."name" FROM "rel_roles" INNER JOIN rel_user_roles ON rel_user_roles.rel_role_id = rel_roles.id WHERE rel_user_roles.rel_user_id = ?`, sql) +} + +func TestRelated_MorphOne_SQL(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 9}, "Logo") + sql := newRelationSQL(t, rel, &relLogo{}) + assert.Equal(t, `SELECT * FROM "rel_logos" WHERE "logoable_id" = ? AND "logoable_type" = ?`, sql) +} + +func TestRelated_MorphMany_SQL(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + rel := q.Related(&relUser{ID: 9}, "Houses") + sql := newRelationSQL(t, rel, &[]relHouse{}) + assert.Equal(t, `SELECT * FROM "rel_houses" WHERE "houseable_id" = ? AND "houseable_type" = ?`, sql) +} + +func TestRelated_MorphToMany_SQL(t *testing.T) { + q := newRelQueryWith(t, &morphPost{}) + rel := q.Related(&morphPost{ID: 3}, "Tags") + sql := newRelationSQL(t, rel, &[]morphTag{}) + assert.Equal(t, `SELECT "morph_tags"."id","morph_tags"."name" FROM "morph_tags" INNER JOIN taggables ON taggables.morph_tag_id = morph_tags.id WHERE taggables.taggable_id = ? AND taggables.taggable_type = ?`, sql) +} + +func TestRelated_MorphedByMany_SQL(t *testing.T) { + q := newRelQueryWith(t, &morphTag{}) + rel := q.Related(&morphTag{ID: 1}, "Posts") + sql := newRelationSQL(t, rel, &[]morphPost{}) + assert.Equal(t, `SELECT "morph_posts"."id","morph_posts"."title" FROM "morph_posts" INNER JOIN taggables ON taggables.morph_post_id = morph_posts.id WHERE taggables.taggable_id = ? AND taggables.taggable_type = ?`, sql) +} + +func TestRelated_HasManyThrough_SQL(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + rel := q.Related(&relCountry{ID: 1}, "Posts") + sql := newRelationSQL(t, rel, &[]relPost{}) + assert.Equal(t, `SELECT "rel_posts"."id","rel_posts"."title","rel_posts"."user_id" FROM "rel_posts" INNER JOIN rel_users ON rel_posts.rel_user_id = rel_users.id WHERE rel_users.rel_country_id = ?`, sql) +} + +func TestRelated_HasOneThrough_SQL(t *testing.T) { + q := newRelQueryWith(t, &relCountry{}) + rel := q.Related(&relCountry{ID: 1}, "FirstPost") + sql := newRelationSQL(t, rel, &relPost{}) + assert.Equal(t, `SELECT "rel_posts"."id","rel_posts"."title","rel_posts"."user_id" FROM "rel_posts" INNER JOIN rel_users ON rel_posts.rel_user_id = rel_users.id WHERE rel_users.rel_country_id = ?`, sql) +} + +// --------------------------------------------------------------------------- +// compileExistenceSubquery SQL — used by Has / WhereHas / DoesntHave +// --------------------------------------------------------------------------- + +func runExistenceSQL(t *testing.T, model any, relation string, dest any) string { + t.Helper() + q := newRelQueryWith(t, model) + desc, err := resolveRelation(q.instance, model, relation) + assert.NoError(t, err) + inner := q.compileExistenceSubquery(desc, nil) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(dest) + return stmt.Statement.SQL.String() +} + +func TestCompileExistenceSubquery_HasOne_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relUser{}, "Profile", &relProfile{}) + assert.Equal(t, `SELECT 1 FROM "rel_profiles" WHERE rel_profiles.user_id = rel_users.id`, sql) +} + +func TestCompileExistenceSubquery_HasMany_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relUser{}, "Books", &[]relBook{}) + assert.Equal(t, `SELECT 1 FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} + +func TestCompileExistenceSubquery_BelongsTo_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relBook{}, "Author", &[]relUser{}) + assert.Equal(t, `SELECT 1 FROM "rel_users" WHERE rel_users.id = rel_books.author_id`, sql) +} + +func TestCompileExistenceSubquery_Many2Many_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relUser{}, "Roles", &[]relRole{}) + assert.Equal(t, `SELECT 1 FROM "rel_roles" INNER JOIN rel_user_roles ON rel_user_roles.rel_role_id = rel_roles.id WHERE rel_user_roles.rel_user_id = rel_users.id`, sql) +} + +func TestCompileExistenceSubquery_MorphOne_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relUser{}, "Logo", &relLogo{}) + assert.Equal(t, `SELECT 1 FROM "rel_logos" WHERE rel_logos.logoable_id = rel_users.id AND rel_logos.logoable_type = ?`, sql) +} + +func TestCompileExistenceSubquery_MorphMany_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relUser{}, "Houses", &[]relHouse{}) + assert.Equal(t, `SELECT 1 FROM "rel_houses" WHERE rel_houses.houseable_id = rel_users.id AND rel_houses.houseable_type = ?`, sql) +} + +func TestCompileExistenceSubquery_MorphToMany_SQL(t *testing.T) { + sql := runExistenceSQL(t, &morphPost{}, "Tags", &[]morphTag{}) + assert.Equal(t, `SELECT 1 FROM "morph_tags" INNER JOIN taggables ON taggables.morph_tag_id = morph_tags.id WHERE taggables.taggable_id = morph_posts.id AND taggables.taggable_type = ?`, sql) +} + +func TestCompileExistenceSubquery_MorphedByMany_SQL(t *testing.T) { + sql := runExistenceSQL(t, &morphTag{}, "Posts", &[]morphPost{}) + assert.Equal(t, `SELECT 1 FROM "morph_posts" INNER JOIN taggables ON taggables.morph_post_id = morph_posts.id WHERE taggables.taggable_id = morph_tags.id AND taggables.taggable_type = ?`, sql) +} + +func TestCompileExistenceSubquery_HasManyThrough_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relCountry{}, "Posts", &[]relPost{}) + assert.Equal(t, `SELECT 1 FROM "rel_posts" INNER JOIN rel_users ON rel_posts.rel_user_id = rel_users.id WHERE rel_users.rel_country_id = rel_countries.id`, sql) +} + +func TestCompileExistenceSubquery_HasOneThrough_SQL(t *testing.T) { + sql := runExistenceSQL(t, &relCountry{}, "FirstPost", &relPost{}) + assert.Equal(t, `SELECT 1 FROM "rel_posts" INNER JOIN rel_users ON rel_posts.rel_user_id = rel_users.id WHERE rel_users.rel_country_id = rel_countries.id`, sql) +} + +// --------------------------------------------------------------------------- +// compileAggregateSubquery SQL — used by WithCount / WithMax / WithMin / WithSum / WithAvg / WithExists +// --------------------------------------------------------------------------- + +func runAggregateSQL(t *testing.T, sub selectSub) string { + t.Helper() + q := newRelQueryWith(t, &relUser{}) + desc, err := resolveRelation(q.instance, &relUser{}, "Books") + assert.NoError(t, err) + inner := q.compileAggregateSubquery(desc, sub) + stmt := inner.Session(&gormio.Session{DryRun: true}).Find(&relBook{}) + return stmt.Statement.SQL.String() +} + +func TestCompileAggregateSubquery_Count_SQL(t *testing.T) { + sql := runAggregateSQL(t, selectSub{relation: "Books", column: "*", function: "count"}) + assert.Equal(t, `SELECT COUNT(*) FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} + +func TestCompileAggregateSubquery_Sum_SQL(t *testing.T) { + sql := runAggregateSQL(t, selectSub{relation: "Books", column: "id", function: "sum"}) + assert.Equal(t, `SELECT SUM(rel_books.id) FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} + +func TestCompileAggregateSubquery_Max_SQL(t *testing.T) { + sql := runAggregateSQL(t, selectSub{relation: "Books", column: "id", function: "max"}) + assert.Equal(t, `SELECT MAX(rel_books.id) FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} + +func TestCompileAggregateSubquery_Min_SQL(t *testing.T) { + sql := runAggregateSQL(t, selectSub{relation: "Books", column: "id", function: "min"}) + assert.Equal(t, `SELECT MIN(rel_books.id) FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} + +func TestCompileAggregateSubquery_Avg_SQL(t *testing.T) { + sql := runAggregateSQL(t, selectSub{relation: "Books", column: "id", function: "avg"}) + assert.Equal(t, `SELECT AVG(rel_books.id) FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} + +func TestCompileAggregateSubquery_Exists_SQL(t *testing.T) { + sql := runAggregateSQL(t, selectSub{relation: "Books", column: "*", function: "exists"}) + assert.Equal(t, `SELECT 1 FROM "rel_books" WHERE rel_books.user_id = rel_users.id`, sql) +} diff --git a/database/gorm/relation_writer_test.go b/database/gorm/relation_writer_test.go new file mode 100644 index 000000000..283d6eb35 --- /dev/null +++ b/database/gorm/relation_writer_test.go @@ -0,0 +1,238 @@ +package gorm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/errors" +) + +// TestRelation_ReturnsWriter verifies Query.Relation returns a writer bound to the parent/name. +func TestRelation_ReturnsWriter(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + parent := &relUser{ID: 1} + + w := q.Relation(parent, "Books") + assert.NotNil(t, w) + + rw, ok := w.(*relationWriter) + assert.True(t, ok) + assert.Same(t, q, rw.q) + assert.Same(t, parent, rw.parent) + assert.Equal(t, "Books", rw.name) +} + +// TestRelation_ImplementsContract verifies relationWriter satisfies the RelationWriter contract. +func TestRelation_ImplementsContract(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + _ = contractsorm.RelationWriter(q.Relation(&relUser{}, "Books")) +} + +// TestRelationWriter_Save_Delegates verifies Save forwards to SaveRelation. +func TestRelationWriter_Save_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.Save(&relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SaveMany_Delegates verifies SaveMany forwards to SaveManyRelation. +func TestRelationWriter_SaveMany_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.SaveMany([]*relBook{{}}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SaveWithPivot_Delegates verifies SaveWithPivot forwards to SaveRelationWithPivot. +func TestRelationWriter_SaveWithPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + err := w.SaveWithPivot(&relRole{}, nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SaveManyWithPivot_Delegates verifies SaveManyWithPivot forwards correctly. +func TestRelationWriter_SaveManyWithPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + err := w.SaveManyWithPivot([]*relRole{{}}, nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Create_Delegates verifies Create forwards to CreateRelation. +func TestRelationWriter_Create_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.Create(&relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_CreateMany_Delegates verifies CreateMany forwards to CreateManyRelation. +func TestRelationWriter_CreateMany_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.CreateMany([]*relBook{{}}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_FindOrNew_Delegates verifies FindOrNew forwards to FindOrNewRelation. +func TestRelationWriter_FindOrNew_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.FindOrNew(1, &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_FirstOrNew_Delegates verifies FirstOrNew forwards to FirstOrNewRelation. +func TestRelationWriter_FirstOrNew_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.FirstOrNew(nil, nil, &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_FirstOrCreate_Delegates verifies FirstOrCreate forwards to FirstOrCreateRelation. +func TestRelationWriter_FirstOrCreate_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.FirstOrCreate(nil, nil, &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_UpdateOrCreate_Delegates verifies UpdateOrCreate forwards correctly. +func TestRelationWriter_UpdateOrCreate_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Books") // pass by value to trigger error path + + err := w.UpdateOrCreate(nil, nil, &relBook{}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Associate_Delegates verifies Associate forwards to AssociateRelation. +func TestRelationWriter_Associate_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + w := q.Relation(relBook{}, "Author") // pass by value to trigger error path + + err := w.Associate(&relUser{ID: 1}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Dissociate_Delegates verifies Dissociate forwards to DissociateRelation. +func TestRelationWriter_Dissociate_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + w := q.Relation(relBook{}, "Author") // pass by value to trigger error path + + err := w.Dissociate() + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Attach_Delegates verifies Attach forwards to AttachRelation. +func TestRelationWriter_Attach_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + err := w.Attach([]any{1, 2}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_AttachWithPivot_Delegates verifies AttachWithPivot forwards correctly. +func TestRelationWriter_AttachWithPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + err := w.AttachWithPivot(map[any]map[string]any{1: nil}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Detach_Delegates verifies Detach forwards to DetachRelation. +func TestRelationWriter_Detach_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.Detach(1, 2) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Sync_Delegates verifies Sync forwards to SyncRelation. +func TestRelationWriter_Sync_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.Sync([]any{1, 2}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SyncWithPivot_Delegates verifies SyncWithPivot forwards correctly. +func TestRelationWriter_SyncWithPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.SyncWithPivot(map[any]map[string]any{1: nil}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SyncWithPivotValues_Delegates verifies SyncWithPivotValues forwards correctly. +func TestRelationWriter_SyncWithPivotValues_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.SyncWithPivotValues([]any{1, 2}, nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SyncWithoutDetaching_Delegates verifies SyncWithoutDetaching forwards. +func TestRelationWriter_SyncWithoutDetaching_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.SyncWithoutDetaching([]any{1, 2}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_SyncWithoutDetachingWithPivot_Delegates verifies the method forwards. +func TestRelationWriter_SyncWithoutDetachingWithPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.SyncWithoutDetachingWithPivot(map[any]map[string]any{1: nil}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_Toggle_Delegates verifies Toggle forwards to ToggleRelation. +func TestRelationWriter_Toggle_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.Toggle([]any{1, 2}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_ToggleWithPivot_Delegates verifies ToggleWithPivot forwards correctly. +func TestRelationWriter_ToggleWithPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.ToggleWithPivot(map[any]map[string]any{1: nil}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// TestRelationWriter_UpdateExistingPivot_Delegates verifies UpdateExistingPivot forwards. +func TestRelationWriter_UpdateExistingPivot_Delegates(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + w := q.Relation(relUser{ID: 1}, "Roles") // pass by value to trigger error path + + _, err := w.UpdateExistingPivot(1, map[string]any{"k": "v"}) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} diff --git a/database/gorm/relation_writes_test.go b/database/gorm/relation_writes_test.go index 149552d30..96973d0cd 100644 --- a/database/gorm/relation_writes_test.go +++ b/database/gorm/relation_writes_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + dbcontract "github.com/goravel/framework/contracts/database/db" contractsorm "github.com/goravel/framework/contracts/database/orm" "github.com/goravel/framework/errors" ) @@ -620,3 +621,124 @@ func TestResolvePivotTimestamps_StructHasOneCol_FallbackFillsOther(t *testing.T) assert.Equal(t, "created_at", created, "struct-provided column wins") assert.Equal(t, "updated_at", updated, "fallback fills the column the struct didn't provide") } + +// syncResultChanged is a pure function used by syncCore/syncCoreWithPivot to decide whether to +// call touchIfTouching. Test all branches. + +func TestSyncResultChanged_AllEmpty(t *testing.T) { + out := &dbcontract.SyncResult{} + assert.False(t, syncResultChanged(out)) +} + +func TestSyncResultChanged_HasAttached(t *testing.T) { + out := &dbcontract.SyncResult{Attached: []any{1}} + assert.True(t, syncResultChanged(out)) +} + +func TestSyncResultChanged_HasDetached(t *testing.T) { + out := &dbcontract.SyncResult{Detached: []any{2}} + assert.True(t, syncResultChanged(out)) +} + +func TestSyncResultChanged_HasUpdated(t *testing.T) { + out := &dbcontract.SyncResult{Updated: []any{3}} + assert.True(t, syncResultChanged(out)) +} + +func TestSyncResultChanged_AllPopulated(t *testing.T) { + out := &dbcontract.SyncResult{Attached: []any{1}, Detached: []any{2}, Updated: []any{3}} + assert.True(t, syncResultChanged(out)) +} + +// applyAttrMap overlays an attrs map onto a target struct via GORM's schema. Tests cover the +// happy path, the early-return for empty attrs, and the silent skip for unknown columns. + +func TestApplyAttrMap_EmptyAttrs_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + dest := &relUser{ID: 5} + err := q.applyAttrMap(dest, nil) + assert.NoError(t, err) + assert.Equal(t, uint(5), dest.ID, "dest unchanged") +} + +func TestApplyAttrMap_EmptyMap_NoOp(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + dest := &relUser{ID: 5} + err := q.applyAttrMap(dest, map[string]any{}) + assert.NoError(t, err) + assert.Equal(t, uint(5), dest.ID, "dest unchanged") +} + +func TestApplyAttrMap_UnknownColumn_Skipped(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + dest := &relUser{ID: 5} + // "no_such_column" doesn't map to any field — applyAttrMap silently skips. + err := q.applyAttrMap(dest, map[string]any{"no_such_column": "x"}) + assert.NoError(t, err) + assert.Equal(t, uint(5), dest.ID, "dest unchanged") +} + +// CreateRelation additional coverage for Many2Many path + +// FindOrNewRelation additional coverage + +// FirstOrCreateRelation additional coverage for Many2Many path + +func TestKindName_AllKinds(t *testing.T) { + assert.Equal(t, "hasOne", kindName(relKindHasOne)) + assert.Equal(t, "hasMany", kindName(relKindHasMany)) + assert.Equal(t, "belongsTo", kindName(relKindBelongsTo)) + assert.Equal(t, "many2Many", kindName(relKindMany2Many)) + assert.Equal(t, "morphOne", kindName(relKindMorphOne)) + assert.Equal(t, "morphMany", kindName(relKindMorphMany)) + assert.Equal(t, "morphTo", kindName(relKindMorphTo)) + assert.Equal(t, "morphToMany", kindName(relKindMorphToMany)) + assert.Equal(t, "hasOneThrough", kindName(relKindHasOneThrough)) + assert.Equal(t, "hasManyThrough", kindName(relKindHasManyThrough)) + assert.Equal(t, "kind=999", kindName(999)) +} + +// SaveRelationWithPivot coverage + +func TestSaveRelationWithPivot_UnsupportedKind_BelongsTo(t *testing.T) { + q := newRelQueryWith(t, &relBook{}) + err := q.SaveRelationWithPivot(&relBook{ID: 1}, "Author", &relUser{}, nil) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSaveRelationWithPivot_NotPointerParent(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveRelationWithPivot(relUser{}, "Roles", &relRole{}, nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +func TestSaveRelationWithPivot_NilChild(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveRelationWithPivot(&relUser{ID: 1}, "Roles", nil, nil) + assert.True(t, errors.Is(err, errors.OrmRelationParentNotPointer)) +} + +// SaveManyRelationWithPivot coverage + +func TestSaveManyRelationWithPivot_NonSlice(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.SaveManyRelationWithPivot(&relUser{ID: 1}, "Roles", "not a slice", nil) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +func TestSaveManyRelationWithPivot_InvalidElement(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + // Slice of non-struct/non-pointer elements + err := q.SaveManyRelationWithPivot(&relUser{ID: 1}, "Roles", []int{1, 2, 3}, nil) + assert.True(t, errors.Is(err, errors.OrmRelationKindNotSupported)) +} + +// CreateManyRelation coverage + +func TestCreateManyRelation_NonSlice(t *testing.T) { + q := newRelQueryWith(t, &relUser{}) + err := q.CreateManyRelation(&relUser{ID: 1}, "Books", "not a slice") + assert.True(t, errors.Is(err, errors.OrmRelationUnsupported)) +} + +// Additional error path coverage diff --git a/database/gorm/row_test.go b/database/gorm/row_test.go new file mode 100644 index 000000000..6f85941f3 --- /dev/null +++ b/database/gorm/row_test.go @@ -0,0 +1,20 @@ +package gorm + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestRow_Err returns the stored error from the Row. +func TestRow_Err_Nil(t *testing.T) { + r := &Row{err: nil} + assert.NoError(t, r.Err()) +} + +func TestRow_Err_NonNil(t *testing.T) { + expected := errors.New("scan error") + r := &Row{err: expected} + assert.Same(t, expected, r.Err()) +} diff --git a/database/orm/model.go b/database/orm/model.go index b62887ff7..c13c04348 100644 --- a/database/orm/model.go +++ b/database/orm/model.go @@ -2,13 +2,10 @@ package orm import ( "gorm.io/gorm" - "gorm.io/gorm/clause" "github.com/goravel/framework/support/carbon" ) -const Associations = clause.Associations - // Model is the base model for all models in the application. type Model struct { Timestamps diff --git a/docs/orm-many-to-many.md b/docs/orm-many-to-many.md deleted file mode 100644 index c135d5e55..000000000 --- a/docs/orm-many-to-many.md +++ /dev/null @@ -1,290 +0,0 @@ -# Many-to-Many Relations (BelongsToMany family) - -The BelongsToMany family covers three relation kinds — all share a pivot table: - -| Kind | Use case | -|---|---| -| `Many2Many` | Plain m:n between two models (e.g. User ↔ Role through `user_roles`) | -| `MorphToMany` | Polymorphic m:n where the parent side is morphable (e.g. Post → Tag, Video → Tag through `taggables`) | -| `MorphedByMany` | Inverse of `MorphToMany` (e.g. Tag → Post, Tag → Video) | - -All three accept the same set of pivot configuration fields described below. - ---- - -## Basic declaration - -```go -type User struct { - ID uint - Roles []*Role `gorm:"-"` -} - -func (User) Relations() map[string]orm.Relation { - return map[string]orm.Relation{ - "Roles": orm.Many2Many{Related: &Role{}, Table: "user_roles"}, - } -} -``` - -`Table` is optional — the framework defaults to the alphabetically-sorted singular pair (e.g. `role_user`). - -## Pivot model (custom Pivot struct) - -To surface pivot-table columns on eager-loaded results, declare a Go struct for the pivot row and add a field of that type to the related model: - -```go -type RoleUserPivot struct { - UserID uint `gorm:"column:user_id"` - RoleID uint `gorm:"column:role_id"` - Active bool `gorm:"column:active"` - CreatedAt time.Time - UpdatedAt time.Time -} - -type Role struct { - ID uint - Name string - Pivot RoleUserPivot `gorm:"-"` // ← framework hydrates this on eager load -} -``` - -No additional config required — the framework reflects the `Pivot` field type, parses its GORM schema, and uses the resulting `db_name` columns as the pivot SELECT list. - -### Custom pivot field name (`PivotField`) - -If a single related model participates in multiple m:n relations with different pivot schemas, use a different field name per relation: - -```go -type Role struct { - ID uint - Name string - UserPivot RoleUserPivot `gorm:"-"` - GroupPivot RoleGroupPivot `gorm:"-"` -} - -// On User -"Roles": orm.Many2Many{Related: &Role{}, PivotField: "UserPivot"} - -// On Group -"Roles": orm.Many2Many{Related: &Role{}, PivotField: "GroupPivot"} -``` - -`PivotField` defaults to `"Pivot"` when omitted. - -## Auto-stamping pivot timestamps - -The framework auto-fills `created_at` (on INSERT) and `updated_at` (on INSERT/UPDATE) using the following priority order: - -1. **Explicit GORM tag** on a Pivot struct field — works for any field name: - ```go - type RoleUserPivot struct { - Stamped time.Time `gorm:"autoCreateTime"` - Edited time.Time `gorm:"autoUpdateTime"` - } - ``` - -2. **GORM convention** — fields named `CreatedAt` / `UpdatedAt` of type `time.Time`: - ```go - type RoleUserPivot struct { - CreatedAt time.Time - UpdatedAt time.Time - } - ``` - -3. **Relation-level fallback** — set `PivotTimestamps: true` when no Pivot struct is declared (or it has no timestamp fields) but the underlying table still has `created_at` / `updated_at` you want auto-filled: - ```go - "Roles": orm.Many2Many{Related: &Role{}, PivotTimestamps: true} - ``` - -### Customising column names - -Use the GORM `column` tag on the Pivot struct field — there is no relation-level override: - -```go -type RoleUserPivot struct { - Stamped time.Time `gorm:"autoCreateTime;column:made_on"` - Edited time.Time `gorm:"autoUpdateTime;column:edited_at"` -} -``` - -### Partial timestamps - -Declaring only `CreatedAt` (or only `UpdatedAt`) is fine — the framework only auto-stamps what you declare: - -```go -type RoleUserPivot struct { - UserID uint - RoleID uint - CreatedAt time.Time // only created_at gets stamped; updated_at is left untouched -} -``` - -## Filtering pivot operations (`OnPivotQuery`) - -Sync / Attach / Detach / Toggle / UpdateExistingPivot can be scoped to a subset of pivot rows via `OnPivotQuery` — equivalent to fedaco's `wherePivot` / `wherePivotIn` / `wherePivotNull`. - -```go -"ActiveRoles": orm.Many2Many{ - Related: &Role{}, - Table: "user_roles", - OnPivotQuery: func(q orm.PivotQuery) orm.PivotQuery { - return q.Where("active", 1).WhereNull("deleted_at") - }, -} -``` - -The callback applies to **SELECT / UPDATE / DELETE** on the pivot table: - -- `Sync` reads "current" rows through the filter — so it only detaches rows matching the filter. -- `Detach` only deletes rows matching the filter. -- `UpdateExistingPivot` only updates rows matching the filter. -- `Attach`'s duplicate-detection SELECT also goes through the filter (so a row not matching the filter is treated as "not attached"). - -INSERT rows from `Attach` / `AttachWithPivot` are **not** auto-injected with these conditions — pass equality columns through the `attrs` map: - -```go -orm.Attach(&user, "ActiveRoles", []any{1, 2, 3}) -// Will INSERT (user_id, role_id) only — without active=1 unless you add it explicitly: -orm.AttachWithPivot(&user, "ActiveRoles", map[any]map[string]any{ - 1: {"active": 1}, - 2: {"active": 1}, -}) -``` - -### `PivotQuery` interface - -```go -type PivotQuery interface { - Where(column string, args ...any) PivotQuery - WhereIn(column string, values []any) PivotQuery - WhereNotIn(column string, values []any) PivotQuery - WhereNull(column string) PivotQuery - WhereNotNull(column string) PivotQuery -} -``` - -## Bumping parent timestamps (`Touches`) - -Pivot writes don't affect the parent row's `updated_at` by default. Set `Touches: true` to make Sync / Attach / Detach / Toggle / UpdateExistingPivot bump the parent's `updated_at` after the pivot operation succeeds (and only when pivot rows actually changed): - -```go -type Post struct { - ID uint - Title string - UpdatedAt time.Time - Tags []*Tag `gorm:"-"` -} - -func (Post) Relations() map[string]orm.Relation { - return map[string]orm.Relation{ - "Tags": orm.MorphToMany{Related: &Tag{}, Name: "taggable", Touches: true}, - } -} - -orm.Sync(&post, "Tags", []any{1, 2, 3}) -// → UPDATE posts SET updated_at = NOW() WHERE id = ? -``` - -Silently no-ops when: -- The relation isn't `Touches: true`. -- The parent's schema has no `updated_at` field. -- The pivot operation didn't actually attach/detach/update any rows (e.g. `Sync` with the same id list as already attached). - -## Pivot operation API - -All pivot operations live on `Orm` (and on the underlying `Query`): - -| Method | Effect | -|---|---| -| `Attach(parent, rel, ids)` | INSERT pivot rows; skips ids already attached | -| `AttachWithPivot(parent, rel, idsWithAttrs)` | Attach with per-id pivot column values | -| `Detach(parent, rel, ids...)` | DELETE pivot rows; with no ids, detaches all | -| `Sync(parent, rel, ids)` | INSERT missing + DELETE extra; idempotent | -| `SyncWithPivot(parent, rel, idsWithAttrs)` | Sync with per-id pivot values; UPDATEs existing rows when attrs non-empty | -| `SyncWithPivotValues(parent, rel, ids, sharedAttrs)` | Convenience: all ids share the same pivot values | -| `SyncWithoutDetaching(parent, rel, ids)` | INSERT missing only — no detach | -| `SyncWithoutDetachingWithPivot(parent, rel, idsWithAttrs)` | Same, with attrs | -| `Toggle(parent, rel, ids)` | Attach missing, detach existing | -| `ToggleWithPivot(parent, rel, idsWithAttrs)` | Toggle with attrs on the attach side | -| `UpdateExistingPivot(parent, rel, id, attrs)` | UPDATE one already-attached pivot row | - -### `SyncResult` - -`Sync*` and `Toggle*` return `*db.SyncResult`: - -```go -type SyncResult struct { - Attached []any - Detached []any - Updated []any // populated by SyncWithPivot* when an existing row's attrs changed -} -``` - -Element type matches the related model's primary-key Go type (e.g. `uint`, `int64`, `string`) — caller-supplied ids are normalised regardless of input type. - -## Complete example - -```go -import ( - "time" - - "github.com/goravel/framework/contracts/database/orm" -) - -type RoleUserPivot struct { - UserID uint `gorm:"column:user_id"` - RoleID uint `gorm:"column:role_id"` - Active bool `gorm:"column:active"` - Priority int `gorm:"column:priority"` - CreatedAt time.Time - UpdatedAt time.Time -} - -type Role struct { - ID uint - Name string - Pivot RoleUserPivot `gorm:"-"` -} - -type User struct { - ID uint - Name string - UpdatedAt time.Time - Roles []*Role `gorm:"-"` -} - -func (User) Relations() map[string]orm.Relation { - return map[string]orm.Relation{ - "Roles": orm.Many2Many{ - Related: &Role{}, - Table: "user_roles", - OnPivotQuery: func(q orm.PivotQuery) orm.PivotQuery { - return q.Where("active", 1) - }, - Touches: true, - }, - } -} - -// Usage: -user := User{ID: 7} -orm := facades.Orm() - -// Attach roles 1 and 2 with pivot data. -orm.AttachWithPivot(&user, "Roles", map[any]map[string]any{ - uint(1): {"active": 1, "priority": 10}, - uint(2): {"active": 1, "priority": 5}, -}) -// Pivot rows inserted with active=1, priority=N, created_at=NOW(), updated_at=NOW(). -// User.UpdatedAt also bumped (Touches: true). - -// Sync to roles {1, 3}: detaches 2, attaches 3, leaves 1 untouched. -result, _ := orm.Sync(&user, "Roles", []any{uint(1), uint(3)}) -// result.Attached = [3], result.Detached = [2], result.Updated = [] - -// Eager load roles with pivot data. -var u User -orm.Query().With("Roles").First(&u, uint(7)) -// u.Roles[0].Pivot.Active == true, u.Roles[0].Pivot.Priority == 10, etc. -``` diff --git a/errors/list.go b/errors/list.go index 2c50d4059..34ac71888 100644 --- a/errors/list.go +++ b/errors/list.go @@ -165,7 +165,6 @@ var ( OrmInitConnection = New("init %s connection error: %v") OrmMissingWhereClause = New("WHERE conditions required") OrmNoDialectorsFound = New("no dialectors found") - OrmQueryAssociationsConflict = New("cannot set orm.Associations and other fields at the same time") OrmQueryConditionRequired = New("query condition is required") OrmQueryEmptyId = New("id can't be empty") OrmQueryEmptyRelation = New("relation can't be empty") diff --git a/tests/models.go b/tests/models.go index f3c7b0b73..5d47126e9 100644 --- a/tests/models.go +++ b/tests/models.go @@ -58,11 +58,11 @@ func (r *User) Relations() map[string]contractsorm.Relation { }, "House": contractsorm.MorphOne{ Related: &House{}, - Name: "Houseable", + Name: "houseable", }, "Phones": contractsorm.MorphMany{ Related: &Phone{}, - Name: "Phoneable", + Name: "phoneable", }, "Roles": contractsorm.Many2Many{ Related: &Role{}, diff --git a/tests/queries_relationships_test.go b/tests/queries_relationships_test.go index 33e69d171..c9fd71906 100644 --- a/tests/queries_relationships_test.go +++ b/tests/queries_relationships_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/suite" contractsorm "github.com/goravel/framework/contracts/database/orm" - "github.com/goravel/framework/database/orm" ) // QueriesRelationshipsTestSuite covers the QueryWithRelations contract: Has, WhereHas, DoesntHave, @@ -44,12 +43,14 @@ func rq(q contractsorm.Query) contractsorm.Query { return q } func (s *QueriesRelationshipsTestSuite) TestHas_Existence() { for driver, query := range s.queries { s.Run(driver, func() { - alice := &User{Name: "rel_has_alice", Books: []*Book{{Name: "ab1"}, {Name: "ab2"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&alice)) + alice := &User{Name: "rel_has_alice"} + s.Nil(query.Query().Create(&alice)) + s.Nil(query.Query().Relation(alice, "Books").SaveMany([]*Book{{Name: "ab1"}, {Name: "ab2"}})) bob := &User{Name: "rel_has_bob"} s.Nil(query.Query().Create(&bob)) - carol := &User{Name: "rel_has_carol", Books: []*Book{{Name: "cb1"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&carol)) + carol := &User{Name: "rel_has_carol"} + s.Nil(query.Query().Create(&carol)) + s.Nil(query.Query().Relation(carol, "Books").Save(&Book{Name: "cb1"})) rq := rq(query.Query()) var users []User @@ -64,10 +65,12 @@ func (s *QueriesRelationshipsTestSuite) TestHas_Existence() { func (s *QueriesRelationshipsTestSuite) TestHas_CountComparison() { for driver, query := range s.queries { s.Run(driver, func() { - alice := &User{Name: "rel_hasc_alice", Books: []*Book{{Name: "h1"}, {Name: "h2"}, {Name: "h3"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&alice)) - bob := &User{Name: "rel_hasc_bob", Books: []*Book{{Name: "h4"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&bob)) + alice := &User{Name: "rel_hasc_alice"} + s.Nil(query.Query().Create(&alice)) + s.Nil(query.Query().Relation(alice, "Books").SaveMany([]*Book{{Name: "h1"}, {Name: "h2"}, {Name: "h3"}})) + bob := &User{Name: "rel_hasc_bob"} + s.Nil(query.Query().Create(&bob)) + s.Nil(query.Query().Relation(bob, "Books").Save(&Book{Name: "h4"})) rq := rq(query.Query()) var users []User @@ -82,8 +85,9 @@ func (s *QueriesRelationshipsTestSuite) TestHas_CountComparison() { func (s *QueriesRelationshipsTestSuite) TestDoesntHave() { for driver, query := range s.queries { s.Run(driver, func() { - withBooks := &User{Name: "rel_dh_with", Books: []*Book{{Name: "dhb"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&withBooks)) + withBooks := &User{Name: "rel_dh_with"} + s.Nil(query.Query().Create(&withBooks)) + s.Nil(query.Query().Relation(withBooks, "Books").Save(&Book{Name: "dhb"})) without := &User{Name: "rel_dh_without"} s.Nil(query.Query().Create(&without)) @@ -100,10 +104,12 @@ func (s *QueriesRelationshipsTestSuite) TestDoesntHave() { func (s *QueriesRelationshipsTestSuite) TestWhereHas_Callback() { for driver, query := range s.queries { s.Run(driver, func() { - match := &User{Name: "rel_wh_match", Books: []*Book{{Name: "wh_target"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&match)) - other := &User{Name: "rel_wh_other", Books: []*Book{{Name: "wh_other"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&other)) + match := &User{Name: "rel_wh_match"} + s.Nil(query.Query().Create(&match)) + s.Nil(query.Query().Relation(match, "Books").Save(&Book{Name: "wh_target"})) + other := &User{Name: "rel_wh_other"} + s.Nil(query.Query().Create(&other)) + s.Nil(query.Query().Relation(other, "Books").Save(&Book{Name: "wh_other"})) rq := rq(query.Query()) cb := func(q contractsorm.Query) contractsorm.Query { @@ -121,8 +127,11 @@ func (s *QueriesRelationshipsTestSuite) TestWhereHas_Callback() { func (s *QueriesRelationshipsTestSuite) TestHas_BelongsTo() { for driver, query := range s.queries { s.Run(driver, func() { - user := &User{Name: "rel_bt_user", Address: &Address{Name: "rel_bt_address"}} - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := &User{Name: "rel_bt_user"} + s.Nil(query.Query().Create(&user)) + addr := &Address{Name: "rel_bt_address"} + s.Nil(query.Query().Create(&addr)) + s.Nil(query.Query().Relation(addr, "User").Associate(user)) rq := rq(query.Query()) var addresses []Address @@ -136,8 +145,11 @@ func (s *QueriesRelationshipsTestSuite) TestHas_BelongsTo() { func (s *QueriesRelationshipsTestSuite) TestHas_ManyToMany() { for driver, query := range s.queries { s.Run(driver, func() { - withRole := &User{Name: "rel_mtm_with", Roles: []*Role{{Name: "rel_mtm_role"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&withRole)) + role := &Role{Name: "rel_mtm_role"} + s.Nil(query.Query().Create(&role)) + withRole := &User{Name: "rel_mtm_with"} + s.Nil(query.Query().Create(&withRole)) + s.Nil(query.Query().Relation(withRole, "Roles").Save(role)) noRole := &User{Name: "rel_mtm_no"} s.Nil(query.Query().Create(&noRole)) @@ -155,8 +167,9 @@ func (s *QueriesRelationshipsTestSuite) TestHas_ManyToMany() { func (s *QueriesRelationshipsTestSuite) TestHasMorph() { for driver, query := range s.queries { s.Run(driver, func() { - withHouse := &User{Name: "rel_hm_with", House: &House{Name: "rel_hm_house"}} - s.Nil(query.Query().Select(orm.Associations).Create(&withHouse)) + withHouse := &User{Name: "rel_hm_with"} + s.Nil(query.Query().Create(&withHouse)) + s.Nil(query.Query().Relation(withHouse, "House").Save(&House{Name: "rel_hm_house"})) noHouse := &User{Name: "rel_hm_no"} s.Nil(query.Query().Create(&noHouse)) @@ -174,15 +187,15 @@ func (s *QueriesRelationshipsTestSuite) TestNestedHas() { for driver, query := range s.queries { s.Run(driver, func() { // User -> Books -> Author. Only carol has a book with an Author. - carol := &User{ - Name: "rel_nested_carol", - Books: []*Book{ - {Name: "carol_book", Author: &Author{Name: "carol_author"}}, - }, - } - s.Nil(query.Query().Select(orm.Associations).Create(&carol)) - dan := &User{Name: "rel_nested_dan", Books: []*Book{{Name: "dan_book"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&dan)) + carol := &User{Name: "rel_nested_carol"} + s.Nil(query.Query().Create(&carol)) + carolBook := &Book{Name: "carol_book"} + s.Nil(query.Query().Relation(carol, "Books").Save(carolBook)) + s.Nil(query.Query().Relation(carolBook, "Author").Save(&Author{Name: "carol_author"})) + + dan := &User{Name: "rel_nested_dan"} + s.Nil(query.Query().Create(&dan)) + s.Nil(query.Query().Relation(dan, "Books").Save(&Book{Name: "dan_book"})) rq := rq(query.Query()) var users []User @@ -235,10 +248,15 @@ func (s *QueriesRelationshipsTestSuite) TestHasManyThrough_WhereHas() { for driver, query := range s.queries { s.Run(driver, func() { // Seed: u1 has a book with author "match"; u2 has a book without an author. - u1 := &User{Name: "rel_th_u1", Books: []*Book{{Name: "th_book1", Author: &Author{Name: "th_author_match"}}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u1)) - u2 := &User{Name: "rel_th_u2", Books: []*Book{{Name: "th_book2"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u2)) + u1 := &User{Name: "rel_th_u1"} + s.Nil(query.Query().Create(&u1)) + b1 := &Book{Name: "th_book1"} + s.Nil(query.Query().Relation(u1, "Books").Save(b1)) + s.Nil(query.Query().Relation(b1, "Author").Save(&Author{Name: "th_author_match"})) + + u2 := &User{Name: "rel_th_u2"} + s.Nil(query.Query().Create(&u2)) + s.Nil(query.Query().Relation(u2, "Books").Save(&Book{Name: "th_book2"})) rq := rq(query.Query().Model(&userWithThrough{})) cb := func(q contractsorm.Query) contractsorm.Query { @@ -448,12 +466,16 @@ func (s *QueriesRelationshipsTestSuite) TestWithCount_Retrieve() { for driver, query := range s.queries { s.Run(driver, func() { // u1: 2 books, u2: 0 books, u3: 1 book with an author. - u1 := &User{Name: "agg_count_u1", Books: []*Book{{Name: "ab1"}, {Name: "ab2"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u1)) + u1 := &User{Name: "agg_count_u1"} + s.Nil(query.Query().Create(&u1)) + s.Nil(query.Query().Relation(u1, "Books").SaveMany([]*Book{{Name: "ab1"}, {Name: "ab2"}})) u2 := &User{Name: "agg_count_u2"} s.Nil(query.Query().Create(&u2)) - u3 := &User{Name: "agg_count_u3", Books: []*Book{{Name: "ab3", Author: &Author{Name: "Author1"}}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u3)) + u3 := &User{Name: "agg_count_u3"} + s.Nil(query.Query().Create(&u3)) + b3 := &Book{Name: "ab3"} + s.Nil(query.Query().Relation(u3, "Books").Save(b3)) + s.Nil(query.Query().Relation(b3, "Author").Save(&Author{Name: "Author1"})) var rows []userAggregates s.Nil(query.Query().Model(&User{}).Where("name like ?", "agg_count_%").OrderBy("name"). @@ -475,8 +497,9 @@ func (s *QueriesRelationshipsTestSuite) TestWithCount_CustomAliasAndCallback() { for driver, query := range s.queries { s.Run(driver, func() { // Three books — only two start with "pop_". Custom alias + callback narrows the count. - u := &User{Name: "agg_alias_u", Books: []*Book{{Name: "pop_x"}, {Name: "pop_y"}, {Name: "boring"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "agg_alias_u"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").SaveMany([]*Book{{Name: "pop_x"}, {Name: "pop_y"}, {Name: "boring"}})) cb := func(q contractsorm.Query) contractsorm.Query { return q.Where("name like ?", "pop_%") @@ -496,8 +519,9 @@ func (s *QueriesRelationshipsTestSuite) TestWithMaxMinSumAvg_Retrieve() { for driver, query := range s.queries { s.Run(driver, func() { // Three books with consecutive auto-increment IDs (1, 2, 3) on a fresh table. - u := &User{Name: "agg_num_u", Books: []*Book{{Name: "n1"}, {Name: "n2"}, {Name: "n3"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "agg_num_u"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").SaveMany([]*Book{{Name: "n1"}, {Name: "n2"}, {Name: "n3"}})) var rows []userAggregates s.Nil(query.Query().Model(&User{}).Where("name = ?", "agg_num_u"). @@ -524,8 +548,9 @@ func (s *QueriesRelationshipsTestSuite) TestWithMaxMinSumAvg_Retrieve() { func (s *QueriesRelationshipsTestSuite) TestWithExists_Retrieve() { for driver, query := range s.queries { s.Run(driver, func() { - withBooks := &User{Name: "agg_exist_yes", Books: []*Book{{Name: "ex1"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&withBooks)) + withBooks := &User{Name: "agg_exist_yes"} + s.Nil(query.Query().Create(&withBooks)) + s.Nil(query.Query().Relation(withBooks, "Books").Save(&Book{Name: "ex1"})) withoutBooks := &User{Name: "agg_exist_no"} s.Nil(query.Query().Create(&withoutBooks)) diff --git a/tests/query_test.go b/tests/query_test.go index 100f424c8..fb2c83fae 100644 --- a/tests/query_test.go +++ b/tests/query_test.go @@ -12,7 +12,6 @@ import ( contractsorm "github.com/goravel/framework/contracts/database/orm" databasedb "github.com/goravel/framework/database/db" - "github.com/goravel/framework/database/orm" "github.com/goravel/framework/errors" "github.com/goravel/framework/support/carbon" "github.com/goravel/framework/support/convert" @@ -68,13 +67,13 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_find_name", - Address: &Address{ - Name: "association_find_address", - }, age: 1, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) + addr := &Address{Name: "association_find_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr s.True(user.ID > 0) s.True(user.Address.ID > 0) @@ -93,18 +92,21 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_has_one_append_name", - Address: &Address{ - Name: "association_has_one_append_address", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "association_has_one_append_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr s.True(user.Address.ID > 0) var user1 User s.Nil(query.Query().Find(&user1, user.ID), driver) s.True(user1.ID > 0, driver) + // Replace existing hasOne: delete old then save new. + _, err := query.Query().Related(&user1, "Address").Delete() + s.Nil(err, driver) s.Nil(query.Query().Relation(&user1, "Address").Save(&Address{Name: "association_has_one_append_address1"}), driver) s.Nil(query.Query().Load(&user1, "Address"), driver) @@ -117,14 +119,16 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_has_many_append_name", - Books: []*Book{ - {Name: "association_has_many_append_address1"}, - {Name: "association_has_many_append_address2"}, - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + books := []*Book{ + {Name: "association_has_many_append_address1"}, + {Name: "association_has_many_append_address2"}, + } + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -133,7 +137,9 @@ func (s *QueryTestSuite) TestAssociation() { s.True(user1.ID > 0) s.Nil(query.Query().Relation(&user1, "Books").Save(&Book{Name: "association_has_many_append_address3"})) - s.Nil(query.Query().Load(&user1, "Books")) + s.Nil(query.Query().Load(&user1, "Books", func(q contractsorm.Query) contractsorm.Query { + return q.OrderBy("id") + })) s.Equal(3, len(user1.Books)) s.Equal("association_has_many_append_address3", user1.Books[2].Name) }, @@ -143,18 +149,21 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_has_one_append_name", - Address: &Address{ - Name: "association_has_one_append_address", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "association_has_one_append_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr s.True(user.Address.ID > 0) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) + // New design: explicit two-step for HasOne replace. + _, err := query.Query().Related(&user1, "Address").Delete() + s.Nil(err) s.Nil(query.Query().Relation(&user1, "Address").Save(&Address{Name: "association_has_one_append_address1"})) s.Nil(query.Query().Load(&user1, "Address")) @@ -167,14 +176,16 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_has_many_replace_name", - Books: []*Book{ - {Name: "association_has_many_replace_address1"}, - {Name: "association_has_many_replace_address2"}, - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + books := []*Book{ + {Name: "association_has_many_replace_address1"}, + {Name: "association_has_many_replace_address2"}, + } + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -182,7 +193,8 @@ func (s *QueryTestSuite) TestAssociation() { s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) // New design: explicit two-step for HasMany replace (old .Replace would fail on NOT NULL FK) - s.Nil(query.Query().Related(&user1, "Books").Delete()) + _, err := query.Query().Related(&user1, "Books").Delete() + s.Nil(err) s.Nil(query.Query().Relation(&user1, "Books").Save(&Book{Name: "association_has_many_replace_address3"})) s.Nil(query.Query().Load(&user1, "Books")) @@ -195,20 +207,21 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_delete_name", - Address: &Address{ - Name: "association_delete_address", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "association_delete_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr s.True(user.Address.ID > 0) // No ID when Delete — new design: true delete (not nullify FK) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Related(&user1, "Address").Where("name", "association_delete_address").Delete()) + _, err := query.Query().Related(&user1, "Address").Where("name", "association_delete_address").Delete() + s.Nil(err) s.Nil(query.Query().Load(&user1, "Address")) s.Nil(user1.Address) @@ -217,9 +230,8 @@ func (s *QueryTestSuite) TestAssociation() { var user2 User s.Nil(query.Query().Find(&user2, user.ID)) s.True(user2.ID > 0) - var userAddress Address - userAddress.ID = user1.Address.ID - s.Nil(query.Query().Related(&user2, "Address").Where("id", userAddress.ID).Delete()) + _, err = query.Query().Related(&user2, "Address").Where("id", addr.ID).Delete() + s.Nil(err) s.Nil(query.Query().Load(&user2, "Address")) s.Nil(user2.Address) @@ -230,20 +242,21 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_clear_name", - Address: &Address{ - Name: "association_clear_address", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "association_clear_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr s.True(user.Address.ID > 0) // No ID when Delete var user1 User s.Nil(query.Query().Find(&user1, user.ID)) s.True(user1.ID > 0) - s.Nil(query.Query().Related(&user1, "Address").Delete()) + _, err := query.Query().Related(&user1, "Address").Delete() + s.Nil(err) s.Nil(query.Query().Load(&user1, "Address")) s.Nil(user1.Address) @@ -254,14 +267,16 @@ func (s *QueryTestSuite) TestAssociation() { setup: func() { user := User{ Name: "association_count_name", - Books: []*Book{ - {Name: "association_count_address1"}, - {Name: "association_count_address2"}, - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + books := []*Book{ + {Name: "association_count_address1"}, + {Name: "association_count_address2"}, + } + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -288,13 +303,13 @@ func (s *QueryTestSuite) TestBelongsTo() { s.Run(driver, func() { user := &User{ Name: "belongs_to_name", - Address: &Address{ - Name: "belongs_to_address", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "belongs_to_address"} + s.Nil(query.Query().Relation(user, "Address").Save(addr)) + user.Address = addr s.True(user.Address.ID > 0) var userAddress Address @@ -495,12 +510,15 @@ func (s *QueryTestSuite) TestCreate() { { name: "success when create with select Associations", setup: func() { - user := User{Name: "create_user", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := User{Name: "create_user"} + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "create_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr + books := []*Book{{Name: "create_book0"}, {Name: "create_book1"}} + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.Address.ID > 0) s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -509,73 +527,30 @@ func (s *QueryTestSuite) TestCreate() { { name: "success when create with select fields", setup: func() { - user := User{Name: "create_user", Avatar: "create_avatar", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.Nil(query.Query().Select("Name", "Avatar", "Address").Create(&user)) + // Relations are no longer auto-created by Select; set them to nil + // to verify the field selection works on the parent only. + user := User{Name: "create_user", Avatar: "create_avatar"} + s.Nil(query.Query().Select("Name", "Avatar").Create(&user)) s.True(user.ID > 0) - s.True(user.Address.ID > 0) - s.True(user.Books[0].ID == 0) - s.True(user.Books[1].ID == 0) + s.Nil(user.Address) + s.Nil(user.Books) }, }, { name: "success when create with omit fields", setup: func() { - user := User{Name: "create_user", Avatar: "create_avatar", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.Nil(query.Query().Omit("Address").Create(&user)) + // Relations are no longer auto-created/omitted by Omit; set them to nil + // to verify the field selection works on the parent only. + user := User{Name: "create_user", Avatar: "create_avatar"} + s.Nil(query.Query().Omit("Avatar").Create(&user)) s.True(user.ID > 0) - s.True(user.Address.ID == 0) - s.True(user.Books[0].ID > 0) - s.True(user.Books[1].ID > 0) - }, - }, - { - name: "success create with omit Associations", - setup: func() { - user := User{Name: "create_user", Avatar: "create_avatar", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.Nil(query.Query().Omit(orm.Associations).Create(&user)) - s.True(user.ID > 0) - s.True(user.Address.ID == 0) - s.True(user.Books[0].ID == 0) - s.True(user.Books[1].ID == 0) - }, - }, - { - name: "error when set select and omit at the same time", - setup: func() { - user := User{Name: "create_user", Avatar: "create_avatar", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.EqualError(query.Query().Omit(orm.Associations).Select("Name").Create(&user), errors.OrmQuerySelectAndOmitsConflict.Error()) - }, - }, - { - name: "error when select that set fields and Associations at the same time", - setup: func() { - user := User{Name: "create_user", Avatar: "create_avatar", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.EqualError(query.Query().Select("Name", orm.Associations).Create(&user), errors.OrmQueryAssociationsConflict.Error()) - }, - }, - { - name: "error when omit that set fields and Associations at the same time", - setup: func() { - user := User{Name: "create_user", Avatar: "create_avatar", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "create_address" - user.Books[0].Name = "create_book0" - user.Books[1].Name = "create_book1" - s.EqualError(query.Query().Omit("Name", orm.Associations).Create(&user), errors.OrmQueryAssociationsConflict.Error()) + s.Nil(user.Address) + s.Nil(user.Books) + + // Avatar should not be persisted because it was omitted + var user1 User + s.Nil(query.Query().Find(&user1, user.ID)) + s.Empty(user1.Avatar) }, }, } @@ -590,11 +565,15 @@ func (s *QueryTestSuite) TestCreate() { func (s *QueryTestSuite) TestCursor() { for driver, query := range s.queries { s.Run(driver, func() { - user := User{Name: "cursor_user", Avatar: "cursor_avatar", Address: &Address{Name: "cursor_address"}, Books: []*Book{ - {Name: "cursor_book"}, - }} - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := User{Name: "cursor_user", Avatar: "cursor_avatar"} + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "cursor_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr + books := []*Book{{Name: "cursor_book"}} + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books user1 := User{Name: "cursor_user", Avatar: "cursor_avatar1"} s.Nil(query.Query().Create(&user1)) @@ -1117,31 +1096,19 @@ func (s *QueryTestSuite) TestEvent_Creating() { { name: "trigger when create with omit", setup: func() { - user := User{Name: "event_creating_omit_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_creating_omit_create_address" - user.Books[0].Name = "event_creating_omit_create_book0" - user.Books[1].Name = "event_creating_omit_create_book1" - s.Nil(query.Query().Omit("Address").Create(&user)) + user := User{Name: "event_creating_omit_create_name", Avatar: "ignored"} + s.Nil(query.Query().Omit("Avatar").Create(&user)) s.True(user.ID > 0) s.Equal("event_creating_omit_create_avatar", user.Avatar) - s.True(user.Address.ID == 0) - s.True(user.Books[0].ID > 0) - s.True(user.Books[1].ID > 0) }, }, { name: "trigger when create with select", setup: func() { - user := User{Name: "event_creating_select_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_creating_select_create_address" - user.Books[0].Name = "event_creating_select_create_book0" - user.Books[1].Name = "event_creating_select_create_book1" - s.Nil(query.Query().Select("Name", "Avatar", "Address").Create(&user)) + user := User{Name: "event_creating_select_create_name"} + s.Nil(query.Query().Select("Name", "Avatar").Create(&user)) s.True(user.ID > 0) s.Equal("event_creating_select_create_avatar", user.Avatar) - s.True(user.Address.ID > 0) - s.True(user.Books[0].ID == 0) - s.True(user.Books[1].ID == 0) }, }, { @@ -1246,16 +1213,10 @@ func (s *QueryTestSuite) TestEvent_Created() { { name: "trigger when create with omit", setup: func() { - user := User{Name: "event_created_omit_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_created_omit_create_address" - user.Books[0].Name = "event_created_omit_create_book0" - user.Books[1].Name = "event_created_omit_create_book1" - s.Nil(query.Query().Omit("Address").Create(&user)) + user := User{Name: "event_created_omit_create_name", Avatar: "ignored"} + s.Nil(query.Query().Omit("Avatar").Create(&user)) s.True(user.ID > 0) s.Equal(fmt.Sprintf("event_created_omit_create_avatar_%d", user.ID), user.Avatar) - s.True(user.Address.ID == 0) - s.True(user.Books[0].ID > 0) - s.True(user.Books[1].ID > 0) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) @@ -1266,16 +1227,10 @@ func (s *QueryTestSuite) TestEvent_Created() { { name: "trigger when create with select", setup: func() { - user := User{Name: "event_created_select_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_created_select_create_address" - user.Books[0].Name = "event_created_select_create_book0" - user.Books[1].Name = "event_created_select_create_book1" - s.Nil(query.Query().Select("ID", "Name", "Avatar", "Address").Create(&user)) + user := User{Name: "event_created_select_create_name"} + s.Nil(query.Query().Select("ID", "Name", "Avatar").Create(&user)) s.True(user.ID > 0) s.Equal(fmt.Sprintf("event_created_select_create_avatar_%d", user.ID), user.Avatar) - s.True(user.Address.ID > 0) - s.True(user.Books[0].ID == 0) - s.True(user.Books[1].ID == 0) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) @@ -1374,31 +1329,19 @@ func (s *QueryTestSuite) TestEvent_Saving() { { name: "trigger when create with omit", setup: func() { - user := User{Name: "event_saving_omit_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_saving_omit_create_address" - user.Books[0].Name = "event_saving_omit_create_book0" - user.Books[1].Name = "event_saving_omit_create_book1" - s.Nil(query.Query().Omit("Address").Create(&user)) + user := User{Name: "event_saving_omit_create_name", Avatar: "ignored"} + s.Nil(query.Query().Omit("Avatar").Create(&user)) s.True(user.ID > 0) s.Equal("event_saving_omit_create_avatar", user.Avatar) - s.True(user.Address.ID == 0) - s.True(user.Books[0].ID > 0) - s.True(user.Books[1].ID > 0) }, }, { name: "trigger when create with select", setup: func() { - user := User{Name: "event_saving_select_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_saving_select_create_address" - user.Books[0].Name = "event_saving_select_create_book0" - user.Books[1].Name = "event_saving_select_create_book1" - s.Nil(query.Query().Select("Name", "Avatar", "Address").Create(&user)) + user := User{Name: "event_saving_select_create_name"} + s.Nil(query.Query().Select("Name", "Avatar").Create(&user)) s.True(user.ID > 0) s.Equal("event_saving_select_create_avatar", user.Avatar) - s.True(user.Address.ID > 0) - s.True(user.Books[0].ID == 0) - s.True(user.Books[1].ID == 0) }, }, { @@ -1524,16 +1467,10 @@ func (s *QueryTestSuite) TestEvent_Saved() { { name: "trigger when create with omit", setup: func() { - user := User{Name: "event_saved_omit_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_saved_omit_create_address" - user.Books[0].Name = "event_saved_omit_create_book0" - user.Books[1].Name = "event_saved_omit_create_book1" - s.Nil(query.Query().Omit("Address").Create(&user)) + user := User{Name: "event_saved_omit_create_name", Avatar: "ignored"} + s.Nil(query.Query().Omit("Avatar").Create(&user)) s.True(user.ID > 0) s.Equal("event_saved_omit_create_avatar", user.Avatar) - s.True(user.Address.ID == 0) - s.True(user.Books[0].ID > 0) - s.True(user.Books[1].ID > 0) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) @@ -1543,16 +1480,10 @@ func (s *QueryTestSuite) TestEvent_Saved() { { name: "trigger when create with select", setup: func() { - user := User{Name: "event_saved_select_create_name", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "event_saved_select_create_address" - user.Books[0].Name = "event_saved_select_create_book0" - user.Books[1].Name = "event_saved_select_create_book1" - s.Nil(query.Query().Select("Name", "Avatar", "Address").Create(&user)) + user := User{Name: "event_saved_select_create_name"} + s.Nil(query.Query().Select("Name", "Avatar").Create(&user)) s.True(user.ID > 0) s.Equal("event_saved_select_create_avatar", user.Avatar) - s.True(user.Address.ID > 0) - s.True(user.Books[0].ID == 0) - s.True(user.Books[1].ID == 0) var user1 User s.Nil(query.Query().Find(&user1, user.ID)) @@ -3122,13 +3053,13 @@ func (s *QueryTestSuite) TestHasOne() { s.Run(driver, func() { user := &User{ Name: "has_one_name", - Address: &Address{ - Name: "has_one_address", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + addr := &Address{Name: "has_one_address"} + s.Nil(query.Query().Relation(user, "Address").Save(addr)) + user.Address = addr s.True(user.Address.ID > 0) var user1 User @@ -3144,12 +3075,12 @@ func (s *QueryTestSuite) TestHasOneMorph() { s.Run(driver, func() { user := &User{ Name: "has_one_morph_name", - House: &House{ - Name: "has_one_morph_house", - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + userHouse := &House{Name: "has_one_morph_house"} + s.Nil(query.Query().Relation(user, "House").Save(userHouse)) + user.House = userHouse s.True(user.House.ID > 0) var user1 User @@ -3171,14 +3102,16 @@ func (s *QueryTestSuite) TestHasMany() { s.Run(driver, func() { user := &User{ Name: "has_many_name", - Books: []*Book{ - {Name: "has_many_book1"}, - {Name: "has_many_book2"}, - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + books := []*Book{ + {Name: "has_many_book1"}, + {Name: "has_many_book2"}, + } + s.Nil(query.Query().Relation(user, "Books").SaveMany(books)) + user.Books = books s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -3195,13 +3128,15 @@ func (s *QueryTestSuite) TestHasManyMorph() { s.Run(driver, func() { user := &User{ Name: "has_many_morph_name", - Phones: []*Phone{ - {Name: "has_many_morph_phone1"}, - {Name: "has_many_morph_phone2"}, - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + userPhones := []*Phone{ + {Name: "has_many_morph_phone1"}, + {Name: "has_many_morph_phone2"}, + } + s.Nil(query.Query().Relation(user, "Phones").SaveMany(userPhones)) + user.Phones = userPhones s.True(user.Phones[0].ID > 0) s.True(user.Phones[1].ID > 0) @@ -3225,14 +3160,16 @@ func (s *QueryTestSuite) TestManyToMany() { s.Run(driver, func() { user := &User{ Name: "many_to_many_name", - Roles: []*Role{ - {Name: "many_to_many_role1"}, - {Name: "many_to_many_role2"}, - }, } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + roles := []*Role{ + {Name: "many_to_many_role1"}, + {Name: "many_to_many_role2"}, + } + s.Nil(query.Query().Relation(user, "Roles").SaveMany(roles)) + user.Roles = roles s.True(user.Roles[0].ID > 0) s.True(user.Roles[1].ID > 0) @@ -3264,11 +3201,13 @@ func (s *QueryTestSuite) TestManyToManyUpdateWithAssociations() { roleA := &Role{Name: "m2m_update_role_a"} roleB := &Role{Name: "m2m_update_role_b"} user := &User{ - Name: "m2m_update_user", - Roles: []*Role{roleA, roleB}, + Name: "m2m_update_user", } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + roles := []*Role{roleA, roleB} + s.Nil(query.Query().Relation(user, "Roles").SaveMany(roles)) + user.Roles = roles s.True(roleA.ID > 0) s.True(roleB.ID > 0) @@ -3279,9 +3218,8 @@ func (s *QueryTestSuite) TestManyToManyUpdateWithAssociations() { // Step 3: update user with only role C (remove A and B from slice) roleC := &Role{Name: "m2m_update_role_c"} - userLoaded.Roles = []*Role{roleC} - _, err := query.Query().Model(&userLoaded).Select(orm.Associations).Update(&userLoaded) - s.Nil(err) + s.Nil(query.Query().Relation(&userLoaded, "Roles").Save(roleC)) + userLoaded.Roles = append(userLoaded.Roles, roleC) // Step 4: reload and assert that A and B are still linked (not deleted), C is added var userAfterUpdate User @@ -3303,15 +3241,17 @@ func (s *QueryTestSuite) TestManyToManyUpdateWithAssociations() { name: "Select(Associations).Update updates main fields and associations", setup: func() { user := &User{ - Name: "m2m_update_field_user", - Roles: []*Role{{Name: "m2m_update_field_role"}}, + Name: "m2m_update_field_user", } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + role := &Role{Name: "m2m_update_field_role"} + s.Nil(query.Query().Relation(user, "Roles").Save(role)) + user.Roles = []*Role{role} // Select("name", Associations) updates only the name column AND associations. user.Name = "m2m_update_field_user_updated" - _, err := query.Query().Model(user).Select("name", orm.Associations).Update(user) + _, err := query.Query().Model(user).Select("name").Update(user) s.Nil(err) var userLoaded User @@ -3324,11 +3264,12 @@ func (s *QueryTestSuite) TestManyToManyUpdateWithAssociations() { setup: func() { roleA := &Role{Name: "m2m_no_select_role_a"} user := &User{ - Name: "m2m_no_select_user", - Roles: []*Role{roleA}, + Name: "m2m_no_select_user", } - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + s.Nil(query.Query().Create(user)) s.True(user.ID > 0) + s.Nil(query.Query().Relation(user, "Roles").Save(roleA)) + user.Roles = []*Role{roleA} s.True(roleA.ID > 0) // Update user with a different role slice but without Select(Associations) @@ -3376,16 +3317,24 @@ func (s *QueryTestSuite) TestLimit() { func (s *QueryTestSuite) TestLoad() { for _, query := range s.queries { - user := User{Name: "load_user", Address: &Address{}, Books: []*Book{{}, {}}, Roles: []*Role{{}, {}}} - user.Address.Name = "load_address" - user.Books[0].Name = "load_book0" - user.Books[0].Author = &Author{Name: "load_book0_author"} - user.Books[1].Name = "load_book1" - user.Roles[0].Name = "load_role0" - user.Roles[1].Name = "load_role1" - - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := User{Name: "load_user"} + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "load_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr + book0 := &Book{Name: "load_book0"} + s.Nil(query.Query().Create(book0)) + author := &Author{Name: "load_book0_author"} + s.Nil(query.Query().Relation(book0, "Author").Save(author)) + book0.Author = author + book1 := &Book{Name: "load_book1"} + books := []*Book{book0, book1} + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books + roles := []*Role{{Name: "load_role0"}, {Name: "load_role1"}} + s.Nil(query.Query().Relation(&user, "Roles").SaveMany(roles)) + user.Roles = roles s.True(user.Address.ID > 0) s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -3502,12 +3451,15 @@ func (s *QueryTestSuite) TestLoad() { func (s *QueryTestSuite) TestLoadMissing() { for driver, query := range s.queries { s.Run(driver, func() { - user := User{Name: "load_missing_user", Address: &Address{}, Books: []*Book{{}, {}}} - user.Address.Name = "load_missing_address" - user.Books[0].Name = "load_missing_book0" - user.Books[1].Name = "load_missing_book1" - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := User{Name: "load_missing_user"} + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + addr := &Address{Name: "load_missing_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr + books := []*Book{{Name: "load_missing_book0"}, {Name: "load_missing_book1"}} + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.Address.ID > 0) s.True(user.Books[0].ID > 0) s.True(user.Books[1].ID > 0) @@ -3636,13 +3588,18 @@ func (s *QueryTestSuite) TestSave() { { name: "success when create with Select", setup: func() { + // Select no longer auto-creates relations; create the House + // explicitly via Relation().Save after the parent Save. user := User{ Name: "save_create_with_select_user", Avatar: "save_create_with_select_avatar", - House: &House{Name: "save_create_with_select_house"}, } - s.Nil(query.Query().Select("Name", "House").Save(&user)) + s.Nil(query.Query().Select("Name").Save(&user)) s.True(user.ID > 0) + + house := &House{Name: "save_create_with_select_house"} + s.Nil(query.Query().Relation(&user, "House").Save(house)) + user.House = house s.True(user.House.ID > 0) var user1 User @@ -3694,15 +3651,19 @@ func (s *QueryTestSuite) TestSave() { user := User{ Name: "save_update_with_select_user", Avatar: "save_update_with_select_avatar", - House: &House{Name: "save_update_with_select_house"}, } s.Nil(query.Query().Select("Name", "Avatar").Create(&user)) s.True(user.ID > 0) - s.True(user.House.ID == 0) + s.Nil(user.House) user.Name = "save_update_with_select_user1" - s.Nil(query.Query().Select("Name", "House").Save(&user)) + s.Nil(query.Query().Select("Name").Save(&user)) + // Relations are no longer auto-created via Select; create + // the House explicitly through the relation writer. + house := &House{Name: "save_update_with_select_house"} + s.Nil(query.Query().Relation(&user, "House").Save(house)) + user.House = house s.True(user.House.ID > 0) var user1 User @@ -3717,21 +3678,30 @@ func (s *QueryTestSuite) TestSave() { { name: "success when update with Omit", setup: func() { + // Create the parent and seed Address via the relation writer + // (Select no longer auto-creates relations). user := User{ - Name: "save_update_with_omit_user", - Avatar: "save_update_with_omit_avatar", - Address: &Address{Name: "save_update_with_omit_address"}, - House: &House{Name: "save_update_with_omit_house"}, + Name: "save_update_with_omit_user", + Avatar: "save_update_with_omit_avatar", } - s.Nil(query.Query().Select("Name", "Avatar", "Address").Create(&user)) + s.Nil(query.Query().Select("Name", "Avatar").Create(&user)) s.True(user.ID > 0) + + address := &Address{Name: "save_update_with_omit_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(address)) + user.Address = address s.True(user.Address.ID > 0) - s.True(user.House.ID == 0) + s.Nil(user.House) user.Name = "save_update_with_omit_user1" user.Address.Name = "save_update_with_omit_address1" - s.Nil(query.Query().Omit("Address").Save(&user)) + s.Nil(query.Query().Omit("Avatar").Save(&user)) + // Omit no longer auto-saves other relations; create the + // House explicitly via the relation writer. + house := &House{Name: "save_update_with_omit_house"} + s.Nil(query.Query().Relation(&user, "House").Save(house)) + user.House = house s.True(user.House.ID > 0) var user1 User @@ -4605,14 +4575,15 @@ func (s *QueryTestSuite) TestWithoutGlobalScopes() { func (s *QueryTestSuite) TestWith() { for driver, query := range s.queries { s.Run(driver, func() { - user := User{Name: "with_user", Address: &Address{ - Name: "with_address", - }, Books: []*Book{{ - Name: "with_book0", - }, { - Name: "with_book1", - }}} - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := User{Name: "with_user"} + s.Nil(query.Query().Create(&user)) + s.True(user.ID > 0) + addr := &Address{Name: "with_address"} + s.Nil(query.Query().Relation(&user, "Address").Save(addr)) + user.Address = addr + books := []*Book{{Name: "with_book0"}, {Name: "with_book1"}} + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.ID > 0) s.True(user.Address.ID > 0) s.True(user.Books[0].ID > 0) @@ -4670,15 +4641,22 @@ func (s *QueryTestSuite) TestWith() { func (s *QueryTestSuite) TestWithNesting() { for driver, query := range s.queries { s.Run(driver, func() { - user := User{Name: "with_nesting_user", Books: []*Book{{ - Name: "with_nesting_book0", - Author: &Author{Name: "with_nesting_author0"}, - }, { - Name: "with_nesting_book1", - Author: &Author{Name: "with_nesting_author1"}, - }}} - s.Nil(query.Query().Select(orm.Associations).Create(&user)) + user := User{Name: "with_nesting_user"} + s.Nil(query.Query().Create(&user)) s.True(user.ID > 0) + book0 := &Book{Name: "with_nesting_book0"} + s.Nil(query.Query().Create(book0)) + author0 := &Author{Name: "with_nesting_author0"} + s.Nil(query.Query().Relation(book0, "Author").Save(author0)) + book0.Author = author0 + book1 := &Book{Name: "with_nesting_book1"} + s.Nil(query.Query().Create(book1)) + author1 := &Author{Name: "with_nesting_author1"} + s.Nil(query.Query().Relation(book1, "Author").Save(author1)) + book1.Author = author1 + books := []*Book{book0, book1} + s.Nil(query.Query().Relation(&user, "Books").SaveMany(books)) + user.Books = books s.True(user.Books[0].ID > 0) s.True(user.Books[0].Author.ID > 0) s.True(user.Books[1].ID > 0) diff --git a/tests/with_test.go b/tests/with_test.go index ad340dc38..4e105d262 100644 --- a/tests/with_test.go +++ b/tests/with_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/suite" contractsorm "github.com/goravel/framework/contracts/database/orm" - "github.com/goravel/framework/database/orm" ) // WithTestSuite covers the With / Without / WithOnly methods, which run Goravel's own eager @@ -50,10 +49,12 @@ func (s *WithTestSuite) sqlite() *TestQuery { func (s *WithTestSuite) TestWith_HasMany() { for driver, query := range s.queries { s.Run(driver, func() { - alice := &User{Name: "wr_hm_alice", Books: []*Book{{Name: "wr_hm_a1"}, {Name: "wr_hm_a2"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&alice)) - bob := &User{Name: "wr_hm_bob", Books: []*Book{{Name: "wr_hm_b1"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&bob)) + alice := &User{Name: "wr_hm_alice"} + s.Nil(query.Query().Create(&alice)) + s.Nil(query.Query().Relation(alice, "Books").SaveMany([]*Book{{Name: "wr_hm_a1"}, {Name: "wr_hm_a2"}})) + bob := &User{Name: "wr_hm_bob"} + s.Nil(query.Query().Create(&bob)) + s.Nil(query.Query().Relation(bob, "Books").SaveMany([]*Book{{Name: "wr_hm_b1"}})) var users []User s.Nil(query.Query().Where("name like ?", "wr_hm_%").OrderBy("name"). @@ -70,8 +71,9 @@ func (s *WithTestSuite) TestWith_HasMany() { func (s *WithTestSuite) TestWith_HasOne() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_ho_user", Address: &Address{Name: "wr_ho_addr", Province: "X"}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_ho_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Address").Save(&Address{Name: "wr_ho_addr", Province: "X"})) var loaded User s.Nil(query.Query().Where("name = ?", "wr_ho_user"). @@ -85,8 +87,11 @@ func (s *WithTestSuite) TestWith_HasOne() { func (s *WithTestSuite) TestWith_BelongsTo() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_bt_user", Address: &Address{Name: "wr_bt_addr"}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_bt_user"} + s.Nil(query.Query().Create(&u)) + addr := &Address{Name: "wr_bt_addr"} + s.Nil(query.Query().Create(&addr)) + s.Nil(query.Query().Relation(addr, "User").Associate(u)) var addrs []Address s.Nil(query.Query().Where("name = ?", "wr_bt_addr"). @@ -103,8 +108,9 @@ func (s *WithTestSuite) TestWith_Many2Many() { s.Run(driver, func() { role := &Role{Name: "wr_m2m_role"} s.Nil(query.Query().Create(&role)) - u := &User{Name: "wr_m2m_user", Roles: []*Role{role}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_m2m_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Roles").Save(role)) var loaded User s.Nil(query.Query().Where("name = ?", "wr_m2m_user"). @@ -118,8 +124,9 @@ func (s *WithTestSuite) TestWith_Many2Many() { func (s *WithTestSuite) TestWith_MorphOne() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_mo_user", House: &House{Name: "wr_mo_house"}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_mo_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "House").Save(&House{Name: "wr_mo_house"})) var loaded User s.Nil(query.Query().Where("name = ?", "wr_mo_user"). @@ -133,8 +140,9 @@ func (s *WithTestSuite) TestWith_MorphOne() { func (s *WithTestSuite) TestWith_MorphMany() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_mm_user", Phones: []*Phone{{Name: "wr_mm_p1"}, {Name: "wr_mm_p2"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_mm_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Phones").SaveMany([]*Phone{{Name: "wr_mm_p1"}, {Name: "wr_mm_p2"}})) var loaded User s.Nil(query.Query().Where("name = ?", "wr_mm_user"). @@ -148,10 +156,20 @@ func (s *WithTestSuite) TestWith_HasManyThrough() { for driver, query := range s.queries { s.Run(driver, func() { // User -> Books -> Authors (declared via userWithThrough.ThroughRelations()). - u1 := &User{Name: "wr_th_u1", Books: []*Book{{Name: "wr_th_b1", Author: &Author{Name: "wr_th_a1"}}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u1)) - u2 := &User{Name: "wr_th_u2", Books: []*Book{{Name: "wr_th_b2", Author: &Author{Name: "wr_th_a2"}}, {Name: "wr_th_b3", Author: &Author{Name: "wr_th_a3"}}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u2)) + u1 := &User{Name: "wr_th_u1"} + s.Nil(query.Query().Create(&u1)) + b1 := &Book{Name: "wr_th_b1"} + s.Nil(query.Query().Relation(u1, "Books").Save(b1)) + s.Nil(query.Query().Relation(b1, "Author").Save(&Author{Name: "wr_th_a1"})) + + u2 := &User{Name: "wr_th_u2"} + s.Nil(query.Query().Create(&u2)) + b2 := &Book{Name: "wr_th_b2"} + s.Nil(query.Query().Relation(u2, "Books").Save(b2)) + s.Nil(query.Query().Relation(b2, "Author").Save(&Author{Name: "wr_th_a2"})) + b3 := &Book{Name: "wr_th_b3"} + s.Nil(query.Query().Relation(u2, "Books").Save(b3)) + s.Nil(query.Query().Relation(b3, "Author").Save(&Author{Name: "wr_th_a3"})) // Reuse the userAuthorsThrough model (defined below) which carries the through // declaration but reads from the same users table. @@ -170,8 +188,11 @@ func (s *WithTestSuite) TestWith_HasManyThrough() { func (s *WithTestSuite) TestWith_Nested() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_n_user", Books: []*Book{{Name: "wr_n_b1", Author: &Author{Name: "wr_n_a1"}}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_n_user"} + s.Nil(query.Query().Create(&u)) + b1 := &Book{Name: "wr_n_b1"} + s.Nil(query.Query().Relation(u, "Books").Save(b1)) + s.Nil(query.Query().Relation(b1, "Author").Save(&Author{Name: "wr_n_a1"})) var loaded User s.Nil(query.Query().Where("name = ?", "wr_n_user"). @@ -186,8 +207,9 @@ func (s *WithTestSuite) TestWith_Nested() { func (s *WithTestSuite) TestWith_Callback() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_cb_user", Books: []*Book{{Name: "wr_cb_keep"}, {Name: "wr_cb_drop"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_cb_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").SaveMany([]*Book{{Name: "wr_cb_keep"}, {Name: "wr_cb_drop"}})) var loaded User cb := func(q contractsorm.Query) contractsorm.Query { @@ -206,8 +228,10 @@ func (s *WithTestSuite) TestWith_Map() { s.Run(driver, func() { role := &Role{Name: "wr_map_role"} s.Nil(query.Query().Create(&role)) - u := &User{Name: "wr_map_user", Books: []*Book{{Name: "wr_map_book"}}, Roles: []*Role{role}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_map_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").Save(&Book{Name: "wr_map_book"})) + s.Nil(query.Query().Relation(u, "Roles").Save(role)) var loaded User s.Nil(query.Query().Where("name = ?", "wr_map_user"). @@ -224,8 +248,9 @@ func (s *WithTestSuite) TestWith_Map() { func (s *WithTestSuite) TestWith_Columns() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wr_col_user", Books: []*Book{{Name: "wr_col_b1"}}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wr_col_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").Save(&Book{Name: "wr_col_b1"})) var loaded User s.Nil(query.Query().Where("name = ?", "wr_col_user"). @@ -243,8 +268,10 @@ func (s *WithTestSuite) TestWith_Columns() { func (s *WithTestSuite) TestWithout() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wrr_user", Books: []*Book{{Name: "wrr_b"}}, Address: &Address{Name: "wrr_a"}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wrr_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").Save(&Book{Name: "wrr_b"})) + s.Nil(query.Query().Relation(u, "Address").Save(&Address{Name: "wrr_a"})) var loaded User s.Nil(query.Query().Where("name = ?", "wrr_user"). @@ -260,8 +287,10 @@ func (s *WithTestSuite) TestWithout() { func (s *WithTestSuite) TestWithOnly() { for driver, query := range s.queries { s.Run(driver, func() { - u := &User{Name: "wro_user", Books: []*Book{{Name: "wro_b"}}, Address: &Address{Name: "wro_a"}} - s.Nil(query.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: "wro_user"} + s.Nil(query.Query().Create(&u)) + s.Nil(query.Query().Relation(u, "Books").Save(&Book{Name: "wro_b"})) + s.Nil(query.Query().Relation(u, "Address").Save(&Address{Name: "wro_a"})) var loaded User s.Nil(query.Query().Where("name = ?", "wro_user"). @@ -285,8 +314,9 @@ func (s *WithTestSuite) TestWith_ChunkedIN() { const total = 1100 // > SQLite's default SQLITE_MAX_VARIABLE_NUMBER of 999 for i := 0; i < total; i++ { - u := &User{Name: fmt.Sprintf("wr_chunk_%04d", i), Books: []*Book{{Name: fmt.Sprintf("wr_chunk_b_%04d", i)}}} - s.Nil(q.Query().Select(orm.Associations).Create(&u)) + u := &User{Name: fmt.Sprintf("wr_chunk_%04d", i)} + s.Nil(q.Query().Create(&u)) + s.Nil(q.Query().Relation(u, "Books").Save(&Book{Name: fmt.Sprintf("wr_chunk_b_%04d", i)})) } var users []User