Skip to content
Draft
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
102 changes: 50 additions & 52 deletions TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ DROP TABLE `reconcile_state`;
A plan diffs this PR's schema files against the live database, so what another PR applied before merging reads here as something to remove. If that is not what you intend, merge that PR, or bring this PR's schema files up to date with it, then re-plan.

⚠️ **Issues**: 2 unsafe changes detected
- `orders`: DROP COLUMN discards the column's data
- `reconcile_state`: DROP TABLE removes all data
1. `orders`: DROP COLUMN discards the column's data
2. `reconcile_state`: DROP TABLE removes all data

**Destructive drop guidance:**

Expand Down Expand Up @@ -812,8 +812,8 @@ schemabot apply -e staging
```

⚠️ **Issues**: 2 unsafe changes detected
- `commerce_sharded/vschema.json`: lookup vindex `customers_email_lookup` is removed: Vitess immediately stops maintaining its rows in backing table `customers_email_lookup`, queries routed through it can fail or scatter, and the lookup data goes stale
- `commerce_sharded/vschema.json`: table `customers` no longer uses vindex `customers_email_lookup`: routing for queries on its columns changes immediately and lookup rows stop being maintained
1. `commerce_sharded/vschema.json`: lookup vindex `customers_email_lookup` is removed: Vitess immediately stops maintaining its rows in backing table `customers_email_lookup`, queries routed through it can fail or scatter, and the lookup data goes stale
2. `commerce_sharded/vschema.json`: table `customers` no longer uses vindex `customers_email_lookup`: routing for queries on its columns changes immediately and lookup rows stop being maintained

📋 **Plan**: **1** vschema update

Expand Down Expand Up @@ -1404,7 +1404,7 @@ ALTER TABLE `customers` DROP COLUMN `nickname`;
---

**⛔ Apply rejected**: 1 unsafe change detected
- `customers`: Unsafe operation detected: DROP COLUMN `nickname`
1. `customers`: Unsafe operation detected: DROP COLUMN `nickname`

**Destructive drop guidance:**

Expand Down Expand Up @@ -1437,7 +1437,7 @@ ALTER TABLE `customers` DROP INDEX `idx_customers_email`;
---

**⛔ Apply rejected**: 1 unsafe change detected
- `customers`: Unsafe operation detected: DROP INDEX `idx_customers_email`
1. `customers`: Unsafe operation detected: DROP INDEX `idx_customers_email`

**Destructive drop guidance:**

Expand Down Expand Up @@ -1472,10 +1472,9 @@ ALTER TABLE `users` RENAME COLUMN `email` TO `email_address`;
---

**⛔ Apply rejected**: 3 unsafe changes detected
- `orders`:
- Primary key column `id` has type `int`
- Column `created_at` uses TIMESTAMP which overflows on 2038-01-19. Consider using DATETIME instead.
- `users`: Column rename detected in table `users`: `email` to `email_address`. Renaming a column cannot be done atomically across application pods, and ORMs that generate column names at compile time (e.g. jOOQ) will break until code is recompiled
1. `orders`: Primary key column `id` has type `int`
2. `orders`: Column `created_at` uses TIMESTAMP which overflows on 2038-01-19. Consider using DATETIME instead.
3. `users`: Column rename detected in table `users`: `email` to `email_address`. Renaming a column cannot be done atomically across application pods, and ORMs that generate column names at compile time (e.g. jOOQ) will break until code is recompiled

**🚨 To proceed with these destructive changes, re-run with `--allow-unsafe`:**
```
Expand Down Expand Up @@ -1572,7 +1571,7 @@ ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```

**`products`**: Cancelled (not started)
**`products`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
Expand Down Expand Up @@ -3908,13 +3907,13 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```

**`users`**: Cancelled (not started)
**`users`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```

**`products`**: Cancelled (not started)
**`products`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
Expand Down Expand Up @@ -3961,7 +3960,7 @@ ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```

**`products`**: Cancelled (not started)
**`products`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
Expand Down Expand Up @@ -4003,13 +4002,13 @@ ALTER TABLE `orders` MODIFY COLUMN `status` enum('NEW','PENDING','SHIPPED','DELI
```
> ❌ Last error: preflight enumReorder check failed: reordering existing ENUM values on column `status` is unsafe: retained values must keep their relative order and new values must be appended at the end

**`users`**: Cancelled (not started)
**`users`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
```

**`products`**: Cancelled (not started)
**`products`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
Expand Down Expand Up @@ -4119,7 +4118,7 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```

**`users`**: Cancelled (not started)
**`users`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
Expand Down Expand Up @@ -5140,7 +5139,7 @@ ALTER TABLE `payments` ADD INDEX `idx_order_id`(`order_id`);
```


### analytics
### 🚫 analytics

**`events`** — Cancelled
```sql
Expand Down Expand Up @@ -5734,10 +5733,10 @@ Sequential mode: First table failed (others cancelled)
~ users: 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜⬜⬜ ❌ Failed
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);

~ orders: Cancelled (not started)
~ orders: 🚫 Cancelled (not started)
ALTER TABLE `orders` ADD INDEX `idx_user_status`(`user_id`, `status`);

~ products: Cancelled (not started)
~ products: 🚫 Cancelled (not started)
ALTER TABLE `products` ADD COLUMN `weight_grams` int DEFAULT 0;


Expand Down Expand Up @@ -5770,7 +5769,7 @@ Sequential mode: Middle table failed
~ users: 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✓ Complete
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);

~ products: Cancelled (not started)
~ products: 🚫 Cancelled (not started)
ALTER TABLE `products` ADD COLUMN `weight_grams` int DEFAULT 0;


Expand Down Expand Up @@ -6148,7 +6147,7 @@ Press Enter to deploy or proceed via the PlanetScale console (ESC to detach)

── myapp_sharded ──

~ orders: 🟧🟧🟧🟧🟧🟧⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜ Cancelled at 30%
~ orders: 🟧🟧🟧🟧🟧🟧⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜ 🚫 Cancelled at 30%
ALTER TABLE `orders` ADD INDEX `idx_total`(`total_cents`);

• Shards: 2 (2 cancelled)
Expand Down Expand Up @@ -6698,7 +6697,7 @@ ESC detach • s stop • v volume

Stop command: User runs 'schemabot stop -e staging <apply_id>'

️ Schema change stopped
️ Schema change stopped

Database: myapp
Environment: staging
Expand Down Expand Up @@ -7059,8 +7058,8 @@ schemabot apply -e production

- ✅ eu — completed
- ❌ us — failed
- ⏸ au — halted — us failed
- ⏸ ca — halted — us failed
- ⏸ au — halted — us failed
- ⏸ ca — halted — us failed

<details>
<summary>✅ eu — completed</summary>
Expand Down Expand Up @@ -7122,7 +7121,7 @@ ALTER TABLE `users` ADD INDEX `idx_email`(`email`);
ALTER TABLE `orders` ADD INDEX `idx_user_id`(`user_id`);
```

**`products`**: Cancelled (not started)
**`products`**: 🚫 Cancelled (not started)

```sql
ALTER TABLE `products` ADD INDEX `idx_price`(`price_cents`);
Expand All @@ -7141,14 +7140,14 @@ schemabot apply -e production
</details>

<details open>
<summary>⏸ au — halted — us failed</summary>
<summary>⏸ au — halted — us failed</summary>

_No details available yet._

</details>

<details open>
<summary>⏸ ca — halted — us failed</summary>
<summary>⏸ ca — halted — us failed</summary>

_No details available yet._

Expand Down Expand Up @@ -7420,8 +7419,8 @@ schemabot apply -e production

- ✅ eu — completed
- ❌ us — failed
- ⏸ au — halted — us failed
- ⏸ ca — halted — us failed
- ⏸ au — halted — us failed
- ⏸ ca — halted — us failed

<details>
<summary>✅ eu — completed</summary>
Expand Down Expand Up @@ -7497,14 +7496,14 @@ schemabot apply -e production
</details>

<details open>
<summary>⏸ au — halted — us failed</summary>
<summary>⏸ au — halted — us failed</summary>

_No details available yet._

</details>

<details open>
<summary>⏸ ca — halted — us failed</summary>
<summary>⏸ ca — halted — us failed</summary>

_No details available yet._

Expand Down Expand Up @@ -7651,7 +7650,7 @@ ALTER TABLE `mutes`
```

⚠️ **Issues**: 1 unsafe change detected
- `mutes` (shard `40-80`): DROP COLUMN removes data and is irreversible
1. `mutes` (shard `40-80`): DROP COLUMN removes data and is irreversible

**Destructive drop guidance:**

Expand Down Expand Up @@ -7713,9 +7712,9 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
| Shard | Status |
| --- | --- |
| `-40` | ❌ failed — resolve shard primary for `-40`: context deadline exceeded |
| `40-80` | ⏸ halted — -40 failed |
| `80-c0` | ⏸ halted — -40 failed |
| `c0-` | ⏸ halted — -40 failed |
| `40-80` | ⏸ halted — -40 failed |
| `80-c0` | ⏸ halted — -40 failed |
| `c0-` | ⏸ halted — -40 failed |

---

Expand Down Expand Up @@ -7865,9 +7864,9 @@ _Last updated: <relative-time datetime="2026-01-01T00:00:00Z">2026-01-01 00:00:0
| Shard | Status |
| --- | --- |
| `-40` | ❌ failed — resolve shard primary for `-40`: context deadline exceeded |
| `40-80` | ⏸ halted — -40 failed |
| `80-c0` | ⏸ halted — -40 failed |
| `c0-` | ⏸ halted — -40 failed |
| `40-80` | ⏸ halted — -40 failed |
| `80-c0` | ⏸ halted — -40 failed |
| `c0-` | ⏸ halted — -40 failed |

### VSchema

Expand Down Expand Up @@ -7958,9 +7957,9 @@ schemabot apply -e production
ALTER TABLE `orders` ADD INDEX `idx_orders_source`(`source`);


⏸ ap-south — halted — eu-west failed (orders-ap-south)
ap-south — halted — eu-west failed (orders-ap-south)

~ orders: Cancelled (not started)
~ orders: 🚫 Cancelled (not started)
ALTER TABLE `orders` ADD COLUMN `source` varchar(32);


Expand Down Expand Up @@ -8163,10 +8162,10 @@ Sequential mode: First table failed (others cancelled)
~ users: 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜⬜⬜ ❌ Failed
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);

~ orders: Cancelled (not started)
~ orders: 🚫 Cancelled (not started)
ALTER TABLE `orders` ADD INDEX `idx_user_status`(`user_id`, `status`);

~ products: Cancelled (not started)
~ products: 🚫 Cancelled (not started)
ALTER TABLE `products` ADD COLUMN `weight_grams` int DEFAULT 0;


Expand Down Expand Up @@ -8199,7 +8198,7 @@ Sequential mode: Middle table failed
~ users: 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✓ Complete
ALTER TABLE `users` ADD INDEX `idx_email_created`(`email`, `created_at`);

~ products: Cancelled (not started)
~ products: 🚫 Cancelled (not started)
ALTER TABLE `products` ADD COLUMN `weight_grams` int DEFAULT 0;


Expand Down Expand Up @@ -8476,12 +8475,11 @@ Lint violations: Non-blocking warnings during plan/apply

Unsafe blocked: Destructive changes require --allow-unsafe

⛔ Apply blocked: 3 unsafe change(s) detected
• users: DROP COLUMN email
• orders: DROP TABLE
• products:
- MODIFY COLUMN price_cents: INT → SMALLINT (potential data loss)
- DROP INDEX idx_category
⛔ Apply blocked: 4 unsafe change(s) detected
1. users: DROP COLUMN email
2. orders: DROP TABLE
3. products: MODIFY COLUMN price_cents: INT → SMALLINT (potential data loss)
4. products: DROP INDEX idx_category

🚨 To proceed with these destructive changes, re-run with --allow-unsafe:

Expand All @@ -8502,8 +8500,8 @@ Unsafe allowed: Proceeding with --allow-unsafe flag
🚨 Unsafe Changes (--allow-unsafe enabled)

The following unsafe changes will be applied:
users: DROP COLUMN email
orders: DROP TABLE
1. users: DROP COLUMN email
2. orders: DROP TABLE

```
</details>
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/commands/watch_tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func TestWatchModel_MultiDeploymentView(t *testing.T) {
"External operation ID: remote-op-us-east-test",
"External apply ID: remote-apply-us-east-test",
"❌ eu-west — failed (orders-eu-west)",
"⏸ ap-south — halted — eu-west failed (orders-ap-south)",
"⏸ ap-south — halted — eu-west failed (orders-ap-south)",
)
assert.Contains(t, view, "duplicate key name 'idx_orders_source'")
assert.Contains(t, view, "orders")
Expand Down
41 changes: 29 additions & 12 deletions pkg/cmd/internal/templates/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ func WriteUnsafeChangesWarning(changes []UnsafeChange) {
// names the blocked apply, not the unsafeness of the changes.
func WriteUnsafeChangesBlocked(changes []UnsafeChange, database, environment, schemaDir string) {
if len(changes) > 0 {
fmt.Printf(glyph.Refused+" Apply blocked: %d unsafe change(s) detected\n", len(changes))
fmt.Printf(glyph.Refused+" Apply blocked: %d unsafe change(s) detected\n", countUnsafeFindings(changes))
writeUnsafeChangesList(changes)
fmt.Println()
}
Expand All @@ -521,20 +521,37 @@ func WriteUnsafeWarningAllowed(changes []UnsafeChange) {
fmt.Println()
}

// writeUnsafeChangesList writes the list of unsafe changes, splitting multi-reason entries.
// writeUnsafeChangesList writes the unsafe changes one numbered line per
// finding, the same list shape as the PR plan comment, so a heading's count
// always equals the number of lines below it and a finding can be referenced
// by its number.
func writeUnsafeChangesList(changes []UnsafeChange) {
n := 0
for _, c := range changes {
reasons := ui.LintReasons(c.Reason)
switch len(reasons) {
case 0:
fmt.Printf(" • %s: %s\n", c.Table, c.ChangeType)
case 1:
fmt.Printf(" • %s: %s\n", c.Table, reasons[0])
default:
fmt.Printf(" • %s:\n", c.Table)
for _, r := range reasons {
fmt.Printf(" - %s\n", r)
}
if len(reasons) == 0 {
n++
fmt.Printf(" %d. %s: %s\n", n, c.Table, c.ChangeType)
continue
}
for _, r := range reasons {
n++
fmt.Printf(" %d. %s: %s\n", n, c.Table, r)
}
}
}

// countUnsafeFindings sums the individual findings across changes so the
// apply-blocked heading counts exactly what the list below shows; a change
// with no parseable reason still counts once.
func countUnsafeFindings(changes []UnsafeChange) int {
n := 0
for _, c := range changes {
if reasons := ui.LintReasons(c.Reason); len(reasons) > 0 {
n += len(reasons)
} else {
n++
}
}
return n
}
Loading
Loading