apps/dashboard/pkg/migration/schemaversion/README.md
This guide provides comprehensive instructions for creating new dashboard schema migrations in Grafana.
Dashboard schema migrations ensure backward compatibility as the dashboard data model evolves. Each migration transforms dashboards from one schema version to the next, allowing older dashboards to work with newer Grafana versions.
apps/dashboard/pkg/migration/schemaversion/public/app/features/dashboard/state/DashboardMigrator.tsCreate a new file: v{N}.go where {N} is the target schema version.
package schemaversion
import "context"
// V{N} migrates [describe what this migration does].
//
// [Detailed description of the migration including:]
// - What properties are being changed
// - Why the migration is necessary
// - Any special handling or edge cases
//
// Example before migration:
//
// "panels": [
// {
// "id": 1,
// "type": "graph",
// "oldProperty": "value"
// }
// ]
//
// Example after migration:
//
// "panels": [
// {
// "id": 1,
// "type": "graph",
// "newProperty": "value"
// }
// ]
func V{N}(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = {N}
// Your migration logic here
// Transform dashboard properties as needed
return nil
}
Add your migration to migrations.go:
func GetMigrations(dsIndexProvider DataSourceIndexProvider) map[int]SchemaVersionMigrationFunc {
return map[int]SchemaVersionMigrationFunc{
// ... existing migrations
{N}: V{N}, // Add your migration here
// ... rest of migrations
}
}
Create testdata/input/v{N}.{name}.json with schema version {N-1} with scenarios to cover the specific migration:
{
"title": "V{N} Migration Test Dashboard",
"schemaVersion": {N-1},
"panels": [
{
"id": 1,
"type": "graph",
"title": "Test Panel",
"oldProperty": "value"
}
]
}
The test system will generate testdata/output/latest_version/v{N}.{name}.v42.json automatically.
# Run migration tests
go test ./apps/dashboard/pkg/migration/...
# Run specific migration test
go test ./apps/dashboard/pkg/migration/... -run TestMigrate
For comprehensive testing strategies including single version tests, frontend comparison tests, and full pipeline testing, see the Comprehensive Testing Strategy section.
Implement different changes in the migration until frontend matches exactly with the backend side. Add as many scenarios as possible to ensure comprehensive coverage of all use cases.
For detailed testing strategies including single version tests, frontend comparison tests, and full pipeline testing, see the Comprehensive Testing Strategy section.
Panel-Level Changes:
Dashboard-Level Changes:
Edge Cases:
Property Transformations:
Create multiple test files for each migration:
v{N}.basic.{name}.json - Basic functionalityv{N}.edge_cases.{name}.json - Edge cases and error conditionsv{N}.complex.{name}.json - Complex dashboard structuresv{N}.property_changes.{name}.json - Property transformation scenariosAdd comprehensive unit tests for both backend and frontend migrations to ensure the correct logic is implemented.
Create unit tests in v{N}_test.go:
package schemaversion_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func TestV{N}(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "basic property migration",
input: map[string]interface{}{
"schemaVersion": {N-1},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"oldProperty": "value",
},
},
},
expected: map[string]interface{}{
"schemaVersion": {N},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
"newProperty": "value",
},
},
},
},
{
name: "edge case - missing property",
input: map[string]interface{}{
"schemaVersion": {N-1},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
},
},
},
expected: map[string]interface{}{
"schemaVersion": {N},
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "graph",
},
},
},
},
// Add more test cases...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := schemaversion.V{N}(context.Background(), tt.input)
require.NoError(t, err)
require.Equal(t, tt.expected, tt.input)
})
}
}
Create unit tests in DashboardMigrator.test.ts:
describe('V{N} Migration', () => {
it('should migrate basic property changes', () => {
const dashboard = {
schemaVersion: {N-1},
panels: [
{
id: 1,
type: 'graph',
oldProperty: 'value'
}
]
};
const model = new DashboardModel(dashboard);
expect(model.schemaVersion).toBe({N});
expect(model.panels[0].newProperty).toBe('value');
expect(model.panels[0].oldProperty).toBeUndefined();
});
it('should handle edge cases correctly', () => {
const dashboard = {
schemaVersion: {N-1},
panels: []
};
const model = new DashboardModel(dashboard);
expect(model.schemaVersion).toBe({N});
expect(model.panels).toEqual([]);
});
// Add more test cases...
});
# Backend unit tests
go test ./apps/dashboard/pkg/migration/schemaversion/... -run TestV{N}
# Frontend unit tests
yarn test DashboardMigrator.test.ts -t "V{N}"
# Integration tests
go test ./apps/dashboard/pkg/migration/... -run TestMigrate
yarn test DashboardMigratorToBackend.test.ts
// Convert various types to float64
func ConvertToFloat(value interface{}) (float64, bool) {
// Implementation in utils.go
}
// Get string value with default
func GetStringValue(data map[string]interface{}, key string, defaultValue string) string {
// Implementation in utils.go
}
// Get boolean value with default
func GetBoolValue(data map[string]interface{}, key string) bool {
// Implementation in utils.go
}
// Safe property access
if value, ok := panel["property"].(string); ok {
// Use value
}
// Safe array access
if panels, ok := dashboard["panels"].([]interface{}); ok {
// Process panels
}
// ❌ Wrong - can panic
value := panel["property"].(string)
// ✅ Correct - safe type assertion
if value, ok := panel["property"].(string); ok {
// Use value
}
// ❌ Wrong - removes intentional nulls
if value == nil {
delete(panel, "property")
}
// ✅ Correct - preserve intentional nulls
if value == nil && !isIntentionallyNull(panel, "property") {
delete(panel, "property")
}
// ❌ Wrong - misses nested panels
for _, panel := range panels {
migratePanel(panel)
}
// ✅ Correct - handles nested panels
for _, panel := range panels {
migratePanelRecursively(panel)
}
Important: Schema version 42 is the final version for the v1 dashboard API. For new migrations beyond v42:
Add Schema Migration when:
Use Panel-Specific Migrations when:
v{N}.{descriptive_name}.jsonv{N}.{descriptive_name}.v42.json{N-1}# Run all migration tests
go test ./apps/dashboard/pkg/migration/...
# Run specific test
go test ./apps/dashboard/pkg/migration/... -run TestMigrate
# Run with verbose output
go test ./apps/dashboard/pkg/migration/... -v
The backend migration tests validate schema version migrations and API conversions:
testdata/input/ covering schema versions 14-42Test execution:
# All backend migration tests
go test ./apps/dashboard/pkg/migration/... -v
# Schema migration tests only
go test ./apps/dashboard/pkg/migration/ -v
# API conversion tests with metrics
go test ./apps/dashboard/pkg/migration/conversion/... -v
The frontend migration comparison tests validate that backend and frontend migration logic produce consistent results:
public/app/features/dashboard/state/DashboardMigratorToBackend.test.tsapps/dashboard/pkg/migration/testdata/input/ and testdata/output/Test execution:
# Frontend migration comparison tests
yarn test DashboardMigratorToBackend.test.ts
Test approach:
jsonInput → DashboardModel → DashboardMigrator → getSaveModelClone()jsonInput → Backend Migration → backendOutput → DashboardModel → getSaveModelClone()The system supports both single version and full pipeline testing:
single_version/ vs latest_version/Test execution:
# Single version migration tests
go test ./apps/dashboard/pkg/migration/... -run TestMigrateSingleVersion
# Full pipeline tests
go test ./apps/dashboard/pkg/migration/... -run TestMigrate