mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
refactor(platform): 平台白名单由数据库约束改为应用层校验
新增迁移 242 删除 user_platform_quotas_platform_check 与 composite_model_routes_target_platform_check(DROP ... IF EXISTS,可重入)。 此后新增平台只需在平台清单中登记,不再需要改约束的迁移;此前 Grok、国产 厂商、MiniMax、OpenCode 共写过 5 次。 平台合法性改由应用层保证,与原约束的拒绝范围相同: - user_platform_quotas 用原生 SQL 写入,不经过 ent Validate,repository 在 BulkInsertInitial / UpsertForUser 写入前校验平台,非法值在发出任何 SQL 前 返回错误(此前由数据库拒绝并回滚); - ent schema 的 platform / target_platform 校验改查平台清单,组合路由新增 target_platform 校验(service 层与 admin 接口的原有校验保留)。 渠道监控的 provider 约束表示已实现的探测能力,不属于平台清单,保留不动。 集成测试(真实 PostgreSQL):在还原旧约束的库上重复执行迁移、约束被移除、 渠道监控约束保留、数据库层可写入未来的新平台值;repository 拒绝未登记平台 且不产生副作用(去掉校验后该测试失败)。全部仓储集成测试通过。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3b89cc08dd
commit
eefa7dd9dc
@@ -906,7 +906,21 @@ func init() {
|
|||||||
// compositemodelroute.DefaultTargetPlatform holds the default value on creation for the target_platform field.
|
// compositemodelroute.DefaultTargetPlatform holds the default value on creation for the target_platform field.
|
||||||
compositemodelroute.DefaultTargetPlatform = compositemodelrouteDescTargetPlatform.Default.(string)
|
compositemodelroute.DefaultTargetPlatform = compositemodelrouteDescTargetPlatform.Default.(string)
|
||||||
// compositemodelroute.TargetPlatformValidator is a validator for the "target_platform" field. It is called by the builders before save.
|
// compositemodelroute.TargetPlatformValidator is a validator for the "target_platform" field. It is called by the builders before save.
|
||||||
compositemodelroute.TargetPlatformValidator = compositemodelrouteDescTargetPlatform.Validators[0].(func(string) error)
|
compositemodelroute.TargetPlatformValidator = func() func(string) error {
|
||||||
|
validators := compositemodelrouteDescTargetPlatform.Validators
|
||||||
|
fns := [...]func(string) error{
|
||||||
|
validators[0].(func(string) error),
|
||||||
|
validators[1].(func(string) error),
|
||||||
|
}
|
||||||
|
return func(target_platform string) error {
|
||||||
|
for _, fn := range fns {
|
||||||
|
if err := fn(target_platform); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
// compositemodelrouteDescUpstreamModel is the schema descriptor for upstream_model field.
|
// compositemodelrouteDescUpstreamModel is the schema descriptor for upstream_model field.
|
||||||
compositemodelrouteDescUpstreamModel := compositemodelrouteFields[4].Descriptor()
|
compositemodelrouteDescUpstreamModel := compositemodelrouteFields[4].Descriptor()
|
||||||
// compositemodelroute.DefaultUpstreamModel holds the default value on creation for the upstream_model field.
|
// compositemodelroute.DefaultUpstreamModel holds the default value on creation for the upstream_model field.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||||
|
|
||||||
@@ -45,6 +47,13 @@ func (CompositeModelRoute) Fields() []ent.Field {
|
|||||||
field.String("target_platform").
|
field.String("target_platform").
|
||||||
MaxLen(50).
|
MaxLen(50).
|
||||||
Default(domain.PlatformOpenAI).
|
Default(domain.PlatformOpenAI).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
// 目标平台须为平台清单中的具体平台;数据库不再维护 CHECK 约束。
|
||||||
|
if !domain.IsConcretePlatform(s) {
|
||||||
|
return fmt.Errorf("target_platform %q is not a concrete platform", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Comment("Concrete provider platform."),
|
Comment("Concrete provider platform."),
|
||||||
field.String("upstream_model").
|
field.String("upstream_model").
|
||||||
MaxLen(200).
|
MaxLen(200).
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
"entgo.io/ent/schema/index"
|
"entgo.io/ent/schema/index"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserPlatformQuota holds the schema definition for per-user per-platform quota.
|
// UserPlatformQuota holds the schema definition for per-user per-platform quota.
|
||||||
@@ -38,15 +39,12 @@ func (UserPlatformQuota) Fields() []ent.Field {
|
|||||||
MaxLen(32).
|
MaxLen(32).
|
||||||
NotEmpty().
|
NotEmpty().
|
||||||
Validate(func(s string) error {
|
Validate(func(s string) error {
|
||||||
// 注意:平台列表的单一权威源为 service.AllowedQuotaPlatforms;
|
// 平台白名单为平台清单(domain/platforms.go)中的全部具体平台,
|
||||||
// 此处为 ent 构建期约束,需与 service.AllowedQuotaPlatforms 保持同步。
|
// 与 service.AllowedQuotaPlatforms 同源;数据库不再维护 CHECK 约束。
|
||||||
switch s {
|
if !domain.IsConcretePlatform(s) {
|
||||||
case "anthropic", "openai", "gemini", "antigravity", "grok",
|
|
||||||
"kimi", "zhipu", "deepseek", "minimax", "opencode_go", "typesafe":
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("platform %q is not allowed", s)
|
return fmt.Errorf("platform %q is not allowed", s)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 日 / 周 / 月 USD 上限:
|
// 日 / 周 / 月 USD 上限:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const (
|
|||||||
PlatformMiniMax = "minimax" // MiniMax (M 系列)
|
PlatformMiniMax = "minimax" // MiniMax (M 系列)
|
||||||
PlatformTypeSafe = "typesafe" // TypeSafe AI System One (Jev)
|
PlatformTypeSafe = "typesafe" // TypeSafe AI System One (Jev)
|
||||||
// PlatformOpenCodeGo 是 OpenCode 平台(账号类型 Zen 按量 / Go 订阅)。
|
// PlatformOpenCodeGo 是 OpenCode 平台(账号类型 Zen 按量 / Go 订阅)。
|
||||||
// 值保持 opencode_go 以兼容已落库的分组、配额与 Composite 路由 CHECK。
|
// 值保持 opencode_go 以兼容已落库的账号、分组、配额与 Composite 路由。
|
||||||
PlatformOpenCodeGo = "opencode_go"
|
PlatformOpenCodeGo = "opencode_go"
|
||||||
PlatformComposite = "composite"
|
PlatformComposite = "composite"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
dbmigrations "github.com/Wei-Shaw/sub2api/migrations"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func platformConstraintExists(t *testing.T, ctx context.Context, client *dbent.Client, table, constraint string) bool {
|
||||||
|
t.Helper()
|
||||||
|
rows, err := client.QueryContext(ctx, `
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_class tbl ON tbl.oid = c.conrelid
|
||||||
|
JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
|
||||||
|
WHERE ns.nspname = 'public' AND tbl.relname = $1 AND c.conname = $2`, table, constraint)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
exists := rows.Next()
|
||||||
|
require.NoError(t, rows.Err())
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移 242 删除平台白名单 CHECK 约束:旧库(约束仍在)上可重复执行,执行后
|
||||||
|
// 新登记的平台无需再做数据库迁移即可写入。
|
||||||
|
func TestMigration242DropsPlatformCheckConstraints(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tx := testEntTx(t)
|
||||||
|
client := tx.Client()
|
||||||
|
|
||||||
|
// 还原迁移 241_add_typesafe_platform 之后、242 之前的约束状态。
|
||||||
|
for _, stmt := range []string{
|
||||||
|
`ALTER TABLE user_platform_quotas DROP CONSTRAINT IF EXISTS user_platform_quotas_platform_check`,
|
||||||
|
`ALTER TABLE user_platform_quotas ADD CONSTRAINT user_platform_quotas_platform_check
|
||||||
|
CHECK (platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok',
|
||||||
|
'kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go', 'typesafe'))`,
|
||||||
|
`ALTER TABLE composite_model_routes DROP CONSTRAINT IF EXISTS composite_model_routes_target_platform_check`,
|
||||||
|
`ALTER TABLE composite_model_routes ADD CONSTRAINT composite_model_routes_target_platform_check
|
||||||
|
CHECK (target_platform IN ('anthropic', 'openai', 'gemini', 'antigravity', 'grok',
|
||||||
|
'kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go', 'typesafe'))`,
|
||||||
|
} {
|
||||||
|
_, err := client.ExecContext(ctx, stmt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.True(t, platformConstraintExists(t, ctx, client, "user_platform_quotas", "user_platform_quotas_platform_check"))
|
||||||
|
require.True(t, platformConstraintExists(t, ctx, client, "composite_model_routes", "composite_model_routes_target_platform_check"))
|
||||||
|
|
||||||
|
migrationSQL, err := dbmigrations.FS.ReadFile("242_drop_platform_check_constraints.sql")
|
||||||
|
require.NoError(t, err)
|
||||||
|
for range 2 {
|
||||||
|
_, err = client.ExecContext(ctx, string(migrationSQL))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.False(t, platformConstraintExists(t, ctx, client, "user_platform_quotas", "user_platform_quotas_platform_check"))
|
||||||
|
require.False(t, platformConstraintExists(t, ctx, client, "composite_model_routes", "composite_model_routes_target_platform_check"))
|
||||||
|
// 渠道监控的 provider 约束表示探测能力,保留。
|
||||||
|
require.True(t, platformConstraintExists(t, ctx, client, "channel_monitors", "channel_monitors_provider_check"))
|
||||||
|
require.True(t, platformConstraintExists(t, ctx, client, "channel_monitor_request_templates", "channel_monitor_request_templates_provider_check"))
|
||||||
|
|
||||||
|
// 数据库层不再限制平台取值:模拟未来新登记的平台。
|
||||||
|
userID := mustCreateUserForQuota(t, client)
|
||||||
|
_, err = client.ExecContext(ctx, `
|
||||||
|
INSERT INTO user_platform_quotas (user_id, platform, daily_limit_usd, daily_usage_usd, weekly_usage_usd, monthly_usage_usd, created_at, updated_at)
|
||||||
|
VALUES ($1, 'future_platform', 1, 0, 0, 0, NOW(), NOW())`, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
group := mustCreateGroup(t, client, &service.Group{Name: "platform-list-composite", Platform: service.PlatformComposite})
|
||||||
|
_, err = client.ExecContext(ctx, `
|
||||||
|
INSERT INTO composite_model_routes (group_id, public_model, target_platform)
|
||||||
|
VALUES ($1, 'future-model', 'future_platform')`, group.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 CHECK 约束后,平台合法性由应用层保证:原生 SQL 写入前的 repository 校验。
|
||||||
|
func TestUserPlatformQuotaRepository_RejectsUnregisteredPlatform(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tx := testEntTx(t)
|
||||||
|
txCtx := dbent.NewTxContext(ctx, tx)
|
||||||
|
client := tx.Client()
|
||||||
|
|
||||||
|
userID := mustCreateUserForQuota(t, client)
|
||||||
|
repo := NewUserPlatformQuotaRepository(client)
|
||||||
|
daily := 5.0
|
||||||
|
require.NoError(t, repo.UpsertForUser(txCtx, userID, []UserPlatformQuotaRecord{
|
||||||
|
{UserID: userID, Platform: service.PlatformOpenCodeGo, DailyLimitUSD: &daily},
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, platform := range []string{"bogus", "moonshot", "Kimi", service.PlatformComposite} {
|
||||||
|
err := repo.BulkInsertInitial(txCtx, []UserPlatformQuotaRecord{
|
||||||
|
{UserID: userID, Platform: service.PlatformAnthropic, DailyLimitUSD: &daily},
|
||||||
|
{UserID: userID, Platform: platform, DailyLimitUSD: &daily},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "is not allowed", platform)
|
||||||
|
err = repo.UpsertForUser(txCtx, userID, []UserPlatformQuotaRecord{
|
||||||
|
{UserID: userID, Platform: platform, DailyLimitUSD: &daily},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "is not allowed", platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 被拒绝的写入不产生任何副作用:既有记录保留、未新增任何行。
|
||||||
|
got, err := repo.ListByUser(txCtx, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
require.Equal(t, service.PlatformOpenCodeGo, got[0].Platform)
|
||||||
|
|
||||||
|
// 三档全空的记录不写入,也不参与校验(与既有 configuredRecords 语义一致)。
|
||||||
|
require.NoError(t, repo.BulkInsertInitial(txCtx, []UserPlatformQuotaRecord{{UserID: userID, Platform: "bogus"}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompositeModelRouteRepository_RejectsUnregisteredTargetPlatform(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tx := testEntTx(t)
|
||||||
|
txCtx := dbent.NewTxContext(ctx, tx)
|
||||||
|
client := tx.Client()
|
||||||
|
|
||||||
|
group := mustCreateGroup(t, client, &service.Group{Name: "platform-list-routes", Platform: service.PlatformComposite})
|
||||||
|
repo := NewCompositeModelRouteRepository(client)
|
||||||
|
|
||||||
|
route := &service.CompositeModelRoute{
|
||||||
|
GroupID: group.ID, PublicModel: "glm-5", MatchType: "exact", TargetPlatform: service.PlatformZhipu,
|
||||||
|
UpstreamModel: "glm-5", Endpoint: "any", Priority: 100, Enabled: true,
|
||||||
|
}
|
||||||
|
require.NoError(t, repo.Create(txCtx, route))
|
||||||
|
|
||||||
|
bad := &service.CompositeModelRoute{
|
||||||
|
GroupID: group.ID, PublicModel: "bogus-model", MatchType: "exact", TargetPlatform: "bogus",
|
||||||
|
UpstreamModel: "bogus-model", Endpoint: "any", Priority: 100, Enabled: true,
|
||||||
|
}
|
||||||
|
require.Error(t, repo.Create(txCtx, bad))
|
||||||
|
|
||||||
|
route.TargetPlatform = service.PlatformComposite
|
||||||
|
require.Error(t, repo.Update(txCtx, route))
|
||||||
|
|
||||||
|
routes, err := repo.ListByGroup(txCtx, group.ID, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, routes, 1)
|
||||||
|
require.Equal(t, service.PlatformZhipu, routes[0].TargetPlatform)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/userplatformquota"
|
"github.com/Wei-Shaw/sub2api/ent/userplatformquota"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,6 +96,9 @@ func (r *userPlatformQuotaRepository) BulkInsertInitial(ctx context.Context, rec
|
|||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if err := validateQuotaPlatforms(records); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
client := clientFromContext(ctx, r.client)
|
client := clientFromContext(ctx, r.client)
|
||||||
|
|
||||||
@@ -318,6 +322,9 @@ func monthlyMaybeReset(prevUsage float64, prevStart *time.Time, cost float64, no
|
|||||||
// 清空某平台的限额即软删该行并放弃其累计用量;之后再次配置限额会新建行、从新窗口起算。
|
// 清空某平台的限额即软删该行并放弃其累计用量;之后再次配置限额会新建行、从新窗口起算。
|
||||||
func (r *userPlatformQuotaRepository) UpsertForUser(ctx context.Context, userID int64, records []UserPlatformQuotaRecord) error {
|
func (r *userPlatformQuotaRepository) UpsertForUser(ctx context.Context, userID int64, records []UserPlatformQuotaRecord) error {
|
||||||
records = configuredRecords(records)
|
records = configuredRecords(records)
|
||||||
|
if err := validateQuotaPlatforms(records); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return r.withTx(ctx, func(txCtx context.Context, txClient *dbent.Client) error {
|
return r.withTx(ctx, func(txCtx context.Context, txClient *dbent.Client) error {
|
||||||
platforms := make([]string, 0, len(records))
|
platforms := make([]string, 0, len(records))
|
||||||
for _, rec := range records {
|
for _, rec := range records {
|
||||||
@@ -484,6 +491,17 @@ func (r *userPlatformQuotaRepository) BatchSnapshotUsage(ctx context.Context, sn
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateQuotaPlatforms 在原生 SQL 写入前校验平台须为平台清单中的具体平台,
|
||||||
|
// 取代迁移 242 删除的 user_platform_quotas_platform_check 约束(原生 SQL 不经过 ent Validate)。
|
||||||
|
func validateQuotaPlatforms(records []UserPlatformQuotaRecord) error {
|
||||||
|
for _, rec := range records {
|
||||||
|
if !domain.IsConcretePlatform(rec.Platform) {
|
||||||
|
return fmt.Errorf("user_platform_quotas: platform %q is not allowed", rec.Platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// configuredRecords 过滤出至少配置了一档限额的记录;三档全空的记录视为未配置。
|
// configuredRecords 过滤出至少配置了一档限额的记录;三档全空的记录视为未配置。
|
||||||
func configuredRecords(records []UserPlatformQuotaRecord) []UserPlatformQuotaRecord {
|
func configuredRecords(records []UserPlatformQuotaRecord) []UserPlatformQuotaRecord {
|
||||||
out := make([]UserPlatformQuotaRecord, 0, len(records))
|
out := make([]UserPlatformQuotaRecord, 0, len(records))
|
||||||
|
|||||||
@@ -1937,9 +1937,9 @@ func (s *AuthService) snapshotPlatformQuotaDefaults(ctx context.Context, userID
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// 平台配额快照是 best-effort(fail-open):必须脱离调用方事务执行。
|
// 平台配额快照是 best-effort(fail-open):必须脱离调用方事务执行。
|
||||||
// 否则某平台违反 user_platform_quotas 的 CHECK 约束(如尚未进约束的新平台)会让
|
// 否则写入时的任何数据库错误(历史上是新平台尚未加入 user_platform_quotas 的
|
||||||
// 整个调用方事务被 Postgres 标记 aborted,把"无关紧要的默认配额快照"放大成
|
// CHECK 约束)都会让整个调用方事务被 Postgres 标记 aborted,把"无关紧要的默认
|
||||||
// "整笔注册失败"(OAuth pending 路径曾因此 500 → 清 cookie → 404)。
|
// 配额快照"放大成"整笔注册失败"(OAuth pending 路径曾因此 500 → 清 cookie → 404)。
|
||||||
ctx = dbent.WithoutTx(ctx)
|
ctx = dbent.WithoutTx(ctx)
|
||||||
// 仅为至少配置了一档限额的平台建行:user_platform_quotas 中不存在的行等价于不限额,
|
// 仅为至少配置了一档限额的平台建行:user_platform_quotas 中不存在的行等价于不限额,
|
||||||
// 三档全空的记录不携带任何可执行的限额。
|
// 三档全空的记录不携带任何可执行的限额。
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- 平台白名单改由应用层的平台清单(internal/domain/platforms.go)统一校验,
|
||||||
|
-- 新增平台不再需要修改数据库约束的迁移。
|
||||||
|
--
|
||||||
|
-- 1. user_platform_quotas.platform:admin 接口 / 设置校验 service.AllowedQuotaPlatforms,
|
||||||
|
-- repository 写入前校验 domain.IsConcretePlatform,ent schema Validate 同源。
|
||||||
|
-- 2. composite_model_routes.target_platform:admin 接口 binding、service
|
||||||
|
-- compositeRouteFromInput 与 ent schema Validate 均校验为已登记的具体平台。
|
||||||
|
--
|
||||||
|
-- channel_monitors / channel_monitor_request_templates 的 provider CHECK 表示
|
||||||
|
-- 渠道监控已实现的探测能力,不属于平台清单,保留不动。
|
||||||
|
--
|
||||||
|
-- DROP ... IF EXISTS 保证可重入。
|
||||||
|
|
||||||
|
ALTER TABLE user_platform_quotas
|
||||||
|
DROP CONSTRAINT IF EXISTS user_platform_quotas_platform_check;
|
||||||
|
|
||||||
|
ALTER TABLE composite_model_routes
|
||||||
|
DROP CONSTRAINT IF EXISTS composite_model_routes_target_platform_check;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDropPlatformCheckConstraintsMigration(t *testing.T) {
|
||||||
|
content, err := FS.ReadFile("242_drop_platform_check_constraints.sql")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sql := strings.Join(strings.Fields(string(content)), " ")
|
||||||
|
require.Contains(t, sql,
|
||||||
|
"ALTER TABLE user_platform_quotas DROP CONSTRAINT IF EXISTS user_platform_quotas_platform_check;")
|
||||||
|
require.Contains(t, sql,
|
||||||
|
"ALTER TABLE composite_model_routes DROP CONSTRAINT IF EXISTS composite_model_routes_target_platform_check;")
|
||||||
|
require.NotContains(t, sql, "ADD CONSTRAINT")
|
||||||
|
require.NotContains(t, sql, "DROP CONSTRAINT IF EXISTS channel_monitors_provider_check")
|
||||||
|
require.NotContains(t, sql, "DROP CONSTRAINT IF EXISTS channel_monitor_request_templates_provider_check")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user