fix: use EXECUTE for dynamic SQL in migrations referencing old schema

PL/pgSQL pre-validates column/table references in static SQL statements
inside DO blocks before evaluating IF/RETURN guards. This caused
migrations 0010-0061 to fail on fresh databases where the canonical
schema uses `providers`/`provider_id` instead of `llm_providers`/
`llm_provider_id`.

Wrap all SQL that references potentially non-existent old schema objects
(llm_providers, llm_provider_id, tts_providers, tts_models, etc.) in
EXECUTE strings so they are only parsed at runtime when actually reached.
This commit is contained in:
Acbox
2026-04-07 00:39:37 +08:00
parent fd6a2fd191
commit cb003116a5
12 changed files with 227 additions and 180 deletions
+14 -14
View File
@@ -8,7 +8,6 @@ BEGIN
RETURN;
END IF;
-- 1) Add client_type column to models (nullable)
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'models' AND column_name = 'client_type') THEN
ALTER TABLE models ADD COLUMN client_type TEXT;
END IF;
@@ -17,18 +16,19 @@ BEGIN
SELECT 1 FROM information_schema.columns
WHERE table_name = 'llm_providers' AND column_name = 'client_type'
) THEN
UPDATE models SET client_type = CASE p.client_type
WHEN 'openai' THEN 'openai-responses'
WHEN 'openai-compat' THEN 'openai-completions'
WHEN 'anthropic' THEN 'anthropic-messages'
WHEN 'google' THEN 'google-generative-ai'
END
FROM llm_providers p
WHERE models.llm_provider_id = p.id
AND p.client_type IN ('openai', 'openai-compat', 'anthropic', 'google');
EXECUTE '
UPDATE models SET client_type = CASE p.client_type
WHEN ''openai'' THEN ''openai-responses''
WHEN ''openai-compat'' THEN ''openai-completions''
WHEN ''anthropic'' THEN ''anthropic-messages''
WHEN ''google'' THEN ''google-generative-ai''
END
FROM llm_providers p
WHERE models.llm_provider_id = p.id
AND p.client_type IN (''openai'', ''openai-compat'', ''anthropic'', ''google'')';
DELETE FROM models WHERE client_type IS NULL AND type = 'chat';
DELETE FROM llm_providers WHERE client_type NOT IN ('openai', 'openai-compat', 'anthropic', 'google');
EXECUTE 'DELETE FROM models WHERE client_type IS NULL AND type = ''chat''';
EXECUTE 'DELETE FROM llm_providers WHERE client_type NOT IN (''openai'', ''openai-compat'', ''anthropic'', ''google'')';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'models_client_type_check') THEN
@@ -40,6 +40,6 @@ BEGIN
CHECK (type != 'chat' OR client_type IS NOT NULL);
END IF;
ALTER TABLE llm_providers DROP CONSTRAINT IF EXISTS llm_providers_client_type_check;
ALTER TABLE llm_providers DROP COLUMN IF EXISTS client_type;
EXECUTE 'ALTER TABLE llm_providers DROP CONSTRAINT IF EXISTS llm_providers_client_type_check';
EXECUTE 'ALTER TABLE llm_providers DROP COLUMN IF EXISTS client_type';
END $$;
@@ -11,8 +11,7 @@ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'models_provider_model_id_unique')
AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'models_provider_id_model_id_unique') THEN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'models' AND column_name = 'llm_provider_id') THEN
ALTER TABLE models
ADD CONSTRAINT models_provider_model_id_unique UNIQUE (llm_provider_id, model_id);
EXECUTE 'ALTER TABLE models ADD CONSTRAINT models_provider_model_id_unique UNIQUE (llm_provider_id, model_id)';
END IF;
END IF;
END
+23 -23
View File
@@ -9,28 +9,30 @@ BEGIN
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'providers') THEN
CREATE TABLE IF NOT EXISTS tts_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
provider TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT tts_providers_name_unique UNIQUE (name)
);
EXECUTE '
CREATE TABLE IF NOT EXISTS tts_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
provider TEXT NOT NULL,
config JSONB NOT NULL DEFAULT ''{}''::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT tts_providers_name_unique UNIQUE (name)
)';
CREATE TABLE IF NOT EXISTS tts_models (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
model_id TEXT NOT NULL,
name TEXT,
tts_provider_id UUID NOT NULL REFERENCES tts_providers(id) ON DELETE CASCADE,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT tts_models_provider_model_id_unique UNIQUE (tts_provider_id, model_id)
);
EXECUTE '
CREATE TABLE IF NOT EXISTS tts_models (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
model_id TEXT NOT NULL,
name TEXT,
tts_provider_id UUID NOT NULL REFERENCES tts_providers(id) ON DELETE CASCADE,
config JSONB NOT NULL DEFAULT ''{}''::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT tts_models_provider_model_id_unique UNIQUE (tts_provider_id, model_id)
)';
CREATE INDEX IF NOT EXISTS idx_tts_models_provider_id ON tts_models(tts_provider_id);
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_tts_models_provider_id ON tts_models(tts_provider_id)';
END IF;
END $$;
@@ -42,9 +44,7 @@ BEGIN
SELECT 1 FROM pg_constraint WHERE conname = 'bots_tts_model_id_fkey'
) THEN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tts_models') THEN
ALTER TABLE bots
ADD CONSTRAINT bots_tts_model_id_fkey
FOREIGN KEY (tts_model_id) REFERENCES tts_models(id) ON DELETE SET NULL;
EXECUTE 'ALTER TABLE bots ADD CONSTRAINT bots_tts_model_id_fkey FOREIGN KEY (tts_model_id) REFERENCES tts_models(id) ON DELETE SET NULL';
ELSE
ALTER TABLE bots
ADD CONSTRAINT bots_tts_model_id_fkey
@@ -6,6 +6,6 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tts_models') THEN
ALTER TABLE tts_models DROP CONSTRAINT IF EXISTS tts_models_provider_model_id_unique;
EXECUTE 'ALTER TABLE tts_models DROP CONSTRAINT IF EXISTS tts_models_provider_model_id_unique';
END IF;
END $$;
@@ -8,26 +8,24 @@ BEGIN
RETURN;
END IF;
ALTER TABLE llm_providers
ADD COLUMN IF NOT EXISTS client_type TEXT NOT NULL DEFAULT 'openai-completions',
ADD COLUMN IF NOT EXISTS icon TEXT;
EXECUTE 'ALTER TABLE llm_providers ADD COLUMN IF NOT EXISTS client_type TEXT NOT NULL DEFAULT ''openai-completions''';
EXECUTE 'ALTER TABLE llm_providers ADD COLUMN IF NOT EXISTS icon TEXT';
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'models' AND column_name = 'client_type') THEN
UPDATE llm_providers p
SET client_type = sub.client_type
FROM (
SELECT DISTINCT ON (llm_provider_id) llm_provider_id, client_type
FROM models
WHERE client_type IS NOT NULL AND client_type != ''
ORDER BY llm_provider_id, created_at ASC
) sub
WHERE p.id = sub.llm_provider_id;
EXECUTE '
UPDATE llm_providers p
SET client_type = sub.client_type
FROM (
SELECT DISTINCT ON (llm_provider_id) llm_provider_id, client_type
FROM models
WHERE client_type IS NOT NULL AND client_type != ''''
ORDER BY llm_provider_id, created_at ASC
) sub
WHERE p.id = sub.llm_provider_id';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'llm_providers_client_type_check') THEN
ALTER TABLE llm_providers
ADD CONSTRAINT llm_providers_client_type_check
CHECK (client_type IN ('openai-responses', 'openai-completions', 'anthropic-messages', 'google-generative-ai'));
EXECUTE 'ALTER TABLE llm_providers ADD CONSTRAINT llm_providers_client_type_check CHECK (client_type IN (''openai-responses'', ''openai-completions'', ''anthropic-messages'', ''google-generative-ai''))';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'models' AND column_name = 'config') THEN
@@ -51,10 +49,16 @@ BEGIN
END IF;
END $$;
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_client_type_check;
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_chat_client_type_check;
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_dimensions_check;
ALTER TABLE models DROP COLUMN IF EXISTS client_type;
ALTER TABLE models DROP COLUMN IF EXISTS dimensions;
ALTER TABLE models DROP COLUMN IF EXISTS input_modalities;
ALTER TABLE models DROP COLUMN IF EXISTS supports_reasoning;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'llm_providers') THEN
RETURN;
END IF;
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_client_type_check;
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_chat_client_type_check;
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_dimensions_check;
ALTER TABLE models DROP COLUMN IF EXISTS client_type;
ALTER TABLE models DROP COLUMN IF EXISTS dimensions;
ALTER TABLE models DROP COLUMN IF EXISTS input_modalities;
ALTER TABLE models DROP COLUMN IF EXISTS supports_reasoning;
END $$;
+1 -1
View File
@@ -5,6 +5,6 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'llm_providers') THEN
ALTER TABLE llm_providers ADD COLUMN IF NOT EXISTS enable BOOLEAN NOT NULL DEFAULT true;
EXECUTE 'ALTER TABLE llm_providers ADD COLUMN IF NOT EXISTS enable BOOLEAN NOT NULL DEFAULT true';
END IF;
END $$;
+15 -14
View File
@@ -5,20 +5,21 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'llm_providers') THEN
CREATE TABLE IF NOT EXISTS llm_provider_oauth_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
llm_provider_id UUID NOT NULL UNIQUE REFERENCES llm_providers(id) ON DELETE CASCADE,
access_token TEXT NOT NULL DEFAULT '',
refresh_token TEXT NOT NULL DEFAULT '',
expires_at TIMESTAMPTZ,
scope TEXT NOT NULL DEFAULT '',
token_type TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL DEFAULT '',
pkce_code_verifier TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_llm_provider_oauth_tokens_state ON llm_provider_oauth_tokens(state) WHERE state != '';
EXECUTE '
CREATE TABLE IF NOT EXISTS llm_provider_oauth_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
llm_provider_id UUID NOT NULL UNIQUE REFERENCES llm_providers(id) ON DELETE CASCADE,
access_token TEXT NOT NULL DEFAULT '''',
refresh_token TEXT NOT NULL DEFAULT '''',
expires_at TIMESTAMPTZ,
scope TEXT NOT NULL DEFAULT '''',
token_type TEXT NOT NULL DEFAULT '''',
state TEXT NOT NULL DEFAULT '''',
pkce_code_verifier TEXT NOT NULL DEFAULT '''',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)';
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_llm_provider_oauth_tokens_state ON llm_provider_oauth_tokens(state) WHERE state != ''''';
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'provider_oauth_tokens') THEN
CREATE TABLE IF NOT EXISTS provider_oauth_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -8,13 +8,14 @@ BEGIN
RETURN;
END IF;
ALTER TABLE llm_providers DROP CONSTRAINT IF EXISTS llm_providers_client_type_check;
ALTER TABLE llm_providers ADD CONSTRAINT llm_providers_client_type_check
CHECK (client_type IN ('openai-responses', 'openai-completions', 'anthropic-messages', 'google-generative-ai', 'openai-codex'));
EXECUTE 'ALTER TABLE llm_providers DROP CONSTRAINT IF EXISTS llm_providers_client_type_check';
EXECUTE 'ALTER TABLE llm_providers ADD CONSTRAINT llm_providers_client_type_check
CHECK (client_type IN (''openai-responses'', ''openai-completions'', ''anthropic-messages'', ''google-generative-ai'', ''openai-codex''))';
UPDATE llm_providers
SET client_type = 'openai-codex',
updated_at = now()
WHERE client_type = 'openai-responses'
AND metadata->>'auth_type' = 'openai-codex-oauth';
EXECUTE '
UPDATE llm_providers
SET client_type = ''openai-codex'',
updated_at = now()
WHERE client_type = ''openai-responses''
AND metadata->>''auth_type'' = ''openai-codex-oauth''';
END $$;
@@ -5,6 +5,6 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tts_providers') THEN
ALTER TABLE tts_providers ADD COLUMN IF NOT EXISTS enable BOOLEAN NOT NULL DEFAULT false;
EXECUTE 'ALTER TABLE tts_providers ADD COLUMN IF NOT EXISTS enable BOOLEAN NOT NULL DEFAULT false';
END IF;
END $$;
+50 -50
View File
@@ -11,84 +11,84 @@ BEGIN
END IF;
-- Step 1: Rename llm_providers → providers
ALTER TABLE llm_providers RENAME TO providers;
ALTER TABLE providers RENAME CONSTRAINT llm_providers_name_unique TO providers_name_unique;
ALTER TABLE providers DROP CONSTRAINT IF EXISTS llm_providers_client_type_check;
EXECUTE 'ALTER TABLE llm_providers RENAME TO providers';
EXECUTE 'ALTER TABLE providers RENAME CONSTRAINT llm_providers_name_unique TO providers_name_unique';
EXECUTE 'ALTER TABLE providers DROP CONSTRAINT IF EXISTS llm_providers_client_type_check';
-- Step 2: Add config JSONB and migrate api_key + base_url into it
ALTER TABLE providers ADD COLUMN IF NOT EXISTS config JSONB NOT NULL DEFAULT '{}'::jsonb;
UPDATE providers SET config = jsonb_build_object('api_key', api_key, 'base_url', base_url)
WHERE api_key IS NOT NULL;
ALTER TABLE providers DROP COLUMN IF EXISTS api_key;
ALTER TABLE providers DROP COLUMN IF EXISTS base_url;
EXECUTE 'ALTER TABLE providers ADD COLUMN IF NOT EXISTS config JSONB NOT NULL DEFAULT ''{}''::jsonb';
EXECUTE 'UPDATE providers SET config = jsonb_build_object(''api_key'', api_key, ''base_url'', base_url) WHERE api_key IS NOT NULL';
EXECUTE 'ALTER TABLE providers DROP COLUMN IF EXISTS api_key';
EXECUTE 'ALTER TABLE providers DROP COLUMN IF EXISTS base_url';
-- Step 3: Expand client_type CHECK
ALTER TABLE providers ADD CONSTRAINT providers_client_type_check CHECK (
EXECUTE 'ALTER TABLE providers ADD CONSTRAINT providers_client_type_check CHECK (
client_type IN (
'openai-responses', 'openai-completions', 'anthropic-messages',
'google-generative-ai', 'openai-codex', 'edge-speech'
''openai-responses'', ''openai-completions'', ''anthropic-messages'',
''google-generative-ai'', ''openai-codex'', ''edge-speech''
)
);
)';
-- Step 4: Rename llm_provider_id → provider_id in models table
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'models' AND column_name = 'llm_provider_id') THEN
ALTER TABLE models RENAME COLUMN llm_provider_id TO provider_id;
EXECUTE 'ALTER TABLE models RENAME COLUMN llm_provider_id TO provider_id';
END IF;
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'models_provider_model_id_unique') THEN
ALTER TABLE models RENAME CONSTRAINT models_provider_model_id_unique TO models_provider_id_model_id_unique;
EXECUTE 'ALTER TABLE models RENAME CONSTRAINT models_provider_model_id_unique TO models_provider_id_model_id_unique';
END IF;
-- Step 5: Expand models type CHECK to include speech
ALTER TABLE models DROP CONSTRAINT IF EXISTS models_type_check;
ALTER TABLE models ADD CONSTRAINT models_type_check CHECK (type IN ('chat', 'embedding', 'speech'));
EXECUTE 'ALTER TABLE models DROP CONSTRAINT IF EXISTS models_type_check';
EXECUTE 'ALTER TABLE models ADD CONSTRAINT models_type_check CHECK (type IN (''chat'', ''embedding'', ''speech''))';
-- Step 6: Migrate tts_providers into providers
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tts_providers') THEN
INSERT INTO providers (id, name, client_type, icon, enable, config, metadata, created_at, updated_at)
SELECT
tp.id,
tp.name,
CASE WHEN tp.provider = 'edge' THEN 'edge-speech' ELSE tp.provider END,
NULL,
tp.enable,
tp.config,
'{}'::jsonb,
tp.created_at,
tp.updated_at
FROM tts_providers tp
WHERE NOT EXISTS (SELECT 1 FROM providers p WHERE p.id = tp.id);
EXECUTE '
INSERT INTO providers (id, name, client_type, icon, enable, config, metadata, created_at, updated_at)
SELECT
tp.id,
tp.name,
CASE WHEN tp.provider = ''edge'' THEN ''edge-speech'' ELSE tp.provider END,
NULL,
tp.enable,
tp.config,
''{}''::jsonb,
tp.created_at,
tp.updated_at
FROM tts_providers tp
WHERE NOT EXISTS (SELECT 1 FROM providers p WHERE p.id = tp.id)';
END IF;
-- Step 7: Migrate tts_models into models
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tts_models') THEN
INSERT INTO models (id, model_id, name, provider_id, type, config, created_at, updated_at)
SELECT
tm.id,
tm.model_id,
tm.name,
tm.tts_provider_id,
'speech',
tm.config,
tm.created_at,
tm.updated_at
FROM tts_models tm
WHERE NOT EXISTS (SELECT 1 FROM models m WHERE m.id = tm.id);
EXECUTE '
INSERT INTO models (id, model_id, name, provider_id, type, config, created_at, updated_at)
SELECT
tm.id,
tm.model_id,
tm.name,
tm.tts_provider_id,
''speech'',
tm.config,
tm.created_at,
tm.updated_at
FROM tts_models tm
WHERE NOT EXISTS (SELECT 1 FROM models m WHERE m.id = tm.id)';
END IF;
-- Step 8: Update bots.tts_model_id FK to reference models instead of tts_models
ALTER TABLE bots DROP CONSTRAINT IF EXISTS bots_tts_model_id_fkey;
ALTER TABLE bots ADD CONSTRAINT bots_tts_model_id_fkey
FOREIGN KEY (tts_model_id) REFERENCES models(id) ON DELETE SET NULL;
EXECUTE 'ALTER TABLE bots DROP CONSTRAINT IF EXISTS bots_tts_model_id_fkey';
EXECUTE 'ALTER TABLE bots ADD CONSTRAINT bots_tts_model_id_fkey FOREIGN KEY (tts_model_id) REFERENCES models(id) ON DELETE SET NULL';
-- Step 9: Drop tts_models and tts_providers
DROP INDEX IF EXISTS idx_tts_models_provider_id;
DROP TABLE IF EXISTS tts_models;
DROP TABLE IF EXISTS tts_providers;
EXECUTE 'DROP INDEX IF EXISTS idx_tts_models_provider_id';
EXECUTE 'DROP TABLE IF EXISTS tts_models';
EXECUTE 'DROP TABLE IF EXISTS tts_providers';
-- Step 10: Rename llm_provider_oauth_tokens → provider_oauth_tokens
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'llm_provider_oauth_tokens') THEN
ALTER TABLE llm_provider_oauth_tokens RENAME TO provider_oauth_tokens;
ALTER TABLE provider_oauth_tokens RENAME COLUMN llm_provider_id TO provider_id;
ALTER INDEX IF EXISTS idx_llm_provider_oauth_tokens_state RENAME TO idx_provider_oauth_tokens_state;
EXECUTE 'ALTER TABLE llm_provider_oauth_tokens RENAME TO provider_oauth_tokens';
EXECUTE 'ALTER TABLE provider_oauth_tokens RENAME COLUMN llm_provider_id TO provider_id';
EXECUTE 'ALTER INDEX IF EXISTS idx_llm_provider_oauth_tokens_state RENAME TO idx_provider_oauth_tokens_state';
END IF;
END $$;