Skip to main content

RFC: Schema Change Detection & Developer Guardrails

  • Status: Draft
  • Created: 2026-01-14
  • Package: framework-m-studio (DevTools, NOT runtime)

Summary

Detect and warn developers about potentially dangerous schema changes before they cause production issues. Provides guardrails with escape hatches (--force).

Motivation

Developers can inadvertently make DocType changes that:

  • Break existing data (incompatible type changes)
  • Cause downtime (non-nullable columns without defaults)
  • Lose data (dropping columns)

Framework should catch these early and guide developers toward safe patterns.

Design Principles

  1. Studio-only: These checks live in framework-m-studio, not framework-m runtime
  2. Runtime stays minimal: Production container doesn't include analysis/warning code
  3. Errors with escape hatch: Dangerous changes error by default, --force to override
  4. Docs-driven: Warnings link to migration pattern documentation

Package Placement

framework-m         (Runtime - Production Container)
├── Minimal core: API, ORM, DocType engine
├── No schema analysis, no heavy dev tooling
└── m start, m migrate (apply only, no analysis)

framework-m-studio (DevTools - Development Only)
├── Schema change analyzer
├── Migration warnings/errors
├── m migrate:create (with analysis)
├── Studio UI warnings
└── CI integration helpers

Change Classification

Risk LevelAuto-Apply?CLI BehaviorExample
SAFE✅ YesProceeds silentlyAdd nullable column, add table
WARNING⚠️ With --forceShows warning, blocks without --forceAdd non-nullable (no default), type widening
DANGEROUS❌ NoHard error, cannot auto-generateDrop column, incompatible type change, rename

CLI Experience (cyclopts)

# apps/studio/src/framework_m_studio/cli/migrate.py
from cyclopts import App
from framework_m_studio.analysis import SchemaAnalyzer, ChangeRisk

migrate_app = App(name="migrate")

@migrate_app.command
async def create(
name: str,
*,
force: bool = False,
skip_analysis: bool = False,
):
"""Create a new migration with schema change analysis."""

if not skip_analysis:
analyzer = SchemaAnalyzer()
changes = await analyzer.analyze_pending_changes()

has_warnings = any(c.risk == ChangeRisk.WARNING for c in changes)
has_dangerous = any(c.risk == ChangeRisk.DANGEROUS for c in changes)

# Display changes
for change in changes:
display_change(change)

if has_dangerous:
console.print("[red]❌ DANGEROUS changes detected.[/red]")
console.print("Cannot auto-generate migration. Write manually.")
console.print(f"See: {change.docs_url}")
raise SystemExit(1)

if has_warnings and not force:
console.print("[yellow]⚠️ Warnings detected.[/yellow]")
console.print("Use --force to proceed anyway.")
raise SystemExit(1)

# Generate migration
await generate_migration(name)

Example Output

$ m migrate:create add_phone_field

📊 Schema Change Analysis (framework-m-studio)

✅ SAFE: Adding 'notes' (nullable Text) to Customer

⚠️ WARNING: Adding 'phone' (non-nullable String) to Customer
│ Existing rows will cause migration to fail.
│ Recommendation: Add with default or make nullable.
│ Docs: https://framework-m.dev/migrations/zero-downtime

└─ Use --force to proceed anyway.

❌ DANGEROUS: Changing 'amount' from String → Decimal in Invoice
│ Incompatible type change requires manual data migration.
│ Pattern: Two-Phase Migration
│ Docs: https://framework-m.dev/migrations/type-changes

└─ Cannot auto-generate. Write migration manually.

Migration NOT created.

Studio UI Integration

When saving a DocType in Studio:

┌─────────────────────────────────────────────────────────────────┐
│ Save DocType: Customer │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ Schema Change Warning │
│ ───────────────────────── │
│ Adding 'phone' as non-nullable without default. │
│ This will fail if Customer table has existing rows. │
│ │
│ Recommendations: │
│ • Add a default value: phone: str = "" │
│ • Make nullable: phone: str | None = None │
│ │
│ [Learn More] [Save Anyway] [Cancel] │
│ │
└─────────────────────────────────────────────────────────────────┘

Schema Analyzer Implementation

# apps/studio/src/framework_m_studio/analysis/schema_analyzer.py
from enum import Enum
from pydantic import BaseModel

class ChangeRisk(Enum):
SAFE = "safe"
WARNING = "warning"
DANGEROUS = "dangerous"

class SchemaChange(BaseModel):
doctype: str
field: str | None
change_type: str # "add_field", "drop_field", "modify_type", "add_table"
risk: ChangeRisk
message: str
recommendation: str | None
docs_url: str | None

class SchemaAnalyzer:
"""Analyzes DocType changes for migration risk."""

DOCS_BASE = "https://framework-m.dev/migrations"

async def analyze_pending_changes(self) -> list[SchemaChange]:
"""Compare registered DocTypes with database schema."""
# Load current DB schema
# Load current Pydantic models
# Diff and classify
...

def _classify_new_field(self, field_info) -> ChangeRisk:
if field_info.is_required and field_info.default is None:
return ChangeRisk.WARNING
return ChangeRisk.SAFE

def _classify_type_change(self, old_type, new_type) -> ChangeRisk:
if self._is_compatible_widening(old_type, new_type):
return ChangeRisk.WARNING
return ChangeRisk.DANGEROUS

def _classify_drop_field(self) -> ChangeRisk:
return ChangeRisk.DANGEROUS

CI Integration

# .gitlab-ci.yml
schema-check:
stage: test
image: framework-m-studio:latest
script:
- m migrate:create --dry-run --fail-on-warning
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

Each change type links to specific pattern documentation:

ChangeLinked Pattern
Non-nullable without defaultZero-Downtime Migrations
Compatible type wideningReview-Required Changes
Incompatible type changeTwo-Phase Migration
Drop columnExpand-Contract Pattern
Rename columnTwo-Phase Migration

Open Questions

  1. Should we detect changes at file save in Studio?

    • Pro: Immediate feedback
    • Con: Noisy if developer is mid-edit
  2. Track "acknowledged" warnings?

    • Once --force is used, remember for that change?
    • Or always warn on every run?
  3. Integration with CI merge checks?

    • GitLab/GitHub status check integration?

Implementation Phases

  • Phase 1: Core analyzer (compare Pydantic ↔ DB schema)
  • Phase 2: CLI integration with cyclopts
  • Phase 3: Studio UI warnings
  • Phase 4: CI helpers and status checks

References