## Summary - Fixes migration 008 failing with "column contains null values" error - Adds COALESCE protection to handle 126 servers without status fields - Includes test utilities to reproduce and verify fixes with production data ## Problem Migration 008 was failing in production because: - 126 out of 781 servers have no `status` field in their JSON - When extracting `rec.value->>'status'`, these became NULL values - The `ALTER TABLE servers ALTER COLUMN status SET NOT NULL` constraint then failed ## Solution 1. **Prevent NULLs during migration**: Use COALESCE to provide 'active' default when status is NULL/empty 2. **Enhanced debugging**: Added RAISE NOTICE statements to log problematic values 3. **Safety check**: Double-check that throws exception if any NULLs remain 4. **Test utilities**: Created mirror_data tools to test migrations with real production data ## Testing Created and used `scripts/mirror_data/` tools to: - Fetch all 781 servers from production API - Load them into a test database - Verify the migration fix works correctly ### Results: - **Before fix**: 126 NULL status values created during migration - **After fix**: 0 NULL status values, all servers have valid status - Migration completes successfully with NOT NULL constraint ## Test plan - [x] Tested locally with production data using mirror_data tools - [x] Verified 0 NULL values after migration (vs 126 before) - [x] Confirmed NOT NULL constraint adds successfully - [ ] Deploy to staging and verify migration succeeds - [ ] Monitor production deployment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
87 lines
2 KiB
Go
87 lines
2 KiB
Go
// This tool was created by Claude Code as a simple way to kick the tires on data migrations
|
|
// by fetching production data from the public registry API.
|
|
// It is not intended for production use.
|
|
//
|
|
//go:build ignore
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
type ServerResponse struct {
|
|
Servers []json.RawMessage `json:"servers"`
|
|
Metadata struct {
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
Count int `json:"count"`
|
|
} `json:"metadata"`
|
|
}
|
|
|
|
func main() {
|
|
baseURL := "https://registry.modelcontextprotocol.io/v0/servers"
|
|
var allServers []json.RawMessage
|
|
cursor := ""
|
|
pageCount := 0
|
|
|
|
for {
|
|
pageCount++
|
|
url := baseURL
|
|
if cursor != "" {
|
|
url = fmt.Sprintf("%s?cursor=%s", baseURL, cursor)
|
|
}
|
|
|
|
fmt.Printf("Fetching page %d: %s\n", pageCount, url)
|
|
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to fetch: %v", err))
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to read body: %v", err))
|
|
}
|
|
|
|
var serverResp ServerResponse
|
|
if err := json.Unmarshal(body, &serverResp); err != nil {
|
|
panic(fmt.Sprintf("Failed to parse JSON: %v", err))
|
|
}
|
|
|
|
allServers = append(allServers, serverResp.Servers...)
|
|
fmt.Printf(" Got %d servers (total: %d)\n", len(serverResp.Servers), len(allServers))
|
|
|
|
if serverResp.Metadata.NextCursor == "" {
|
|
break
|
|
}
|
|
cursor = serverResp.Metadata.NextCursor
|
|
|
|
// Be nice to the API
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// Save all servers to a file
|
|
output := map[string]interface{}{
|
|
"servers": allServers,
|
|
"count": len(allServers),
|
|
"fetched": time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
data, err := json.MarshalIndent(output, "", " ")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to marshal output: %v", err))
|
|
}
|
|
|
|
outputFile := "production_servers.json"
|
|
if err := os.WriteFile(outputFile, data, 0644); err != nil {
|
|
panic(fmt.Sprintf("Failed to write file: %v", err))
|
|
}
|
|
|
|
fmt.Printf("\nDone! Saved %d servers to %s\n", len(allServers), outputFile)
|
|
}
|