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.
- 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.
- No row counts. "DROP TABLE audit_log_2023" cannot be annotated with how many rows that is; nothing in the path counts.
- 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.
- 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.
- 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 ¶
- func DropOnlyWhatItCouldCreate() string
- func MySQLCaveat() string
- func Split(applySQL string) []string
- type Counts
- type Kind
- type Plan
- func (p Plan) ChurnNote() string
- func (p Plan) Counts() Counts
- func (p Plan) Destructive() []Statement
- func (p Plan) Empty() bool
- func (p Plan) HasDestructive() bool
- func (p Plan) RenderDestructive() string
- func (p Plan) RenderStatements() string
- func (p Plan) SummaryLine() string
- func (p Plan) TransactionalWarning() string
- type Severity
- type Statement
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 ¶
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 (Plan) ChurnNote ¶
ChurnNote reports how much of a MySQL plan is likely to be that no-op churn, or "" when none of it is.
func (Plan) Destructive ¶
Destructive returns the statements that delete data, in execution order.
func (Plan) HasDestructive ¶
HasDestructive reports whether any statement deletes data. This is the question the deploy gate asks.
func (Plan) RenderDestructive ¶
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 ¶
RenderStatements is the full plan in execution order, each statement annotated with what it costs.
func (Plan) SummaryLine ¶
SummaryLine is the one-line count of what this plan does.
func (Plan) TransactionalWarning ¶
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 ¶
Destructive reports whether this statement deletes data.