I’m trying to find the total amount of winnings for a specific backer in my database.
query =
from b in Gaming.Stakes.Backer,
left_join: bwt in assoc(b, :wallet_transactions),
left_join: bpwt in assoc(b, :backer_promotional_wallet_transactions),
where: b.id == ^backer.id,
where: bwt.type == "winnings" or bpwt.type == "winnings",
group_by: b.id
from e in subquery(query),
select: [e.backer_id, e.wallet_sums + e.promotional_wallet_sums]
I’m a bit stuck on how to do the coalesce, and the nested select. Ultimately what I need a single value to come back from the database.
This is the raw SQL query that works as intended and give me the data I need. I’m trying to get this into Ecto code.
SELECT
backer_id,
wallet_sums + promotional_wallet_sums AS total
FROM
(
SELECT
backers.id AS backer_id,
SUM( COALESCE( wallet_transactions.amount,
(
'USD',
0
)
:: money_with_currency ) ) AS wallet_sums,
SUM( COALESCE( promotional_wallet_transactions.amount,
(
'USD',
0
)
:: money_with_currency ) ) AS promotional_wallet_sums
FROM
backers
LEFT JOIN
backer_wallet_transactions
ON backer_wallet_transactions.backer_id = backers.id
LEFT JOIN
wallet_transactions
ON wallet_transactions.id = backer_wallet_transactions.wallet_transaction_id
LEFT JOIN
backer_promotional_wallet_transactions
ON backer_promotional_wallet_transactions.backer_id = backers.id
LEFT JOIN
promotional_wallet_transactions
ON promotional_wallet_transactions.id = baker_promotional_wallet_transactions.promotional_wallet_transaction_id
WHERE
backers.id = 'c9ffd955-3333-3333-3333-578346b33333'
AND
(
wallet_transactions.type = 'winnings'
OR promotional_wallet_transactions.type = 'winnings'
)
GROUP BY
backers.id
)
sums
Do I understand correctly that the intent is to sum :money_with_currency amounts, but treat NULL amounts as 0 amounts in the currently aggregating currency (USD for your example?). That might be possible and reasonable to implement as an additional aggregate function and I’ll take a look (I’m the author of ex_money which it appears you may be using).
Another bit of default behavior for a “strict” transition function is that the previous state value is retained unchanged whenever a null input value is encountered. Thus, null values are ignored. If you need some other behavior for null inputs, do not declare your transition function as strict; instead code it to test for null inputs and do whatever is needed.
And the current aggregate function is implemented as STRICT:
CREATE OR REPLACE FUNCTION money_state_function(agg_state money_with_currency, money money_with_currency)
RETURNS money_with_currency
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
expected_currency char(3);
aggregate numeric;
addition numeric;
BEGIN
if currency_code(agg_state) IS NULL then
expected_currency := currency_code(money);
aggregate := 0;
else
expected_currency := currency_code(agg_state);
aggregate := amount(agg_state);
end if;
IF currency_code(money) = expected_currency THEN
addition := aggregate + amount(money);
return row(expected_currency, addition);
ELSE
RAISE EXCEPTION
'Incompatible currency codes. Expected all currency codes to be %', expected_currency
USING HINT = 'Please ensure all columns have the same currency code',
ERRCODE = '22033';
END IF;
END;
$$;
CREATE AGGREGATE sum(money_with_currency)
(
sfunc = money_state_function,
stype = money_with_currency
);
I recall that @LostKobrakai had some very good input when I was originally implementing this function so perhaps he will have a point of view.
Basically I can remove the STRICT tag from the function and handle NULL inputs directly so that part is clear. Postgres expects a NULL return for an aggregate if there are now non-NULL values and I can manage that too.
What I’m not sure about is whether overall this is the right thing to do. Treating NULL values as zero money amounts is probably not what most developers would expect.
Therefore I think the right approach is to implement a separate aggregate function that very explicitly treats NULL values as zero-amount money values. It will not only be explicit, but it won’t risk changing the behaviour unexpectantly for current users.
The downside is that it means the built-in support for SUM in Ecto won’t be usable since two functions in the database with the same parameters are not possible. However a fragment should make that transparent (I can add a macro to do that).
In my opinion sum for money should work exactly as sum works on numeric/decimal columns would, granted all currencies are the same. Any different behavior should be a custom and separately documented aggregation function.