evolve

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 6 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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

func Register(api huma.API, db sqlb.Executor) error

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.

func (Customer) TableName

func (Customer) TableName() string

TableName is the table Customer maps to.

type CustomerCreate

type CustomerCreate struct {
	EmailAddress string `json:"email_address"`
	Name         string `json:"name"`
}

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 UpdateCustomer

func UpdateCustomer() *CustomerUpdate

UpdateCustomer starts a typed update.

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

SetEmail sets email.

func (*SupportAgentUpdate) SetName

SetName sets name.

func (*SupportAgentUpdate) SetUpdatedAt

func (u *SupportAgentUpdate) SetUpdatedAt(v time.Time) *SupportAgentUpdate

SetUpdatedAt sets updated_at.

func (*SupportAgentUpdate) Stmt

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.

func (Ticket) TableName

func (Ticket) TableName() string

TableName is the table Ticket maps to.

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 UpdateTicket

func UpdateTicket() *TicketUpdate

UpdateTicket starts a typed update.

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.

Jump to

Keyboard shortcuts

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