sqlplan

package
v1.4.5 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package sqlplan turns the apply SQL of a nuzur schema push into something a human can decide on: an ordered statement list, each labelled with what it does and what it can cost.

It exists because the CLI receives the migration as one opaque string. sql-push hands over the exact DDL it is about to run — see the confirmation step in extension-sql-push's manager — and nothing between there and here says which of those statements deletes a table.

What this package cannot know

Read this before trusting its output, and before extending it. A preview users over-trust is worse than no preview.

  1. No per-statement hazards. pg-schema-diff computes Statement.Hazards, Timeout and LockTimeout for every statement it generates, and every one of those is discarded before the CLI sees it (in the local agent's pg-schema-plan handler, twice in nuzur-go's sql-diff-manager, and finally by the wire types themselves — ComputePgSchemaPlanResponse.apply_sql and GetChangesDiffResponse.apply are plain strings). So this package cannot tell you which statements take an ACCESS EXCLUSIVE lock and therefore take the table offline, which are multi-hour index builds, or which the differ itself considered correctness-affecting. It re-derives only what a keyword can see.
  2. No row counts. "DROP TABLE audit_log_2023" cannot be annotated with how many rows that is; nothing in the path counts.
  3. No idea whether an ALTER will succeed. Adding a foreign key against orphan rows, SET NOT NULL against nulls, a unique index against duplicates: all of those are flagged "may fail", never resolved. pg-schema-diff could resolve them by validating against a temporary database, but nuzur asks it not to.
  4. No atomicity to report. sql-push does not ask for a transaction and the query manager splits on ";" and runs the fragments one at a time, so a failure at statement 7 of 12 leaves 1 through 6 applied. This package says so; it cannot fix it.
  5. MySQL diffs contain churn this package cannot identify. nuzur cannot read a MySQL schema directly, so the "existing" side is reconstructed by introspecting the database into a project version and re-rendering it as DDL. Anything the model cannot express comes back normalized, producing ALTERs that change nothing and reappear on every deploy. Those land in the narrowing class; a caller with a MySQL target should say so out loud.

The one thing it is careful to get right is the data-loss class, because that is what a caller gates on. Everything unrecognized is KindOther at SeverityNone: never read "not flagged" as "proven safe".

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DropOnlyWhatItCouldCreate

func DropOnlyWhatItCouldCreate() string

DropOnlyWhatItCouldCreate is the bound on a reconciling deploy's blast radius, and no user currently knows it.

The diff restricts BOTH sides to what a project version can express — on Postgres by an object-class allowlist (schemas, tables, indexes, foreign keys), on MySQL because both sides are rendered by the same generator. Anything the model cannot represent is therefore absent from both sides and can never be proposed for a drop.

func MySQLCaveat

func MySQLCaveat() string

MySQLCaveat explains why a MySQL plan can contain statements that change nothing. Callers print it whenever the target is MySQL.

It matters because MySQL is the default engine, and because a user who sees a dozen pointless MODIFY COLUMNs and is not told why will conclude the tool is broken — which is roughly what happened to the user this feature was built for.

func Split

func Split(applySQL string) []string

Split splits apply SQL into statements exactly the way the connection manager will execute it: on ";", discarding fragments that are only whitespace.

This is deliberately the same naive split as executeRawQuery in nuzur-go's sql-query-manager. A smarter splitter would produce a prettier preview and a less truthful one: whatever a ";" inside a string literal does to the real execution, the preview has to show the same thing. If executeRawQuery ever learns to parse, this must learn with it.

Fragments are trimmed for display. Leading and trailing whitespace is the one difference from what is executed, and it changes nothing about what runs.

Types

type Counts

type Counts struct {
	Total          int `json:"total"`
	Additive       int `json:"additive"`
	DataLoss       int `json:"data_loss"`
	ConstraintLoss int `json:"constraint_loss"`
	Narrowing      int `json:"narrowing"`
}

Counts summarizes a plan by severity.

type Kind

type Kind string

Kind is what a statement does, at the granularity a reader cares about.

const (
	KindCreateTable    Kind = "create_table"
	KindCreateIndex    Kind = "create_index"
	KindCreateSchema   Kind = "create_schema"
	KindAddColumn      Kind = "add_column"
	KindAddConstraint  Kind = "add_constraint"
	KindAlterColumn    Kind = "alter_column"
	KindDropTable      Kind = "drop_table"
	KindDropColumn     Kind = "drop_column"
	KindDropIndex      Kind = "drop_index"
	KindDropConstraint Kind = "drop_constraint"
	KindDropSchema     Kind = "drop_schema"
	KindDropDatabase   Kind = "drop_database"
	KindTruncate       Kind = "truncate"
	KindOther          Kind = "other"
)

type Plan

type Plan struct {
	Statements []Statement `json:"statements"`
}

Plan is an ordered migration.

func Analyze

func Analyze(applySQL string) Plan

Analyze splits apply SQL into statements and labels each one.

func (Plan) ChurnNote

func (p Plan) ChurnNote() string

ChurnNote reports how much of a MySQL plan is likely to be that no-op churn, or "" when none of it is.

func (Plan) Counts

func (p Plan) Counts() Counts

Counts summarizes the plan by severity.

func (Plan) Destructive

func (p Plan) Destructive() []Statement

Destructive returns the statements that delete data, in execution order.

func (Plan) Empty

func (p Plan) Empty() bool

Empty reports whether there is nothing to apply.

func (Plan) HasDestructive

func (p Plan) HasDestructive() bool

HasDestructive reports whether any statement deletes data. This is the question the deploy gate asks.

func (Plan) RenderDestructive

func (p Plan) RenderDestructive() string

RenderDestructive is the called-out block of statements that delete data, or "" when there are none. Numbering is the plan's execution order, so a reader can find each one in the full listing.

func (Plan) RenderStatements

func (p Plan) RenderStatements() string

RenderStatements is the full plan in execution order, each statement annotated with what it costs.

func (Plan) SummaryLine

func (p Plan) SummaryLine() string

SummaryLine is the one-line count of what this plan does.

func (Plan) TransactionalWarning

func (p Plan) TransactionalWarning() string

TransactionalWarning is the note that a partial failure leaves a partly-migrated database. It is unconditional for a non-empty plan: sql-push never asks for a transaction, so this is always true and has never been said out loud.

type Severity

type Severity string

Severity is what a statement can cost you.

const (
	// SeverityNone is additive: it creates or widens, and cannot fail against data
	// that is already there.
	SeverityNone Severity = ""
	// SeverityDataLoss means rows or column values disappear. This is the class
	// callers gate on.
	SeverityDataLoss Severity = "data_loss"
	// SeverityConstraintLoss means an index, key or constraint disappears. No data
	// is deleted, but a guarantee — and possibly a query plan — is.
	SeverityConstraintLoss Severity = "constraint_loss"
	// SeverityNarrowing means the statement may fail, or truncate, when applied to
	// rows that already exist.
	SeverityNarrowing Severity = "narrowing"
)

type Statement

type Statement struct {
	// Index is 1-based and is execution order.
	Index int `json:"index"`
	// SQL is the statement as it will be sent, without its trailing ";".
	SQL      string   `json:"sql"`
	Kind     Kind     `json:"kind"`
	Severity Severity `json:"severity,omitempty"`
	// Object is a best-effort "table" or "table.column" — for pointing at the
	// thing being lost, not for programmatic use.
	Object string `json:"object,omitempty"`
	// Reason says what this costs, in a sentence a person can act on.
	Reason string `json:"reason,omitempty"`
}

Statement is one fragment of the migration, as it will be executed.

func (Statement) Destructive

func (s Statement) Destructive() bool

Destructive reports whether this statement deletes data.

Jump to

Keyboard shortcuts

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