Skip to content

fix(postgres): make node traffic sync robust after public API inbound updates#5038

Open
rqzbeh wants to merge 2 commits into
MHSanaei:mainfrom
rqzbeh:fix/pg-node-traffic-sync-api-update
Open

fix(postgres): make node traffic sync robust after public API inbound updates#5038
rqzbeh wants to merge 2 commits into
MHSanaei:mainfrom
rqzbeh:fix/pg-node-traffic-sync-api-update

Conversation

@rqzbeh
Copy link
Copy Markdown

@rqzbeh rqzbeh commented Jun 7, 2026

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 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.

Summary

Why

Type of change

  • [ x] Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Documentation
  • Tests only
  • Build / CI / tooling
  • Other

Areas affected

  • Frontend (UI / panel pages)
  • [x ] Backend (API endpoints, login, settings)
  • Xray config generation
  • Subscription (share links / Clash / JSON)
  • Statistics / traffic counters
  • [x ] Database / migrations
  • Install / upgrade script
  • Docker image
  • Multi-node (sub-nodes)
  • Telegram bot

How was this tested?

Checklist

  • I tested the change locally and confirmed the described behavior.
  • I added or updated tests for the new behavior (when applicable).
  • [x ] go build ./... and the test suite pass locally.
  • For frontend changes: npm run lint, npm run typecheck, and npm run build pass.
  • I updated the Wiki / README / API docs if user-facing behavior changed.
  • [x ] My commits follow the project's existing message style.
  • [ x] I have no unrelated changes mixed into this PR.

… 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.
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 91643f6.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant