Skip to main content

Subqueries, sets, and CTEs

A query can sit inside another one as a list of values, a single value, or a table. Global filters and soft deletes still apply inside it.

A list of values​

ant.SubOf turns a query and a column into something InSub can use.

authors := ant.SubOf(db.Posts.Where(ant.Eq(&P.Published, true)), &P.UserID)

wrote, err := db.Users.Where(ant.InSub(&U.ID, authors)).All(ctx)
quiet, err := db.Users.Where(ant.NotInSub(&U.ID, authors)).All(ctx)

Exists​

A subquery can refer to the outer row with EqCol.

theirs := db.Posts.Where(ant.EqCol(&P.UserID, &U.ID))

wrote, err := db.Users.Where(ant.Exists(theirs)).All(ctx)
quiet, err := db.Users.Where(ant.NotExists(theirs)).All(ctx)

For a relation, ant.Has says the same thing more briefly.

A single value​

ant.SubExpr runs a query for a single value, which you can use anywhere an expression goes.

oldest := ant.SubExpr(db.Users.Where(), ant.Max(&U.Age)).Expr()

users, err := db.Users.Where(ant.Col(&U.Age).EqExpr(oldest)).All(ctx)

It works for ordering, and for updates too.

posts := ant.SubExpr(db.Posts.Where(ant.EqCol(&P.UserID, &U.ID)), ant.Count()).Expr()

busiest, err := db.Users.Order(posts.Desc()).All(ctx)

From a subquery​

adults := db.Users.Where(ant.Gt(&U.Age, 18))

users, err := db.Users.Query().From(adults).Where(ant.Lt(&U.Age, 35)).All(ctx)

Include can't be part of a subquery, and returns ant.ErrIncludeInSubquery.

Union, intersect, except​

These combine queries of one model. Conditions and ordering added afterward apply to the combined rows, so the result is a normal query.

young := db.Users.Where(ant.Lt(&U.Age, 35))
old := db.Users.Where(ant.Gt(&U.Age, 25))

either, err := young.Union(old).OrderBy(&U.Name).All(ctx)
repeats, err := young.UnionAll(old).All(ctx)
both, err := young.Intersect(old).All(ctx)
onlyYoung, err := young.Except(old).All(ctx)

Common table expressions​

FromCTE names a subquery in a WITH clause.

named := db.Users.Where(ant.Like(&U.Name, "A%"))

users, err := db.Users.Query().FromCTE("named", named).OrderBy(&U.Name).All(ctx)

Recursion​

Recursive follows a model's relation to itself as far as it goes, and RecursiveOne walks up instead. A loop in the data doesn't hang it, because rows it has already seen are dropped.

type Folder struct {
ID int64
ParentID *int64
Name string
Parent *Folder
Children []Folder
}

var F = ant.Fields[Folder]()

tree, err := db.Folders.Where(ant.Eq(&F.ID, id)).
Recursive(&F.Children).
OrderBy(&F.Name).
All(ctx)

ancestors, err := db.Folders.Where(ant.Eq(&F.ID, id)).
RecursiveOne(&F.Parent).
All(ctx)

Rows held in memory​

ant.Values treats rows in your program as a table, so you can join against a list. It needs a dialect with migration support, which means PostgreSQL for now, and an empty list is ant.ErrNoRows.

type Wanted struct {
UserID int64
Label string
}

var W = ant.Fields[Wanted]()

wanted := ant.Values(db, Wanted{alice.ID, "first"}, Wanted{bob.ID, "second"})

rows, err := ant.JoinSub(db.Users.Query(), wanted, ant.EqCol(&U.ID, &W.UserID)).
Select(func(u *User, w *Wanted) string { return w.Label + ": " + u.Name }).
All(ctx)

Table functions​

ant.FromFunc reads a function that returns rows, and queries build on it.

type Number struct {
N int64
}

var N = ant.Fields[Number]()

numbers, err := ant.
FromFunc[Number](db, "generate_series(?::bigint, ?::bigint) AS numbers(n)", 1, 5).
Where(ant.Gt(&N.N, 3)).
All(ctx)