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
112 changes: 95 additions & 17 deletions TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2525,7 +2525,7 @@ Verify the database name, or run the command against the SchemaBot instance that
```sql
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);
```
Rows: 3,500,000 / 7,200,000 · ETA: 5m 30s
- Rows: 3,500,000 / 7,200,000 · ETA: 5m 30s


---
Expand Down Expand Up @@ -2556,7 +2556,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);
```
Rows: 3,500,000 / 7,200,000 · ETA: 5m 30s
- Rows: 3,500,000 / 7,200,000 · ETA: 5m 30s


---
Expand Down Expand Up @@ -2639,7 +2639,7 @@ schemabot apply -e staging
```sql
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);
```
Rows: 156,342 / 397,453
- Rows: 156,342 / 397,453


---
Expand Down Expand Up @@ -2718,7 +2718,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```
Rows: 321,450 / 1,466,232 · ETA: 5m 40s
- Rows: 321,450 / 1,466,232 · ETA: 5m 40s

**`users`**: ⏳ Queued

Expand Down Expand Up @@ -2765,7 +2765,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows: 914,707 / 1,466,232 · ETA: 3m 15s
- Rows: 914,707 / 1,466,232 · ETA: 3m 15s

**`products`**: ⏳ Queued

Expand Down Expand Up @@ -2828,6 +2828,54 @@ ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```


---

To stop this schema change:
```
schemabot stop apply-a1b2c3d4e5f6 -e staging
```

_Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:00 UTC</relative-time> (2026-01-01 00:00:00 UTC)_

</details>

<details>
<summary><a name="second-table-throttled"></a><strong>Second Table Throttled</strong></summary>


## Schema Change Status — Staging

**Database**: `testapp` | **Apply ID**: `apply-a1b2c3d4e5f6`

*Applied by @jackjackbits at 2026-01-01 00:00:00 UTC*

**Status**: In Progress

📊 1/3 complete · 1 running (62%) · 1 queued

**Schema `testapp`**

**`users`**: 🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦⬜⬜⬜⬜⬜⬜⬜⬜ 62% (throttled)

```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
- Rows: 914,707 / 1,466,232 · ETA: 3m 15s
- ℹ️ _Throttled: commit-latency 112.4ms >= 100ms_

**`products`**: ⏳ Queued

```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
```

**`orders`**: 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Complete

```sql
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```


---

To stop this schema change:
Expand Down Expand Up @@ -2860,7 +2908,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows copied: 1,466,232
- Rows copied: 1,466,232

**`products`**: ⏳ Queued

Expand Down Expand Up @@ -2907,7 +2955,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows verified: 321,450 / 1,466,232
- Rows verified: 321,450 / 1,466,232

**`products`**: ⏳ Queued

Expand Down Expand Up @@ -2954,7 +3002,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows copied: 1,466,232
- Rows copied: 1,466,232

**`products`**: ⏳ Queued

Expand Down Expand Up @@ -3001,7 +3049,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
```
Rows: 87,231 / 523,140 · ETA: 7m 0s
- Rows: 87,231 / 523,140 · ETA: 7m 0s

**`orders`**: 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Complete

Expand Down Expand Up @@ -3046,7 +3094,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows: 914,707 / 1,466,232 · ETA: 3m 15s
- Rows: 914,707 / 1,466,232 · ETA: 3m 15s
└ shards: ✓ -40 · ◐ 40-80 62% · ◐ 80-c0 31% · ⏳ c0-


Expand Down Expand Up @@ -3080,7 +3128,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `orders` ADD COLUMN `region` varchar(32);
```
Rows: 4,200,000,000 / 6,000,000,000 · ETA: 1h 30m
- Rows: 4,200,000,000 / 6,000,000,000 · ETA: 1h 30m
└ 256 shards: 200 ✓ · 52 ◐ copying · 4 ⏳ · slowest f7- 12%


Expand Down Expand Up @@ -3453,7 +3501,7 @@ schemabot apply -e staging
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows: 1,055,687 / 1,466,232
- Rows: 1,055,687 / 1,466,232

**`orders`**: 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Complete

Expand Down Expand Up @@ -3928,7 +3976,7 @@ Use `schemabot status -e <environment>` to find the apply ID.
```sql
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);
```
Rows: 2,300,000 / 7,200,000 · ETA: 13m 0s
- Rows: 2,300,000 / 7,200,000 · ETA: 13m 0s


---
Expand Down Expand Up @@ -3967,7 +4015,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);
```
Rows: 2,300,000 / 7,200,000
- Rows: 2,300,000 / 7,200,000


---
Expand Down Expand Up @@ -4127,7 +4175,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
```sql
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);
```
Rows: 2,300,000 / 7,200,000
- Rows: 2,300,000 / 7,200,000


---
Expand Down Expand Up @@ -4653,7 +4701,7 @@ ALTER TABLE `events` ADD INDEX `idx_created_at`(`created_at`);
```sql
ALTER TABLE `users` DROP INDEX `idx_email`;
```
Rows: 45,000 / 100,000
- Rows: 45,000 / 100,000


---
Expand Down Expand Up @@ -4970,6 +5018,36 @@ Sequential mode: First complete, second catching up on accumulated changes
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);


```
</details>

<details>
<summary><a name="mysql-multitable-second-table-throttled"></a><strong>MySQL: Multi-table Second Table Throttled</strong></summary>

```

Sequential mode: First complete, second paused by the engine's throttler

┌──────────────────────────────────┐
│ Apply ID: apply-a1b2c3d4e5f6 │
│ State: Running │
│ Started: Jan 15 14:10:00 UTC │
│ Duration: 20m │
└──────────────────────────────────┘


~ orders: 🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦⬜⬜⬜⬜⬜⬜⬜⬜ 62% (throttled)
ALTER TABLE `orders` ADD INDEX `idx_user_status`(`user_id`, `status`);
• Rows: 3,100,000 / 5,000,000
• ℹ️ Throttled: commit-latency 112.4ms >= 100ms

~ products: ⏳ Queued
ALTER TABLE `products` ADD COLUMN `weight_grams` int DEFAULT 0;

~ users: 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✓ Complete
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);


```
</details>

Expand Down Expand Up @@ -6360,7 +6438,7 @@ SchemaBot triggers cutover automatically — no action needed.
```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```
Rows: 914,707 / 1,466,232 · ETA: 3m 15s
- Rows: 914,707 / 1,466,232 · ETA: 3m 15s

**`products`**: ⏳ Queued

Expand Down
22 changes: 20 additions & 2 deletions docs/spirit_progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ key to debugging stale-progress issues.
| `CurrentState` | `status.State` | Atomic int32 enum: `Initial`, `CopyRows`, `WaitingOnSentinelTable`, `Checksum`, `CutOver`, `Close`, ... |
| `Summary` | `string` | `"71436/221193 32.30% copyRows ETA 5m 30s"` |
| `Tables[]` | `[]TableProgress` | Per-table: `TableName`, `RowsCopied` (uint64), `RowsTotal` (uint64), `IsComplete` (bool) |
| `Resume` | `bool` | True only after the runner successfully resumed from its durable checkpoint; a fresh start (or an abandoned resume attempt) reports false. |
| `Throttle` | `status.ThrottleStatus` | `Throttled` (bool), `Reason` (display-only string, `"<signal> <observed> <op> <threshold>"`), `Utilization` (float64, 0 means unknown — never render it as idle). |

Key details:
- **ETA is embedded in `Summary`**, not a separate field. Downstream layers parse it out with a regex.
Expand All @@ -59,6 +61,12 @@ Key details:
`CurrentState == Checksum`** — it reads zero before the verify starts and again after it
finishes (including during `PostChecksum`). A renderer that shows checksum counters for any
other phase shows zeros.
- **`Throttle` is populated only for the paced phases** — the row copy and the checksum
verify report the throttler's live status; every other phase reports the zero
`ThrottleStatus{}`, so a finished schema change can never look paused on an aged-out
signal. A composite throttler joins the reasons of every currently-throttling child with
`"; "`, and `Throttled` can be true with an empty `Reason` (a throttler that implements
no reason extension).

## Spirit runner lifecycle

Expand Down Expand Up @@ -114,6 +122,14 @@ Two properties matter for display:
- Stamps the runner-wide checksum estimate (`ChecksumRowsChecked`/`Total`) on
every table unconditionally — Spirit populates it only during the verify
phase, and every copy is complete by then.
- Stamps the runner-wide throttle status (`Throttled`/`ThrottleReason`) on the
tables participating in paced work — a table still copying, or every table
during the verify (`tableInPacedPhase`) — so a completed table is never
rendered as paused by another table's copy. The reason passes through
`engine.SanitizeThrottleReason` so it can never break an operator surface.
4. Sets `ProgressResult.ResumedFromCheckpoint` from `Progress.Resume`, which the
drive turns into a one-per-claim "resumed from checkpoint" timeline event
(`logEngineResumeOnce`).

Key types: `engine.ProgressResult`, `engine.TableProgress` (`pkg/engine/engine.go`).

Expand Down Expand Up @@ -188,7 +204,7 @@ no live result for a reader on another pod, and the drive keeps stored current.
2. Picks the most relevant task (priority: active > stopped > pending > terminal).
3. Builds the per-table response from the stored task rows: `Status`,
`RowsCopied`, `RowsTotal`, `ProgressPercent`, `ETASeconds`,
`ChecksumRowsChecked/Total`. When per-shard rows are persisted, the table
`ChecksumRowsChecked/Total`, `Throttled`, `ThrottleReason`. When per-shard rows are persisted, the table
headline is the aggregate of those rows, computed at read time.

Reading stored state matters for:
Expand All @@ -212,6 +228,8 @@ Task fields updated during polling (`storage.Task` in `pkg/storage/types.go`):
| `ETASeconds` | int | From `engine.TableProgress.ETASeconds` |
| `ChecksumRowsChecked` | int64 | From `engine.TableProgress.ChecksumRowsChecked` |
| `ChecksumRowsTotal` | int64 | From `engine.TableProgress.ChecksumRowsTotal` |
| `Throttled` | bool | From `engine.TableProgress.Throttled`; cleared when the task comes to rest (`transitionTaskState`) |
| `ThrottleReason` | string | From `engine.TableProgress.ThrottleReason`, sanitized at every ingest boundary; empty whenever `Throttled` is false |
| `IsInstant` | bool | From `engine.TableProgress.IsInstant` |
| `State` | string | Mapped from `engine.State` |
| `StartedAt` | time | Set when task transitions to RUNNING |
Expand All @@ -222,7 +240,7 @@ Task fields updated during polling (`storage.Task` in `pkg/storage/types.go`):

| Trigger | What writes | Frequency | Fields updated |
|---------|-------------|-----------|----------------|
| `pollForCompletionAtomic` (atomic mode) | Poller goroutine | Every 500ms | `State`, `RowsCopied`, `RowsTotal`, `ProgressPercent`, `ETASeconds`, `UpdatedAt`, `CompletedAt` (on terminal) |
| `pollForCompletionAtomic` (atomic mode) | Poller goroutine | Every 500ms | `State`, `RowsCopied`, `RowsTotal`, `ProgressPercent`, `ETASeconds`, `Throttled`, `ThrottleReason`, `UpdatedAt`, `CompletedAt` (on terminal) |
| `pollTaskToCompletion` (sequential mode) | Poller goroutine | Every 500ms | Same as above, plus `IsInstant` |
| `LocalClient.Stop()` | Stop handler | Once, after `eng.Stop()` blocks | `State` → STOPPED (or COMPLETED if table finished), `RowsCopied`, `RowsTotal`, `ProgressPercent`, `ETASeconds`, `CompletedAt` |
| `LocalClient.Progress()` | Progress handler | On each API call (if engine state changed and task is non-terminal) | `State`, `UpdatedAt`, `CompletedAt` |
Expand Down
4 changes: 2 additions & 2 deletions e2e/consumermodule/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// version imported by pkg/api/telemetry.go; the test guards this precondition.
module github.com/block/schemabot/e2e/consumermodule

go 1.26.5
go 1.26.6

require (
github.com/block/schemabot v0.0.0
Expand Down Expand Up @@ -37,7 +37,7 @@ require (
github.com/aws/smithy-go v1.27.7 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/block/pg-sprite v0.0.0-20260814025010-d6cf677e4feb // indirect
github.com/block/spirit v0.16.1-0.20260808013537-864cf2ee6b6b // indirect
github.com/block/spirit v0.16.1-0.20260815035524-fc3efbddfa34 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions e2e/consumermodule/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/block/pg-sprite v0.0.0-20260814025010-d6cf677e4feb h1:SppN5gzk5OT9XQtMopxlUNXzyE8d791Q+kz/HldCips=
github.com/block/pg-sprite v0.0.0-20260814025010-d6cf677e4feb/go.mod h1:vZxHdTMrCOPAYgswveB7PSjOaOuRgnDLGRw6WoOizRg=
github.com/block/spirit v0.16.1-0.20260808013537-864cf2ee6b6b h1:Z12hyvAmkzjVlZeKHejfRbr+nytBB6DeneF+aAmI/Rs=
github.com/block/spirit v0.16.1-0.20260808013537-864cf2ee6b6b/go.mod h1:aR9KJ8sca3Lo5IpwH6ZGRqK2zdl4Ukhuh7jNdBCRdDk=
github.com/block/spirit v0.16.1-0.20260815035524-fc3efbddfa34 h1:J8YV309/3aJzIdvX/3TV//UnQs1plvNW+a0RMz9Vzlc=
github.com/block/spirit v0.16.1-0.20260815035524-fc3efbddfa34/go.mod h1:mhA5cAELBCJq1jM7uaWnmzLFYBVY1XZ++yhiGJzpwps=
github.com/block/tidb/pkg/parser v0.0.0-20260506200501-e528fd979fc8 h1:+OfdTacrEyjlqcRUpBFX9uJ6ROBq6cUjwY4DClhnsdU=
github.com/block/tidb/pkg/parser v0.0.0-20260506200501-e528fd979fc8/go.mod h1:zDLDsfNBU5+L6T4J9/OgWAHc/WZvMUjbpgHqQ/t3yKo=
github.com/block/vitess v0.0.0-20260703150944-881ec2298245 h1:R7e7uAxl6WIZpeY957JDsrZtuihck6vm7QgRooI295U=
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/block/schemabot

go 1.26.5
go 1.26.6

require (
github.com/alecthomas/kong v1.16.0
Expand All @@ -10,7 +10,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.44.5
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3
github.com/block/pg-sprite v0.0.0-20260814025010-d6cf677e4feb
github.com/block/spirit v0.16.1-0.20260808013537-864cf2ee6b6b
github.com/block/spirit v0.16.1-0.20260815035524-fc3efbddfa34
github.com/bradleyfalzon/ghinstallation/v2 v2.18.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/block/pg-sprite v0.0.0-20260814025010-d6cf677e4feb h1:SppN5gzk5OT9XQtMopxlUNXzyE8d791Q+kz/HldCips=
github.com/block/pg-sprite v0.0.0-20260814025010-d6cf677e4feb/go.mod h1:vZxHdTMrCOPAYgswveB7PSjOaOuRgnDLGRw6WoOizRg=
github.com/block/spirit v0.16.1-0.20260808013537-864cf2ee6b6b h1:Z12hyvAmkzjVlZeKHejfRbr+nytBB6DeneF+aAmI/Rs=
github.com/block/spirit v0.16.1-0.20260808013537-864cf2ee6b6b/go.mod h1:aR9KJ8sca3Lo5IpwH6ZGRqK2zdl4Ukhuh7jNdBCRdDk=
github.com/block/spirit v0.16.1-0.20260815035524-fc3efbddfa34 h1:J8YV309/3aJzIdvX/3TV//UnQs1plvNW+a0RMz9Vzlc=
github.com/block/spirit v0.16.1-0.20260815035524-fc3efbddfa34/go.mod h1:mhA5cAELBCJq1jM7uaWnmzLFYBVY1XZ++yhiGJzpwps=
github.com/block/tidb/pkg/parser v0.0.0-20260506200501-e528fd979fc8 h1:+OfdTacrEyjlqcRUpBFX9uJ6ROBq6cUjwY4DClhnsdU=
github.com/block/tidb/pkg/parser v0.0.0-20260506200501-e528fd979fc8/go.mod h1:zDLDsfNBU5+L6T4J9/OgWAHc/WZvMUjbpgHqQ/t3yKo=
github.com/block/vitess v0.0.0-20260703150944-881ec2298245 h1:R7e7uAxl6WIZpeY957JDsrZtuihck6vm7QgRooI295U=
Expand Down
6 changes: 6 additions & 0 deletions pkg/api/progress_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ func progressResponseFromProto(resp *ternv1.ProgressResponse) *apitypes.Progress
ETASeconds: t.EtaSeconds,
ChecksumRowsChecked: t.ChecksumRowsChecked,
ChecksumRowsTotal: t.ChecksumRowsTotal,
Throttled: t.Throttled,
ThrottleReason: t.ThrottleReason,
IsInstant: t.IsInstant,
ProgressDetail: t.ProgressDetail,
TaskID: t.TaskId,
Expand Down Expand Up @@ -1148,6 +1150,8 @@ func (s *Service) progressFromLocalStorage(ctx context.Context, apply *storage.A
PercentComplete: int32(task.ProgressPercent),
ChecksumRowsChecked: task.ChecksumRowsChecked,
ChecksumRowsTotal: task.ChecksumRowsTotal,
Throttled: task.Throttled,
ThrottleReason: task.ThrottleReason,
IsInstant: task.IsInstant,
TaskID: task.TaskIdentifier,
}
Expand Down Expand Up @@ -1215,6 +1219,8 @@ func (s *Service) syncTasksFromTern(ctx context.Context, apply *storage.Apply, t
task.ProgressPercent = int(tp.PercentComplete)
task.ChecksumRowsChecked = tp.ChecksumRowsChecked
task.ChecksumRowsTotal = tp.ChecksumRowsTotal
task.Throttled = tp.Throttled
task.ThrottleReason = tp.ThrottleReason
task.UpdatedAt = now
if err := s.storage.Tasks().Update(ctx, task); err != nil {
s.logger.Error("sync task failed", append(task.LogAttrs(), "error", err)...)
Expand Down
Loading
Loading