compat

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package compat defines the engine-neutral contract used by the compatibility implementation. It deliberately requires explicit targets: a claim of exact compatibility has no meaning without source and destination versions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CompileDDL

func CompileDDL(target Target, schema Schema) ([]string, error)

CompileDDL emits the target-specific physical schema from the canonical AST. Callers must audit transformed features before treating the result as an exact migration plan.

func CompileDropTable added in v0.2.0

func CompileDropTable(target Target, table string) (string, error)

CompileDropTable compiles the statement that drops one existing table.

It does NOT emit IF EXISTS: dropping a table that is not there is a mistake in the caller's model of the database, and the engine's "no such table" error is the only signal that surfaces it. Idempotence is available explicitly through CompileDropTableIfExists / Store.DropTableIfExists — two named entry points instead of an unnamed boolean at the call site, so a reader of the call always sees which semantics were chosen.

The table name is quoted with quoteIdentifier, so a name containing double quotes, a semicolon or any other punctuation stays a single identifier and can never introduce extra SQL.

func CompileDropTableIfExists added in v0.2.0

func CompileDropTableIfExists(target Target, table string) (string, error)

CompileDropTableIfExists compiles the idempotent form, `DROP TABLE IF EXISTS`, for callers that genuinely want "make sure this table is gone" (teardown, reruns of a rebuild). IF EXISTS is spelled out in the function name precisely because it hides a real error — a typo in the table name becomes a silent no-op — so choosing it must be visible at the call site.

The IF EXISTS keyword is likewise identical in both engines.

func Placeholder added in v0.4.0

func Placeholder(engine Engine, position int) string

Placeholder composes the bind-parameter marker for one argument on the given engine: `?` on SQLite, `$n` on PostgreSQL. It is the single place in this package that knows that difference — every internal statement builder (metadata upsert, insertRow, replication, capture, routine WHERE/LIMIT/OFFSET) goes through it, so there is exactly one implementation, not a copy per caller.

It is exported because a consumer sometimes *must* write raw SQL and would otherwise branch on the engine by hand — the one thing this package exists to avoid. The reason raw SQL is unavoidable is structural, not a defect of the caller: CallRoutine and QueryRoutine open their own transaction, so they cannot be used inside a transaction the consumer already owns (DDL plus its registry and metadata rows that must commit together). It is the same limit that already motivated splitting the pure CompileDropTable from the executing DropTable.

Contract:

  • position is 1-based: the first argument is position 1.
  • The caller is responsible for the order of the arguments matching the positions it emitted. SQLite binds `?` markers by their order of appearance in the statement, PostgreSQL binds `$n` by its number, so the two agree only when the emitted sequence is 1, 2, 3, … in the same order the values are passed to Exec/Query. Emitting `$2` before `$1`, or reusing `$1` twice, compiles on PostgreSQL and means something different on SQLite; this function cannot detect that and does not try to.
  • The returned value is a SQL fragment, never a value. It exists so caller data is bound, not concatenated.

A position below 1 is a caller bug, and it makes BOTH engines reject the statement. That convergence is deliberate and is the whole reason this function does not just emit `?` unconditionally.

PostgreSQL rejects an invalid position on its own: `$0` is "there is no parameter $0" (SQLSTATE 42P02) and `$-1` is a syntax error (42601). SQLite is the engine that needed the guard, because a bare `?` carries no number — the mistake would simply vanish and the statement would run, which is the "works in development, explodes in production" divergence this package exists to prevent. So for position < 1 SQLite is given the numbered marker `?0`, which it rejects with `variable number must be between ?1 and ?32766`.

The emitted marker for an invalid position is the CONSTANT `?0`, not `?` plus the position. That distinction is not cosmetic and was measured against real SQLite: `?-1` does NOT tokenize as an invalid numbered marker, it tokenizes as the positional `?` followed by the arithmetic `- 1`. `WHERE "id" = ?-1` is therefore accepted as `WHERE "id" = ? - 1` and quietly returns the WRONG rows (binding 2 matches the row with id 1) — strictly worse than the bug it was meant to surface. `?0` is invalid for every position below 1, so it is the marker used for all of them.

Position >= 1 is untouched: SQLite still emits the plain positional `?`, byte-identical to what this package has always emitted.

Normalizing an invalid position to 1 was rejected as a fix: it would hide the caller's bug behind SQL that silently means something else. Adding an error return was also rejected: the signature is a pure string composer used inside expression concatenation (`"... = " + Placeholder(engine, 1)`), and threading an error through every internal statement builder would buy nothing that the engine-level rejection does not already guarantee.

func RequireEquivalent

func RequireEquivalent(report VerificationReport) error

func RequireExact

func RequireExact(findings []Finding) error

func SnapshotDigest

func SnapshotDigest(snapshot Snapshot) (string, error)

SnapshotDigest creates a deterministic data-integrity proof for a canonical snapshot. Row order is normalized; table, column and value names are encoded by encoding/json with stable map ordering.

Types

type Assignment

type Assignment struct {
	Column string     `json:"column"`
	Value  Expression `json:"value"`
}

type CatalogObject

type CatalogObject struct {
	Kind       string `json:"kind"`
	Name       string `json:"name"`
	Definition string `json:"definition,omitempty"`
	Reason     string `json:"reason"`
}

type Change

type Change struct {
	Source        Target     `json:"source"`
	Sequence      uint64     `json:"sequence"`
	CommittedAt   time.Time  `json:"committed_at"`
	Kind          ChangeKind `json:"kind"`
	Table         string     `json:"table"`
	PrimaryKey    Row        `json:"primary_key"`
	Before        Row        `json:"before,omitempty"`
	After         Row        `json:"after,omitempty"`
	TransactionID string     `json:"transaction_id"`
}

Change is an engine-neutral mutation. Sequence is monotonically increasing per source, which lets a destination apply the same ordered history.

func OrderedChanges

func OrderedChanges(changes []Change) ([]Change, error)

OrderedChanges rejects ambiguous or out-of-order histories before applying them to the other engine.

func (Change) Validate

func (change Change) Validate() error

type ChangeKind

type ChangeKind string
const (
	Insert ChangeKind = "insert"
	Update ChangeKind = "update"
	Delete ChangeKind = "delete"
)

type Column

type Column struct {
	Name     string      `json:"name"`
	Type     Type        `json:"type"`
	Nullable bool        `json:"nullable"`
	Default  *Expression `json:"default,omitempty"`
	// DomainRef, when set, names a Schema.Domains entry this column takes its
	// CHECK/NOT NULL/DEFAULT from. It is additive and omitempty, so a column
	// without it stays byte-identical. The column MUST still carry Type equal to
	// the domain's base type: the data chain (export/import canonicalization) keys
	// off Column.Type.Family, and both engines physically store the domain value
	// as its base type, so the redundant Type keeps that path unchanged and keeps
	// PG(native domain) and SQLite(inlined) storing identical values. A
	// domain-referencing column must be otherwise neutral — Nullable true, no own
	// Default, no Generated — so the domain is the single source of the constraint
	// (validated in Schema.Validate). On PostgreSQL the column's SQL type is the
	// domain name; on SQLite it is the base type with the domain's CHECK/NOT
	// NULL/DEFAULT inlined.
	DomainRef string `json:"domain,omitempty"`
	// Generated, when set, makes this a STORED generated column whose value is
	// computed from Expression rather than supplied on INSERT/UPDATE. It is an
	// additive, omitempty field: a column without it stays byte-identical in the
	// canonical JSON and in every emitted statement. See GeneratedColumn.
	Generated *GeneratedColumn `json:"generated,omitempty"`
}

type CommonTableExpr

type CommonTableExpr struct {
	Name  string      `json:"name"`
	Query SelectQuery `json:"query"`
}

CommonTableExpr is one named, non-recursive common table expression: the CTE name and the query it binds. Referenced from a FROM clause by its name like an ordinary table, a CTE has identical materialized-result semantics in SQLite and PostgreSQL. A CTE name shadows a real table of the same name for the duration of the query in both engines (standard SQL), so no special resolution is needed.

type CompoundSelect

type CompoundSelect struct {
	Operator string      `json:"operator"`
	Query    SelectQuery `json:"query"`
}

CompoundSelect is one branch of a compound (set-operation) SELECT: the set operator that joins it to everything to its left, plus the branch query. The operator is one of "union", "union_all", "intersect", "except"; all four have identical set semantics in SQLite and PostgreSQL. A chain that mixes "intersect" with any other operator is rejected, because INTERSECT binds more tightly than UNION/EXCEPT in PostgreSQL but has equal (left-associative) precedence in SQLite, so a flat left-associative chain would group differently between the two engines.

type ConflictError

type ConflictError struct {
	Table      string
	PrimaryKey Row
	Expected   Row
	Actual     Row
}

func (*ConflictError) Error

func (err *ConflictError) Error() string

type Constraint

type Constraint struct {
	Kind       ConstraintKind `json:"kind"`
	Columns    []string       `json:"columns"`
	References *Reference     `json:"references,omitempty"`
	Expression *Expression    `json:"expression,omitempty"`
}

type ConstraintKind

type ConstraintKind string
const (
	PrimaryKey ConstraintKind = "primary_key"
	UniqueKey  ConstraintKind = "unique"
	ForeignKey ConstraintKind = "foreign_key"
	Check      ConstraintKind = "check"
)

type Contract

type Contract struct {
	Source           Target    `json:"source"`
	Destination      Target    `json:"destination"`
	RequiredFeatures []Feature `json:"required_features"`
}

Contract is the complete compatibility claim made for one migration or synchronization relationship. RequiredFeatures must be audited before data movement begins; unknown capabilities are failures, never silent fallbacks.

func (Contract) Validate

func (c Contract) Validate() error

type Domain

type Domain struct {
	Name    string      `json:"name"`
	Type    Type        `json:"type"`
	Check   *Expression `json:"check,omitempty"`
	NotNull bool        `json:"not_null,omitempty"`
	Default *Expression `json:"default,omitempty"`
}

Domain is a named SQL domain: a base Type plus an optional CHECK constraint, NOT NULL flag and DEFAULT expression. It is asymmetric across the two engines by nature:

  • PostgreSQL emits a native CREATE DOMAIN and columns reference it by name.
  • SQLite has no domains, so every column that references the domain is compiled INLINE with the domain's base type and the same CHECK/NOT NULL/DEFAULT — semantically equivalent (same base type, same constraint), the only portable rendering.

The CHECK expression refers to the value under test with the placeholder node Expression{Kind:"domain_value"}: it compiles to the SQL keyword VALUE on PostgreSQL (the domain's own value) and to the referencing column's name when inlined on SQLite. Grammar validity of the CHECK is enforced at compile time by the same compileExpression path as any other expression, so an out-of-grammar CHECK fails with an explicit error.

A domain is a schema-level constraint, not data: the stored value is identical whether the constraint is enforced by a native PG domain or by an inline SQLite CHECK, so data fidelity is preserved. Only the canonical path (schema created by this layer, kept in __compat_schema) round-trips the domain exactly; external inspection cannot rebuild a domain that never physically existed as such (see docs/COMPATIBILITY.md).

type Engine

type Engine string
const (
	SQLite   Engine = "sqlite"
	Postgres Engine = "postgres"
)

type Expression

type Expression struct {
	Kind  string       `json:"kind"`
	Value string       `json:"value,omitempty"`
	Args  []Expression `json:"args,omitempty"`
}

Expression is an AST placeholder. Raw SQL is intentionally excluded from this layer because exact compatibility requires the expression to be parsed and compiled separately for each target dialect.

type Feature

type Feature string
const (
	Tables               Feature = "tables"
	PrimaryKeys          Feature = "primary_keys"
	ForeignKeys          Feature = "foreign_keys"
	CanonicalForeignKeys Feature = "canonical_foreign_keys"
	CheckRules           Feature = "check_constraints"
	CanonicalChecks      Feature = "canonical_check_constraints"
	Transactions         Feature = "transactions"
	Indexes              Feature = "indexes"
	CanonicalIndexes     Feature = "canonical_indexes"
	JSONValues           Feature = "json"
	UUIDValues           Feature = "uuid"
	Triggers             Feature = "triggers"
	CanonicalTriggers    Feature = "canonical_triggers"
	Views                Feature = "views"
	CanonicalViews       Feature = "canonical_views"
	StoredRoutines       Feature = "stored_routines"
	CanonicalRoutines    Feature = "canonical_routines"
	FullText             Feature = "full_text"
	CanonicalFullText    Feature = "canonical_full_text"
	CanonicalVectors     Feature = "canonical_vectors"
)

func InferFeatures

func InferFeatures(schema Schema) []Feature

InferFeatures derives the minimum capabilities required by a canonical schema. Migration callers use this to prevent a schema from claiming an exact plan while omitting its difficult capabilities from the contract.

type Finding

type Finding struct {
	Feature Feature       `json:"feature"`
	Status  MappingStatus `json:"status"`
	Reason  string        `json:"reason,omitempty"`
}

func Audit

func Audit(contract Contract) ([]Finding, error)

Audit reports the current compatibility status. It never represents a transformed or emulated behavior as exact equivalence.

type GeneratedColumn

type GeneratedColumn struct {
	Expression Expression `json:"expression"`
	Stored     bool       `json:"stored"`
}

GeneratedColumn describes a STORED generated column: `col TYPE GENERATED ALWAYS AS (<expression>) STORED`. Only STORED is supported because it is computed and physically stored identically by SQLite (>= 3.31) and PostgreSQL (>= 12) with this exact syntax; the value is recomputed on the destination from the same deterministic expression, which is the equivalence proof. VIRTUAL is deliberately not supported (PostgreSQL cannot express it), so Stored must be true — a false Stored is rejected by Schema.Validate rather than silently emitting divergent DDL. Expression uses the canonical grammar (compat/sqlparse.go, parseCatalogExpression) and is compiled by the same compileExpression path as any other expression, so an out-of-grammar expression fails at compile time with an explicit error. A generated column cannot also carry a Default and cannot be part of a canonical primary key (both engines restrict this).

type Index

type Index struct {
	Name    string        `json:"name"`
	Table   string        `json:"table"`
	Unique  bool          `json:"unique,omitempty"`
	Columns []IndexColumn `json:"columns"`
	Where   *Expression   `json:"where,omitempty"`
}

type IndexColumn

type IndexColumn struct {
	Column     string      `json:"column,omitempty"`
	Descending bool        `json:"descending,omitempty"`
	Expression *Expression `json:"expression,omitempty"`
}

IndexColumn is one key of an index. Ordinarily the key is a plain column (Column). When Expression is set the key is that catalog expression (Section 3 grammar) compiled inside parentheses — an expression index, supported by SQLite (>= 3.9) and PostgreSQL with identical `(expr)` key syntax — and Column is left empty. Descending applies to either form. Expression is additive and omitted from JSON when nil, so a plain-column index stays byte-identical in canonical metadata and in every emitted statement.

type Inspection

type Inspection struct {
	Schema     Schema          `json:"schema"`
	Exact      bool            `json:"exact"`
	Source     string          `json:"source"`
	Unresolved []CatalogObject `json:"unresolved,omitempty"`
}

type Join

type Join struct {
	Kind  string      `json:"kind"`
	Table TableSource `json:"table"`
	On    Expression  `json:"on"`
}

type MappingStatus

type MappingStatus string
const (
	Exact       MappingStatus = "exact"
	Transformed MappingStatus = "transformed"
	Emulated    MappingStatus = "emulated"
	Unsupported MappingStatus = "unsupported"
	Unknown     MappingStatus = "unknown"
)

type Ordering

type Ordering struct {
	Expression Expression `json:"expression"`
	Descending bool       `json:"descending,omitempty"`
}

type Projection

type Projection struct {
	Expression Expression `json:"expression"`
	Alias      string     `json:"alias,omitempty"`
}

type Reference

type Reference struct {
	Table    string            `json:"table"`
	Columns  []string          `json:"columns"`
	OnUpdate ReferentialAction `json:"on_update,omitempty"`
	OnDelete ReferentialAction `json:"on_delete,omitempty"`
}

type ReferentialAction

type ReferentialAction string
const (
	NoAction   ReferentialAction = "no_action"
	Restrict   ReferentialAction = "restrict"
	Cascade    ReferentialAction = "cascade"
	SetNull    ReferentialAction = "set_null"
	SetDefault ReferentialAction = "set_default"
)

type Routine

type Routine struct {
	Name       string             `json:"name"`
	Parameters []RoutineParameter `json:"parameters,omitempty"`
	Actions    []RoutineAction    `json:"actions"`
}

type RoutineAction

type RoutineAction struct {
	Kind        string       `json:"kind"`
	Table       string       `json:"table"`
	Assignments []Assignment `json:"assignments,omitempty"`
	Where       *Expression  `json:"where,omitempty"`
	// The fields below belong to the read action ("select") and are all additive
	// and omitempty, so an insert/update/delete action serializes byte-identically
	// to how it did before the read action existed.
	//
	// Relation is the source of a "select": a table OR a view. It is a separate
	// field from Table (which names the written table of a write action) because a
	// read may target a view, where the joins/aggregates live — the canonical
	// division that keeps the read action free of joins.
	Relation string `json:"relation,omitempty"`
	// Columns are the DECLARED output columns of a "select". A View carries no
	// output types (it is {Name, Query}) and they cannot be inferred in general
	// (count(*), arbitrary expressions), so the action declares them: this layer
	// declares, it does not guess. The declaration is also the check — a value
	// that is not of the declared family fails loudly in canonicalValue instead of
	// reaching the caller as garbage.
	Columns []RoutineResultColumn `json:"columns,omitempty"`
	// OrderBy is the DECLARED ordering of a "select". Order columns are never
	// caller-supplied: a column name cannot be bound as a placeholder, so taking
	// it from the caller would be an injection path. It is mandatory whenever
	// Limit or Offset is present, because without a total order "page 2" can mean
	// different rows on each engine.
	OrderBy []RoutineOrdering `json:"order_by,omitempty"`
	// Limit and Offset are values, so they MAY come from the caller: each is a
	// routine expression resolving to a non-negative integer ("parameter" or
	// "integer"), bound as a placeholder after the WHERE placeholders. Offset
	// requires Limit: SQLite has no OFFSET without LIMIT, and emitting a synthetic
	// "LIMIT -1" to paper over that would be exactly the silent divergence this
	// layer refuses.
	Limit  *Expression `json:"limit,omitempty"`
	Offset *Expression `json:"offset,omitempty"`
}

type RoutineOrdering added in v0.3.0

type RoutineOrdering struct {
	Column     string `json:"column"`
	Descending bool   `json:"descending,omitempty"`
}

RoutineOrdering is one declared ORDER BY key of a "select" routine action. It is a plain column name (quoted at compile time), never an expression and never a caller-supplied value.

type RoutineParameter

type RoutineParameter struct {
	Name string `json:"name"`
	Type Type   `json:"type"`
}

type RoutineResultColumn added in v0.3.0

type RoutineResultColumn struct {
	Name string `json:"name"`
	Type Type   `json:"type"`
}

RoutineResultColumn is one declared output column of a "select" routine action: the column name as projected by the relation, plus the canonical type family used to canonicalize every scanned value (the same contract as Column.Type on a table). It mirrors RoutineParameter, which declares the inputs.

type Row

type Row map[string]Value

type Schema

type Schema struct {
	Tables  []Table `json:"tables"`
	Indexes []Index `json:"indexes,omitempty"`
	// Domains is the list of named SQL domains (a base type plus an optional
	// CHECK, NOT NULL and DEFAULT). It is additive and omitempty, so a schema
	// without domains stays byte-identical in the canonical JSON and in every
	// emitted statement. PostgreSQL has native domains (CREATE DOMAIN emitted
	// before the tables); SQLite has none, so a column that references a domain is
	// inlined with the domain's base type + CHECK (+ NOT NULL/DEFAULT). See Domain.
	Domains  []Domain  `json:"domains,omitempty"`
	Views    []View    `json:"views,omitempty"`
	Triggers []Trigger `json:"triggers,omitempty"`
	Routines []Routine `json:"routines,omitempty"`
}

Schema is the engine-neutral representation used before emitting SQLite or PostgreSQL DDL. Every engine-specific construct must be represented as an explicit capability rather than hidden in a raw SQL string.

func (Schema) Validate

func (s Schema) Validate() error

type SearchResult

type SearchResult struct {
	ID string `json:"id"`
}

type SelectQuery

type SelectQuery struct {
	// With is the list of non-recursive common table expressions (CTEs) that
	// precede this query: WITH name AS (SELECT ...), ... <query>. Each CTE query
	// is itself a SelectQuery in the same bounded grammar. WITH RECURSIVE is
	// deliberately not modeled and is rejected at parse time, because its
	// termination and row-ordering semantics are hard to guarantee byte-identical
	// between SQLite and PostgreSQL. Absent CTEs leave the query byte-identical,
	// and the field is omitted from JSON so existing view snapshots do not change.
	With     []CommonTableExpr `json:"with,omitempty"`
	Distinct bool              `json:"distinct,omitempty"`
	Columns  []Projection      `json:"columns"`
	From     TableSource       `json:"from"`
	Joins    []Join            `json:"joins,omitempty"`
	Where    *Expression       `json:"where,omitempty"`
	GroupBy  []Expression      `json:"group_by,omitempty"`
	Having   *Expression       `json:"having,omitempty"`
	// Compounds is the left-associative chain of set operations applied after
	// this (the leading) SELECT: q0 op1 q1 op2 q2 ... The trailing OrderBy,
	// Limit and Offset below apply to the whole compound, not to the last
	// branch, so each CompoundSelect.Query carries none of them. Absent
	// compounds leave the single-SELECT behavior byte-identical, and the field
	// is omitted from JSON so existing view snapshots do not change.
	Compounds []CompoundSelect `json:"compounds,omitempty"`
	OrderBy   []Ordering       `json:"order_by,omitempty"`
	Limit     *int             `json:"limit,omitempty"`
	Offset    *int             `json:"offset,omitempty"`
}

type Snapshot

type Snapshot struct {
	Schema Schema           `json:"schema"`
	Rows   map[string][]Row `json:"rows"`
}

type Store

type Store struct {
	Target Target
	DB     *sql.DB
}

Store is the database/sql boundary for a concrete engine. All values crossing this boundary are converted into the canonical Value representation.

func OpenPostgres

func OpenPostgres(version Version, dsn string) (*Store, error)

func OpenSQLite

func OpenSQLite(version Version, dsn string) (*Store, error)

func OpenStore

func OpenStore(target Target, dsn string) (*Store, error)

func (*Store) ApplyChanges

func (store *Store) ApplyChanges(ctx context.Context, schema Schema, changes []Change) error

ApplyChanges applies one ordered source stream atomically. Reapplying the same stream is safe: committed source sequences are recorded in the target transaction and skipped on subsequent attempts.

func (*Store) ApplyChangesTolerant

func (store *Store) ApplyChangesTolerant(ctx context.Context, schema Schema, changes []Change) error

ApplyChangesTolerant applies one ordered source stream atomically with an opt-in, catch-up conflict policy. It exists for zero-window migrations whose capture-install → snapshot → catch-up sequence inherently overlaps: a change journaled after capture was installed may already have traveled inside the snapshot, so re-applying it would trip a spurious ConflictError even though the destination already reflects the change's final state.

The only difference from ApplyChanges is conflict resolution. A change is treated as already applied — and recorded in __compat_applied_changes as if it had just been applied — when the destination's CURRENT state already equals the change's FINAL state:

  • insert: the row already exists and rowsEqual(after, actual);
  • update: the row exists and rowsEqual(after, actual) even though Before no longer matches (the snapshot already carried the after state);
  • delete: the row no longer exists.

Any other divergence remains a strict ConflictError: the tolerant mode is a catch-up convenience, not a bypass. ApplyChanges is unchanged.

func (*Store) ApplySchema

func (store *Store) ApplySchema(ctx context.Context, schema Schema) error

func (*Store) CallRoutine

func (store *Store) CallRoutine(ctx context.Context, schema Schema, name string, arguments map[string]Value) error

CallRoutine executes a canonical routine inside one transaction. The routine is stored in the canonical schema and therefore has the same implementation on SQLite and PostgreSQL instead of relying on engine-specific languages.

func (*Store) Close

func (store *Store) Close() error

func (*Store) DropTable added in v0.2.0

func (store *Store) DropTable(ctx context.Context, table string) error

DropTable compiles (CompileDropTable) and executes the drop against this store's database. Engine errors — a missing table, or a foreign key from another table pointing at this one — are wrapped with the operation name and propagated; nothing is retried with CASCADE.

Note on the change-capture triggers installed by InstallChangeCapture: both engines drop the triggers attached to the table together with the table itself. On PostgreSQL the trigger FUNCTIONS (`__compat_capture_<table>_<kind>_fn`) are separate catalog objects and survive the drop, orphaned but harmless; re-running InstallChangeCapture recreates them with CREATE OR REPLACE. The rows already written to `__compat_change_journal` for the dropped table are also kept on both engines — the journal is an append-only history and this package never deletes data implicitly.

func (*Store) DropTableIfExists added in v0.2.0

func (store *Store) DropTableIfExists(ctx context.Context, table string) error

DropTableIfExists is DropTable with the idempotent statement; see CompileDropTableIfExists for why the choice is explicit in the name.

func (*Store) ExportSnapshot

func (store *Store) ExportSnapshot(ctx context.Context, schema Schema) (Snapshot, error)

func (*Store) ImportSnapshot

func (store *Store) ImportSnapshot(ctx context.Context, snapshot Snapshot) error

ImportSnapshot creates the canonical schema and inserts every canonical row. It is intentionally append-only; replacement and synchronization policies are handled by higher layers so data is never deleted implicitly.

func (*Store) InspectSchema

func (store *Store) InspectSchema(ctx context.Context) (Inspection, error)

InspectSchema reconstructs the exact canonical schema when the database was managed by this compatibility layer. For external databases it falls back to catalog inspection and explicitly reports objects not yet translated.

func (*Store) InstallChangeCapture

func (store *Store) InstallChangeCapture(ctx context.Context, schema Schema) error

InstallChangeCapture installs engine-native triggers which journal every committed row mutation in canonical, ordered form.

func (*Store) IsUniqueViolation added in v0.4.0

func (store *Store) IsUniqueViolation(err error) bool

IsUniqueViolation reports whether err is the engine's way of saying "a unique constraint rejected this write". It is a classifier over the driver error, not a new error type: nothing is wrapped, replaced or re-raised.

It exists because the portable-looking way to detect a duplicate is to match the text `UNIQUE constraint failed`, which is SQLite's wording. PostgreSQL says `duplicate key value violates unique constraint "..."` and reports SQLSTATE 23505, so a consumer that migrates keeps compiling and silently stops detecting duplicates: a clean 400 turns into a 500, and nobody finds out until a user duplicates a value in production. That regression has no compiler signal, which is what makes it worth a primitive.

Classification is by **structured code**, never by message text:

  • SQLite (modernc.org/sqlite): `*sqlite.Error` with the extended result code SQLITE_CONSTRAINT_UNIQUE (2067) or SQLITE_CONSTRAINT_PRIMARYKEY (1555). Both are included on purpose — a primary key IS a unique constraint, and PostgreSQL reports a duplicate primary key with the very same 23505 it uses for a UNIQUE index. Leaving 1555 out would make the two engines disagree on the identical write.
  • PostgreSQL (github.com/jackc/pgx/v5): `*pgconn.PgError` with SQLSTATE 23505 (unique_violation), which covers both UNIQUE and PRIMARY KEY.

Both lookups go through errors.As, so an error the consumer has wrapped — `fmt.Errorf("save entry: %w", err)`, which is how it will actually arrive after crossing a repository layer — is still classified. A nil error is false.

It dispatches on this store's engine rather than trying both drivers: the answer must be the one the engine actually connected to would give, and a PostgreSQL error reaching a SQLite store is a bug in the caller that should not be papered over by a lucky match.

**Documented limitation — it returns a boolean, and cannot tell you WHICH constraint was violated.** That is a deliberate stop, not an oversight: the engines report different granularities. SQLite names the `table.column` list in the message; PostgreSQL names the CONSTRAINT, which it also auto-generates when the declaration does not fix one (`zzt_email_key`, `zzt_pkey`). Exposing "which constraint" would mean mapping engine-chosen names back onto the canonical schema — a larger API with its own ambiguities. The consequence the caller must know: **with two unique constraints on the same table you cannot tell which one was violated**, on either engine. If a caller needs to distinguish them (to report a different field to the user), the portable way is to check the candidate values before the write, not to parse this error.

func (*Store) QueryRoutine added in v0.3.0

func (store *Store) QueryRoutine(ctx context.Context, schema Schema, name string, arguments map[string]Value) ([]Row, error)

QueryRoutine executes a canonical READ routine and returns canonical rows. It is the read counterpart of CallRoutine, which stays write-only and unchanged: CallRoutine returns only error and has production consumers, so reading is new, additive surface rather than a signature change.

The routine must contain exactly one action, of kind "select" (enforced by Schema.Validate too). The statement is composed from pieces that already exist: compileRoutineWhere for the filter — which emits the right placeholder per engine and binds every caller value instead of inlining it, and already maps LIKE to ILIKE on PostgreSQL — and the exportTable pattern for reading, which canonicalizes every scanned value by type family. Here the family comes from the action's DECLARED output columns, because a view has no output types.

Every identifier (relation, output columns, order keys) goes through quoteIdentifier; every caller value, including LIMIT and OFFSET, is bound as a placeholder. Nothing supplied by the caller is ever concatenated into the SQL.

func (*Store) ReadCapturedChanges

func (store *Store) ReadCapturedChanges(ctx context.Context, schema Schema, after uint64, limit int) ([]Change, error)

ReadCapturedChanges reads an ordered source stream after the supplied cursor.

func (*Store) SearchText

func (store *Store) SearchText(ctx context.Context, table, idColumn string, textColumns []string, query string) ([]SearchResult, error)

SearchText implements deterministic Unicode token matching in the common Go runtime. It does not delegate tokenization or ranking to either database.

func (*Store) TableExists added in v0.4.0

func (store *Store) TableExists(ctx context.Context, table string) (bool, error)

TableExists reports whether the table physically exists in the engine's own catalog: `sqlite_master` on SQLite, `pg_class` on PostgreSQL (that relation does not exist there, and `sqlite_master` has no PostgreSQL equivalent, which is exactly why the consumer cannot write this once).

It does NOT consult `__compat_schema`, and that is the whole point of the method rather than an implementation detail. InspectSchema prefers the stored canonical metadata over the physical catalog, which is right for the canonical path and wrong in precisely the situations where this question gets asked: a hand-written migration, or a half-finished table rebuild, where the metadata is the thing about to be rewritten and therefore cannot be the oracle. That cache has already caused four incidents across this family of projects. This method is the honest way out — it asks the engine, not the cache — and callers should read a disagreement between TableExists and InspectSchema as a real signal, not as a bug.

Engine notes, verified against real engines rather than assumed:

  • PostgreSQL: the lookup is restricted to `current_schema()`, which is the first existing schema of the session `search_path` and therefore the exact place an unqualified `CREATE TABLE` from ApplySchema lands. Asking any other schema of the database — or asking without a schema filter — would answer a different question than "did ApplySchema create it here?".
  • A VIEW with the searched name answers false on both engines: SQLite filters `type = 'table'` and PostgreSQL filters `relkind IN ('r','p')` (ordinary and partitioned tables). `information_schema.tables` is deliberately not used on PostgreSQL because it also lists views.
  • The name is matched as a value through a bound placeholder, never spliced into the SQL, so it needs no quoting and a name containing a quote or a semicolon is simply a name. It is matched **byte for byte** on both engines: this package quotes every identifier it emits, so `CREATE TABLE "Foo"` stores `Foo` in both catalogs and neither engine folds the case. `TableExists(ctx, "foo")` is therefore false for a table declared as `Foo`, identically on SQLite and PostgreSQL — pass the name exactly as it was declared in the canonical schema.

The four reserved internal tables are NOT rejected here (unlike DropTable): the question is physical, and `__compat_schema` really does exist once this package has written it.

type Table

type Table struct {
	Name        string       `json:"name"`
	Columns     []Column     `json:"columns"`
	Constraints []Constraint `json:"constraints,omitempty"`
}

type TableSource

type TableSource struct {
	Table    string       `json:"table,omitempty"`
	Alias    string       `json:"alias,omitempty"`
	Subquery *SelectQuery `json:"subquery,omitempty"`
}

TableSource is a FROM/JOIN source: either a named table (Table) or a derived table (Subquery). Exactly one of Table or Subquery is set. A derived table is a full SelectQuery evaluated as a table; both engines give it identical results (it cannot be correlated with the enclosing query in standard SQL). A derived table requires an Alias. Subquery is omitted from JSON when absent, so existing table-source snapshots do not change.

type Target

type Target struct {
	Engine  Engine  `json:"engine"`
	Version Version `json:"version"`
}

func (Target) Validate

func (t Target) Validate() error

type Trigger

type Trigger struct {
	Name    string          `json:"name"`
	Table   string          `json:"table"`
	Timing  string          `json:"timing"`
	Event   string          `json:"event"`
	When    *Expression     `json:"when,omitempty"`
	Actions []TriggerAction `json:"actions"`
}

type TriggerAction

type TriggerAction struct {
	Kind        string       `json:"kind"`
	Table       string       `json:"table"`
	Assignments []Assignment `json:"assignments,omitempty"`
	Where       *Expression  `json:"where,omitempty"`
}

type Type

type Type struct {
	Family TypeFamily `json:"family"`
	// Arguments preserve details such as precision, scale, length or array
	// dimensions without coupling the canonical schema to SQL text.
	Arguments []int `json:"arguments,omitempty"`
}

type TypeFamily

type TypeFamily string
const (
	BooleanType   TypeFamily = "boolean"
	IntegerType   TypeFamily = "integer"
	DecimalType   TypeFamily = "decimal"
	FloatType     TypeFamily = "float"
	TextType      TypeFamily = "text"
	BinaryType    TypeFamily = "binary"
	DateType      TypeFamily = "date"
	TimestampType TypeFamily = "timestamp"
	JSONType      TypeFamily = "json"
	UUIDType      TypeFamily = "uuid"
	VectorType    TypeFamily = "vector"
)

type Value

type Value struct {
	Kind  ValueKind `json:"kind"`
	Value string    `json:"value,omitempty"`
}

Value keeps data independent from either driver's Go representation. Values are encoded canonically before persistence in a migration artifact or change journal, preserving null and type information across both engines.

type ValueKind

type ValueKind string
const (
	NullValue      ValueKind = "null"
	BooleanValue   ValueKind = "boolean"
	IntegerValue   ValueKind = "integer"
	DecimalValue   ValueKind = "decimal"
	FloatValue     ValueKind = "float"
	TextValue      ValueKind = "text"
	BinaryValue    ValueKind = "binary"
	DateValue      ValueKind = "date"
	TimestampValue ValueKind = "timestamp"
	JSONValue      ValueKind = "json"
	UUIDValue      ValueKind = "uuid"
	VectorValue    ValueKind = "vector"
)

type VerificationReport

type VerificationReport struct {
	SourceDigest      string `json:"source_digest"`
	DestinationDigest string `json:"destination_digest"`
	Equivalent        bool   `json:"equivalent"`
}

func VerifySnapshots

func VerifySnapshots(source, destination Snapshot) (VerificationReport, error)

type Version

type Version struct {
	Major int `json:"major"`
	Minor int `json:"minor"`
	Patch int `json:"patch"`
}

func (Version) String

func (v Version) String() string

func (Version) Valid

func (v Version) Valid() bool

type View

type View struct {
	Name  string      `json:"name"`
	Query SelectQuery `json:"query"`
}

Jump to

Keyboard shortcuts

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