mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 21:34:33 +00:00
* fix(postgres): make node traffic sync robust after public API inbound updates
The background NodeTrafficSyncJob (every 5s) started failing after a
successful POST /panel/api/inbounds/update/{id} (including flows that
inject streamSettings.externalProxy) with:
node traffic sync: merge for <node> failed:
ERROR: CASE types boolean and integer cannot be matched (SQLSTATE 42804)
Root cause:
- The merge lives in setRemoteTrafficLocked (called from SetRemoteTraffic).
- The client_traffics delta path used a dialect-sensitive expression:
enable = enable AND ?
last_online = GREATEST(last_online, ?)
- On PostgreSQL, GREATEST / AND / COALESCE are implemented with internal
CASE expressions. When "enable" columns (client_traffics, inbounds, ...)
were INTEGER (common after SQLite → PG data migrations, older
AutoMigrate, or mixed write paths) and the right-hand side was a
boolean parameter (from snapshot ClientStats or form-bound API payload),
PG rejected the expression at plan time.
- The public API update path (unlike the internal remote wire path)
always runs updateClientTraffics + UpdateClientStat + SyncInbound.
This touches client_traffics.enable rows for any inbound that has
clients.
- SQLite tolerated 0/1 numeric bools; PG is strict.
Fix:
- Use an explicit CASE with ::boolean casts in the critical enable
expression so the result type is always boolean.
- Make GreatestExpr emit safe casts on Postgres.
- Add a one-time normalization step in MigrationRequirements (runs on
startup + xray restarts) that forces the relevant enable/enabled
columns to boolean on Postgres using an idempotent DO block + USING
cast. This cleans up pre-existing skew without a full re-migration.
This branch is based on upstream/main (original mhsanaei/3x-ui main).
The node traffic sync now survives arbitrary public-API inbound
updates on PostgreSQL.
* fix: make client traffic enable merge expression safe on SQLite too
The previous commit introduced an explicit CASE for the "only node
can disable" logic in the node traffic sync merge to fix the PG
"CASE types boolean and integer cannot be matched" error after
public API inbound updates.
That expression used PostgreSQL-only `::boolean` casts:
CASE WHEN ?::boolean THEN enable::boolean ELSE false END
This is invalid syntax on SQLite (and would break the merge when
the client_traffics delta UPDATE runs — which is commonly triggered
right after an API /inbounds/update because that path calls
updateClientTraffics + SyncInbound and touches client_traffics rows).
Extracted the expression to a new dialect-aware helper
`ClientTrafficEnableMergeExpr()` (following the same pattern as
GreatestExpr, JSONClientsFromInbound, etc.).
- On Postgres: keeps the strict boolean-typed CASE with casts.
- On SQLite: uses a numeric-compatible form
`CASE WHEN ? THEN enable ELSE 0 END` that produces the expected
0/1 result matching the column affinity.
The logical behavior ("node may only force-disable, never re-enable")
is preserved on both databases.
This is a follow-up commit on the same branch so that one PR
contains both the original Postgres fix and the SQLite compatibility
fix.
Builds directly on top of 91643f68.
* fix
---------
Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
53 lines
1.9 KiB
Go
53 lines
1.9 KiB
Go
package database
|
|
|
|
import "fmt"
|
|
|
|
// JSONClientsFromInbound returns the FROM clause that yields one row per element
|
|
// of inbounds.settings -> clients, with a column named `client.value` whose text
|
|
// fields can be read with JSONFieldText("client.value", "<key>").
|
|
func JSONClientsFromInbound() string {
|
|
if IsPostgres() {
|
|
return "FROM inbounds, jsonb_array_elements(inbounds.settings::jsonb -> 'clients') AS client(value)"
|
|
}
|
|
return "FROM inbounds, JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client"
|
|
}
|
|
|
|
func JSONFieldText(expr, key string) string {
|
|
if IsPostgres() {
|
|
return fmt.Sprintf("(%s ->> '%s')", expr, key)
|
|
}
|
|
|
|
return fmt.Sprintf("TRIM(JSON_EXTRACT(%s, '$.%s'), '\"')", expr, key)
|
|
}
|
|
|
|
func GreatestExpr(a, b string) string {
|
|
if IsPostgres() {
|
|
return fmt.Sprintf("GREATEST(%s::bigint, %s::bigint)", a, b)
|
|
}
|
|
return fmt.Sprintf("MAX(%s, %s)", a, b)
|
|
}
|
|
|
|
// ClientTrafficEnableMergeExpr returns the SQL expression used in the
|
|
// node traffic merge to update client_traffics.enable.
|
|
//
|
|
// The intent is: only allow the remote node to *disable* a client
|
|
// (never re-enable one that the central panel has disabled).
|
|
//
|
|
// We use a dialect-specific expression because:
|
|
// - On PostgreSQL we want strict boolean typing and casts to avoid
|
|
// "CASE types boolean and integer cannot be matched" errors
|
|
// (and similar internal expansions of AND/GREATEST).
|
|
// - On SQLite, enable is stored with INTEGER affinity (0/1), there is
|
|
// no :: cast syntax, and we must produce a numeric-compatible result.
|
|
//
|
|
// The expression must be valid SQL for tx.Exec with a boolean parameter
|
|
// as the first ?.
|
|
func ClientTrafficEnableMergeExpr() string {
|
|
if IsPostgres() {
|
|
return "CASE WHEN ?::boolean THEN enable::boolean ELSE false END"
|
|
}
|
|
// SQLite: no :: casts. Use numeric CASE. 1/0 work as true/false
|
|
// thanks to SQLite's affinity and how GORM/drivers bind bools.
|
|
return "CASE WHEN ? THEN enable ELSE 0 END"
|
|
}
|