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
- Studio-only: These checks live in
framework-m-studio, notframework-mruntime - Runtime stays minimal: Production container doesn't include analysis/warning code
- Errors with escape hatch: Dangerous changes error by default,
--forceto override - 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 Level | Auto-Apply? | CLI Behavior | Example |
|---|---|---|---|
| SAFE | ✅ Yes | Proceeds silently | Add nullable column, add table |
| WARNING | ⚠️ With --force | Shows warning, blocks without --force | Add non-nullable (no default), type widening |
| DANGEROUS | ❌ No | Hard error, cannot auto-generate | Drop 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"
Documentation Links
Each change type links to specific pattern documentation:
| Change | Linked Pattern |
|---|---|
| Non-nullable without default | Zero-Downtime Migrations |
| Compatible type widening | Review-Required Changes |
| Incompatible type change | Two-Phase Migration |
| Drop column | Expand-Contract Pattern |
| Rename column | Two-Phase Migration |
Open Questions
-
Should we detect changes at file save in Studio?
- Pro: Immediate feedback
- Con: Noisy if developer is mid-edit
-
Track "acknowledged" warnings?
- Once --force is used, remember for that change?
- Or always warn on every run?
-
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
- Phase 10: Migration Strategy
- Phase 07: Studio
- dependency-injector wiring