-- -- This file is part of TALER -- Copyright (C) 2014--2022 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software -- Foundation; either version 3, or (at your option) any later version. -- -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -- A PARTICULAR PURPOSE. See the GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License along with -- TALER; see the file COPYING. If not, see -- -- Everything in one big transaction BEGIN; SET search_path TO exchange; --------------------------------------------------------------------------- -- General procedures for DB setup --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS exchange_tables (table_serial_id BIGINT GENERATED BY DEFAULT AS IDENTITY ,name VARCHAR NOT NULL ,version VARCHAR NOT NULL ,action VARCHAR NOT NULL ,partitioned BOOL NOT NULL ,by_range BOOL NOT NULL ,finished BOOL NOT NULL DEFAULT(FALSE)); COMMENT ON TABLE exchange_tables IS 'Tables of the exchange and their status'; COMMENT ON COLUMN exchange_tables.name IS 'Base name of the table (without partition/shard)'; COMMENT ON COLUMN exchange_tables.version IS 'Version of the DB in which the given action happened'; COMMENT ON COLUMN exchange_tables.action IS 'Action to take on the table (e.g. create, alter, constrain, unconstrain, or drop)'; COMMENT ON COLUMN exchange_tables.partitioned IS 'TRUE if the table is partitioned'; COMMENT ON COLUMN exchange_tables.by_range IS 'TRUE if the table is partitioned by range'; COMMENT ON COLUMN exchange_tables.finished IS 'TRUE if the respective migration has been run'; CREATE OR REPLACE FUNCTION create_partitioned_table( IN table_definition VARCHAR ,IN table_name VARCHAR ,IN main_table_partition_str VARCHAR -- Used only when it is the main table - we do not partition shard tables ,IN shard_suffix VARCHAR DEFAULT NULL ) RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN IF shard_suffix IS NOT NULL THEN table_name=table_name || '_' || shard_suffix; main_table_partition_str = ''; END IF; EXECUTE FORMAT( table_definition, table_name, main_table_partition_str ); END $$; CREATE OR REPLACE FUNCTION create_tables( num_partitions INTEGER ,shard_domain VARCHAR ) RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE tc CURSOR FOR SELECT table_serial_id ,name ,action ,by_range FROM exchange_tables WHERE NOT finished ORDER BY table_serial_id ASC; BEGIN FOR rec IN tc LOOP -- First create the master table, either -- completely unpartitioned, or with one -- master and the 'default' partition IF IS NULL num_partitions THEN EXECUTE FORMAT( 'PERFORM %s_table_%s (%s)'::text ,rec.action ,rec.name ,NULL ); ELSE EXECUTE FORMAT( 'PERFORM %s_table_%s (%s)'::text ,rec.action ,rec.name ,0 ); END IF IF NOT NULL shard_domain THEN -- FIXME: attach shards! -- FIXME: how will migration work with shards!? FOR i IN 1..num_partitions LOOP EXECUTE FORMAT( 'PERFORM %s_XXX_%s (%s)'::text ,rec.action ,rec.name ,i::varchar ); END LOOP; ELSE FOR i IN 1..num_partitions LOOP EXECUTE FORMAT( 'PERFORM %s_table_%s (%s)'::text ,rec.action ,rec.name ,i::varchar ); END LOOP; END IF IF 0 < num_partitions THEN -- FIXME: detach default partition! END IF UPDATE exchange_tables SET finished=TRUE WHERE table_serial_id=rec.table_serial_id; END LOOP; END $$; COMMENT ON FUNCTION create_tables IS 'Creates all tables for the given number of partitions that need creating.'; -- This is run last by dbinit, if partitions exist -- or if 'force_create_partitions' is set (otherwise, -- we are not expected to create partitions if there -- is only 1). CREATE OR REPLACE FUNCTION create_partitions( IN part_idx INTEGER ) RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE tc CURSOR FOR SELECT name ,action ,partitioned ,by_range FROM exchange_tables WHERE version=in_version AND partitioned ORDER BY table_seria_id ASC; BEGIN FOR rec IN tc LOOP EXECUTE FORMAT( 'PERFORM %s_table_%s (%s)' ,rec.action ,rec.name ,shard_idx::varchar ); END LOOP; END $$; COMMENT ON FUNCTION create_partitions IS 'Creates all partitions that need creating.'; CREATE OR REPLACE FUNCTION drop_default_partitions_NG() RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE tc CURSOR FOR SELECT name FROM exchange_tables WHERE partitioned AND NOT by_range; BEGIN RAISE NOTICE 'Dropping default tables of partitioned tables'; FOR rec IN tc LOOP EXECUTE FORMAT ( 'DROP TABLE IF EXISTS %s_default ;'::text, rec.name; END $$; COMMENT ON FUNCTION drop_default_partitions IS 'Drop all default partitions once other partitions are attached. Might be needed in sharding too.'; CREATE OR REPLACE FUNCTION detach_default_partitions_NG() RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE tc CURSOR FOR SELECT name FROM exchange_tables WHERE partitioned AND NOT by_range; BEGIN RAISE NOTICE 'Detaching all default table partitions'; FOR rec IN tc LOOP EXECUTE FORMAT ( 'ALTER TABLE IF EXISTS %s DETACH PARTITION %s_default;'::text, rec.name, rec.name ); END LOOP; END $$; COMMENT ON FUNCTION detach_default_partitions IS 'We need to drop default and create new one before deleting the default partitions otherwise constraints get lost too. Might be needed in sharding too'; CREATE OR REPLACE FUNCTION create_hash_partition_NG( source_table_name VARCHAR ,modulus INTEGER ,partition_num INTEGER ) RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'Creating partition %_%', source_table_name, partition_num; EXECUTE FORMAT( 'CREATE TABLE IF NOT EXISTS %I ' 'PARTITION OF %I ' 'FOR VALUES WITH (MODULUS %s, REMAINDER %s)' ,source_table_name || '_' || partition_num ,source_table_name ,modulus ,partition_num-1 ); END $$; CREATE OR REPLACE FUNCTION create_partitions_NG( num_partitions INTEGER ) RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE tc CURSOR FOR SELECT name FROM exchange_tables WHERE partitioned AND NOT by_range; DECLARE i INTEGER; BEGIN PERFORM detach_default_partitions(); FOR rec IN tc LOOP i := num_partitions LOOP PERFORM create_hash_partition( quote_literal (rec.name) ,num_partitions ,i ); i=i-1; EXIT WHEN i=0; END LOOP; -- i = num_partitions ... 0 END LOOP; -- for all partitioned tables PERFORM drop_default_partitions(); END $$; -- OLD LOGIC: ------------------------------------------------------------------- ------------------------- Partitions ------------------------------ ------------------------------------------------------------------- CREATE OR REPLACE FUNCTION create_range_partition( source_table_name VARCHAR ,partition_num INTEGER ) RETURNS void LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'TODO'; END $$; CREATE OR REPLACE FUNCTION detach_default_partitions() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'Detaching all default table partitions'; ALTER TABLE IF EXISTS wire_targets DETACH PARTITION wire_targets_default; ALTER TABLE IF EXISTS reserves DETACH PARTITION reserves_default; ALTER TABLE IF EXISTS reserves_in DETACH PARTITION reserves_in_default; ALTER TABLE IF EXISTS reserves_close DETACH PARTITION reserves_close_default; ALTER TABLE IF EXISTS history_requests DETACH partition history_requests_default; ALTER TABLE IF EXISTS close_requests DETACH partition close_requests_default; ALTER TABLE IF EXISTS reserves_open_requests DETACH partition reserves_open_requests_default; ALTER TABLE IF EXISTS reserves_out DETACH PARTITION reserves_out_default; ALTER TABLE IF EXISTS reserves_out_by_reserve DETACH PARTITION reserves_out_by_reserve_default; ALTER TABLE IF EXISTS known_coins DETACH PARTITION known_coins_default; ALTER TABLE IF EXISTS refresh_commitments DETACH PARTITION refresh_commitments_default; ALTER TABLE IF EXISTS refresh_revealed_coins DETACH PARTITION refresh_revealed_coins_default; ALTER TABLE IF EXISTS refresh_transfer_keys DETACH PARTITION refresh_transfer_keys_default; ALTER TABLE IF EXISTS deposits DETACH PARTITION deposits_default; --- TODO range partitioning -- ALTER TABLE IF EXISTS deposits_by_ready -- DETACH PARTITION deposits_by_ready_default; -- -- ALTER TABLE IF EXISTS deposits_for_matching -- DETACH PARTITION deposits_default_for_matching_default; ALTER TABLE IF EXISTS refunds DETACH PARTITION refunds_default; ALTER TABLE IF EXISTS wire_out DETACH PARTITION wire_out_default; ALTER TABLE IF EXISTS aggregation_transient DETACH PARTITION aggregation_transient_default; ALTER TABLE IF EXISTS aggregation_tracking DETACH PARTITION aggregation_tracking_default; ALTER TABLE IF EXISTS recoup DETACH PARTITION recoup_default; ALTER TABLE IF EXISTS recoup_by_reserve DETACH PARTITION recoup_by_reserve_default; ALTER TABLE IF EXISTS recoup_refresh DETACH PARTITION recoup_refresh_default; ALTER TABLE IF EXISTS prewire DETACH PARTITION prewire_default; ALTER TABLE IF EXISTS cs_nonce_locks DETACH partition cs_nonce_locks_default; ALTER TABLE IF EXISTS purse_requests DETACH partition purse_requests_default; ALTER TABLE IF EXISTS purse_decision DETACH partition purse_decision_default; ALTER TABLE IF EXISTS purse_merges DETACH partition purse_merges_default; ALTER TABLE IF EXISTS account_merges DETACH partition account_merges_default; ALTER TABLE IF EXISTS contracts DETACH partition contracts_default; ALTER TABLE IF EXISTS purse_deposits DETACH partition purse_deposits_default; ALTER TABLE IF EXISTS wad_out_entries DETACH partition wad_out_entries_default; ALTER TABLE IF EXISTS wads_in DETACH partition wads_in_default; ALTER TABLE IF EXISTS wad_in_entries DETACH partition wad_in_entries_default; END $$; COMMENT ON FUNCTION detach_default_partitions IS 'We need to drop default and create new one before deleting the default partitions otherwise constraints get lost too. Might be needed in sharding too'; CREATE OR REPLACE FUNCTION drop_default_partitions() RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'Dropping default table partitions'; DROP TABLE IF EXISTS wire_targets_default; DROP TABLE IF EXISTS reserves_default; DROP TABLE IF EXISTS reserves_in_default; DROP TABLE IF EXISTS reserves_close_default; DROP TABLE IF EXISTS reserves_open_requests_default; DROP TABLE IF EXISTS history_requests_default; DROP TABLE IF EXISTS close_requests_default; DROP TABLE IF EXISTS reserves_out_default; DROP TABLE IF EXISTS reserves_out_by_reserve_default; DROP TABLE IF EXISTS known_coins_default; DROP TABLE IF EXISTS refresh_commitments_default; DROP TABLE IF EXISTS refresh_revealed_coins_default; DROP TABLE IF EXISTS refresh_transfer_keys_default; DROP TABLE IF EXISTS deposits_default; --DROP TABLE IF EXISTS deposits_by_ready_default; --DROP TABLE IF EXISTS deposits_for_matching_default; DROP TABLE IF EXISTS refunds_default; DROP TABLE IF EXISTS wire_out_default; DROP TABLE IF EXISTS aggregation_transient_default; DROP TABLE IF EXISTS aggregation_tracking_default; DROP TABLE IF EXISTS recoup_default; DROP TABLE IF EXISTS recoup_by_reserve_default; DROP TABLE IF EXISTS recoup_refresh_default; DROP TABLE IF EXISTS prewire_default; DROP TABLE IF EXISTS cs_nonce_locks_default; DROP TABLE IF EXISTS purse_requests_default; DROP TABLE IF EXISTS purse_decision_default; DROP TABLE IF EXISTS purse_merges_default; DROP TABLE IF EXISTS account_merges_default; DROP TABLE IF EXISTS purse_deposits_default; DROP TABLE IF EXISTS contracts_default; DROP TABLE IF EXISTS wad_out_entries_default; DROP TABLE IF EXISTS wads_in_default; DROP TABLE IF EXISTS wad_in_entries_default; END $$; COMMENT ON FUNCTION drop_default_partitions IS 'Drop all default partitions once other partitions are attached. Might be needed in sharding too.'; CREATE OR REPLACE FUNCTION create_partitions( num_partitions INTEGER ) RETURNS VOID LANGUAGE plpgsql AS $$ DECLARE modulus INTEGER; BEGIN modulus := num_partitions; PERFORM detach_default_partitions(); LOOP PERFORM create_hash_partition( 'wire_targets' ,modulus ,num_partitions ); PERFORM add_constraints_to_wire_targets_partition(num_partitions::varchar); PERFORM create_hash_partition( 'reserves' ,modulus ,num_partitions ); PERFORM create_hash_partition( 'reserves_in' ,modulus ,num_partitions ); PERFORM add_constraints_to_reserves_in_partition(num_partitions::varchar); PERFORM create_hash_partition( 'reserves_close' ,modulus ,num_partitions ); PERFORM add_constraints_to_reserves_close_partition(num_partitions::varchar); PERFORM create_hash_partition( 'reserves_out' ,modulus ,num_partitions ); PERFORM add_constraints_to_reserves_out_partition(num_partitions::varchar); PERFORM create_hash_partition( 'reserves_out_by_reserve' ,modulus ,num_partitions ); PERFORM create_hash_partition( 'known_coins' ,modulus ,num_partitions ); PERFORM add_constraints_to_known_coins_partition(num_partitions::varchar); PERFORM create_hash_partition( 'refresh_commitments' ,modulus ,num_partitions ); PERFORM add_constraints_to_refresh_commitments_partition(num_partitions::varchar); PERFORM create_hash_partition( 'refresh_revealed_coins' ,modulus ,num_partitions ); PERFORM add_constraints_to_refresh_revealed_coins_partition(num_partitions::varchar); PERFORM create_hash_partition( 'refresh_transfer_keys' ,modulus ,num_partitions ); PERFORM add_constraints_to_refresh_transfer_keys_partition(num_partitions::varchar); PERFORM create_hash_partition( 'deposits' ,modulus ,num_partitions ); PERFORM add_constraints_to_deposits_partition(num_partitions::varchar); -- TODO: dynamically (!) creating/deleting deposits partitions: -- create new partitions 'as needed', drop old ones once the aggregator has made -- them empty; as 'new' deposits will always have deadlines in the future, this -- would basically guarantee no conflict between aggregator and exchange service! -- SEE also: https://www.cybertec-postgresql.com/en/automatic-partition-creation-in-postgresql/ -- (article is slightly wrong, as this works:) --CREATE TABLE tab ( -- id bigint GENERATED ALWAYS AS IDENTITY, -- ts timestamp NOT NULL, -- data text -- PARTITION BY LIST ((ts::date)); -- CREATE TABLE tab_def PARTITION OF tab DEFAULT; -- BEGIN -- CREATE TABLE tab_part2 (LIKE tab); -- insert into tab_part2 (id,ts, data) values (5,'2022-03-21', 'foo'); -- alter table tab attach partition tab_part2 for values in ('2022-03-21'); -- commit; -- Naturally, to ensure this is actually 100% conflict-free, we'd -- need to create tables at the granularity of the wire/refund deadlines; -- that is right now configurable via AGGREGATOR_SHIFT option. -- FIXME: range partitioning -- PERFORM create_range_partition( -- 'deposits_by_ready' -- ,modulus -- ,num_partitions -- ); -- -- PERFORM create_range_partition( -- 'deposits_for_matching' -- ,modulus -- ,num_partitions -- ); PERFORM create_hash_partition( 'refunds' ,modulus ,num_partitions ); PERFORM add_constraints_to_refunds_partition(num_partitions::varchar); PERFORM create_hash_partition( 'wire_out' ,modulus ,num_partitions ); PERFORM add_constraints_to_wire_out_partition(num_partitions::varchar); PERFORM create_hash_partition( 'aggregation_transient' ,modulus ,num_partitions ); PERFORM create_hash_partition( 'aggregation_tracking' ,modulus ,num_partitions ); PERFORM add_constraints_to_aggregation_tracking_partition(num_partitions::varchar); PERFORM create_hash_partition( 'recoup' ,modulus ,num_partitions ); PERFORM add_constraints_to_recoup_partition(num_partitions::varchar); PERFORM create_hash_partition( 'recoup_by_reserve' ,modulus ,num_partitions ); PERFORM create_hash_partition( 'recoup_refresh' ,modulus ,num_partitions ); PERFORM add_constraints_to_recoup_refresh_partition(num_partitions::varchar); PERFORM create_hash_partition( 'prewire' ,modulus ,num_partitions ); PERFORM create_hash_partition( 'cs_nonce_locks' ,modulus ,num_partitions ); PERFORM add_constraints_to_cs_nonce_locks_partition(num_partitions::varchar); PERFORM create_hash_partition( 'close_requests' ,modulus ,num_partitions ); PERFORM create_hash_partition( 'reserves_open_requests' ,modulus ,num_partitions ); PERFORM add_constraints_to_reserves_open_request_partition(num_partitions::varchar); PERFORM create_hash_partition( 'history_requests' ,modulus ,num_partitions ); ---------------- P2P ---------------------- PERFORM create_hash_partition( 'purse_requests' ,modulus ,num_partitions ); PERFORM add_constraints_to_purse_requests_partition(num_partitions::varchar); PERFORM create_hash_partition( 'purse_decision' ,modulus ,num_partitions ); PERFORM add_constraints_to_purse_decision_partition(num_partitions::varchar); PERFORM create_hash_partition( 'purse_merges' ,modulus ,num_partitions ); PERFORM add_constraints_to_purse_merges_partition(num_partitions::varchar); PERFORM create_hash_partition( 'account_merges' ,modulus ,num_partitions ); PERFORM add_constraints_to_account_merges_partition(num_partitions::varchar); PERFORM create_hash_partition( 'contracts' ,modulus ,num_partitions ); PERFORM add_constraints_to_contracts_partition(num_partitions::varchar); PERFORM create_hash_partition( 'purse_deposits' ,modulus ,num_partitions ); PERFORM add_constraints_to_purse_deposits_partition(num_partitions::varchar); PERFORM create_hash_partition( 'wad_out_entries' ,modulus ,num_partitions ); PERFORM add_constraints_to_wad_out_entries_partition(num_partitions::varchar); PERFORM create_hash_partition( 'wads_in' ,modulus ,num_partitions ); PERFORM add_constraints_to_wads_in_partition(num_partitions::varchar); PERFORM create_hash_partition( 'wad_in_entries' ,modulus ,num_partitions ); PERFORM add_constraints_to_wad_in_entries_partition(num_partitions::varchar); num_partitions=num_partitions-1; EXIT WHEN num_partitions=0; END LOOP; PERFORM drop_default_partitions(); END $$;