Commit 73a8683c authored by 陈曦's avatar 陈曦
Browse files

append migration files by v117

parent b017f461
Pipeline #82250 passed with stage
in 23 seconds
ALTER TABLE channels ADD COLUMN IF NOT EXISTS features TEXT NOT NULL DEFAULT '';
COMMENT ON COLUMN channels.features IS '渠道特性描述,JSON 数组格式,用于支付页面展示';
-- Account statistics pricing: allow channels to configure custom pricing for account cost tracking.
-- 1. Channel-level toggle
ALTER TABLE channels ADD COLUMN IF NOT EXISTS apply_pricing_to_account_stats BOOLEAN NOT NULL DEFAULT FALSE;
-- 2. Account stats pricing rules (ordered list per channel)
CREATE TABLE IF NOT EXISTS channel_account_stats_pricing_rules (
id BIGSERIAL PRIMARY KEY,
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL DEFAULT '',
group_ids BIGINT[] NOT NULL DEFAULT '{}',
account_ids BIGINT[] NOT NULL DEFAULT '{}',
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_cas_pricing_rules_channel_id ON channel_account_stats_pricing_rules(channel_id);
-- 3. Model pricing for each rule (same structure as channel_model_pricing)
CREATE TABLE IF NOT EXISTS channel_account_stats_model_pricing (
id BIGSERIAL PRIMARY KEY,
rule_id BIGINT NOT NULL REFERENCES channel_account_stats_pricing_rules(id) ON DELETE CASCADE,
platform VARCHAR(50) NOT NULL DEFAULT '',
models JSONB NOT NULL DEFAULT '[]',
billing_mode VARCHAR(20) NOT NULL DEFAULT 'token',
input_price NUMERIC(20,10),
output_price NUMERIC(20,10),
cache_write_price NUMERIC(20,10),
cache_read_price NUMERIC(20,10),
image_output_price NUMERIC(20,10),
per_request_price NUMERIC(20,10),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_cas_model_pricing_rule_id ON channel_account_stats_model_pricing(rule_id);
-- 4. Usage logs: pre-computed account stats cost (NULL = use default formula)
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS account_stats_cost NUMERIC(20,10);
-- Balance notification user preferences
ALTER TABLE users ADD COLUMN IF NOT EXISTS balance_notify_enabled BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE users ADD COLUMN IF NOT EXISTS balance_notify_threshold DECIMAL(20,8) DEFAULT NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS balance_notify_extra_emails TEXT NOT NULL DEFAULT '[]';
ALTER TABLE channels ADD COLUMN IF NOT EXISTS features_config JSONB NOT NULL DEFAULT '{}';
COMMENT ON COLUMN channels.features_config IS '渠道特性配置(如 web_search_emulation),JSON 对象格式';
-- Add threshold type support (fixed / percentage) to balance notification
ALTER TABLE users ADD COLUMN IF NOT EXISTS balance_notify_threshold_type VARCHAR(10) NOT NULL DEFAULT 'fixed';
-- Track cumulative recharge amount for percentage threshold calculation
ALTER TABLE users ADD COLUMN IF NOT EXISTS total_recharged DECIMAL(20,8) NOT NULL DEFAULT 0;
ALTER TABLE payment_provider_instances ADD COLUMN IF NOT EXISTS allow_user_refund BOOLEAN NOT NULL DEFAULT false;
-- Migrate notification email lists from old []string format to new []NotifyEmailEntry format
-- Old: ["a@x.com", "b@x.com"]
-- New: [{"email":"a@x.com","disabled":false,"verified":true}, ...]
-- Existing emails are marked as verified=false (unverified), disabled=false (enabled)
-- 1. User balance notification emails
UPDATE users
SET balance_notify_extra_emails = (
SELECT COALESCE(
jsonb_agg(jsonb_build_object('email', elem::text, 'disabled', false, 'verified', false)),
'[]'::jsonb
)::text
FROM jsonb_array_elements_text(balance_notify_extra_emails::jsonb) AS elem
)
WHERE balance_notify_extra_emails IS NOT NULL
AND balance_notify_extra_emails <> '[]'
AND balance_notify_extra_emails <> ''
AND (balance_notify_extra_emails::jsonb -> 0) IS NOT NULL
AND jsonb_typeof(balance_notify_extra_emails::jsonb -> 0) = 'string';
-- 2. Admin account quota notification emails
UPDATE settings
SET value = (
SELECT COALESCE(
jsonb_agg(jsonb_build_object('email', elem::text, 'disabled', false, 'verified', false)),
'[]'::jsonb
)::text
FROM jsonb_array_elements_text(value::jsonb) AS elem
)
WHERE key = 'account_quota_notify_emails'
AND value IS NOT NULL
AND value <> '[]'
AND value <> ''
AND (value::jsonb -> 0) IS NOT NULL
AND jsonb_typeof(value::jsonb -> 0) = 'string';
-- Convert old boolean web_search_emulation to tri-state string
-- true → "enabled", false → remove key (becomes "default")
UPDATE accounts
SET extra = (extra - 'web_search_emulation') || jsonb_build_object('web_search_emulation', 'enabled')
WHERE extra ? 'web_search_emulation'
AND extra->>'web_search_emulation' = 'true';
UPDATE accounts
SET extra = extra - 'web_search_emulation'
WHERE extra ? 'web_search_emulation'
AND extra->>'web_search_emulation' = 'false';
-- Add intervals table for account stats pricing rules (mirrors channel_pricing_intervals).
CREATE TABLE IF NOT EXISTS channel_account_stats_pricing_intervals (
id BIGSERIAL PRIMARY KEY,
pricing_id BIGINT NOT NULL REFERENCES channel_account_stats_model_pricing(id) ON DELETE CASCADE,
min_tokens INT NOT NULL DEFAULT 0,
max_tokens INT,
tier_label VARCHAR(50),
input_price NUMERIC(20,12),
output_price NUMERIC(20,12),
cache_write_price NUMERIC(20,12),
cache_read_price NUMERIC(20,12),
per_request_price NUMERIC(20,12),
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_account_stats_pricing_intervals_pricing_id
ON channel_account_stats_pricing_intervals (pricing_id);
-- Add account_cost column to dashboard aggregation tables for admin dashboard display.
-- account_cost = SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1))
ALTER TABLE usage_dashboard_hourly ADD COLUMN IF NOT EXISTS account_cost DECIMAL(20, 10) NOT NULL DEFAULT 0;
ALTER TABLE usage_dashboard_daily ADD COLUMN IF NOT EXISTS account_cost DECIMAL(20, 10) NOT NULL DEFAULT 0;
ALTER TABLE users
ADD COLUMN IF NOT EXISTS signup_source VARCHAR(20) NOT NULL DEFAULT 'email',
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS last_active_at TIMESTAMPTZ NULL;
UPDATE users
SET signup_source = 'email'
WHERE signup_source IS NULL OR signup_source = '';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'users_signup_source_check'
) THEN
ALTER TABLE users
ADD CONSTRAINT users_signup_source_check
CHECK (signup_source IN ('email', 'linuxdo', 'wechat', 'oidc'));
END IF;
END $$;
CREATE TABLE IF NOT EXISTS auth_identities (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_type VARCHAR(20) NOT NULL,
provider_key TEXT NOT NULL,
provider_subject TEXT NOT NULL,
verified_at TIMESTAMPTZ NULL,
issuer TEXT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT auth_identities_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc'))
);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identities_provider_subject_key
ON auth_identities (provider_type, provider_key, provider_subject);
CREATE INDEX IF NOT EXISTS auth_identities_user_id_idx
ON auth_identities (user_id);
CREATE INDEX IF NOT EXISTS auth_identities_user_provider_idx
ON auth_identities (user_id, provider_type);
CREATE TABLE IF NOT EXISTS auth_identity_channels (
id BIGSERIAL PRIMARY KEY,
identity_id BIGINT NOT NULL REFERENCES auth_identities(id) ON DELETE CASCADE,
provider_type VARCHAR(20) NOT NULL,
provider_key TEXT NOT NULL,
channel VARCHAR(20) NOT NULL,
channel_app_id TEXT NOT NULL,
channel_subject TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT auth_identity_channels_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc'))
);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identity_channels_channel_key
ON auth_identity_channels (provider_type, provider_key, channel, channel_app_id, channel_subject);
CREATE INDEX IF NOT EXISTS auth_identity_channels_identity_id_idx
ON auth_identity_channels (identity_id);
CREATE TABLE IF NOT EXISTS pending_auth_sessions (
id BIGSERIAL PRIMARY KEY,
session_token VARCHAR(255) NOT NULL,
intent VARCHAR(40) NOT NULL,
provider_type VARCHAR(20) NOT NULL,
provider_key TEXT NOT NULL,
provider_subject TEXT NOT NULL,
target_user_id BIGINT NULL REFERENCES users(id) ON DELETE SET NULL,
redirect_to TEXT NOT NULL DEFAULT '',
resolved_email TEXT NOT NULL DEFAULT '',
registration_password_hash TEXT NOT NULL DEFAULT '',
upstream_identity_claims JSONB NOT NULL DEFAULT '{}'::jsonb,
local_flow_state JSONB NOT NULL DEFAULT '{}'::jsonb,
browser_session_key TEXT NOT NULL DEFAULT '',
completion_code_hash TEXT NOT NULL DEFAULT '',
completion_code_expires_at TIMESTAMPTZ NULL,
email_verified_at TIMESTAMPTZ NULL,
password_verified_at TIMESTAMPTZ NULL,
totp_verified_at TIMESTAMPTZ NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pending_auth_sessions_intent_check
CHECK (intent IN ('login', 'bind_current_user', 'adopt_existing_user_by_email')),
CONSTRAINT pending_auth_sessions_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc'))
);
CREATE UNIQUE INDEX IF NOT EXISTS pending_auth_sessions_session_token_key
ON pending_auth_sessions (session_token);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_target_user_id_idx
ON pending_auth_sessions (target_user_id);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_expires_at_idx
ON pending_auth_sessions (expires_at);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_provider_idx
ON pending_auth_sessions (provider_type, provider_key, provider_subject);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_completion_code_idx
ON pending_auth_sessions (completion_code_hash);
CREATE TABLE IF NOT EXISTS identity_adoption_decisions (
id BIGSERIAL PRIMARY KEY,
pending_auth_session_id BIGINT NOT NULL REFERENCES pending_auth_sessions(id) ON DELETE CASCADE,
identity_id BIGINT NULL REFERENCES auth_identities(id) ON DELETE SET NULL,
adopt_display_name BOOLEAN NOT NULL DEFAULT FALSE,
adopt_avatar BOOLEAN NOT NULL DEFAULT FALSE,
decided_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS identity_adoption_decisions_pending_auth_session_id_key
ON identity_adoption_decisions (pending_auth_session_id);
CREATE INDEX IF NOT EXISTS identity_adoption_decisions_identity_id_idx
ON identity_adoption_decisions (identity_id);
CREATE TABLE IF NOT EXISTS auth_identity_migration_reports (
id BIGSERIAL PRIMARY KEY,
report_type VARCHAR(40) NOT NULL,
report_key TEXT NOT NULL,
details JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS auth_identity_migration_reports_type_idx
ON auth_identity_migration_reports (report_type);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identity_migration_reports_type_key
ON auth_identity_migration_reports (report_type, report_key);
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'auth_identity_migration_reports'
AND column_name = 'report_type'
AND COALESCE(character_maximum_length, 0) < 80
) THEN
ALTER TABLE auth_identity_migration_reports
ALTER COLUMN report_type TYPE VARCHAR(80);
END IF;
END $$;
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
u.id,
'email',
'email',
LOWER(BTRIM(u.email)),
COALESCE(u.updated_at, u.created_at, NOW()),
jsonb_build_object(
'backfill_source', 'users.email',
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND BTRIM(COALESCE(u.email, '')) <> ''
AND RIGHT(LOWER(BTRIM(u.email)), LENGTH('@linuxdo-connect.invalid')) <> '@linuxdo-connect.invalid'
AND RIGHT(LOWER(BTRIM(u.email)), LENGTH('@oidc-connect.invalid')) <> '@oidc-connect.invalid'
AND RIGHT(LOWER(BTRIM(u.email)), LENGTH('@wechat-connect.invalid')) <> '@wechat-connect.invalid'
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
u.id,
'linuxdo',
'linuxdo',
SUBSTRING(BTRIM(u.email) FROM '(?i)^linuxdo-(.+)@linuxdo-connect\.invalid$'),
COALESCE(u.updated_at, u.created_at, NOW()),
jsonb_build_object(
'backfill_source', 'synthetic_email',
'legacy_email', BTRIM(u.email),
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^linuxdo-.+@linuxdo-connect\.invalid$'
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
u.id,
'wechat',
'wechat',
SUBSTRING(BTRIM(u.email) FROM '(?i)^wechat-(.+)@wechat-connect\.invalid$'),
COALESCE(u.updated_at, u.created_at, NOW()),
jsonb_build_object(
'backfill_source', 'synthetic_email',
'legacy_email', BTRIM(u.email),
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^wechat-.+@wechat-connect\.invalid$'
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
UPDATE users
SET signup_source = 'linuxdo'
WHERE deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(email, ''))) ~ '^linuxdo-.+@linuxdo-connect\.invalid$';
UPDATE users
SET signup_source = 'wechat'
WHERE deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(email, ''))) ~ '^wechat-.+@wechat-connect\.invalid$';
UPDATE users
SET signup_source = 'oidc'
WHERE deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(email, ''))) ~ '^oidc-.+@oidc-connect\.invalid$';
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'oidc_synthetic_email_requires_manual_recovery',
CAST(u.id AS TEXT),
jsonb_build_object(
'user_id', u.id,
'email', LOWER(BTRIM(u.email)),
'reason', 'cannot recover issuer_plus_sub deterministically from synthetic email alone',
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^oidc-.+@oidc-connect\.invalid$'
ON CONFLICT (report_type, report_key) DO NOTHING;
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
CAST(u.id AS TEXT),
jsonb_build_object(
'user_id', u.id,
'email', LOWER(BTRIM(u.email)),
'reason', 'legacy wechat synthetic identity requires explicit unionid remediation if channel-only data exists',
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^wechat-.+@wechat-connect\.invalid$'
AND NOT EXISTS (
SELECT 1
FROM auth_identities ai
WHERE ai.user_id = u.id
AND ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat'
)
ON CONFLICT (report_type, report_key) DO NOTHING;
CREATE TABLE IF NOT EXISTS user_provider_default_grants (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_type VARCHAR(20) NOT NULL,
grant_reason VARCHAR(20) NOT NULL DEFAULT 'first_bind',
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT user_provider_default_grants_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc')),
CONSTRAINT user_provider_default_grants_reason_check
CHECK (grant_reason IN ('signup', 'first_bind'))
);
CREATE UNIQUE INDEX IF NOT EXISTS user_provider_default_grants_user_provider_reason_key
ON user_provider_default_grants (user_id, provider_type, grant_reason);
CREATE INDEX IF NOT EXISTS user_provider_default_grants_user_id_idx
ON user_provider_default_grants (user_id);
CREATE TABLE IF NOT EXISTS user_avatars (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
storage_provider VARCHAR(20) NOT NULL DEFAULT 'database',
storage_key TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
content_type VARCHAR(100) NOT NULL DEFAULT '',
byte_size INT NOT NULL DEFAULT 0,
sha256 VARCHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS user_avatars_user_id_key
ON user_avatars (user_id);
INSERT INTO settings (key, value)
VALUES
('auth_source_default_email_balance', '0'),
('auth_source_default_email_concurrency', '5'),
('auth_source_default_email_subscriptions', '[]'),
('auth_source_default_email_grant_on_signup', 'false'),
('auth_source_default_email_grant_on_first_bind', 'false'),
('auth_source_default_linuxdo_balance', '0'),
('auth_source_default_linuxdo_concurrency', '5'),
('auth_source_default_linuxdo_subscriptions', '[]'),
('auth_source_default_linuxdo_grant_on_signup', 'false'),
('auth_source_default_linuxdo_grant_on_first_bind', 'false'),
('auth_source_default_oidc_balance', '0'),
('auth_source_default_oidc_concurrency', '5'),
('auth_source_default_oidc_subscriptions', '[]'),
('auth_source_default_oidc_grant_on_signup', 'false'),
('auth_source_default_oidc_grant_on_first_bind', 'false'),
('auth_source_default_wechat_balance', '0'),
('auth_source_default_wechat_concurrency', '5'),
('auth_source_default_wechat_subscriptions', '[]'),
('auth_source_default_wechat_grant_on_signup', 'false'),
('auth_source_default_wechat_grant_on_first_bind', 'false'),
('force_email_on_third_party_signup', 'false')
ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value)
VALUES
('payment_visible_method_alipay_source', ''),
('payment_visible_method_wxpay_source', ''),
('payment_visible_method_alipay_enabled', 'false'),
('payment_visible_method_wxpay_enabled', 'false'),
('openai_advanced_scheduler_enabled', 'false')
ON CONFLICT (key) DO NOTHING;
ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS provider_key VARCHAR(30);
UPDATE payment_orders
SET provider_key = (
SELECT provider_key
FROM payment_provider_instances
WHERE CAST(id AS TEXT) = payment_orders.provider_instance_id
)
WHERE provider_key IS NULL
AND provider_instance_id IS NOT NULL;
UPDATE auth_identities AS ai
SET
provider_key = 'wechat-main',
metadata = COALESCE(ai.metadata, '{}'::jsonb) || jsonb_build_object(
'legacy_provider_key', 'wechat',
'normalized_by_migration', '113_normalize_legacy_wechat_provider_key'
),
updated_at = NOW()
WHERE ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat'
AND NOT EXISTS (
SELECT 1
FROM auth_identities AS canon
WHERE canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.provider_subject = ai.provider_subject
);
UPDATE auth_identity_channels AS channel
SET
provider_key = 'wechat-main',
metadata = COALESCE(channel.metadata, '{}'::jsonb) || jsonb_build_object(
'legacy_provider_key', 'wechat',
'normalized_by_migration', '113_normalize_legacy_wechat_provider_key'
),
updated_at = NOW()
WHERE channel.provider_type = 'wechat'
AND channel.provider_key = 'wechat'
AND NOT EXISTS (
SELECT 1
FROM auth_identity_channels AS canon
WHERE canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.channel = channel.channel
AND canon.channel_app_id = channel.channel_app_id
AND canon.channel_subject = channel.channel_subject
);
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_provider_key_conflict',
CAST(ai.id AS TEXT),
jsonb_build_object(
'legacy_identity_id', ai.id,
'legacy_user_id', ai.user_id,
'provider_subject', ai.provider_subject,
'canonical_identity_id', canon.id,
'canonical_user_id', canon.user_id,
'same_user', canon.user_id = ai.user_id,
'migration', '113_normalize_legacy_wechat_provider_key'
)
FROM auth_identities AS ai
JOIN auth_identities AS canon
ON canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.provider_subject = ai.provider_subject
WHERE ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat'
ON CONFLICT (report_type, report_key) DO NOTHING;
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_channel_provider_key_conflict',
CAST(channel.id AS TEXT),
jsonb_build_object(
'legacy_channel_id', channel.id,
'legacy_identity_id', channel.identity_id,
'canonical_channel_id', canon.id,
'canonical_identity_id', canon.identity_id,
'channel', channel.channel,
'channel_app_id', channel.channel_app_id,
'channel_subject', channel.channel_subject,
'same_user', COALESCE(legacy_identity.user_id = canonical_identity.user_id, FALSE),
'migration', '113_normalize_legacy_wechat_provider_key'
)
FROM auth_identity_channels AS channel
JOIN auth_identity_channels AS canon
ON canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.channel = channel.channel
AND canon.channel_app_id = channel.channel_app_id
AND canon.channel_subject = channel.channel_subject
LEFT JOIN auth_identities AS legacy_identity
ON legacy_identity.id = channel.identity_id
LEFT JOIN auth_identities AS canonical_identity
ON canonical_identity.id = canon.identity_id
WHERE channel.provider_type = 'wechat'
AND channel.provider_key = 'wechat'
ON CONFLICT (report_type, report_key) DO NOTHING;
ALTER TABLE auth_identity_migration_reports
ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ NULL;
ALTER TABLE auth_identity_migration_reports
ADD COLUMN IF NOT EXISTS resolved_by_user_id BIGINT NULL;
ALTER TABLE auth_identity_migration_reports
ADD COLUMN IF NOT EXISTS resolution_note TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_auth_identity_migration_reports_resolved_at
ON auth_identity_migration_reports (resolved_at);
CREATE OR REPLACE FUNCTION public.__migration_115_safe_legacy_metadata_jsonb(input_text TEXT)
RETURNS JSONB
LANGUAGE plpgsql
AS $$
DECLARE
parsed JSONB;
BEGIN
IF input_text IS NULL OR BTRIM(input_text) = '' THEN
RETURN '{}'::jsonb;
END IF;
BEGIN
parsed := input_text::jsonb;
EXCEPTION
WHEN OTHERS THEN
RETURN '{}'::jsonb;
END;
IF jsonb_typeof(parsed) = 'object' THEN
RETURN parsed;
END IF;
RETURN jsonb_build_object('_legacy_metadata_raw_json', parsed);
END;
$$;
DO $$
BEGIN
IF to_regclass('public.user_external_identities') IS NULL THEN
RETURN;
END IF;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_username) AS provider_username,
BTRIM(uei.display_name) AS display_name,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
uei.created_at,
uei.updated_at
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_user_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_user_id
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_user_id
ORDER BY COALESCE(legacy.updated_at, legacy.created_at, NOW()) DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_user_id
AND subjects.distinct_user_count = 1
)
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
'linuxdo',
'linuxdo',
legacy.provider_user_id,
COALESCE(legacy.updated_at, legacy.created_at, NOW()),
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'provider_user_id', legacy.provider_user_id,
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM canonical_legacy AS legacy
WHERE legacy.canonical_row_num = 1
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(uei.provider_username) AS provider_username,
BTRIM(uei.display_name) AS display_name,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
uei.created_at,
uei.updated_at
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_union_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_union_id
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_union_id
ORDER BY COALESCE(legacy.updated_at, legacy.created_at, NOW()) DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_union_id
AND subjects.distinct_user_count = 1
)
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
'wechat',
'wechat-main',
legacy.provider_union_id,
COALESCE(legacy.updated_at, legacy.created_at, NOW()),
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'openid', legacy.provider_user_id,
'unionid', legacy.provider_union_id,
'provider_user_id', legacy.provider_user_id,
'provider_union_id', legacy.provider_union_id,
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM canonical_legacy AS legacy
WHERE legacy.canonical_row_num = 1
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(COALESCE(meta.metadata_json ->> 'channel', '')) AS channel,
BTRIM(COALESCE(meta.metadata_json ->> 'channel_app_id', meta.metadata_json ->> 'appid', meta.metadata_json ->> 'app_id', '')) AS channel_app_id,
meta.metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
CROSS JOIN LATERAL (
SELECT public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
) AS meta
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_union_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_union_id
)
INSERT INTO auth_identity_channels (
identity_id,
provider_type,
provider_key,
channel,
channel_app_id,
channel_subject,
metadata
)
SELECT
ai.id,
'wechat',
'wechat-main',
legacy.channel,
legacy.channel_app_id,
legacy.provider_user_id,
legacy.metadata_json || jsonb_build_object(
'openid', legacy.provider_user_id,
'unionid', legacy.provider_union_id,
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_union_id
AND subjects.distinct_user_count = 1
JOIN auth_identities AS ai
ON ai.user_id = legacy.user_id
AND ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat-main'
AND ai.provider_subject = legacy.provider_union_id
WHERE legacy.channel <> ''
AND legacy.channel_app_id <> ''
AND legacy.provider_user_id <> ''
ON CONFLICT DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'user_id', legacy.user_id,
'openid', legacy.provider_user_id,
'reason', 'legacy user_external_identities row only has openid and cannot be canonicalized offline',
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_union_id, '')) = ''
) AS legacy
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
END $$;
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
'synthetic_auth_identity:' || ai.id::text,
COALESCE(ai.metadata, '{}'::jsonb) || jsonb_build_object(
'auth_identity_id', ai.id,
'user_id', ai.user_id,
'provider_subject', ai.provider_subject,
'reason', 'synthetic wechat auth identity still lacks unionid metadata and needs remediation',
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM auth_identities AS ai
WHERE ai.provider_type = 'wechat'
AND COALESCE(ai.metadata ->> 'backfill_source', '') = 'synthetic_email'
AND BTRIM(COALESCE(ai.metadata ->> 'unionid', '')) = ''
ON CONFLICT (report_type, report_key) DO NOTHING;
DROP FUNCTION IF EXISTS public.__migration_115_safe_legacy_metadata_jsonb(TEXT);
CREATE OR REPLACE FUNCTION public.__migration_116_safe_legacy_metadata_jsonb(input_text TEXT)
RETURNS JSONB
LANGUAGE plpgsql
AS $$
DECLARE
parsed JSONB;
BEGIN
IF input_text IS NULL OR BTRIM(input_text) = '' THEN
RETURN '{}'::jsonb;
END IF;
BEGIN
parsed := input_text::jsonb;
EXCEPTION
WHEN OTHERS THEN
RETURN '{}'::jsonb;
END;
IF jsonb_typeof(parsed) = 'object' THEN
RETURN parsed;
END IF;
RETURN jsonb_build_object('_legacy_metadata_raw_json', parsed);
END;
$$;
CREATE OR REPLACE FUNCTION public.__migration_116_is_valid_legacy_metadata_jsonb(input_text TEXT)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
DECLARE
parsed JSONB;
BEGIN
IF input_text IS NULL OR BTRIM(input_text) = '' THEN
RETURN TRUE;
END IF;
parsed := input_text::jsonb;
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
RETURN FALSE;
END;
$$;
DO $$
BEGIN
IF to_regclass('public.user_external_identities') IS NULL THEN
RETURN;
END IF;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_external_identity_invalid_metadata_json',
'legacy_external_identity:' || uei.id::text,
jsonb_build_object(
'legacy_identity_id', uei.id,
'user_id', uei.user_id,
'provider', LOWER(BTRIM(COALESCE(uei.provider, ''))),
'provider_user_id', BTRIM(COALESCE(uei.provider_user_id, '')),
'provider_union_id', BTRIM(COALESCE(uei.provider_union_id, '')),
'reason', 'legacy metadata is not valid JSON; migration downgraded metadata to empty object',
'raw_metadata', LEFT(BTRIM(COALESCE(uei.metadata, '')), 1000),
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND BTRIM(COALESCE(uei.metadata, '')) <> ''
AND NOT public.__migration_116_is_valid_legacy_metadata_jsonb(uei.metadata)
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_external_identity_conflict',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'legacy_user_id', legacy.user_id,
'provider_type', legacy.provider_type,
'provider_key', legacy.provider_key,
'provider_subject', legacy.provider_subject,
'conflicting_legacy_user_ids', ambiguous.conflicting_legacy_user_ids,
'reason', 'legacy canonical identity subject belongs to multiple legacy users and cannot be auto-resolved',
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM (
SELECT
uei.id,
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy
JOIN (
SELECT
provider_type,
provider_key,
provider_subject,
to_jsonb(array_agg(DISTINCT user_id ORDER BY user_id)) AS conflicting_legacy_user_ids
FROM (
SELECT
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy_subjects
GROUP BY provider_type, provider_key, provider_subject
HAVING COUNT(DISTINCT user_id) > 1
) AS ambiguous
ON ambiguous.provider_type = legacy.provider_type
AND ambiguous.provider_key = legacy.provider_key
AND ambiguous.provider_subject = legacy.provider_subject
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_external_identity_conflict',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'legacy_user_id', legacy.user_id,
'existing_identity_id', ai.id,
'existing_user_id', ai.user_id,
'provider_type', legacy.provider_type,
'provider_key', legacy.provider_key,
'provider_subject', legacy.provider_subject,
'reason', 'legacy canonical identity subject already belongs to another user',
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM (
SELECT
uei.id,
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject,
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id,
BTRIM(COALESCE(uei.provider_username, '')) AS provider_username,
BTRIM(COALESCE(uei.display_name, '')) AS display_name,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy
JOIN (
SELECT
provider_type,
provider_key,
provider_subject
FROM (
SELECT
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
) AS legacy_subjects
GROUP BY provider_type, provider_key, provider_subject
HAVING COUNT(DISTINCT user_id) = 1
) AS clear_subjects
ON clear_subjects.provider_type = legacy.provider_type
AND clear_subjects.provider_key = legacy.provider_key
AND clear_subjects.provider_subject = legacy.provider_subject
JOIN auth_identities AS ai
ON ai.provider_type = legacy.provider_type
AND ai.provider_key = legacy.provider_key
AND ai.provider_subject = legacy.provider_subject
WHERE ai.user_id <> legacy.user_id
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.id,
uei.user_id,
LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main'
ELSE 'linuxdo'
END AS provider_key,
CASE
WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, ''))
ELSE BTRIM(COALESCE(uei.provider_user_id, ''))
END AS provider_subject,
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id,
BTRIM(COALESCE(uei.provider_username, '')) AS provider_username,
BTRIM(COALESCE(uei.display_name, '')) AS display_name,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
COALESCE(uei.updated_at, uei.created_at, NOW()) AS verified_at
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) IN ('linuxdo', 'wechat')
AND (
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '')
OR
(LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '')
)
),
clear_subjects AS (
SELECT
provider_type,
provider_key,
provider_subject
FROM legacy
GROUP BY provider_type, provider_key, provider_subject
HAVING COUNT(DISTINCT user_id) = 1
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_type, legacy.provider_key, legacy.provider_subject
ORDER BY legacy.verified_at DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN clear_subjects
ON clear_subjects.provider_type = legacy.provider_type
AND clear_subjects.provider_key = legacy.provider_key
AND clear_subjects.provider_subject = legacy.provider_subject
)
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
legacy.provider_type,
legacy.provider_key,
legacy.provider_subject,
legacy.verified_at,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'provider_user_id', legacy.provider_user_id,
'provider_union_id', NULLIF(legacy.provider_union_id, ''),
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM canonical_legacy AS legacy
LEFT JOIN auth_identities AS ai
ON ai.provider_type = legacy.provider_type
AND ai.provider_key = legacy.provider_key
AND ai.provider_subject = legacy.provider_subject
WHERE legacy.canonical_row_num = 1
AND ai.id IS NULL
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_external_channel_conflict',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'legacy_user_id', legacy.user_id,
'existing_channel_id', channel.id,
'existing_identity_id', existing_ai.id,
'existing_user_id', existing_ai.user_id,
'provider_type', 'wechat',
'provider_key', 'wechat-main',
'provider_subject', legacy.provider_union_id,
'channel', legacy.channel,
'channel_app_id', legacy.channel_app_id,
'channel_subject', legacy.provider_user_id,
'reason', 'legacy channel subject already belongs to another user',
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM (
SELECT
uei.id,
uei.user_id,
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
BTRIM(COALESCE(public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel', '')) AS channel,
BTRIM(COALESCE(
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel_app_id',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'appid',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'app_id',
''
)) AS channel_app_id
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
) AS legacy
JOIN (
SELECT
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_subject
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
GROUP BY BTRIM(COALESCE(uei.provider_union_id, ''))
HAVING COUNT(DISTINCT uei.user_id) = 1
) AS clear_subjects
ON clear_subjects.provider_subject = legacy.provider_union_id
JOIN auth_identities AS legacy_ai
ON legacy_ai.user_id = legacy.user_id
AND legacy_ai.provider_type = 'wechat'
AND legacy_ai.provider_key = 'wechat-main'
AND legacy_ai.provider_subject = legacy.provider_union_id
JOIN auth_identity_channels AS channel
ON channel.provider_type = 'wechat'
AND channel.provider_key = 'wechat-main'
AND channel.channel = legacy.channel
AND channel.channel_app_id = legacy.channel_app_id
AND channel.channel_subject = legacy.provider_user_id
JOIN auth_identities AS existing_ai
ON existing_ai.id = channel.identity_id
WHERE legacy.channel <> ''
AND legacy.channel_app_id <> ''
AND existing_ai.user_id <> legacy.user_id
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.user_id,
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
BTRIM(COALESCE(public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel', '')) AS channel,
BTRIM(COALESCE(
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel_app_id',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'appid',
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'app_id',
''
)) AS channel_app_id
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
),
clear_subjects AS (
SELECT
provider_union_id AS provider_subject
FROM legacy
GROUP BY provider_union_id
HAVING COUNT(DISTINCT user_id) = 1
)
INSERT INTO auth_identity_channels (
identity_id,
provider_type,
provider_key,
channel,
channel_app_id,
channel_subject,
metadata
)
SELECT
legacy_ai.id,
'wechat',
'wechat-main',
legacy.channel,
legacy.channel_app_id,
legacy.provider_user_id,
legacy.metadata_json || jsonb_build_object(
'openid', legacy.provider_user_id,
'unionid', legacy.provider_union_id,
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM legacy
JOIN clear_subjects
ON clear_subjects.provider_subject = legacy.provider_union_id
JOIN auth_identities AS legacy_ai
ON legacy_ai.user_id = legacy.user_id
AND legacy_ai.provider_type = 'wechat'
AND legacy_ai.provider_key = 'wechat-main'
AND legacy_ai.provider_subject = legacy.provider_union_id
LEFT JOIN auth_identity_channels AS channel
ON channel.provider_type = 'wechat'
AND channel.provider_key = 'wechat-main'
AND channel.channel = legacy.channel
AND channel.channel_app_id = legacy.channel_app_id
AND channel.channel_subject = legacy.provider_user_id
WHERE legacy.channel <> ''
AND legacy.channel_app_id <> ''
AND channel.id IS NULL
ON CONFLICT DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'user_id', legacy.user_id,
'openid', legacy.provider_user_id,
'reason', 'legacy user_external_identities row only has openid and cannot be canonicalized offline',
'migration', '116_auth_identity_legacy_external_safety_reports'
)
FROM (
SELECT
uei.id,
uei.user_id,
BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id,
public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_union_id, '')) = ''
) AS legacy
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'auth_identities_metadata_is_object_check'
) THEN
ALTER TABLE auth_identities
ADD CONSTRAINT auth_identities_metadata_is_object_check
CHECK (jsonb_typeof(metadata) = 'object');
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'auth_identity_channels_metadata_is_object_check'
) THEN
ALTER TABLE auth_identity_channels
ADD CONSTRAINT auth_identity_channels_metadata_is_object_check
CHECK (jsonb_typeof(metadata) = 'object');
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'auth_identity_migration_reports_details_is_object_check'
) THEN
ALTER TABLE auth_identity_migration_reports
ADD CONSTRAINT auth_identity_migration_reports_details_is_object_check
CHECK (jsonb_typeof(details) = 'object');
END IF;
END $$;
DROP FUNCTION IF EXISTS public.__migration_116_is_valid_legacy_metadata_jsonb(TEXT);
DROP FUNCTION IF EXISTS public.__migration_116_safe_legacy_metadata_jsonb(TEXT);
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment