mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 18:26:37 +08:00
fix(backend): migrate PostgreSQL traffic columns from int4 to bigint (#290)
## Summary - Widens legacy PostgreSQL traffic/quota columns from `integer` to `bigint` to prevent int4 overflow - Fixes federation share creation failure when traffic limits exceed 2GB (e.g., `536870912000` bytes = 500GB) - Bumps schema version from 4 to 5 with auto-migration on backend startup ## Affected Tables - `user`: `flow`, `in_flow`, `out_flow` - `forward`: `in_flow`, `out_flow` - `statistics_flow`: `flow`, `total_flow` - `tunnel`: `flow` - `user_tunnel`: `flow`, `in_flow`, `out_flow` - `peer_share`: `max_bandwidth`, `current_flow` ## Test Plan - ✅ Unit tests added for migration execution and error handling - ✅ Contract tests passed - ✅ Repository tests passed Commands: ```bash cd go-backend && go test ./internal/store/repo/... cd go-backend && go test ./tests/contract/... ```
This commit is contained in:
@@ -2612,11 +2612,12 @@ func (r *Repository) GetUserTunnelByID(id int64) (*model.UserTunnel, error) {
|
|||||||
|
|
||||||
// ─── Migration ───────────────────────────────────────────────────────
|
// ─── Migration ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const currentSchemaVersion = 4
|
const currentSchemaVersion = 5
|
||||||
|
|
||||||
var ensurePostgresIDDefaultsFn = ensurePostgresIDDefaults
|
var ensurePostgresIDDefaultsFn = ensurePostgresIDDefaults
|
||||||
var migrateViteConfigValueColumnTypeFn = migrateViteConfigValueColumnType
|
var migrateViteConfigValueColumnTypeFn = migrateViteConfigValueColumnType
|
||||||
var migrateSpeedLimitTunnelBindingFn = migrateSpeedLimitTunnelBinding
|
var migrateSpeedLimitTunnelBindingFn = migrateSpeedLimitTunnelBinding
|
||||||
|
var migratePostgresTrafficInt64ColumnsFn = migratePostgresTrafficInt64Columns
|
||||||
|
|
||||||
func getSchemaVersion(db *gorm.DB) int {
|
func getSchemaVersion(db *gorm.DB) int {
|
||||||
var v model.SchemaVersion
|
var v model.SchemaVersion
|
||||||
@@ -2680,6 +2681,12 @@ func migrateSchema(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ver < 5 {
|
||||||
|
if err := migratePostgresTrafficInt64ColumnsFn(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSchemaVersion(db, currentSchemaVersion)
|
setSchemaVersion(db, currentSchemaVersion)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -2744,6 +2751,87 @@ func migrateSpeedLimitTunnelBinding(db *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migratePostgresTrafficInt64Columns(db *gorm.DB) error {
|
||||||
|
if db == nil {
|
||||||
|
return errors.New("nil db")
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.Dialector.Name() != "postgres" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type trafficColumn struct {
|
||||||
|
TableName string
|
||||||
|
ColumnName string
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := []trafficColumn{
|
||||||
|
{TableName: "user", ColumnName: "flow"},
|
||||||
|
{TableName: "user", ColumnName: "in_flow"},
|
||||||
|
{TableName: "user", ColumnName: "out_flow"},
|
||||||
|
{TableName: "forward", ColumnName: "in_flow"},
|
||||||
|
{TableName: "forward", ColumnName: "out_flow"},
|
||||||
|
{TableName: "statistics_flow", ColumnName: "flow"},
|
||||||
|
{TableName: "statistics_flow", ColumnName: "total_flow"},
|
||||||
|
{TableName: "tunnel", ColumnName: "flow"},
|
||||||
|
{TableName: "user_tunnel", ColumnName: "flow"},
|
||||||
|
{TableName: "user_tunnel", ColumnName: "in_flow"},
|
||||||
|
{TableName: "user_tunnel", ColumnName: "out_flow"},
|
||||||
|
{TableName: "peer_share", ColumnName: "max_bandwidth"},
|
||||||
|
{TableName: "peer_share", ColumnName: "current_flow"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, column := range columns {
|
||||||
|
if err := alterPostgresColumnToBigIntIfNeeded(db, column.TableName, column.ColumnName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func alterPostgresColumnToBigIntIfNeeded(db *gorm.DB, tableName, columnName string) error {
|
||||||
|
if db == nil {
|
||||||
|
return errors.New("nil db")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tableName == "" || columnName == "" {
|
||||||
|
return errors.New("empty table or column name")
|
||||||
|
}
|
||||||
|
|
||||||
|
type columnRow struct {
|
||||||
|
DataType string `gorm:"column:data_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var row columnRow
|
||||||
|
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(&row).Error; err != nil {
|
||||||
|
return fmt.Errorf("inspect %s.%s type: %w", tableName, columnName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.DataType == "" || strings.EqualFold(row.DataType, "bigint") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(row.DataType, "integer") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Exec(fmt.Sprintf(
|
||||||
|
"ALTER TABLE %s ALTER COLUMN %s TYPE BIGINT",
|
||||||
|
quoteSQLIdentifier(tableName),
|
||||||
|
quoteSQLIdentifier(columnName),
|
||||||
|
)).Error; err != nil {
|
||||||
|
return fmt.Errorf("alter %s.%s to bigint: %w", tableName, columnName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func ensurePostgresIDDefaults(db *gorm.DB) error {
|
func ensurePostgresIDDefaults(db *gorm.DB) error {
|
||||||
if db.Dialector.Name() != "postgres" {
|
if db.Dialector.Name() != "postgres" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
gsqlite "github.com/glebarez/sqlite"
|
gsqlite "github.com/glebarez/sqlite"
|
||||||
@@ -250,3 +251,115 @@ func TestMigrateSchemaClearsSpeedLimitTunnelBinding(t *testing.T) {
|
|||||||
t.Fatalf("expected schema version %d, got %d", currentSchemaVersion, schemaVersion)
|
t.Fatalf("expected schema version %d, got %d", currentSchemaVersion, schemaVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMigrateSchemaRunsTrafficInt64MigrationForLegacySchema(t *testing.T) {
|
||||||
|
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
if sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := db.Exec(`CREATE TABLE schema_version (version INTEGER NOT NULL DEFAULT 0)`).Error; err != nil {
|
||||||
|
t.Fatalf("create schema_version: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`INSERT INTO schema_version(version) VALUES(?)`, 4).Error; err != nil {
|
||||||
|
t.Fatalf("seed schema_version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalIDRepair := ensurePostgresIDDefaultsFn
|
||||||
|
ensurePostgresIDDefaultsFn = func(db *gorm.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ensurePostgresIDDefaultsFn = originalIDRepair
|
||||||
|
})
|
||||||
|
|
||||||
|
called := 0
|
||||||
|
originalMigrate := migratePostgresTrafficInt64ColumnsFn
|
||||||
|
migratePostgresTrafficInt64ColumnsFn = func(db *gorm.DB) error {
|
||||||
|
called++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
migratePostgresTrafficInt64ColumnsFn = originalMigrate
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := migrateSchema(db); err != nil {
|
||||||
|
t.Fatalf("migrateSchema: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if called != 1 {
|
||||||
|
t.Fatalf("expected traffic bigint migration to run once, got %d", called)
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemaVersion int
|
||||||
|
if err := db.Raw(`SELECT version FROM schema_version LIMIT 1`).Row().Scan(&schemaVersion); err != nil {
|
||||||
|
t.Fatalf("query schema_version: %v", err)
|
||||||
|
}
|
||||||
|
if schemaVersion != currentSchemaVersion {
|
||||||
|
t.Fatalf("expected schema version %d, got %d", currentSchemaVersion, schemaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateSchemaReturnsTrafficInt64MigrationError(t *testing.T) {
|
||||||
|
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
if sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := db.Exec(`CREATE TABLE schema_version (version INTEGER NOT NULL DEFAULT 0)`).Error; err != nil {
|
||||||
|
t.Fatalf("create schema_version: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`INSERT INTO schema_version(version) VALUES(?)`, 4).Error; err != nil {
|
||||||
|
t.Fatalf("seed schema_version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalIDRepair := ensurePostgresIDDefaultsFn
|
||||||
|
ensurePostgresIDDefaultsFn = func(db *gorm.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ensurePostgresIDDefaultsFn = originalIDRepair
|
||||||
|
})
|
||||||
|
|
||||||
|
wantErr := errors.New("traffic bigint migration failed")
|
||||||
|
originalMigrate := migratePostgresTrafficInt64ColumnsFn
|
||||||
|
migratePostgresTrafficInt64ColumnsFn = func(db *gorm.DB) error {
|
||||||
|
return wantErr
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
migratePostgresTrafficInt64ColumnsFn = originalMigrate
|
||||||
|
})
|
||||||
|
|
||||||
|
err = migrateSchema(db)
|
||||||
|
if !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("expected error %v, got %v", wantErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlterPostgresColumnToBigIntIfNeededValidatesNames(t *testing.T) {
|
||||||
|
if err := alterPostgresColumnToBigIntIfNeeded(nil, "peer_share", "max_bandwidth"); err == nil || !strings.Contains(err.Error(), "nil db") {
|
||||||
|
t.Fatalf("expected nil db error, got %v", err)
|
||||||
|
}
|
||||||
|
if err := alterPostgresColumnToBigIntIfNeeded(&gorm.DB{}, "", "max_bandwidth"); err == nil || !strings.Contains(err.Error(), "empty table or column name") {
|
||||||
|
t.Fatalf("expected empty name error, got %v", err)
|
||||||
|
}
|
||||||
|
if err := alterPostgresColumnToBigIntIfNeeded(&gorm.DB{}, "peer_share", ""); err == nil || !strings.Contains(err.Error(), "empty table or column name") {
|
||||||
|
t.Fatalf("expected empty name error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 019 Federation Share Traffic Bigint Migration
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [x] Inspect federation share creation failure and identify the PostgreSQL `int4` overflow source.
|
||||||
|
- [x] Audit other traffic-related legacy PostgreSQL columns that may still be `integer` despite Go models using `int64`.
|
||||||
|
- [x] Add a schema migration that widens legacy traffic/quota columns from `integer` to `bigint`.
|
||||||
|
- [x] Add migration tests covering the new schema version branch and error propagation.
|
||||||
|
- [x] Run focused backend verification for the migration changes.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The reported failing value `536870912000` is 500 GiB in bytes and overflows PostgreSQL `int4`.
|
||||||
|
- The fix widens historical PostgreSQL traffic columns in `user`, `forward`, `statistics_flow`, `tunnel`, `user_tunnel`, and `peer_share` to `BIGINT` when needed.
|
||||||
|
|
||||||
|
## Test Record
|
||||||
|
|
||||||
|
- Command: `cd go-backend && go test ./internal/store/repo/...`
|
||||||
|
- Result: passed.
|
||||||
|
- Command: `cd go-backend && go test ./tests/contract/...`
|
||||||
|
- Result: passed.
|
||||||
Reference in New Issue
Block a user