diff --git a/.github/POSTGRES_COMPATIBILITY.md b/.github/POSTGRES_COMPATIBILITY.md index b4b7afbb5a3..13378137e59 100644 --- a/.github/POSTGRES_COMPATIBILITY.md +++ b/.github/POSTGRES_COMPATIBILITY.md @@ -180,6 +180,13 @@ audit of these fixes found four recurring mistakes: the arbitrary-pick preservation the wrap is usually justified as. Confirmed on CI; see #56241. Note a local macOS PostgreSQL gives a **false all-clear** โ€” its collation happens to agree with MariaDB on case. Fix: take a representative row rather than sorting text. + **Picking that row is the hard part.** `Min(name)` is still a text sort: `autoname="hash"` is + not reliably lower case, because `_get_timestamp_prefix()` prepends `get_trace_id()[-1:]` + un-lowered and a client-supplied `X-Frappe-Request-Id` can put an upper case `A-F` there. A + non-text key (`Min(idx)`) works only where it is **unique within the group** and the join-back + carries the **full group key** โ€” a date is usually neither, and joining on a duplicated value + turns one group into several rows (ยง3). Otherwise select the row in Python, sorting with + `key=str.casefold` so the order matches MariaDB's collation without depending on the database's. - **Wrong bound** โ€” where the value has a semantic, pick the bound deliberately: `Min(schedule_date)` for a "required by", `Min(idx)` for first-line ordering, a qty-weighted average for a rate. A blind `Max` can understate urgency or overstate a figure. diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 09b5a60fc53..2837ed47a28 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2415,8 +2415,8 @@ class QueryPaymentLedger: .groupby(ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party) ).as_("grouped") - # Payment Ledger Entry has no autoname rule, so frappe names it by hash -- lower-case, which - # keeps Min(name) free of the collation divergence that picking Max() over free text has. + # KNOWN DIVERGENCE: Min(name) is a text sort. Hash names are not reliably lower case -- the + # trace-id prefix is not lowered -- so the engines can pick different rows here. representative_ple = qb.DocType("Payment Ledger Entry").as_("representative_ple") query_voucher_amount = ( qb.from_(grouped_voucher_amount)