-- -- E-Maj: migration from 2.0.0 to 2.0.1 -- -- This software is distributed under the GNU General Public License. -- -- This script upgrades an existing installation of E-Maj extension. -- -- complain if this script is executed in psql, rather than via an ALTER EXTENSION statement \echo Use "ALTER EXTENSION emaj UPDATE TO..." to upgrade the E-Maj extension. \quit --SET client_min_messages TO WARNING; SET client_min_messages TO NOTICE; ------------------------------------ -- -- -- checks -- -- -- ------------------------------------ -- Check that the upgrade conditions are met. DO $do$ DECLARE v_emajVersion TEXT; v_groupList TEXT; BEGIN -- check the current role is a superuser PERFORM 0 FROM pg_roles WHERE rolname = current_user AND rolsuper; IF NOT FOUND THEN RAISE EXCEPTION 'E-Maj upgrade: the current user (%) is not a superuser.', current_user; END IF; -- the emaj version registered in emaj_param must be '2.0.0' SELECT param_value_text INTO v_emajVersion FROM emaj.emaj_param WHERE param_key = 'emaj_version'; IF v_emajVersion <> '2.0.0' THEN RAISE EXCEPTION 'E-Maj upgrade: the current E-Maj version (%) is not 2.0.0',v_emajVersion; END IF; -- the installed postgres version must be at least 9.1 IF current_setting('server_version_num')::int < 90100 THEN RAISE EXCEPTION 'E-Maj upgrade: the current PostgreSQL version (%) is not compatible with E-Maj 2.0.1. The PostgreSQL version should be at least 9.1.', current_setting('server_version'); END IF; -- no existing group must have been created with a postgres version prior 8.4 SELECT string_agg(group_name, ', ') INTO v_groupList FROM emaj.emaj_group WHERE cast(to_number(substring(group_pg_version FROM E'^(\\d+)'),'99') * 100 + to_number(substring(group_pg_version FROM E'^\\d+\\.(\\d+)'),'99') AS INTEGER) < 804; IF v_groupList IS NOT NULL THEN RAISE EXCEPTION 'E-Maj upgrade: groups "%" have been created with a too old postgres version (< 8.4). Drop these groups before upgrading. ',v_groupList; END IF; END; $do$; -- OK, the upgrade operation can start... -- lock emaj_group table to avoid any concurrent E-Maj activity LOCK TABLE emaj.emaj_group IN EXCLUSIVE MODE; DO $do$ DECLARE BEGIN -- if tspemaj tablespace exists, use it as default_tablespace for emaj tables creation -- and grant the create rights on it to emaj_adm PERFORM 0 FROM pg_tablespace WHERE spcname = 'tspemaj'; IF FOUND THEN SET default_tablespace TO tspemaj; GRANT CREATE ON TABLESPACE tspemaj TO emaj_adm; END IF; END; $do$; -- disable the event triggers SELECT emaj._disable_event_triggers(); --------------------------------------- -- -- -- emaj tables, views and sequences -- -- -- --------------------------------------- -- -- process the emaj_relation table -- -- create a temporary table with the old structure and copy the source content CREATE TEMP TABLE tmp_relation_old (LIKE emaj.emaj_relation); INSERT INTO tmp_relation_old SELECT * FROM emaj.emaj_relation; -- drop the old table -- before, function referencing the table type must be dropped DROP FUNCTION emaj._drop_tbl(r_rel emaj.emaj_relation); DROP FUNCTION emaj._drop_seq(r_rel emaj.emaj_relation); DROP FUNCTION emaj._rlbk_tbl(r_rel emaj.emaj_relation, v_minGlobalSeq BIGINT, v_maxGlobalSeq BIGINT, v_nbSession INT, v_isLoggedRlbk BOOLEAN); DROP FUNCTION emaj._rlbk_seq(r_rel emaj.emaj_relation, v_timeId BIGINT); DROP FUNCTION emaj._delete_log_tbl(r_rel emaj.emaj_relation, v_beginTimeId BIGINT, v_endTimeId BIGINT, v_lastGlobalSeq BIGINT); DROP FUNCTION emaj._log_stat_tbl(r_rel emaj.emaj_relation, v_beginTimeId BIGINT, v_endTimeId BIGINT); DROP FUNCTION emaj._gen_sql_tbl(r_rel emaj.emaj_relation, v_conditions TEXT); -- and drop the table (the CASCADE clause could have been used, but a bug in extension management blocks its use) DROP TABLE emaj.emaj_relation; -- create the new table, with its indexes, comment, constraints (except foreign key)... CREATE TABLE emaj.emaj_relation ( rel_schema TEXT NOT NULL, -- schema name containing the relation rel_tblseq TEXT NOT NULL, -- application table or sequence name rel_group TEXT NOT NULL, -- name of the group that owns the relation rel_kind TEXT, -- similar to the relkind column of pg_class table -- ('r' = table, 'S' = sequence) rel_priority INTEGER, -- priority level of processing inside the group -- next columns are specific for tables and remain NULL for sequences rel_log_schema TEXT, -- schema for the log table, functions and sequence rel_log_table TEXT, -- name of the log table associated rel_log_dat_tsp TEXT, -- tablespace for the log table rel_log_index TEXT, -- name of the index of the log table rel_log_idx_tsp TEXT, -- tablespace for the log index rel_log_sequence TEXT, -- name of the log sequence rel_log_function TEXT, -- name of the function associated to the log trigger -- created on the application table rel_sql_columns TEXT, -- piece of sql used to rollback: list of the columns rel_sql_pk_columns TEXT, -- piece of sql used to rollback: list of the pk columns rel_sql_pk_eq_conditions TEXT, -- piece of sql used to rollback: equality conditions on the pk columns PRIMARY KEY (rel_schema, rel_tblseq) ); COMMENT ON TABLE emaj.emaj_relation IS $$Contains the content (tables and sequences) of created E-Maj groups.$$; -- index on emaj_relation used to speedup most functions working on groups with large E-Maj configuration CREATE INDEX emaj_relation_idx1 ON emaj.emaj_relation (rel_group, rel_kind); -- index on emaj_relation used to speedup _verify_schema() with large E-Maj configuration CREATE INDEX emaj_relation_idx2 ON emaj.emaj_relation (rel_log_schema); -- populate the new table -- we need to compute the 3 new columns by reading the table structure from the catalog DO $do$ DECLARE v_fullTableName TEXT; v_colList TEXT; v_pkColList TEXT; v_pkCondList TEXT; r_rel RECORD; BEGIN FOR r_rel IN SELECT * FROM tmp_relation_old ORDER BY rel_schema, rel_tblseq LOOP v_fullTableName = quote_ident(r_rel.rel_schema) || '.' || quote_ident(r_rel.rel_tblseq); -- Build the pieces of SQL statements that will be needed at table rollback time -- build the tables's columns list SELECT string_agg(col_name, ',') INTO v_colList FROM ( SELECT 'tbl.' || quote_ident(attname) AS col_name FROM pg_catalog.pg_attribute WHERE attrelid = v_fullTableName::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum) AS t; -- build the pkey columns list and the "equality on the primary key" conditions SELECT string_agg(col_pk_name, ','), string_agg(col_pk_cond, ' AND ') INTO v_pkColList, v_pkCondList FROM ( SELECT quote_ident(attname) AS col_pk_name, 'tbl.' || quote_ident(attname) || ' = keys.' || quote_ident(attname) AS col_pk_cond FROM pg_catalog.pg_attribute, pg_catalog.pg_index WHERE pg_attribute.attrelid = pg_index.indrelid AND attnum = ANY (indkey) AND indrelid = v_fullTableName::regclass AND indisprimary AND attnum > 0 AND attisdropped = false ORDER BY attnum) AS t; -- insert the new row into emaj_relation INSERT INTO emaj.emaj_relation VALUES (r_rel.rel_schema, r_rel.rel_tblseq, r_rel.rel_group, r_rel.rel_kind, r_rel.rel_priority, r_rel.rel_log_schema, r_rel.rel_log_table, r_rel.rel_log_dat_tsp, r_rel.rel_log_index, r_rel.rel_log_idx_tsp, r_rel.rel_log_sequence, r_rel.rel_log_function, v_colList, v_pkColList, v_pkCondList); END LOOP; END; $do$; -- and finaly drop the temporary table DROP TABLE tmp_relation_old; -- create the foreign keys dropped at the beginning of the tables upgrade or not yet created for the new tables ALTER TABLE emaj.emaj_relation ADD FOREIGN KEY (rel_group) REFERENCES emaj.emaj_group (group_name); -- register created or recreated tables and sequences as "dumpable" by pg_dump -- in this upgrade script, fix the missing statements in the unpackaged to 1.3.1 and the 1.3.1 to 2.0.0 upgrade scripts SELECT pg_catalog.pg_extension_config_dump('emaj_param','WHERE param_key <> ''emaj_version'''); SELECT pg_catalog.pg_extension_config_dump('emaj_hist',''); SELECT pg_catalog.pg_extension_config_dump('emaj_group_def',''); SELECT pg_catalog.pg_extension_config_dump('emaj_time_stamp',''); SELECT pg_catalog.pg_extension_config_dump('emaj_group',''); SELECT pg_catalog.pg_extension_config_dump('emaj_relation',''); SELECT pg_catalog.pg_extension_config_dump('emaj_mark',''); SELECT pg_catalog.pg_extension_config_dump('emaj_sequence',''); SELECT pg_catalog.pg_extension_config_dump('emaj_seq_hole',''); SELECT pg_catalog.pg_extension_config_dump('emaj_rlbk',''); SELECT pg_catalog.pg_extension_config_dump('emaj_rlbk_session',''); SELECT pg_catalog.pg_extension_config_dump('emaj_rlbk_plan',''); SELECT pg_catalog.pg_extension_config_dump('emaj_rlbk_stat',''); SELECT pg_catalog.pg_extension_config_dump('emaj_global_seq',''); SELECT pg_catalog.pg_extension_config_dump('emaj.emaj_hist_hist_id_seq',''); SELECT pg_catalog.pg_extension_config_dump('emaj.emaj_time_stamp_time_id_seq',''); SELECT pg_catalog.pg_extension_config_dump('emaj.emaj_mark_mark_id_seq',''); SELECT pg_catalog.pg_extension_config_dump('emaj.emaj_rlbk_rlbk_id_seq',''); ------------------------------------ -- -- -- emaj types -- -- -- ------------------------------------ ------------------------------------ -- -- -- emaj functions -- -- -- ------------------------------------ -- recreate functions that have been previously dropped in the tables structure upgrade step and will not be recreated later in this script CREATE OR REPLACE FUNCTION emaj._delete_log_tbl(r_rel emaj.emaj_relation, v_beginTimeId BIGINT, v_endTimeId BIGINT, v_lastGlobalSeq BIGINT) RETURNS BIGINT LANGUAGE plpgsql AS $_delete_log_tbl$ -- This function deletes the part of a log table corresponding to updates that have been rolled back. -- The function is only called by emaj._rlbk_session_exec(), for unlogged rollbacks. -- It deletes sequences records corresponding to marks that are not visible anymore after the rollback. -- It also registers the hole in sequence numbers generated by the deleted log rows. -- Input: row from emaj_relation corresponding to the appplication table to proccess, -- begin and end time stamp ids to define the time range identifying the hole to create in the log sequence -- global sequence value limit for rollback, mark timestamp, -- flag to specify if the rollback is logged -- Output: deleted rows DECLARE v_nbRows BIGINT; BEGIN -- delete obsolete log rows EXECUTE 'DELETE FROM ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_table) || ' WHERE emaj_gid > ' || v_lastGlobalSeq; GET DIAGNOSTICS v_nbRows = ROW_COUNT; -- record the sequence holes generated by the delete operation -- this is due to the fact that log sequences are not rolled back, this information will be used by the emaj_log_stat_group -- function (and indirectly by emaj_estimate_rollback_group() and emaj_estimate_rollback_groups()) -- first delete, if exist, sequence holes that have disappeared with the rollback DELETE FROM emaj.emaj_seq_hole WHERE sqhl_schema = r_rel.rel_schema AND sqhl_table = r_rel.rel_tblseq AND sqhl_begin_time_id >= v_beginTimeId AND sqhl_begin_time_id < v_endTimeId; -- and then insert the new sequence hole EXECUTE 'INSERT INTO emaj.emaj_seq_hole (sqhl_schema, sqhl_table, sqhl_begin_time_id, sqhl_end_time_id, sqhl_hole_size) VALUES (' || quote_literal(r_rel.rel_schema) || ',' || quote_literal(r_rel.rel_tblseq) || ',' || v_beginTimeId || ',' || v_endTimeId || ', (' || ' SELECT CASE WHEN is_called THEN last_value + increment_by ELSE last_value END FROM ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_sequence) || ')-(' || ' SELECT CASE WHEN sequ_is_called THEN sequ_last_val + sequ_increment ELSE sequ_last_val END FROM ' || ' emaj.emaj_sequence WHERE' || ' sequ_schema = ' || quote_literal(r_rel.rel_log_schema) || ' AND sequ_name = ' || quote_literal(r_rel.rel_log_sequence) || ' AND sequ_time_id = ' || v_beginTimeId || '))'; RETURN v_nbRows; END; $_delete_log_tbl$; CREATE OR REPLACE FUNCTION emaj._rlbk_seq(r_rel emaj.emaj_relation, v_timeId BIGINT) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $_rlbk_seq$ -- This function rollbacks one application sequence to a given mark -- The function is called by emaj.emaj._rlbk_end() -- Input: the emaj_group_def row related to the application sequence to process, time id of the mark to rollback to -- The function is defined as SECURITY DEFINER so that emaj_adm role can use it even if it is not the owner of the application sequence. DECLARE v_fullSeqName TEXT; v_stmt TEXT; mark_seq_rec RECORD; curr_seq_rec RECORD; BEGIN -- Read sequence's characteristics at mark time BEGIN SELECT sequ_schema, sequ_name, sequ_last_val, sequ_start_val, sequ_increment, sequ_max_val, sequ_min_val, sequ_cache_val, sequ_is_cycled, sequ_is_called INTO STRICT mark_seq_rec FROM emaj.emaj_sequence WHERE sequ_schema = r_rel.rel_schema AND sequ_name = r_rel.rel_tblseq AND sequ_time_id = v_timeId; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION '_rlbk_seq: Mark at time id "%" not found for sequence "%.%".', v_timeId, r_rel.rel_schema, r_rel.rel_tblseq; END; -- Read the current sequence's characteristics v_fullSeqName = quote_ident(r_rel.rel_schema) || '.' || quote_ident(r_rel.rel_tblseq); EXECUTE 'SELECT last_value, start_value, increment_by, max_value, min_value, cache_value, is_cycled, is_called FROM ' || v_fullSeqName INTO STRICT curr_seq_rec; -- Build the ALTER SEQUENCE statement, depending on the differences between the present values and the related -- values at the requested mark time v_stmt=''; IF curr_seq_rec.last_value <> mark_seq_rec.sequ_last_val OR curr_seq_rec.is_called <> mark_seq_rec.sequ_is_called THEN IF mark_seq_rec.sequ_is_called THEN v_stmt=v_stmt || ' RESTART ' || mark_seq_rec.sequ_last_val + mark_seq_rec.sequ_increment; ELSE v_stmt=v_stmt || ' RESTART ' || mark_seq_rec.sequ_last_val; END IF; END IF; IF curr_seq_rec.start_value <> mark_seq_rec.sequ_start_val THEN v_stmt=v_stmt || ' START ' || mark_seq_rec.sequ_start_val; END IF; IF curr_seq_rec.increment_by <> mark_seq_rec.sequ_increment THEN v_stmt=v_stmt || ' INCREMENT ' || mark_seq_rec.sequ_increment; END IF; IF curr_seq_rec.min_value <> mark_seq_rec.sequ_min_val THEN v_stmt=v_stmt || ' MINVALUE ' || mark_seq_rec.sequ_min_val; END IF; IF curr_seq_rec.max_value <> mark_seq_rec.sequ_max_val THEN v_stmt=v_stmt || ' MAXVALUE ' || mark_seq_rec.sequ_max_val; END IF; IF curr_seq_rec.cache_value <> mark_seq_rec.sequ_cache_val THEN v_stmt=v_stmt || ' CACHE ' || mark_seq_rec.sequ_cache_val; END IF; IF curr_seq_rec.is_cycled <> mark_seq_rec.sequ_is_cycled THEN IF mark_seq_rec.sequ_is_cycled = 'f' THEN v_stmt=v_stmt || ' NO '; END IF; v_stmt=v_stmt || ' CYCLE '; END IF; -- and execute the statement if at least one parameter has changed IF v_stmt <> '' THEN EXECUTE 'ALTER SEQUENCE ' || v_fullSeqName || v_stmt; END IF; -- insert event in history INSERT INTO emaj.emaj_hist (hist_function, hist_object, hist_wording) VALUES ('ROLLBACK_SEQUENCE', v_fullSeqName, substr(v_stmt,2)); RETURN; END; $_rlbk_seq$; CREATE OR REPLACE FUNCTION emaj._log_stat_tbl(r_rel emaj.emaj_relation, v_beginTimeId BIGINT, v_endTimeId BIGINT) RETURNS BIGINT LANGUAGE plpgsql AS $_log_stat_tbl$ -- This function returns the number of log rows for a single table between 2 time stamps or between a time stamp and the current situation. -- It is called by the emaj_log_stat_group(), _rlbk_planning(), _rlbk_start_mark() and _gen_sql_groups() functions. -- These statistics are computed using the serial id of log tables and holes is sequences recorded into emaj_seq_hole at rollback time or -- rollback consolidation time. -- Input: schema name and table name, log schema, the time stamp ids defining the time range to examine -- a end time stamp id set to NULL indicates the current situation -- Output: number of log rows between both marks for the table DECLARE v_beginLastValue BIGINT; v_endLastValue BIGINT; v_sumHole BIGINT; BEGIN -- get the log table id at begin time id SELECT CASE WHEN sequ_is_called THEN sequ_last_val ELSE sequ_last_val - sequ_increment END INTO v_beginLastValue FROM emaj.emaj_sequence WHERE sequ_schema = r_rel.rel_log_schema AND sequ_name = r_rel.rel_log_sequence AND sequ_time_id = v_beginTimeId; IF v_endTimeId IS NULL THEN -- last time id is NULL, so examine the current state of the log table id EXECUTE 'SELECT CASE WHEN is_called THEN last_value ELSE last_value - increment_by END FROM ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_sequence) INTO v_endLastValue; -- and count the sum of hole from the start time to now SELECT coalesce(sum(sqhl_hole_size),0) INTO v_sumHole FROM emaj.emaj_seq_hole WHERE sqhl_schema = r_rel.rel_schema AND sqhl_table = r_rel.rel_tblseq AND sqhl_begin_time_id >= v_beginTimeId; ELSE -- last time id is not NULL, so get the log table id at end time id SELECT CASE WHEN sequ_is_called THEN sequ_last_val ELSE sequ_last_val - sequ_increment END INTO v_endLastValue FROM emaj.emaj_sequence WHERE sequ_schema = r_rel.rel_log_schema AND sequ_name = r_rel.rel_log_sequence AND sequ_time_id = v_endTimeId; -- and count the sum of hole from the start time to the end time SELECT coalesce(sum(sqhl_hole_size),0) INTO v_sumHole FROM emaj.emaj_seq_hole WHERE sqhl_schema = r_rel.rel_schema AND sqhl_table = r_rel.rel_tblseq AND sqhl_begin_time_id >= v_beginTimeId AND sqhl_end_time_id <= v_endTimeId; END IF; -- return the stat row for the table RETURN (v_endLastValue - v_beginLastValue - v_sumHole); END; $_log_stat_tbl$; CREATE OR REPLACE FUNCTION emaj._gen_sql_tbl(r_rel emaj.emaj_relation, v_conditions TEXT) RETURNS BIGINT LANGUAGE plpgsql SECURITY DEFINER SET standard_conforming_strings = ON AS $_gen_sql_tbl$ -- This function generates SQL commands representing all updates performed on a table between 2 marks -- or beetween a mark and the current situation. These command are stored into a temporary table created -- by the _gen_sql_groups() calling function. -- Input: - fully qualified application table to process -- - fully qualified associated log table -- - sql conditions corresponding to the marks range to process -- Output: number of generated SQL statements DECLARE v_fullTableName TEXT; v_logTableName TEXT; v_valList TEXT; v_setList TEXT; v_pkCondList TEXT; v_unquotedType TEXT[] = array['smallint','integer','bigint','numeric','decimal', 'int2','int4','int8','serial','bigserial', 'real','double precision','float','float4','float8','oid']; v_rqInsert TEXT; v_rqUpdate TEXT; v_rqDelete TEXT; v_rqTruncate TEXT; v_nbSQL BIGINT; r_col RECORD; BEGIN -- build schema specified table name and log table name v_fullTableName = quote_ident(r_rel.rel_schema) || '.' || quote_ident(r_rel.rel_tblseq); v_logTableName = quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_table); -- retrieve from pg_attribute all columns of the application table and build : -- - the VALUES list used in the INSERT statements -- - the SET list used in the UPDATE statements v_valList = ''; v_setList = ''; FOR r_col IN SELECT attname, format_type(atttypid,atttypmod) FROM pg_catalog.pg_attribute WHERE attrelid = v_fullTableName ::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum LOOP -- test if the column format (up to the parenthesis) belongs to the list of formats that do not require any quotation (like numeric data types) IF regexp_replace(r_col.format_type,E'\\(.*$','') = ANY(v_unquotedType) THEN -- literal for this column can remain as is v_valList = v_valList || ''' || coalesce(o.' || quote_ident(r_col.attname) || '::text,''NULL'') || '', '; v_setList = v_setList || quote_ident(replace(r_col.attname,'''','''''')) || ' = '' || coalesce(n.' || quote_ident(r_col.attname) || ' ::text,''NULL'') || '', '; ELSE -- literal for this column must be quoted v_valList = v_valList || ''' || quote_nullable(o.' || quote_ident(r_col.attname) || ') || '', '; v_setList = v_setList || quote_ident(replace(r_col.attname,'''','''''')) || ' = '' || quote_nullable(n.' || quote_ident(r_col.attname) || ') || '', '; END IF; END LOOP; -- suppress the final separators v_valList = substring(v_valList FROM 1 FOR char_length(v_valList) - 2); v_setList = substring(v_setList FROM 1 FOR char_length(v_setList) - 2); -- retrieve all columns that represents the pkey and build the "pkey equal" conditions set that will be used in UPDATE and DELETE statements -- (taking column names in pg_attribute from the table's definition instead of index definition is mandatory -- starting from pg9.0, joining tables with indkey instead of indexrelid) v_pkCondList = ''; FOR r_col IN SELECT attname, format_type(atttypid,atttypmod) FROM pg_catalog.pg_attribute, pg_catalog.pg_index WHERE pg_attribute.attrelid = pg_index.indrelid AND attnum = ANY (indkey) AND indrelid = v_fullTableName ::regclass AND indisprimary AND attnum > 0 AND NOT attisdropped LOOP -- test if the column format (at least up to the parenthesis) belongs to the list of formats that do not require any quotation (like numeric data types) IF regexp_replace (r_col.format_type,E'\\(.*$','') = ANY(v_unquotedType) THEN -- literal for this column can remain as is -- v_pkCondList = v_pkCondList || quote_ident(replace(r_col.attname,'''','''''')) || ' = '' || o.' || quote_ident(r_col.attname) || ' || ''::' || r_col.format_type || ' AND '; v_pkCondList = v_pkCondList || quote_ident(replace(r_col.attname,'''','''''')) || ' = '' || o.' || quote_ident(r_col.attname) || ' || '' AND '; ELSE -- literal for this column must be quoted -- v_pkCondList = v_pkCondList || quote_ident(replace(r_col.attname,'''','''''')) || ' = '' || quote_literal(o.' || quote_ident(r_col.attname) || ') || ''::' || r_col.format_type || ' AND '; v_pkCondList = v_pkCondList || quote_ident(replace(r_col.attname,'''','''''')) || ' = '' || quote_literal(o.' || quote_ident(r_col.attname) || ') || '' AND '; END IF; END LOOP; -- suppress the final separator v_pkCondList = substring(v_pkCondList FROM 1 FOR char_length(v_pkCondList) - 5); -- prepare sql skeletons for each statement type v_rqInsert = '''INSERT INTO ' || replace(v_fullTableName,'''','''''') || ' VALUES (' || v_valList || ');'''; v_rqUpdate = '''UPDATE ONLY ' || replace(v_fullTableName,'''','''''') || ' SET ' || v_setList || ' WHERE ' || v_pkCondList || ';'''; v_rqDelete = '''DELETE FROM ONLY ' || replace(v_fullTableName,'''','''''') || ' WHERE ' || v_pkCondList || ';'''; v_rqTruncate = '''TRUNCATE ' || replace(v_fullTableName,'''','''''') || ';'''; -- now scan the log table to process all statement types at once EXECUTE 'INSERT INTO emaj_temp_script ' || 'SELECT o.emaj_gid, 0, o.emaj_txid, CASE ' || ' WHEN o.emaj_verb = ''INS'' THEN ' || v_rqInsert || ' WHEN o.emaj_verb = ''UPD'' AND o.emaj_tuple = ''OLD'' THEN ' || v_rqUpdate || ' WHEN o.emaj_verb = ''DEL'' THEN ' || v_rqDelete || ' WHEN o.emaj_verb = ''TRU'' THEN ' || v_rqTruncate || ' END ' || ' FROM ' || v_logTableName || ' o' || ' LEFT OUTER JOIN ' || v_logTableName || ' n ON n.emaj_gid = o.emaj_gid' || ' AND (n.emaj_verb = ''UPD'' AND n.emaj_tuple = ''NEW'') ' || ' WHERE NOT (o.emaj_verb = ''UPD'' AND o.emaj_tuple = ''NEW'')' || ' AND ' || v_conditions; GET DIAGNOSTICS v_nbSQL = ROW_COUNT; RETURN v_nbSQL; END; $_gen_sql_tbl$; -- pattern used by the tool that extracts and insert the functions definition ------------------------------------------------------------------ -- drop obsolete functions or functions with modified interface -- ------------------------------------------------------------------ DROP FUNCTION emaj._rlbk_check(V_GROUPNAMES TEXT[],V_MARK TEXT); DROP FUNCTION emaj._enable_event_triggers(V_EVENT_TRIGGER_ARRAY TEXT[]); ------------------------------------------------------------------ -- create new or modified functions -- ------------------------------------------------------------------ CREATE OR REPLACE FUNCTION emaj._create_tbl(r_grpdef emaj.emaj_group_def, v_groupName TEXT, v_isRollbackable BOOLEAN, v_defTsp TEXT) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $_create_tbl$ -- This function creates all what is needed to manage the log and rollback operations for an application table -- Input: the emaj_group_def row related to the application table to process, the group name, a boolean indicating whether the group is rollbackable, and the default tablespace to use if no specific tablespace is set for this application table -- Are created in the log schema: -- - the associated log table, with its own sequence -- - the function that logs the tables updates, defined as a trigger -- The function is defined as SECURITY DEFINER so that emaj_adm role can use it even if he is not the owner of the application table. DECLARE v_emajSchema TEXT = 'emaj'; v_schemaPrefix TEXT = 'emaj'; v_relPersistence CHAR(1); v_relhaspkey BOOLEAN; v_emajNamesPrefix TEXT; v_baseLogTableName TEXT; v_baseLogIdxName TEXT; v_baseLogFnctName TEXT; v_baseSequenceName TEXT; v_logSchema TEXT; v_fullTableName TEXT; v_logTableName TEXT; v_logIdxName TEXT; v_logFnctName TEXT; v_sequenceName TEXT; v_logDatTsp TEXT; v_logIdxTsp TEXT; v_dataTblSpace TEXT; v_idxTblSpace TEXT; v_colList TEXT; v_pkColList TEXT; v_pkCondList TEXT; v_stmt TEXT; v_triggerList TEXT; BEGIN -- check the table is neither a temporary nor an unlogged table SELECT relpersistence INTO v_relPersistence FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = r_grpdef.grpdef_schema AND relname = r_grpdef.grpdef_tblseq; IF v_relPersistence = 't' THEN RAISE EXCEPTION '_create_tbl: table "%" is a temporary table.', r_grpdef.grpdef_tblseq; ELSIF v_relPersistence = 'u' THEN RAISE EXCEPTION '_create_tbl: table "%.%" is an unlogged table.', r_grpdef.grpdef_schema, r_grpdef.grpdef_tblseq; END IF; -- check the table has a primary key SELECT true INTO v_relhaspkey FROM pg_catalog.pg_class, pg_catalog.pg_namespace, pg_catalog.pg_constraint WHERE relnamespace = pg_namespace.oid AND connamespace = pg_namespace.oid AND conrelid = pg_class.oid AND contype = 'p' AND nspname = r_grpdef.grpdef_schema AND relname = r_grpdef.grpdef_tblseq; IF NOT FOUND THEN v_relhaspkey = false; END IF; IF v_isRollbackable AND v_relhaspkey = FALSE THEN RAISE EXCEPTION '_create_tbl: table "%.%" has no PRIMARY KEY.', r_grpdef.grpdef_schema, r_grpdef.grpdef_tblseq; END IF; -- build the prefix of all emaj object to create, by default _ v_emajNamesPrefix = coalesce(r_grpdef.grpdef_emaj_names_prefix, r_grpdef.grpdef_schema || '_' || r_grpdef.grpdef_tblseq); -- build the name of emaj components associated to the application table (non schema qualified and not quoted) v_baseLogTableName = v_emajNamesPrefix || '_log'; v_baseLogIdxName = v_emajNamesPrefix || '_log_idx'; v_baseLogFnctName = v_emajNamesPrefix || '_log_fnct'; v_baseSequenceName = v_emajNamesPrefix || '_log_seq'; -- build the different name for table, trigger, functions,... v_logSchema = coalesce(v_schemaPrefix || r_grpdef.grpdef_log_schema_suffix, v_emajSchema); v_fullTableName = quote_ident(r_grpdef.grpdef_schema) || '.' || quote_ident(r_grpdef.grpdef_tblseq); v_logTableName = quote_ident(v_logSchema) || '.' || quote_ident(v_baseLogTableName); v_logIdxName = quote_ident(v_baseLogIdxName); v_logFnctName = quote_ident(v_logSchema) || '.' || quote_ident(v_baseLogFnctName); v_sequenceName = quote_ident(v_logSchema) || '.' || quote_ident(v_baseSequenceName); -- prepare TABLESPACE clauses for data and index v_logDatTsp = coalesce(r_grpdef.grpdef_log_dat_tsp, v_defTsp); v_logIdxTsp = coalesce(r_grpdef.grpdef_log_idx_tsp, v_defTsp); v_dataTblSpace = coalesce('TABLESPACE ' || quote_ident(v_logDatTsp),''); v_idxTblSpace = coalesce('TABLESPACE ' || quote_ident(v_logIdxTsp),''); -- Build some pieces of SQL statements that will be needed at table rollback time -- build the tables's columns list SELECT string_agg(col_name, ',') INTO v_colList FROM ( SELECT 'tbl.' || quote_ident(attname) AS col_name FROM pg_catalog.pg_attribute WHERE attrelid = v_fullTableName::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum) AS t; -- build the pkey columns list and the "equality on the primary key" conditions SELECT string_agg(col_pk_name, ','), string_agg(col_pk_cond, ' AND ') INTO v_pkColList, v_pkCondList FROM ( SELECT quote_ident(attname) AS col_pk_name, 'tbl.' || quote_ident(attname) || ' = keys.' || quote_ident(attname) AS col_pk_cond FROM pg_catalog.pg_attribute, pg_catalog.pg_index WHERE pg_attribute.attrelid = pg_index.indrelid AND attnum = ANY (indkey) AND indrelid = v_fullTableName::regclass AND indisprimary AND attnum > 0 AND attisdropped = false ORDER BY attnum) AS t; -- create the log table: it looks like the application table, with some additional technical columns EXECUTE 'DROP TABLE IF EXISTS ' || v_logTableName; EXECUTE 'CREATE TABLE ' || v_logTableName || ' (LIKE ' || v_fullTableName || ') ' || v_dataTblSpace; EXECUTE 'ALTER TABLE ' || v_logTableName || ' ADD COLUMN emaj_verb VARCHAR(3),' || ' ADD COLUMN emaj_tuple VARCHAR(3),' || ' ADD COLUMN emaj_gid BIGINT NOT NULL DEFAULT nextval(''emaj.emaj_global_seq''),' || ' ADD COLUMN emaj_changed TIMESTAMPTZ DEFAULT clock_timestamp(),' || ' ADD COLUMN emaj_txid BIGINT DEFAULT txid_current(),' || ' ADD COLUMN emaj_user VARCHAR(32) DEFAULT session_user,' || ' ADD COLUMN emaj_user_ip INET DEFAULT inet_client_addr(),' || ' ADD COLUMN emaj_user_port INT DEFAULT inet_client_port()'; -- creation of the index on the log table EXECUTE 'CREATE UNIQUE INDEX ' || v_logIdxName || ' ON ' || v_logTableName || ' (emaj_gid, emaj_tuple) ' || v_idxTblSpace; -- set the index associated to the primary key as cluster index. It may be useful for CLUSTER command. EXECUTE 'ALTER TABLE ONLY ' || v_logTableName || ' CLUSTER ON ' || v_logIdxName; -- remove the NOT NULL constraints of application columns. -- They are useless and blocking to store truncate event for tables belonging to audit_only tables SELECT string_agg(action, ',') INTO v_stmt FROM ( SELECT ' ALTER COLUMN ' || quote_ident(attname) || ' DROP NOT NULL' AS action FROM pg_catalog.pg_attribute, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND attrelid = pg_class.oid AND nspname = v_logSchema AND relname = v_baseLogTableName AND attnum > 0 AND attnotnull AND attisdropped = false AND attname NOT LIKE E'emaj\\_%') AS t; IF v_stmt IS NOT NULL THEN EXECUTE 'ALTER TABLE ' || v_logTableName || v_stmt; END IF; -- create the sequence associated to the log table EXECUTE 'CREATE SEQUENCE ' || v_sequenceName; -- creation of the log fonction that will be mapped to the log trigger later -- The new row is logged for each INSERT, the old row is logged for each DELETE -- and the old and the new rows are logged for each UPDATE. EXECUTE 'CREATE OR REPLACE FUNCTION ' || v_logFnctName || '() RETURNS TRIGGER AS $logfnct$' || 'BEGIN' -- The sequence associated to the log table is incremented at the beginning of the function ... || ' PERFORM NEXTVAL(' || quote_literal(v_sequenceName) || ');' -- ... and the global id sequence is incremented by the first/only INSERT into the log table. || ' IF (TG_OP = ''DELETE'') THEN' || ' INSERT INTO ' || v_logTableName || ' SELECT OLD.*, ''DEL'', ''OLD'';' || ' RETURN OLD;' || ' ELSIF (TG_OP = ''UPDATE'') THEN' || ' INSERT INTO ' || v_logTableName || ' SELECT OLD.*, ''UPD'', ''OLD'';' || ' INSERT INTO ' || v_logTableName || ' SELECT NEW.*, ''UPD'', ''NEW'', lastval();' || ' RETURN NEW;' || ' ELSIF (TG_OP = ''INSERT'') THEN' || ' INSERT INTO ' || v_logTableName || ' SELECT NEW.*, ''INS'', ''NEW'';' || ' RETURN NEW;' || ' END IF;' || ' RETURN NULL;' || 'END;' || '$logfnct$ LANGUAGE plpgsql SECURITY DEFINER;'; -- creation of the log trigger on the application table, using the previously created log function -- But the trigger is not immediately activated (it will be at emaj_start_group time) EXECUTE 'DROP TRIGGER IF EXISTS emaj_log_trg ON ' || v_fullTableName; EXECUTE 'CREATE TRIGGER emaj_log_trg' || ' AFTER INSERT OR UPDATE OR DELETE ON ' || v_fullTableName || ' FOR EACH ROW EXECUTE PROCEDURE ' || v_logFnctName || '()'; EXECUTE 'ALTER TABLE ' || v_fullTableName || ' DISABLE TRIGGER emaj_log_trg'; -- creation of the trigger that manage any TRUNCATE on the application table -- But the trigger is not immediately activated (it will be at emaj_start_group time) EXECUTE 'DROP TRIGGER IF EXISTS emaj_trunc_trg ON ' || v_fullTableName; IF v_isRollbackable THEN -- For rollbackable groups, use the common _forbid_truncate_fnct() function that blocks the operation EXECUTE 'CREATE TRIGGER emaj_trunc_trg' || ' BEFORE TRUNCATE ON ' || v_fullTableName || ' FOR EACH STATEMENT EXECUTE PROCEDURE emaj._forbid_truncate_fnct()'; ELSE -- For audit_only groups, use the common _log_truncate_fnct() function that records the operation into the log table EXECUTE 'CREATE TRIGGER emaj_trunc_trg' || ' BEFORE TRUNCATE ON ' || v_fullTableName || ' FOR EACH STATEMENT EXECUTE PROCEDURE emaj._log_truncate_fnct()'; END IF; EXECUTE 'ALTER TABLE ' || v_fullTableName || ' DISABLE TRIGGER emaj_trunc_trg'; -- register the table into emaj_relation INSERT INTO emaj.emaj_relation (rel_schema, rel_tblseq, rel_group, rel_priority, rel_log_schema, rel_log_dat_tsp, rel_log_idx_tsp, rel_kind, rel_log_table, rel_log_index, rel_log_sequence, rel_log_function, rel_sql_columns, rel_sql_pk_columns, rel_sql_pk_eq_conditions) VALUES (r_grpdef.grpdef_schema, r_grpdef.grpdef_tblseq, r_grpdef.grpdef_group, r_grpdef.grpdef_priority, v_logSchema, v_logDatTsp, v_logIdxTsp, 'r', v_baseLogTableName, v_baseLogIdxName, v_baseSequenceName, v_baseLogFnctName, v_colList, v_pkColList, v_pkCondList); -- -- check if the table has (neither internal - ie. created for fk - nor previously created by emaj) trigger SELECT string_agg(tgname, ', ') INTO v_triggerList FROM ( SELECT tgname FROM pg_catalog.pg_trigger WHERE tgrelid = v_fullTableName::regclass AND tgconstraint = 0 AND tgname NOT LIKE E'emaj\\_%\\_trg') AS t; -- if yes, issue a warning (if a trigger updates another table in the same table group or outside) it could generate problem at rollback time) IF v_triggerList IS NOT NULL THEN RAISE WARNING '_create_tbl: table "%" has triggers (%). Verify the compatibility with emaj rollback operations (in particular if triggers update one or several other tables). Triggers may have to be manualy disabled before rollback.', v_fullTableName, v_triggerList; END IF; -- grant appropriate rights to both emaj roles EXECUTE 'GRANT SELECT ON TABLE ' || v_logTableName || ' TO emaj_viewer'; EXECUTE 'GRANT ALL PRIVILEGES ON TABLE ' || v_logTableName || ' TO emaj_adm'; EXECUTE 'GRANT SELECT ON SEQUENCE ' || v_sequenceName || ' TO emaj_viewer'; EXECUTE 'GRANT ALL PRIVILEGES ON SEQUENCE ' || v_sequenceName || ' TO emaj_adm'; RETURN; END; $_create_tbl$; CREATE OR REPLACE FUNCTION emaj._drop_tbl(r_rel emaj.emaj_relation) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $_drop_tbl$ -- The function deletes all what has been created by _create_tbl function -- Required inputs: row from emaj_relation corresponding to the appplication table to proccess -- The function is defined as SECURITY DEFINER so that emaj_adm role can use it even if he is not the owner of the application table. DECLARE v_fullTableName TEXT; BEGIN v_fullTableName = quote_ident(r_rel.rel_schema) || '.' || quote_ident(r_rel.rel_tblseq); -- check the table exists before dropping its triggers PERFORM 0 FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = r_rel.rel_schema AND relname = r_rel.rel_tblseq AND relkind = 'r'; IF FOUND THEN -- delete the log and truncate triggers on the application table EXECUTE 'DROP TRIGGER IF EXISTS emaj_log_trg ON ' || v_fullTableName; EXECUTE 'DROP TRIGGER IF EXISTS emaj_trunc_trg ON ' || v_fullTableName; END IF; -- delete the log function EXECUTE 'DROP FUNCTION IF EXISTS ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_function) || '() CASCADE'; -- delete the sequence associated to the log table EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_sequence); -- delete the log table EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_table) || ' CASCADE'; -- delete rows related to the log sequence from emaj_sequence table DELETE FROM emaj.emaj_sequence WHERE sequ_schema = r_rel.rel_log_schema AND sequ_name = r_rel.rel_log_sequence; -- delete rows related to the table from emaj_seq_hole table DELETE FROM emaj.emaj_seq_hole WHERE sqhl_schema = quote_ident(r_rel.rel_schema) AND sqhl_table = quote_ident(r_rel.rel_tblseq); -- and finaly delete the table reference from the emaj_relation table DELETE FROM emaj.emaj_relation WHERE rel_schema = r_rel.rel_schema AND rel_tblseq = r_rel.rel_tblseq; RETURN; END; $_drop_tbl$; CREATE OR REPLACE FUNCTION emaj._drop_seq(r_rel emaj.emaj_relation) RETURNS VOID LANGUAGE plpgsql AS $_drop_seq$ -- The function deletes the rows stored into emaj_sequence for a particular sequence -- Required inputs: row from emaj_relation corresponding to the appplication sequence to proccess BEGIN -- delete rows from emaj_sequence EXECUTE 'DELETE FROM emaj.emaj_sequence WHERE sequ_schema = ' || quote_literal(r_rel.rel_schema) || ' AND sequ_name = ' || quote_literal(r_rel.rel_tblseq); -- and finaly delete the sequence reference from the emaj_relation table DELETE FROM emaj.emaj_relation WHERE rel_schema = r_rel.rel_schema AND rel_tblseq = r_rel.rel_tblseq; RETURN; END; $_drop_seq$; CREATE OR REPLACE FUNCTION emaj._rlbk_tbl(r_rel emaj.emaj_relation, v_minGlobalSeq BIGINT, v_maxGlobalSeq BIGINT, v_nbSession INT, v_isLoggedRlbk BOOLEAN) RETURNS BIGINT LANGUAGE plpgsql SECURITY DEFINER AS $_rlbk_tbl$ -- This function rollbacks one table to a given point in time represented by the value of the global sequence -- The function is called by emaj._rlbk_session_exec() -- Input: row from emaj_relation corresponding to the appplication table to proccess -- global sequence (non inclusive) lower and (inclusive) upper limits covering the rollback time frame -- Output: number of rolled back primary keys -- For unlogged rollback, the log triggers have been disabled previously and will be enabled later. -- The function is defined as SECURITY DEFINER so that emaj_adm role can use it even if he is not the owner of the application table. DECLARE v_fullTableName TEXT; v_logTableName TEXT; v_tmpTable TEXT; v_tableType TEXT; v_nbPk BIGINT; BEGIN v_fullTableName = quote_ident(r_rel.rel_schema) || '.' || quote_ident(r_rel.rel_tblseq); v_logTableName = quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_table); -- insert begin event in history INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object, hist_wording) VALUES ('ROLLBACK_TABLE', 'BEGIN', v_fullTableName, 'All log rows with emaj_gid > ' || v_minGlobalSeq || ' and <= ' || v_maxGlobalSeq); -- create the temporary table containing all primary key values with their earliest emaj_gid IF v_nbSession = 1 THEN v_tableType = 'TEMP'; v_tmpTable = 'emaj_tmp_' || pg_backend_pid(); ELSE -- with multi session parallel rollbacks, the table cannot be a TEMP table because it would not be usable in 2PC -- but it may be an UNLOGGED table v_tableType = 'UNLOGGED'; v_tmpTable = 'emaj.emaj_tmp_' || pg_backend_pid(); END IF; EXECUTE 'CREATE ' || v_tableType || ' TABLE ' || v_tmpTable || ' AS ' || ' SELECT ' || r_rel.rel_sql_pk_columns || ', min(emaj_gid) as emaj_gid' || ' FROM ' || v_logTableName || ' WHERE emaj_gid > ' || v_minGlobalSeq || 'AND emaj_gid <= ' || v_maxGlobalSeq || ' GROUP BY ' || r_rel.rel_sql_pk_columns; GET DIAGNOSTICS v_nbPk = ROW_COUNT; -- delete all rows from the application table corresponding to each touched primary key -- this deletes rows inserted or updated during the rolled back period EXECUTE 'DELETE FROM ONLY ' || v_fullTableName || ' tbl USING ' || v_tmpTable || ' keys ' || ' WHERE ' || r_rel.rel_sql_pk_eq_conditions; -- for logged rollbacks, if the number of pkey to process is greater than 1.000, ANALYZE the log table to take into account -- the impact of just inserted rows, avoiding a potentialy bad plan for the next INSERT statement IF v_isLoggedRlbk AND v_nbPk > 1000 THEN EXECUTE 'ANALYZE ' || v_logTableName; END IF; -- insert into the application table rows that were deleted or updated during the rolled back period EXECUTE 'INSERT INTO ' || v_fullTableName || ' SELECT ' || r_rel.rel_sql_columns || ' FROM ' || v_logTableName || ' tbl, ' || v_tmpTable || ' keys ' || ' WHERE ' || r_rel.rel_sql_pk_eq_conditions || ' AND tbl.emaj_gid = keys.emaj_gid AND tbl.emaj_tuple = ''OLD''' || ' AND tbl.emaj_gid > ' || v_minGlobalSeq || 'AND tbl.emaj_gid <= ' || v_maxGlobalSeq; -- drop the now useless temporary table EXECUTE 'DROP TABLE ' || v_tmpTable; -- insert end event in history INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object, hist_wording) VALUES ('ROLLBACK_TABLE', 'END', v_fullTableName, v_nbPk || ' rolled back primary keys'); RETURN v_nbPk; END; $_rlbk_tbl$; CREATE OR REPLACE FUNCTION emaj._delete_log_tbl(r_rel emaj.emaj_relation, v_beginTimeId BIGINT, v_endTimeId BIGINT, v_lastGlobalSeq BIGINT) RETURNS BIGINT LANGUAGE plpgsql AS $_delete_log_tbl$ -- This function deletes the part of a log table corresponding to updates that have been rolled back. -- The function is only called by emaj._rlbk_session_exec(), for unlogged rollbacks. -- It deletes sequences records corresponding to marks that are not visible anymore after the rollback. -- It also registers the hole in sequence numbers generated by the deleted log rows. -- Input: row from emaj_relation corresponding to the appplication table to proccess, -- begin and end time stamp ids to define the time range identifying the hole to create in the log sequence -- global sequence value limit for rollback, mark timestamp, -- flag to specify if the rollback is logged -- Output: deleted rows DECLARE v_nbRows BIGINT; BEGIN -- delete obsolete log rows EXECUTE 'DELETE FROM ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_table) || ' WHERE emaj_gid > ' || v_lastGlobalSeq; GET DIAGNOSTICS v_nbRows = ROW_COUNT; -- record the sequence holes generated by the delete operation -- this is due to the fact that log sequences are not rolled back, this information will be used by the emaj_log_stat_group -- function (and indirectly by emaj_estimate_rollback_group() and emaj_estimate_rollback_groups()) -- first delete, if exist, sequence holes that have disappeared with the rollback DELETE FROM emaj.emaj_seq_hole WHERE sqhl_schema = r_rel.rel_schema AND sqhl_table = r_rel.rel_tblseq AND sqhl_begin_time_id >= v_beginTimeId AND sqhl_begin_time_id < v_endTimeId; -- and then insert the new sequence hole EXECUTE 'INSERT INTO emaj.emaj_seq_hole (sqhl_schema, sqhl_table, sqhl_begin_time_id, sqhl_end_time_id, sqhl_hole_size) VALUES (' || quote_literal(r_rel.rel_schema) || ',' || quote_literal(r_rel.rel_tblseq) || ',' || v_beginTimeId || ',' || v_endTimeId || ', (' || ' SELECT CASE WHEN is_called THEN last_value + increment_by ELSE last_value END FROM ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_sequence) || ')-(' || ' SELECT CASE WHEN sequ_is_called THEN sequ_last_val + sequ_increment ELSE sequ_last_val END FROM ' || ' emaj.emaj_sequence WHERE' || ' sequ_schema = ' || quote_literal(r_rel.rel_log_schema) || ' AND sequ_name = ' || quote_literal(r_rel.rel_log_sequence) || ' AND sequ_time_id = ' || v_beginTimeId || '))'; RETURN v_nbRows; END; $_delete_log_tbl$; CREATE OR REPLACE FUNCTION emaj._verify_groups(v_groups TEXT[], v_onErrorStop BOOLEAN) RETURNS SETOF emaj._verify_groups_type LANGUAGE plpgsql AS $_verify_groups$ -- The function verifies the consistency of a tables groups array. -- Input: - tables groups array, -- - a boolean indicating whether the function has to raise an exception in case of detected unconsistency. -- If onErrorStop boolean is false, it returns a set of _verify_groups_type records, one row per detected unconsistency, including the faulting schema and table or sequence names and a detailed message. -- If no error is detected, no row is returned. DECLARE v_hint TEXT = 'You may use "SELECT * FROM emaj.emaj_verify_all()" to look for other issues.'; r_object RECORD; BEGIN -- Note that there is no check that the supplied groups exist. This has already been done by all calling functions. -- Let's start with some global checks that always raise an exception if an issue is detected -- check the postgres version: E-Maj needs postgres 9.1+ IF emaj._pg_version_num() < 90100 THEN RAISE EXCEPTION 'The current postgres version (%) is not compatible with E-Maj.', version(); END IF; -- OK, now look for groups unconsistency -- Unlike emaj_verify_all(), there is no direct check that application schemas exist -- check all application relations referenced in the emaj_relation table still exist FOR r_object IN SELECT t.rel_schema, t.rel_tblseq, 'In group "' || r.rel_group || '", the ' || CASE WHEN t.rel_kind = 'r' THEN 'table "' ELSE 'sequence "' END || t.rel_schema || '"."' || t.rel_tblseq || '" does not exist any more.' AS msg FROM ( -- all relations known by E-Maj SELECT rel_schema, rel_tblseq, rel_kind FROM emaj.emaj_relation WHERE rel_group = ANY (v_groups) EXCEPT -- all relations known by postgres SELECT nspname, relname, relkind FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND relkind IN ('r','S') ) AS t, emaj.emaj_relation r -- join with emaj_relation to get the group name WHERE t.rel_schema = r.rel_schema AND t.rel_tblseq = r.rel_tblseq ORDER BY 1,2,3 LOOP IF v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (1): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- check the log table for all tables referenced in the emaj_relation table still exist FOR r_object IN SELECT rel_schema, rel_tblseq, 'In group "' || rel_group || '", the log table "' || rel_log_schema || '"."' || rel_log_table || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_group = ANY (v_groups) AND rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_namespace, pg_catalog.pg_class WHERE nspname = rel_log_schema AND relname = rel_log_table AND relnamespace = pg_namespace.oid) ORDER BY 1,2,3 LOOP IF v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (2): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- check the log function for each table referenced in the emaj_relation table still exists FOR r_object IN -- the schema and table names are rebuilt from the returned function name SELECT rel_schema, rel_tblseq, 'In group "' || rel_group || '", the log function "' || rel_log_schema || '"."' || rel_log_function || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_group = ANY (v_groups) AND rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_proc, pg_catalog.pg_namespace WHERE nspname = rel_log_schema AND proname = rel_log_function AND pronamespace = pg_namespace.oid) ORDER BY 1,2,3 LOOP IF v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (3): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- check log and truncate triggers for all tables referenced in the emaj_relation table still exist -- start with log trigger FOR r_object IN SELECT rel_schema, rel_tblseq, 'In group "' || rel_group || '", the log trigger "emaj_log_trg" on table "' || rel_schema || '"."' || rel_tblseq || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_group = ANY (v_groups) AND rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_trigger, pg_catalog.pg_namespace, pg_catalog.pg_class WHERE nspname = rel_schema AND relname = rel_tblseq AND tgname = 'emaj_log_trg' AND tgrelid = pg_class.oid AND relnamespace = pg_namespace.oid) ORDER BY 1,2,3 LOOP IF v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (4): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- then truncate trigger FOR r_object IN SELECT rel_schema, rel_tblseq, 'In group "' || rel_group || '", the truncate trigger "emaj_trunc_trg" on table "' || rel_schema || '"."' || rel_tblseq || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_group = ANY (v_groups) AND rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_trigger, pg_catalog.pg_namespace, pg_catalog.pg_class WHERE nspname = rel_schema AND relname = rel_tblseq AND tgname = 'emaj_trunc_trg' AND tgrelid = pg_class.oid AND relnamespace = pg_namespace.oid) ORDER BY 1,2,3 LOOP IF v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (5): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- check all log tables have a structure consistent with the application tables they reference -- (same columns and same formats). It only returns one row per faulting table. FOR r_object IN WITH cte_app_tables_columns AS ( -- application table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM emaj.emaj_relation, pg_catalog.pg_attribute, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = rel_schema AND relname = rel_tblseq AND attrelid = pg_class.oid AND attnum > 0 AND attisdropped = false AND rel_group = ANY (v_groups) AND rel_kind = 'r'), cte_log_tables_columns AS ( -- log table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM emaj.emaj_relation, pg_catalog.pg_attribute, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = rel_log_schema AND relname = rel_log_table AND attrelid = pg_class.oid AND attnum > 0 AND attisdropped = false AND attname NOT LIKE 'emaj%' AND rel_group = ANY (v_groups) AND rel_kind = 'r') SELECT DISTINCT rel_schema, rel_tblseq, 'In group "' || rel_group || '", the structure of the application table "' || rel_schema || '"."' || rel_tblseq || '" is not coherent with its log table ("' || rel_log_schema || '"."' || rel_log_table || '").' AS msg FROM ( ( -- application table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_app_tables_columns EXCEPT -- minus log table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_log_tables_columns ) UNION ( -- log table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_log_tables_columns EXCEPT -- minus application table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_app_tables_columns )) AS t ORDER BY 1,2,3 LOOP if v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (6): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- check all tables have their primary key if they belong to a rollbackable group FOR r_object IN SELECT rel_schema, rel_tblseq, 'In rollbackable group "' || rel_group || '", the table "' || rel_schema || '"."' || rel_tblseq || '" has no primary key any more.' AS msg FROM emaj.emaj_relation, emaj.emaj_group WHERE rel_group = ANY (v_groups) AND rel_kind = 'r' AND rel_group = group_name AND group_is_rollbackable AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_class, pg_catalog.pg_namespace, pg_catalog.pg_constraint WHERE nspname = rel_schema AND relname = rel_tblseq AND relnamespace = pg_namespace.oid AND connamespace = pg_namespace.oid AND conrelid = pg_class.oid AND contype = 'p') ORDER BY 1,2,3 LOOP if v_onErrorStop THEN RAISE EXCEPTION '_verify_groups (7): % %',r_object.msg,v_hint; END IF; RETURN NEXT r_object; END LOOP; -- RETURN; END; $_verify_groups$; CREATE OR REPLACE FUNCTION emaj._drop_group(v_groupName TEXT, v_isForced BOOLEAN) RETURNS INT LANGUAGE plpgsql SECURITY DEFINER AS $_drop_group$ -- This function effectively deletes the emaj objects for all tables of a group -- It also drops secondary schemas that are not useful any more -- Input: group name, and a boolean indicating whether the group's state has to be checked -- Output: number of processed tables and sequences -- The function is defined as SECURITY DEFINER so that secondary schemas can be dropped DECLARE v_groupIsLogging BOOLEAN; v_eventTriggers TEXT[]; v_schemasToDrop TEXT[]; v_nbTb INT = 0; v_schemaPrefix TEXT = 'emaj'; v_logSchema TEXT; r_rel emaj.emaj_relation%ROWTYPE; BEGIN -- check that the group is recorded in emaj_group table SELECT group_is_logging INTO v_groupIsLogging FROM emaj.emaj_group WHERE group_name = v_groupName FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION '_drop_group: group "%" has not been created.', v_groupName; END IF; -- if the state of the group has to be checked, IF NOT v_isForced THEN -- check that the group is not in LOGGING state IF v_groupIsLogging THEN RAISE EXCEPTION '_drop_group: The group "%" cannot be deleted because it is in LOGGING state.', v_groupName; END IF; END IF; -- OK -- disable event triggers that protect emaj components and keep in memory these triggers name SELECT emaj._disable_event_triggers() INTO v_eventTriggers; -- build the list of secondary schemas to drop later SELECT coalesce(array_agg(rel_log_schema),'{}') INTO v_schemasToDrop FROM ( SELECT DISTINCT rel_log_schema FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_log_schema <> v_schemaPrefix EXCEPT SELECT DISTINCT rel_log_schema FROM emaj.emaj_relation WHERE rel_group <> v_groupName AND rel_log_schema <> v_schemaPrefix ORDER BY 1 ) AS t; -- delete the emaj objets for each table of the group FOR r_rel IN SELECT * FROM emaj.emaj_relation WHERE rel_group = v_groupName ORDER BY rel_priority, rel_schema, rel_tblseq LOOP IF r_rel.rel_kind = 'r' THEN -- if it is a table, delete the related emaj objects PERFORM emaj._drop_tbl(r_rel); ELSEIF r_rel.rel_kind = 'S' THEN -- if it is a sequence, delete all related data from emaj_sequence table PERFORM emaj._drop_seq(r_rel); END IF; v_nbTb = v_nbTb + 1; END LOOP; -- drop the E-Maj secondary schemas previously identified as useless (i.e. not used by any other created group) FOREACH v_logSchema IN ARRAY v_schemasToDrop LOOP -- drop the schema PERFORM emaj._drop_log_schema(v_logSchema, v_isForced); -- and record the schema suppression in emaj_hist table INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object) VALUES (CASE WHEN v_isForced THEN 'FORCE_DROP_GROUP' ELSE 'DROP_GROUP' END,'SCHEMA DROPPED',quote_ident(v_logSchema)); END LOOP; -- delete group row from the emaj_group table. -- By cascade, it also deletes rows from emaj_mark DELETE FROM emaj.emaj_group WHERE group_name = v_groupName; -- enable previously disabled event triggers PERFORM emaj._enable_event_triggers(v_eventTriggers); RETURN v_nbTb; END; $_drop_group$; CREATE OR REPLACE FUNCTION emaj.emaj_alter_group(v_groupName TEXT) RETURNS INT LANGUAGE plpgsql AS $emaj_alter_group$ -- This function alters a tables group. -- It takes into account the changes recorded in the emaj_group_def table since the group has been created. -- Executing emaj_alter_group() is equivalent to chaining emaj_drop_group() and emaj_create_group(). -- But only emaj objects that need to be dropped or created are processed. -- Input: group name -- Output: number of tables and sequences belonging to the group after the operation DECLARE v_emajSchema TEXT = 'emaj'; v_schemaPrefix TEXT = 'emaj'; v_nbCreate INT = 0; v_nbDrop INT = 0; v_nbTbl INT; v_nbSeq INT; v_groupIsLogging BOOLEAN; v_isRollbackable BOOLEAN; v_timeId BIGINT; v_logSchema TEXT; v_logSchemasToDrop TEXT[]; v_logSchemasToCreate TEXT[]; v_aLogSchema TEXT; v_defTsp TEXT; v_eventTriggers TEXT[]; r_grpdef emaj.emaj_group_def%ROWTYPE; r_rel emaj.emaj_relation%ROWTYPE; r_tblsq RECORD; r_schema RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object) VALUES ('ALTER_GROUP', 'BEGIN', v_groupName); -- check that the group is recorded in emaj_group table SELECT group_is_logging, group_is_rollbackable INTO v_groupIsLogging, v_isRollbackable FROM emaj.emaj_group WHERE group_name = v_groupName FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_alter_group: group "%" has not been created.', v_groupName; END IF; -- check that the group is not in LOGGING state IF v_groupIsLogging THEN RAISE EXCEPTION 'emaj_alter_group: The group "%" cannot be altered because it is in LOGGING state.', v_groupName; END IF; -- check there are remaining rows for the group in emaj_group_def table PERFORM 0 FROM emaj.emaj_group_def WHERE grpdef_group = v_groupName LIMIT 1; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_alter_group: Group "%" is unknown in emaj_group_def table.', v_groupName; END IF; -- performs various checks on the group's content described in the emaj_group_def table PERFORM emaj._check_group_content(v_groupName); -- OK -- get the time stamp of the operation SELECT emaj._set_time_stamp('A') INTO v_timeId; -- define the default tablespace, NULL if tspemaj tablespace doesn't exist SELECT 'tspemaj' INTO v_defTsp FROM pg_catalog.pg_tablespace WHERE spcname = 'tspemaj'; -- disable event triggers that protect emaj components and keep in memory these triggers name SELECT emaj._disable_event_triggers() INTO v_eventTriggers; -- We can now process: -- - relations that do not belong to the tables group any more, by dropping their emaj components -- - relations that continue to belong to the tables group but with different characteristics, -- by first dropping their emaj components and letting the last step recreate them -- - new relations in the tables group, by (re)creating their emaj components -- -- build the list of secondary log schemas that will need to be dropped once obsolete log tables will be dropped SELECT array_agg(rel_log_schema) INTO v_logSchemasToDrop FROM ( SELECT rel_log_schema FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_log_schema <> v_emajSchema -- secondary log schemas that currently exist for the group EXCEPT SELECT rel_log_schema FROM emaj.emaj_relation WHERE rel_group <> v_groupName -- minus those that exist for other groups EXCEPT SELECT v_schemaPrefix || grpdef_log_schema_suffix FROM emaj.emaj_group_def WHERE grpdef_group = v_groupName AND grpdef_log_schema_suffix IS NOT NULL AND grpdef_log_schema_suffix <> '' -- minus those that will remain for the group ) AS t; -- build the list of secondary log schemas that will need to be created before new log tables will be created SELECT array_agg(log_schema) INTO v_logSchemasToCreate FROM ( SELECT DISTINCT v_schemaPrefix || grpdef_log_schema_suffix AS log_schema FROM emaj.emaj_group_def WHERE grpdef_group = v_groupName AND grpdef_log_schema_suffix IS NOT NULL AND grpdef_log_schema_suffix <> '' -- secondary log schemas needed for the group EXCEPT SELECT DISTINCT rel_log_schema FROM emaj.emaj_relation -- minus those already created ORDER BY 1 ) AS t; -- list all relations that do not belong to the tables group any more FOR r_rel IN SELECT * FROM emaj.emaj_relation WHERE rel_group = v_groupName AND NOT EXISTS ( SELECT NULL FROM emaj.emaj_group_def WHERE grpdef_schema = rel_schema AND grpdef_tblseq = rel_tblseq AND grpdef_group = v_groupName) UNION -- ... and all relations that are damaged or whose log table is not synchronised with them any more SELECT emaj.emaj_relation.* FROM ( -- all damaged or out of sync tables SELECT DISTINCT ver_schema, ver_tblseq FROM emaj._verify_groups(ARRAY[v_groupName], false) ) AS t, emaj.emaj_relation WHERE rel_schema = ver_schema AND rel_tblseq = ver_tblseq ORDER BY rel_priority, rel_schema, rel_tblseq LOOP CASE r_rel.rel_kind WHEN 'r' THEN -- if it is a table, delete the related emaj objects PERFORM emaj._drop_tbl(r_rel); WHEN 'S' THEN -- if it is a sequence, delete all related data from emaj_sequence table PERFORM emaj._drop_seq(r_rel); END CASE; v_nbDrop = v_nbDrop + 1; END LOOP; -- -- list relations that still belong to the tables group FOR r_tblsq IN SELECT rel_priority, rel_schema, rel_tblseq, rel_kind, rel_log_schema, rel_log_table, rel_log_dat_tsp, rel_log_idx_tsp, grpdef_priority, grpdef_schema, grpdef_tblseq, grpdef_log_schema_suffix, grpdef_emaj_names_prefix, grpdef_log_dat_tsp, grpdef_log_idx_tsp FROM emaj.emaj_relation, emaj.emaj_group_def WHERE rel_schema = grpdef_schema AND rel_tblseq = grpdef_tblseq AND rel_group = v_groupName AND grpdef_group = v_groupName ORDER BY rel_priority, rel_schema, rel_tblseq LOOP -- now detect other changes that justify to drop and recreate the relation -- detect if the log data tablespace in emaj_group_def has changed IF (r_tblsq.rel_kind = 'r' AND coalesce(r_tblsq.rel_log_dat_tsp,'') <> coalesce(r_tblsq.grpdef_log_dat_tsp, v_defTsp,'')) OR (r_tblsq.rel_kind = 'S' AND r_tblsq.grpdef_log_dat_tsp IS NOT NULL) -- or if the log index tablespace in emaj_group_def has changed OR (r_tblsq.rel_kind = 'r' AND coalesce(r_tblsq.rel_log_idx_tsp,'') <> coalesce(r_tblsq.grpdef_log_idx_tsp, v_defTsp,'')) OR (r_tblsq.rel_kind = 'S' AND r_tblsq.grpdef_log_idx_tsp IS NOT NULL) -- or if the log schema in emaj_group_def has changed OR (r_tblsq.rel_kind = 'r' AND r_tblsq.rel_log_schema <> (v_schemaPrefix || coalesce(r_tblsq.grpdef_log_schema_suffix, ''))) OR (r_tblsq.rel_kind = 'S' AND r_tblsq.grpdef_log_schema_suffix IS NOT NULL) -- or if the emaj names prefix in emaj_group_def has changed (detected with the log table name) OR (r_tblsq.rel_kind = 'r' AND r_tblsq.rel_log_table <> (coalesce(r_tblsq.grpdef_emaj_names_prefix, r_tblsq.grpdef_schema || '_' || r_tblsq.grpdef_tblseq) || '_log')) OR (r_tblsq.rel_kind = 'S' AND r_tblsq.grpdef_emaj_names_prefix IS NOT NULL) THEN -- then drop the relation (it will be recreated later) -- get the related row in emaj_relation SELECT * FROM emaj.emaj_relation WHERE rel_schema = r_tblsq.rel_schema AND rel_tblseq = r_tblsq.rel_tblseq INTO r_rel; IF r_tblsq.rel_kind = 'r' THEN -- if it is a table, delete the related emaj objects PERFORM emaj._drop_tbl (r_rel); ELSEIF r_tblsq.rel_kind = 'S' THEN -- if it is a sequence, delete all related data from emaj_sequence table PERFORM emaj._drop_seq (r_rel); END IF; v_nbDrop = v_nbDrop + 1; -- other case ? -- has the priority changed in emaj_group_def ? If yes, just report the change into emaj_relation ELSEIF (r_tblsq.rel_priority IS NULL AND r_tblsq.grpdef_priority IS NOT NULL) OR (r_tblsq.rel_priority IS NOT NULL AND r_tblsq.grpdef_priority IS NULL) OR (r_tblsq.rel_priority <> r_tblsq.grpdef_priority) THEN UPDATE emaj.emaj_relation SET rel_priority = r_tblsq.grpdef_priority WHERE rel_schema = r_tblsq.grpdef_schema AND rel_tblseq = r_tblsq.grpdef_tblseq; END IF; END LOOP; -- -- cleanup all remaining log tables PERFORM emaj._reset_group(v_groupName); -- drop useless log schemas, using the list of schemas to drop built previously IF v_logSchemasToDrop IS NOT NULL THEN FOREACH v_aLogSchema IN ARRAY v_logSchemasToDrop LOOP -- drop the log schema PERFORM emaj._drop_log_schema(v_aLogSchema, false); -- and record the schema drop in emaj_hist table INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object) VALUES ('ALTER_GROUP','SCHEMA DROPPED',quote_ident(v_aLogSchema)); END LOOP; END IF; -- create new log schemas, using the list of potential schemas to create built previously IF v_logSchemasToCreate IS NOT NULL THEN FOREACH v_aLogSchema IN ARRAY v_logSchemasToCreate LOOP -- create the schema PERFORM emaj._create_log_schema(v_aLogSchema); -- and record the schema creation in emaj_hist table INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object) VALUES ('ALTER_GROUP','SCHEMA CREATED',quote_ident(v_aLogSchema)); END LOOP; END IF; -- -- get and process new tables in the tables group (really new or intentionaly dropped in the preceeding steps) FOR r_grpdef IN SELECT emaj.emaj_group_def.* FROM emaj.emaj_group_def, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE grpdef_group = v_groupName AND NOT EXISTS ( SELECT NULL FROM emaj.emaj_relation WHERE rel_schema = grpdef_schema AND rel_tblseq = grpdef_tblseq AND rel_group = v_groupName) AND relnamespace = pg_namespace.oid AND nspname = grpdef_schema AND relname = grpdef_tblseq AND relkind = 'r' ORDER BY grpdef_priority, grpdef_schema, grpdef_tblseq LOOP PERFORM emaj._create_tbl(r_grpdef, v_groupName, v_isRollbackable, v_defTsp); v_nbCreate = v_nbCreate + 1; END LOOP; -- get and process new sequences in the tables group (really new or intentionaly dropped in the preceeding steps) FOR r_grpdef IN SELECT emaj.emaj_group_def.* FROM emaj.emaj_group_def, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE grpdef_group = v_groupName AND NOT EXISTS ( SELECT NULL FROM emaj.emaj_relation WHERE rel_schema = grpdef_schema AND rel_tblseq = grpdef_tblseq AND rel_group = v_groupName) AND relnamespace = pg_namespace.oid AND nspname = grpdef_schema AND relname = grpdef_tblseq AND relkind = 'S' ORDER BY grpdef_priority, grpdef_schema, grpdef_tblseq LOOP PERFORM emaj._create_seq(r_grpdef, v_groupName); v_nbCreate = v_nbCreate + 1; END LOOP; -- update tables and sequences counters and the last alter timestamp in the emaj_group table SELECT count(*) INTO v_nbTbl FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'r'; SELECT count(*) INTO v_nbSeq FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'S'; UPDATE emaj.emaj_group SET group_last_alter_time_id = v_timeId, group_nb_table = v_nbTbl, group_nb_sequence = v_nbSeq WHERE group_name = v_groupName; -- delete old marks of the tables group from emaj_mark DELETE FROM emaj.emaj_mark WHERE mark_group = v_groupName; -- enable previously disabled event triggers PERFORM emaj._enable_event_triggers(v_eventTriggers); -- check foreign keys with tables outside the group PERFORM emaj._check_fk_groups(array[v_groupName]); -- insert end in the history INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_object, hist_wording) VALUES ('ALTER_GROUP', 'END', v_groupName, v_nbDrop || ' dropped relations and ' || v_nbCreate || ' (re)created relations'); RETURN v_nbTbl + v_nbSeq; END; $emaj_alter_group$; COMMENT ON FUNCTION emaj.emaj_alter_group(TEXT) IS $$Alter an E-Maj group.$$; CREATE OR REPLACE FUNCTION emaj.emaj_protect_group(v_groupName TEXT) RETURNS INT LANGUAGE plpgsql AS $emaj_protect_group$ -- This function sets a protection on a group against accidental rollback. -- However this doesn't block rollback simulations performed with the emaj_estimate_rollback_group() function. -- Input: group name -- Output: 1 if successful, 0 if the group was already in protected state DECLARE v_groupIsRollbackable BOOLEAN; v_groupIsLogging BOOLEAN; v_groupIsProtected BOOLEAN; v_status INT; BEGIN -- check that the group is recorded in emaj_group table SELECT group_is_rollbackable, group_is_logging, group_is_rlbk_protected INTO v_groupIsRollbackable, v_groupIsLogging, v_groupIsProtected FROM emaj.emaj_group WHERE group_name = v_groupName FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_protect_group: group % has not been created.', v_groupName; END IF; -- check that the group is ROLLBACKABLE IF NOT v_groupIsRollbackable THEN RAISE EXCEPTION 'emaj_protect_group: The group "%" cannot be protected because it is an AUDIT_ONLY group.', v_groupName; END IF; -- check that the group is not in LOGGING state IF NOT v_groupIsLogging THEN RAISE EXCEPTION 'emaj_protect_group: The group "%" cannot be protected because it is not in LOGGING state.', v_groupName; END IF; -- OK, set the protection IF v_groupIsProtected THEN v_status = 0; ELSE UPDATE emaj.emaj_group SET group_is_rlbk_protected = TRUE WHERE group_name = v_groupName; v_status = 1; END IF; -- insert event in the history INSERT INTO emaj.emaj_hist (hist_function, hist_object, hist_wording) VALUES ('PROTECT_GROUP', v_groupName, 'Status ' || v_status); RETURN v_status; END; $emaj_protect_group$; COMMENT ON FUNCTION emaj.emaj_protect_group(TEXT) IS $$Sets a protection against a rollback on an E-Maj group.$$; CREATE OR REPLACE FUNCTION emaj.emaj_protect_mark_group(v_groupName TEXT, v_mark TEXT) RETURNS INT LANGUAGE plpgsql AS $emaj_protect_mark_group$ -- This function sets a protection on a mark for a group against accidental rollback. -- However this doesn't block rollback simulations performed with the emaj_estimate_rollback_group() function. -- Input: group name, mark to protect -- Output: 1 if successful, 0 if the mark was already in protected state -- The group must be ROLLBACKABLE and in LOGGING state. DECLARE v_groupIsRollbackable BOOLEAN; v_groupIsLogging BOOLEAN; v_realMark TEXT; v_markIsProtected BOOLEAN; v_status INT; BEGIN -- check that the group is recorded in emaj_group table SELECT group_is_rollbackable, group_is_logging INTO v_groupIsRollbackable, v_groupIsLogging FROM emaj.emaj_group WHERE group_name = v_groupName FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_protect_mark_group: group "%" has not been created.', v_groupName; END IF; -- check that the group is ROLLBACKABLE IF NOT v_groupIsRollbackable THEN RAISE EXCEPTION 'emaj_protect_mark_group: A mark on the group "%" cannot be protected because it is an AUDIT_ONLY group.', v_groupName; END IF; -- check that the group is not in LOGGING state IF NOT v_groupIsLogging THEN RAISE EXCEPTION 'emaj_protect_mark_group: A mark on the group "%" cannot be protected because it is not in LOGGING state.', v_groupName; END IF; -- retrieve and check the mark name SELECT emaj._get_mark_name(v_groupName,v_mark) INTO v_realMark; IF v_realMark IS NULL THEN RAISE EXCEPTION 'emaj_protect_mark_group: mark "%" does not exist for group "%".', v_mark, v_groupName; END IF; -- OK, set the protection SELECT mark_is_rlbk_protected INTO v_markIsProtected FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_realMark; IF v_markIsProtected THEN v_status = 0; ELSE UPDATE emaj.emaj_mark SET mark_is_rlbk_protected = TRUE WHERE mark_group = v_groupName AND mark_name = v_realMark; v_status = 1; END IF; -- insert event in the history INSERT INTO emaj.emaj_hist (hist_function, hist_object, hist_wording) VALUES ('PROTECT_MARK_GROUP', v_groupName, 'Mark ' || v_realMark || ' ; status ' || v_status); RETURN v_status; END; $emaj_protect_mark_group$; COMMENT ON FUNCTION emaj.emaj_protect_mark_group(TEXT,TEXT) IS $$Sets a protection against a rollback on a mark of an E-Maj group.$$; CREATE OR REPLACE FUNCTION emaj._rlbk_check(v_groupNames TEXT[], v_mark TEXT, isRollbackSimulation BOOLEAN DEFAULT FALSE) RETURNS TEXT LANGUAGE plpgsql AS $_rlbk_check$ -- This functions performs checks on group names and mark names supplied as parameter for the emaj_rollback_groups() -- and emaj_estimate_rollback_groups() functions. -- It returns the real mark name. DECLARE v_aGroupName TEXT; v_groupIsLogging BOOLEAN; v_groupIsProtected BOOLEAN; v_groupIsRollbackable BOOLEAN; v_markName TEXT; v_markId BIGINT; v_markIsDeleted BOOLEAN; v_protectedMarkList TEXT; v_cpt INT; BEGIN -- check that each group ... -- ...is recorded in emaj_group table FOREACH v_aGroupName IN ARRAY v_groupNames LOOP SELECT group_is_logging, group_is_rollbackable, group_is_rlbk_protected INTO v_groupIsLogging, v_groupIsRollbackable, v_groupIsProtected FROM emaj.emaj_group WHERE group_name = v_aGroupName; IF NOT FOUND THEN RAISE EXCEPTION '_rlbk_check: group "%" has not been created.', v_aGroupName; END IF; -- ... is in LOGGING state IF NOT v_groupIsLogging THEN RAISE EXCEPTION '_rlbk_check: Group "%" is not in LOGGING state.', v_aGroupName; END IF; -- ... is ROLLBACKABLE IF NOT v_groupIsRollbackable THEN RAISE EXCEPTION '_rlbk_check: Group "%" has been created for audit only purpose.', v_aGroupName; END IF; -- ... is not protected against rollback (check disabled for rollback simulation) IF v_groupIsProtected AND NOT isRollbackSimulation THEN RAISE EXCEPTION '_rlbk_check: Group "%" is currently protected against rollback.', v_aGroupName; END IF; -- ... owns the requested mark SELECT emaj._get_mark_name(v_aGroupName,v_mark) INTO v_markName; IF NOT FOUND OR v_markName IS NULL THEN RAISE EXCEPTION '_rlbk_check: No mark "%" exists for group "%".', v_mark, v_aGroupName; END IF; -- ... and this mark is ACTIVE SELECT mark_id, mark_is_deleted INTO v_markId, v_markIsDeleted FROM emaj.emaj_mark WHERE mark_group = v_aGroupName AND mark_name = v_markName; IF v_markIsDeleted THEN RAISE EXCEPTION '_rlbk_check: mark "%" for group "%" is not usable for rollback.', v_markName, v_aGroupName; END IF; -- ... and the rollback wouldn't delete protected marks (check disabled for rollback simulation) IF NOT isRollbackSimulation THEN SELECT string_agg(mark_name,', ') INTO v_protectedMarkList FROM ( SELECT mark_name FROM emaj.emaj_mark WHERE mark_group = v_aGroupName AND mark_id > v_markId AND mark_is_rlbk_protected ORDER BY mark_id) AS t; IF v_protectedMarkList IS NOT NULL THEN RAISE EXCEPTION '_rlbk_check: protected marks (%) for group "%" block the rollback to mark "%".', v_protectedMarkList, v_aGroupName, v_markName; END IF; END IF; END LOOP; -- get the mark timestamp and check it is the same for all groups of the array SELECT count(DISTINCT emaj._get_mark_time_id(group_name,v_mark)) INTO v_cpt FROM emaj.emaj_group WHERE group_name = ANY (v_groupNames); IF v_cpt > 1 THEN RAISE EXCEPTION '_rlbk_check: Mark "%" does not represent the same point in time for all groups.', v_mark; END IF; RETURN v_markName; END; $_rlbk_check$; CREATE OR REPLACE FUNCTION emaj._reset_group(v_groupName TEXT) RETURNS INT LANGUAGE plpgsql SECURITY DEFINER AS $_reset_group$ -- This function empties the log tables for all tables of a group, using a TRUNCATE, and deletes the sequences saves -- It is called by both emaj_reset_group and emaj_start_group functions -- Input: group name -- Output: number of processed tables and sequences -- There is no check of the group state -- The function is defined as SECURITY DEFINER so that an emaj_adm role can truncate log tables DECLARE v_nbTb INT; r_rel RECORD; BEGIN -- delete all marks for the group from the emaj_mark table DELETE FROM emaj.emaj_mark WHERE mark_group = v_groupName; -- delete emaj_sequence rows related to the tables of the group DELETE FROM emaj.emaj_sequence USING emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'r' AND sequ_schema = rel_log_schema AND sequ_name = rel_log_sequence; -- delete all sequence holes for the tables of the group DELETE FROM emaj.emaj_seq_hole USING emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'r' AND rel_schema = sqhl_schema AND rel_tblseq = sqhl_table; -- initialize the return value with the number of sequences SELECT count(*) INTO v_nbTb FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'S'; -- delete emaj_sequence rows related to the sequences of the group DELETE FROM emaj.emaj_sequence USING emaj.emaj_relation WHERE rel_schema = sequ_schema AND rel_tblseq = sequ_name AND rel_group = v_groupName AND rel_kind = 'S'; -- then, truncate log tables for application tables FOR r_rel IN SELECT rel_log_schema, rel_log_table, rel_log_sequence FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'r' ORDER BY rel_priority, rel_schema, rel_tblseq LOOP -- truncate the log table EXECUTE 'TRUNCATE ' || quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_table); -- and reset the log sequence PERFORM setval(quote_ident(r_rel.rel_log_schema) || '.' || quote_ident(r_rel.rel_log_sequence), 1, false); v_nbTb = v_nbTb + 1; END LOOP; RETURN v_nbTb; END; $_reset_group$; CREATE OR REPLACE FUNCTION emaj.emaj_detailed_log_stat_group(v_groupName TEXT, v_firstMark TEXT, v_lastMark TEXT) RETURNS SETOF emaj.emaj_detailed_log_stat_type LANGUAGE plpgsql AS $emaj_detailed_log_stat_group$ -- This function returns statistics on row updates executed between 2 marks as viewed through the log tables -- It provides more information than emaj_log_stat_group but it needs to scan log tables in order to provide these data. -- So the response time may be much longer. -- Input: group name, the 2 marks names defining a range -- a NULL value or an empty string as first_mark indicates the first recorded mark -- a NULL value or an empty string as last_mark indicates the current situation -- The keyword 'EMAJ_LAST_MARK' can be used as first or last mark to specify the last set mark. -- Output: table of updates by user and table DECLARE v_realFirstMark TEXT; v_realLastMark TEXT; v_firstMarkId BIGINT; v_lastMarkId BIGINT; v_firstMarkTs TIMESTAMPTZ; v_lastMarkTs TIMESTAMPTZ; v_firstEmajGid BIGINT; v_lastEmajGid BIGINT; v_logTableName TEXT; v_stmt TEXT; r_tblsq RECORD; r_stat RECORD; BEGIN -- check that the group is recorded in emaj_group table PERFORM 0 FROM emaj.emaj_group WHERE group_name = v_groupName; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_detailed_log_stat_group: group "%" has not been created.', v_groupName; END IF; -- catch the timestamp of the first mark IF v_firstMark IS NOT NULL AND v_firstMark <> '' THEN -- check and retrieve the global sequence value and the timestamp of the start mark for the group SELECT emaj._get_mark_name(v_groupName,v_firstMark) INTO v_realFirstMark; IF v_realFirstMark IS NULL THEN RAISE EXCEPTION 'emaj_detailed_log_stat_group: Start mark "%" is unknown for group "%".', v_firstMark, v_groupName; END IF; SELECT mark_id, time_last_emaj_gid, time_clock_timestamp INTO v_firstMarkId, v_firstEmajGid, v_firstMarkTs FROM emaj.emaj_mark, emaj.emaj_time_stamp WHERE mark_time_id = time_id AND mark_group = v_groupName AND mark_name = v_realFirstMark; END IF; -- catch the timestamp of the last mark IF v_lastMark IS NOT NULL AND v_lastMark <> '' THEN -- else, check and retrieve the global sequence value and the timestamp of the end mark for the group SELECT emaj._get_mark_name(v_groupName,v_lastMark) INTO v_realLastMark; IF v_realLastMark IS NULL THEN RAISE EXCEPTION 'emaj_detailed_log_stat_group: End mark "%" is unknown for group "%".', v_lastMark, v_groupName; END IF; SELECT mark_id, time_last_emaj_gid, time_clock_timestamp INTO v_lastMarkId, v_lastEmajGid, v_lastMarkTs FROM emaj.emaj_mark, emaj.emaj_time_stamp WHERE mark_time_id = time_id AND mark_group = v_groupName AND mark_name = v_realLastMark; END IF; -- check that the first_mark < end_mark IF v_realFirstMark IS NOT NULL AND v_realLastMark IS NOT NULL AND v_firstMarkId > v_lastMarkId THEN RAISE EXCEPTION 'emaj_detailed_log_stat_group: mark id for "%" (% = %) is greater than mark id for "%" (% = %).', v_realFirstMark, v_firstMarkId, v_firstMarkTs, v_realLastMark, v_lastMarkId, v_lastMarkTs; END IF; -- for each table of the emaj_relation table FOR r_tblsq IN SELECT rel_priority, rel_schema, rel_tblseq, rel_log_schema, rel_kind, rel_log_table FROM emaj.emaj_relation WHERE rel_group = v_groupName AND rel_kind = 'r' ORDER BY rel_priority, rel_schema, rel_tblseq LOOP -- count the number of operations per type (INSERT, UPDATE and DELETE) and role -- compute the log table name and its sequence name for this table v_logTableName = quote_ident(r_tblsq.rel_log_schema) || '.' || quote_ident(r_tblsq.rel_log_table); -- prepare and execute the statement v_stmt= 'SELECT ' || quote_literal(v_groupName) || '::TEXT as emaj_group,' || ' ' || quote_literal(r_tblsq.rel_schema) || '::TEXT as emaj_schema,' || ' ' || quote_literal(r_tblsq.rel_tblseq) || '::TEXT as emaj_table,' || ' emaj_user,' || ' CASE emaj_verb WHEN ''INS'' THEN ''INSERT''' || ' WHEN ''UPD'' THEN ''UPDATE''' || ' WHEN ''DEL'' THEN ''DELETE''' || ' ELSE ''?'' END::VARCHAR(6) as emaj_verb,' || ' count(*) as emaj_rows' || ' FROM ' || v_logTableName || ' WHERE NOT (emaj_verb = ''UPD'' AND emaj_tuple = ''OLD'')'; IF v_firstMark IS NOT NULL AND v_firstMark <> '' THEN v_stmt = v_stmt || ' AND emaj_gid > '|| v_firstEmajGid ; END IF; IF v_lastMark IS NOT NULL AND v_lastMark <> '' THEN v_stmt = v_stmt || ' AND emaj_gid <= '|| v_lastEmajGid ; END IF; v_stmt = v_stmt || ' GROUP BY emaj_group, emaj_schema, emaj_table, emaj_user, emaj_verb' || ' ORDER BY emaj_user, emaj_verb'; FOR r_stat IN EXECUTE v_stmt LOOP RETURN NEXT r_stat; END LOOP; END LOOP; RETURN; END; $emaj_detailed_log_stat_group$; COMMENT ON FUNCTION emaj.emaj_detailed_log_stat_group(TEXT,TEXT,TEXT) IS $$Returns detailed statistics about logged events for an E-Maj group between 2 marks.$$; CREATE OR REPLACE FUNCTION emaj._estimate_rollback_groups(v_groupNames TEXT[], v_mark TEXT, v_isLoggedRlbk BOOLEAN) RETURNS INTERVAL LANGUAGE plpgsql SECURITY DEFINER AS $_estimate_rollback_groups$ -- This function effectively computes an approximate duration of a rollback to a predefined mark for a groups array. -- It simulates a rollback on 1 session, by calling the _rlbk_planning function that already estimates elementary -- rollback steps duration. Once the global estimate is got, the rollback planning is cancelled. -- Input: a group names array, the mark name of the rollback operation, the rollback type. -- Output: the approximate duration that the rollback would need as time interval. -- The function is declared SECURITY DEFINER so that emaj_viewer doesn't need a specific INSERT permission on emaj_rlbk. DECLARE v_markName TEXT; v_fixed_table_rlbk INTERVAL; v_rlbkId INT; v_estimDuration INTERVAL; v_nbTblseq INT; BEGIN -- check supplied group names and mark parameters with the isRollbackSimulation flag set to true SELECT emaj._rlbk_check(v_groupNames, v_mark, TRUE) INTO v_markName; -- compute a random negative rollback-id (not to interfere with ids of real rollbacks) SELECT (random() * -2147483648)::int INTO v_rlbkId; -- -- simulate a rollback planning -- BEGIN -- insert a row into the emaj_rlbk table for this simulated rollback operation INSERT INTO emaj.emaj_rlbk (rlbk_id, rlbk_groups, rlbk_mark, rlbk_mark_time_id, rlbk_is_logged, rlbk_nb_session) VALUES (v_rlbkId, v_groupNames, v_mark, emaj._get_mark_time_id(v_groupNames[1], v_markName), v_isLoggedRlbk, 1); -- call the _rlbk_planning function PERFORM emaj._rlbk_planning(v_rlbkId); -- compute the sum of the duration estimates of all elementary steps (except LOCK_TABLE) SELECT coalesce(sum(rlbp_estimated_duration), '0 SECONDS'::INTERVAL) INTO v_estimDuration FROM emaj.emaj_rlbk_plan WHERE rlbp_rlbk_id = v_rlbkId AND rlbp_step <> 'LOCK_TABLE'; -- cancel the effect of the rollback planning RAISE EXCEPTION ''; EXCEPTION WHEN RAISE_EXCEPTION THEN -- catch the raised exception and continue END; -- get the "fixed_table_rollback_duration" parameter from the emaj_param table SELECT coalesce ((SELECT param_value_interval FROM emaj.emaj_param WHERE param_key = 'fixed_table_rollback_duration'),'1 millisecond'::interval) INTO v_fixed_table_rlbk; -- get the the number of tables to lock and sequences to rollback SELECT sum(group_nb_table)+sum(group_nb_sequence) INTO v_nbTblseq FROM emaj.emaj_group WHERE group_name = ANY(v_groupNames); -- compute the final estimated duration v_estimDuration = v_estimDuration + (v_nbTblseq * v_fixed_table_rlbk); RETURN v_estimDuration; END; $_estimate_rollback_groups$; CREATE OR REPLACE FUNCTION emaj._verify_all_groups() RETURNS SETOF TEXT LANGUAGE plpgsql AS $_verify_all_groups$ -- The function verifies the consistency of all E-Maj groups. -- It returns a set of warning messages for discovered discrepancies. If no error is detected, no row is returned. DECLARE BEGIN -- check the postgres version at groups creation time is compatible (i.e. >= 9.1) RETURN QUERY SELECT 'The group "' || group_name || '" has been created with a non compatible postgresql version (' || group_pg_version || '). It must be dropped and recreated.' AS msg FROM emaj.emaj_group WHERE cast(to_number(substring(group_pg_version FROM E'^(\\d+)'),'99') * 100 + to_number(substring(group_pg_version FROM E'^\\d+\\.(\\d+)'),'99') AS INTEGER) < 901 ORDER BY msg; -- check all application schemas referenced in the emaj_relation table still exist RETURN QUERY SELECT 'The application schema "' || rel_schema || '" does not exist any more.' AS msg FROM ( SELECT DISTINCT rel_schema FROM emaj.emaj_relation EXCEPT SELECT nspname FROM pg_catalog.pg_namespace ) AS t ORDER BY msg; -- check all application relations referenced in the emaj_relation table still exist RETURN QUERY SELECT 'In group "' || r.rel_group || '", the ' || CASE WHEN t.rel_kind = 'r' THEN 'table "' ELSE 'sequence "' END || t.rel_schema || '"."' || t.rel_tblseq || '" does not exist any more.' AS msg FROM ( -- all expected application relations SELECT rel_schema, rel_tblseq, rel_kind FROM emaj.emaj_relation EXCEPT -- minus relations known by postgres SELECT nspname, relname, relkind FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND relkind IN ('r','S') ) AS t, emaj.emaj_relation r -- join with emaj_relation to get the group name WHERE t.rel_schema = r.rel_schema AND t.rel_tblseq = r.rel_tblseq ORDER BY t.rel_schema, t.rel_tblseq, 1; -- check the log table for all tables referenced in the emaj_relation table still exist RETURN QUERY SELECT 'In group "' || rel_group || '", the log table "' || rel_log_schema || '"."' || rel_log_table || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_namespace, pg_catalog.pg_class WHERE nspname = rel_log_schema AND relname = rel_log_table AND relnamespace = pg_namespace.oid) ORDER BY rel_schema, rel_tblseq, 1; -- check the log function for each table referenced in the emaj_relation table still exist RETURN QUERY SELECT 'In group "' || rel_group || '", the log function "' || rel_log_schema || '"."' || rel_log_function || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_proc, pg_catalog.pg_namespace WHERE nspname = rel_log_schema AND proname = rel_log_function AND pronamespace = pg_namespace.oid) ORDER BY rel_schema, rel_tblseq, 1; -- check log and truncate triggers for all tables referenced in the emaj_relation table still exist -- start with log triggers RETURN QUERY SELECT 'In group "' || rel_group || '", the log trigger "emaj_log_trg" on table "' || rel_schema || '"."' || rel_tblseq || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_trigger, pg_catalog.pg_namespace, pg_catalog.pg_class WHERE nspname = rel_schema AND relname = rel_tblseq AND tgname = 'emaj_log_trg' AND tgrelid = pg_class.oid AND relnamespace = pg_namespace.oid) -- do not issue a row if the application table does not exist, -- this case has been already detected AND EXISTS (SELECT NULL FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE nspname = rel_schema AND relname = rel_tblseq AND relnamespace = pg_namespace.oid) ORDER BY rel_schema, rel_tblseq, 1; -- then truncate triggers RETURN QUERY SELECT 'In group "' || rel_group || '", the truncate trigger "emaj_trunc_trg" on table "' || rel_schema || '"."' || rel_tblseq || '" is not found.' AS msg FROM emaj.emaj_relation WHERE rel_kind = 'r' AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_trigger, pg_catalog.pg_namespace, pg_catalog.pg_class WHERE nspname = rel_schema AND relname = rel_tblseq AND tgname = 'emaj_trunc_trg' AND tgrelid = pg_class.oid AND relnamespace = pg_namespace.oid) -- do not issue a row if the application table does not exist, -- this case has been already detected AND EXISTS (SELECT NULL FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE nspname = rel_schema AND relname = rel_tblseq AND relnamespace = pg_namespace.oid) ORDER BY rel_schema, rel_tblseq, 1; -- check all log tables have a structure consistent with the application tables they reference -- (same columns and same formats). It only returns one row per faulting table. RETURN QUERY SELECT msg FROM ( WITH cte_app_tables_columns AS ( -- application table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM emaj.emaj_relation, pg_catalog.pg_attribute, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = rel_schema AND relname = rel_tblseq AND attrelid = pg_class.oid AND attnum > 0 AND attisdropped = false AND rel_kind = 'r'), cte_log_tables_columns AS ( -- log table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM emaj.emaj_relation, pg_catalog.pg_attribute, pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = rel_log_schema AND relname = rel_log_table AND attrelid = pg_class.oid AND attnum > 0 AND attisdropped = false AND attname NOT LIKE 'emaj%' AND rel_kind = 'r') SELECT DISTINCT rel_schema, rel_tblseq, 'In group "' || rel_group || '", the structure of the application table "' || rel_schema || '"."' || rel_tblseq || '" is not coherent with its log table ("' || rel_log_schema || '"."' || rel_log_table || '").' AS msg FROM ( ( -- application table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_app_tables_columns EXCEPT -- minus log table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_log_tables_columns ) UNION ( -- log table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_log_tables_columns EXCEPT -- minus application table's columns SELECT rel_group, rel_schema, rel_tblseq, rel_log_schema, rel_log_table, attname, atttypid, attlen, atttypmod FROM cte_app_tables_columns )) AS t -- do not issue a row if the log or application table does not exist, -- these cases have been already detected WHERE (rel_log_schema, rel_log_table) IN (SELECT nspname, relname FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid) AND (rel_schema, rel_tblseq) IN (SELECT nspname, relname FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE relnamespace = pg_namespace.oid) ORDER BY 1,2,3 ) AS t; -- check all tables of rollbackable groups have their primary key RETURN QUERY SELECT 'In rollbackable group "' || rel_group || '", the table "' || rel_schema || '"."' || rel_tblseq || '" has no primary key any more.' AS msg FROM emaj.emaj_relation, emaj.emaj_group WHERE rel_kind = 'r' AND rel_group = group_name AND group_is_rollbackable AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_class, pg_catalog.pg_namespace, pg_catalog.pg_constraint WHERE nspname = rel_schema AND relname = rel_tblseq AND relnamespace = pg_namespace.oid AND connamespace = pg_namespace.oid AND conrelid = pg_class.oid AND contype = 'p') -- do not issue a row if the application table does not exist, -- this case has been already detected AND EXISTS (SELECT NULL FROM pg_catalog.pg_class, pg_catalog.pg_namespace WHERE nspname = rel_schema AND relname = rel_tblseq AND relnamespace = pg_namespace.oid) ORDER BY rel_schema, rel_tblseq, 1; RETURN; END; $_verify_all_groups$; CREATE OR REPLACE FUNCTION emaj.emaj_disable_protection_by_event_triggers() RETURNS INT LANGUAGE plpgsql AS $emaj_disable_protection_by_event_triggers$ -- This function disables all known E-Maj event triggers that are in enabled state. -- It may be used by an emaj_adm role. -- Output: number of effectively disabled event triggers DECLARE v_eventTriggers TEXT[]; BEGIN -- call the _disable_event_triggers() function and get the disabled event trigger names array SELECT emaj._disable_event_triggers() INTO v_eventTriggers; -- insert a row into the emaj_hist table INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_wording) VALUES ('DISABLE_PROTECTION', 'EVENT TRIGGERS DISABLED', CASE WHEN v_eventTriggers <> ARRAY[]::TEXT[] THEN array_to_string(v_eventTriggers, ', ') ELSE '' END); -- return the number of disabled event triggers RETURN coalesce(array_length(v_eventTriggers,1),0); END; $emaj_disable_protection_by_event_triggers$; COMMENT ON FUNCTION emaj.emaj_disable_protection_by_event_triggers() IS $$Disables the protection of E-Maj components by event triggers.$$; CREATE OR REPLACE FUNCTION emaj.emaj_enable_protection_by_event_triggers() RETURNS INT LANGUAGE plpgsql AS $emaj_enable_protection_by_event_triggers$ -- This function enables all known E-Maj event triggers that are in disabled state. -- It may be used by an emaj_adm role. -- Output: number of effectively enabled event triggers DECLARE v_eventTriggers TEXT[]; BEGIN IF emaj._pg_version_num() >= 90300 THEN -- build the event trigger names array from the pg_event_trigger table -- (pg_event_trigger table doesn't exists in 9.2- postgres versions) SELECT coalesce(array_agg(evtname ORDER BY evtname),ARRAY[]::TEXT[]) INTO v_eventTriggers FROM pg_catalog.pg_event_trigger WHERE evtname LIKE 'emaj%' AND evtenabled = 'D'; -- call the _enable_event_triggers() function PERFORM emaj._enable_event_triggers(v_eventTriggers); END IF; -- insert a row into the emaj_hist table INSERT INTO emaj.emaj_hist (hist_function, hist_event, hist_wording) VALUES ('ENABLE_PROTECTION', 'EVENT TRIGGERS ENABLED', CASE WHEN v_eventTriggers <> ARRAY[]::TEXT[] THEN array_to_string(v_eventTriggers, ', ') ELSE '' END); -- return the number of disabled event triggers RETURN coalesce(array_length(v_eventTriggers,1),0); END; $emaj_enable_protection_by_event_triggers$; COMMENT ON FUNCTION emaj.emaj_enable_protection_by_event_triggers() IS $$Enables the protection of E-Maj components by event triggers.$$; CREATE OR REPLACE FUNCTION emaj._disable_event_triggers() RETURNS TEXT[] LANGUAGE plpgsql SECURITY DEFINER AS $_disable_event_triggers$ -- This function disables all known E-Maj event triggers that are in enabled state. -- The function is called by functions that alter or drop E-Maj components, such as emaj_drop_group(). -- It is also called by the user emaj_disable_event_triggers_protection() function. -- Output: array of effectively disabled event trigger names. It can be reused as input when calling _enable_event_triggers() DECLARE v_eventTrigger TEXT; v_eventTriggers TEXT[] = ARRAY[]::TEXT[]; BEGIN IF emaj._pg_version_num() >= 90300 THEN -- build the event trigger names array from the pg_event_trigger table -- (pg_event_trigger table doesn't exists in 9.2- postgres versions) SELECT coalesce(array_agg(evtname ORDER BY evtname),ARRAY[]::TEXT[]) INTO v_eventTriggers FROM pg_catalog.pg_event_trigger WHERE evtname LIKE 'emaj%' AND evtenabled <> 'D'; -- disable each event trigger FOREACH v_eventTrigger IN ARRAY v_eventTriggers LOOP EXECUTE 'ALTER EVENT TRIGGER ' || v_eventTrigger || ' DISABLE'; END LOOP; END IF; RETURN v_eventTriggers; END; $_disable_event_triggers$; CREATE OR REPLACE FUNCTION emaj._enable_event_triggers(v_eventTriggers TEXT[]) RETURNS TEXT[] LANGUAGE plpgsql SECURITY DEFINER AS $_enable_event_triggers$ -- This function enables all event triggers supplied as parameter -- The function is called by functions that alter or drop E-Maj components, such as emaj_drop_group(). -- It is also called by the user emaj_enable_event_triggers_protection() function. -- Input: array of event trigger names to enable -- Output: same array DECLARE v_eventTrigger TEXT; BEGIN FOREACH v_eventTrigger IN ARRAY v_eventTriggers LOOP EXECUTE 'ALTER EVENT TRIGGER ' || v_eventTrigger || ' ENABLE'; END LOOP; RETURN v_eventTriggers; END; $_enable_event_triggers$; -- pattern used by the tool that extracts and insert the functions definition ------------------------------------------ -- -- -- event triggers and related functions -- -- -- ------------------------------------------ ------------------------------------ -- -- -- emaj roles and rights -- -- -- ------------------------------------ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA emaj FROM PUBLIC; GRANT ALL ON ALL TABLES IN SCHEMA emaj TO emaj_adm; GRANT ALL ON ALL SEQUENCES IN SCHEMA emaj TO emaj_adm; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA emaj TO emaj_adm; GRANT SELECT ON ALL TABLES IN SCHEMA emaj TO emaj_viewer; GRANT SELECT ON ALL SEQUENCES IN SCHEMA emaj TO emaj_viewer; REVOKE SELECT ON TABLE emaj.emaj_param FROM emaj_viewer; GRANT EXECUTE ON FUNCTION emaj._log_stat_tbl(r_rel emaj.emaj_relation, v_firstMarkTimeId BIGINT, v_lastMarkTimeId BIGINT) TO emaj_viewer; ------------------------------------ -- -- -- complete the upgrade -- -- -- ------------------------------------ -- enable the event triggers DO $tmp$ DECLARE v_event_trigger_array TEXT[]; BEGIN IF emaj._pg_version_num() >= 90300 THEN -- build the event trigger names array from the pg_event_trigger table SELECT coalesce(array_agg(evtname),ARRAY[]::TEXT[]) INTO v_event_trigger_array FROM pg_catalog.pg_event_trigger WHERE evtname LIKE 'emaj%' AND evtenabled = 'D'; -- call the _enable_event_triggers() function PERFORM emaj._enable_event_triggers(v_event_trigger_array); END IF; END; $tmp$; -- Set comments for all internal functions, -- by directly inserting a row in the pg_description table for all emaj functions that do not have yet a recorded comment INSERT INTO pg_catalog.pg_description (objoid, classoid, objsubid, description) SELECT pg_proc.oid, pg_class.oid, 0 , 'E-Maj internal function' FROM pg_catalog.pg_proc, pg_catalog.pg_class WHERE pg_class.relname = 'pg_proc' AND pg_proc.oid IN -- list all emaj functions that do not have yet a comment in pg_description (SELECT pg_proc.oid FROM pg_catalog.pg_proc JOIN pg_catalog.pg_namespace ON (pronamespace=pg_namespace.oid) LEFT OUTER JOIN pg_catalog.pg_description ON (pg_description.objoid = pg_proc.oid AND classoid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'pg_proc') AND objsubid = 0) WHERE nspname = 'emaj' AND (proname LIKE E'emaj\\_%' OR proname LIKE E'\\_%') AND pg_description.description IS NULL ); -- update the version id in the emaj_param table UPDATE emaj.emaj_param SET param_value_text = '2.0.1' WHERE param_key = 'emaj_version'; -- insert the upgrade record in the operation history INSERT INTO emaj.emaj_hist (hist_function, hist_object, hist_wording) VALUES ('EMAJ_INSTALL','E-Maj 2.0.1', 'Upgrade from 2.0.0 completed'); -- post installation checks DO $tmp$ DECLARE BEGIN -- check the max_prepared_transactions GUC value IF current_setting('max_prepared_transactions')::int <= 1 THEN RAISE WARNING 'E-Maj upgrade: as the max_prepared_transactions parameter value (%) on this cluster is too low, no parallel rollback is possible.', current_setting('max_prepared_transactions'); END IF; END; $tmp$; RESET default_tablespace; SET client_min_messages TO default;