Fix migration numbering and add migration 010 for package format (#675)
<!-- Provide a brief summary of your changes --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> The following PR fixes an issue we introduced on September, 30th that caused some of the migrations to not apply. This PR fixes it. After merging we'll have to reset staging as this would not work there (this is expected). ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
This commit is contained in:
parent
1142e5d872
commit
d9099e9117
4 changed files with 6 additions and 2 deletions
2
.github/workflows/cancel-pulumi-lock.yml
vendored
2
.github/workflows/cancel-pulumi-lock.yml
vendored
|
|
@ -73,4 +73,4 @@ jobs:
|
|||
run: |
|
||||
echo "$PULUMI_PROD_PASSPHRASE" > passphrase.prod.txt
|
||||
pulumi login gs://mcp-registry-prod-pulumi-state
|
||||
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi cancel --stack gcpProd --yes
|
||||
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi cancel --stack gcpProd --yes
|
||||
|
|
|
|||
197
internal/database/migrations/008_clean_invalid_data.sql
Normal file
197
internal/database/migrations/008_clean_invalid_data.sql
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
-- Migration 007a: Clean up invalid data before applying stricter constraints in migration 008
|
||||
-- This migration removes or fixes data that would violate constraints introduced in the next migration
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Safety check: Count exactly what we'll be modifying
|
||||
DO $$
|
||||
DECLARE
|
||||
invalid_name_count INTEGER;
|
||||
empty_version_count INTEGER;
|
||||
invalid_status_count INTEGER;
|
||||
duplicate_count INTEGER;
|
||||
total_to_delete INTEGER;
|
||||
total_servers INTEGER;
|
||||
has_known_bad_server BOOLEAN;
|
||||
r RECORD;
|
||||
BEGIN
|
||||
-- Get total server count
|
||||
SELECT COUNT(*) INTO total_servers FROM servers;
|
||||
|
||||
-- Check if we have the specific known problematic server from production
|
||||
-- This server only exists in production data, not in test fixtures
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM servers
|
||||
WHERE value->>'name' = 'io.github.joelverhagen/Knapcode.SampleMcpServer/aot'
|
||||
) INTO has_known_bad_server;
|
||||
|
||||
-- Skip migration if we don't have the known bad data (indicates test environment)
|
||||
IF NOT has_known_bad_server THEN
|
||||
RAISE NOTICE 'Migration 007a: Skipping cleanup - no known problematic data found (likely test/dev environment)';
|
||||
RETURN; -- Exit early, don't run any cleanup
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'Migration 007a: Found known problematic data, proceeding with cleanup';
|
||||
|
||||
-- Count servers with invalid name format
|
||||
SELECT COUNT(*) INTO invalid_name_count
|
||||
FROM servers
|
||||
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]';
|
||||
|
||||
-- Count servers with empty or NULL versions
|
||||
SELECT COUNT(*) INTO empty_version_count
|
||||
FROM servers
|
||||
WHERE value->>'version' IS NULL OR value->>'version' = '';
|
||||
|
||||
-- Count servers with invalid status
|
||||
SELECT COUNT(*) INTO invalid_status_count
|
||||
FROM servers
|
||||
WHERE value->>'status' IS NOT NULL
|
||||
AND value->>'status' != ''
|
||||
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted');
|
||||
|
||||
-- Count duplicate name+version combinations
|
||||
SELECT COUNT(*) INTO duplicate_count
|
||||
FROM (
|
||||
SELECT value->>'name', value->>'version', COUNT(*) as cnt
|
||||
FROM servers
|
||||
GROUP BY value->>'name', value->>'version'
|
||||
HAVING COUNT(*) > 1
|
||||
) dups;
|
||||
|
||||
-- Calculate total deletions (some servers might have both invalid name AND empty version)
|
||||
SELECT COUNT(*) INTO total_to_delete
|
||||
FROM servers
|
||||
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]'
|
||||
OR value->>'version' IS NULL
|
||||
OR value->>'version' = '';
|
||||
|
||||
-- Log the cleanup operations with safety check
|
||||
RAISE NOTICE 'Migration 007a Data Cleanup Plan:';
|
||||
RAISE NOTICE ' Total servers in database: %', total_servers;
|
||||
|
||||
IF total_servers > 0 THEN
|
||||
RAISE NOTICE ' Servers to DELETE: % (% percent of total)', total_to_delete, ROUND((total_to_delete::numeric / total_servers * 100), 2);
|
||||
ELSE
|
||||
RAISE NOTICE ' Servers to DELETE: %', total_to_delete;
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE ' - Invalid names: %', invalid_name_count;
|
||||
RAISE NOTICE ' - Empty versions: %', empty_version_count;
|
||||
RAISE NOTICE ' Servers to UPDATE (fix status): %', invalid_status_count;
|
||||
RAISE NOTICE ' Duplicate name+version pairs: %', duplicate_count;
|
||||
|
||||
-- Always log the specific servers that will be deleted for transparency
|
||||
IF total_to_delete > 0 THEN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'Servers that will be deleted:';
|
||||
FOR r IN
|
||||
SELECT value->>'name' as name, value->>'version' as version,
|
||||
CASE
|
||||
WHEN value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]' THEN 'Invalid name format'
|
||||
WHEN value->>'version' IS NULL THEN 'NULL version'
|
||||
WHEN value->>'version' = '' THEN 'Empty version'
|
||||
END as reason
|
||||
FROM servers
|
||||
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]'
|
||||
OR value->>'version' IS NULL
|
||||
OR value->>'version' = ''
|
||||
ORDER BY value->>'name'
|
||||
LOOP
|
||||
RAISE NOTICE ' - % @ % (reason: %)', r.name, COALESCE(r.version, '<NULL>'), r.reason;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
-- Always log the specific servers that will have status updated for transparency
|
||||
IF invalid_status_count > 0 THEN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'Servers that will have status updated:';
|
||||
FOR r IN
|
||||
SELECT value->>'name' as name, value->>'version' as version, value->>'status' as status
|
||||
FROM servers
|
||||
WHERE value->>'status' IS NOT NULL
|
||||
AND value->>'status' != ''
|
||||
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted')
|
||||
ORDER BY value->>'name'
|
||||
LOOP
|
||||
RAISE NOTICE ' - % @ % (current status: %)', r.name, r.version, r.status;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
-- SAFETY CHECK: Ensure we're deleting exactly the expected data
|
||||
-- We only reach this point if we found the known bad server
|
||||
-- In production (2025-09-30), we expect exactly 5 deletions and 1 status update
|
||||
IF total_to_delete != 5 THEN
|
||||
RAISE EXCEPTION 'Safety check failed: Expected to delete exactly 5 servers but would delete %. Check the log above for details. Aborting to prevent data loss.', total_to_delete;
|
||||
END IF;
|
||||
|
||||
IF invalid_status_count != 1 THEN
|
||||
RAISE EXCEPTION 'Safety check failed: Expected to update exactly 1 server status but would update %. Check the log above for details. Aborting to prevent data corruption.', invalid_status_count;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Delete servers with invalid names or empty versions
|
||||
-- These cannot be reasonably fixed and would violate primary key constraints
|
||||
DELETE FROM servers
|
||||
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]'
|
||||
OR value->>'version' IS NULL
|
||||
OR value->>'version' = '';
|
||||
|
||||
-- Fix invalid status values by setting them to 'active'
|
||||
-- These can be reasonably defaulted to a valid value
|
||||
UPDATE servers
|
||||
SET value = jsonb_set(value, '{status}', '"active"')
|
||||
WHERE value->>'status' IS NOT NULL
|
||||
AND value->>'status' != ''
|
||||
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted');
|
||||
|
||||
-- Remove duplicate name+version combinations
|
||||
-- Keep the one with the most recent publishedAt date
|
||||
DELETE FROM servers s1
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM servers s2
|
||||
WHERE s2.value->>'name' = s1.value->>'name'
|
||||
AND s2.value->>'version' = s1.value->>'version'
|
||||
AND (s2.value->>'publishedAt')::timestamp > (s1.value->>'publishedAt')::timestamp
|
||||
);
|
||||
|
||||
-- Verify the operations completed as expected
|
||||
DO $$
|
||||
DECLARE
|
||||
remaining_count INTEGER;
|
||||
actual_deleted INTEGER;
|
||||
actual_updated INTEGER;
|
||||
still_invalid_names INTEGER;
|
||||
still_empty_versions INTEGER;
|
||||
still_invalid_status INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO remaining_count FROM servers;
|
||||
|
||||
-- Check if any invalid data remains
|
||||
SELECT COUNT(*) INTO still_invalid_names
|
||||
FROM servers
|
||||
WHERE value->>'name' NOT SIMILAR TO '[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]';
|
||||
|
||||
SELECT COUNT(*) INTO still_empty_versions
|
||||
FROM servers
|
||||
WHERE value->>'version' IS NULL OR value->>'version' = '';
|
||||
|
||||
SELECT COUNT(*) INTO still_invalid_status
|
||||
FROM servers
|
||||
WHERE value->>'status' IS NOT NULL
|
||||
AND value->>'status' != ''
|
||||
AND value->>'status' NOT IN ('active', 'deprecated', 'deleted');
|
||||
|
||||
RAISE NOTICE 'Data cleanup complete:';
|
||||
RAISE NOTICE ' Servers remaining: %', remaining_count;
|
||||
RAISE NOTICE ' Invalid names remaining: %', still_invalid_names;
|
||||
RAISE NOTICE ' Empty versions remaining: %', still_empty_versions;
|
||||
RAISE NOTICE ' Invalid status remaining: %', still_invalid_status;
|
||||
|
||||
-- Final safety check: Ensure we cleaned everything we intended to
|
||||
IF still_invalid_names > 0 OR still_empty_versions > 0 OR still_invalid_status > 0 THEN
|
||||
RAISE EXCEPTION 'Cleanup incomplete! Invalid data still remains. Aborting.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
220
internal/database/migrations/009_separate_official_metadata.sql
Normal file
220
internal/database/migrations/009_separate_official_metadata.sql
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
-- Separate official metadata from JSON to dedicated columns for better performance and consistency
|
||||
-- This migration transforms the database from JSON-heavy to columnar structure for registry-managed data
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Add new columns for official metadata and core identifiers
|
||||
ALTER TABLE servers ADD COLUMN server_name VARCHAR(255);
|
||||
ALTER TABLE servers ADD COLUMN version VARCHAR(255);
|
||||
ALTER TABLE servers ADD COLUMN status VARCHAR(50) DEFAULT 'active';
|
||||
ALTER TABLE servers ADD COLUMN published_at TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE servers ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE servers ADD COLUMN is_latest BOOLEAN;
|
||||
|
||||
-- Create a function to migrate existing data from JSON to columns
|
||||
CREATE OR REPLACE FUNCTION migrate_official_metadata()
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
official_meta JSONB;
|
||||
updated_value JSONB;
|
||||
server_meta JSONB;
|
||||
publisher_provided JSONB;
|
||||
BEGIN
|
||||
-- Iterate through all existing records
|
||||
FOR rec IN SELECT version_id, value FROM servers ORDER BY version_id LOOP
|
||||
-- Extract core identifiers
|
||||
UPDATE servers
|
||||
SET
|
||||
server_name = rec.value->>'name',
|
||||
version = rec.value->>'version'
|
||||
WHERE version_id = rec.version_id;
|
||||
|
||||
-- Extract official metadata from nested JSON structure
|
||||
official_meta := rec.value->'_meta'->'io.modelcontextprotocol.registry/official';
|
||||
|
||||
IF official_meta IS NOT NULL THEN
|
||||
-- Update columns with extracted metadata
|
||||
-- Note: status is at top level in old format, not in official_meta
|
||||
UPDATE servers
|
||||
SET
|
||||
status = COALESCE(
|
||||
NULLIF(NULLIF(rec.value->>'status', ''), 'null'),
|
||||
'active'
|
||||
),
|
||||
published_at = COALESCE((official_meta->>'publishedAt')::TIMESTAMP WITH TIME ZONE, NOW()),
|
||||
updated_at = COALESCE((official_meta->>'updatedAt')::TIMESTAMP WITH TIME ZONE, NOW()),
|
||||
is_latest = COALESCE((official_meta->>'isLatest')::BOOLEAN, true)
|
||||
WHERE version_id = rec.version_id;
|
||||
ELSE
|
||||
-- Handle records without official metadata (set defaults)
|
||||
UPDATE servers
|
||||
SET
|
||||
status = COALESCE(
|
||||
NULLIF(NULLIF(rec.value->>'status', ''), 'null'),
|
||||
'active'
|
||||
),
|
||||
published_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
is_latest = true
|
||||
WHERE version_id = rec.version_id;
|
||||
END IF;
|
||||
|
||||
-- Clean up the JSON: remove status field and official metadata
|
||||
updated_value := rec.value - 'status';
|
||||
|
||||
-- Reconstruct _meta with only publisher-provided data
|
||||
publisher_provided := rec.value->'_meta'->'io.modelcontextprotocol.registry/publisher-provided';
|
||||
|
||||
IF publisher_provided IS NOT NULL THEN
|
||||
server_meta := jsonb_build_object('io.modelcontextprotocol.registry/publisher-provided', publisher_provided);
|
||||
updated_value := jsonb_set(updated_value, '{_meta}', server_meta);
|
||||
ELSE
|
||||
-- Remove _meta entirely if no publisher-provided data
|
||||
updated_value := updated_value - '_meta';
|
||||
END IF;
|
||||
|
||||
-- Update the JSON with cleaned structure (keeping name and version for immutable server.json)
|
||||
UPDATE servers
|
||||
SET value = updated_value
|
||||
WHERE version_id = rec.version_id;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Execute the migration
|
||||
SELECT migrate_official_metadata();
|
||||
|
||||
-- Drop the migration function
|
||||
DROP FUNCTION migrate_official_metadata();
|
||||
|
||||
-- Safety check: Normalize invalid status values before adding NOT NULL constraints
|
||||
-- This handles: NULL (missing field), empty string, string "null", and any invalid values
|
||||
-- First, log how many records have problematic status values for debugging
|
||||
DO $$
|
||||
DECLARE
|
||||
null_count INTEGER;
|
||||
empty_count INTEGER;
|
||||
invalid_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO null_count FROM servers WHERE status IS NULL;
|
||||
SELECT COUNT(*) INTO empty_count FROM servers WHERE status = '' OR status = 'null';
|
||||
SELECT COUNT(*) INTO invalid_count FROM servers WHERE status IS NOT NULL AND status NOT IN ('active', 'deprecated', 'deleted', '', 'null');
|
||||
|
||||
IF null_count > 0 OR empty_count > 0 OR invalid_count > 0 THEN
|
||||
RAISE NOTICE 'Status cleanup: NULL=%, empty/null string=%, invalid=%', null_count, empty_count, invalid_count;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Fix all problematic status values
|
||||
UPDATE servers
|
||||
SET status = 'active'
|
||||
WHERE status IS NULL
|
||||
OR status = ''
|
||||
OR status = 'null'
|
||||
OR status NOT IN ('active', 'deprecated', 'deleted');
|
||||
|
||||
-- Double-check that no NULLs remain (this should never fail after the above)
|
||||
DO $$
|
||||
DECLARE
|
||||
remaining_nulls INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO remaining_nulls FROM servers WHERE status IS NULL;
|
||||
IF remaining_nulls > 0 THEN
|
||||
RAISE EXCEPTION 'Still have % NULL status values after cleanup!', remaining_nulls;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
UPDATE servers SET published_at = NOW() WHERE published_at IS NULL;
|
||||
|
||||
-- Safety check: Ensure updated_at is never NULL
|
||||
UPDATE servers SET updated_at = NOW() WHERE updated_at IS NULL;
|
||||
|
||||
-- For is_latest: preserve existing true values, handle NULLs deterministically
|
||||
-- First, for servers where NO version has is_latest=true, mark the most recent as latest
|
||||
WITH servers_without_latest AS (
|
||||
SELECT DISTINCT s1.server_name
|
||||
FROM servers s1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM servers s2
|
||||
WHERE s2.server_name = s1.server_name
|
||||
AND s2.is_latest = true
|
||||
)
|
||||
),
|
||||
latest_versions AS (
|
||||
SELECT s.server_name, MAX(s.published_at) as max_published
|
||||
FROM servers s
|
||||
INNER JOIN servers_without_latest swl ON s.server_name = swl.server_name
|
||||
GROUP BY s.server_name
|
||||
)
|
||||
UPDATE servers s
|
||||
SET is_latest = true
|
||||
FROM latest_versions lv
|
||||
WHERE s.server_name = lv.server_name
|
||||
AND s.published_at = lv.max_published
|
||||
AND s.is_latest IS NULL;
|
||||
|
||||
-- Finally, set remaining NULLs to false
|
||||
UPDATE servers SET is_latest = false WHERE is_latest IS NULL;
|
||||
|
||||
-- Make the new columns NOT NULL now that all records have values
|
||||
ALTER TABLE servers ALTER COLUMN server_name SET NOT NULL;
|
||||
ALTER TABLE servers ALTER COLUMN version SET NOT NULL;
|
||||
ALTER TABLE servers ALTER COLUMN status SET NOT NULL;
|
||||
ALTER TABLE servers ALTER COLUMN published_at SET NOT NULL;
|
||||
ALTER TABLE servers ALTER COLUMN updated_at SET NOT NULL;
|
||||
ALTER TABLE servers ALTER COLUMN is_latest SET NOT NULL;
|
||||
|
||||
-- Drop the old primary key constraint
|
||||
ALTER TABLE servers DROP CONSTRAINT servers_pkey;
|
||||
|
||||
-- Create new composite primary key using name + version (natural key)
|
||||
ALTER TABLE servers ADD CONSTRAINT servers_pkey PRIMARY KEY (server_name, version);
|
||||
|
||||
-- Drop the old version_id column since we're using name-based keys now
|
||||
ALTER TABLE servers DROP COLUMN version_id;
|
||||
|
||||
-- Drop old indexes that used JSON paths and UUIDs
|
||||
DROP INDEX IF EXISTS idx_servers_version_id;
|
||||
DROP INDEX IF EXISTS idx_servers_name_latest;
|
||||
DROP INDEX IF EXISTS idx_servers_updated_at;
|
||||
DROP INDEX IF EXISTS idx_servers_remotes;
|
||||
DROP INDEX IF EXISTS idx_unique_server_version;
|
||||
DROP INDEX IF EXISTS idx_unique_latest_version;
|
||||
|
||||
-- Create new efficient indexes on the dedicated columns
|
||||
CREATE INDEX idx_servers_name ON servers (server_name);
|
||||
CREATE INDEX idx_servers_name_version ON servers (server_name, version);
|
||||
CREATE INDEX idx_servers_name_latest ON servers (server_name, is_latest) WHERE is_latest = true;
|
||||
CREATE INDEX idx_servers_status ON servers (status);
|
||||
CREATE INDEX idx_servers_published_at ON servers (published_at DESC);
|
||||
CREATE INDEX idx_servers_updated_at ON servers (updated_at DESC);
|
||||
|
||||
-- Ensure only one version per server can be marked as latest
|
||||
CREATE UNIQUE INDEX idx_unique_latest_per_server
|
||||
ON servers (server_name)
|
||||
WHERE is_latest = true;
|
||||
|
||||
-- Create GIN index for remaining JSON queries (remotes, packages, etc.)
|
||||
CREATE INDEX idx_servers_json_remotes ON servers USING GIN((value->'remotes'));
|
||||
CREATE INDEX idx_servers_json_packages ON servers USING GIN((value->'packages'));
|
||||
|
||||
-- Update $schema field to latest version for all entries
|
||||
UPDATE servers
|
||||
SET value = jsonb_set(value, '{$schema}', '"https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json"')
|
||||
WHERE value ? '$schema' AND value IS NOT NULL;
|
||||
|
||||
-- Add check constraints for data integrity
|
||||
ALTER TABLE servers ADD CONSTRAINT check_status_valid
|
||||
CHECK (status IN ('active', 'deprecated', 'deleted'));
|
||||
|
||||
ALTER TABLE servers ADD CONSTRAINT check_server_name_format
|
||||
CHECK (server_name ~ '^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$');
|
||||
|
||||
ALTER TABLE servers ADD CONSTRAINT check_version_not_empty
|
||||
CHECK (length(trim(version)) > 0);
|
||||
|
||||
ALTER TABLE servers ADD CONSTRAINT check_published_at_reasonable
|
||||
CHECK (published_at >= '2020-01-01'::timestamp AND published_at <= NOW() + interval '1 day');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
-- Migrate package references to canonical format
|
||||
-- This migration:
|
||||
-- - Converts OCI packages to use canonical single-line references (registry/namespace/image:tag)
|
||||
-- - Removes redundant version and registryBaseUrl fields from MCPB packages
|
||||
-- - Removes package-type-specific forbidden fields (e.g., fileSha256 from non-MCPB packages)
|
||||
-- - Ensures all packages have required transport field
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Helper function to convert OCI package to canonical reference format
|
||||
CREATE OR REPLACE FUNCTION convert_oci_package_to_canonical(pkg jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
registry_url text;
|
||||
registry_host text;
|
||||
identifier text;
|
||||
version_str text;
|
||||
canonical_ref text;
|
||||
BEGIN
|
||||
-- Start with the original package
|
||||
result := pkg;
|
||||
|
||||
-- Only process OCI packages
|
||||
IF pkg->>'registryType' != 'oci' THEN
|
||||
RETURN result;
|
||||
END IF;
|
||||
|
||||
-- Get current values
|
||||
registry_url := pkg->>'registryBaseUrl';
|
||||
identifier := pkg->>'identifier';
|
||||
version_str := pkg->>'version';
|
||||
|
||||
-- Skip if already in canonical format (no registryBaseUrl and no separate version)
|
||||
-- Canonical format has registry in identifier like "ghcr.io/owner/repo:tag"
|
||||
IF registry_url IS NULL AND (identifier LIKE '%:%' OR identifier LIKE '%@sha256:%') THEN
|
||||
RETURN result;
|
||||
END IF;
|
||||
|
||||
-- Extract registry host from registryBaseUrl
|
||||
IF registry_url IS NOT NULL THEN
|
||||
registry_host := regexp_replace(registry_url, '^https?://', '');
|
||||
ELSE
|
||||
-- Default to docker.io if no registry specified
|
||||
registry_host := 'docker.io';
|
||||
END IF;
|
||||
|
||||
-- Build canonical reference
|
||||
-- Format: registry/namespace/image:tag
|
||||
IF version_str IS NOT NULL AND version_str != '' THEN
|
||||
canonical_ref := registry_host || '/' || identifier || ':' || version_str;
|
||||
ELSE
|
||||
-- Default to 'latest' tag if no version specified
|
||||
canonical_ref := registry_host || '/' || identifier || ':latest';
|
||||
END IF;
|
||||
|
||||
-- Update identifier to canonical reference
|
||||
result := jsonb_set(result, '{identifier}', to_jsonb(canonical_ref));
|
||||
|
||||
-- Remove registryBaseUrl field (no longer needed for OCI)
|
||||
result := result - 'registryBaseUrl';
|
||||
|
||||
-- Remove version field (now part of identifier for OCI)
|
||||
result := result - 'version';
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Helper function to convert MCPB package to canonical reference format
|
||||
CREATE OR REPLACE FUNCTION convert_mcpb_package_to_canonical(pkg jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
BEGIN
|
||||
-- Start with the original package
|
||||
result := pkg;
|
||||
|
||||
-- Only process MCPB packages
|
||||
IF pkg->>'registryType' != 'mcpb' THEN
|
||||
RETURN result;
|
||||
END IF;
|
||||
|
||||
-- Remove version field if it exists (version is embedded in the download URL)
|
||||
result := result - 'version';
|
||||
|
||||
-- Remove registryBaseUrl field if it exists (not needed for MCPB)
|
||||
result := result - 'registryBaseUrl';
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Helper function to remove package-type-specific forbidden fields
|
||||
CREATE OR REPLACE FUNCTION remove_forbidden_fields(pkg jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
registry_type text;
|
||||
BEGIN
|
||||
result := pkg;
|
||||
registry_type := pkg->>'registryType';
|
||||
|
||||
-- Remove fileSha256 from non-MCPB packages (it's MCPB-only)
|
||||
IF registry_type != 'mcpb' AND result ? 'fileSha256' THEN
|
||||
result := result - 'fileSha256';
|
||||
END IF;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Helper function to ensure transport field exists
|
||||
CREATE OR REPLACE FUNCTION ensure_transport_field(pkg jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
registry_type text;
|
||||
runtime_hint text;
|
||||
BEGIN
|
||||
result := pkg;
|
||||
|
||||
-- If transport already exists, return as-is
|
||||
IF result ? 'transport' THEN
|
||||
RETURN result;
|
||||
END IF;
|
||||
|
||||
-- Get registry type and runtime hint
|
||||
registry_type := pkg->>'registryType';
|
||||
runtime_hint := pkg->>'runtimeHint';
|
||||
|
||||
-- Add default transport based on package type
|
||||
-- Most packages use stdio transport
|
||||
result := jsonb_set(result, '{transport}', '{"type": "stdio"}'::jsonb);
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Helper function to convert all packages in a packages array
|
||||
CREATE OR REPLACE FUNCTION convert_packages_array(packages jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
result jsonb := '[]'::jsonb;
|
||||
pkg jsonb;
|
||||
BEGIN
|
||||
-- Handle null or empty arrays
|
||||
IF packages IS NULL OR jsonb_array_length(packages) = 0 THEN
|
||||
RETURN packages;
|
||||
END IF;
|
||||
|
||||
-- Process each package
|
||||
FOR pkg IN SELECT * FROM jsonb_array_elements(packages)
|
||||
LOOP
|
||||
-- First convert OCI packages to canonical format
|
||||
pkg := convert_oci_package_to_canonical(pkg);
|
||||
|
||||
-- Then convert MCPB packages to canonical format
|
||||
pkg := convert_mcpb_package_to_canonical(pkg);
|
||||
|
||||
-- Remove package-type-specific forbidden fields
|
||||
pkg := remove_forbidden_fields(pkg);
|
||||
|
||||
-- Finally ensure transport field exists
|
||||
pkg := ensure_transport_field(pkg);
|
||||
|
||||
-- Add to result array
|
||||
result := result || jsonb_build_array(pkg);
|
||||
END LOOP;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Migrate all server packages to canonical format
|
||||
-- After migration 003, all data is stored in the 'value' JSONB column
|
||||
UPDATE servers
|
||||
SET value = jsonb_set(
|
||||
value,
|
||||
'{packages}',
|
||||
convert_packages_array(value->'packages')
|
||||
)
|
||||
WHERE value ? 'packages'
|
||||
AND value->'packages' IS NOT NULL
|
||||
AND jsonb_array_length(value->'packages') > 0;
|
||||
|
||||
-- Clean up helper functions
|
||||
DROP FUNCTION IF EXISTS convert_oci_package_to_canonical(jsonb);
|
||||
DROP FUNCTION IF EXISTS convert_mcpb_package_to_canonical(jsonb);
|
||||
DROP FUNCTION IF EXISTS remove_forbidden_fields(jsonb);
|
||||
DROP FUNCTION IF EXISTS ensure_transport_field(jsonb);
|
||||
DROP FUNCTION IF EXISTS convert_packages_array(jsonb);
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in a new issue