refactor(migrations): squash schema into initial baseline

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-08-14 00:13:09 +08:00
co-authored by Codex
parent 82ba4fcbba
commit b04616f59f
9 changed files with 158 additions and 427 deletions
+1 -8
View File
@@ -1,8 +1 @@
5f79e74c9a7a7366c39cd1afe43175eff6c44afe9217c0f26f33f173bd40c781 0001_initial.sql
f4168f7e23d42cf9d37096339e931b0e0c823f7a8dbfc6580a04f096c4c3d8e3 0002_provider_accounts_nullable_upstream_user_id.sql
4163f61183d9e90f1368c08f614040d6a1a0048de3886a77dd708ce21430b2d0 0003_backfill_openai_access_token_expiry.sql
b41bb3be5a84b95852158d82267456f8415811a1137025581eefd1c96c0882d1 0004_normalize_account_state_facts.sql
00ef40c9cd404a09afe916262e894eec361039a60a93d6c46e6c96104b1346b9 0005_account_groups.sql
73846775d2c533fc17dc12164efd9ce6beaa99e2a67a064589ebadd50cd9b349 0006_account_group_color.sql
4e3a367b1bf75de82e7706127080627e0bb8f190b4d3bbc69be954de38eb1ead 0007_account_group_color_alpha.sql
3f07a905e7413f0be519825a10c73effb392df4e6069a5df7901bcd0ec2ff1af 0008_provider_account_scheduling.sql
bebf8c786b08ca3b2852d542d8d88049034a1fb566cc6efcc958dd3fc97241af 0001_initial.sql
+157 -22
View File
@@ -51,7 +51,6 @@ create table client_api_keys (
id text primary key,
name text not null,
label text,
provider_kind text not null,
key text not null unique,
enabled boolean not null default true,
max_concurrency bigint not null default 0,
@@ -62,9 +61,6 @@ create table client_api_keys (
constraint client_api_keys_key_ck check (
key ~ '^sk_[A-Za-z0-9_-]{43}$'
),
constraint client_api_keys_provider_kind_ck check (
provider_kind ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'
),
constraint client_api_keys_limits_ck check (
max_concurrency >= 0
and requests_per_minute >= 0
@@ -120,7 +116,7 @@ create table provider_accounts (
provider_kind text not null,
name text not null,
email text,
upstream_user_id text not null,
upstream_user_id text,
upstream_account_id text,
plan_type text,
authentication_kind text not null,
@@ -130,14 +126,20 @@ create table provider_accounts (
access_token_expires_at timestamptz,
next_refresh_at timestamptz,
enabled boolean not null default true,
availability text not null default 'unknown',
credential_state text not null default 'unknown',
provider_quota_json jsonb,
quota_limit_reached boolean not null default false,
last_error_message text,
availability_observed_at timestamptz not null,
credential_observed_at timestamptz not null,
quota_observed_at timestamptz,
created_at timestamptz not null,
updated_at timestamptz not null,
quota_access_state text not null default 'unknown',
quota_evidence text,
quota_access_observed_at timestamptz,
quota_reset_at timestamptz,
last_error_reason text,
concurrency_limit bigint,
weight smallint not null default 1,
constraint provider_accounts_revision_ck check (credential_revision > 0),
constraint provider_accounts_authentication_kind_ck check (
authentication_kind ~ '^[a-z][a-z0-9_]{0,63}$'
@@ -153,25 +155,61 @@ create table provider_accounts (
and octet_length(provider_quota_json::text) <= 131072
)
),
constraint provider_accounts_quota_observation_ck check (
(provider_quota_json is null) = (quota_observed_at is null)
),
constraint provider_accounts_refresh_ck check (
has_refresh_token or next_refresh_at is null
),
constraint provider_accounts_availability_ck check (
availability in (
'unknown',
'ready',
'quota_exhausted',
'expired',
'banned',
'invalid'
constraint provider_accounts_credential_state_ck check (
credential_state in ('unknown', 'ready', 'expired', 'banned', 'invalid')
),
constraint provider_accounts_quota_access_state_ck check (
quota_access_state in ('unknown', 'allowed', 'exhausted')
),
constraint provider_accounts_quota_evidence_ck check (
quota_evidence is null
or quota_evidence in (
'provider_denied',
'account_limit_reached',
'usage_limit_reached',
'payment_required'
)
),
constraint provider_accounts_quota_fact_ck check (
(quota_access_state = 'unknown' and quota_evidence is null and quota_reset_at is null)
or (
quota_access_state = 'allowed'
and quota_evidence is null
and quota_access_observed_at is not null
and quota_reset_at is null
)
or (
quota_access_state = 'exhausted'
and quota_evidence is not null
and quota_access_observed_at is not null
)
),
constraint provider_accounts_quota_observation_ck check (
(provider_quota_json is null) = (quota_observed_at is null)
),
constraint provider_accounts_error_reason_ck check (
last_error_reason is null
or last_error_reason in (
'account_unverified',
'access_token_expired',
'credential_expired',
'credential_invalid',
'account_banned'
)
),
constraint provider_accounts_concurrency_limit_ck check (
concurrency_limit is null or concurrency_limit between 1 and 4294967295
),
constraint provider_accounts_weight_ck check (
weight between 1 and 100
),
constraint provider_accounts_time_ck check (
created_at <= updated_at
and availability_observed_at <= updated_at
and credential_observed_at <= updated_at
and (quota_access_observed_at is null or quota_access_observed_at <= updated_at)
and (quota_observed_at is null or quota_observed_at <= updated_at)
)
);
@@ -184,8 +222,10 @@ create unique index provider_accounts_upstream_identity_uq
);
create index provider_accounts_runtime_idx
on provider_accounts (provider_kind, enabled, id);
create index provider_accounts_availability_idx
on provider_accounts (availability, id);
create index provider_accounts_credential_state_idx
on provider_accounts (credential_state, id);
create index provider_accounts_quota_access_idx
on provider_accounts (quota_access_state, quota_reset_at, id);
create index provider_accounts_access_expiry_idx
on provider_accounts (access_token_expires_at, id);
create index provider_accounts_refresh_due_idx
@@ -195,6 +235,76 @@ create index provider_accounts_email_idx
on provider_accounts (provider_kind, lower(email))
where email is not null;
-- 与 Provider 无关的账号组,以及账号和客户端 API 密钥的组成员关系。
create table account_groups (
id text primary key,
name text not null,
description text,
enabled boolean not null default true,
created_at timestamptz not null,
updated_at timestamptz not null,
color text not null,
constraint account_groups_id_ck check (
id ~ '^grp_[0-9a-f]{32}$'
),
constraint account_groups_name_ck check (
char_length(btrim(name)) between 1 and 100
and name = btrim(name)
and name !~ '[[:cntrl:]]'
),
constraint account_groups_description_ck check (
description is null or (
octet_length(description) <= 4096
and description !~ '[[:cntrl:]]'
)
),
constraint account_groups_time_ck check (
created_at <= updated_at
),
constraint account_groups_color_ck check (color ~ '^#[0-9A-F]{8}$')
);
create unique index account_groups_name_uq
on account_groups (lower(name));
create index account_groups_list_idx
on account_groups (enabled, created_at desc, id desc);
create table account_group_accounts (
account_group_id text not null,
provider_account_id text not null,
created_at timestamptz not null,
primary key (account_group_id, provider_account_id),
constraint account_group_accounts_group_fk foreign key (account_group_id)
references account_groups (id)
on update restrict
on delete cascade,
constraint account_group_accounts_account_fk foreign key (provider_account_id)
references provider_accounts (id)
on update restrict
on delete cascade
);
create index account_group_accounts_account_idx
on account_group_accounts (provider_account_id, account_group_id);
create table client_api_key_groups (
client_api_key_id text not null,
account_group_id text not null,
created_at timestamptz not null,
primary key (client_api_key_id, account_group_id),
constraint client_api_key_groups_key_fk foreign key (client_api_key_id)
references client_api_keys (id)
on update restrict
on delete cascade,
constraint client_api_key_groups_group_fk foreign key (account_group_id)
references account_groups (id)
on update restrict
on delete restrict
);
create index client_api_key_groups_group_idx
on client_api_key_groups (account_group_id, client_api_key_id);
-- 模型请求、上游尝试、用量、计费与结果。
create table model_requests (
id text primary key,
@@ -263,6 +373,9 @@ create table model_requests (
started_at timestamptz not null,
deadline_at timestamptz not null,
completed_at timestamptz,
routing_scope text not null,
routing_group_refs text[] not null default '{}',
routing_group_names_snapshot jsonb not null default '[]'::jsonb,
constraint model_requests_client_ref_ck check (
client_api_key_id is null or client_api_key_id = client_api_key_ref
),
@@ -332,6 +445,26 @@ create table model_requests (
and service_tier !~ '[[:cntrl:]]'
)
),
constraint model_requests_routing_scope_ck check (
routing_scope in ('legacy_provider', 'all', 'groups')
),
constraint model_requests_routing_group_names_ck check (
jsonb_typeof(routing_group_names_snapshot) = 'array'
and (
(
routing_scope in ('legacy_provider', 'all')
and cardinality(routing_group_refs) = 0
and jsonb_array_length(routing_group_names_snapshot) = 0
)
or (
routing_scope = 'groups'
and cardinality(routing_group_refs) > 0
and array_position(routing_group_refs, null) is null
and jsonb_array_length(routing_group_names_snapshot)
= cardinality(routing_group_refs)
)
)
),
constraint model_requests_cost_ck check (
cost_source in ('provider_reported', 'calculated', 'unavailable')
and (
@@ -425,6 +558,8 @@ create index model_requests_running_deadline_idx
create index model_requests_retention_idx
on model_requests (completed_at)
where outcome <> 'running';
create index model_requests_routing_group_refs_idx
on model_requests using gin (routing_group_refs);
-- 运行期警告与错误事件。
create table ops_events (
@@ -1,3 +0,0 @@
-- Initial OAuth exchange requires an id_token, but its ChatGPT user claim is optional.
alter table provider_accounts
alter column upstream_user_id drop not null;
@@ -1,70 +0,0 @@
-- Direct OpenAI OAuth imports between 3.1.15 and 3.1.16 persisted JWT access
-- tokens but accidentally omitted their derived expiry. Recover only future,
-- standard JWT expiries; malformed/opaque tokens remain unchanged so this
-- one-shot repair never fabricates a credential lifetime or refresh schedule.
create function _cpr_openai_access_token_expiry(access_token text)
returns timestamptz
language plpgsql
immutable
strict
as $$
declare
payload text;
claims jsonb;
epoch_seconds bigint;
begin
if access_token !~ '^[^.]+[.][^.]+[.][^.]+$' then
return null;
end if;
payload := split_part(access_token, '.', 2);
payload := rpad(
translate(payload, '-_', '+/'),
((length(payload) + 3) / 4) * 4,
'='
);
claims := convert_from(decode(payload, 'base64'), 'UTF8')::jsonb;
if jsonb_typeof(claims -> 'exp') <> 'number' then
return null;
end if;
epoch_seconds := (claims ->> 'exp')::bigint;
return to_timestamp(epoch_seconds::double precision);
exception
when others then
return null;
end;
$$;
with candidates as (
select
account.id,
clock_timestamp() as observed_at,
_cpr_openai_access_token_expiry(
account.provider_credentials_json ->> 'access_token'
) as access_token_expires_at
from provider_accounts as account
where account.provider_kind = 'openai'
and account.authentication_kind = 'oauth'
and account.has_refresh_token
and account.access_token_expires_at is null
and account.next_refresh_at is null
and account.availability not in ('expired', 'invalid', 'banned')
and jsonb_typeof(account.provider_credentials_json -> 'access_token') = 'string'
)
update provider_accounts as account
set
access_token_expires_at = candidates.access_token_expires_at,
updated_at = greatest(account.updated_at, candidates.observed_at)
from candidates
where account.id = candidates.id
and candidates.access_token_expires_at > candidates.observed_at
and account.provider_kind = 'openai'
and account.authentication_kind = 'oauth'
and account.has_refresh_token
and account.access_token_expires_at is null
and account.next_refresh_at is null
and account.availability not in ('expired', 'invalid', 'banned');
drop function _cpr_openai_access_token_expiry(text);
@@ -1,165 +0,0 @@
-- Separate credential identity facts from quota access facts. The public account
-- status remains derived at read time because Redis cooldowns and reset times are
-- time-dependent.
alter table provider_accounts
rename column availability to credential_state;
alter table provider_accounts
rename column availability_observed_at to credential_observed_at;
alter table provider_accounts
drop constraint provider_accounts_availability_ck;
alter table provider_accounts
add constraint provider_accounts_credential_state_ck check (
credential_state in ('unknown', 'ready', 'expired', 'banned', 'invalid')
) not valid;
alter table provider_accounts
add column quota_access_state text not null default 'unknown',
add column quota_evidence text,
add column quota_access_observed_at timestamptz,
add column quota_reset_at timestamptz,
add column last_error_reason text;
alter table provider_accounts
drop constraint provider_accounts_quota_observation_ck;
-- Old quota_exhausted credential states are quota facts. Re-derive OpenAI quota
-- access from the raw authoritative response and never trust the old percentage-
-- derived quota_limit_reached boolean.
update provider_accounts
set
quota_access_state = case
when provider_kind = 'openai'
and provider_quota_json #>> '{rate_limit,allowed}' = 'true'
then 'allowed'
when provider_kind = 'openai'
and (
provider_quota_json #>> '{rate_limit,allowed}' = 'false'
or (
coalesce(provider_quota_json #>> '{rate_limit,allowed}', '') <> 'true'
and (
provider_quota_json #>> '{rate_limit,limit_reached}' = 'true'
or provider_quota_json #>> '{rate_limit,primary_window,limit_reached}' = 'true'
)
)
)
then 'exhausted'
when provider_kind = 'xai' and credential_state = 'quota_exhausted'
then 'exhausted'
when provider_quota_json is not null then 'unknown'
else 'unknown'
end,
quota_evidence = case
when provider_kind = 'openai'
and provider_quota_json #>> '{rate_limit,allowed}' = 'false'
then 'provider_denied'
when provider_kind = 'openai'
and coalesce(provider_quota_json #>> '{rate_limit,allowed}', '') <> 'true'
and (
provider_quota_json #>> '{rate_limit,limit_reached}' = 'true'
or provider_quota_json #>> '{rate_limit,primary_window,limit_reached}' = 'true'
)
then 'account_limit_reached'
when provider_kind = 'xai' and credential_state = 'quota_exhausted'
then 'usage_limit_reached'
else null
end,
quota_access_observed_at = case
when provider_kind = 'openai' and provider_quota_json is not null
then quota_observed_at
when provider_kind = 'xai' and credential_state = 'quota_exhausted'
then credential_observed_at
else null
end,
quota_reset_at = case
when provider_kind = 'openai'
and jsonb_typeof(provider_quota_json #> '{rate_limit,primary_window,reset_at}') = 'number'
then to_timestamp((provider_quota_json #>> '{rate_limit,primary_window,reset_at}')::double precision)
else null
end,
credential_state = case
when credential_state = 'quota_exhausted' and upstream_user_id is not null then 'ready'
when credential_state = 'quota_exhausted' then 'unknown'
else credential_state
end,
last_error_reason = case credential_state
when 'unknown' then 'account_unverified'
when 'expired' then 'credential_expired'
when 'invalid' then 'credential_invalid'
when 'banned' then 'account_banned'
else null
end,
last_error_message = case
when credential_state in ('unknown', 'expired', 'invalid', 'banned') then last_error_message
else null
end;
alter table provider_accounts
validate constraint provider_accounts_credential_state_ck;
alter table provider_accounts
add constraint provider_accounts_quota_access_state_ck check (
quota_access_state in ('unknown', 'allowed', 'exhausted')
),
add constraint provider_accounts_quota_evidence_ck check (
quota_evidence is null
or quota_evidence in (
'provider_denied',
'account_limit_reached',
'usage_limit_reached',
'payment_required'
)
),
add constraint provider_accounts_quota_fact_ck check (
(quota_access_state = 'unknown' and quota_evidence is null and quota_reset_at is null)
or (
quota_access_state = 'allowed'
and quota_evidence is null
and quota_access_observed_at is not null
and quota_reset_at is null
)
or (
quota_access_state = 'exhausted'
and quota_evidence is not null
and quota_access_observed_at is not null
)
),
add constraint provider_accounts_quota_observation_ck check (
(provider_quota_json is null) = (quota_observed_at is null)
),
add constraint provider_accounts_error_reason_ck check (
last_error_reason is null
or last_error_reason in (
'account_unverified',
'access_token_expired',
'credential_expired',
'credential_invalid',
'account_banned'
)
);
drop index provider_accounts_availability_idx;
create index provider_accounts_credential_state_idx
on provider_accounts (credential_state, id);
create index provider_accounts_quota_access_idx
on provider_accounts (quota_access_state, quota_reset_at, id);
alter table provider_accounts
drop column quota_limit_reached;
alter table provider_accounts
rename constraint provider_accounts_time_ck to provider_accounts_time_ck_old;
alter table provider_accounts
drop constraint provider_accounts_time_ck_old;
alter table provider_accounts
add constraint provider_accounts_time_ck check (
created_at <= updated_at
and credential_observed_at <= updated_at
and (quota_access_observed_at is null or quota_access_observed_at <= updated_at)
and (quota_observed_at is null or quota_observed_at <= updated_at)
);
-135
View File
@@ -1,135 +0,0 @@
-- Account groups are provider-neutral account sets. Existing provider-scoped
-- keys are placed in ordinary migration groups before their provider column is
-- removed, preserving their pre-upgrade account scope.
create table account_groups (
id text primary key,
name text not null,
description text,
enabled boolean not null default true,
created_at timestamptz not null,
updated_at timestamptz not null,
constraint account_groups_id_ck check (
id ~ '^grp_[0-9a-f]{32}$'
),
constraint account_groups_name_ck check (
char_length(btrim(name)) between 1 and 100
and name = btrim(name)
and name !~ '[[:cntrl:]]'
),
constraint account_groups_description_ck check (
description is null or (
octet_length(description) <= 4096
and description !~ '[[:cntrl:]]'
)
),
constraint account_groups_time_ck check (
created_at <= updated_at
)
);
create unique index account_groups_name_uq
on account_groups (lower(name));
create index account_groups_list_idx
on account_groups (enabled, created_at desc, id desc);
create table account_group_accounts (
account_group_id text not null,
provider_account_id text not null,
created_at timestamptz not null,
primary key (account_group_id, provider_account_id),
constraint account_group_accounts_group_fk foreign key (account_group_id)
references account_groups (id)
on update restrict
on delete cascade,
constraint account_group_accounts_account_fk foreign key (provider_account_id)
references provider_accounts (id)
on update restrict
on delete cascade
);
create index account_group_accounts_account_idx
on account_group_accounts (provider_account_id, account_group_id);
create table client_api_key_groups (
client_api_key_id text not null,
account_group_id text not null,
created_at timestamptz not null,
primary key (client_api_key_id, account_group_id),
constraint client_api_key_groups_key_fk foreign key (client_api_key_id)
references client_api_keys (id)
on update restrict
on delete cascade,
constraint client_api_key_groups_group_fk foreign key (account_group_id)
references account_groups (id)
on update restrict
on delete restrict
);
create index client_api_key_groups_group_idx
on client_api_key_groups (account_group_id, client_api_key_id);
with provider_kinds as (
select provider_kind from provider_accounts
union
select provider_kind from client_api_keys
)
insert into account_groups (id, name, description, enabled, created_at, updated_at)
select
'grp_' || md5('codex-proxy-rs:migration-group:' || provider_kind),
'迁移池-' || provider_kind,
'升级时按原 Provider 权限自动生成',
true,
now(),
now()
from provider_kinds;
insert into account_group_accounts (account_group_id, provider_account_id, created_at)
select
'grp_' || md5('codex-proxy-rs:migration-group:' || provider_kind),
id,
clock_timestamp()
from provider_accounts;
insert into client_api_key_groups (client_api_key_id, account_group_id, created_at)
select
id,
'grp_' || md5('codex-proxy-rs:migration-group:' || provider_kind),
clock_timestamp()
from client_api_keys;
alter table model_requests
add column routing_scope text not null default 'legacy_provider',
add column routing_group_refs text[] not null default '{}',
add column routing_group_names_snapshot jsonb not null default '[]'::jsonb,
add constraint model_requests_routing_scope_ck check (
routing_scope in ('legacy_provider', 'all', 'groups')
),
add constraint model_requests_routing_group_names_ck check (
jsonb_typeof(routing_group_names_snapshot) = 'array'
and (
(
routing_scope in ('legacy_provider', 'all')
and cardinality(routing_group_refs) = 0
and jsonb_array_length(routing_group_names_snapshot) = 0
)
or (
routing_scope = 'groups'
and cardinality(routing_group_refs) > 0
and array_position(routing_group_refs, null) is null
and jsonb_array_length(routing_group_names_snapshot)
= cardinality(routing_group_refs)
)
)
);
create index model_requests_routing_group_refs_idx
on model_requests using gin (routing_group_refs);
alter table model_requests
alter column routing_scope drop default;
alter table client_api_keys
drop constraint client_api_keys_provider_kind_ck,
drop column provider_kind;
@@ -1,9 +0,0 @@
alter table account_groups
add column color text;
update account_groups
set color = '#2563EB';
alter table account_groups
alter column color set not null,
add constraint account_groups_color_ck check (color ~ '^#[0-9A-F]{6}$');
@@ -1,8 +0,0 @@
alter table account_groups
drop constraint account_groups_color_ck;
update account_groups
set color = color || 'FF';
alter table account_groups
add constraint account_groups_color_ck check (color ~ '^#[0-9A-F]{8}$');
@@ -1,7 +0,0 @@
alter table provider_accounts
add column concurrency_limit bigint,
add column weight smallint not null default 1,
add constraint provider_accounts_concurrency_limit_ck
check (concurrency_limit is null or concurrency_limit between 1 and 4294967295),
add constraint provider_accounts_weight_ck
check (weight between 1 and 100);