Documentation
¶
Overview ¶
Package computed shows how to get a derived value — one the row does not store — out of Postgres through sqlb.
schema.Computed now declares one, and declared.go is this package's three derived values written that way. The four techniques below are still here because three of them did not go away when it landed: a column Postgres maintains is cheaper than an expression evaluated per read, and it is the right answer often enough that a declaration slot should not be allowed to hide it. ADR-0041 says which tier is which.
The four, in the order to reach for them:
- A STORED generated column. Postgres computes it on write, it is a real column, and it can be indexed. Restricted to IMMUTABLE expressions over the same row, so nothing involving now() or another table qualifies.
- A trigger-maintained counter. For values that come from other rows — "how many tasks does this project have" — where recomputing per read would be a correlated subquery on every request.
- A projected expression, via sqlb.RawSel and sqlb.Collect. Costs nothing on write, evaluated per read, and it is the only one of the four that can take a bind parameter — which is what a per-viewer field like "did *I* star this" requires.
- A database VIEW, described with Table(). One hand-written object, and the whole generated read path works over it.
(1) and (2) produce ordinary columns, so sqlb needs to be told nothing: they are Filterable, Sortable and indexable like any other, and the only sqlb-side decision is ReadOnly, which keeps them out of the generated request bodies. (3) is where the ceiling was, and where schema.Computed now sits — see declared.go, which produces the same SQL from a declaration the emitters can read. What (3) still has that a declaration does not is a per-call-site expression: schema.Computed is a property of the table, so a value only one query wants is still a RawSel.
Index ¶
- Constants
- Variables
- func DeclaredView(viewer int64) *sqlb.Builder[DeclaredProject]
- func Derived(viewer int64) []sqlb.Selectable
- func OverdueFirst(b *sqlb.Builder[Project]) *sqlb.Builder[Project]
- func StarredBy(b *sqlb.Builder[Project], viewer int64) *sqlb.Builder[Project]
- func View(viewer int64) *sqlb.Builder[Project]
- type DeclaredProject
- type Project
- type ProjectView
Constants ¶
const Schema = `` /* 1089-byte string literal not displayed */
Schema is the DDL the techniques below assume. It is written out rather than declared through the schema package because two of the three objects — the generated column and the trigger — have no spelling in the DSL today, so a project using them writes this migration by hand and declares the resulting columns as ordinary ones.
Variables ¶
var Declared = declaredRegistry()
Technique (5): declare the expression.
The four techniques in computed.go all work and three of them are still the right answer in the cases their doc comments name. What none of them can do is tell the *generator* that a derived value exists — so `is_overdue` is in the SQL and nowhere else: not in the TypeScript type, not in the CLI's columns, not in the OpenAPI response schema, and not in the filter grammar a REST client is allowed to use. That was the gap ADR-0041 set out to close, and this file is the closed version of the same three values.
The declaration ¶
Declared is the schema half. A computed column emits no DDL — the CREATE TABLE this registry produces is the one in Schema minus the three expressions — and it reaches every emitter that describes a row.
Functions ¶
func DeclaredView ¶ added in v0.5.0
func DeclaredView(viewer int64) *sqlb.Builder[DeclaredProject]
DeclaredView is the read. Compare it with View: there is no projection to assemble, no RawSel to parenthesise by hand, and the per-viewer bind is supplied once rather than written at each site that mentions it.
In a REST application this Bind lives in a BeforeQuery hook, so no handler and no caller passes the viewer around — which is also what makes the mount-time check able to insist on it.
func Derived ¶
func Derived(viewer int64) []sqlb.Selectable
Derived is the projection ProjectView expects: every column of the table, then the three expressions.
Select appends to the projection but replaces the default one, so the table's own columns have to be named once something else is added. columnsOf does that from the model rather than by hand, so a column added to Project reaches the view without a second edit.
func OverdueFirst ¶
OverdueFirst orders by a derived value without projecting it.
This is the half of technique (3) that has no ceiling problem: an expression in ORDER BY or WHERE is just SQL, and sqlb has never stopped anyone writing it. What it costs is the guarantee — `sqlb.Raw` is not checked against the schema, so a column renamed under it fails at the database rather than at generate time, and a REST client cannot reach it at all because the filter grammar only admits declared columns.
That gap is the argument for ADR-0041: not that this cannot be written, but that writing it here leaves the TypeScript client, the CLI and the OpenAPI document not knowing the field exists.
func StarredBy ¶
StarredBy filters on the per-viewer fact, which is the same EXISTS as the projection and the same reason it needs a bind.
func View ¶
View builds the read. It is Query[Project] rather than Query[ProjectView] because the table is the same table — ProjectView describes the *result*, and Collect is what pairs the two.
Note what did not have to change: hooks registered on Project still run, so the tenant predicate applies to this query as much as to a plain list. The derived values ride along on a query the domain still constrains.
Types ¶
type DeclaredProject ¶ added in v0.5.0
type DeclaredProject struct {
ID int64 `db:"id" json:"id" sqlb:"pk"`
OrgID int64 `db:"org_id" json:"org_id" sqlb:"filter,readonly,scope"`
Name string `db:"name" json:"name" sqlb:"filter,search,sort"`
DueDate *time.Time `db:"due_date" json:"due_date" sqlb:"filter,sort"`
TotalTasks int32 `db:"total_tasks" json:"total_tasks" sqlb:"filter,sort,readonly"`
CompletedTasks int32 `db:"completed_tasks" json:"completed_tasks" sqlb:"filter,sort,readonly"`
OpenTasks int32 `db:"open_tasks" json:"open_tasks" sqlb:"filter,sort,readonly"`
IsOverdue bool `db:"is_overdue" json:"is_overdue" sqlb:"filter,readonly"`
Progress *int32 `db:"progress" json:"progress" sqlb:"filter,sort,readonly"`
IsStarred bool `db:"is_starred" json:"is_starred" sqlb:"filter,readonly"`
}
DeclaredProject is what codegen emits from the table above: an ordinary struct whose derived fields are ordinary fields, plus one method carrying the expressions.
The expressions are in a method rather than in the `sqlb` tag because a tag is a comma-separated list of words and SQL is neither. Everything else about these columns is said in the tag exactly as it is for a stored one, which is the property that makes them work everywhere without a second code path.
func (DeclaredProject) ComputedColumns ¶ added in v0.5.0
func (DeclaredProject) ComputedColumns() []sqlb.Computed
ComputedColumns carries the expressions the schema declared.
func (DeclaredProject) TableName ¶ added in v0.5.0
func (DeclaredProject) TableName() string
type Project ¶
type Project struct {
ID int64 `db:"id" sqlb:"pk,default"`
OrgID int64 `db:"org_id" sqlb:"filter,scope,readonly"`
Name string `db:"name" sqlb:"filter,sort,search"`
DueDate *time.Time `db:"due_date" sqlb:"filter,sort"`
TotalTasks int `db:"total_tasks" sqlb:"filter,sort,readonly,default"`
CompletedTasks int `db:"completed_tasks" sqlb:"filter,sort,readonly,default"`
OpenTasks int `db:"open_tasks" sqlb:"filter,sort,readonly,default"`
}
Project is the row. The three derived columns are declared exactly like the stored ones, because to everything above Postgres that is what they are.
ReadOnly is the load-bearing word. Without it the generated create and update bodies would accept a total_tasks, and a request could make the counter disagree with the tasks it counts. Postgres would reject a write to open_tasks on its own — it is GENERATED ALWAYS — but as a 500 naming a constraint, where ReadOnly is a 400 naming the field.
type ProjectView ¶
type ProjectView struct {
Project
// Evaluated per read. IsOverdue reads current_date, so it can change
// without the row changing — which is exactly why it cannot be technique
// (1): Postgres requires a generated column's expression to be IMMUTABLE.
IsOverdue bool `db:"is_overdue"`
// NULL when the project has no tasks, which is why this is a pointer.
// NULLIF is doing that: a zero denominator would otherwise be a division
// error taking the whole request with it, not a zero.
Progress *int `db:"progress"`
// Depends on who is asking, not on the row. No column and no view can hold
// this, because there is no "the" answer — it is a function of the request.
IsStarred bool `db:"is_starred"`
}
ProjectView is the row plus the values no column holds. Embedding Project keeps one definition of the shared columns — an untagged embedded struct contributes its own fields, so the view maps every column the table has and then some.
It is a separate type from Project on purpose. Query[Project] stays the plain table read that INSERT ... RETURNING and every hook already work against; asking for the derived values is a different, more expensive query, and the type is what says so at the call site.
func (ProjectView) TableName ¶
func (ProjectView) TableName() string