relations

package
v0.30.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package relations holds the sixteen relation types, and the eager loading that keeps them from being N+1 queries.

The thirteen walking methods BelongsToMany and HasOneOrManyThrough each expose -- Chunk, ChunkByID, ChunkByIDDesc, OrderedChunkByID, Each, EachByID, Lazy, LazyByID, LazyByIDDesc, Cursor, Paginate, SimplePaginate and CursorPaginate -- have one body between them, in chunking.go. The only line that differs is BelongsToMany hydrating the pivot on each page, which is a field of the shared body rather than a second copy of it.

The four methods

AddEagerConstraints, InitRelation, Match and GetEager are what turn a hundred queries into two. One query fetches the parents; AddEagerConstraints widens the relation's where into a single `in (...)` over every parent key; InitRelation seeds each parent so that one with no children answers empty instead of going back to the database; Match buckets the flat result set by key and hands each parent its own. EagerLoadRelation is the loop, and the test for it counts the statements the connection was asked for -- because the values come out right either way, which is why an N+1 passes review.

Every read takes the Grant, and every read filters by tenant

GetResults, Get, First, GetEager, Attach, Detach and Sync all take a context and an auth.Grant, and every statement is narrowed to auth.Tenant(g) before it leaves -- on the eager path as much as the lazy one. The eager path is where it matters most: a parent query that is correctly scoped and a child query that is not returns the right parents carrying another customer's children, and every row on the screen looks like it belongs there. A Grant carrying no tenant is refused rather than compiled into a comparison with the empty string.

The query builder decides no such thing -- it builds SQL. What is enforced here is that a relation cannot be executed without the Grant that authorized it, because the Grant is in the signature.

The morph map is mandatory here, and that is the trade being bought

There is no type resolved from a name at run time in Go, so a *_type column holds an alias registered with MorphMap. The type it names can then be renamed, moved or split without a single stored row becoming unreadable. An unregistered alias is an error that says which alias and what is registered.

Construction and overrides

An initialism is upper case: ParseIDs, ThroughKey. A shared behaviour is a struct to embed, and the parts a subtype must supply arrive as function fields.

The one shape that had to move is virtual dispatch. Go embedding does not dispatch to the outer type, so each concrete constructor calls its own AddConstraints as its last statement, and an override the shared half needs to reach -- the aliased pivot columns, the one-of-many relation query -- is a field the subtype sets rather than a method it redeclares.

Index

Constants

View Source
const ThroughKey = "arandu_through_key"

ThroughKey is the alias the intermediate key is selected under.

The name is the framework's rather than the user's, and it is a column name that comes back on every row of a has-many-through, which makes it the one string in this package a reader is guaranteed to meet in a result set.

Variables

View Source
var ErrModelNotFound = errors.New("relations: no query results for the model")

ErrModelNotFound is returned by the failing reads on a relation -- the ones that must produce a row -- when the query matched none.

It is the sentinel a handler turns into a 404. The reads that tolerate a miss answer nil instead, so seeing this error means the caller asked for a row that has to exist. The wrapping message carries the table, and the key when the lookup had one.

View Source
var ErrMorphNotMapped = fmt.Errorf("relations: morph type is not in the morph map")

ErrMorphNotMapped is what a polymorphic read returns when the value in the type column names nothing.

It is what an unregistered type produces in both directions: writing a model with no alias, and reading an alias nothing was registered under. There is no type name in the column to fall back to.

View Source
var ErrMultipleRecordsFound = errors.New("relations: more than one record matched a query that expected one")

ErrMultipleRecordsFound is returned by the singular reads on a relation -- Sole, and the pivot lookups that expect one row -- when the query matched more than one.

It means the relation is not as unique as the caller assumed: a has-one pointing at a column with duplicates, or a pivot row that exists twice. The wrapping message carries the count and the table.

Functions

func EnforceMorphMap

func EnforceMorphMap(entries map[string]func() Model, merge ...bool) map[string]func() Model

EnforceMorphMap answers Relation::enforceMorphMap.

func FlushMorphMap

func FlushMorphMap()

FlushMorphMap empties the alias registry.

It exists for the tests, and it exists because the registry is package state with no other way out of it: a test that registers "post" leaves it there for every test that runs after it in the same binary, and the failure that eventually shows up names a type the failing test never mentioned.

It is not a runtime facility. An application registers its aliases once, at wiring, and calling this in one would unregister them for every goroutine at the same time.

func ForeignKeyFor

func ForeignKeyFor(model Model) string

ForeignKeyFor answers Model::getForeignKey for a relation guessing its own key: the parent's morph alias, snake cased, plus the key name.

It is here rather than on the model because a relation is the only caller, and because the alias -- not a class name -- is what names a model in this collection.

func GetMorphAlias

func GetMorphAlias(model Model) (string, error)

GetMorphAlias answers Relation::getMorphAlias.

The PHP searches the map for a class name. A model here already knows the alias it was registered under -- that is what GetMorphClass answers -- so the search is a lookup, and its only remaining job is refusing a model that was never registered while requireMorphMap is on.

func GetMorphMap

func GetMorphMap() map[string]func() Model

GetMorphMap returns the registered aliases. The PHP reads the public static property; a package variable cannot be exported without letting a caller replace the map mid-flight, so it is read through a function.

func GetMorphedModel

func GetMorphedModel(alias string) func() Model

GetMorphedModel answers Relation::getMorphedModel: the factory registered under an alias, or nil.

func MorphMap

func MorphMap(entries map[string]func() Model, merge ...bool) map[string]func() Model

MorphMap registers the aliases a polymorphic type column holds, and returns the map.

The map is mandatory here, and that is the better half of the trade

Go has no type name at run time to write into the column, so the map is the mechanism rather than a recommendation. What the column holds is an alias the application chose -- "post", "video" -- and the type it points at can be renamed, moved between packages, or split in two, without a single row of data becoming unreadable.

The values are factories rather than instances, because every read needs a fresh model and a shared one would be a data race the first time two requests loaded the same morph type.

relations.MorphMap(map[string]func() relations.Model{
    "post":  func() relations.Model { return &Post{} },
    "video": func() relations.Model { return &Video{} },
})

merge defaults to true.

func RequireMorphMap

func RequireMorphMap(required ...bool)

RequireMorphMap answers Relation::requireMorphMap.

It is on by default here, which the PHP's is not. Turning it off cannot restore the PHP's behaviour -- there is no class name in the column to resolve -- so what it changes is only whether an unmapped alias is refused early or fails later with a less helpful message.

func RequiresMorphMap

func RequiresMorphMap() bool

RequiresMorphMap answers Relation::requiresMorphMap.

Types

type BaseRelation

type BaseRelation struct {
	// Query is Relation::$query.
	Query Builder

	// Parent is Relation::$parent.
	Parent Model

	// Related is Relation::$related.
	Related Model

	// EagerKeysWereEmpty is Relation::$eagerKeysWereEmpty: set when the parents
	// carried no keys at all, so that GetEager answers an empty collection
	// instead of running `where in ()`.
	EagerKeysWereEmpty bool

	// ExistenceCompareKey is getExistenceCompareKey as the subtype answers it.
	//
	// It is a field for the reason AliasedPivotColumns is one: the method below
	// is called from GetRelationExistenceQuery, which lives on this type, and Go
	// dispatches statically -- so the call would reach this type's own answer and
	// never the override, whatever the relation actually is. The field is what
	// makes the subtype's version run.
	//
	// What it cost while it was missing: a whereHas over a has-many compiled
	// `where exists (select * from posts where users.id = posts.id)`, comparing
	// the parent's key against the child's own key instead of against the foreign
	// key pointing back. That is a well formed query, it runs, and it answers
	// with the rows whose ids happen to line up.
	ExistenceCompareKey func() string
}

BaseRelation answers the body of the abstract Relation class: everything it implements rather than declares.

A relation embeds it and supplies the five abstract methods. It is exported for the same reason query.BaseGrammar is: embedding is how Go says "extends" for the half that is code rather than contract.

There is no virtual dispatch, so the constructor does not call AddConstraints

The PHP constructor ends with $this->addConstraints(), and late binding sends it to the subclass. Embedding in Go does not: a call from here would reach BaseRelation's own method and quietly build an unconstrained relation. So each concrete constructor calls its own AddConstraints as its last statement, and that is the one line of the PHP shape that had to move.

func NewBaseRelation

func NewBaseRelation(query Builder, parent Model) BaseRelation

NewBaseRelation answers the shared half of Relation::__construct.

func (*BaseRelation) CreatedAt

func (r *BaseRelation) CreatedAt() string

CreatedAt answers Relation::createdAt.

func (*BaseRelation) Find

func (r *BaseRelation) Find(ctx context.Context, g auth.Grant, id any) (Model, error)

Find answers Relation::find, through the builder.

func (*BaseRelation) FindMany

func (r *BaseRelation) FindMany(ctx context.Context, g auth.Grant, ids []any) ([]Model, error)

FindMany answers BelongsToMany::findMany.

func (*BaseRelation) FindOr

func (r *BaseRelation) FindOr(ctx context.Context, g auth.Grant, id any, callback func() (Model, error)) (Model, error)

FindOr answers BelongsToMany::findOr: the callback answers the miss.

func (*BaseRelation) FindOrFail

func (r *BaseRelation) FindOrFail(ctx context.Context, g auth.Grant, id any) (Model, error)

FindOrFail answers BelongsToMany::findOrFail: the miss is an error rather than a nil the caller has to remember to check.

func (*BaseRelation) FindSole

func (r *BaseRelation) FindSole(ctx context.Context, g auth.Grant, id any) (Model, error)

FindSole answers BelongsToMany::findSole.

func (*BaseRelation) First

func (r *BaseRelation) First(ctx context.Context, g auth.Grant) (Model, error)

First answers the Builder::first that every one-result relation calls.

func (*BaseRelation) FirstOr

func (r *BaseRelation) FirstOr(ctx context.Context, g auth.Grant, callback func() (Model, error)) (Model, error)

FirstOr answers BelongsToMany::firstOr.

func (*BaseRelation) FirstOrFail

func (r *BaseRelation) FirstOrFail(ctx context.Context, g auth.Grant) (Model, error)

FirstOrFail answers BelongsToMany::firstOrFail.

func (*BaseRelation) FirstWhere

func (r *BaseRelation) FirstWhere(ctx context.Context, g auth.Grant, column string, args ...any) (Model, error)

FirstWhere answers BelongsToMany::firstWhere.

func (*BaseRelation) Get

func (r *BaseRelation) Get(ctx context.Context, g auth.Grant, columns ...any) ([]Model, error)

Get answers Relation::get.

It is the one place every relation's reads funnel through, and it is where the tenant filter goes on. On a clone, so that reading a relation twice does not add the clause twice, and before the Grant reaches the connection, so that a read authorized for one customer cannot return another's rows.

func (*BaseRelation) GetBaseQuery

func (r *BaseRelation) GetBaseQuery() *query.Builder

GetBaseQuery answers Relation::getBaseQuery.

func (*BaseRelation) GetEager

func (r *BaseRelation) GetEager(ctx context.Context, g auth.Grant) ([]Model, error)

GetEager answers Relation::getEager.

The empty-keys branch is not an optimization. `where in ()` is a syntax error on some engines and a match-nothing on others, and the parents that produced no keys still have to come back with their relation initialized -- so the query is skipped and the empty collection is the answer.

func (*BaseRelation) GetExistenceCompareKey

func (r *BaseRelation) GetExistenceCompareKey() string

GetExistenceCompareKey answers Relation::getExistenceCompareKey.

Each relation overrides it, and sets ExistenceCompareKey in its constructor so that the override is what this answers -- see the field. Without one set, this answers the related model's key, which is what a relation with no foreign key of its own would compare.

func (*BaseRelation) GetKeys

func (r *BaseRelation) GetKeys(models []Model, key string) ([]any, error)

GetKeys answers Relation::getKeys: every parent's key, deduplicated and sorted.

Sorted because the PHP sorts, and the PHP sorts because the emitted `in (...)` is then stable between requests -- which is what lets a query log, a slow query report and a prepared statement cache recognize the same query twice.

func (*BaseRelation) GetParent

func (r *BaseRelation) GetParent() Model

GetParent answers Relation::getParent.

func (*BaseRelation) GetQualifiedParentKeyName

func (r *BaseRelation) GetQualifiedParentKeyName() string

GetQualifiedParentKeyName answers Relation::getQualifiedParentKeyName.

func (*BaseRelation) GetQuery

func (r *BaseRelation) GetQuery() Builder

GetQuery answers Relation::getQuery.

func (*BaseRelation) GetRelated

func (r *BaseRelation) GetRelated() Model

GetRelated answers Relation::getRelated.

func (*BaseRelation) GetRelationCountHash

func (r *BaseRelation) GetRelationCountHash(incrementJoinCount ...bool) string

GetRelationCountHash answers Relation::getRelationCountHash: the alias a self-join needs so the same table can appear twice.

func (*BaseRelation) GetRelationExistenceCountQuery

func (r *BaseRelation) GetRelationExistenceCountQuery(q Builder, parentQuery Builder) Builder

GetRelationExistenceCountQuery answers Relation::getRelationExistenceCountQuery.

func (*BaseRelation) GetRelationExistenceQuery

func (r *BaseRelation) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers Relation::getRelationExistenceQuery: the subquery a whereHas compares against, which matches on column names rather than on values.

func (*BaseRelation) GetRelationQuery

func (r *BaseRelation) GetRelationQuery() Builder

GetRelationQuery answers Relation::getRelationQuery. CanBeOneOfMany overrides it to point the constraints at the subquery.

func (*BaseRelation) RawUpdate

func (r *BaseRelation) RawUpdate(ctx context.Context, g auth.Grant, attributes map[string]any) (int64, error)

RawUpdate answers Relation::rawUpdate.

func (*BaseRelation) RelatedUpdatedAt

func (r *BaseRelation) RelatedUpdatedAt() string

RelatedUpdatedAt answers Relation::relatedUpdatedAt.

func (*BaseRelation) Sole

func (r *BaseRelation) Sole(ctx context.Context, g auth.Grant, columns ...any) (Model, error)

Sole answers Relation::sole.

It carries two errors where the PHP throws two exceptions: nothing matched, and more than one did. Both are the caller's assumption being wrong, and both are worth telling apart.

func (*BaseRelation) ToBase

func (r *BaseRelation) ToBase() *query.Builder

ToBase answers Relation::toBase.

func (*BaseRelation) Touch

func (r *BaseRelation) Touch(ctx context.Context, g auth.Grant) error

Touch answers Relation::touch: it stamps the related rows' updated_at.

func (*BaseRelation) UpdatedAt

func (r *BaseRelation) UpdatedAt() string

UpdatedAt answers Relation::updatedAt.

func (*BaseRelation) WhereInEager

func (r *BaseRelation) WhereInEager(key string, modelKeys []any, q ...Builder)

WhereInEager answers Relation::whereInEager.

The PHP picks between whereIn and whereIntegerInRaw by method name; the choice is an optimization for integer keys that the base builder here does not offer, so this is whereIn and the flag it sets is the part that matters: an eager load with no keys must not run a query at all.

func (*BaseRelation) WhereInMethod

func (r *BaseRelation) WhereInMethod(model Model, key string) string

WhereInMethod answers Relation::whereInMethod.

It reports whether the key is the model's own integer primary key, which is the condition under which the PHP switches to whereIntegerInRaw. Kept because the question it asks is real and a driver-aware builder will want it; the answer changes no SQL today.

type BelongsTo

type BelongsTo struct {
	BaseRelation
	concerns.SupportsDefaultModels
	concerns.ComparesRelatedModels
	// contains filtered or unexported fields
}

BelongsTo is the inverse relation, where the foreign key is on this table and points at the other one.

The model holding the key is reachable as both the child and the parent: the shared half calls it the parent, and "parent" reads backwards for the row that carries the key.

func NewBelongsTo

func NewBelongsTo(query Builder, child Model, foreignKey, ownerKey, relationName string) *BelongsTo

NewBelongsTo builds the relation and narrows it to its parent.

func NewBelongsToUnconstrained

func NewBelongsToUnconstrained(query Builder, child Model, foreignKey, ownerKey, relationName string) *BelongsTo

NewBelongsToUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*BelongsTo) AddConstraints

func (r *BelongsTo) AddConstraints()

AddConstraints answers BelongsTo::addConstraints.

func (*BelongsTo) AddEagerConstraints

func (r *BelongsTo) AddEagerConstraints(models []Model) error

AddEagerConstraints answers BelongsTo::addEagerConstraints.

func (*BelongsTo) Associate

func (r *BelongsTo) Associate(model any) Model

Associate answers BelongsTo::associate.

It takes a model or a bare key, as the PHP does. With a model the relation is set too, so reading it back does not go to the database; with a key it is unset, because what is on the other end is not known.

func (*BelongsTo) Disassociate

func (r *BelongsTo) Disassociate() Model

Disassociate answers BelongsTo::disassociate, the alias the PHP keeps for the spelling people reach for.

func (*BelongsTo) Dissociate

func (r *BelongsTo) Dissociate() Model

Dissociate answers BelongsTo::dissociate.

func (*BelongsTo) GetChild

func (r *BelongsTo) GetChild() Model

GetChild answers BelongsTo::getChild.

func (*BelongsTo) GetExistenceCompareKey

func (r *BelongsTo) GetExistenceCompareKey() string

GetExistenceCompareKey answers the base class's method for this relation.

func (*BelongsTo) GetForeignKeyName

func (r *BelongsTo) GetForeignKeyName() string

GetForeignKeyName answers BelongsTo::getForeignKeyName.

func (*BelongsTo) GetOwnerKeyName

func (r *BelongsTo) GetOwnerKeyName() string

GetOwnerKeyName answers BelongsTo::getOwnerKeyName.

func (*BelongsTo) GetParentKey

func (r *BelongsTo) GetParentKey() any

GetParentKey answers BelongsTo::getParentKey: the child's foreign key, which is the value the owner is found by.

func (*BelongsTo) GetQualifiedForeignKeyName

func (r *BelongsTo) GetQualifiedForeignKeyName() string

GetQualifiedForeignKeyName answers BelongsTo::getQualifiedForeignKeyName.

func (*BelongsTo) GetQualifiedOwnerKeyName

func (r *BelongsTo) GetQualifiedOwnerKeyName() string

GetQualifiedOwnerKeyName answers BelongsTo::getQualifiedOwnerKeyName.

func (*BelongsTo) GetRelationExistenceQuery

func (r *BelongsTo) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers BelongsTo::getRelationExistenceQuery.

func (*BelongsTo) GetRelationExistenceQueryForSelfRelation

func (r *BelongsTo) GetRelationExistenceQueryForSelfRelation(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQueryForSelfRelation answers BelongsTo::getRelationExistenceQueryForSelfRelation.

func (*BelongsTo) GetRelationName

func (r *BelongsTo) GetRelationName() string

GetRelationName answers BelongsTo::getRelationName.

func (*BelongsTo) GetResults

func (r *BelongsTo) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers BelongsTo::getResults.

func (*BelongsTo) InitRelation

func (r *BelongsTo) InitRelation(models []Model, relation string) []Model

InitRelation answers BelongsTo::initRelation.

func (*BelongsTo) Match

func (r *BelongsTo) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers BelongsTo::match.

The dictionary here is one owner per key rather than a bucket of children, which is the whole difference between this and HasMany: many children share one owner, so the same loaded model is handed to each of them.

func (*BelongsTo) Touch

func (r *BelongsTo) Touch(ctx context.Context, g auth.Grant) error

Touch answers BelongsTo::touch: nothing to stamp when there is no owner.

type BelongsToMany

type BelongsToMany struct {
	BaseRelation
	concerns.InteractsWithPivotTable

	// AliasedPivotColumns is the PHP's protected aliasedPivotColumns, as a hook.
	// MorphToMany overrides that method to add the type column; Go dispatches
	// statically, so an override would never be reached from here -- the field
	// is what makes the subtype's version run.
	AliasedPivotColumns func() []any
	// contains filtered or unexported fields
}

BelongsToMany is many rows on the other table, reached through an intermediate table that holds the pairs.

The intermediate table is what makes this relation different from every other one: it is joined for reads, written directly for attach and detach, and its extra columns come back on the related model under an accessor -- "pivot" by default -- as a Pivot model rather than as attributes of the related row.

func NewBelongsToMany

func NewBelongsToMany(query Builder, parent Model, table, foreignPivotKey, relatedPivotKey, parentKey, relatedKey, relationName string) *BelongsToMany

NewBelongsToMany builds the relation and narrows it to its parent.

func NewBelongsToManyUnconstrained

func NewBelongsToManyUnconstrained(query Builder, parent Model, table, foreignPivotKey, relatedPivotKey, parentKey, relatedKey, relationName string) *BelongsToMany

NewBelongsToManyUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*BelongsToMany) AddConstraints

func (r *BelongsToMany) AddConstraints()

AddConstraints answers BelongsToMany::addConstraints.

func (*BelongsToMany) AddEagerConstraints

func (r *BelongsToMany) AddEagerConstraints(models []Model) error

AddEagerConstraints answers BelongsToMany::addEagerConstraints.

func (*BelongsToMany) As

func (r *BelongsToMany) As(accessor string) *BelongsToMany

As answers BelongsToMany::as: the accessor the pivot is read under, for a relation where "pivot" reads wrong -- $role->subscription rather than $role->pivot.

func (*BelongsToMany) Chunk

func (r *BelongsToMany) Chunk(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool) (bool, error)

Chunk answers BelongsToMany::chunk: the results a page at a time, with the pivot hydrated on each page.

func (*BelongsToMany) ChunkById

func (r *BelongsToMany) ChunkById(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool, column, alias string) (bool, error)

ChunkById answers BelongsToMany::chunkById, which the PHP writes as orderedChunkById with descending left false.

func (*BelongsToMany) ChunkByIdDesc

func (r *BelongsToMany) ChunkByIdDesc(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool, column, alias string) (bool, error)

ChunkByIdDesc answers BelongsToMany::chunkByIdDesc.

func (*BelongsToMany) Create

func (r *BelongsToMany) Create(ctx context.Context, g auth.Grant, attributes map[string]any, joining map[string]any, touch ...bool) (Model, error)

Create answers BelongsToMany::create.

func (*BelongsToMany) CreateMany

func (r *BelongsToMany) CreateMany(ctx context.Context, g auth.Grant, records []map[string]any, joinings []map[string]any) ([]Model, error)

CreateMany answers BelongsToMany::createMany.

func (*BelongsToMany) CreateOrFirst

func (r *BelongsToMany) CreateOrFirst(ctx context.Context, g auth.Grant, attributes map[string]any, values map[string]any, joining map[string]any, touch ...bool) (Model, error)

CreateOrFirst answers BelongsToMany::createOrFirst: create the related row and attach it, and if somebody else created it first, attach theirs.

It is the same race HasOneOrMany::createOrFirst loses, with one more way to lose it, and the PHP body is two try blocks for exactly that reason. The first insert can collide on the related table's unique index; the recovery then finds the row somebody else wrote and attaches it, and that attach can collide on the pivot's own unique index, because the other request is attaching it too. The second collision is success -- the row exists and the link exists, which is what the caller asked for -- so it is answered with the row rather than the error.

Every error that is not a unique violation is returned. A caller that gets a row back from this method can rely on the link existing; one that got a row back from a swallowed error could not.

func (*BelongsToMany) CreatedAt

func (r *BelongsToMany) CreatedAt() string

CreatedAt answers BelongsToMany::createdAt.

func (*BelongsToMany) Cursor

func (r *BelongsToMany) Cursor(ctx context.Context, g auth.Grant) iter.Seq2[Model, error]

Cursor answers BelongsToMany::cursor.

func (*BelongsToMany) CursorPaginate

func (r *BelongsToMany) CursorPaginate(ctx context.Context, g auth.Grant, perPage int, cursor *pagination.Cursor, opts pagination.Options, columns ...any) (*pagination.CursorPaginator[Model], error)

CursorPaginate answers BelongsToMany::cursorPaginate.

func (*BelongsToMany) Each

func (r *BelongsToMany) Each(ctx context.Context, g auth.Grant, callback func(value Model, key int) bool, count int) (bool, error)

Each answers BelongsToMany::each.

func (*BelongsToMany) EachById

func (r *BelongsToMany) EachById(ctx context.Context, g auth.Grant, callback func(value Model, key int) bool, count int, column, alias string) (bool, error)

EachById answers BelongsToMany::eachById.

func (*BelongsToMany) Get

func (r *BelongsToMany) Get(ctx context.Context, g auth.Grant, columns ...any) ([]Model, error)

Get answers BelongsToMany::get.

The pivot columns are added to the select and then lifted back off the related model into a Pivot: they arrive as pivot_user_id and pivot_role_id because they would otherwise collide with the related table's own columns, and a related model carrying a column from another table is a model whose attributes lie about where they came from.

func (*BelongsToMany) GetEager

func (r *BelongsToMany) GetEager(ctx context.Context, g auth.Grant) ([]Model, error)

GetEager answers Relation::getEager for this relation: the pivot hydration has to happen on the eager path too, or Match has nothing to key on.

func (*BelongsToMany) GetExistenceCompareKey

func (r *BelongsToMany) GetExistenceCompareKey() string

GetExistenceCompareKey answers BelongsToMany::getExistenceCompareKey.

func (*BelongsToMany) GetForeignPivotKeyName

func (r *BelongsToMany) GetForeignPivotKeyName() string

GetForeignPivotKeyName answers BelongsToMany::getForeignPivotKeyName.

func (*BelongsToMany) GetParentKeyName

func (r *BelongsToMany) GetParentKeyName() string

GetParentKeyName answers BelongsToMany::getParentKeyName.

func (*BelongsToMany) GetPivotAccessor

func (r *BelongsToMany) GetPivotAccessor() string

GetPivotAccessor answers BelongsToMany::getPivotAccessor.

func (*BelongsToMany) GetPivotClass

func (r *BelongsToMany) GetPivotClass() PivotFactory

GetPivotClass answers BelongsToMany::getPivotClass.

The PHP answers a class name; the equivalent here is the factory that builds the pivot, which is what using() takes. nil means the plain Pivot.

func (*BelongsToMany) GetPivotColumns

func (r *BelongsToMany) GetPivotColumns() []string

GetPivotColumns answers BelongsToMany::getPivotColumns.

func (*BelongsToMany) GetQualifiedForeignPivotKeyName

func (r *BelongsToMany) GetQualifiedForeignPivotKeyName() string

GetQualifiedForeignPivotKeyName answers the method of the same name.

func (*BelongsToMany) GetQualifiedParentKeyName

func (r *BelongsToMany) GetQualifiedParentKeyName() string

GetQualifiedParentKeyName answers BelongsToMany::getQualifiedParentKeyName.

func (*BelongsToMany) GetQualifiedRelatedKeyName

func (r *BelongsToMany) GetQualifiedRelatedKeyName() string

GetQualifiedRelatedKeyName answers BelongsToMany::getQualifiedRelatedKeyName.

func (*BelongsToMany) GetQualifiedRelatedPivotKeyName

func (r *BelongsToMany) GetQualifiedRelatedPivotKeyName() string

GetQualifiedRelatedPivotKeyName answers the method of the same name.

func (*BelongsToMany) GetRelatedKeyName

func (r *BelongsToMany) GetRelatedKeyName() string

GetRelatedKeyName answers BelongsToMany::getRelatedKeyName.

func (*BelongsToMany) GetRelatedPivotKeyName

func (r *BelongsToMany) GetRelatedPivotKeyName() string

GetRelatedPivotKeyName answers BelongsToMany::getRelatedPivotKeyName.

func (*BelongsToMany) GetRelationExistenceQuery

func (r *BelongsToMany) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers BelongsToMany::getRelationExistenceQuery.

func (*BelongsToMany) GetRelationExistenceQueryForSelfJoin

func (r *BelongsToMany) GetRelationExistenceQueryForSelfJoin(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQueryForSelfJoin answers the method of the same name.

Exported because the PHP's is public, and because a self-referencing many-to-many -- a user who follows users -- is the case where somebody has to read it to believe the aliasing.

func (*BelongsToMany) GetRelationName

func (r *BelongsToMany) GetRelationName() string

GetRelationName answers BelongsToMany::getRelationName.

func (*BelongsToMany) GetResults

func (r *BelongsToMany) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers BelongsToMany::getResults.

func (*BelongsToMany) GetTable

func (r *BelongsToMany) GetTable() string

GetTable answers BelongsToMany::getTable: the intermediate table.

func (*BelongsToMany) InitRelation

func (r *BelongsToMany) InitRelation(models []Model, relation string) []Model

InitRelation answers BelongsToMany::initRelation.

func (*BelongsToMany) Lazy

func (r *BelongsToMany) Lazy(ctx context.Context, g auth.Grant, chunkSize int) iter.Seq2[Model, error]

Lazy answers BelongsToMany::lazy.

func (*BelongsToMany) LazyById

func (r *BelongsToMany) LazyById(ctx context.Context, g auth.Grant, chunkSize int, column, alias string) iter.Seq2[Model, error]

LazyById answers BelongsToMany::lazyById.

func (*BelongsToMany) LazyByIdDesc

func (r *BelongsToMany) LazyByIdDesc(ctx context.Context, g auth.Grant, chunkSize int, column, alias string) iter.Seq2[Model, error]

LazyByIdDesc answers BelongsToMany::lazyByIdDesc.

func (*BelongsToMany) Limit

func (r *BelongsToMany) Limit(value int) *BelongsToMany

Limit answers BelongsToMany::limit.

func (*BelongsToMany) Match

func (r *BelongsToMany) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers BelongsToMany::match.

The dictionary is keyed by the pivot's copy of the parent key, not by anything on the related row -- the related row has no idea which parent pulled it in, which is why the pivot columns are selected in the first place.

func (*BelongsToMany) NewPivot

func (r *BelongsToMany) NewPivot(attributes map[string]any, exists bool) Model

NewPivot answers BelongsToMany::newPivot.

func (*BelongsToMany) NewPivotQuery

func (r *BelongsToMany) NewPivotQuery(g auth.Grant) (*query.Builder, error)

NewPivotQuery answers BelongsToMany::newPivotQuery.

func (*BelongsToMany) NewPivotStatement

func (r *BelongsToMany) NewPivotStatement(g auth.Grant) (*query.Builder, error)

NewPivotStatement answers BelongsToMany::newPivotStatement.

It is the base query builder over the intermediate table, and it comes back already filtered by the Grant's tenant. That filter is not optional and not the caller's job: attach, detach, sync and toggle all reach the table through here, and a pivot table is shared by every tenant in the system.

func (*BelongsToMany) OrWherePivot

func (r *BelongsToMany) OrWherePivot(column string, args ...any) *BelongsToMany

OrWherePivot answers BelongsToMany::orWherePivot.

func (*BelongsToMany) OrWherePivotBetween

func (r *BelongsToMany) OrWherePivotBetween(column string, from, to any) *BelongsToMany

OrWherePivotBetween answers BelongsToMany::orWherePivotBetween.

func (*BelongsToMany) OrWherePivotIn

func (r *BelongsToMany) OrWherePivotIn(column string, values []any) *BelongsToMany

OrWherePivotIn answers BelongsToMany::orWherePivotIn.

func (*BelongsToMany) OrWherePivotNotBetween

func (r *BelongsToMany) OrWherePivotNotBetween(column string, from, to any) *BelongsToMany

OrWherePivotNotBetween answers BelongsToMany::orWherePivotNotBetween.

func (*BelongsToMany) OrWherePivotNotIn

func (r *BelongsToMany) OrWherePivotNotIn(column string, values []any) *BelongsToMany

OrWherePivotNotIn answers BelongsToMany::orWherePivotNotIn.

func (*BelongsToMany) OrWherePivotNotNull

func (r *BelongsToMany) OrWherePivotNotNull(column string) *BelongsToMany

OrWherePivotNotNull answers BelongsToMany::orWherePivotNotNull.

func (*BelongsToMany) OrWherePivotNull

func (r *BelongsToMany) OrWherePivotNull(column string) *BelongsToMany

OrWherePivotNull answers BelongsToMany::orWherePivotNull.

func (*BelongsToMany) OrderByPivot

func (r *BelongsToMany) OrderByPivot(column string, direction ...string) *BelongsToMany

OrderByPivot answers BelongsToMany::orderByPivot.

func (*BelongsToMany) OrderByPivotDesc

func (r *BelongsToMany) OrderByPivotDesc(column string) *BelongsToMany

OrderByPivotDesc answers BelongsToMany::orderByPivotDesc.

func (*BelongsToMany) OrderedChunkById

func (r *BelongsToMany) OrderedChunkById(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool, column, alias string, descending bool) (bool, error)

OrderedChunkById answers BelongsToMany::orderedChunkById: pages taken by comparing against the last id seen, in the given direction.

The empty column and alias take the PHP's defaults, which are the related model's qualified key and the related key's name.

func (*BelongsToMany) Paginate

func (r *BelongsToMany) Paginate(ctx context.Context, g auth.Grant, perPage, page int, opts pagination.Options, columns ...any) (*pagination.LengthAwarePaginator[Model], error)

Paginate answers BelongsToMany::paginate.

func (*BelongsToMany) ParentKeyValue

func (r *BelongsToMany) ParentKeyValue() any

ParentKeyValue answers the PHP's $this->parent->{$this->parentKey}, which the pivot trait reads through the host contract.

func (*BelongsToMany) QualifyPivotColumn

func (r *BelongsToMany) QualifyPivotColumn(column string) string

QualifyPivotColumn answers BelongsToMany::qualifyPivotColumn.

func (*BelongsToMany) Save

func (r *BelongsToMany) Save(ctx context.Context, g auth.Grant, model Model, pivotAttributes map[string]any, touch ...bool) (Model, error)

Save answers BelongsToMany::save: save the related model, then attach it.

func (*BelongsToMany) SaveMany

func (r *BelongsToMany) SaveMany(ctx context.Context, g auth.Grant, models []Model, pivotAttributes map[string]any) ([]Model, error)

SaveMany answers BelongsToMany::saveMany.

func (*BelongsToMany) SimplePaginate

func (r *BelongsToMany) SimplePaginate(ctx context.Context, g auth.Grant, perPage, page int, opts pagination.Options, columns ...any) (*pagination.Paginator[Model], error)

SimplePaginate answers BelongsToMany::simplePaginate.

func (*BelongsToMany) Take

func (r *BelongsToMany) Take(value int) *BelongsToMany

Take answers BelongsToMany::take.

func (*BelongsToMany) Touch

func (r *BelongsToMany) Touch(ctx context.Context, g auth.Grant) error

Touch answers BelongsToMany::touch: it stamps the related rows this relation reaches, found through the pivot table rather than through the join.

func (*BelongsToMany) TouchIfTouching

func (r *BelongsToMany) TouchIfTouching(ctx context.Context, g auth.Grant) error

TouchIfTouching answers BelongsToMany::touchIfTouching.

func (*BelongsToMany) UpdatedAt

func (r *BelongsToMany) UpdatedAt() string

UpdatedAt answers BelongsToMany::updatedAt.

func (*BelongsToMany) Using

func (r *BelongsToMany) Using(factory PivotFactory) *BelongsToMany

Using answers BelongsToMany::using.

The PHP takes a class name and instantiates it. Here it takes the factory, because a name is not a type at run time and a registry keyed by string would be a second morph map for a problem that does not need one.

func (*BelongsToMany) WherePivot

func (r *BelongsToMany) WherePivot(column string, args ...any) *BelongsToMany

WherePivot answers BelongsToMany::wherePivot.

func (*BelongsToMany) WherePivotBetween

func (r *BelongsToMany) WherePivotBetween(column string, from, to any) *BelongsToMany

WherePivotBetween answers BelongsToMany::wherePivotBetween.

func (*BelongsToMany) WherePivotIn

func (r *BelongsToMany) WherePivotIn(column string, values []any) *BelongsToMany

WherePivotIn answers BelongsToMany::wherePivotIn.

func (*BelongsToMany) WherePivotNotBetween

func (r *BelongsToMany) WherePivotNotBetween(column string, from, to any) *BelongsToMany

WherePivotNotBetween answers BelongsToMany::wherePivotNotBetween.

func (*BelongsToMany) WherePivotNotIn

func (r *BelongsToMany) WherePivotNotIn(column string, values []any) *BelongsToMany

WherePivotNotIn answers BelongsToMany::wherePivotNotIn.

func (*BelongsToMany) WherePivotNotNull

func (r *BelongsToMany) WherePivotNotNull(column string) *BelongsToMany

WherePivotNotNull answers BelongsToMany::wherePivotNotNull.

func (*BelongsToMany) WherePivotNull

func (r *BelongsToMany) WherePivotNull(column string) *BelongsToMany

WherePivotNull answers BelongsToMany::wherePivotNull.

func (*BelongsToMany) WithPivot

func (r *BelongsToMany) WithPivot(columns ...string) *BelongsToMany

WithPivot answers InteractsWithPivotTable::withPivot, redeclared so it returns the relation rather than the embedded struct and the chain reads like the PHP's.

func (*BelongsToMany) WithPivotValue

func (r *BelongsToMany) WithPivotValue(column string, value any) (*BelongsToMany, error)

WithPivotValue answers BelongsToMany::withPivotValue: a pivot column that is both a filter on read and a default on write.

It carries an error where the PHP throws InvalidArgumentException, for the same case: a null value would be a filter that matches nothing and a default that writes nothing.

func (*BelongsToMany) WithTimestamps

func (r *BelongsToMany) WithTimestamps(columns ...string) *BelongsToMany

WithTimestamps answers BelongsToMany::withTimestamps.

type Builder

type Builder = concerns.Builder

Builder is the query a relation narrows and then runs: the interface of selects, wheres, joins and orders every relation in this package builds on top of, with a context and an auth.Grant on every method that reaches the database.

It is an alias rather than a declaration, for the reason Model is: the interface is defined in relations/concerns because the traits there need the same one and Go forbids a subpackage from importing its parent. Aliasing it back means relations.Builder is that interface, not a second one shaped like it.

type HasMany

type HasMany struct {
	HasOneOrMany
}

HasMany is every row on the other table whose foreign key points here.

func NewHasMany

func NewHasMany(query Builder, parent Model, foreignKey, localKey string) *HasMany

NewHasMany builds the relation and narrows it to its parent.

func NewHasManyUnconstrained

func NewHasManyUnconstrained(query Builder, parent Model, foreignKey, localKey string) *HasMany

NewHasManyUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*HasMany) GetResults

func (r *HasMany) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults returns every related model for the parent, or an empty slice if the parent has no key yet.

func (*HasMany) InitRelation

func (r *HasMany) InitRelation(models []Model, relation string) []Model

InitRelation seeds relation on every model in models with an empty slice.

Seeding every parent with an empty collection is what makes a parent with no children answer "none" instead of going back to the database to ask.

func (*HasMany) Match

func (r *HasMany) Match(models []Model, results []Model, relation string) ([]Model, error)

Match assigns each model in models every result in results that belongs to it, via MatchMany, and stores the slice under relation.

func (*HasMany) One

func (r *HasMany) One() *HasOne

One returns the same relation read as a single model instead of a slice.

It is built unconstrained, because the new HasOne would otherwise add a second copy of the where clause the HasMany already put on the shared query.

type HasManyThrough

type HasManyThrough struct {
	HasOneOrManyThrough
}

HasManyThrough is the many end of a relation that reaches its rows through an intermediate table: a country's posts, where posts belong to users and users belong to the country.

There is no column on posts naming the country, so the query joins users and filters on the country's key. GetResults answers the matching rows, or an empty slice when the far parent has no key yet -- an unsaved parent has no children, and the join would otherwise match every row.

func NewHasManyThrough

func NewHasManyThrough(query Builder, farParent, throughParent Model, firstKey, secondKey, localKey, secondLocalKey string) *HasManyThrough

NewHasManyThrough builds the relation and narrows it to its parent.

func NewHasManyThroughUnconstrained

func NewHasManyThroughUnconstrained(query Builder, farParent, throughParent Model, firstKey, secondKey, localKey, secondLocalKey string) *HasManyThrough

NewHasManyThroughUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*HasManyThrough) GetResults

func (r *HasManyThrough) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers HasManyThrough::getResults.

func (*HasManyThrough) InitRelation

func (r *HasManyThrough) InitRelation(models []Model, relation string) []Model

InitRelation answers HasManyThrough::initRelation.

func (*HasManyThrough) Match

func (r *HasManyThrough) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers HasManyThrough::match.

func (*HasManyThrough) One

func (r *HasManyThrough) One() *HasOneThrough

One answers HasManyThrough::one.

type HasOne

HasOne is one row on the other table, found by a foreign key pointing here.

It is HasMany matching one row instead of many, plus the three shared halves that make it comparable, defaultable and reducible to one of many.

func NewHasOne

func NewHasOne(query Builder, parent Model, foreignKey, localKey string) *HasOne

NewHasOne builds the relation and narrows it to its parent.

func NewHasOneUnconstrained

func NewHasOneUnconstrained(query Builder, parent Model, foreignKey, localKey string) *HasOne

NewHasOneUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*HasOne) AddConstraints

func (r *HasOne) AddConstraints()

AddConstraints redeclares HasOneOrMany's constraint logic so that it reaches the one-of-many subquery when there is one. Go promotes the embedded method but not the overridden GetRelationQuery it calls, which is the whole of what "no virtual dispatch" costs.

func (*HasOne) AddOneOfManyJoinSubQueryConstraints

func (r *HasOne) AddOneOfManyJoinSubQueryConstraints(join *query.JoinClause)

AddOneOfManyJoinSubQueryConstraints joins the subquery to the related table by equating their foreign key columns.

func (*HasOne) AddOneOfManySubQueryConstraints

func (r *HasOne) AddOneOfManySubQueryConstraints(q Builder, column, aggregate string)

AddOneOfManySubQueryConstraints adds the relation's qualified foreign key to q's select list.

func (*HasOne) GetOneOfManySubQuerySelectColumns

func (r *HasOne) GetOneOfManySubQuerySelectColumns() []any

GetOneOfManySubQuerySelectColumns returns the relation's qualified foreign key as the sole column to select in the one-of-many subquery.

func (*HasOne) GetRelationExistenceQuery

func (r *HasOne) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery merges the one-of-many joins into q when the relation is one-of-many, then delegates to HasOneOrMany's existence query.

func (*HasOne) GetRelationQuery

func (r *HasOne) GetRelationQuery() Builder

GetRelationQuery builds the relation's query through the embedded CanBeOneOfMany, using r.Query as its base builder. It overrides the method promoted from Relation because on a one-of-many relation, the constraints belong on the subquery.

func (*HasOne) GetResults

func (r *HasOne) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults returns the first related model, or the relation's default model if the parent has no key yet or no related row exists.

func (*HasOne) InitRelation

func (r *HasOne) InitRelation(models []Model, relation string) []Model

InitRelation seeds relation on every model in models with that model's default related instance.

func (*HasOne) Match

func (r *HasOne) Match(models []Model, results []Model, relation string) ([]Model, error)

Match assigns each model in models the one result from results that belongs to it, via MatchOne, and stores it under relation.

func (*HasOne) NewRelatedInstanceFor

func (r *HasOne) NewRelatedInstanceFor(parent Model) Model

NewRelatedInstanceFor returns a new related model instance with its foreign key already set to parent's local key, and the inverse relation applied.

type HasOneOrMany

type HasOneOrMany struct {
	BaseRelation
	concerns.SupportsInverseRelations
	// contains filtered or unexported fields
}

HasOneOrMany is the shared body of HasOne and HasMany.

The relation is a foreign key on the other table pointing back at this one. Everything below follows from that: the constraint is `where posts.user_id = ?`, the eager constraint is the same clause widened to `in (?, ?, ?)`, and the match keys the results by that column.

func NewHasOneOrMany

func NewHasOneOrMany(query Builder, parent Model, foreignKey, localKey string) HasOneOrMany

NewHasOneOrMany answers HasOneOrMany::__construct.

It does not call AddConstraints. The PHP constructor does, through the subclass; Go has no virtual dispatch, so HasOne and HasMany call their own as the last statement of theirs.

func (*HasOneOrMany) AddConstraints

func (r *HasOneOrMany) AddConstraints()

AddConstraints answers HasOneOrMany::addConstraints.

The second clause is not redundant. Without `whereNotNull`, a parent whose local key is null would match every child whose foreign key is null -- on engines where that comparison holds -- and produce a relation full of other people's orphans.

func (*HasOneOrMany) AddEagerConstraints

func (r *HasOneOrMany) AddEagerConstraints(models []Model) error

AddEagerConstraints answers HasOneOrMany::addEagerConstraints.

This is one half of what makes with() two queries instead of N: every parent's key goes into one `in (...)`, so the children of a hundred parents arrive in a single round trip.

func (*HasOneOrMany) Create

func (r *HasOneOrMany) Create(ctx context.Context, g auth.Grant, attributes map[string]any) (Model, error)

Create answers HasOneOrMany::create.

func (*HasOneOrMany) CreateMany

func (r *HasOneOrMany) CreateMany(ctx context.Context, g auth.Grant, records []map[string]any) ([]Model, error)

CreateMany answers HasOneOrMany::createMany.

func (*HasOneOrMany) CreateManyQuietly

func (r *HasOneOrMany) CreateManyQuietly(ctx context.Context, g auth.Grant, records []map[string]any) ([]Model, error)

CreateManyQuietly answers HasOneOrMany::createManyQuietly.

func (*HasOneOrMany) CreateOrFirst

func (r *HasOneOrMany) CreateOrFirst(ctx context.Context, g auth.Grant, attributes, values map[string]any) (Model, error)

CreateOrFirst answers HasOneOrMany::createOrFirst: create the row, and if somebody else created it first, read theirs.

It is the race FirstOrCreate cannot win on its own. Two requests that both find nothing both go on to insert, and one of them loses to the unique index; this catches exactly that loss and answers the row the winner wrote. Every other statement error is returned, because a NOT NULL violation answered with a silent select is a bug that looks like a missing record.

Two things the PHP does are not here, and neither is available to reach. withSavepointIfNeeded belongs to ManagesTransactions, which this package does not hold; and useWritePdo names a read/write split that this connection does not offer. The retry reads through the same connection the insert used, which is what useWritePdo is asking for in the first place.

func (*HasOneOrMany) CreateQuietly

func (r *HasOneOrMany) CreateQuietly(ctx context.Context, g auth.Grant, attributes map[string]any) (Model, error)

CreateQuietly answers HasOneOrMany::createQuietly.

func (*HasOneOrMany) FindOrNew

func (r *HasOneOrMany) FindOrNew(ctx context.Context, g auth.Grant, id any) (Model, error)

FindOrNew answers HasOneOrMany::findOrNew.

func (*HasOneOrMany) FirstOrCreate

func (r *HasOneOrMany) FirstOrCreate(ctx context.Context, g auth.Grant, attributes, values map[string]any) (Model, error)

FirstOrCreate answers HasOneOrMany::firstOrCreate.

func (*HasOneOrMany) FirstOrNew

func (r *HasOneOrMany) FirstOrNew(ctx context.Context, g auth.Grant, attributes, values map[string]any) (Model, error)

FirstOrNew answers HasOneOrMany::firstOrNew.

func (*HasOneOrMany) ForceCreate

func (r *HasOneOrMany) ForceCreate(ctx context.Context, g auth.Grant, attributes map[string]any) (Model, error)

ForceCreate answers HasOneOrMany::forceCreate: mass assignment allowed.

func (*HasOneOrMany) ForceCreateMany

func (r *HasOneOrMany) ForceCreateMany(ctx context.Context, g auth.Grant, records []map[string]any) ([]Model, error)

ForceCreateMany answers HasOneOrMany::forceCreateMany.

func (*HasOneOrMany) ForceCreateManyQuietly

func (r *HasOneOrMany) ForceCreateManyQuietly(ctx context.Context, g auth.Grant, records []map[string]any) ([]Model, error)

ForceCreateManyQuietly answers HasOneOrMany::forceCreateManyQuietly.

func (*HasOneOrMany) ForceCreateQuietly

func (r *HasOneOrMany) ForceCreateQuietly(ctx context.Context, g auth.Grant, attributes map[string]any) (Model, error)

ForceCreateQuietly answers HasOneOrMany::forceCreateQuietly.

func (*HasOneOrMany) GetExistenceCompareKey

func (r *HasOneOrMany) GetExistenceCompareKey() string

GetExistenceCompareKey answers HasOneOrMany::getExistenceCompareKey.

func (*HasOneOrMany) GetForeignKeyName

func (r *HasOneOrMany) GetForeignKeyName() string

GetForeignKeyName answers HasOneOrMany::getForeignKeyName: the plain column, with any table prefix cut off.

func (*HasOneOrMany) GetLocalKeyName

func (r *HasOneOrMany) GetLocalKeyName() string

GetLocalKeyName answers HasOneOrMany::getLocalKeyName.

func (*HasOneOrMany) GetParentKey

func (r *HasOneOrMany) GetParentKey() any

GetParentKey answers HasOneOrMany::getParentKey.

func (*HasOneOrMany) GetQualifiedForeignKeyName

func (r *HasOneOrMany) GetQualifiedForeignKeyName() string

GetQualifiedForeignKeyName answers HasOneOrMany::getQualifiedForeignKeyName.

func (*HasOneOrMany) GetQualifiedParentKeyName

func (r *HasOneOrMany) GetQualifiedParentKeyName() string

GetQualifiedParentKeyName answers HasOneOrMany::getQualifiedParentKeyName.

func (*HasOneOrMany) GetRelationExistenceQuery

func (r *HasOneOrMany) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers HasOneOrMany::getRelationExistenceQuery.

func (*HasOneOrMany) GetRelationExistenceQueryForSelfRelation

func (r *HasOneOrMany) GetRelationExistenceQueryForSelfRelation(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQueryForSelfRelation answers HasOneOrMany::getRelationExistenceQueryForSelfRelation.

func (*HasOneOrMany) Limit

func (r *HasOneOrMany) Limit(value int) *HasOneOrMany

Limit answers HasOneOrMany::limit.

The branch is the one that makes `->latest()->limit(3)` mean "three per parent" on an eager load: with no parent row to narrow to, a plain limit would take three rows from the whole result set, so it becomes a per-group limit instead.

func (*HasOneOrMany) Make

func (r *HasOneOrMany) Make(attributes map[string]any) Model

Make answers HasOneOrMany::make: an unsaved related model with the foreign key already set.

func (*HasOneOrMany) MakeMany

func (r *HasOneOrMany) MakeMany(records []map[string]any) []Model

MakeMany answers HasOneOrMany::makeMany.

func (*HasOneOrMany) MatchMany

func (r *HasOneOrMany) MatchMany(models []Model, results []Model, relation string) ([]Model, error)

MatchMany answers HasOneOrMany::matchMany.

func (*HasOneOrMany) MatchOne

func (r *HasOneOrMany) MatchOne(models []Model, results []Model, relation string) ([]Model, error)

MatchOne answers HasOneOrMany::matchOne.

func (*HasOneOrMany) Save

func (r *HasOneOrMany) Save(ctx context.Context, g auth.Grant, model Model) (Model, error)

Save answers HasOneOrMany::save.

func (*HasOneOrMany) SaveMany

func (r *HasOneOrMany) SaveMany(ctx context.Context, g auth.Grant, models []Model) ([]Model, error)

SaveMany answers HasOneOrMany::saveMany.

func (*HasOneOrMany) SaveManyQuietly

func (r *HasOneOrMany) SaveManyQuietly(ctx context.Context, g auth.Grant, models []Model) ([]Model, error)

SaveManyQuietly answers HasOneOrMany::saveManyQuietly.

func (*HasOneOrMany) SaveQuietly

func (r *HasOneOrMany) SaveQuietly(ctx context.Context, g auth.Grant, model Model) (Model, error)

SaveQuietly answers HasOneOrMany::saveQuietly.

func (*HasOneOrMany) SetForeignAttributesForCreate

func (r *HasOneOrMany) SetForeignAttributesForCreate(model Model)

SetForeignAttributesForCreate answers HasOneOrMany::setForeignAttributesForCreate.

func (*HasOneOrMany) Take

func (r *HasOneOrMany) Take(value int) *HasOneOrMany

Take answers HasOneOrMany::take.

func (*HasOneOrMany) UpdateOrCreate

func (r *HasOneOrMany) UpdateOrCreate(ctx context.Context, g auth.Grant, attributes, values map[string]any) (Model, error)

UpdateOrCreate answers HasOneOrMany::updateOrCreate.

func (*HasOneOrMany) Upsert

func (r *HasOneOrMany) Upsert(ctx context.Context, g auth.Grant, values []map[string]any, uniqueBy, update []string) (int64, error)

Upsert answers HasOneOrMany::upsert: insert the rows, updating the ones that collide, with the foreign key stamped on every one.

The stamping is the whole point of the override. Without it an upsert through a relation writes rows that belong to no parent, and they are invisible to the relation that wrote them.

The PHP's first line wraps a single row in an array so that upsert(['a' => 1]) and upsert([['a' => 1]]) both work. A Go signature says which one it takes, so that branch has nothing to decide and is gone -- one of the shapes PHP needs a run-time check for and Go gets from the type.

type HasOneOrManyThrough

type HasOneOrManyThrough struct {
	BaseRelation
	// contains filtered or unexported fields
}

HasOneOrManyThrough is a relation that reaches its rows through a third table -- a country's posts, through its users.

The intermediate table is joined rather than queried, so it is still two queries for an eager load and not three. What makes the eager load work is selecting the intermediate table's foreign key alongside the related row, under ThroughKey: without it the result set could not say which far parent each row came from, because the related table has no column that names it.

func NewHasOneOrManyThrough

func NewHasOneOrManyThrough(query Builder, farParent, throughParent Model, firstKey, secondKey, localKey, secondLocalKey string) HasOneOrManyThrough

NewHasOneOrManyThrough answers HasOneOrManyThrough::__construct.

func (*HasOneOrManyThrough) AddConstraints

func (r *HasOneOrManyThrough) AddConstraints()

AddConstraints answers HasOneOrManyThrough::addConstraints.

func (*HasOneOrManyThrough) AddEagerConstraints

func (r *HasOneOrManyThrough) AddEagerConstraints(models []Model) error

AddEagerConstraints answers HasOneOrManyThrough::addEagerConstraints.

func (*HasOneOrManyThrough) Chunk

func (r *HasOneOrManyThrough) Chunk(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool) (bool, error)

Chunk answers HasOneOrManyThrough::chunk: the results a page at a time, so a table that does not fit in memory does not have to.

The callback answers false to stop, as the PHP's does, and Chunk answers false when it was stopped rather than exhausted.

func (*HasOneOrManyThrough) ChunkById

func (r *HasOneOrManyThrough) ChunkById(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool, column, alias string) (bool, error)

ChunkById answers HasOneOrManyThrough::chunkById: pages taken by comparing against the last id seen rather than by an offset.

The empty column and alias take the PHP's defaults, which are the related model's qualified key and its key name.

func (*HasOneOrManyThrough) ChunkByIdDesc

func (r *HasOneOrManyThrough) ChunkByIdDesc(ctx context.Context, g auth.Grant, count int, callback func(results []Model, page int) bool, column, alias string) (bool, error)

ChunkByIdDesc answers HasOneOrManyThrough::chunkByIdDesc.

func (*HasOneOrManyThrough) Cursor

Cursor answers HasOneOrManyThrough::cursor: one statement, streamed.

func (*HasOneOrManyThrough) CursorPaginate

func (r *HasOneOrManyThrough) CursorPaginate(ctx context.Context, g auth.Grant, perPage int, cursor *pagination.Cursor, opts pagination.Options, columns ...any) (*pagination.CursorPaginator[Model], error)

CursorPaginate answers HasOneOrManyThrough::cursorPaginate.

func (*HasOneOrManyThrough) Each

func (r *HasOneOrManyThrough) Each(ctx context.Context, g auth.Grant, callback func(value Model, key int) bool, count int) (bool, error)

Each answers HasOneOrManyThrough::each: chunk, one row at a time.

func (*HasOneOrManyThrough) EachById

func (r *HasOneOrManyThrough) EachById(ctx context.Context, g auth.Grant, callback func(value Model, key int) bool, count int, column, alias string) (bool, error)

EachById answers HasOneOrManyThrough::eachById.

func (*HasOneOrManyThrough) First

func (r *HasOneOrManyThrough) First(ctx context.Context, g auth.Grant) (Model, error)

First answers the one-row read, with the same select as Get.

func (*HasOneOrManyThrough) Get

func (r *HasOneOrManyThrough) Get(ctx context.Context, g auth.Grant, columns ...any) ([]Model, error)

Get answers HasOneOrManyThrough::get: the select carries the intermediate key so that Match has something to key on.

func (*HasOneOrManyThrough) GetEager

func (r *HasOneOrManyThrough) GetEager(ctx context.Context, g auth.Grant) ([]Model, error)

GetEager answers Relation::getEager for a through relation.

func (*HasOneOrManyThrough) GetFarParent

func (r *HasOneOrManyThrough) GetFarParent() Model

GetParent answers Relation::getParent. For a through relation the parent of the relation is the intermediate model, and the far parent is what declared it -- the PHP keeps both, and so does this.

func (*HasOneOrManyThrough) GetFirstKeyName

func (r *HasOneOrManyThrough) GetFirstKeyName() string

GetFirstKeyName answers HasOneOrManyThrough::getFirstKeyName.

func (*HasOneOrManyThrough) GetForeignKeyName

func (r *HasOneOrManyThrough) GetForeignKeyName() string

GetForeignKeyName answers HasOneOrManyThrough::getForeignKeyName.

func (*HasOneOrManyThrough) GetLocalKeyName

func (r *HasOneOrManyThrough) GetLocalKeyName() string

GetLocalKeyName answers HasOneOrManyThrough::getLocalKeyName.

func (*HasOneOrManyThrough) GetParentKey

func (r *HasOneOrManyThrough) GetParentKey() any

GetParentKey answers the far parent's local key, which is what a through relation is narrowed by.

func (*HasOneOrManyThrough) GetQualifiedFarKeyName

func (r *HasOneOrManyThrough) GetQualifiedFarKeyName() string

GetQualifiedFarKeyName answers HasOneOrManyThrough::getQualifiedFarKeyName.

func (*HasOneOrManyThrough) GetQualifiedFirstKeyName

func (r *HasOneOrManyThrough) GetQualifiedFirstKeyName() string

GetQualifiedFirstKeyName answers HasOneOrManyThrough::getQualifiedFirstKeyName.

func (*HasOneOrManyThrough) GetQualifiedForeignKeyName

func (r *HasOneOrManyThrough) GetQualifiedForeignKeyName() string

GetQualifiedForeignKeyName answers HasOneOrManyThrough::getQualifiedForeignKeyName.

func (*HasOneOrManyThrough) GetQualifiedLocalKeyName

func (r *HasOneOrManyThrough) GetQualifiedLocalKeyName() string

GetQualifiedLocalKeyName answers HasOneOrManyThrough::getQualifiedLocalKeyName.

func (*HasOneOrManyThrough) GetQualifiedParentKeyName

func (r *HasOneOrManyThrough) GetQualifiedParentKeyName() string

GetQualifiedParentKeyName answers HasOneOrManyThrough::getQualifiedParentKeyName.

func (*HasOneOrManyThrough) GetRelationExistenceQuery

func (r *HasOneOrManyThrough) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers HasOneOrManyThrough::getRelationExistenceQuery.

func (*HasOneOrManyThrough) GetRelationExistenceQueryForThroughSelfRelation

func (r *HasOneOrManyThrough) GetRelationExistenceQueryForThroughSelfRelation(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQueryForThroughSelfRelation answers HasOneOrManyThrough::getRelationExistenceQueryForThroughSelfRelation: the existence subquery when the intermediate table is the same table the outer query already names.

The join has to alias the intermediate table, or the two references to it collapse into one and the subquery correlates with itself. The alias is GetRelationCountHash, which is what every self-join in this package uses.

func (*HasOneOrManyThrough) GetSecondLocalKeyName

func (r *HasOneOrManyThrough) GetSecondLocalKeyName() string

GetSecondLocalKeyName answers HasOneOrManyThrough::getSecondLocalKeyName.

func (*HasOneOrManyThrough) GetThroughParent

func (r *HasOneOrManyThrough) GetThroughParent() Model

GetThroughParent answers HasOneOrManyThrough::$throughParent.

func (*HasOneOrManyThrough) Lazy

func (r *HasOneOrManyThrough) Lazy(ctx context.Context, g auth.Grant, chunkSize int) iter.Seq2[Model, error]

Lazy answers HasOneOrManyThrough::lazy: the chunked walk as a sequence.

func (*HasOneOrManyThrough) LazyById

func (r *HasOneOrManyThrough) LazyById(ctx context.Context, g auth.Grant, chunkSize int, column, alias string) iter.Seq2[Model, error]

LazyById answers HasOneOrManyThrough::lazyById.

func (*HasOneOrManyThrough) LazyByIdDesc

func (r *HasOneOrManyThrough) LazyByIdDesc(ctx context.Context, g auth.Grant, chunkSize int, column, alias string) iter.Seq2[Model, error]

LazyByIdDesc answers HasOneOrManyThrough::lazyByIdDesc.

func (*HasOneOrManyThrough) Limit

func (r *HasOneOrManyThrough) Limit(value int) *HasOneOrManyThrough

Limit answers HasOneOrManyThrough::limit.

func (*HasOneOrManyThrough) Paginate

func (r *HasOneOrManyThrough) Paginate(ctx context.Context, g auth.Grant, perPage, page int, opts pagination.Options, columns ...any) (*pagination.LengthAwarePaginator[Model], error)

Paginate answers HasOneOrManyThrough::paginate.

func (*HasOneOrManyThrough) SimplePaginate

func (r *HasOneOrManyThrough) SimplePaginate(ctx context.Context, g auth.Grant, perPage, page int, opts pagination.Options, columns ...any) (*pagination.Paginator[Model], error)

SimplePaginate answers HasOneOrManyThrough::simplePaginate.

func (*HasOneOrManyThrough) Take

Take answers HasOneOrManyThrough::take.

func (*HasOneOrManyThrough) ThroughParentSoftDeletes

func (r *HasOneOrManyThrough) ThroughParentSoftDeletes() bool

ThroughParentSoftDeletes answers HasOneOrManyThrough::throughParentSoftDeletes.

func (*HasOneOrManyThrough) WithTrashedParents

func (r *HasOneOrManyThrough) WithTrashedParents() *HasOneOrManyThrough

WithTrashedParents answers HasOneOrManyThrough::withTrashedParents: the rows whose intermediate parent was soft deleted come back too.

PHP removes the 'SoftDeletableHasManyThrough' global scope from the query. There is no scope registry on the builder a relation holds here, so the filter is added by constrainThroughParents on the way to the database and this turns it off. The name and the effect are the PHP's; the mechanism is the one this collection has.

type HasOneThrough

HasOneThrough is the singular end of the same join HasManyThrough makes: a mechanic's car owner, through the car.

It is HasManyThrough narrowed to the first row. What it adds is a default: where the many form answers an empty slice, this one answers whatever WithDefault was given -- so a caller reads fields off the result without testing for nil first. With no default configured the miss is still nil.

It also compares related models, which is what makes Is and IsNot work against a relation that has to be resolved before it can be compared.

func NewHasOneThrough

func NewHasOneThrough(query Builder, farParent, throughParent Model, firstKey, secondKey, localKey, secondLocalKey string) *HasOneThrough

NewHasOneThrough builds the relation and narrows it to its parent.

func NewHasOneThroughUnconstrained

func NewHasOneThroughUnconstrained(query Builder, farParent, throughParent Model, firstKey, secondKey, localKey, secondLocalKey string) *HasOneThrough

NewHasOneThroughUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*HasOneThrough) GetResults

func (r *HasOneThrough) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers HasOneThrough::getResults.

func (*HasOneThrough) InitRelation

func (r *HasOneThrough) InitRelation(models []Model, relation string) []Model

InitRelation answers HasOneThrough::initRelation.

func (*HasOneThrough) Match

func (r *HasOneThrough) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers HasOneThrough::match.

type Model

type Model = concerns.Model

Model is what a relation asks of a model.

It is an alias rather than a declaration: the type is defined in relations/concerns, because Go forbids a subpackage from importing its parent and the traits in there need the same one. Aliasing it back means relations.Model is that type, not a second one that looks like it.

func CreateModelByType

func CreateModelByType(alias string) (Model, error)

CreateModelByType answers MorphTo::createModelByType together with Model::getActualClassNameForMorph.

It carries an error where the PHP would either instantiate a class by name or throw: an alias nobody registered is a row this process cannot read, and the message says which alias and what is registered, because the answer is always "add it to the morph map".

func EagerLoadRelation

func EagerLoadRelation(ctx context.Context, g auth.Grant, models []Model, name string, relation Relation, constraints func(Relation)) ([]Model, error)

EagerLoadRelation loads one relation for a batch of parents.

It is four lines and it is the whole of what eager loading buys:

relation.AddEagerConstraints(models)  // one `in (...)` for every parent
constraints(relation)                 // the caller's extra filters
relation.InitRelation(models, name)   // every parent starts non-nil
relation.Match(models, eager, name)   // distributed in memory

It lives in this package rather than on the builder because the four methods it calls are the four this package exists to implement, and because a reader asking "what does eager loading actually do" should find the answer next to them. The typed builder calls it; it is not a second way to load a relation.

The count, which is the only measurement that matters

For a hundred parents, this is 1 query. Reading the relation off each parent instead is 100. That is the whole subject, and it is why the test for this counts the statements the connection was asked for rather than checking that the values came out right -- values come out right either way, which is exactly why an N+1 survives code review.

MorphTo is the exception and it is inherent: its parents point at several tables, so it is one query per distinct type in the batch. Its GetEager does its own matching and Match answers the parents unchanged, which is why the order below is GetEager after InitRelation rather than before.

type MorphMany

type MorphMany struct {
	MorphOneOrMany
}

MorphMany is the comments of a post, on a table that also holds the comments of a video.

func NewMorphMany

func NewMorphMany(query Builder, parent Model, typ, id, localKey string) *MorphMany

NewMorphMany builds the relation and narrows it to its parent.

func NewMorphManyUnconstrained

func NewMorphManyUnconstrained(query Builder, parent Model, typ, id, localKey string) *MorphMany

NewMorphManyUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*MorphMany) GetResults

func (r *MorphMany) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers MorphMany::getResults.

func (*MorphMany) InitRelation

func (r *MorphMany) InitRelation(models []Model, relation string) []Model

InitRelation answers MorphMany::initRelation.

func (*MorphMany) Match

func (r *MorphMany) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers MorphMany::match.

func (*MorphMany) One

func (r *MorphMany) One() *MorphOne

One answers MorphMany::one.

type MorphOne

MorphOne is one row on a polymorphic table: the image of a post or of a user.

func NewMorphOne

func NewMorphOne(query Builder, parent Model, typ, id, localKey string) *MorphOne

NewMorphOne builds the relation and narrows it to its parent.

func NewMorphOneUnconstrained

func NewMorphOneUnconstrained(query Builder, parent Model, typ, id, localKey string) *MorphOne

NewMorphOneUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*MorphOne) AddConstraints

func (r *MorphOne) AddConstraints()

AddConstraints answers MorphOneOrMany::addConstraints, redeclared so that it reaches this type's GetRelationQuery.

func (*MorphOne) AddOneOfManyJoinSubQueryConstraints

func (r *MorphOne) AddOneOfManyJoinSubQueryConstraints(join *query.JoinClause)

AddOneOfManyJoinSubQueryConstraints answers the method of the same name.

func (*MorphOne) AddOneOfManySubQueryConstraints

func (r *MorphOne) AddOneOfManySubQueryConstraints(q Builder, column, aggregate string)

AddOneOfManySubQueryConstraints answers the method of the same name: the subquery has to carry the type column too, or the join would pick the newest comment of any commentable with that id.

func (*MorphOne) GetOneOfManySubQuerySelectColumns

func (r *MorphOne) GetOneOfManySubQuerySelectColumns() []any

GetOneOfManySubQuerySelectColumns answers the method of the same name.

func (*MorphOne) GetRelationExistenceQuery

func (r *MorphOne) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers MorphOne::getRelationExistenceQuery.

func (*MorphOne) GetRelationQuery

func (r *MorphOne) GetRelationQuery() Builder

GetRelationQuery answers CanBeOneOfMany::getRelationQuery.

func (*MorphOne) GetResults

func (r *MorphOne) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers MorphOne::getResults.

func (*MorphOne) InitRelation

func (r *MorphOne) InitRelation(models []Model, relation string) []Model

InitRelation answers MorphOne::initRelation.

func (*MorphOne) Match

func (r *MorphOne) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers MorphOne::match.

type MorphOneOrMany

type MorphOneOrMany struct {
	HasOneOrMany
	// contains filtered or unexported fields
}

MorphOneOrMany is the shared body of MorphOne and MorphMany.

A has-many with a second column: the foreign key says which row, the morph type says which table it was on. The type column is what makes one comments table serve posts and videos both, and it is why the constraint is two clauses rather than one -- an id of 7 exists on every table.

func NewMorphOneOrMany

func NewMorphOneOrMany(query Builder, parent Model, typ, id, localKey string) MorphOneOrMany

NewMorphOneOrMany answers MorphOneOrMany::__construct.

The morph class is the parent's alias, and it is the value written into the type column. See morphmap.go for why it is an alias here and a class name there.

func (*MorphOneOrMany) AddConstraints

func (r *MorphOneOrMany) AddConstraints()

AddConstraints answers MorphOneOrMany::addConstraints.

func (*MorphOneOrMany) AddEagerConstraints

func (r *MorphOneOrMany) AddEagerConstraints(models []Model) error

AddEagerConstraints answers MorphOneOrMany::addEagerConstraints.

The type clause goes on after the keys, exactly as in the PHP: without it the eager load would pull the comments of the video whose id happens to match the post's -- one query, wrong rows, and every parent looking plausible.

func (*MorphOneOrMany) Create

func (r *MorphOneOrMany) Create(ctx context.Context, g auth.Grant, attributes map[string]any) (Model, error)

Create answers HasOneOrMany::create with the morph type set.

func (*MorphOneOrMany) ForceCreate

func (r *MorphOneOrMany) ForceCreate(ctx context.Context, g auth.Grant, attributes map[string]any) (Model, error)

ForceCreate answers MorphOneOrMany::forceCreate.

func (*MorphOneOrMany) GetMorphClass

func (r *MorphOneOrMany) GetMorphClass() string

GetMorphClass answers MorphOneOrMany::getMorphClass.

func (*MorphOneOrMany) GetMorphType

func (r *MorphOneOrMany) GetMorphType() string

GetMorphType answers MorphOneOrMany::getMorphType: the plain column.

func (*MorphOneOrMany) GetQualifiedMorphType

func (r *MorphOneOrMany) GetQualifiedMorphType() string

GetQualifiedMorphType answers MorphOneOrMany::getQualifiedMorphType.

func (*MorphOneOrMany) GetRelationExistenceQuery

func (r *MorphOneOrMany) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers MorphOneOrMany::getRelationExistenceQuery.

func (*MorphOneOrMany) Make

func (r *MorphOneOrMany) Make(attributes map[string]any) Model

Make answers HasOneOrMany::make with the morph type set.

func (*MorphOneOrMany) PossibleInverseRelations

func (r *MorphOneOrMany) PossibleInverseRelations() []string

PossibleInverseRelations answers MorphOneOrMany::getPossibleInverseRelations: commentable_type suggests the relation is called commentable.

func (*MorphOneOrMany) SetForeignAttributesForCreate

func (r *MorphOneOrMany) SetForeignAttributesForCreate(model Model)

SetForeignAttributesForCreate answers MorphOneOrMany::setForeignAttributesForCreate.

func (*MorphOneOrMany) Upsert

func (r *MorphOneOrMany) Upsert(ctx context.Context, g auth.Grant, values []map[string]any, uniqueBy, update []string) (int64, error)

Upsert answers MorphOneOrMany::upsert: HasOneOrMany's upsert with the morph type stamped on every row as well as the foreign key.

Both columns are needed and for the same reason. A comments table shared by posts and videos keys a comment by the pair, so a row written with the id and not the type belongs to whichever of the two happens to share the number -- which is a row appearing under the wrong parent, not a row appearing nowhere.

type MorphPivot

type MorphPivot struct {
	Pivot
	// contains filtered or unexported fields
}

MorphPivot is a pivot row on a table shared by several parent types, so every statement it writes carries the type as well as the keys.

func MorphPivotFromAttributes

func MorphPivotFromAttributes(parent Model, attributes map[string]any, table string, exists bool) *MorphPivot

MorphPivotFromAttributes answers MorphPivot's inherited fromAttributes.

func (*MorphPivot) Delete

func (p *MorphPivot) Delete(ctx context.Context, g auth.Grant) (int64, error)

Delete answers MorphPivot::delete: the type column narrows the delete, or a post's tag row would be removed by a video that shares the tag id.

func (*MorphPivot) GetMorphType

func (p *MorphPivot) GetMorphType() string

GetMorphType answers MorphPivot::getMorphType.

func (*MorphPivot) GetQueueableID

func (p *MorphPivot) GetQueueableID() any

GetQueueableID answers MorphPivot::getQueueableId: the pair of keys of AsPivot, plus the type column and the alias it holds.

Without the type half, two rows of the same intermediate table -- a post's tag and a video's tag, same tag id -- would serialize to the same identity and a queued job would restore the wrong one.

func (*MorphPivot) NewQueryForRestoration

func (p *MorphPivot) NewQueryForRestoration(g auth.Grant, ids ...any) (Builder, error)

NewQueryForRestoration answers MorphPivot::newQueryForRestoration.

The PHP writes this body out again rather than reaching AsPivot's, because the identifier carries a third pair -- the type column and the alias it holds -- and so the query needs a third clause. This does the same, for the same reason: without the type half, a post's tag row and a video's tag row share an identity and a queued job restores whichever the database returns first.

The error cases are the ones AsPivot documents: an empty list, a pivot with no query, and an identifier that GetQueueableID did not write. So is the Grant, and so is what it fixes: a morph pivot restored without one read every customer's rows of a table that is shared twice over -- by tenant and by parent type.

func (*MorphPivot) SetKeysForSaveQuery

func (p *MorphPivot) SetKeysForSaveQuery(q Builder, model Model) Builder

SetKeysForSaveQuery answers MorphPivot::setKeysForSaveQuery.

func (*MorphPivot) SetMorphClass

func (p *MorphPivot) SetMorphClass(morphClass string) *MorphPivot

SetMorphClass answers MorphPivot::setMorphClass.

func (*MorphPivot) SetMorphType

func (p *MorphPivot) SetMorphType(morphType string) *MorphPivot

SetMorphType answers MorphPivot::setMorphType.

type MorphTo

type MorphTo struct {
	BelongsTo
	// contains filtered or unexported fields
}

MorphTo is the inverse of a polymorphic relation, where the row says both which table and which row.

It is the one relation whose eager load is not two queries, and it cannot be: the parents point at several tables, and no dialect selects from a table named by a column. It is one query per distinct type in the batch -- three queries for a feed of posts, videos and links, not one per row -- and that distinction is the whole of what eager loading buys here.

func NewMorphTo

func NewMorphTo(query Builder, parent Model, foreignKey, ownerKey, typ, relation string) *MorphTo

NewMorphTo answers MorphTo::__construct.

ownerKey may be empty, and empty means "the related model's own key name", which is not known until the type column is read.

func NewMorphToUnconstrained

func NewMorphToUnconstrained(query Builder, parent Model, foreignKey, ownerKey, typ, relation string) *MorphTo

NewMorphToUnconstrained builds the relation without narrowing it to one parent, which is what an eager load needs: the batch resolves the type first and then reads every owner of that type in one statement.

func (*MorphTo) AddEagerConstraints

func (r *MorphTo) AddEagerConstraints(models []Model) error

AddEagerConstraints answers MorphTo::addEagerConstraints.

It adds no clause to any query. There is no single query to constrain: the work is bucketing the parents by type, and the queries are built one per bucket in GetEager.

func (*MorphTo) Associate

func (r *MorphTo) Associate(model any) Model

Associate answers MorphTo::associate: it writes both columns, the key and the type.

func (*MorphTo) Constrain

func (r *MorphTo) Constrain(callbacks map[string]func(Builder)) *MorphTo

Constrain answers MorphTo::constrain: a callback per morph type, applied to that type's query.

func (*MorphTo) Dissociate

func (r *MorphTo) Dissociate() Model

Dissociate answers MorphTo::dissociate.

func (*MorphTo) GetDictionary

func (r *MorphTo) GetDictionary() map[string]map[string][]Model

GetDictionary answers MorphTo::getDictionary.

func (*MorphTo) GetEager

func (r *MorphTo) GetEager(ctx context.Context, g auth.Grant) ([]Model, error)

GetEager answers MorphTo::getEager: one query per morph type, matched as it goes, and the parents come back out.

The types are visited in sorted order. PHP visits them in the order the dictionary was built; a Go map has none, and a query log that shuffles between identical requests is a log nobody can diff.

func (*MorphTo) GetMorphType

func (r *MorphTo) GetMorphType() string

GetMorphType answers MorphTo::getMorphType.

func (*MorphTo) GetMorphableEagerLoadCounts

func (r *MorphTo) GetMorphableEagerLoadCounts() map[string][]string

GetMorphableEagerLoadCounts answers the map morphWithCount built.

func (*MorphTo) GetMorphableEagerLoads

func (r *MorphTo) GetMorphableEagerLoads() map[string][]string

GetMorphableEagerLoads answers the map morphWith built, for the builder that runs the nested loads.

func (*MorphTo) GetQualifiedOwnerKeyName

func (r *MorphTo) GetQualifiedOwnerKeyName() string

GetQualifiedOwnerKeyName answers MorphTo::getQualifiedOwnerKeyName: empty when there is no owner key, because there is no table to qualify it with until the type is read.

func (*MorphTo) GetResults

func (r *MorphTo) GetResults(ctx context.Context, g auth.Grant) (any, error)

GetResults answers MorphTo::getResults.

It exists because the inherited one is wrong here, and wrong in the way that is hardest to see: BelongsTo::getResults reads the query this relation was built with, and for a MorphTo that query is over a placeholder model handed to the constructor -- one that only carries a connection to start from. So a lazy read selected from whatever table the placeholder named, with no morph type in the where clause. The eager path never had the problem, because it resolves the type first; the lazy path had no test.

The type comes off the child, like everything else about a morph, and a child that names no type has no owner rather than an owner of the wrong kind.

func (*MorphTo) Match

func (r *MorphTo) Match(models []Model, results []Model, relation string) ([]Model, error)

Match answers MorphTo::match: nothing, because GetEager already matched.

func (*MorphTo) MorphWith

func (r *MorphTo) MorphWith(with map[string][]string) *MorphTo

MorphWith answers MorphTo::morphWith: nested eager loads, per morph type.

func (*MorphTo) MorphWithCount

func (r *MorphTo) MorphWithCount(withCount map[string][]string) *MorphTo

MorphWithCount answers MorphTo::morphWithCount: relationship counts to load per morph type, alongside the relations morphWith names.

func (*MorphTo) OnlyTrashed

func (r *MorphTo) OnlyTrashed() *MorphTo

OnlyTrashed answers MorphTo::onlyTrashed: only soft deleted rows come back.

func (*MorphTo) WithTrashed

func (r *MorphTo) WithTrashed() *MorphTo

WithTrashed answers MorphTo::withTrashed: soft deleted rows come back too.

A MorphTo cannot answer this when it is called. The related model is named by a column, so which types are involved -- and which of them are even soft deletable -- is not known until the type column has been read. PHP records the call in $macroBuffer and replays it on each per-type builder; this records it in a field and applies it in getResultsByType, at the same moment and for the same reason.

A type that is not soft deletable is left alone rather than refused, which is what the PHP's hasMacro check does. One feed of posts and videos where only posts are soft deletable is the ordinary case, not a mistake.

func (*MorphTo) WithoutTrashed

func (r *MorphTo) WithoutTrashed() *MorphTo

WithoutTrashed answers MorphTo::withoutTrashed: soft deleted rows are excluded.

The clause is added rather than a global scope being left in place. A relation here holds no scope registry -- see WithTrashedParents on HasOneOrManyThrough for the same trade -- so nothing filters deleted rows unless asked, and this is the asking.

type MorphToMany

type MorphToMany struct {
	BelongsToMany
	// contains filtered or unexported fields
}

MorphToMany is a many-to-many whose intermediate table serves several parent types -- taggables holding the tags of posts and of videos both.

Every statement it writes carries the type as well as the two keys, and every read filters on it. Without that, a post and a video sharing an id share their tags.

func NewMorphToMany

func NewMorphToMany(q Builder, parent Model, name, table, foreignPivotKey, relatedPivotKey, parentKey, relatedKey, relationName string, inverse bool) *MorphToMany

NewMorphToMany answers MorphToMany::__construct.

func NewMorphToManyUnconstrained added in v0.18.0

func NewMorphToManyUnconstrained(q Builder, parent Model, name, table, foreignPivotKey, relatedPivotKey, parentKey, relatedKey, relationName string, inverse bool) *MorphToMany

NewMorphToManyUnconstrained builds the relation without narrowing it to one parent.

It exists because the constraint used to be switched off through a process-wide flag, which meant a relation built on another goroutine while the flag was down came back unconstrained -- every parent's children, in a well-formed query nobody could tell apart from the right one. The call site says which it wants now, and there is no flag to leave down.

func (*MorphToMany) AddEagerConstraints

func (r *MorphToMany) AddEagerConstraints(models []Model) error

AddEagerConstraints answers MorphToMany::addEagerConstraints.

func (*MorphToMany) GetInverse

func (r *MorphToMany) GetInverse() bool

GetInverse answers MorphToMany::getInverse: whether this is the morphedByMany side, which is what decides whose alias goes in the type column.

func (*MorphToMany) GetMorphClass

func (r *MorphToMany) GetMorphClass() string

GetMorphClass answers MorphToMany::getMorphClass.

func (*MorphToMany) GetMorphType

func (r *MorphToMany) GetMorphType() string

GetMorphType answers MorphToMany::getMorphType.

func (*MorphToMany) GetQualifiedMorphTypeName

func (r *MorphToMany) GetQualifiedMorphTypeName() string

GetQualifiedMorphTypeName answers MorphToMany::getQualifiedMorphTypeName.

func (*MorphToMany) GetRelationExistenceQuery

func (r *MorphToMany) GetRelationExistenceQuery(q Builder, parentQuery Builder, columns ...any) Builder

GetRelationExistenceQuery answers MorphToMany::getRelationExistenceQuery.

func (*MorphToMany) NewPivot

func (r *MorphToMany) NewPivot(attributes map[string]any, exists bool) Model

NewPivot answers MorphToMany::newPivot.

func (*MorphToMany) NewPivotQuery

func (r *MorphToMany) NewPivotQuery(g auth.Grant) (*query.Builder, error)

NewPivotQuery answers MorphToMany::newPivotQuery.

type Pivot

type Pivot struct {
	concerns.AsPivot

	// NewQueryFor is how a pivot reaches the database. The relation that built
	// it supplies the factory, because a pivot has no connection of its own to
	// build a query from.
	NewQueryFor func(table string) Builder
	// contains filtered or unexported fields
}

Pivot is the row of an intermediate table, as a model.

There is no model to embed here without importing the model package, which imports this one -- so the model surface is implemented on the type, and it is the small surface a pivot actually needs: attributes, the two keys, a table and a save. What it is not is a second model implementation for general use; nothing but a pivot should embed it.

It does not increment and it guards nothing: a pivot row has no id of its own to increment, and it is written by the relation rather than by mass assignment from a request.

func FromAttributes

func FromAttributes(parent Model, attributes map[string]any, table string, exists bool) *Pivot

FromAttributes answers AsPivot::fromAttributes.

A static in PHP that calls `new static`. Go has no late static binding, so there is one function per pivot type rather than one method that knows which type it was called on -- MorphPivotFromAttributes is the other.

func FromRawAttributes

func FromRawAttributes(parent Model, attributes map[string]any, table string, exists bool) *Pivot

FromRawAttributes answers AsPivot::fromRawAttributes.

func (*Pivot) Delete

func (p *Pivot) Delete(ctx context.Context, g auth.Grant) (int64, error)

Delete answers AsPivot::delete.

func (*Pivot) Exists

func (p *Pivot) Exists() bool

Exists answers Model::$exists.

func (*Pivot) Fill

func (p *Pivot) Fill(attributes map[string]any)

Fill answers Model::fill. A pivot's $guarded is empty, so it is ForceFill.

func (*Pivot) ForceFill

func (p *Pivot) ForceFill(attributes map[string]any)

ForceFill answers Model::forceFill.

func (*Pivot) FreshTimestamp

func (p *Pivot) FreshTimestamp() time.Time

FreshTimestamp answers Model::freshTimestamp.

func (*Pivot) GetAttribute

func (p *Pivot) GetAttribute(key string) any

GetAttribute answers Model::getAttribute.

func (*Pivot) GetAttributes

func (p *Pivot) GetAttributes() map[string]any

GetAttributes answers Model::getAttributes.

func (*Pivot) GetForeignKey

func (p *Pivot) GetForeignKey() string

GetForeignKey answers Model::getForeignKey.

func (*Pivot) GetKey

func (p *Pivot) GetKey() any

GetKey answers Model::getKey.

func (*Pivot) GetKeyName

func (p *Pivot) GetKeyName() string

GetKeyName answers Model::getKeyName.

func (*Pivot) GetKeyType

func (p *Pivot) GetKeyType() string

GetKeyType answers Model::getKeyType.

func (*Pivot) GetMorphClass

func (p *Pivot) GetMorphClass() string

GetMorphClass answers Model::getMorphClass: the table, because a pivot is not something a morph column ever points at.

func (*Pivot) GetOriginal

func (p *Pivot) GetOriginal(key string) any

GetOriginal answers Model::getOriginal, which setKeysForSelectQuery reads so that a changed key still updates the row it came from.

func (*Pivot) GetQueueableID

func (p *Pivot) GetQueueableID() any

GetQueueableID answers AsPivot::getQueueableId for a pivot row.

PHP's trait reads $this->attributes directly; the concern here holds no attributes, so it is given the row. This is the no-argument method a caller reaches, and it hands itself over.

func (*Pivot) GetRelation

func (p *Pivot) GetRelation(relation string) (any, bool)

GetRelation answers Model::getRelation.

func (*Pivot) GetTable

func (p *Pivot) GetTable() string

GetTable answers AsPivot::getTable.

func (*Pivot) IsRelation

func (p *Pivot) IsRelation(key string) bool

IsRelation answers Model::isRelation.

func (*Pivot) NewInstance

func (p *Pivot) NewInstance(attributes map[string]any) Model

NewInstance answers Model::newInstance.

func (*Pivot) NewQuery

func (p *Pivot) NewQuery() Builder

NewQuery answers Model::newQuery.

func (*Pivot) NewQueryForRestoration

func (p *Pivot) NewQueryForRestoration(g auth.Grant, ids ...any) (Builder, error)

NewQueryForRestoration answers AsPivot::newQueryForRestoration for a pivot row: the query that finds again what GetQueueableID wrote down.

Like GetQueueableID above, this is the no-argument-model form a caller reaches, handing itself to the concern that holds no attributes.

The Grant is what it used to be missing, and what AsPivot's namesake explains: the restoration read a shared table with no tenant on it.

func (*Pivot) QualifyColumn

func (p *Pivot) QualifyColumn(column string) string

QualifyColumn answers Model::qualifyColumn.

func (*Pivot) RelationLoaded

func (p *Pivot) RelationLoaded(relation string) bool

RelationLoaded answers Model::relationLoaded.

func (*Pivot) Save

func (p *Pivot) Save(ctx context.Context, g auth.Grant) error

Save answers Model::save for a pivot row.

The interesting half is which row it writes to. A pivot table usually has no id, so an update cannot be keyed by one; setKeysForSaveQuery keys it by the pair of foreign keys, taken from the original attributes. Without that, an update after changing one of the keys would write to a row that does not exist and report zero rows affected -- silently.

func (*Pivot) SetAttribute

func (p *Pivot) SetAttribute(key string, value any)

SetAttribute answers Model::setAttribute.

func (*Pivot) SetKeysForSaveQuery

func (p *Pivot) SetKeysForSaveQuery(q Builder, model Model) Builder

SetKeysForSaveQuery answers AsPivot::setKeysForSaveQuery, with the receiver the trait cannot reach in Go.

func (*Pivot) SetRawAttributes

func (p *Pivot) SetRawAttributes(attributes map[string]any, sync bool)

SetRawAttributes answers Model::setRawAttributes.

func (*Pivot) SetRelation

func (p *Pivot) SetRelation(relation string, value any)

SetRelation answers Model::setRelation.

func (*Pivot) SetTable

func (p *Pivot) SetTable(table string)

SetTable answers Model::setTable.

func (*Pivot) SyncOriginal

func (p *Pivot) SyncOriginal()

SyncOriginal answers Model::syncOriginal.

func (*Pivot) Touch

func (p *Pivot) Touch(ctx context.Context, g auth.Grant) error

Touch answers Model::touch.

func (*Pivot) Touches

func (p *Pivot) Touches(relation string) bool

Touches answers Model::touches.

func (*Pivot) UnsetAttribute

func (p *Pivot) UnsetAttribute(key string)

UnsetAttribute answers unset($pivot->$key).

func (*Pivot) UnsetRelation

func (p *Pivot) UnsetRelation(relation string)

UnsetRelation answers Model::unsetRelation.

func (*Pivot) UsesTimestamps

func (p *Pivot) UsesTimestamps() bool

UsesTimestamps answers Model::usesTimestamps.

func (*Pivot) WasRecentlyCreated

func (p *Pivot) WasRecentlyCreated() bool

WasRecentlyCreated answers Model::$wasRecentlyCreated.

func (*Pivot) WithoutEvents

func (p *Pivot) WithoutEvents(callback func() error) error

WithoutEvents answers Model::withoutEvents. A pivot fires none, so it runs the callback.

type PivotFactory

type PivotFactory func(parent Model, attributes map[string]any, table string, exists bool) Model

PivotFactory builds the model a custom pivot class would be in PHP. using() takes one.

type Relation

type Relation interface {
	// AddConstraints answers Relation::addConstraints: the where clauses that
	// narrow the relation to one parent.
	AddConstraints()

	// AddEagerConstraints answers Relation::addEagerConstraints: the same
	// narrowing, widened to every parent at once. This is the method that
	// replaces N queries with one.
	AddEagerConstraints(models []Model) error

	// InitRelation answers Relation::initRelation: it seeds every parent with
	// an empty value, so that a parent with no children answers an empty
	// collection rather than a lazy load.
	InitRelation(models []Model, relation string) []Model

	// Match answers Relation::match: it distributes one flat result set over
	// the parents it belongs to, in memory, with no further queries.
	Match(models []Model, results []Model, relation string) ([]Model, error)

	// GetResults answers Relation::getResults: the relation as the developer
	// reads it -- one model for a has-one, many for a has-many.
	GetResults(ctx context.Context, g auth.Grant) (any, error)

	// GetEager answers Relation::getEager: the results for an eager load.
	GetEager(ctx context.Context, g auth.Grant) ([]Model, error)

	// GetQuery answers Relation::getQuery.
	GetQuery() Builder

	// GetParent answers Relation::getParent.
	GetParent() Model

	// GetRelated answers Relation::getRelated.
	GetRelated() Model
}

Relation is the contract every relation type implements.

The contract is here and the shared body is BaseRelation, the same division the query package makes between Grammar and BaseGrammar.

The four methods that matter are AddEagerConstraints, InitRelation, Match and GetEager. They are what turns N+1 queries into two, and they are worth reading in that order: one query for the parents, one for every child of every parent, and a dictionary in memory to put them together.

type SoftDeletableThrough

type SoftDeletableThrough interface {
	// IsSoftDeletable answers Model::isSoftDeletable.
	IsSoftDeletable() bool

	// GetDeletedAtColumn answers SoftDeletes::getDeletedAtColumn.
	GetDeletedAtColumn() string
}

SoftDeletableThrough is what ThroughParentSoftDeletes asks of the intermediate model.

PHP writes $this->throughParent::isSoftDeletable(), a static call on whatever class the model happens to be. Go has no late static binding and the Model interface a relation works against says nothing about soft deletes -- so the question is an optional interface, and a through parent that does not implement it is simply not soft deletable.

Directories

Path Synopsis
Package concerns holds the shared halves the sixteen relation types are assembled from: AsPivot, CanBeOneOfMany, ComparesRelatedModels, InteractsWithDictionary, InteractsWithPivotTable, SupportsDefaultModels and SupportsInverseRelations.
Package concerns holds the shared halves the sixteen relation types are assembled from: AsPivot, CanBeOneOfMany, ComparesRelatedModels, InteractsWithDictionary, InteractsWithPivotTable, SupportsDefaultModels and SupportsInverseRelations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL