Documentation
¶
Overview ¶
Package evolve is a schema that changed five times, and the machinery that kept the changes honest.
The other examples show a schema at rest. This one is about what happens to one over time — which is where a data layer either helps or gets in the way, and where the interesting failures are not "this does not compile" but "this deleted a column of production data" and "this shipped a clean migration and broke every client".
docs/refactoring-a-database.md is the narrative. What is here:
evolveschema/schema.go the current state, and only the current state migrations/ how it got there, in the order it happened history_test.go what revisions 4 and 5 did to the API
The shape, and why it is this shape ¶
There is no v1 package and no v2 package. A real project has one schema file that it edits, and a directory of migrations it has already applied to databases it cannot go back and change — so that is what this is. The cost is that no intermediate state is readable as Go; the benefit is that the example is arranged the way the thing it teaches actually is.
The five revisions, and what each was chosen to show:
00010 initial_schema the baseline 00020 safe_additions a column with a default, a table, an index 00021 safe_additions_indexes ...which needed a file of its own 00030 widen_ticket_subject varchar(80) → text, free in this direction 00040 rename_email_and_agents a column and a table, both declared 00050 drop_legacy_ref destructive, and rendered only under a flag
Revision 2 rendering two files is not a mistake. An index on a live table is created CONCURRENTLY, which cannot run inside a transaction, so putting it with the others would have removed the rollback guarantee from all of them. migrate.Split is what separates them, and the versions here are spaced by ten so a revision that splits has somewhere to put the extra file.
What checks what ¶
Three gates, because a schema edit can go wrong in three unrelated ways:
- `mise run generate-check` — the generated code still matches the declaration. Catches a forgotten `sqlb generate`.
- `mise run impact-check` — the REST contract has not changed since the committed baseline. Catches a wire break, which a clean migration and a clean regeneration will both happily pass.
- pgtest/evolve — the migration history, replayed into an empty database, still builds what evolveschema declares. Catches the one thing no file comparison can: schema.go edited with no migration written. It needs a database, so it lives in the module that has one.
The second and third are the pair worth understanding, because revision 4 is the case where they disagree. Renaming customers.email to email_address is a single clean ALTER TABLE ... RENAME COLUMN that loses no data, so the replay gate is satisfied. It is also a 400 for every client still sending ?email=…, which is what impact-check is for. Neither gate can see what the other sees.
Index ¶
- Variables
- func Register(api huma.API, db sqlb.Executor) error
- type Customer
- type CustomerCreate
- type CustomerPatch
- type CustomerUpdate
- func (u *CustomerUpdate) SetCreatedAt(v time.Time) *CustomerUpdate
- func (u *CustomerUpdate) SetEmailAddress(v string) *CustomerUpdate
- func (u *CustomerUpdate) SetName(v string) *CustomerUpdate
- func (u *CustomerUpdate) SetUpdatedAt(v time.Time) *CustomerUpdate
- func (u *CustomerUpdate) Stmt() *sqlb.Update[Customer]
- func (u *CustomerUpdate) Where(preds ...sqlb.Pred) *CustomerUpdate
- type SupportAgent
- type SupportAgentUpdate
- func (u *SupportAgentUpdate) SetActive(v bool) *SupportAgentUpdate
- func (u *SupportAgentUpdate) SetCreatedAt(v time.Time) *SupportAgentUpdate
- func (u *SupportAgentUpdate) SetEmail(v string) *SupportAgentUpdate
- func (u *SupportAgentUpdate) SetName(v string) *SupportAgentUpdate
- func (u *SupportAgentUpdate) SetUpdatedAt(v time.Time) *SupportAgentUpdate
- func (u *SupportAgentUpdate) Stmt() *sqlb.Update[SupportAgent]
- func (u *SupportAgentUpdate) Where(preds ...sqlb.Pred) *SupportAgentUpdate
- type Ticket
- type TicketCreate
- type TicketPatch
- type TicketPriority
- type TicketStatus
- type TicketUpdate
- func (u *TicketUpdate) SetBody(v string) *TicketUpdate
- func (u *TicketUpdate) SetCreatedAt(v time.Time) *TicketUpdate
- func (u *TicketUpdate) SetCustomerID(v string) *TicketUpdate
- func (u *TicketUpdate) SetPriority(v TicketPriority) *TicketUpdate
- func (u *TicketUpdate) SetStatus(v TicketStatus) *TicketUpdate
- func (u *TicketUpdate) SetSubject(v string) *TicketUpdate
- func (u *TicketUpdate) SetUpdatedAt(v time.Time) *TicketUpdate
- func (u *TicketUpdate) Stmt() *sqlb.Update[Ticket]
- func (u *TicketUpdate) Where(preds ...sqlb.Pred) *TicketUpdate
Constants ¶
This section is empty.
Variables ¶
var CustomerCols = customerColumns{ ID: sqlb.Typed[string]("id"), EmailAddress: sqlb.TextColumn[string]("email_address"), Name: sqlb.TextColumn[string]("name"), CreatedAt: sqlb.Typed[time.Time]("created_at"), UpdatedAt: sqlb.Typed[time.Time]("updated_at"), }
CustomerCols are the typed columns of customers.
var SupportAgentCols = supportAgentColumns{ ID: sqlb.Typed[string]("id"), Email: sqlb.TextColumn[string]("email"), Name: sqlb.TextColumn[string]("name"), Active: sqlb.Typed[bool]("active"), CreatedAt: sqlb.Typed[time.Time]("created_at"), UpdatedAt: sqlb.Typed[time.Time]("updated_at"), }
SupportAgentCols are the typed columns of support_agents.
var TicketCols = ticketColumns{ ID: sqlb.Typed[string]("id"), CustomerID: sqlb.Typed[string]("customer_id"), Subject: sqlb.TextColumn[string]("subject"), Body: sqlb.TextColumn[string]("body"), Status: sqlb.Typed[TicketStatus]("status"), Priority: sqlb.Typed[TicketPriority]("priority"), CreatedAt: sqlb.Typed[time.Time]("created_at"), UpdatedAt: sqlb.Typed[time.Time]("updated_at"), }
TicketCols are the typed columns of tickets.
Functions ¶
func Register ¶
Register mounts every exposed resource on api.
The handlers are rest.Resource, instantiated per model. Registration is generic rather than reflective because query hooks are keyed by type: a BeforeQuery hook registered on a model applies to its REST reads too, which is how tenant scoping stops being something each handler must remember.
Types ¶
type Customer ¶
type Customer struct {
ID string `db:"id" json:"id" sqlb:"type:uuid,pk,default,filter,readonly"`
EmailAddress string `db:"email_address" json:"email_address" sqlb:"type:text,filter,search"`
Name string `db:"name" json:"name" sqlb:"type:text,filter,sort,search"`
CreatedAt time.Time `db:"created_at" json:"created_at" sqlb:"type:timestamptz,default,sort,readonly"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at" sqlb:"type:timestamptz,default,sort,readonly"`
}
Customer whoever a ticket is on behalf of.
type CustomerCreate ¶
CustomerCreate is the request body for creating a Customer.
Read-only columns are absent: the database or a BeforeCreate hook owns them. A column with a default is optional, so leaving it out means the database supplies the value rather than the zero value overwriting it.
func (CustomerCreate) Row ¶
func (c CustomerCreate) Row() (*Customer, error)
Row builds the row to insert. It satisfies rest.CreateBody.
type CustomerPatch ¶
type CustomerPatch struct {
EmailAddress *string `json:"email_address,omitempty"`
Name *string `json:"name,omitempty"`
// contains filtered or unexported fields
}
CustomerPatch is the request body for patching a Customer.
Every field is a pointer and every field is optional, so a request writes only the columns it names. Immutable columns are absent: they are settable once, at create.
func (CustomerPatch) Changes ¶
func (u CustomerPatch) Changes() (map[string]any, error)
Changes reports the columns the request named. It satisfies rest.UpdateBody.
func (*CustomerPatch) UnmarshalJSON ¶
func (u *CustomerPatch) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the body and remembers which properties were present.
Without this a nil pointer would be ambiguous: `{}` and `{"email_address": null}` decode identically, but the first must change nothing and the second must write NULL.
type CustomerUpdate ¶
type CustomerUpdate struct {
// contains filtered or unexported fields
}
CustomerUpdate is a typed update statement for customers.
func (*CustomerUpdate) SetCreatedAt ¶
func (u *CustomerUpdate) SetCreatedAt(v time.Time) *CustomerUpdate
SetCreatedAt sets created_at.
func (*CustomerUpdate) SetEmailAddress ¶
func (u *CustomerUpdate) SetEmailAddress(v string) *CustomerUpdate
SetEmailAddress sets email_address.
func (*CustomerUpdate) SetName ¶
func (u *CustomerUpdate) SetName(v string) *CustomerUpdate
SetName sets name.
func (*CustomerUpdate) SetUpdatedAt ¶
func (u *CustomerUpdate) SetUpdatedAt(v time.Time) *CustomerUpdate
SetUpdatedAt sets updated_at.
func (*CustomerUpdate) Stmt ¶
func (u *CustomerUpdate) Stmt() *sqlb.Update[Customer]
Stmt exposes the underlying statement for what the wrapper does not cover, such as Everything, SetExpr, Exec and One.
func (*CustomerUpdate) Where ¶
func (u *CustomerUpdate) Where(preds ...sqlb.Pred) *CustomerUpdate
Where narrows the affected rows.
type SupportAgent ¶
type SupportAgent struct {
ID string `db:"id" json:"id" sqlb:"type:uuid,pk,default,filter,readonly"`
Email string `db:"email" json:"email" sqlb:"type:text"`
Name string `db:"name" json:"name" sqlb:"type:text,filter,sort,search"`
Active bool `db:"active" json:"active" sqlb:"type:bool,default,filter"`
CreatedAt time.Time `db:"created_at" json:"created_at" sqlb:"type:timestamptz,default,sort,readonly"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at" sqlb:"type:timestamptz,default,sort,readonly"`
}
SupportAgent someone who answers tickets.
func (SupportAgent) TableName ¶
func (SupportAgent) TableName() string
TableName is the table SupportAgent maps to.
type SupportAgentUpdate ¶
type SupportAgentUpdate struct {
// contains filtered or unexported fields
}
SupportAgentUpdate is a typed update statement for support_agents.
func UpdateSupportAgent ¶
func UpdateSupportAgent() *SupportAgentUpdate
UpdateSupportAgent starts a typed update.
func (*SupportAgentUpdate) SetActive ¶
func (u *SupportAgentUpdate) SetActive(v bool) *SupportAgentUpdate
SetActive sets active.
func (*SupportAgentUpdate) SetCreatedAt ¶
func (u *SupportAgentUpdate) SetCreatedAt(v time.Time) *SupportAgentUpdate
SetCreatedAt sets created_at.
func (*SupportAgentUpdate) SetEmail ¶
func (u *SupportAgentUpdate) SetEmail(v string) *SupportAgentUpdate
SetEmail sets email.
func (*SupportAgentUpdate) SetName ¶
func (u *SupportAgentUpdate) SetName(v string) *SupportAgentUpdate
SetName sets name.
func (*SupportAgentUpdate) SetUpdatedAt ¶
func (u *SupportAgentUpdate) SetUpdatedAt(v time.Time) *SupportAgentUpdate
SetUpdatedAt sets updated_at.
func (*SupportAgentUpdate) Stmt ¶
func (u *SupportAgentUpdate) Stmt() *sqlb.Update[SupportAgent]
Stmt exposes the underlying statement for what the wrapper does not cover, such as Everything, SetExpr, Exec and One.
func (*SupportAgentUpdate) Where ¶
func (u *SupportAgentUpdate) Where(preds ...sqlb.Pred) *SupportAgentUpdate
Where narrows the affected rows.
type Ticket ¶
type Ticket struct {
ID string `db:"id" json:"id" sqlb:"type:uuid,pk,default,filter,readonly"`
CustomerID string `db:"customer_id" json:"customer_id" sqlb:"type:uuid,filter,expand"`
Customer *Customer `db:"-" json:"customer,omitempty" sqlb:"expands=customer_id"` // filled in by ?expand=customer
Subject string `db:"subject" json:"subject" sqlb:"type:text,filter,sort,search"`
Body string `db:"body" json:"body" sqlb:"type:text,filter,search"`
Status TicketStatus `db:"status" json:"status" sqlb:"type:enum,default,filter,sort"`
Priority TicketPriority `db:"priority" json:"priority" sqlb:"type:enum,default,filter,sort"`
CreatedAt time.Time `db:"created_at" json:"created_at" sqlb:"type:timestamptz,default,sort,readonly"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at" sqlb:"type:timestamptz,default,sort,readonly"`
}
Ticket one request from one customer.
type TicketCreate ¶
type TicketCreate struct {
CustomerID string `json:"customer_id"`
Subject string `json:"subject"`
Body string `json:"body"`
Status *TicketStatus `json:"status,omitempty" enum:"open,pending,closed"`
Priority *TicketPriority `json:"priority,omitempty" enum:"low,normal,high,urgent"`
}
TicketCreate is the request body for creating a Ticket.
Read-only columns are absent: the database or a BeforeCreate hook owns them. A column with a default is optional, so leaving it out means the database supplies the value rather than the zero value overwriting it.
func (TicketCreate) Row ¶
func (c TicketCreate) Row() (*Ticket, error)
Row builds the row to insert. It satisfies rest.CreateBody.
type TicketPatch ¶
type TicketPatch struct {
CustomerID *string `json:"customer_id,omitempty"`
Subject *string `json:"subject,omitempty"`
Body *string `json:"body,omitempty"`
Status *TicketStatus `json:"status,omitempty" enum:"open,pending,closed"`
Priority *TicketPriority `json:"priority,omitempty" enum:"low,normal,high,urgent"`
// contains filtered or unexported fields
}
TicketPatch is the request body for patching a Ticket.
Every field is a pointer and every field is optional, so a request writes only the columns it names. Immutable columns are absent: they are settable once, at create.
func (TicketPatch) Changes ¶
func (u TicketPatch) Changes() (map[string]any, error)
Changes reports the columns the request named. It satisfies rest.UpdateBody.
func (*TicketPatch) UnmarshalJSON ¶
func (u *TicketPatch) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the body and remembers which properties were present.
Without this a nil pointer would be ambiguous: `{}` and `{"customer_id": null}` decode identically, but the first must change nothing and the second must write NULL.
type TicketPriority ¶
type TicketPriority string
TicketPriority is the tickets.priority column's value set.
const ( TicketPriorityLow TicketPriority = "low" TicketPriorityNormal TicketPriority = "normal" TicketPriorityHigh TicketPriority = "high" TicketPriorityUrgent TicketPriority = "urgent" )
type TicketStatus ¶
type TicketStatus string
TicketStatus is the tickets.status column's value set.
const ( TicketStatusOpen TicketStatus = "open" TicketStatusPending TicketStatus = "pending" TicketStatusClosed TicketStatus = "closed" )
type TicketUpdate ¶
type TicketUpdate struct {
// contains filtered or unexported fields
}
TicketUpdate is a typed update statement for tickets.
func (*TicketUpdate) SetBody ¶
func (u *TicketUpdate) SetBody(v string) *TicketUpdate
SetBody sets body.
func (*TicketUpdate) SetCreatedAt ¶
func (u *TicketUpdate) SetCreatedAt(v time.Time) *TicketUpdate
SetCreatedAt sets created_at.
func (*TicketUpdate) SetCustomerID ¶
func (u *TicketUpdate) SetCustomerID(v string) *TicketUpdate
SetCustomerID sets customer_id.
func (*TicketUpdate) SetPriority ¶
func (u *TicketUpdate) SetPriority(v TicketPriority) *TicketUpdate
SetPriority sets priority.
func (*TicketUpdate) SetStatus ¶
func (u *TicketUpdate) SetStatus(v TicketStatus) *TicketUpdate
SetStatus sets status.
func (*TicketUpdate) SetSubject ¶
func (u *TicketUpdate) SetSubject(v string) *TicketUpdate
SetSubject sets subject.
func (*TicketUpdate) SetUpdatedAt ¶
func (u *TicketUpdate) SetUpdatedAt(v time.Time) *TicketUpdate
SetUpdatedAt sets updated_at.
func (*TicketUpdate) Stmt ¶
func (u *TicketUpdate) Stmt() *sqlb.Update[Ticket]
Stmt exposes the underlying statement for what the wrapper does not cover, such as Everything, SetExpr, Exec and One.
func (*TicketUpdate) Where ¶
func (u *TicketUpdate) Where(preds ...sqlb.Pred) *TicketUpdate
Where narrows the affected rows.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package evolveschema is the schema of a support desk, and the subject of docs/refactoring-a-database.md.
|
Package evolveschema is the schema of a support desk, and the subject of docs/refactoring-a-database.md. |