Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ All SQL statements processed by SchemaBot **must be parseable by the dialect's r

### Storage Schema (Self-Bootstrapping)

SchemaBot's storage schema is self-bootstrapping via `EnsureSchema` (`pkg/api/ensure_schema.go`), which runs on every server startup before accepting traffic and routes to a per-dialect bootstrapper. On MySQL it reads all embedded SQL files from `pkg/schema/mysql/`, diffs them against the live database using Spirit, and applies any DDL needed — adding a new table or column to `pkg/schema/mysql/` is all that's needed; the next deploy picks it up automatically. On PostgreSQL (`pkg/api/ensure_schema_postgres.go`) it creates missing tables from `pkg/schema/postgres/` and verifies that existing tables contain every expected column and unique index, failing startup when one is missing; a missing non-unique index only logs a startup warning, and it never alters existing tables or rejects extra columns. Apply column changes to already-bootstrapped PostgreSQL databases before deploying schema files that expect them. Keep the two dialect directories in lockstep; the schema parity tests in `pkg/schema` pin this.
SchemaBot's storage schema is self-bootstrapping via `EnsureSchema` (`pkg/api/ensure_schema.go`), which runs on every server startup before accepting traffic and routes to a per-dialect bootstrapper. On MySQL it reads all embedded SQL files from `pkg/schema/mysql/`, diffs them against the live database using Spirit, and applies any DDL needed — adding a new table or column to `pkg/schema/mysql/` is all that's needed; the next deploy picks it up automatically. On PostgreSQL (`pkg/api/ensure_schema_postgres.go`) it transactionally creates missing tables, columns, and indexes under the bootstrap advisory lock. PostgreSQL convergence is additive-only: it tolerates extra objects and checks columns by presence, while a missing `NOT NULL` column without a `DEFAULT` fails startup with a manual-remediation error. Keep the two dialect directories in lockstep; the schema parity tests in `pkg/schema` pin this.

### SQL Schema

Expand Down
47 changes: 29 additions & 18 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,23 +762,34 @@ on the storage dialect:
tables carry a long history, create a newly declared index by hand before
rolling out: the startup diff then finds nothing to do, instead of copying
the table inside the budget on every pod.
- **PostgreSQL** creates missing tables and verifies that existing tables
contain every column and standalone unique index declared by the embedded
schema. Missing objects fail startup with the affected table and objects
identified; extra columns are tolerated, and a missing non-unique index is
tolerated with a startup warning naming it. Column verification is
presence-only: type, length, and nullability drift
is outside its scope and is not detected. Existing tables are never altered,
and `allow_destructive_schema_changes` has no effect because this flow never
produces destructive DDL. Apply column changes to already-bootstrapped
PostgreSQL databases before deploying schema files that expect them.

Non-unique indexes work the same way, and the consequence is quieter: an
index added to an embedded schema file reaches newly created databases only,
so an already-bootstrapped database keeps answering the queries that index
was added for — correctly, but without it, and startup warns about the gap
on every deploy until it is closed. Create those by hand. A database
bootstrapped before `idx_plans_created_at` was added to `plans` needs:
- **PostgreSQL** automatically creates missing tables, columns, and standalone
indexes. It discovers drift before taking the bootstrap advisory lock, then
re-checks and applies each table's changes transactionally under that lock.
A missing `NOT NULL` column without a `DEFAULT`, a generated or identity
column, or a column with a constraint shape not explicitly classified as
safe fails startup with instructions for manual remediation. Generated and
identity columns rewrite the populated table under an exclusive lock.
Startup also fails when additive DDL cannot be parsed or executed, or when
re-verification finds unresolved drift.

Convergence is additive-only: extra columns and indexes remain in place for
binary rollback, and `allow_destructive_schema_changes` has no effect because
this flow never produces destructive DDL. Column verification remains
presence-only, so type, length, and nullability drift is outside its scope and
is not detected.

Indexes added to an embedded schema file after a database was bootstrapped
converge on the next startup as plain `CREATE INDEX` statements, each in
its own transaction under the bootstrap advisory lock. A plain
`CREATE INDEX` holds a `SHARE` lock on the table for the full build and
blocks writes to it, and the startup budget is the build's only duration
ceiling, so on a deployment whose storage tables carry a long history,
pre-create the index by hand before rolling out — the startup diff then
finds it present and skips the build. The indexes below are the ones a
long-lived database is most likely to be missing.

A database bootstrapped before `idx_plans_created_at` was added to `plans`
needs:

```sql
CREATE INDEX idx_plans_created_at ON plans (created_at);
Expand All @@ -793,7 +804,7 @@ on the storage dialect:
```

Without it, every driver claim sorts the full claimable set before taking
one row, which slows claiming as apply history grows. And one bootstrapped
one row, which slows claiming as apply history grows. One bootstrapped
before refused applies started naming the schema change holding the
database needs:

Expand Down
Loading
Loading