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/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/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_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/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/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/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..069ebb18e --- /dev/null +++ b/database/gorm/eager_loader_test.go @@ -0,0 +1,528 @@ +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, + } +} + +// 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/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/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/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/queries_relationships.go b/database/gorm/queries_relationships.go new file mode 100644 index 000000000..caf14f99f --- /dev/null +++ b/database/gorm/queries_relationships.go @@ -0,0 +1,921 @@ +// 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 { + return strings.NewReplacer("`", "", "'", "", `"`, "").Replace(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..eda41eb51 --- /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/query.go b/database/gorm/query.go index 9bb020634..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 @@ -94,12 +91,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 +147,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 +453,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 +496,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 { @@ -786,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 != "*" }) @@ -1152,16 +1164,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 +1262,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 +1540,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 @@ -1595,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 } @@ -1755,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 } @@ -1772,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 { @@ -1793,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 } @@ -1834,6 +1782,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 } @@ -1842,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 { @@ -1862,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 } @@ -1894,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 } @@ -1941,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{ @@ -1956,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.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_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_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)) +} 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_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.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..96973d0cd --- /dev/null +++ b/database/gorm/relation_writes_test.go @@ -0,0 +1,744 @@ +package gorm + +import ( + "reflect" + "testing" + "time" + + "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" +) + +// 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") +} + +// 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.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) } 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/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")) +} 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..34ac71888 100644 --- a/errors/list.go +++ b/errors/list.go @@ -158,25 +158,42 @@ 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") + 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 +} 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 -} 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{} } 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/models.go b/tests/models.go index ae1d8ce3d..5d47126e9 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 { diff --git a/tests/queries_relationships_test.go b/tests/queries_relationships_test.go new file mode 100644 index 000000000..c9fd71906 --- /dev/null +++ b/tests/queries_relationships_test.go @@ -0,0 +1,567 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + contractsorm "github.com/goravel/framework/contracts/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"} + 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"} + s.Nil(query.Query().Create(&carol)) + s.Nil(query.Query().Relation(carol, "Books").Save(&Book{Name: "cb1"})) + + 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"} + 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 + 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"} + 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)) + + 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"} + 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 { + 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"} + 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 + 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() { + 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)) + + 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"} + 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)) + + 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"} + 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 + 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"} + 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 { + 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"} + 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"} + 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"). + 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"} + 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_%") + } + 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"} + 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"). + 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"} + 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)) + + 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..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) @@ -83,7 +82,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) }, @@ -93,19 +92,22 @@ 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) - s.Nil(query.Query().Model(&user1).Association("Address").Append(&Address{Name: "association_has_one_append_address1"}), 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) s.True(user1.Address.ID > 0, driver) @@ -117,23 +119,27 @@ 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) 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.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,19 +149,22 @@ 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) - s.Nil(query.Query().Model(&user1).Association("Address").Replace(&Address{Name: "association_has_one_append_address1"})) + // 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")) s.True(user1.Address.ID > 0) @@ -167,21 +176,26 @@ 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) 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) + _, 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")) s.Equal(1, len(user1.Books)) @@ -193,32 +207,31 @@ 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 + // 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"})) + _, err := query.Query().Related(&user1, "Address").Where("name", "association_delete_address").Delete() + s.Nil(err) 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 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().Model(&user2).Association("Address").Delete(&userAddress)) + _, 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) @@ -229,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().Model(&user1).Association("Address").Clear()) + _, err := query.Query().Related(&user1, "Address").Delete() + s.Nil(err) s.Nil(query.Query().Load(&user1, "Address")) s.Nil(user1.Address) @@ -253,21 +267,25 @@ 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) 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) }, }, } @@ -285,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 @@ -492,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) @@ -506,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) }, }, } @@ -587,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)) @@ -1114,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) }, }, { @@ -1243,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)) @@ -1263,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)) @@ -1371,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) }, }, { @@ -1521,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)) @@ -1540,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)) @@ -3119,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 @@ -3141,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 @@ -3168,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) @@ -3192,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) @@ -3222,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) @@ -3261,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) @@ -3276,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 @@ -3300,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 @@ -3321,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) @@ -3373,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) @@ -3418,7 +3370,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)) @@ -3497,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) @@ -3531,7 +3488,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) @@ -3629,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 @@ -3687,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 @@ -3710,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 @@ -4598,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) @@ -4630,7 +4608,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)) @@ -4661,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 new file mode 100644 index 000000000..4e105d262 --- /dev/null +++ b/tests/with_test.go @@ -0,0 +1,377 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + contractsorm "github.com/goravel/framework/contracts/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"} + 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"). + 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"} + 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"). + 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"} + 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"). + 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"} + 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"). + 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"} + 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"). + 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"} + 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"). + 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"} + 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. + 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"} + 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"). + 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"} + 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 { + 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"} + 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"). + 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"} + 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"). + 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"} + 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"). + 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"} + 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"). + 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)} + 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 + 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", + }, + } +}