| 1 | -- advanced_db_tests.sql
|
|---|
| 2 | -- EduBerza - tests for the P7 rules in advanced_db.sql
|
|---|
| 3 | --
|
|---|
| 4 | -- Run right after data_load.sql (seed state: alice 8250 USD + 0.5 ETH,
|
|---|
| 5 | -- bob 5000 USD, charlie 2500 USD, ETH/USD last traded at 3520).
|
|---|
| 6 | -- It plays a short trading story and, along the way, tries to break every
|
|---|
| 7 | -- rule. Each check prints PASS/FAIL as a NOTICE; everything is rolled back
|
|---|
| 8 | -- at the end, so the data is left exactly as it was.
|
|---|
| 9 | --
|
|---|
| 10 | -- The reservation and balance checks are deferred to COMMIT; the tests force
|
|---|
| 11 | -- them with SET CONSTRAINTS ALL IMMEDIATE so a violation shows up inside the
|
|---|
| 12 | -- test instead of at the final COMMIT.
|
|---|
| 13 |
|
|---|
| 14 | BEGIN;
|
|---|
| 15 | SET search_path TO project, public;
|
|---|
| 16 |
|
|---|
| 17 | CREATE TEMP TABLE test_results (name text, passed boolean) ON COMMIT DROP;
|
|---|
| 18 | CREATE TEMP TABLE ids (name text PRIMARY KEY, id uuid) ON COMMIT DROP;
|
|---|
| 19 |
|
|---|
| 20 | -- expect_error: run p_sql (and the deferred checks); it must fail with an
|
|---|
| 21 | -- error containing p_fragment.
|
|---|
| 22 | CREATE PROCEDURE pg_temp.expect_error(p_name text, p_sql text, p_fragment text)
|
|---|
| 23 | LANGUAGE plpgsql AS $$
|
|---|
| 24 | BEGIN
|
|---|
| 25 | BEGIN
|
|---|
| 26 | EXECUTE p_sql;
|
|---|
| 27 | SET CONSTRAINTS ALL IMMEDIATE;
|
|---|
| 28 | RAISE NOTICE 'FAIL %: no error raised', p_name;
|
|---|
| 29 | INSERT INTO test_results VALUES (p_name, false);
|
|---|
| 30 | EXCEPTION WHEN OTHERS THEN
|
|---|
| 31 | IF SQLERRM ILIKE '%' || p_fragment || '%' THEN
|
|---|
| 32 | RAISE NOTICE 'PASS %: %', p_name, SQLERRM;
|
|---|
| 33 | INSERT INTO test_results VALUES (p_name, true);
|
|---|
| 34 | ELSE
|
|---|
| 35 | RAISE NOTICE 'FAIL %: unexpected error: %', p_name, SQLERRM;
|
|---|
| 36 | INSERT INTO test_results VALUES (p_name, false);
|
|---|
| 37 | END IF;
|
|---|
| 38 | END;
|
|---|
| 39 | SET CONSTRAINTS ALL DEFERRED;
|
|---|
| 40 | END $$;
|
|---|
| 41 |
|
|---|
| 42 | CREATE FUNCTION pg_temp.expect_true(p_name text, p_ok boolean, p_detail text DEFAULT '')
|
|---|
| 43 | RETURNS void LANGUAGE plpgsql AS $$
|
|---|
| 44 | BEGIN
|
|---|
| 45 | RAISE NOTICE '% %: %', CASE WHEN coalesce(p_ok, false) THEN 'PASS' ELSE 'FAIL' END, p_name, p_detail;
|
|---|
| 46 | INSERT INTO test_results VALUES (p_name, coalesce(p_ok, false));
|
|---|
| 47 | END $$;
|
|---|
| 48 |
|
|---|
| 49 | -- consistent: all deferred checks pass right now
|
|---|
| 50 | CREATE FUNCTION pg_temp.consistent() RETURNS boolean LANGUAGE plpgsql AS $$
|
|---|
| 51 | BEGIN
|
|---|
| 52 | SET CONSTRAINTS ALL IMMEDIATE;
|
|---|
| 53 | SET CONSTRAINTS ALL DEFERRED;
|
|---|
| 54 | RETURN true;
|
|---|
| 55 | EXCEPTION WHEN OTHERS THEN
|
|---|
| 56 | RAISE NOTICE ' consistency check failed: %', SQLERRM;
|
|---|
| 57 | RETURN false;
|
|---|
| 58 | END $$;
|
|---|
| 59 |
|
|---|
| 60 | CREATE FUNCTION pg_temp.uid(p_name text) RETURNS uuid LANGUAGE sql AS $$
|
|---|
| 61 | SELECT id FROM users WHERE username = p_name
|
|---|
| 62 | $$;
|
|---|
| 63 | CREATE FUNCTION pg_temp.oid(p_name text) RETURNS uuid LANGUAGE sql AS $$
|
|---|
| 64 | SELECT id FROM ids WHERE name = p_name
|
|---|
| 65 | $$;
|
|---|
| 66 |
|
|---|
| 67 |
|
|---|
| 68 | -- ===========================================================================
|
|---|
| 69 | -- A. Placing orders reserves what they commit
|
|---|
| 70 | -- ===========================================================================
|
|---|
| 71 | INSERT INTO ids VALUES ('alice_ask',
|
|---|
| 72 | place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600));
|
|---|
| 73 | INSERT INTO ids VALUES ('bob_bid',
|
|---|
| 74 | place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
|
|---|
| 75 |
|
|---|
| 76 | SELECT pg_temp.expect_true('place: limit sell above the market rests in the book, crypto reserved',
|
|---|
| 77 | (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'open'
|
|---|
| 78 | AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0.3,
|
|---|
| 79 | 'alice ETH reserved = ' || (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')));
|
|---|
| 80 | SELECT pg_temp.expect_true('place: limit buy below the market rests in the book, cash reserved',
|
|---|
| 81 | (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4650.0000, 350.0000),
|
|---|
| 82 | (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
|
|---|
| 83 | SELECT pg_temp.expect_true('event: placement recorded automatically',
|
|---|
| 84 | (SELECT count(*) FROM order_events WHERE order_id IN (pg_temp.oid('alice_ask'), pg_temp.oid('bob_bid'))
|
|---|
| 85 | AND event_type = 'placed') = 2);
|
|---|
| 86 | SELECT pg_temp.expect_true('view: order book shows both price levels',
|
|---|
| 87 | (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side)
|
|---|
| 88 | FROM v_order_book WHERE symbol = 'ETH') = 'buy 0.1000 @ 3500.000000, sell 0.3000 @ 3600.000000',
|
|---|
| 89 | (SELECT string_agg(side || ' ' || quantity || ' @ ' || price, ', ' ORDER BY side) FROM v_order_book WHERE symbol = 'ETH'));
|
|---|
| 90 | SELECT pg_temp.expect_true('consistency holds after placing', pg_temp.consistent());
|
|---|
| 91 |
|
|---|
| 92 | CALL pg_temp.expect_error('place: buy without enough free cash',
|
|---|
| 93 | $q$SELECT place_order(pg_temp.uid('charlie'), 'a1111111-1111-1111-1111-111111111111', 'buy', 'limit', 1, 60000)$q$,
|
|---|
| 94 | 'insufficient funds');
|
|---|
| 95 | CALL pg_temp.expect_error('place: sell more than is free (0.2 of 0.5 is already reserved)',
|
|---|
| 96 | $q$SELECT place_order(pg_temp.uid('alice'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.3, 3600)$q$,
|
|---|
| 97 | 'insufficient holding');
|
|---|
| 98 |
|
|---|
| 99 | -- ===========================================================================
|
|---|
| 100 | -- B. A trade between two compatible orders, partial fill
|
|---|
| 101 | -- ===========================================================================
|
|---|
| 102 | -- bob bids 0.2 at 3650: crosses alice's ask at 3600 -> trade 0.2 @ 3600
|
|---|
| 103 | INSERT INTO ids VALUES ('bob_bid2',
|
|---|
| 104 | place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.2, 3650));
|
|---|
| 105 |
|
|---|
| 106 | SELECT pg_temp.expect_true('match: trade between the two orders at the resting price',
|
|---|
| 107 | EXISTS (SELECT 1 FROM market_trades
|
|---|
| 108 | WHERE buy_order_id = pg_temp.oid('bob_bid2') AND sell_order_id = pg_temp.oid('alice_ask')
|
|---|
| 109 | AND quantity = 0.2 AND price = 3600 AND source = 'match'));
|
|---|
| 110 | SELECT pg_temp.expect_true('status: seller partially filled, buyer executed (automatic)',
|
|---|
| 111 | (SELECT status || ' ' || filled_quantity FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'partially_filled 0.2000'
|
|---|
| 112 | AND (SELECT status FROM orders WHERE id = pg_temp.oid('bob_bid2')) = 'executed',
|
|---|
| 113 | (SELECT format('alice_ask %s %s/%s', status, filled_quantity, quantity) FROM orders WHERE id = pg_temp.oid('alice_ask')));
|
|---|
| 114 | SELECT pg_temp.expect_true('money: buyer paid 720, got back the 10 reserved above the trade price',
|
|---|
| 115 | (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (3930.0000, 350.0000),
|
|---|
| 116 | (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
|
|---|
| 117 | SELECT pg_temp.expect_true('crypto: 0.2 ETH moved from alice (0.1 still reserved) to bob',
|
|---|
| 118 | (SELECT (quantity, reserved_quantity) FROM holdings WHERE user_id = pg_temp.uid('alice')) = (0.3000, 0.1000)
|
|---|
| 119 | AND (SELECT (quantity, avg_price) FROM holdings WHERE user_id = pg_temp.uid('bob')) = (0.2000, 3600.000000));
|
|---|
| 120 | SELECT pg_temp.expect_true('ledger: one buy and one sell row, linked to the orders',
|
|---|
| 121 | (SELECT count(*) FROM transactions WHERE related_order IN (pg_temp.oid('bob_bid2'), pg_temp.oid('alice_ask'))) = 2);
|
|---|
| 122 | SELECT pg_temp.expect_true('event: fills recorded automatically',
|
|---|
| 123 | (SELECT string_agg(event_type, ',' ORDER BY id) FROM order_events WHERE order_id = pg_temp.oid('alice_ask'))
|
|---|
| 124 | = 'placed,partially_filled');
|
|---|
| 125 | SELECT pg_temp.expect_true('consistency holds after the trade', pg_temp.consistent());
|
|---|
| 126 |
|
|---|
| 127 | -- ===========================================================================
|
|---|
| 128 | -- C. Market order: book first, then the simulated market
|
|---|
| 129 | -- ===========================================================================
|
|---|
| 130 | -- market price is now 3600 (last trade); charlie buys 0.15 at market:
|
|---|
| 131 | -- 0.1 from alice's remaining ask @ 3600, the other 0.05 from the market @ 3600
|
|---|
| 132 | INSERT INTO ids VALUES ('charlie_mkt',
|
|---|
| 133 | place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'market', 0.15));
|
|---|
| 134 |
|
|---|
| 135 | SELECT pg_temp.expect_true('market order: filled completely in two trades',
|
|---|
| 136 | (SELECT (status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt'))
|
|---|
| 137 | = ('executed'::varchar, 2::bigint, 3600.000000::numeric),
|
|---|
| 138 | (SELECT format('%s, %s trades, avg %s', status, trades, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('charlie_mkt')));
|
|---|
| 139 | SELECT pg_temp.expect_true('market order: alice''s ask is now executed, nothing left reserved',
|
|---|
| 140 | (SELECT status FROM orders WHERE id = pg_temp.oid('alice_ask')) = 'executed'
|
|---|
| 141 | AND (SELECT reserved_quantity FROM holdings WHERE user_id = pg_temp.uid('alice')) = 0
|
|---|
| 142 | AND (SELECT reserved_balance FROM users WHERE username = 'charlie') = 0);
|
|---|
| 143 | SELECT pg_temp.expect_true('consistency holds after the market order', pg_temp.consistent());
|
|---|
| 144 |
|
|---|
| 145 | -- ===========================================================================
|
|---|
| 146 | -- D. Trades are only possible between valid, compatible orders
|
|---|
| 147 | -- ===========================================================================
|
|---|
| 148 | INSERT INTO ids VALUES ('charlie_ask',
|
|---|
| 149 | place_order(pg_temp.uid('charlie'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3700));
|
|---|
| 150 | INSERT INTO ids VALUES ('bob_ask',
|
|---|
| 151 | place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'sell', 'limit', 0.1, 3800));
|
|---|
| 152 |
|
|---|
| 153 | CALL pg_temp.expect_error('trade: price above the buyer''s limit',
|
|---|
| 154 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
|
|---|
| 155 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('charlie_ask'))$q$,
|
|---|
| 156 | 'outside the limit');
|
|---|
| 157 | CALL pg_temp.expect_error('trade: more than the order has remaining',
|
|---|
| 158 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
|
|---|
| 159 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.5, pg_temp.oid('bob_bid'))$q$,
|
|---|
| 160 | 'exceeds the remaining quantity');
|
|---|
| 161 | CALL pg_temp.expect_error('trade: a sell order used as the buy side',
|
|---|
| 162 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
|
|---|
| 163 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3700, 0.1, pg_temp.oid('charlie_ask'))$q$,
|
|---|
| 164 | 'cannot be the buy side');
|
|---|
| 165 | CALL pg_temp.expect_error('trade: order of another market',
|
|---|
| 166 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
|
|---|
| 167 | VALUES ('a1111111-1111-1111-1111-111111111111', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
|
|---|
| 168 | 'different market');
|
|---|
| 169 | CALL pg_temp.expect_error('trade: executed order cannot trade again',
|
|---|
| 170 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, sell_order_id)
|
|---|
| 171 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3600, 0.1, pg_temp.oid('alice_ask'))$q$,
|
|---|
| 172 | 'is executed and cannot trade');
|
|---|
| 173 | CALL pg_temp.expect_error('trade: a user with their own order',
|
|---|
| 174 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id, sell_order_id)
|
|---|
| 175 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'), pg_temp.oid('bob_ask'))$q$,
|
|---|
| 176 | 'own order');
|
|---|
| 177 | CALL pg_temp.expect_error('trade: a trade that filled orders cannot be deleted',
|
|---|
| 178 | $q$DELETE FROM market_trades WHERE buy_order_id = pg_temp.oid('bob_bid2')$q$,
|
|---|
| 179 | 'cannot be changed or deleted');
|
|---|
| 180 |
|
|---|
| 181 | -- ===========================================================================
|
|---|
| 182 | -- E. Order state changes
|
|---|
| 183 | -- ===========================================================================
|
|---|
| 184 | CALL pg_temp.expect_error('state: status cannot be set to executed by hand',
|
|---|
| 185 | $q$UPDATE orders SET status = 'executed' WHERE id = pg_temp.oid('bob_bid')$q$,
|
|---|
| 186 | 'status is derived automatically');
|
|---|
| 187 | CALL pg_temp.expect_error('state: filled quantity cannot be changed by hand',
|
|---|
| 188 | $q$UPDATE orders SET filled_quantity = 0.05 WHERE id = pg_temp.oid('bob_bid')$q$,
|
|---|
| 189 | 'can only change through a trade');
|
|---|
| 190 | CALL pg_temp.expect_error('state: executed order cannot be processed again',
|
|---|
| 191 | $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('alice_ask')$q$,
|
|---|
| 192 | 'already executed');
|
|---|
| 193 | CALL pg_temp.expect_error('state: ordered quantity cannot change',
|
|---|
| 194 | $q$UPDATE orders SET quantity = 1 WHERE id = pg_temp.oid('bob_bid')$q$,
|
|---|
| 195 | 'cannot change');
|
|---|
| 196 | CALL pg_temp.expect_error('state: cancelling by hand without releasing the reservation',
|
|---|
| 197 | $q$UPDATE orders SET status = 'cancelled' WHERE id = pg_temp.oid('bob_bid')$q$,
|
|---|
| 198 | 'reserved balance');
|
|---|
| 199 |
|
|---|
| 200 | -- ===========================================================================
|
|---|
| 201 | -- F. Cancelling releases the reservation, exactly once
|
|---|
| 202 | -- ===========================================================================
|
|---|
| 203 | CALL pg_temp.expect_error('cancel: someone else''s order',
|
|---|
| 204 | $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('charlie'))$q$,
|
|---|
| 205 | 'does not belong');
|
|---|
| 206 | SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'));
|
|---|
| 207 | SELECT pg_temp.expect_true('cancel: 350 back from reserved to available, event recorded',
|
|---|
| 208 | (SELECT (available_balance, reserved_balance) FROM users WHERE username = 'bob') = (4280.0000, 0.0000)
|
|---|
| 209 | AND EXISTS (SELECT 1 FROM order_events WHERE order_id = pg_temp.oid('bob_bid') AND event_type = 'cancelled'),
|
|---|
| 210 | (SELECT format('bob available %s reserved %s', available_balance, reserved_balance) FROM users WHERE username = 'bob'));
|
|---|
| 211 | CALL pg_temp.expect_error('cancel: a cancelled order cannot be cancelled again',
|
|---|
| 212 | $q$SELECT cancel_order(pg_temp.oid('bob_bid'), pg_temp.uid('bob'))$q$,
|
|---|
| 213 | 'cannot be cancelled');
|
|---|
| 214 | CALL pg_temp.expect_error('trade: cancelled order cannot trade',
|
|---|
| 215 | $q$INSERT INTO market_trades (market_id, executed_at, price, quantity, buy_order_id)
|
|---|
| 216 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3500, 0.1, pg_temp.oid('bob_bid'))$q$,
|
|---|
| 217 | 'is cancelled and cannot trade');
|
|---|
| 218 |
|
|---|
| 219 | -- ===========================================================================
|
|---|
| 220 | -- G. Balances cannot be put in an inconsistent state
|
|---|
| 221 | -- ===========================================================================
|
|---|
| 222 | CALL pg_temp.expect_error('balance: reserving cash with no order behind it',
|
|---|
| 223 | $q$UPDATE users SET available_balance = available_balance - 100, reserved_balance = reserved_balance + 100
|
|---|
| 224 | WHERE username = 'charlie'$q$,
|
|---|
| 225 | 'reserved balance');
|
|---|
| 226 | CALL pg_temp.expect_error('balance: reserving crypto with no order behind it',
|
|---|
| 227 | $q$UPDATE holdings SET reserved_quantity = reserved_quantity + 0.01 WHERE user_id = pg_temp.uid('alice')$q$,
|
|---|
| 228 | 'reserved quantity');
|
|---|
| 229 | CALL pg_temp.expect_error('balance: cash changed without a ledger row',
|
|---|
| 230 | $q$UPDATE users SET available_balance = available_balance + 100 WHERE username = 'charlie'$q$,
|
|---|
| 231 | 'does not match the ledger');
|
|---|
| 232 |
|
|---|
| 233 | -- ===========================================================================
|
|---|
| 234 | -- H. Background job: resting limit orders filled when the market reaches them
|
|---|
| 235 | -- ===========================================================================
|
|---|
| 236 | INSERT INTO ids VALUES ('bob_bid3',
|
|---|
| 237 | place_order(pg_temp.uid('bob'), 'a2222222-2222-2222-2222-222222222222', 'buy', 'limit', 0.1, 3500));
|
|---|
| 238 | -- the simulator moves the price down to 3450 (a bot tick, no orders)
|
|---|
| 239 | INSERT INTO market_trades (market_id, executed_at, price, quantity, side)
|
|---|
| 240 | VALUES ('a2222222-2222-2222-2222-222222222222', now(), 3450, 0.01, 'sell');
|
|---|
| 241 |
|
|---|
| 242 | CREATE TEMP TABLE job_run ON COMMIT DROP AS SELECT fill_marketable_orders() AS filled;
|
|---|
| 243 |
|
|---|
| 244 | SELECT pg_temp.expect_true('job: fills exactly the orders the new price reached',
|
|---|
| 245 | (SELECT filled FROM job_run) = 1
|
|---|
| 246 | AND (SELECT (status, avg_fill_price) FROM v_order_history WHERE order_id = pg_temp.oid('bob_bid3'))
|
|---|
| 247 | = ('executed'::varchar, 3450.000000::numeric)
|
|---|
| 248 | AND (SELECT status FROM orders WHERE id = pg_temp.oid('charlie_ask')) = 'open',
|
|---|
| 249 | (SELECT format('bob_bid3 %s @ %s, charlie_ask (3700) still %s', h.status, h.avg_fill_price, o.status)
|
|---|
| 250 | FROM v_order_history h, orders o WHERE h.order_id = pg_temp.oid('bob_bid3') AND o.id = pg_temp.oid('charlie_ask')));
|
|---|
| 251 | SELECT pg_temp.expect_true('job: buyer paid 345, got the 5 above the fill price back',
|
|---|
| 252 | (SELECT reserved_balance FROM users WHERE username = 'bob') = 0
|
|---|
| 253 | AND (SELECT count(*) FROM transactions WHERE related_order = pg_temp.oid('bob_bid3') AND amount = -345) = 1);
|
|---|
| 254 | SELECT pg_temp.expect_true('job: nothing more to do on a second run', fill_marketable_orders() = 0);
|
|---|
| 255 | SELECT pg_temp.expect_true('consistency holds after the job', pg_temp.consistent());
|
|---|
| 256 |
|
|---|
| 257 | -- ===========================================================================
|
|---|
| 258 | -- Final state of every trader
|
|---|
| 259 | -- ===========================================================================
|
|---|
| 260 | SELECT pg_temp.expect_true('views: every trader''s cash equals their ledger',
|
|---|
| 261 | NOT EXISTS (SELECT 1 FROM v_trader_balances WHERE total_cash <> ledger_total));
|
|---|
| 262 |
|
|---|
| 263 | SELECT username, available_balance, reserved_balance, ledger_total, holdings_value
|
|---|
| 264 | FROM v_trader_balances ORDER BY username;
|
|---|
| 265 |
|
|---|
| 266 | SELECT count(*) FILTER (WHERE passed) AS passed,
|
|---|
| 267 | count(*) FILTER (WHERE NOT passed) AS failed
|
|---|
| 268 | FROM test_results;
|
|---|
| 269 |
|
|---|
| 270 | ROLLBACK;
|
|---|