Add legacy identity safety remediation migration

This commit is contained in:
IanShaw027
2026-04-21 00:59:20 +08:00
parent c297d0112e
commit 7a9488ff37
2 changed files with 582 additions and 0 deletions

View File

@@ -200,6 +200,252 @@ FROM auth_identity_migration_reports
var afterCount int var afterCount int
require.NoError(t, tx.QueryRowContext(ctx, ` require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*) SELECT COUNT(*)
FROM auth_identity_migration_reports
`).Scan(&afterCount))
require.Equal(t, beforeCount, afterCount)
}
func TestAuthIdentityLegacyExternalSafetyMigration_ReportsConflictsAndDowngradesInvalidJSON(t *testing.T) {
tx := testTx(t)
ctx := context.Background()
migrationPath := filepath.Join("..", "..", "migrations", "116_auth_identity_legacy_external_safety_reports.sql")
migrationSQL, err := os.ReadFile(migrationPath)
require.NoError(t, err)
_, err = tx.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS user_external_identities (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
provider TEXT NOT NULL,
provider_user_id TEXT NOT NULL,
provider_union_id TEXT NULL,
provider_username TEXT NOT NULL DEFAULT '',
display_name TEXT NOT NULL DEFAULT '',
profile_url TEXT NOT NULL DEFAULT '',
avatar_url TEXT NOT NULL DEFAULT '',
metadata TEXT NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
TRUNCATE TABLE
auth_identity_channels,
auth_identities,
auth_identity_migration_reports,
user_external_identities,
users
RESTART IDENTITY;
`)
require.NoError(t, err)
userIDs := make([]int64, 0, 8)
for _, email := range []string{
"linuxdo-conflict-legacy@example.com",
"linuxdo-conflict-owner@example.com",
"wechat-conflict-legacy@example.com",
"wechat-conflict-owner@example.com",
"wechat-channel-legacy@example.com",
"wechat-channel-owner@example.com",
"linuxdo-invalid-json@example.com",
"wechat-openid-invalid-json@example.com",
} {
var userID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO users (email, password_hash, role, status, balance, concurrency)
VALUES ($1, 'hash', 'user', 'active', 0, 1)
RETURNING id`, email).Scan(&userID))
userIDs = append(userIDs, userID)
}
linuxdoConflictLegacyUserID := userIDs[0]
linuxdoConflictOwnerUserID := userIDs[1]
wechatConflictLegacyUserID := userIDs[2]
wechatConflictOwnerUserID := userIDs[3]
wechatChannelLegacyUserID := userIDs[4]
wechatChannelOwnerUserID := userIDs[5]
linuxdoInvalidJSONUserID := userIDs[6]
wechatInvalidOpenIDUserID := userIDs[7]
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO auth_identities (user_id, provider_type, provider_key, provider_subject, metadata)
VALUES ($1, 'linuxdo', 'linuxdo', 'linuxdo-conflict', '{}'::jsonb)
RETURNING id`, linuxdoConflictOwnerUserID).Scan(new(int64)))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO auth_identities (user_id, provider_type, provider_key, provider_subject, metadata)
VALUES ($1, 'wechat', 'wechat-main', 'union-conflict', '{}'::jsonb)
RETURNING id`, wechatConflictOwnerUserID).Scan(new(int64)))
var wechatChannelOwnerIdentityID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO auth_identities (user_id, provider_type, provider_key, provider_subject, metadata)
VALUES ($1, 'wechat', 'wechat-main', 'union-channel-owner', '{}'::jsonb)
RETURNING id`, wechatChannelOwnerUserID).Scan(&wechatChannelOwnerIdentityID))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO auth_identity_channels (
identity_id,
provider_type,
provider_key,
channel,
channel_app_id,
channel_subject,
metadata
)
VALUES ($1, 'wechat', 'wechat-main', 'oa', 'wx-app-conflict', 'openid-channel-conflict', '{}'::jsonb)
RETURNING id`, wechatChannelOwnerIdentityID).Scan(new(int64)))
var linuxdoConflictLegacyID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO user_external_identities (
user_id,
provider,
provider_user_id,
provider_union_id,
provider_username,
display_name,
metadata
) VALUES ($1, 'linuxdo', 'linuxdo-conflict', NULL, 'legacy-linuxdo', 'Legacy LinuxDo Conflict', '{"source":"legacy"}')
RETURNING id
`, linuxdoConflictLegacyUserID).Scan(&linuxdoConflictLegacyID))
var wechatConflictLegacyID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO user_external_identities (
user_id,
provider,
provider_user_id,
provider_union_id,
provider_username,
display_name,
metadata
) VALUES ($1, 'wechat', 'openid-union-conflict', 'union-conflict', 'legacy-wechat', 'Legacy WeChat Conflict', '{"channel":"oa","appid":"wx-app-conflict-canon"}')
RETURNING id
`, wechatConflictLegacyUserID).Scan(&wechatConflictLegacyID))
var wechatChannelConflictLegacyID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO user_external_identities (
user_id,
provider,
provider_user_id,
provider_union_id,
provider_username,
display_name,
metadata
) VALUES ($1, 'wechat', 'openid-channel-conflict', 'union-channel-legacy', 'legacy-wechat-channel', 'Legacy WeChat Channel Conflict', '{"channel":"oa","appid":"wx-app-conflict"}')
RETURNING id
`, wechatChannelLegacyUserID).Scan(&wechatChannelConflictLegacyID))
var linuxdoInvalidJSONLegacyID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO user_external_identities (
user_id,
provider,
provider_user_id,
provider_union_id,
provider_username,
display_name,
metadata
) VALUES ($1, 'linuxdo', 'linuxdo-invalid-json', NULL, 'legacy-linuxdo-invalid', 'Legacy LinuxDo Invalid JSON', '{invalid')
RETURNING id
`, linuxdoInvalidJSONUserID).Scan(&linuxdoInvalidJSONLegacyID))
var wechatInvalidOpenIDLegacyID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO user_external_identities (
user_id,
provider,
provider_user_id,
provider_union_id,
provider_username,
display_name,
metadata
) VALUES ($1, 'wechat', 'openid-invalid-json-only', NULL, 'legacy-wechat-invalid', 'Legacy WeChat Invalid JSON', '{still-invalid')
RETURNING id
`, wechatInvalidOpenIDUserID).Scan(&wechatInvalidOpenIDLegacyID))
_, err = tx.ExecContext(ctx, string(migrationSQL))
require.NoError(t, err)
var linuxdoConflictReportCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports
WHERE report_type = 'legacy_external_identity_conflict'
AND report_key = $1
`, "legacy_external_identity:"+strconv.FormatInt(linuxdoConflictLegacyID, 10)).Scan(&linuxdoConflictReportCount))
require.Equal(t, 1, linuxdoConflictReportCount)
var wechatConflictReportCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports
WHERE report_type = 'legacy_external_identity_conflict'
AND report_key = $1
`, "legacy_external_identity:"+strconv.FormatInt(wechatConflictLegacyID, 10)).Scan(&wechatConflictReportCount))
require.Equal(t, 1, wechatConflictReportCount)
var channelConflictReportCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports
WHERE report_type = 'legacy_external_channel_conflict'
AND report_key = $1
`, "legacy_external_identity:"+strconv.FormatInt(wechatChannelConflictLegacyID, 10)).Scan(&channelConflictReportCount))
require.Equal(t, 1, channelConflictReportCount)
var invalidJSONReportCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports
WHERE report_type = 'legacy_external_identity_invalid_metadata_json'
AND report_key IN ($1, $2)
`, "legacy_external_identity:"+strconv.FormatInt(linuxdoInvalidJSONLegacyID, 10), "legacy_external_identity:"+strconv.FormatInt(wechatInvalidOpenIDLegacyID, 10)).Scan(&invalidJSONReportCount))
require.Equal(t, 2, invalidJSONReportCount)
var linuxdoInvalidIdentityCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identities
WHERE user_id = $1
AND provider_type = 'linuxdo'
AND provider_key = 'linuxdo'
AND provider_subject = 'linuxdo-invalid-json'
`, linuxdoInvalidJSONUserID).Scan(&linuxdoInvalidIdentityCount))
require.Equal(t, 1, linuxdoInvalidIdentityCount)
var wechatOpenIDOnlyReportCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports
WHERE report_type = 'wechat_openid_only_requires_remediation'
AND report_key = $1
`, "legacy_external_identity:"+strconv.FormatInt(wechatInvalidOpenIDLegacyID, 10)).Scan(&wechatOpenIDOnlyReportCount))
require.Equal(t, 1, wechatOpenIDOnlyReportCount)
}
func TestAuthIdentityLegacyExternalSafetyMigration_IsSafeWhenLegacyTableMissing(t *testing.T) {
tx := testTx(t)
ctx := context.Background()
migrationPath := filepath.Join("..", "..", "migrations", "116_auth_identity_legacy_external_safety_reports.sql")
migrationSQL, err := os.ReadFile(migrationPath)
require.NoError(t, err)
var beforeCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports
`).Scan(&beforeCount))
_, err = tx.ExecContext(ctx, string(migrationSQL))
require.NoError(t, err)
var afterCount int
require.NoError(t, tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM auth_identity_migration_reports FROM auth_identity_migration_reports
`).Scan(&afterCount)) `).Scan(&afterCount))
require.Equal(t, beforeCount, afterCount) require.Equal(t, beforeCount, afterCount)

View File

@@ -0,0 +1,336 @@
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,
'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 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$
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 (
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, '')) <> '')
)
) 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 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 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$
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 (
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, '')) <> ''
) AS legacy
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 $$;
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);