Commit 538ae31a authored by 陈曦's avatar 陈曦
Browse files

merge v0.1.121 and fixed conflict

parents 74828a7c 48912014
Pipeline #82338 passed with stage
in 17 seconds
CREATE TABLE IF NOT EXISTS payment_provider_instances (
id BIGSERIAL PRIMARY KEY,
provider_key VARCHAR(30) NOT NULL,
name VARCHAR(100) NOT NULL DEFAULT '',
config TEXT NOT NULL,
supported_types VARCHAR(200) NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INT NOT NULL DEFAULT 0,
limits TEXT NOT NULL DEFAULT '',
refund_enabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_payment_provider_instances_provider_key ON payment_provider_instances(provider_key);
CREATE INDEX IF NOT EXISTS idx_payment_provider_instances_enabled ON payment_provider_instances(enabled);
-- 097_fix_settings_updated_at_default.sql
--
-- 修复 settings.updated_at 列在历史实例上可能缺失 SQL DEFAULT 的问题。
--
-- 背景:
-- 早期版本曾依赖 ent 自动迁移建表(ent 的 Default(time.Now) 仅是 Go 层默认值,
-- 不会在 SQL 层落地为 DEFAULT),随后引入的 005_schema_parity.sql 使用了
-- CREATE TABLE IF NOT EXISTS,对已存在的 settings 表不会重建,导致这部分实例
-- 的 updated_at 列虽然是 NOT NULL,但缺少 SQL DEFAULT。
--
-- 后续 098_migrate_purchase_subscription_to_custom_menu.sql 是项目中唯一使用
-- 原生 SQL INSERT INTO settings 的迁移(其余 settings 写入都走 ent / Go 层),
-- 因此该 schema 缺陷直到 098 才会触发:
-- "null value in column \"updated_at\" of relation \"settings\" violates not-null constraint"
--
-- 幂等性:
-- - ALTER COLUMN ... SET DEFAULT NOW() 在已经具备相同默认值的实例上是无操作,
-- 不会报错(PostgreSQL 允许重复设置相同的默认值)。
-- - UPDATE 子句的 WHERE updated_at IS NULL 在健康实例上匹配 0 行,不影响数据。
--
-- 这样可以同时兼容:
-- 1. 从未运行过旧版迁移的全新部署(005 已经把列建对,本迁移变成 no-op)。
-- 2. 历史损坏实例(本迁移修复缺失的默认值,使后续 098 能够正常 INSERT)。
ALTER TABLE settings ALTER COLUMN updated_at SET DEFAULT NOW();
UPDATE settings SET updated_at = NOW() WHERE updated_at IS NULL;
-- 096_migrate_purchase_subscription_to_custom_menu.sql
--
-- Migrates the legacy purchase_subscription_url setting into custom_menu_items.
-- After migration, purchase_subscription_enabled is set to "false" and
-- purchase_subscription_url is cleared.
--
-- Idempotent: skips if custom_menu_items already contains
-- "migrated_purchase_subscription".
DO $$
DECLARE
v_enabled text;
v_url text;
v_raw text;
v_items jsonb;
v_new_item jsonb;
BEGIN
-- Read legacy settings
SELECT value INTO v_enabled
FROM settings WHERE key = 'purchase_subscription_enabled';
SELECT value INTO v_url
FROM settings WHERE key = 'purchase_subscription_url';
-- Skip if not enabled or URL is empty
IF COALESCE(v_enabled, '') <> 'true' OR COALESCE(TRIM(v_url), '') = '' THEN
RETURN;
END IF;
-- Read current custom_menu_items
SELECT value INTO v_raw
FROM settings WHERE key = 'custom_menu_items';
IF COALESCE(v_raw, '') = '' OR v_raw = 'null' THEN
v_items := '[]'::jsonb;
ELSE
v_items := v_raw::jsonb;
END IF;
-- Skip if already migrated (item with id "migrated_purchase_subscription" exists)
IF EXISTS (
SELECT 1 FROM jsonb_array_elements(v_items) elem
WHERE elem ->> 'id' = 'migrated_purchase_subscription'
) THEN
RETURN;
END IF;
-- Build the new menu item
v_new_item := jsonb_build_object(
'id', 'migrated_purchase_subscription',
'label', 'Purchase',
'icon_svg', '',
'url', TRIM(v_url),
'visibility', 'user',
'sort_order', 100
);
-- Append to array
v_items := v_items || jsonb_build_array(v_new_item);
-- Upsert custom_menu_items
INSERT INTO settings (key, value)
VALUES ('custom_menu_items', v_items::text)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
-- Clear legacy settings
UPDATE settings SET value = 'false' WHERE key = 'purchase_subscription_enabled';
UPDATE settings SET value = '' WHERE key = 'purchase_subscription_url';
RAISE NOTICE '[migration-096] Migrated purchase_subscription_url (%) to custom_menu_items', v_url;
END $$;
-- 097_fix_migrated_purchase_menu_label_icon.sql
--
-- Fixes the custom menu item created by migration 096: updates the label
-- from hardcoded English "Purchase" to "充值/订阅", and sets the icon_svg
-- to a credit-card SVG matching the sidebar CreditCardIcon.
--
-- Idempotent: only modifies items where id = 'migrated_purchase_subscription'.
DO $$
DECLARE
v_raw text;
v_items jsonb;
v_idx int;
v_icon text;
v_elem jsonb;
v_i int := 0;
BEGIN
SELECT value INTO v_raw
FROM settings WHERE key = 'custom_menu_items';
IF COALESCE(v_raw, '') = '' OR v_raw = 'null' THEN
RETURN;
END IF;
v_items := v_raw::jsonb;
-- Find the index of the migrated item by iterating the array
v_idx := NULL;
FOR v_elem IN SELECT jsonb_array_elements(v_items) LOOP
IF v_elem ->> 'id' = 'migrated_purchase_subscription' THEN
v_idx := v_i;
EXIT;
END IF;
v_i := v_i + 1;
END LOOP;
IF v_idx IS NULL THEN
RETURN; -- item not found, nothing to fix
END IF;
-- Credit card SVG (Heroicons outline, matches CreditCardIcon in AppSidebar)
v_icon := '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"/></svg>';
-- Update label and icon_svg
v_items := jsonb_set(v_items, ARRAY[v_idx::text, 'label'], '"充值/订阅"'::jsonb);
v_items := jsonb_set(v_items, ARRAY[v_idx::text, 'icon_svg'], to_jsonb(v_icon));
UPDATE settings SET value = v_items::text WHERE key = 'custom_menu_items';
RAISE NOTICE '[migration-097] Fixed migrated_purchase_subscription: label=充值/订阅, icon=CreditCard SVG';
END $$;
-- 098_remove_easypay_from_enabled_payment_types.sql
--
-- Removes "easypay" from ENABLED_PAYMENT_TYPES setting.
-- "easypay" is a provider key, not a payment type. Valid payment types
-- are: alipay, wxpay, alipay_direct, wxpay_direct, stripe.
--
-- Idempotent: safe to run multiple times.
UPDATE settings
SET value = array_to_string(
array_remove(
string_to_array(value, ','),
'easypay'
), ','
)
WHERE key = 'ENABLED_PAYMENT_TYPES'
AND value LIKE '%easypay%';
-- 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 payment_mode field to payment_provider_instances
-- Values: 'redirect' (hosted page redirect), 'api' (API call for QR/payurl), '' (default/N/A)
ALTER TABLE payment_provider_instances ADD COLUMN IF NOT EXISTS payment_mode VARCHAR(20) NOT NULL DEFAULT '';
-- Migrate existing data: easypay instances with 'easypay' in supported_types → redirect mode
-- Remove 'easypay' from supported_types and set payment_mode = 'redirect'
UPDATE payment_provider_instances
SET payment_mode = 'redirect',
supported_types = TRIM(BOTH ',' FROM REPLACE(REPLACE(REPLACE(
supported_types, 'easypay,', ''), ',easypay', ''), 'easypay', ''))
WHERE provider_key = 'easypay' AND supported_types LIKE '%easypay%';
-- EasyPay instances without 'easypay' in supported_types → api mode
UPDATE payment_provider_instances
SET payment_mode = 'api'
WHERE provider_key = 'easypay' AND payment_mode = '';
-- 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;
-- 100_add_out_trade_no_to_payment_orders.sql
-- Adds out_trade_no column for external order ID used with payment providers.
-- Allows webhook handlers to look up orders by external ID instead of embedding DB ID.
ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS out_trade_no VARCHAR(64) NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS paymentorder_out_trade_no ON payment_orders (out_trade_no);
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);
-- Add capture_requests flag to api_keys
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS capture_requests boolean NOT NULL DEFAULT false;
-- Create request_capture_logs table (monthly range-partitioned by created_at)
-- PRIMARY KEY must include the partition key, so we use (id, created_at).
CREATE TABLE IF NOT EXISTS request_capture_logs (
id bigserial NOT NULL,
api_key_id bigint NOT NULL,
user_id bigint NOT NULL,
request_id varchar(64),
path varchar(100),
method varchar(10),
ip_address varchar(45),
request_body text,
response_body text,
nfs_file_path varchar(500),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE INDEX IF NOT EXISTS idx_rcl_api_key_created ON request_capture_logs (api_key_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_rcl_user_id ON request_capture_logs (user_id);
-- Pre-create partitions for previous, current, and next month
DO $$
DECLARE
month_start DATE;
prev_month DATE;
next_month DATE;
BEGIN
month_start := date_trunc('month', now() AT TIME ZONE 'UTC')::date;
prev_month := (month_start - INTERVAL '1 month')::date;
next_month := (month_start + INTERVAL '1 month')::date;
EXECUTE format(
'CREATE TABLE IF NOT EXISTS request_capture_logs_%s PARTITION OF request_capture_logs FOR VALUES FROM (%L) TO (%L)',
to_char(prev_month, 'YYYYMM'), prev_month, month_start
);
EXECUTE format(
'CREATE TABLE IF NOT EXISTS request_capture_logs_%s PARTITION OF request_capture_logs FOR VALUES FROM (%L) TO (%L)',
to_char(month_start, 'YYYYMM'), month_start, next_month
);
EXECUTE format(
'CREATE TABLE IF NOT EXISTS request_capture_logs_%s PARTITION OF request_capture_logs FOR VALUES FROM (%L) TO (%L)',
to_char(next_month, 'YYYYMM'), next_month, (next_month + INTERVAL '1 month')::date
);
END $$;
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;
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