Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var (
errOriginalPasswordFail = errors.New("original password is incorrect")
)

const maxUserQuotaValue = 1<<31 - 1
const maxUserQuotaValue = int64(^uint64(0) >> 1)

func Login(c *gin.Context) {
if !common.PasswordLoginEnabled {
Expand Down Expand Up @@ -996,14 +996,14 @@ type ManageRequest struct {
}

func isValidQuotaOverride(value int) bool {
return value >= 0 && value <= maxUserQuotaValue
return value >= 0 && int64(value) <= maxUserQuotaValue
}

func isValidQuotaAddition(current int, delta int) bool {
if delta <= 0 || delta > maxUserQuotaValue {
if delta <= 0 || int64(delta) > maxUserQuotaValue {
return false
}
return current <= maxUserQuotaValue-delta
return int64(current) <= maxUserQuotaValue-int64(delta)
}

// ManageUser Only admin user can do this
Expand Down
11 changes: 6 additions & 5 deletions controller/user_setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,14 @@ func TestRegisterConsumesEmailVerificationCode(t *testing.T) {
}

func TestQuotaBoundsValidation(t *testing.T) {
legacyInt32Max := 1<<31 - 1
aboveLegacyInt32Max := legacyInt32Max + 1

require.True(t, isValidQuotaOverride(0))
require.True(t, isValidQuotaOverride(maxUserQuotaValue))
require.True(t, isValidQuotaOverride(aboveLegacyInt32Max))
require.False(t, isValidQuotaOverride(-1))
require.False(t, isValidQuotaOverride(maxUserQuotaValue+1))

require.True(t, isValidQuotaAddition(maxUserQuotaValue-1, 1))
require.False(t, isValidQuotaAddition(maxUserQuotaValue, 1))
require.False(t, isValidQuotaAddition(0, maxUserQuotaValue+1))
require.True(t, isValidQuotaAddition(aboveLegacyInt32Max, 1))
require.True(t, isValidQuotaAddition(0, aboveLegacyInt32Max))
require.False(t, isValidQuotaAddition(0, 0))
}
Comment on lines 219 to 230

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't exercise the actual int64-max boundary or overflow protection.

The updated assertions only prove values beyond the old int32 limit are now accepted; they don't cover the new overflow-safe subtraction check (current <= maxUserQuotaValue-delta) near the real maxUserQuotaValue boundary, which is the actual correctness improvement in isValidQuotaAddition.

✅ Suggested additional boundary cases
 	require.True(t, isValidQuotaAddition(aboveLegacyInt32Max, 1))
 	require.True(t, isValidQuotaAddition(0, aboveLegacyInt32Max))
 	require.False(t, isValidQuotaAddition(0, 0))
+	require.True(t, isValidQuotaOverride(int(maxUserQuotaValue)))
+	require.False(t, isValidQuotaAddition(int(maxUserQuotaValue), 1))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestQuotaBoundsValidation(t *testing.T) {
legacyInt32Max := 1<<31 - 1
aboveLegacyInt32Max := legacyInt32Max + 1
require.True(t, isValidQuotaOverride(0))
require.True(t, isValidQuotaOverride(maxUserQuotaValue))
require.True(t, isValidQuotaOverride(aboveLegacyInt32Max))
require.False(t, isValidQuotaOverride(-1))
require.False(t, isValidQuotaOverride(maxUserQuotaValue+1))
require.True(t, isValidQuotaAddition(maxUserQuotaValue-1, 1))
require.False(t, isValidQuotaAddition(maxUserQuotaValue, 1))
require.False(t, isValidQuotaAddition(0, maxUserQuotaValue+1))
require.True(t, isValidQuotaAddition(aboveLegacyInt32Max, 1))
require.True(t, isValidQuotaAddition(0, aboveLegacyInt32Max))
require.False(t, isValidQuotaAddition(0, 0))
}
func TestQuotaBoundsValidation(t *testing.T) {
legacyInt32Max := 1<<31 - 1
aboveLegacyInt32Max := legacyInt32Max + 1
require.True(t, isValidQuotaOverride(0))
require.True(t, isValidQuotaOverride(aboveLegacyInt32Max))
require.False(t, isValidQuotaOverride(-1))
require.True(t, isValidQuotaAddition(aboveLegacyInt32Max, 1))
require.True(t, isValidQuotaAddition(0, aboveLegacyInt32Max))
require.False(t, isValidQuotaAddition(0, 0))
require.True(t, isValidQuotaOverride(int(maxUserQuotaValue)))
require.False(t, isValidQuotaAddition(int(maxUserQuotaValue), 1))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/user_setting_test.go` around lines 219 - 230, Extend
TestQuotaBoundsValidation to cover the actual maxUserQuotaValue boundary: verify
isValidQuotaOverride accepts the maximum and rejects values above it, and verify
isValidQuotaAddition accepts an exact-to-maximum sum while rejecting additions
that would exceed the maximum, including a case with a large delta that would
previously risk overflow. Keep the existing int32-boundary assertions.

55 changes: 55 additions & 0 deletions model/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ func migrateDB() error {
if err := migrateTokenModelLimitsToText(); err != nil {
return err
}
if err := migrateUserQuotaColumnsToBigInt(); err != nil {
return err
}

err := DB.AutoMigrate(
&Channel{},
Expand Down Expand Up @@ -331,6 +334,9 @@ func migrateDB() error {
}

func migrateDBFast() error {
if err := migrateUserQuotaColumnsToBigInt(); err != nil {
return err
}

var wg sync.WaitGroup

Expand Down Expand Up @@ -660,6 +666,55 @@ func migrateTokenModelLimitsToText() error {
return nil
}

func migrateUserQuotaColumnsToBigInt() error {
if DB == nil || common.UsingSQLite || !DB.Migrator().HasTable(&User{}) {
return nil
}

tableName := "users"
columnNames := []string{"quota", "used_quota", "aff_quota", "aff_history"}

for _, columnName := range columnNames {
if !DB.Migrator().HasColumn(&User{}, columnName) {
continue
}

var alterSQL string
if common.UsingPostgreSQL {
var dataType string
if err := DB.Raw(`SELECT data_type FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = ? AND column_name = ?`,
tableName, columnName).Scan(&dataType).Error; err != nil {
common.SysLog(fmt.Sprintf("Warning: failed to query metadata for %s.%s: %v", tableName, columnName, err))
} else if strings.EqualFold(dataType, "bigint") {
continue
}
alterSQL = fmt.Sprintf(`ALTER TABLE "%s" ALTER COLUMN "%s" TYPE bigint USING "%s"::bigint`,
tableName, columnName, columnName)
} else if common.UsingMySQL {
var columnType string
if err := DB.Raw(`SELECT COLUMN_TYPE FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?`,
tableName, columnName).Scan(&columnType).Error; err != nil {
common.SysLog(fmt.Sprintf("Warning: failed to query metadata for %s.%s: %v", tableName, columnName, err))
} else if strings.HasPrefix(strings.ToLower(columnType), "bigint") {
continue
}
alterSQL = fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` bigint DEFAULT 0", tableName, columnName)
Comment on lines +694 to +703

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- model/main.go around 660-730 ---'
sed -n '660,730p' model/main.go

echo
echo '--- search quota-related definitions/usages ---'
rg -n "quota|used_quota|aff_quota|aff_history" model . --glob '!**/vendor/**' --glob '!**/node_modules/**'

Repository: MAX-API-Next/MAX-API

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- model/user.go struct around quota fields ---'
sed -n '50,85p' model/user.go

echo
echo '--- search for user table migrations / schema creation ---'
rg -n "AutoMigrate|CreateTable|quota.*not null|used_quota.*not null|aff_quota.*not null|aff_history.*not null|type:bigint;default:0" model --glob '!**/vendor/**' --glob '!**/node_modules/**'

Repository: MAX-API-Next/MAX-API

Length of output: 4935


🌐 Web query:

GORM v2 default tag MySQL AutoMigrate nullable NOT NULL integer field behavior

💡 Result:

In GORM v2, AutoMigrate handles integer field nullable/NOT NULL constraints based on the Go struct type definition and GORM tags [1][2][3]. 1. Field Nullability: By default, Go primitive types (e.g., int, int64) are treated as non-nullable [3]. To make an integer field nullable, use a pointer type (e.g., *int) or sql.NullInt64 [4][5][3]. 2. NOT NULL Constraint: Use the gorm:"not null" tag to explicitly enforce the NOT NULL constraint [6][3]. 3. AutoMigrate Behavior: - GORM v2's AutoMigrate will automatically add or remove the NOT NULL constraint if you change the tag in your Go struct [1][2]. - If you change a column from non-nullable to nullable, GORM will typically issue an ALTER TABLE statement [1][6]. - If you change a column from nullable to non-nullable, the migration may fail if the database already contains NULL values in that column [6]. - Note that AutoMigrate logic for constraints has historically had complexities; GORM specifically checks for tag settings and compares them against existing database schema metadata [7]. 4. Default Tag: Using gorm:"default:..." defines the database-level default value [3]. Be cautious with gorm:"default:null", as it has been reported in some versions/dialects to cause unexpected behavior like repeated table recreations or issues with value handling during persistence [8][9]. For robust handling of nullable integer fields, using pointer types or sql.NullInt64 is the recommended approach to ensure GORM correctly maps the field to a database-level NULL [4][5][3].

Citations:


Preserve the existing NOT NULL constraint in the MySQL migration. ALTER TABLE ... MODIFY COLUMN ... bigint DEFAULT 0 restates only the type/default, so MySQL can drop NOT NULL and any other omitted attributes. Use bigint NOT NULL DEFAULT 0 here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/main.go` around lines 694 - 703, Update the MySQL ALTER TABLE statement
in the UsingMySQL migration branch to explicitly preserve the column constraint
by changing the generated definition to bigint NOT NULL DEFAULT 0. Keep the
existing table and column identifier handling unchanged.

}

if alterSQL == "" {
continue
}
if err := DB.Exec(alterSQL).Error; err != nil {
return fmt.Errorf("failed to migrate %s.%s to bigint: %w", tableName, columnName, err)
}
common.SysLog(fmt.Sprintf("Successfully migrated %s.%s to bigint", tableName, columnName))
}

return nil
}

Comment on lines +669 to +717

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Raw-SQL pre-migration for a large users table can be slow/locking.

MODIFY COLUMN/ALTER COLUMN TYPE on a large users table can hold a table-level lock for a noticeable duration on MySQL/Postgres during startup migration, delaying app boot. Worth noting for operators with large user tables (consider off-peak deploys or an online-DDL tool for very large tables).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/main.go` around lines 669 - 717, Document the startup migration’s
locking and duration impact for large users tables in
migrateUserQuotaColumnsToBigInt, advising operators to run deployments during
off-peak periods and consider an online-DDL approach for very large tables; do
not change the migration SQL behavior.

// migrateSubscriptionPlanPriceAmount migrates price_amount column from float/double to decimal(10,6)
// This is safe to run multiple times - it checks the column type first
func migrateSubscriptionPlanPriceAmount() {
Expand Down
10 changes: 5 additions & 5 deletions model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ type User struct {
TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"`
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
AccessToken *string `json:"-" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
Quota int `json:"quota" gorm:"type:int;default:0"`
UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Quota int `json:"quota" gorm:"type:bigint;default:0"`
UsedQuota int `json:"used_quota" gorm:"type:bigint;default:0;column:used_quota"` // used quota
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Group string `json:"group" gorm:"type:varchar(64);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
AffCount int `json:"aff_count" gorm:"type:int;default:0;column:aff_count"`
AffQuota int `json:"aff_quota" gorm:"type:int;default:0;column:aff_quota"` // 邀请剩余额度
AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度
AffQuota int `json:"aff_quota" gorm:"type:bigint;default:0;column:aff_quota"` // 邀请剩余额度
AffHistoryQuota int `json:"aff_history_quota" gorm:"type:bigint;default:0;column:aff_history"` // 邀请历史额度
Comment on lines +67 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Assess ripple effect of widening quota fields to int64
rg -nP '\.(Quota|UsedQuota|AffQuota|AffHistoryQuota)\b' --type=go -C2 | rg -v '_test\.go'

Repository: MAX-API-Next/MAX-API

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## model/user.go context\n'
sed -n '1,160p' model/user.go

printf '\n## quota-related symbols in Go files\n'
rg -n --glob '*.go' '\b(Quota|UsedQuota|AffQuota|AffHistoryQuota)\b|isValidQuota(Override|Addition)|quota' model controller service pkg -C 2

Repository: MAX-API-Next/MAX-API

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,160p' model/user.go

Repository: MAX-API-Next/MAX-API

Length of output: 7113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'FILE: model/user.go\n'
cat -n model/user.go | sed -n '55,90p'

printf '\nUSAGES:\n'
rg -n --glob '*.go' '\b(Quota|UsedQuota|AffQuota|AffHistoryQuota)\b|isValidQuota(Override|Addition)' .

Repository: MAX-API-Next/MAX-API

Length of output: 22787


Use int64 for the quota fields backed by bigint. Quota, UsedQuota, AffQuota, and AffHistoryQuota are still int, so the model only carries the full bigint range on 64-bit builds. If these values need to exceed 32-bit limits, widen the user quota path together, including the quota validation helpers in controller/user.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/user.go` around lines 67 - 74, Change User fields Quota, UsedQuota,
AffQuota, and AffHistoryQuota from int to int64 to match their bigint database
columns, then update related quota validation and arithmetic helpers in
controller/user.go to use int64 consistently, including parameters, local
variables, comparisons, and return values.

InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
Expand Down
Loading