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
125 changes: 122 additions & 3 deletions TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,38 @@ ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
- `users`: Column `created_at` uses `TIMESTAMP` which overflows on 2038-01-19. Consider using `DATETIME` instead.
- `products`: Index `idx_category` on column `category` is redundant - covered by index `idx_category_price` on columns (`category`, `price`)

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


---

▶️ **To apply** all schema changes from this PR, comment:
```
schemabot apply -e staging
```

</details>

<details>
<summary><a name="mysql-plan-columnonly-alter-no-table-sizes"></a><strong>MySQL Plan (Column-only Alter, No Table Sizes)</strong></summary>


## Schema Change Plan — Staging

**Database**: `testapp` | **Type**: `MySQL` | **Schema Name**: `testapp`

*Requested by @jackjackbits at 2026-01-01 00:00:00 UTC · planned from [`abcdef1`](https://github.com/block/schemabot/commit/abcdef1234567890abcdef1234567890abcdef12)*

```sql
ALTER TABLE `products` ADD COLUMN `discount_cents` bigint;
```

📋 **Plan**: **1** table to alter


---

▶️ **To apply** all schema changes from this PR, comment:
Expand Down Expand Up @@ -93,6 +122,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter

ℹ️ Namespaces excluded from this plan by `ignore_namespaces`: `local_fixtures`
Expand Down Expand Up @@ -543,6 +575,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -755,9 +790,44 @@ CREATE TABLE `addresses` (
) ENGINE InnoDB,
CHARSET utf8mb4,
COLLATE utf8mb4_0900_ai_ci;

ALTER TABLE `customers` ADD INDEX `idx_loyalty_tier`(`loyalty_tier`);
```

📊 **Table sizes**:
- `customers`: ~48.2M rows · ~23.4 GB across 2 shards (largest shard ~24.6M rows)

📋 **Plan**: **2** tables to create, **1** table to alter, **2** vschema updates


---

▶️ **To apply** all schema changes from this PR, comment:
```
schemabot apply -e staging
```

</details>

<details>
<summary><a name="vitess-plan-bytesonly-table-sizes"></a><strong>Vitess Plan (Bytes-only Table Sizes)</strong></summary>


## Schema Change Plan — Staging

**Database**: `commerce` | **Type**: `Vitess`

*Requested by @jackjackbits at 2026-01-01 00:00:00 UTC · planned from [`abcdef1`](https://github.com/block/schemabot/commit/abcdef1234567890abcdef1234567890abcdef12)*

#### Keyspace: `commerce_sharded`
```sql
ALTER TABLE `addresses` ADD INDEX `idx_region`(`region`);
```

📊 **Table sizes**:
- `addresses`: ~48 GB across 4 shards

📋 **Plan**: **2** tables to create, **2** vschema updates
📋 **Plan**: **1** table to alter


---
Expand Down Expand Up @@ -938,9 +1008,14 @@ CREATE TABLE `addresses` (
) ENGINE InnoDB,
CHARSET utf8mb4,
COLLATE utf8mb4_0900_ai_ci;

ALTER TABLE `customers` ADD INDEX `idx_loyalty_tier`(`loyalty_tier`);
```

📋 **Plan**: **2** tables to create, **2** vschema updates
📊 **Table sizes**:
- `customers`: ~48.2M rows · ~23.4 GB across 2 shards (largest shard ~24.6M rows)

📋 **Plan**: **2** tables to create, **1** table to alter, **2** vschema updates


**Options**: ⏸️ Defer Cutover | ⏩ Skip Revert
Expand Down Expand Up @@ -1036,6 +1111,9 @@ ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);

</details>

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter

---
Expand Down Expand Up @@ -1098,6 +1176,9 @@ ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);

</details>

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter

---
Expand Down Expand Up @@ -1151,6 +1232,9 @@ ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);

</details>

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter

### Production
Expand Down Expand Up @@ -1218,6 +1302,9 @@ ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
- `users`: Column `created_at` uses `TIMESTAMP` which overflows on 2038-01-19. Consider using `DATETIME` instead.
- `products`: Index `idx_category` on column `category` is redundant - covered by index `idx_category_price` on columns (`category`, `price`)

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter

---
Expand Down Expand Up @@ -1271,6 +1358,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -1324,6 +1414,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -1373,6 +1466,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -1627,6 +1723,9 @@ ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
- `users`: Column `created_at` uses `TIMESTAMP` which overflows on 2038-01-19. Consider using `DATETIME` instead.
- `products`: Index `idx_category` on column `category` is redundant - covered by index `idx_category_price` on columns (`category`, `price`)

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -2343,6 +2442,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -2389,6 +2491,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -2437,6 +2542,9 @@ CREATE TABLE `orders` (
ALTER TABLE `products` ADD INDEX `idx_category_price`(`category`, `price`);
```

📊 **Table sizes**:
- `products`: ~2.3M rows · ~1.1 GB

📋 **Plan**: **2** tables to create, **1** table to alter


Expand Down Expand Up @@ -2532,9 +2640,14 @@ CREATE TABLE `addresses` (
) ENGINE InnoDB,
CHARSET utf8mb4,
COLLATE utf8mb4_0900_ai_ci;

ALTER TABLE `customers` ADD INDEX `idx_loyalty_tier`(`loyalty_tier`);
```

📋 **Plan**: **2** tables to create, **2** vschema updates
📊 **Table sizes**:
- `customers`: ~48.2M rows · ~23.4 GB across 2 shards (largest shard ~24.6M rows)

📋 **Plan**: **2** tables to create, **1** table to alter, **2** vschema updates


**Options**: ⏸️ Defer Cutover | ⏩ Skip Revert
Expand Down Expand Up @@ -7385,6 +7498,9 @@ ALTER TABLE `mutes`
ADD COLUMN `reason` varchar(255);
```

📊 **Table sizes**:
- `mutes`: ~48.2M rows across 4 shards (largest shard ~13.1M rows)

📋 **Plan**: **1** table to alter


Expand Down Expand Up @@ -7454,6 +7570,9 @@ _Already applied — no change._
ALTER TABLE `mutes` ADD INDEX `created_at`(`created_at`);
```

📊 **Table sizes**:
- `mutes`: size estimate unavailable · 3 shards

📋 **Plan**: 1 DDL statement


Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/internal/templates/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const (
// Comment template previews (GitHub PR comments)
PreviewCommentPlan PreviewType = "comment_plan" // Plan comment with DDL changes + lint violations
PreviewCommentPlanIgnoredNamespaces PreviewType = "comment_plan_ignored_namespaces" // Plan with namespaces withheld by ignore_namespaces
PreviewCommentPlanColumnOnlyAlter PreviewType = "comment_plan_column_only_alter" // Plan whose alter is metadata-only, so no table-size section renders
PreviewCommentPlanBlocked PreviewType = "comment_plan_blocked" // Plan with a statement the engine refuses (blocked verdict)
PreviewCommentPlanDirect PreviewType = "comment_plan_direct" // Locked plan with a statement routed to direct execution (direct verdict)
PreviewCommentPlanCopyDiscarded PreviewType = "comment_plan_copy_discarded" // Plan whose apply would throw away an unfinished copy on the target
Expand All @@ -132,6 +133,7 @@ const (
PreviewCommentMultiEnvDiff PreviewType = "comment_multi_env_diff" // Multi-env plan (different per env)
PreviewCommentMultiEnvLint PreviewType = "comment_multi_env_lint" // Multi-env plan with lint violations
PreviewCommentVitessPlan PreviewType = "comment_vitess_plan" // Vitess plan with keyspaces + VSchema
PreviewCommentVitessPlanBytesOnlySizes PreviewType = "comment_vitess_plan_bytes_only_sizes" // Vitess plan whose size context is storage bytes with no row counts
PreviewCommentVitessPlanVSchemaRemoval PreviewType = "comment_vitess_plan_vschema_removal" // Vitess plan with unsafe VSchema removals
PreviewCommentVitessApplyPlan PreviewType = "comment_vitess_apply_plan" // Locked Vitess apply-plan with options
PreviewCommentMySQLMultiSchema PreviewType = "comment_mysql_multi_schema" // MySQL plan with multiple schema names
Expand Down
3 changes: 3 additions & 0 deletions pkg/cmd/internal/templates/preview_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func previewCommentAllOutput() {
fn func()
}{
{"PLAN COMMENT", func() { fmt.Print(webhooktemplates.PreviewCommentPlan()) }},
{"PLAN COMMENT (COLUMN-ONLY ALTER, NO TABLE SIZES)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanColumnOnlyAlter()) }},
{"PLAN COMMENT (IGNORED NAMESPACES)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanIgnoredNamespaces()) }},
{"PLAN COMMENT (MANY LINT WARNINGS)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanManyLintWarnings()) }},
{"PLAN COMMENT (ENGINE-BLOCKED CHANGE)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanBlocked()) }},
Expand Down Expand Up @@ -144,6 +145,7 @@ func previewCommentPlanAllOutput() {
fn func()
}{
{"MYSQL PLAN", func() { fmt.Print(webhooktemplates.PreviewCommentPlan()) }},
{"MYSQL PLAN (COLUMN-ONLY ALTER, NO TABLE SIZES)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanColumnOnlyAlter()) }},
{"MYSQL PLAN (IGNORED NAMESPACES)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanIgnoredNamespaces()) }},
{"MYSQL PLAN (MANY LINT WARNINGS)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanManyLintWarnings()) }},
{"MYSQL PLAN (ENGINE-BLOCKED CHANGE)", func() { fmt.Print(webhooktemplates.PreviewCommentPlanBlocked()) }},
Expand Down Expand Up @@ -172,6 +174,7 @@ func previewCommentPlanAllOutput() {
{"RECONCILIATION REQUIRED (IN PROGRESS)", func() { fmt.Print(webhooktemplates.PreviewCommentSchemaReconciliationInProgress()) }},
{"RECONCILIATION REQUIRED (COMPLETED)", func() { fmt.Print(webhooktemplates.PreviewCommentSchemaReconciliationCompleted()) }},
{"VITESS PLAN", func() { fmt.Print(webhooktemplates.PreviewCommentVitessPlan()) }},
{"VITESS PLAN (BYTES-ONLY TABLE SIZES)", func() { fmt.Print(webhooktemplates.PreviewCommentVitessPlanBytesOnlySizes()) }},
{"VITESS PLAN: VSCHEMA REMOVAL (UNSAFE)", func() { fmt.Print(webhooktemplates.PreviewCommentVitessPlanVSchemaRemoval()) }},
{"POSTGRES PLAN", func() { fmt.Print(webhooktemplates.PreviewCommentPostgresPlan()) }},
{"SCHEMA CHANGE APPLY (LOCKED + OPTIONS)", func() { fmt.Print(webhooktemplates.PreviewCommentVitessApplyPlan()) }},
Expand Down
4 changes: 4 additions & 0 deletions pkg/cmd/internal/templates/preview_dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ func PreviewCLIOutput(previewType PreviewType) {
fmt.Print(webhooktemplates.PreviewCommentPlan())
case PreviewCommentPlanIgnoredNamespaces:
fmt.Print(webhooktemplates.PreviewCommentPlanIgnoredNamespaces())
case PreviewCommentPlanColumnOnlyAlter:
fmt.Print(webhooktemplates.PreviewCommentPlanColumnOnlyAlter())
case PreviewCommentPlanBlocked:
fmt.Print(webhooktemplates.PreviewCommentPlanBlocked())
case PreviewCommentPlanDirect:
Expand Down Expand Up @@ -162,6 +164,8 @@ func PreviewCLIOutput(previewType PreviewType) {
fmt.Print(webhooktemplates.PreviewCommentMultiEnvPlanLint())
case PreviewCommentVitessPlan:
fmt.Print(webhooktemplates.PreviewCommentVitessPlan())
case PreviewCommentVitessPlanBytesOnlySizes:
fmt.Print(webhooktemplates.PreviewCommentVitessPlanBytesOnlySizes())
case PreviewCommentVitessPlanVSchemaRemoval:
fmt.Print(webhooktemplates.PreviewCommentVitessPlanVSchemaRemoval())
case PreviewCommentVitessApplyPlan:
Expand Down
41 changes: 40 additions & 1 deletion pkg/webhook/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (

"github.com/block/schemabot/pkg/api"
"github.com/block/schemabot/pkg/apitypes"
"github.com/block/schemabot/pkg/ddl"
ghclient "github.com/block/schemabot/pkg/github"
"github.com/block/schemabot/pkg/metrics"
schemapkg "github.com/block/schemabot/pkg/schema"
"github.com/block/schemabot/pkg/storage"
"github.com/block/schemabot/pkg/ui"
"github.com/block/schemabot/pkg/webhook/action"
Expand Down Expand Up @@ -841,7 +843,30 @@ func splitExistingCopies(copies []*apitypes.ExistingCopyResponse) (discarded, ad
return discarded, adopted, running
}

// buildPlanCommentData converts plan results into template data.
// statementCostScalesWithSize reports whether a plan statement's execution
// cost grows with the table — an index build, a table copy or rebuild, or a
// full-table validation scan — using the real parser for the database's
// dialect. Table sizes are display-only context, so a statement that cannot
// be parsed logs a warning and renders without a size line rather than
// failing the comment.
func statementCostScalesWithSize(schemaResult *ghclient.SchemaRequestResult, stmt string) bool {
parser, err := ddl.ParserForDialect(schemapkg.DialectForDatabaseType(schemaResult.Type))
if err != nil {
slog.Warn("no statement parser for dialect; plan comment omits the table-size line",
"repo", schemaResult.Repository, "database", schemaResult.Database,
"database_type", schemaResult.Type, "error", err)
return false
}
scales, err := parser.CostScalesWithTableSize(stmt)
if err != nil {
slog.Warn("failed to inspect plan statement for table-size-scaling cost; plan comment omits the table-size line",
"repo", schemaResult.Repository, "database", schemaResult.Database,
"database_type", schemaResult.Type, "error", err)
return false
}
return scales
}

func buildPlanCommentData(schema *ghclient.SchemaRequestResult, planResp *apitypes.PlanResponse, environment, tenant, requestedBy, agentHint string) templates.PlanCommentData {
data := templates.PlanCommentData{
Database: schema.Database,
Expand Down Expand Up @@ -899,6 +924,20 @@ func buildPlanCommentData(schema *ghclient.SchemaRequestResult, planResp *apityp
}
for _, t := range sc.TableChanges {
ksData.Statements = append(ksData.Statements, t.DDL)
// Table sizes are shown only for statements whose cost scales
// with the table's size — index builds, copies/rebuilds, and
// validation scans. Metadata-only statements carrying a size line
// would be noise on the plan.
if !statementCostScalesWithSize(schema, t.DDL) {
continue
}
ksData.TableSizes = append(ksData.TableSizes, templates.TableSizeData{
Table: t.TableName,
EstimatedRows: t.EstimatedRows,
ShardCount: t.ShardCount,
LargestShardRows: t.LargestShardRows,
EstimatedBytes: t.EstimatedBytes,
})
}
// Extract VSchema changes from metadata
if sc.HasVSchemaChange() {
Expand Down
Loading
Loading