MHSanaei/3x-ui · critical

copy %T: %w

Error message

copy %T: %w

What it means

Thrown when copyTable fails for one of the migrationModels() while inserting into PostgreSQL inside the transaction (find reads run on src, CreateInBatches writes on tx). copyTable streams source rows in 500-row batches and inserts them with explicit ids, so this wraps whichever batch hit the first constraint, type, or connection error. The failure aborts and rolls back the entire copy, including the earlier table clears.

Source

Thrown at internal/database/migrate_data.go:130

		// client_traffics rows whose inbound was deleted. Drop it here too so copying
		// such orphaned rows can't fail with an fk_inbounds_client_stats violation.
		if err := tx.Exec("ALTER TABLE client_traffics DROP CONSTRAINT IF EXISTS fk_inbounds_client_stats").Error; err != nil {
			return fmt.Errorf("drop legacy foreign key: %w", err)
		}

		// Empty the destination tables before copying: a fresh PostgreSQL DB
		// already holds an auto-seeded admin (id=1) from any prior panel start,
		// so a plain INSERT with explicit ids would collide on users_pkey. Only
		// the panel's own tables are cleared, and a failure anywhere in this
		// transaction rolls the clear back with everything else.
		if err := truncatePostgresTables(tx, migrationModels()); err != nil {
			return fmt.Errorf("clear destination tables: %w", err)
		}

		for _, m := range migrationModels() {
			n, err := copyTable(src, tx, m)
			if err != nil {
				return fmt.Errorf("copy %T: %w", m, err)
			}
			totalRows += n
			log.Printf("  %-32s %d rows", reflect.TypeOf(m).Elem().Name(), n)
		}
		return nil
	})
	if txErr != nil {
		return txErr
	}

	// setval is never rolled back by PostgreSQL, so sequences are resynced only
	// after the transaction has committed.
	if err := resetPostgresSequences(dst); err != nil {
		log.Printf("warning: failed to reset some postgres sequences: %v", err)
	}

	log.Printf("Migration complete: %d rows across %d tables.", totalRows, len(migrationModels()))
	log.Println("Set XUI_DB_TYPE=postgres and XUI_DB_DSN=... in /etc/default/x-ui, then restart x-ui.")

View on GitHub (pinned to ad32144c42)

Solutions

  1. Unwrap the %w chain — the model %T and the Postgres SQLSTATE identify the offending table and constraint.
  2. Inspect the failing table's rows on the SQLite source for NULLs in now-required columns or duplicate values on unique indexes; fix or drop them at the source and re-run (the migration is idempotent: it re-clears and re-copies).
  3. Drop user-added FK/CHECK constraints on the destination that reference panel tables but are not part of the panel schema.
  4. For timeouts, raise statement_timeout/lock_timeout for the migration session or batch-clean the source.
Defensive patterns

Strategy: try-catch

Try / catch

err := RunSQLiteToPostgres(srcPath, dstDSN)
if err != nil {
    var pk *pgconn.PgError
    if errors.As(err, &pk) && pk.Code == pgerrcode.ForeignKeyViolation {
        // a non-panel FK references a migrated table: drop it on dst and re-run
    }
    // any failure rolled back the whole tx — destination unchanged, safe to re-run
}

Prevention

When it happens

Trigger: A destination FK referencing a panel table from OUTSIDE migrationModels() rejecting copied rows; a NOT NULL/CHECK/unique constraint on dst stricter than the SQLite data; a value SQLite stored loosely (e.g. oversized text, invalid UTF-8) rejected by Postgres; statement/network failure mid-copy; a serial sequence clash on unique columns other than id.

Common situations: Migrating a DB whose schema was hand-modified or extended with extra FKs; source backups from very old panel versions holding rows that violate newer constraints; copying millions of rows and hitting an idle/statement timeout on a managed Postgres.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/a4cefbcebc4cd958. Report an issue: GitHub.