From 1bc3f4a07bc466d2724123f0e5a6484d126bf0a5 Mon Sep 17 00:00:00 2001 From: CSCITech Date: Wed, 14 Oct 2026 04:01:37 +0800 Subject: [PATCH] test: verify pre3 SQLite startup and settlement recovery --- docs/installation/sqlite-upgrade.md | 25 ++ model/sqlite_upgrade_test.go | 504 +++++++++++++++++++++ model/testdata/sqlite/pre3_schema.sql | 625 ++++++++++++++++++++++++++ 3 files changed, 1154 insertions(+) create mode 100644 docs/installation/sqlite-upgrade.md create mode 100644 model/sqlite_upgrade_test.go create mode 100644 model/testdata/sqlite/pre3_schema.sql diff --git a/docs/installation/sqlite-upgrade.md b/docs/installation/sqlite-upgrade.md new file mode 100644 index 00000000..41e8146e --- /dev/null +++ b/docs/installation/sqlite-upgrade.md @@ -0,0 +1,25 @@ +# SQLite 升级验收 / SQLite upgrade acceptance + +升级前停止接收新请求,在原数据库完成待处理资金结算及日志副作用,停止服务后一起备份业务库、日志库和配置。SQLite 使用 WAL 时,保留尚未合并的 WAL 文件;运行中的单个数据库文件复制不能作为完整备份。 + +Before upgrading, stop new traffic, finish pending funding settlements and log effects against their original databases, stop the service, and back up the business database, log database, and configuration together. Preserve uncheckpointed WAL files. Copying only a database file while it is in use does not establish a complete backup. + +先对备份副本运行升级版本,核对钱包、Token、订阅用量与周期、历史日志、`billing_settlements` 和 `billing_log_receipts`,再重复启动并确认恢复没有再次扣费、退款或生成重复日志。待人工处理、未知结果或跨周期结算保留其核对状态,不能视为升级后自动成功。 + +Run the upgraded version against an isolated backup copy first. Check wallet and token balances, subscription usage and period, historical logs, `billing_settlements`, and `billing_log_receipts`. Restart again and confirm recovery does not debit, refund, or project a log twice. Manual, unknown-outcome, and cross-period records retain their reconciliation boundaries; upgrading does not imply their successful settlement. + +## 日志文件 / Log files + +默认同库和旧 `LOG_SQL_DSN=local` 可以继续使用。新增 `LOG_SQLITE_PATH` 时按[独立日志数据库指引](sqlite-log-database.md)准备目标文件;这个参数不复制或合并旧日志。完成原库的 pending effects,并保持日志和 receipt 一起保留,是切换前的必要准备。新空日志文件的启动验证只适用于已完成这些准备的场景。 + +Existing shared-database and `LOG_SQL_DSN=local` settings remain supported. Follow the [separate log database guide](sqlite-log-database.md) when configuring `LOG_SQLITE_PATH`. The setting does not copy or merge old logs. Finish effects against the original file and retain each log with its receipt before switching. Fresh-file startup acceptance assumes these preparations are complete. + +## 自动化覆盖 / Automated coverage + +`model/sqlite_upgrade_test.go` 使用 MAX-API `v2.0.0-smartops.pre3` 固定提交 `48260ce6517a9ab68851a7ac499477981d78da63` 的真实 `InitDB/InitLogDB` 生成 schema fixture(39 张表、160 个索引),并填入合成数据。当前版本在独立测试进程中执行主节点迁移及恢复,覆盖共享、旧 local、已有独立目标与完成结算后的新空目标,多次启动、64 位余额、明确零费用、原资金来源与周期、旧 receipt 去重及目标文件的日志标记回填。LF 和 Windows CRLF 格式均验证。fixture 只有 schema,不包含用户数据、密钥或运行时二进制。 + +The fixture comes from real `InitDB/InitLogDB` execution at the pinned MAX-API pre3 source revision: 39 tables and 160 indexes, with synthetic data added by the tests. Fresh test processes run current master migrations and recovery for shared, legacy-local, existing separate, and quiesced fresh log targets. Coverage includes repeated startup, 64-bit balances, explicit zero usage, original funding and periods, old receipt deduplication, and target-specific retry-marker backfill. Both LF and Windows CRLF inputs are checked. The committed fixture contains schema only, with no user data, credentials, or runtime binary. + +这个基线不等于验证了所有历史部署。更老版本、真实旧镜像与用户库、MySQL/PostgreSQL、混合引擎、Redis 多节点、在线切库、实盘掉电、保留清理策略及实际渠道仍需对应环境验收。升级后写入的数据不保证可直接降级;需要回退时使用成套升级前备份,并核对外部已接受请求和未完成结算。 + +This baseline does not establish compatibility with every historical deployment. Older releases, actual old images and user databases, MySQL/PostgreSQL, mixed engines, Redis clusters, online switching, power interruption, retention cleanup, and real channels require their own acceptance. New-version writes are not guaranteed to support direct downgrade. Restore the complete pre-upgrade backup for rollback and reconcile externally accepted requests and incomplete settlements. diff --git a/model/sqlite_upgrade_test.go b/model/sqlite_upgrade_test.go new file mode 100644 index 00000000..e58fba8e --- /dev/null +++ b/model/sqlite_upgrade_test.go @@ -0,0 +1,504 @@ +package model + +import ( + "context" + "crypto/sha256" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/MAX-API-Next/MAX-API/common" + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/require" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// Generated by running the actual MAX-API pre3 InitDB/InitLogDB at +// 48260ce6517a9ab68851a7ac499477981d78da63, not by migrating current models. +// The fixture contains schema only. All data below is synthetic. +// +//go:embed testdata/sqlite/pre3_schema.sql +var sqlitePre3Schema string + +const sqlitePre3SchemaSHA = "f4af2569217ecb14863d551aae40b36d655e821a57503c9cdee5bef34e2a8a26" +const sqliteUpgradeConfigEnv = "MAXAPI_SQLITE_UPGRADE_TEST_CONFIG" + +type sqliteUpgradeConfig struct { + Directory string + Mode string + Recover bool +} + +type sqliteUpgradeCase struct { + ID int + Source, State string + Quota int64 +} + +func (item sqliteUpgradeCase) requestID() string { return fmt.Sprintf("synthetic-pre3-%d", item.ID) } +func (item sqliteUpgradeCase) tokenID() int { return item.ID + 1000 } +func (item sqliteUpgradeCase) subscriptionID() int { return item.ID + 2000 } +func (item sqliteUpgradeCase) operationKey() string { + return BillingRequestFinalizeOperationKey(item.requestID()) +} + +func sqliteUpgradeCases(mode string) []sqliteUpgradeCase { + var cases []sqliteUpgradeCase + for _, source := range []string{BillingSettlementSourceWallet, BillingSettlementSourceSubscription} { + for _, quota := range []int64{0, 6, 100, 130} { + states := []string{"pending", "effect_pending", "effect_logged", "complete"} + if mode == "separate_fresh" { + // A fresh target requires quiesced effects on the original file. + // This does not claim an online transfer of pending receipts. + states = []string{"complete"} + } + for _, state := range states { + cases = append(cases, sqliteUpgradeCase{ID: 8800 + len(cases), Source: source, State: state, Quota: quota}) + } + } + if mode != "separate_fresh" { + for _, state := range []string{"manual", "outcome_unknown"} { + cases = append(cases, sqliteUpgradeCase{ID: 8800 + len(cases), Source: source, State: state, Quota: 130}) + } + if source == BillingSettlementSourceSubscription { + cases = append(cases, sqliteUpgradeCase{ID: 8800 + len(cases), Source: source, State: "period_changed", Quota: 6}) + } + } + } + return cases +} + +func openSQLiteUpgradeFixture(t *testing.T, path string, logOnly bool) *gorm.DB { + return openSQLiteUpgradeSchema(t, path, logOnly, sqlitePre3Schema) +} + +func openSQLiteUpgradeSchema(t *testing.T, path string, logOnly bool, schema string) *gorm.DB { + t.Helper() + // Git checkouts can convert text fixtures to CRLF on Windows. + schema = strings.ReplaceAll(schema, "\r\n", "\n") + require.Equal(t, sqlitePre3SchemaSHA, fmt.Sprintf("%x", sha256.Sum256([]byte(schema))), "historical fixture cannot silently become a current-model schema") + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + require.NoError(t, db.Transaction(func(tx *gorm.DB) error { + for _, statement := range strings.Split(schema, "\n-- MAX_API_SQLITE_SCHEMA_STATEMENT\n")[1:] { + if logOnly && !strings.Contains(statement, "CREATE TABLE `logs`") && + !strings.Contains(statement, "CREATE TABLE `billing_log_receipts`") && + !strings.Contains(statement, " ON `logs`") && !strings.Contains(statement, " ON `billing_log_receipts`") { + continue + } + if err := tx.Exec(statement).Error; err != nil { + return err + } + } + return nil + })) + return db +} + +func TestSQLitePre3FixtureFormat(t *testing.T) { + for _, format := range []string{"LF", "CRLF"} { + t.Run(format, func(t *testing.T) { + schema := strings.ReplaceAll(sqlitePre3Schema, "\r\n", "\n") + if format == "CRLF" { + schema = strings.ReplaceAll(schema, "\n", "\r\n") + } + db := openSQLiteUpgradeSchema(t, filepath.Join(t.TempDir(), "fixture.sqlite"), false, schema) + defer func() { require.NoError(t, closeDB(db)) }() + var tables, indexes int64 + require.NoError(t, db.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").Scan(&tables).Error) + require.NoError(t, db.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_%'").Scan(&indexes).Error) + require.EqualValues(t, 39, tables) + require.EqualValues(t, 160, indexes) + }) + } +} + +func seedSQLiteUpgradeFixture(t *testing.T, mode string) string { + t.Helper() + directory := filepath.Join(t.TempDir(), "upgrade 中文 space") + require.NoError(t, os.MkdirAll(directory, 0755)) + db := openSQLiteUpgradeFixture(t, filepath.Join(directory, "ledger.sqlite"), false) + t.Cleanup(func() { _ = closeDB(db) }) + logDB := db + if mode == "separate_existing" { + logDB = openSQLiteUpgradeFixture(t, filepath.Join(directory, "logs.sqlite"), true) + t.Cleanup(func() { _ = closeDB(logDB) }) + require.False(t, logDB.Migrator().HasTable(&User{})) + } + // Seed values directly into the captured schema without running current + // save hooks or creating current tables before the upgrade under test. + seed := db.Session(&gorm.Session{SkipHooks: true}) + require.NoError(t, seed.Create(&User{Id: 8700, Username: "synthetic-pre3-history", AffCode: "uphist", Email: "Legacy@Example.invalid", Quota: 3000000000, + UsedQuota: 4000000000, RequestCount: 7, Status: common.UserStatusEnabled}).Error) + require.NoError(t, seed.Create(&Token{Id: 8701, UserId: 8700, Key: "synthetic-pre3-history", RemainQuota: 5000000000, + UsedQuota: 4000000000, Status: common.TokenStatusEnabled}).Error) + require.NoError(t, seed.Create(&Channel{Id: 8702, UsedQuota: 4000000000}).Error) + history := Log{Id: 8703, UserId: 8700, Type: LogTypeConsume, Quota: 23, PromptTokens: 10, CompletionTokens: 13, + Content: "synthetic 历史记录", RequestId: "synthetic-pre3-history", Other: `{"retry_log":true}`} + require.NoError(t, seed.Create(&history).Error) + require.NoError(t, seed.Create(&BillingLogReceipt{ID: 8704, OperationKey: "synthetic-pre3-history", ClaimToken: "synthetic-history", CreatedAt: 1700000000}).Error) + if mode == "separate_existing" { + target := logDB.Session(&gorm.Session{SkipHooks: true}) + require.NoError(t, target.Create(&Log{Id: 8703, UserId: 8700, Type: LogTypeConsume, Quota: 23, + Content: "synthetic 独立历史日志", RequestId: "synthetic-pre3-target-history", Other: `{"retry_log":true}`}).Error) + require.NoError(t, target.Create(&BillingLogReceipt{ID: 8704, OperationKey: "synthetic-pre3-target-history", + ClaimToken: "synthetic-target-history", CreatedAt: 1700000000}).Error) + } + + for _, item := range sqliteUpgradeCases(mode) { + finalized := item.State == "effect_pending" || item.State == "effect_logged" || item.State == "complete" + amount := int64(100) + if finalized { + amount = item.Quota + } + wallet := int64(1000) + if item.Source == BillingSettlementSourceWallet { + wallet -= amount + } + used, requests := int64(0), 0 + if item.State == "complete" { + used, requests = item.Quota, 1 + } + require.NoError(t, seed.Create(&User{Id: item.ID, Username: item.requestID(), AffCode: fmt.Sprintf("up%d", item.ID), Quota: wallet, UsedQuota: used, + RequestCount: requests, Status: common.UserStatusEnabled}).Error) + require.NoError(t, seed.Create(&Token{Id: item.tokenID(), UserId: item.ID, Key: item.requestID(), + RemainQuota: 1000 - amount, UsedQuota: amount, Status: common.TokenStatusEnabled}).Error) + require.NoError(t, seed.Create(&Channel{Id: item.ID, UsedQuota: used}).Error) + if item.Source == BillingSettlementSourceSubscription { + allow := false + require.NoError(t, seed.Create(&SubscriptionPlan{Id: item.ID, Title: item.requestID(), Enabled: true, + TotalAmount: 1000, AllowWalletOverflow: &allow, QuotaResetPeriod: SubscriptionResetNever}).Error) + period := int64(1700000000) + if item.State == "period_changed" { + period++ + } + require.NoError(t, seed.Create(&UserSubscription{Id: item.subscriptionID(), UserId: item.ID, PlanId: item.ID, + AmountTotal: 1000, AmountUsed: amount, Status: "active", StartTime: 1700000000, + EndTime: 9999999999, LastResetTime: period, AllowWalletOverflow: false}).Error) + require.NoError(t, seed.Create(&SubscriptionPreConsumeRecord{RequestId: item.requestID(), UserId: item.ID, + TokenId: item.tokenID(), UserSubscriptionId: item.subscriptionID(), SubscriptionLastResetTime: 1700000000, + ModelName: "synthetic-upgrade-model", PreConsumed: 100, Status: "consumed", CreatedAt: 1700000001, UpdatedAt: 1700000001}).Error) + } + require.NoError(t, seed.Create(&BillingPreConsumeSelection{RequestID: item.requestID(), Source: item.Source, + UserID: item.ID, TokenID: item.tokenID(), ModelName: "synthetic-upgrade-model", RequestedQuota: 100, + EffectiveQuota: 100, CreatedAt: 1700000001}).Error) + prompt, completion := int(item.Quota), 0 + if item.Quota > 0 { + prompt, completion = int(item.Quota)-2, 2 + } + effect := BillingSettlementEffect{LogType: LogTypeConsume, Content: "synthetic pre3 recovery", ChannelID: item.ID, + ModelName: "synthetic-upgrade-model", TokenID: item.tokenID(), TokenName: "synthetic-upgrade-token", Group: "default", + UpdateUsage: true, Quota: item.Quota, QuotaIsActual: true, PromptTokens: prompt, CompletionTokens: completion, RequestID: item.requestID()} + payload, err := common.Marshal(effect) + require.NoError(t, err) + record := BillingSettlement{OperationKey: item.operationKey(), Source: item.Source, UserID: item.ID, TokenID: item.tokenID(), + FundingDelta: item.Quota - 100, TokenDelta: item.Quota - 100, Status: BillingSettlementStatusPending, + Revision: 1, EffectPayload: string(payload), EffectStatus: BillingSettlementEffectPending, CreatedAt: 1700000002, UpdatedAt: 1700000002} + if item.Source == BillingSettlementSourceSubscription { + record.SubscriptionID, record.SubscriptionPreConsumeRequestID = item.subscriptionID(), item.requestID() + } + if finalized { + record.Status = BillingSettlementStatusApplied + record.AppliedFundingDelta, record.AppliedTokenDelta = record.FundingDelta, record.TokenDelta + } + if item.State == "manual" || item.State == "outcome_unknown" { + record.Status = item.State + } + if item.State == "complete" { + record.EffectStatus = BillingSettlementEffectApplied + } + require.NoError(t, seed.Create(&record).Error) + if item.State == "effect_logged" || item.State == "complete" { + logs := logDB.Session(&gorm.Session{SkipHooks: true}) + require.NoError(t, logs.Create(&Log{UserId: item.ID, Type: LogTypeConsume, Quota: int(item.Quota), PromptTokens: prompt, + CompletionTokens: completion, ModelName: effect.ModelName, TokenId: item.tokenID(), RequestId: item.requestID()}).Error) + require.NoError(t, logs.Create(&BillingLogReceipt{OperationKey: item.operationKey(), ClaimToken: "synthetic-old-receipt", CreatedAt: 1700000003}).Error) + } + } + require.NoError(t, closeDB(logDB)) + if logDB != db { + require.NoError(t, closeDB(db)) + } + require.NoError(t, os.WriteFile(filepath.Join(directory, ".synthetic-upgrade-fixture"), []byte(sqlitePre3SchemaSHA), 0600)) + return directory +} + +// A cold process runs the real master startup with isolated globals and +// connections. The parent never substitutes a fresh AutoMigrate schema. +func TestSQLiteUpgradeProcessHelper(t *testing.T) { + encoded := os.Getenv(sqliteUpgradeConfigEnv) + if encoded == "" { + t.Skip("cold-start helper invoked by TestSQLitePre3Upgrade") + } + var config sqliteUpgradeConfig + require.NoError(t, common.UnmarshalJsonStr(encoded, &config)) + marker, err := os.ReadFile(filepath.Join(config.Directory, ".synthetic-upgrade-fixture")) + require.NoError(t, err) + require.Equal(t, sqlitePre3SchemaSHA, string(marker)) + require.Contains(t, []string{"shared", "legacy_local", "separate_existing", "separate_fresh"}, config.Mode) + t.Setenv("SQL_DSN", "local") + t.Setenv("LOG_SQL_DSN", "") + t.Setenv("LOG_SQLITE_PATH", "") + t.Setenv("SQL_MAX_IDLE_CONNS", "1") + t.Setenv("SQL_MAX_OPEN_CONNS", "1") + t.Setenv("QUOTA_DATA_AGGREGATE_MIGRATION_ENABLED", "false") + if config.Mode == "legacy_local" { + t.Setenv("LOG_SQL_DSN", "local") + } + if strings.HasPrefix(config.Mode, "separate_") { + t.Setenv("LOG_SQLITE_PATH", filepath.Join(config.Directory, "logs.sqlite")) + } + common.SQLitePath = filepath.Join(config.Directory, "ledger.sqlite") + common.IsMasterNode, common.UsingSQLite = true, true + common.UsingMySQL, common.UsingPostgreSQL = false, false + common.RedisEnabled, common.BatchUpdateEnabled, common.DataExportEnabled = false, false, false + common.LogConsumeEnabled, common.DebugEnabled = true, false + logRetryMarkerBackfillAsyncRunner = func(fn func()) { fn() } + require.NoError(t, InitDB()) + t.Cleanup(func() { require.NoError(t, CloseDB()) }) + require.NoError(t, InitLogDB()) + DB.Logger, LOG_DB.Logger = logger.Default.LogMode(logger.Silent), logger.Default.LogMode(logger.Silent) + var history User + require.NoError(t, DB.First(&history, 8700).Error) + require.Equal(t, "synthetic-pre3-history", history.Username) + if config.Recover { + ProcessPendingBillingSettlementsOnce() + ProcessPendingBillingSettlementsOnce() + for _, item := range sqliteUpgradeCases(config.Mode) { + var record BillingSettlement + require.NoError(t, DB.Where("operation_key = ?", item.operationKey()).First(&record).Error) + if item.State == "manual" || item.State == "outcome_unknown" || item.State == "period_changed" { + continue + } + var effect BillingSettlementEffect + require.NoError(t, common.UnmarshalJsonStr(record.EffectPayload, &effect)) + for range 2 { + applied, duplicate, err := ApplyBillingSettlementOnce(BillingSettlementInput{OperationKey: record.OperationKey, + Source: record.Source, UserID: record.UserID, SubscriptionID: record.SubscriptionID, TokenID: record.TokenID, + FundingDelta: record.FundingDelta, TokenDelta: record.TokenDelta, + SubscriptionPreConsumeRequestID: record.SubscriptionPreConsumeRequestID, Effect: &effect}) + require.NoError(t, err) + require.True(t, duplicate) + require.Equal(t, item.Quota-100, applied) + require.NoError(t, ProcessBillingSettlementEffect(record.OperationKey)) + } + } + } +} + +func runSQLiteUpgradeProcess(t *testing.T, config sqliteUpgradeConfig) { + t.Helper() + encoded, err := common.Marshal(config) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + command := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestSQLiteUpgradeProcessHelper$", "-test.count=1", "-test.timeout=40s", "-test.v") + for _, entry := range os.Environ() { + if !strings.HasPrefix(entry, sqliteUpgradeConfigEnv+"=") { + command.Env = append(command.Env, entry) + } + } + command.Env = append(command.Env, sqliteUpgradeConfigEnv+"="+string(encoded)) + output, err := command.CombinedOutput() + require.NoError(t, err, "cold startup/recovery: %s", output) +} + +func sqliteUpgradeFinancialSnapshot(t *testing.T, directory string) string { + t.Helper() + db, err := gorm.Open(sqlite.Open(filepath.Join(directory, "ledger.sqlite")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + defer func() { require.NoError(t, closeDB(db)) }() + var users []struct { + Id int + Quota, UsedQuota int64 + RequestCount int + } + var tokens []Token + var subscriptions []UserSubscription + var selections []BillingPreConsumeSelection + var reservations []SubscriptionPreConsumeRecord + var settlements []BillingSettlement + var channels []struct { + Id int + UsedQuota int64 + } + require.NoError(t, db.Model(&User{}).Order("id").Find(&users).Error) + require.NoError(t, db.Order("id").Find(&tokens).Error) + require.NoError(t, db.Order("id").Find(&subscriptions).Error) + require.NoError(t, db.Order("id").Find(&selections).Error) + require.NoError(t, db.Order("id").Find(&reservations).Error) + require.NoError(t, db.Order("id").Find(&settlements).Error) + require.NoError(t, db.Model(&Channel{}).Order("id").Find(&channels).Error) + data, err := common.Marshal([]any{users, tokens, subscriptions, selections, reservations, settlements, channels}) + require.NoError(t, err) + return string(data) +} + +func assertSQLiteUpgradedRecovery(t *testing.T, directory, mode string) { + t.Helper() + db, err := gorm.Open(sqlite.Open(filepath.Join(directory, "ledger.sqlite")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + defer func() { require.NoError(t, closeDB(db)) }() + logDB := db + if strings.HasPrefix(mode, "separate_") { + logDB, err = gorm.Open(sqlite.Open(filepath.Join(directory, "logs.sqlite")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + defer func() { require.NoError(t, closeDB(logDB)) }() + require.False(t, logDB.Migrator().HasTable(&User{})) + require.False(t, logDB.Migrator().HasTable(&BillingSettlement{})) + } + for _, item := range sqliteUpgradeCases(mode) { + t.Run(fmt.Sprintf("%s/%s/quota%d", item.Source, item.State, item.Quota), func(t *testing.T) { + blocked := item.State == "manual" || item.State == "outcome_unknown" || item.State == "period_changed" + amount := item.Quota + if blocked { + amount = 100 + } + var user User + var token Token + var channel Channel + var record BillingSettlement + require.NoError(t, db.First(&user, item.ID).Error) + require.NoError(t, db.First(&token, item.tokenID()).Error) + require.NoError(t, db.First(&channel, item.ID).Error) + require.NoError(t, db.Where("operation_key = ?", item.operationKey()).First(&record).Error) + wallet := int64(1000) + if item.Source == BillingSettlementSourceWallet { + wallet -= amount + } + require.Equal(t, wallet, user.Quota) + require.Equal(t, 1000-amount, token.RemainQuota) + require.Equal(t, amount, token.UsedQuota) + require.Equal(t, item.Source, record.Source) + require.Equal(t, item.ID, record.UserID) + require.Equal(t, item.tokenID(), record.TokenID) + if item.Source == BillingSettlementSourceSubscription { + var subscription UserSubscription + require.NoError(t, db.First(&subscription, item.subscriptionID()).Error) + require.Equal(t, amount, subscription.AmountUsed) + require.False(t, subscription.AllowWalletOverflow) + var reservation SubscriptionPreConsumeRecord + require.NoError(t, db.Where("request_id = ?", item.requestID()).First(&reservation).Error) + require.EqualValues(t, 1700000000, reservation.SubscriptionLastResetTime) + require.EqualValues(t, 100, reservation.PreConsumed) + require.Equal(t, item.subscriptionID(), record.SubscriptionID) + } + if blocked { + want := item.State + if want == "period_changed" { + want = BillingSettlementStatusManual + } + require.Equal(t, want, record.Status) + require.Zero(t, record.AppliedFundingDelta) + require.Zero(t, record.AppliedTokenDelta) + require.Zero(t, user.UsedQuota) + require.Zero(t, user.RequestCount) + require.Zero(t, channel.UsedQuota) + } else { + require.Equal(t, BillingSettlementStatusApplied, record.Status) + require.Equal(t, BillingSettlementEffectApplied, record.EffectStatus) + require.Equal(t, item.Quota-100, record.AppliedFundingDelta) + require.Equal(t, item.Quota-100, record.AppliedTokenDelta) + require.Equal(t, item.Quota, user.UsedQuota) + require.Equal(t, 1, user.RequestCount) + require.Equal(t, item.Quota, channel.UsedQuota) + } + var logs, receipts int64 + require.NoError(t, logDB.Model(&Log{}).Where("request_id = ?", item.requestID()).Count(&logs).Error) + require.NoError(t, logDB.Model(&BillingLogReceipt{}).Where("operation_key = ?", item.operationKey()).Count(&receipts).Error) + want := int64(1) + if blocked || mode == "separate_fresh" { + want = 0 + } + require.Equal(t, want, logs) + require.Equal(t, want, receipts) + if want == 1 { + var log Log + require.NoError(t, logDB.Where("request_id = ?", item.requestID()).First(&log).Error) + require.EqualValues(t, item.Quota, log.Quota) + require.EqualValues(t, item.Quota, log.PromptTokens+log.CompletionTokens) + if item.State == "effect_logged" || item.State == "complete" { + var receipt BillingLogReceipt + require.NoError(t, logDB.Where("operation_key = ?", item.operationKey()).First(&receipt).Error) + require.Equal(t, "synthetic-old-receipt", receipt.ClaimToken) + require.EqualValues(t, 1700000003, receipt.CreatedAt) + } + } + }) + } + var history User + require.NoError(t, db.First(&history, 8700).Error) + require.EqualValues(t, 3000000000, history.Quota) + require.EqualValues(t, 4000000000, history.UsedQuota) + require.Equal(t, 7, history.RequestCount) + require.Equal(t, "Legacy@Example.invalid", history.Email) + require.Equal(t, "legacy@example.invalid", history.NormalizedEmail) + var historyLog Log + require.NoError(t, db.First(&historyLog, 8703).Error) + require.Equal(t, "synthetic 历史记录", historyLog.Content) + require.Equal(t, 23, historyLog.Quota) + require.Equal(t, !strings.HasPrefix(mode, "separate_"), historyLog.IsRetry) + var receipt BillingLogReceipt + require.NoError(t, db.First(&receipt, 8704).Error) + require.Equal(t, "synthetic-pre3-history", receipt.OperationKey) + require.Equal(t, "synthetic-history", receipt.ClaimToken) + var options []Option + require.NoError(t, db.Find(&options).Error) + var completed []Option + for _, option := range options { + if strings.HasPrefix(option.Key, logRetryMarkerBackfillOptionKey) { + completed = append(completed, option) + } + } + require.Len(t, completed, 1, "repeated startup retains one completion identity for the active file") + require.Equal(t, "true", completed[0].Value) + if mode == "shared" { + require.Equal(t, logRetryMarkerBackfillOptionKey, completed[0].Key) + } else { + require.NotEqual(t, logRetryMarkerBackfillOptionKey, completed[0].Key) + } + if mode == "separate_existing" { + var targetHistory Log + require.NoError(t, logDB.First(&targetHistory, 8703).Error) + require.Equal(t, "synthetic 独立历史日志", targetHistory.Content) + require.True(t, targetHistory.IsRetry) + require.True(t, targetHistory.IsErrorRetry) + require.NoError(t, logDB.First(&receipt, 8704).Error) + require.Equal(t, "synthetic-pre3-target-history", receipt.OperationKey) + require.Equal(t, "synthetic-target-history", receipt.ClaimToken) + } + if mode == "separate_fresh" { + var logs, receipts int64 + require.NoError(t, logDB.Model(&Log{}).Count(&logs).Error) + require.NoError(t, logDB.Model(&BillingLogReceipt{}).Count(&receipts).Error) + require.Zero(t, logs, "fresh target does not implicitly copy old history") + require.Zero(t, receipts) + } +} + +func TestSQLitePre3Upgrade(t *testing.T) { + for _, mode := range []string{"shared", "legacy_local", "separate_existing", "separate_fresh"} { + t.Run(mode, func(t *testing.T) { + directory := seedSQLiteUpgradeFixture(t, mode) + before := sqliteUpgradeFinancialSnapshot(t, directory) + config := sqliteUpgradeConfig{Directory: directory, Mode: mode} + for range 2 { + runSQLiteUpgradeProcess(t, config) + require.Equal(t, before, sqliteUpgradeFinancialSnapshot(t, directory), "startup migration must not debit/refund or change a durable intent") + } + config.Recover = true + runSQLiteUpgradeProcess(t, config) + assertSQLiteUpgradedRecovery(t, directory, mode) + settled := sqliteUpgradeFinancialSnapshot(t, directory) + runSQLiteUpgradeProcess(t, config) + assertSQLiteUpgradedRecovery(t, directory, mode) + require.Equal(t, settled, sqliteUpgradeFinancialSnapshot(t, directory), "fresh-process recovery returns recorded outcomes without another mutation") + }) + } +} diff --git a/model/testdata/sqlite/pre3_schema.sql b/model/testdata/sqlite/pre3_schema.sql new file mode 100644 index 00000000..96211b48 --- /dev/null +++ b/model/testdata/sqlite/pre3_schema.sql @@ -0,0 +1,625 @@ +-- MAX-API SQLite schema generated by real pre3 InitDB/InitLogDB. +-- Source: v2.0.0-smartops.pre3 48260ce6517a9ab68851a7ac499477981d78da63 +-- Schema only; no user data or credentials. + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `abilities` (`group` varchar(64),`model` varchar(255),`channel_id` integer,`enabled` numeric,`priority` integer DEFAULT 0,`weight` integer DEFAULT 0,`tag` text,PRIMARY KEY (`group`,`model`,`channel_id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `auth_flows` (`id` integer,`token_hash` char(64) NOT NULL,`purpose` varchar(32) NOT NULL,`provider` varchar(64),`intent` varchar(16),`user_id` integer,`session_id` varchar(64),`payload` text,`created_at` datetime,`expires_at` datetime NOT NULL,`consumed_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `authz_user_overrides` (`id` integer,`user_id` integer NOT NULL,`resource` varchar(64) NOT NULL,`action` varchar(64) NOT NULL,`allowed` numeric NOT NULL,`created_at` integer NOT NULL,`updated_at` integer NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `billing_log_receipts` (`id` integer,`operation_key` varchar(191) NOT NULL,`claim_token` varchar(36) NOT NULL DEFAULT "",`created_at` integer NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `billing_pre_consume_selections` (`id` integer,`request_id` varchar(64) NOT NULL,`source` varchar(32) NOT NULL,`user_id` integer NOT NULL,`token_id` integer NOT NULL DEFAULT 0,`model_name` varchar(191) NOT NULL DEFAULT "",`requested_quota` integer NOT NULL DEFAULT 0,`effective_quota` integer NOT NULL DEFAULT 0,`created_at` integer NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `billing_settlements` (`id` integer,`operation_key` varchar(191) NOT NULL,`source` varchar(32) NOT NULL,`user_id` integer NOT NULL DEFAULT 0,`subscription_id` integer NOT NULL DEFAULT 0,`token_id` integer NOT NULL DEFAULT 0,`funding_delta` integer NOT NULL DEFAULT 0,`applied_funding_delta` integer NOT NULL DEFAULT 0,`token_delta` integer NOT NULL DEFAULT 0,`applied_token_delta` integer NOT NULL DEFAULT 0,`task_id` integer NOT NULL DEFAULT 0,`task_quota` integer NOT NULL DEFAULT 0,`task_quota_target` integer NOT NULL DEFAULT 0,`status` varchar(16) NOT NULL DEFAULT "applied",`attempts` integer NOT NULL DEFAULT 0,`last_error` text,`next_attempt` integer NOT NULL DEFAULT 0,`revision` integer NOT NULL DEFAULT 1,`subscription_pre_consume_request_id` varchar(64),`finalize_subscription_pre_consume` numeric NOT NULL DEFAULT false,`allow_missing_token` numeric NOT NULL DEFAULT false,`manual_on_failure` numeric NOT NULL DEFAULT false,`pre_consume_request_id` varchar(64),`pre_consume_model_name` varchar(191) NOT NULL DEFAULT "",`pre_consume_requested_quota` integer NOT NULL DEFAULT 0,`pre_consume_effective_quota` integer NOT NULL DEFAULT 0,`effect_payload` text,`effect_status` varchar(16),`reconciliation_reviewed_at` integer NOT NULL DEFAULT 0,`reconciliation_reviewed_by` integer NOT NULL DEFAULT 0,`reconciliation_review_note` text,`user_blocking_override` numeric,`created_at` integer NOT NULL,`updated_at` integer NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `cache_invalidation_tasks` (`id` integer,`kind` varchar(16) NOT NULL,`entity_key` varchar(96) NOT NULL,`version_key` varchar(96) NOT NULL,`delete_entry` numeric NOT NULL DEFAULT false,`attempts` integer NOT NULL DEFAULT 0,`last_error` text,`next_attempt` integer NOT NULL DEFAULT 0,`created_at` integer NOT NULL,`updated_at` integer NOT NULL,`revision` integer NOT NULL DEFAULT 1,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `channels` (`id` integer,`type` integer DEFAULT 0,`key` text NOT NULL,`open_ai_organization` text,`test_model` text,`status` integer DEFAULT 1,`name` text,`weight` integer DEFAULT 0,`created_time` integer,`test_time` integer,`response_time` integer,`base_url` text DEFAULT "",`other` text,`balance` real,`balance_updated_time` integer,`models` text,`group` varchar(64) DEFAULT "default",`used_quota` integer DEFAULT 0,`model_mapping` text,`status_code_mapping` varchar(1024) DEFAULT "",`priority` integer DEFAULT 0,`auto_ban` integer DEFAULT 1,`other_info` text,`tag` text,`setting` text,`param_override` text,`header_override` text,`remark` varchar(255),`channel_info` json,`settings` text,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `checkins` (`id` integer,`user_id` integer NOT NULL,`checkin_date` varchar(10) NOT NULL,`quota_awarded` integer NOT NULL,`created_at` integer,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `custom_oauth_providers` (`id` integer,`name` varchar(64) NOT NULL,`slug` varchar(64) NOT NULL,`icon` varchar(128) DEFAULT "",`enabled` numeric DEFAULT false,`client_id` varchar(256),`client_secret` varchar(512),`authorization_endpoint` varchar(512),`token_endpoint` varchar(512),`user_info_endpoint` varchar(512),`scopes` varchar(256) DEFAULT "openid profile email",`user_id_field` varchar(128) DEFAULT "sub",`username_field` varchar(128) DEFAULT "preferred_username",`display_name_field` varchar(128) DEFAULT "name",`email_field` varchar(128) DEFAULT "email",`well_known` varchar(512),`auth_style` integer DEFAULT 0,`access_policy` text,`access_denied_message` varchar(512),`created_at` datetime,`updated_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `logs` (`id` integer,`user_id` integer,`created_at` integer,`type` integer,`content` text,`username` text DEFAULT "",`token_name` text DEFAULT "",`model_name` text DEFAULT "",`quota` integer DEFAULT 0,`prompt_tokens` integer DEFAULT 0,`completion_tokens` integer DEFAULT 0,`use_time` integer DEFAULT 0,`is_stream` numeric,`channel_id` integer,`channel_name` text,`token_id` integer DEFAULT 0,`group` text,`ip` text DEFAULT "",`request_id` varchar(64) DEFAULT "",`upstream_request_id` varchar(128) DEFAULT "",`other` text,`is_retry` numeric DEFAULT false,`is_error_retry` numeric DEFAULT false,`is_empty_retry` numeric DEFAULT false,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `midjourney_billing_claims` (`id` integer,`channel_id` integer NOT NULL,`mj_id` varchar(180) NOT NULL,`user_id` integer NOT NULL,`billing_task_id` integer NOT NULL DEFAULT 0,`billing_request_id` varchar(64) NOT NULL DEFAULT "",`created_at` integer NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `midjourneys` (`id` integer,`code` integer,`user_id` integer,`action` varchar(40),`mj_id` text,`prompt` text,`prompt_en` text,`description` text,`state` text,`submit_time` integer,`start_time` integer,`finish_time` integer,`image_url` text,`video_url` text,`video_urls` text,`status` varchar(20),`progress` varchar(30),`fail_reason` text,`channel_id` integer,`quota` integer,`buttons` text,`properties` text,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `models` (`id` integer,`model_name` text NOT NULL,`name_key` text NOT NULL DEFAULT "",`description` text,`icon` varchar(128),`tags` varchar(255),`vendor_id` integer,`endpoints` text,`status` integer DEFAULT 1,`sync_official` integer DEFAULT 1,`created_time` integer,`updated_time` integer,`deleted_at` datetime,`name_rule` integer DEFAULT 0,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `options` (`key` text,`value` text,PRIMARY KEY (`key`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `passkey_credentials` (`id` integer,`user_id` integer NOT NULL,`credential_id` varchar(512) NOT NULL,`public_key` text NOT NULL,`attestation_type` varchar(255),`aa_guid` varchar(512),`sign_count` integer DEFAULT 0,`clone_warning` numeric,`user_present` numeric,`user_verified` numeric,`backup_eligible` numeric,`backup_state` numeric,`transports` text,`attachment` varchar(32),`last_used_at` datetime,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `perf_metric_flush_receipts` (`id` integer,`receipt_key` text NOT NULL,`claim_token` text NOT NULL,`created_at` integer NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `perf_metrics` (`id` integer,`model_name` text,`group` text,`bucket_ts` integer,`request_count` integer DEFAULT 0,`success_count` integer DEFAULT 0,`total_latency_ms` integer DEFAULT 0,`ttft_sum_ms` integer DEFAULT 0,`ttft_count` integer DEFAULT 0,`output_tokens` integer DEFAULT 0,`generation_ms` integer DEFAULT 0,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `prefill_groups` (`id` integer,`name` text NOT NULL,`type` text NOT NULL,`items` json,`description` varchar(255),`created_time` integer,`updated_time` integer,`deleted_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `quota_data` (`id` integer,`aggregate_key` varchar(64),`user_id` integer,`username` text DEFAULT "",`model_name` text DEFAULT "",`created_at` integer,`use_group` text DEFAULT "",`token_id` integer DEFAULT 0,`channel_id` integer DEFAULT 0,`node_name` text DEFAULT "",`token_used` integer DEFAULT 0,`count` integer DEFAULT 0,`quota` integer DEFAULT 0,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `quota_data_operation_locks` (`name` varchar(64) PRIMARY KEY, `owner` varchar(64) NOT NULL, `expires_at` bigint NOT NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `quota_data_snapshot_retries` (`snapshot_id` varchar(64),`retry_until` integer,`updated_at` integer,PRIMARY KEY (`snapshot_id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `quota_data_snapshots` (`snapshot_id` varchar(64),`created_at` integer,PRIMARY KEY (`snapshot_id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `redemptions` (`id` integer,`user_id` integer,`key` char(32),`status` integer DEFAULT 1,`name` text,`quota` integer DEFAULT 100,`created_time` integer,`redeemed_time` integer,`used_user_id` integer,`deleted_at` datetime,`expired_time` integer,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `setups` (`id` integer,`version` varchar(50) NOT NULL,`initialized_at` bigint NOT NULL,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `subscription_orders` (`id` integer,`user_id` integer,`plan_id` integer,`money` real,`trade_no` varchar(255) UNIQUE,`payment_method` varchar(50),`payment_provider` varchar(50) DEFAULT "",`status` text,`create_time` integer,`complete_time` integer,`provider_payload` text,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `subscription_plans` ( +`id` integer, +`title` varchar(128) NOT NULL, +`subtitle` varchar(255) DEFAULT '', +`price_amount` decimal(10,6) NOT NULL, +`currency` varchar(8) NOT NULL DEFAULT 'USD', +`duration_unit` varchar(16) NOT NULL DEFAULT 'month', +`duration_value` integer NOT NULL DEFAULT 1, +`custom_seconds` bigint NOT NULL DEFAULT 0, +`enabled` numeric DEFAULT 1, +`sort_order` integer DEFAULT 0, +`allow_balance_pay` numeric DEFAULT 1, +`allow_wallet_overflow` numeric DEFAULT 1, +`stripe_price_id` varchar(128) DEFAULT '', +`creem_product_id` varchar(128) DEFAULT '', +`waffo_pancake_product_id` varchar(128) DEFAULT '', +`max_purchase_per_user` integer DEFAULT 0, +`upgrade_group` varchar(64) DEFAULT '', +`downgrade_group` varchar(64) DEFAULT '', +`total_amount` bigint NOT NULL DEFAULT 0, +`quota_reset_period` varchar(16) DEFAULT 'never', +`quota_reset_custom_seconds` bigint DEFAULT 0, +`created_at` bigint, +`updated_at` bigint, +PRIMARY KEY (`id`) +); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `subscription_pre_consume_records` (`id` integer,`request_id` varchar(64),`user_id` integer,`token_id` integer,`user_subscription_id` integer,`subscription_last_reset_time` integer NOT NULL DEFAULT 0,`model_name` varchar(191),`quota_type` integer,`pre_consumed` bigint NOT NULL DEFAULT 0,`status` varchar(32),`created_at` integer,`updated_at` integer,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `system_instances` (`node_name` varchar(128),`info` text,`started_at` integer,`last_seen_at` integer,`created_at` integer,`updated_at` integer,PRIMARY KEY (`node_name`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `system_tasks` (`id` integer,`task_id` varchar(64),`type` varchar(64),`status` varchar(32),`active_key` varchar(64),`payload` text,`state` text,`result` text,`error` text,`locked_by` varchar(128),`locked_until` integer,`created_at` integer,`updated_at` integer,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `tasks` (`id` integer,`created_at` integer,`updated_at` integer,`task_id` varchar(191),`platform` varchar(30),`user_id` integer,`group` varchar(50),`channel_id` integer,`quota` integer,`action` varchar(40),`status` varchar(20),`fail_reason` text,`submit_time` integer,`start_time` integer,`finish_time` integer,`progress` varchar(20),`properties` json,`private_data` json,`data` json,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `tokens` (`id` integer,`user_id` integer,`key` varchar(128),`status` integer DEFAULT 1,`name` text,`created_time` integer,`accessed_time` integer,`expired_time` integer DEFAULT -1,`remain_quota` bigint DEFAULT 0,`unlimited_quota` numeric,`model_limits_enabled` numeric,`model_limits` text,`allow_ips` text DEFAULT "",`used_quota` bigint DEFAULT 0,`group` text DEFAULT "",`cross_group_retry` numeric,`routing_policy` text,`deleted_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `top_ups` (`id` integer,`user_id` integer,`amount` integer,`money` real,`trade_no` varchar(255) UNIQUE,`payment_method` varchar(50),`payment_provider` varchar(50) DEFAULT "",`create_time` integer,`complete_time` integer,`status` text,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `two_fa_backup_codes` (`id` integer,`user_id` integer NOT NULL,`code_hash` varchar(255) NOT NULL,`is_used` numeric,`used_at` datetime,`created_at` datetime,`deleted_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `two_fas` (`id` integer,`user_id` integer NOT NULL UNIQUE,`secret` varchar(255) NOT NULL,`is_enabled` numeric,`failed_attempts` integer DEFAULT 0,`locked_until` datetime,`last_used_at` datetime,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `user_oauth_bindings` (`id` integer,`user_id` integer NOT NULL,`provider_id` integer NOT NULL,`provider_user_id` varchar(256) NOT NULL,`created_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `user_subscriptions` (`id` integer,`user_id` integer,`plan_id` integer,`amount_total` bigint NOT NULL DEFAULT 0,`amount_used` bigint NOT NULL DEFAULT 0,`start_time` integer,`end_time` integer,`status` varchar(32),`source` varchar(32) DEFAULT "order",`last_reset_time` bigint DEFAULT 0,`next_reset_time` bigint DEFAULT 0,`upgrade_group` varchar(64) DEFAULT "",`prev_user_group` varchar(64) DEFAULT "",`downgrade_group` varchar(64) DEFAULT "",`allow_wallet_overflow` numeric NOT NULL,`created_at` integer,`updated_at` integer,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `users` (`id` integer,`username` text UNIQUE,`password` text NOT NULL,`display_name` text,`role` integer DEFAULT 1,`status` integer DEFAULT 1,`email` text,`normalized_email` text,`github_id` text,`discord_id` text,`oidc_id` text,`wechat_id` text,`telegram_id` text,`access_token` char(32),`quota` bigint DEFAULT 0,`used_quota` bigint DEFAULT 0,`request_count` integer DEFAULT 0,`group` varchar(64) DEFAULT "default",`aff_code` varchar(32),`aff_count` integer DEFAULT 0,`aff_quota` bigint DEFAULT 0,`aff_history` bigint DEFAULT 0,`inviter_id` integer,`deleted_at` datetime,`linux_do_id` text,`setting` text,`remark` varchar(255),`stripe_customer` varchar(64),`created_at` integer,`last_login_at` integer DEFAULT 0,`session_generation` integer NOT NULL DEFAULT 0,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE TABLE `vendors` (`id` integer,`name` text NOT NULL,`name_key` text NOT NULL DEFAULT "",`description` text,`icon` varchar(128),`status` integer DEFAULT 1,`created_time` integer,`updated_time` integer,`deleted_at` datetime,PRIMARY KEY (`id`)); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_abilities_channel_id` ON `abilities`(`channel_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_abilities_priority` ON `abilities`(`priority`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_abilities_tag` ON `abilities`(`tag`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_abilities_weight` ON `abilities`(`weight`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_auth_flow_purpose_expiry` ON `auth_flows`(`purpose`,`expires_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_auth_flows_consumed_at` ON `auth_flows`(`consumed_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_auth_flows_session_id` ON `auth_flows`(`session_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_auth_flows_token_hash` ON `auth_flows`(`token_hash`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_auth_flows_user_id` ON `auth_flows`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_authz_user_overrides_user_id` ON `authz_user_overrides`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_billing_log_receipts_operation_key` ON `billing_log_receipts`(`operation_key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_billing_pre_consume_selections_request_id` ON `billing_pre_consume_selections`(`request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlement_admission` ON `billing_settlements`(`user_id`,`status`,`funding_delta`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlement_reconciliation` ON `billing_settlements`(`status`,`funding_delta`,`reconciliation_reviewed_at`,`user_blocking_override`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlement_status_funding` ON `billing_settlements`(`status`,`funding_delta`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlements_effect_status` ON `billing_settlements`(`effect_status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlements_next_attempt` ON `billing_settlements`(`next_attempt`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_billing_settlements_operation_key` ON `billing_settlements`(`operation_key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlements_pre_consume_request_id` ON `billing_settlements`(`pre_consume_request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlements_status` ON `billing_settlements`(`status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlements_subscription_pre_consume_request_id` ON `billing_settlements`(`subscription_pre_consume_request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_billing_settlements_updated_at` ON `billing_settlements`(`updated_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_cache_invalidation_target` ON `cache_invalidation_tasks`(`kind`,`entity_key`,`delete_entry`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_cache_invalidation_tasks_next_attempt` ON `cache_invalidation_tasks`(`next_attempt`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_channels_name` ON `channels`(`name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_channels_tag` ON `channels`(`tag`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_created_at_id` ON `logs`(`created_at`,`id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_created_at_type` ON `logs`(`created_at`,`type`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_custom_oauth_providers_slug` ON `custom_oauth_providers`(`slug`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_channel_id` ON `logs`(`channel_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_group` ON `logs`(`group`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_ip` ON `logs`(`ip`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_is_empty_retry` ON `logs`(`is_empty_retry`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_is_error_retry` ON `logs`(`is_error_retry`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_is_retry` ON `logs`(`is_retry`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_model_name` ON `logs`(`model_name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_quota` ON `logs`(`quota`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_request_id` ON `logs`(`request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_token_id` ON `logs`(`token_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_token_name` ON `logs`(`token_name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_type_quota_created_at` ON `logs`(`type`,`quota`,`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_upstream_request_id` ON `logs`(`upstream_request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_user_id` ON `logs`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_logs_username` ON `logs`(`username`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_midjourney_billing_claim` ON `midjourney_billing_claims`(`channel_id`,`mj_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourney_billing_claims_billing_request_id` ON `midjourney_billing_claims`(`billing_request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourney_billing_claims_billing_task_id` ON `midjourney_billing_claims`(`billing_task_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourney_billing_claims_user_id` ON `midjourney_billing_claims`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_action` ON `midjourneys`(`action`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_finish_time` ON `midjourneys`(`finish_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_mj_id` ON `midjourneys`(`mj_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_progress` ON `midjourneys`(`progress`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_start_time` ON `midjourneys`(`start_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_status` ON `midjourneys`(`status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_submit_time` ON `midjourneys`(`submit_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_midjourneys_user_id` ON `midjourneys`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_models_deleted_at` ON `models`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_models_vendor_id` ON `models`(`vendor_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_passkey_credentials_credential_id` ON `passkey_credentials`(`credential_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_passkey_credentials_deleted_at` ON `passkey_credentials`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_passkey_credentials_user_id` ON `passkey_credentials`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_perf_bucket_ts` ON `perf_metrics`(`bucket_ts`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_perf_metric_flush_receipts_created_at` ON `perf_metric_flush_receipts`(`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_perf_metric_flush_receipts_receipt_key` ON `perf_metric_flush_receipts`(`receipt_key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_perf_model_group_bucket` ON `perf_metrics`(`model_name`,`group`,`bucket_ts`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_prefill_groups_deleted_at` ON `prefill_groups`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_prefill_groups_type` ON `prefill_groups`(`type`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_qdt_created_at` ON `quota_data`(`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_qdt_model_user_name` ON `quota_data`(`model_name`,`username`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_channel_id` ON `quota_data`(`channel_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_node_name` ON `quota_data`(`node_name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_snapshot_retries_retry_until` ON `quota_data_snapshot_retries`(`retry_until`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_snapshot_retries_updated_at` ON `quota_data_snapshot_retries`(`updated_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_snapshots_created_at` ON `quota_data_snapshots`(`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_token_id` ON `quota_data`(`token_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_use_group` ON `quota_data`(`use_group`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_quota_data_user_id` ON `quota_data`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_redemptions_deleted_at` ON `redemptions`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_redemptions_key` ON `redemptions`(`key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_redemptions_name` ON `redemptions`(`name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_orders_plan_id` ON `subscription_orders`(`plan_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_orders_trade_no` ON `subscription_orders`(`trade_no`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_orders_user_id` ON `subscription_orders`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_subscription_pre_consume_records_request_id` ON `subscription_pre_consume_records`(`request_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_pre_consume_records_status` ON `subscription_pre_consume_records`(`status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_pre_consume_records_token_id` ON `subscription_pre_consume_records`(`token_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_pre_consume_records_updated_at` ON `subscription_pre_consume_records`(`updated_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_pre_consume_records_user_id` ON `subscription_pre_consume_records`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_subscription_pre_consume_records_user_subscription_id` ON `subscription_pre_consume_records`(`user_subscription_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_instances_created_at` ON `system_instances`(`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_instances_last_seen_at` ON `system_instances`(`last_seen_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_instances_started_at` ON `system_instances`(`started_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_instances_updated_at` ON `system_instances`(`updated_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_system_tasks_active_key` ON `system_tasks`(`active_key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_tasks_created_at` ON `system_tasks`(`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_tasks_locked_by` ON `system_tasks`(`locked_by`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_tasks_locked_until` ON `system_tasks`(`locked_until`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_tasks_status` ON `system_tasks`(`status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_system_tasks_task_id` ON `system_tasks`(`task_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_tasks_type` ON `system_tasks`(`type`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_system_tasks_updated_at` ON `system_tasks`(`updated_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_task_timeout_cursor` ON `tasks`(`submit_time`,`id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_action` ON `tasks`(`action`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_channel_id` ON `tasks`(`channel_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_created_at` ON `tasks`(`created_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_finish_time` ON `tasks`(`finish_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_platform` ON `tasks`(`platform`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_progress` ON `tasks`(`progress`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_start_time` ON `tasks`(`start_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_status` ON `tasks`(`status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_submit_time` ON `tasks`(`submit_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_task_id` ON `tasks`(`task_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tasks_user_id` ON `tasks`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tokens_deleted_at` ON `tokens`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_tokens_key` ON `tokens`(`key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tokens_name` ON `tokens`(`name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_tokens_user_id` ON `tokens`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_top_ups_trade_no` ON `top_ups`(`trade_no`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_top_ups_user_id` ON `top_ups`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_two_fa_backup_codes_deleted_at` ON `two_fa_backup_codes`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_two_fa_backup_codes_user_id` ON `two_fa_backup_codes`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_two_fas_deleted_at` ON `two_fas`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_two_fas_user_id` ON `two_fas`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_user_checkin_date` ON `checkins`(`user_id`,`checkin_date`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_id_id` ON `logs`(`user_id`,`id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_sub_active` ON `user_subscriptions`(`user_id`,`status`,`end_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_subscriptions_end_time` ON `user_subscriptions`(`end_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_subscriptions_next_reset_time` ON `user_subscriptions`(`next_reset_time`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_subscriptions_plan_id` ON `user_subscriptions`(`plan_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_subscriptions_status` ON `user_subscriptions`(`status`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_user_subscriptions_user_id` ON `user_subscriptions`(`user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_users_access_token` ON `users`(`access_token`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `idx_users_aff_code` ON `users`(`aff_code`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_discord_id` ON `users`(`discord_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_display_name` ON `users`(`display_name`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_email` ON `users`(`email`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_git_hub_id` ON `users`(`github_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_inviter_id` ON `users`(`inviter_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_linux_do_id` ON `users`(`linux_do_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_normalized_email` ON `users`(`normalized_email`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_oidc_id` ON `users`(`oidc_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_stripe_customer` ON `users`(`stripe_customer`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_telegram_id` ON `users`(`telegram_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_username` ON `users`(`username`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_users_we_chat_id` ON `users`(`wechat_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `idx_vendors_deleted_at` ON `vendors`(`deleted_at`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE INDEX `index_username_model_name` ON `logs`(`model_name`,`username`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `uk_models_active_name_key` ON `models` (`name_key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `uk_prefill_name` ON `prefill_groups`(`name`) WHERE deleted_at IS NULL; + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `uk_vendors_active_name_key` ON `vendors` (`name_key`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_authz_user_permission` ON `authz_user_overrides`(`user_id`,`resource`,`action`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_provider_userid` ON `user_oauth_bindings`(`provider_id`,`provider_user_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_user_provider` ON `user_oauth_bindings`(`user_id`,`provider_id`); + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_users_discord_id` ON `users` (`discord_id`) WHERE `discord_id` IS NOT NULL AND `discord_id` <> ''; + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_users_github_id` ON `users` (`github_id`) WHERE `github_id` IS NOT NULL AND `github_id` <> ''; + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_users_linux_do_id` ON `users` (`linux_do_id`) WHERE `linux_do_id` IS NOT NULL AND `linux_do_id` <> ''; + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_users_oidc_id` ON `users` (`oidc_id`) WHERE `oidc_id` IS NOT NULL AND `oidc_id` <> ''; + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_users_telegram_id` ON `users` (`telegram_id`) WHERE `telegram_id` IS NOT NULL AND `telegram_id` <> ''; + +-- MAX_API_SQLITE_SCHEMA_STATEMENT +CREATE UNIQUE INDEX `ux_users_wechat_id` ON `users` (`wechat_id`) WHERE `wechat_id` IS NOT NULL AND `wechat_id` <> '';