The intra-customer transfer arm (method = TRANSFER) of the transaction
UNION, as a view. Source and target wallets are owned by the same customer
(enforced by the ws.customer_id = wt.customer_id predicate in t), so the
row's customer audience is unambiguous and the counterparty is the other
wallet, not another customer. Cross-customer transfers are a different arm
(networkTransfer.ts), which is why there is no payment-instrument lookup
here — NETWORK_WALLET PIs only apply there.
Same rationale as depositTransactionView: assembled inline the 12-arm
UNION serialises past the RDS Proxy's 16KB parse limit and pins the client's
database connection for the rest of the session. Behind a view the runtime
statement is ~90 bytes.
NOT MATERIALIZED on t is deliberate even though t is referenced twice
(which otherwise forces materialization): the caller's filters are outer
predicates on this view's projected columns, and only an inlined CTE lets
them reach ledger_account_transfer's indexes.
The caller-supplied filters live in
transaction-services/queries/transfer.ts as predicates on this view's own
projected columns, because a view takes no parameters. They are plain
contract-column comparisons emitted by the shared
transactionScalarConditions, so what matters here is what each filtered
column projects:
customer_id — cs.external_id (the source customer) on the debit sub-arm,
ct.external_id on the credit sub-arm; t's
ws.customer_id = wt.customer_id predicate makes cs and ct literally
the same customer row, so the two are equal on every row this view can
emit and no row here belongs to a second customer that could leak.
signature — receipt ->> 'id', resolved inside t and projected
unchanged by both sub-arms, so the predicate still pushes down to
idx_ledger_account_transfer_receipt_id.
bank_id is unconditionally NULL — transfers never touch a bank rail — so
there is no bankIds filter.
Changing what those columns project silently changes the filters. The column
list must also stay identical to the sibling arms, or the UNION breaks.
securityInvoker is required, not cosmetic — every underlying table has RLS
enabled, and without it the view would run with the owner's row visibility
and silently bypass the querying role's policies.
The intra-customer transfer arm (method =
TRANSFER) of the transaction UNION, as a view. Source and target wallets are owned by the same customer (enforced by thews.customer_id = wt.customer_idpredicate int), so the row's customer audience is unambiguous and the counterparty is the other wallet, not another customer. Cross-customer transfers are a different arm (networkTransfer.ts), which is why there is no payment-instrument lookup here — NETWORK_WALLET PIs only apply there.Same rationale as
depositTransactionView: assembled inline the 12-arm UNION serialises past the RDS Proxy's 16KB parse limit and pins the client's database connection for the rest of the session. Behind a view the runtime statement is ~90 bytes.NOT MATERIALIZEDontis deliberate even thoughtis referenced twice (which otherwise forces materialization): the caller's filters are outer predicates on this view's projected columns, and only an inlined CTE lets them reachledger_account_transfer's indexes.The caller-supplied filters live in
transaction-services/queries/transfer.tsas predicates on this view's own projected columns, because a view takes no parameters. They are plain contract-column comparisons emitted by the sharedtransactionScalarConditions, so what matters here is what each filtered column projects:customer_id—cs.external_id(the source customer) on the debit sub-arm,ct.external_idon the credit sub-arm;t'sws.customer_id = wt.customer_idpredicate makescsandctliterally the same customer row, so the two are equal on every row this view can emit and no row here belongs to a second customer that could leak.signature—receipt ->> 'id', resolved insidetand projected unchanged by both sub-arms, so the predicate still pushes down toidx_ledger_account_transfer_receipt_id.bank_idis unconditionally NULL — transfers never touch a bank rail — so there is nobankIdsfilter.Changing what those columns project silently changes the filters. The column list must also stay identical to the sibling arms, or the UNION breaks.
securityInvokeris required, not cosmetic — every underlying table has RLS enabled, and without it the view would run with the owner's row visibility and silently bypass the querying role's policies.