-- -- E-MAJ : logs and rollbacks table updates : V 0.5 -- -- This script creates all objects needed to log and rollback table updates. -- -- These objects are recorded into a specific schema, named "emaj". -- It mainly contains few technical tables, a set of functions, and one "log table" per processed application table. -- -- This script must be executed by a role having SUPERUSER privileges. -- Before its execution: -- -> the concerned cluster must contain a tablespace named "tspemaj", for instance previously created by -- CREATE TABLESPACE tspemaj LOCATION '/.../tspemaj', -- -> the plpgsql language must have been created in the concerned database. \set ON_ERROR_STOP BEGIN TRANSACTION; ------------------------------------ -- -- -- emaj schema and tables -- -- -- ------------------------------------ -- (re)creation of the schema 'emaj' containing all the needed objets DROP SCHEMA IF EXISTS emaj CASCADE; CREATE SCHEMA emaj; -- uncomment the next line to let emaj schema visible to all user (for test purpose) --GRANT USAGE ON SCHEMA emaj TO PUBLIC; -- creation of the technical tables -- table containing the history of operations CREATE TABLE emaj.emaj_hist ( oper_datetime TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp() PRIMARY KEY, oper_type TEXT NOT NULL, oper_object TEXT, oper_wording TEXT, oper_user TEXT DEFAULT session_user ) TABLESPACE tspemaj; -- table containing the active marks (the marks set at or after start_group) CREATE TABLE emaj.emaj_mark ( mark_datetime TIMESTAMPTZ NOT NULL PRIMARY KEY, mark_group TEXT, mark_name TEXT ) TABLESPACE tspemaj; -- table containing the tables groups CREATE TABLE emaj.emaj_group ( group_name TEXT NOT NULL, group_schema TEXT NOT NULL, group_table TEXT NOT NULL, PRIMARY KEY (group_name, group_schema, group_table) ) TABLESPACE tspemaj; -- table containing the sequences log -- (to record the state at mark time of application sequences and sequences used by log tables) CREATE TABLE emaj.emaj_sequence ( sequ_schema TEXT NOT NULL, sequ_name TEXT NOT NULL, sequ_datetime TIMESTAMPTZ NOT NULL, sequ_mark TEXT NOT NULL, sequ_last_val BIGINT NOT NULL, sequ_start_val BIGINT NOT NULL, sequ_increment BIGINT NOT NULL, sequ_max_val BIGINT NOT NULL, sequ_min_val BIGINT NOT NULL, sequ_cache_val BIGINT NOT NULL, sequ_is_cycled BOOLEAN NOT NULL, sequ_is_called BOOLEAN NOT NULL, PRIMARY KEY (sequ_schema, sequ_name, sequ_datetime) ) TABLESPACE tspemaj; -- uncomment the 4 next lines to let emaj tables visible to all user (for test purpose) --GRANT SELECT ON emaj.emaj_hist TO PUBLIC; --GRANT SELECT ON emaj.emaj_mark TO PUBLIC; --GRANT SELECT ON emaj.emaj_group TO PUBLIC; --GRANT SELECT ON emaj.emaj_sequence TO PUBLIC; ------------------------------------ -- -- -- Low level Functions -- -- -- ------------------------------------ CREATE or REPLACE FUNCTION emaj.emaj_check_class(v_schemaName TEXT, v_className TEXT) RETURNS TEXT AS $emaj_check_class$ -- This function verifies that an application table or sequence exists in pg_class -- It also protects from a recursive use : tables or sequences from emaj schema cannot be managed by EMAJ -- Input: the names of the schema and the class (table or sequence) -- Output: the relkind of the class : 'r' for a table and 's' for a sequence -- If the schema or the class is not known, the function stops. DECLARE v_relkind TEXT; v_schemaOid OID; BEGIN IF v_schemaName = 'emaj' THEN RAISE EXCEPTION 'emaj_check_class : object from schema % cannot be managed by EMAJ', v_schemaName; END IF; SELECT oid INTO v_schemaOid FROM pg_namespace WHERE nspname = v_schemaName; IF NOT found THEN RAISE EXCEPTION 'emaj_check_class : schema % doesn''t exist', v_schemaName; END IF; SELECT relkind INTO v_relkind FROM pg_class WHERE relNameSpace = v_schemaOid AND relName = v_className AND relkind in ('r','S'); IF NOT found THEN RAISE EXCEPTION 'emaj_check_class : table or sequence % doesn''t exist', v_className; END IF; RETURN v_relkind; END; $emaj_check_class$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_create_log(v_schemaName TEXT, v_tableName TEXT) RETURNS void AS $emaj_create_log$ -- This function creates all what is needed to manage the log and rollback operations for an application table -- Input: schema name (mandatory even for the 'public' schema) and table name -- Are created: -- - the associated log table, with its own sequence -- - the function that logs the tables updates, defined as a trigger -- - the rollback function (one per table) DECLARE -- variables for the name of tables, functions, triggers,... v_fullTableName TEXT; v_emajSchema TEXT := 'emaj'; v_emajTblSpace TEXT := 'tspemaj'; v_logTableName TEXT; v_logFnctName TEXT; v_rlbkFnctName TEXT; v_triggerName TEXT; -- variables to hold pieces of SQL v_pkCondList TEXT; v_colList TEXT; v_setList TEXT; -- other variables v_attname TEXT; v_relhaspkey BOOLEAN; -- cursor to retrieve all columns of the application table col1_curs CURSOR (tbl regclass) FOR SELECT attname FROM pg_attribute WHERE attrelid = tbl AND attnum > 0 AND attisdropped = false; -- cursor to retrieve all columns of table's primary key col2_curs CURSOR (tbl regclass) FOR SELECT attname FROM pg_attribute, pg_index WHERE pg_attribute.attrelid = pg_index.indexrelid AND indrelid = tbl AND attnum > 0 AND attisdropped = false; -- cursor to retrive all columns that are not in the primary key col3_curs CURSOR (tbl regclass) FOR SELECT attname FROM pg_attribute WHERE attrelid = tbl AND attnum > 0 AND attisdropped = false EXCEPT SELECT attname FROM pg_attribute, pg_index WHERE pg_attribute.attrelid = pg_index.indexrelid AND indrelid = tbl AND attnum > 0 AND attisdropped = false; BEGIN -- check the table has a primary key BEGIN SELECT relhaspkey INTO v_relhaspkey FROM pg_class, pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = v_schemaName AND relname = v_tableName; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_create_log: Internal error - schema.table not found in pg_class'; END IF; END; IF v_relhaspkey = FALSE THEN RAISE EXCEPTION 'emaj_create_log : table % has no PRIMARY KEY', v_tableName; END IF; -- OK, build the different name for table, trigger, functions,... v_fullTableName := quote_ident(v_schemaName) || '.' || quote_ident(v_tableName); v_logTableName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_log'); v_logFnctName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_log_fnct'); v_rlbkFnctName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_rlbk_fnct'); v_triggerName := quote_ident(v_schemaName || '_' || v_tableName || '_emaj_trg'); -- creation of the log table: the log table 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 || ') TABLESPACE ' || v_emajTblSpace ; EXECUTE 'ALTER TABLE ' || v_logTableName || ' ADD COLUMN emaj_verb VARCHAR(3)'; EXECUTE 'ALTER TABLE ' || v_logTableName || ' ADD COLUMN emaj_tuple VARCHAR(3)'; EXECUTE 'ALTER TABLE ' || v_logTableName || ' ADD COLUMN emaj_id BIGSERIAL'; EXECUTE 'ALTER TABLE ' || v_logTableName || ' ADD COLUMN emaj_changed TIMESTAMPTZ DEFAULT current_timestamp'; EXECUTE 'ALTER TABLE ' || v_logTableName || ' ADD COLUMN emaj_user VARCHAR(32) DEFAULT session_user'; -- 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' || ' 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'';' || ' 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 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 ' || v_triggerName || ' ON ' || v_fullTableName; EXECUTE 'CREATE TRIGGER ' || v_triggerName || ' AFTER INSERT OR UPDATE OR DELETE ON ' || v_fullTableName || ' FOR EACH ROW EXECUTE PROCEDURE ' || v_logFnctName || '()'; EXECUTE 'ALTER TABLE ' || v_fullTableName || ' DISABLE TRIGGER ' || v_triggerName; -- -- creation of the rollback function -- -- First build some pieces of the CREATE FUNCTION statement -- build the tables's columns list v_colList := ''; OPEN col1_curs (v_fullTableName); LOOP FETCH col1_curs INTO v_attname; EXIT WHEN NOT FOUND; IF v_colList = '' THEN v_colList := 'rec_log.' || v_attname; ELSE v_colList := v_colList || ', rec_log.' || v_attname; END IF; END LOOP; CLOSE col1_curs; -- build "equality on the primary key" conditions, from the list of the primary key's columns v_pkCondList := ''; OPEN col2_curs (v_fullTableName); LOOP FETCH col2_curs INTO v_attname; EXIT WHEN NOT FOUND; IF v_pkCondList = '' THEN v_pkCondList := v_attname || ' = rec_log.' || v_attname; ELSE v_pkCondList := v_pkCondList || ' AND ' || v_attname || ' = rec_log.' || v_attname; END IF; END LOOP; CLOSE col2_curs; -- build the SET clause for the UPDATE, from the list of the columns that don't belong to the primary key v_setList := ''; OPEN col3_curs (v_fullTableName); LOOP FETCH col3_curs INTO v_attname; EXIT WHEN NOT FOUND; IF v_setList = '' THEN v_setList := v_attname || ' = rec_old_log.' || v_attname; ELSE v_setList := v_setList || ', ' || v_attname || ' = rec_old_log.' || v_attname; END IF; END LOOP; CLOSE col3_curs; -- Then finaly create the rollback function associated to the table -- At execution, it will loop on each row from the log table in reverse order -- It will insert the old deleted rows, delete the new inserted row -- and update the new rows by setting back the old rows -- The function returns the number of rollbacked elementary operations or rows -- All these functions will be called by the emaj_rlbk_table function, which is activated by the -- emaj_rollback_group function EXECUTE 'CREATE or REPLACE FUNCTION ' || v_rlbkFnctName || ' (v_rollback_id_limit bigint)' || ' RETURNS bigint AS $rlbkfnct$' || ' DECLARE' || ' v_nb_rows bigint := 0;' || ' v_nb_proc_rows integer;' || ' rec_log ' || v_logTableName || '%ROWTYPE;' || ' rec_old_log ' || v_logTableName || '%ROWTYPE;' || ' log_curs CURSOR FOR ' || ' SELECT * FROM ' || v_logTableName || ' WHERE emaj_id >= v_rollback_id_limit ' || ' ORDER BY emaj_id DESC;' || ' BEGIN' || ' OPEN log_curs;' || ' LOOP ' || ' FETCH log_curs INTO rec_log;' || ' EXIT WHEN NOT FOUND;' || ' IF rec_log.emaj_verb = ''INS'' THEN' -- || ' RAISE NOTICE ''emaj_id = % ; INS'', rec_log.emaj_id;' || ' DELETE FROM ' || v_fullTableName || ' WHERE ' || v_pkCondList || ';' || ' ELSIF rec_log.emaj_verb = ''UPD'' THEN' -- || ' RAISE NOTICE ''emaj_id = % ; UPD ; %'', rec_log.emaj_id,rec_log.emaj_tuple;' || ' FETCH log_curs into rec_old_log;' -- || ' RAISE NOTICE ''emaj_id = % ; UPD ; %'', rec_old_log.emaj_id,rec_old_log.emaj_tuple;' || ' UPDATE ' || v_fullTableName || ' SET ' || v_setList || ' WHERE ' || v_pkCondList || ';' || ' ELSIF rec_log.emaj_verb = ''DEL'' THEN' -- || ' RAISE NOTICE ''emaj_id = % ; DEL'', rec_log.emaj_id;' || ' INSERT INTO ' || v_fullTableName || ' VALUES (' || v_colList || ');' || ' ELSE' || ' RAISE EXCEPTION ''' || v_rlbkFnctName || ': internal error - emaj_verb = % unknown, emaj_id = %'',' || ' rec_log.emaj_verb, rec_log.emaj_id;' || ' END IF;' || ' GET DIAGNOSTICS v_nb_proc_rows = ROW_COUNT;' || ' IF v_nb_proc_rows <> 1 THEN' || ' RAISE EXCEPTION ''' || v_rlbkFnctName || ': internal error - emaj_verb = %, emaj_id = %, # processed rows = % ''' || ' ,rec_log.emaj_verb, rec_log.emaj_id, v_nb_proc_rows;' || ' END IF;' || ' v_nb_rows := v_nb_rows + 1;' || ' END LOOP;' || ' CLOSE log_curs;' -- || ' RAISE NOTICE ''Table ' || v_fullTableName || ' -> % rollbacked rows'', v_nb_rows;' || ' RETURN v_nb_rows;' || ' END;' || '$rlbkfnct$ LANGUAGE plpgsql;'; RETURN; END; $emaj_create_log$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_delete_log(v_schemaName TEXT, v_tableName TEXT) RETURNS void AS $emaj_delete_log$ -- The function deletes all what has been created by emaj_create_log -- Required inputs: schema name (mandatory even if "public") and table name DECLARE v_emajSchema TEXT := 'emaj'; v_fullTableName TEXT; v_logTableName TEXT; v_logFnctName TEXT; v_rlbkFnctName TEXT; v_triggerName TEXT; BEGIN v_fullTableName := quote_ident(v_schemaName) || '.' || quote_ident(v_tableName); v_logTableName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_log'); v_logFnctName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_log_fnct'); v_rlbkFnctName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_rlbk_fnct'); v_triggerName := quote_ident(v_schemaName || '_' || v_tableName || '_emaj_trg'); -- delete the trigger on the application table EXECUTE 'DROP TRIGGER IF EXISTS ' || v_triggerName || ' ON ' || v_fullTableName; -- delete both log and rollback functions EXECUTE 'DROP FUNCTION IF EXISTS ' || v_logFnctName || '()'; EXECUTE 'DROP FUNCTION IF EXISTS ' || v_rlbkFnctName || '(bigint)'; -- delete the log table EXECUTE 'DROP TABLE IF EXISTS ' || v_logTableName || ' CASCADE'; RETURN; END; $emaj_delete_log$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_delete_seq(v_schemaName TEXT, v_seqName TEXT) RETURNS void AS $emaj_delete_seq$ -- The function deletes the rows stored into emaj_sequence for a particular sequence -- Required inputs: schema name and sequence name BEGIN -- delete rows from emaj_sequence EXECUTE 'DELETE FROM emaj.emaj_sequence WHERE sequ_schema = ''' || v_schemaName || ''' AND sequ_name = ''' || v_seqName || ''''; RETURN; END; $emaj_delete_seq$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_rlbk_table(v_schemaName TEXT, v_tableName TEXT, v_timestamp TIMESTAMPTZ) RETURNS void AS $emaj_rlbk_table$ -- This function rollbacks one table to a given timestamp -- The function is used by emaj.emaj_rollback_group -- Input: schema name and table name, timestamp limit for rollback DECLARE v_emajSchema TEXT := 'emaj'; v_fullTableName TEXT; v_logTableName TEXT; v_rlbkFnctName TEXT; v_triggerName TEXT; v_fullSeqName TEXT; v_emaj_id BIGINT; v_nb_rows BIGINT; BEGIN v_fullTableName := quote_ident(v_schemaName) || '.' || quote_ident(v_tableName); v_logTableName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_log'); v_rlbkFnctName := quote_ident(v_emajSchema) || '.' || quote_ident(v_schemaName || '_' || v_tableName || '_rlbk_fnct'); v_triggerName := quote_ident(v_schemaName || '_' || v_tableName || '_emaj_trg'); v_fullSeqName := quote_ident(v_schemaName || '_' || v_tableName || '_log_emaj_id_seq'); -- get the emaj_id to rollback to from the sequence (first emaj_id to delete) SELECT CASE WHEN sequ_is_called THEN sequ_last_val + sequ_increment ELSE sequ_last_val END INTO v_emaj_id FROM emaj.emaj_sequence WHERE sequ_schema = 'emaj' AND sequ_name = v_fullSeqName AND sequ_datetime = v_timestamp; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_rlbk_table: internal error - sequence for % and % not found in emaj_sequence',v_fullSeqName, v_timestamp; END IF; -- insert begin event in history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('ROLLBACK_TABLE BEGIN', v_fullTableName, 'Up to log_id ' || v_emaj_id); -- first deactivate the trigger on the application table EXECUTE 'ALTER TABLE ' || v_fullTableName || ' DISABLE TRIGGER ' || v_triggerName; -- rollback the table EXECUTE 'SELECT ' || v_rlbkFnctName || '(' || v_emaj_id || ')' INTO v_nb_rows; -- suppress the rollbacked log part EXECUTE 'DELETE FROM ' || v_logTableName || ' WHERE emaj_id >= ' || v_emaj_id; -- re-activate the trigger on the application table EXECUTE 'ALTER TABLE ' || v_fullTableName || ' ENABLE TRIGGER ' || v_triggerName; -- suppress from emaj_sequence table the row regarding the emaj sequence for this application table DELETE FROM emaj.emaj_sequence WHERE sequ_schema = 'emaj' AND sequ_name = v_fullSeqName AND sequ_datetime = v_timestamp; -- insert end event in history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('ROLLBACK_TABLE END', v_fullTableName, ' -> ' || v_nb_rows || ' rollbacked rows '); RETURN; END; $emaj_rlbk_table$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_rlbk_sequence(v_schemaName TEXT, v_seqName TEXT, v_timestamp TIMESTAMPTZ) RETURNS void AS $emaj_rlbk_sequence$ -- This function rollbacks one sequence to a given mark -- The function is used by emaj.emaj_rollback_group -- Input: schema name and table name, mark DECLARE v_pgversion TEXT := substring (version() from E'PostgreSQL\\s(\\d+\\.\\d+)\\.'); 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_mark, 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 = v_schemaName AND sequ_name = v_seqName AND sequ_datetime = v_timestamp; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'emaj_rlbk_sequence: Mark at % not found for sequence % / %', v_timestamp, v_schemaName, v_seqName; WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'emaj_rlbk_sequence: Internal error 1'; END; -- Read the current sequence's characteristics v_fullSeqName := quote_ident(v_schemaName) || '.' || quote_ident(v_seqName); v_stmt = 'SELECT last_value, '; IF v_pgversion <= '8.3' THEN v_stmt = v_stmt || '0 as start_value, '; ELSE v_stmt = v_stmt || 'start_value, '; END IF; v_stmt = v_stmt || 'increment_by, max_value, min_value, cache_value, is_cycled, is_called FROM ' || v_fullSeqName; EXECUTE v_stmt 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 RAISE NOTICE 'Rollback sequence % with%', v_fullSeqName, v_stmt; EXECUTE 'ALTER SEQUENCE ' || v_fullSeqName || v_stmt; END IF; -- delete the rollbacked sequence from the sequence table DELETE FROM emaj.emaj_sequence WHERE sequ_schema = v_schemaName AND sequ_name = v_seqName AND sequ_datetime >= v_timestamp; -- insert event in history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('ROLLBACK_SEQUENCE', v_fullSeqName, SUBSTR(v_stmt,2)); RETURN; END; $emaj_rlbk_sequence$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_verify_all() RETURNS void AS $emaj_verify_all$ -- The function verifies the consistency between log and application tables -- It generates an error if the check fails DECLARE v_emajSchema TEXT := 'emaj'; v_fullTableName TEXT; v_logTableName TEXT; v_logFnctName TEXT; v_rlbkFnctName TEXT; v_triggerName TEXT; v_relkind TEXT; r_tblsq RECORD; BEGIN -- verify that no table or sequence belongs to several groups BEGIN PERFORM group_schema, group_table, count(*) FROM emaj.emaj_group GROUP BY group_schema, group_table HAVING count(*) > 1; IF FOUND THEN RAISE EXCEPTION 'emaj_verify_all: At least one table is referenced by several groups'; END IF; END; -- verify that all log tables correspond to a row in the groups table BEGIN PERFORM relname FROM pg_class, pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = 'emaj' AND relkind = 'r' AND relname LIKE '%_log' EXCEPT SELECT group_schema || '_' || group_table || '_log' FROM emaj.emaj_group; IF FOUND THEN RAISE EXCEPTION 'emaj_verify_all: At least one log table is not linked to an application table though the emaj_group table'; END IF; END; -- per table verifications FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, ... v_logTableName := r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log'; v_logFnctName := r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log_fnct'; v_rlbkFnctName := r_tblsq.group_schema || '_' || r_tblsq.group_table || '_rlbk_fnct'; v_triggerName := r_tblsq.group_schema || '_' || r_tblsq.group_table || '_emaj_trg'; v_fullTableName := quote_ident(r_tblsq.group_schema) || '.' || quote_ident(r_tblsq.group_table); -- -> check the log table exists PERFORM relname FROM pg_class, pg_namespace WHERE relnamespace = pg_namespace.oid AND nspname = 'emaj' AND relkind = 'r' AND relname = v_logTableName; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_verify_all: Log table % not found',v_logTableName; END IF; -- -> check boths functions exists PERFORM proname from pg_proc , pg_namespace WHERE pronamespace = pg_namespace.oid AND nspname = 'emaj' AND proname = v_logFnctName; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_verify_all: Log function % not found',v_logFnctName; END IF; PERFORM proname from pg_proc , pg_namespace WHERE pronamespace = pg_namespace.oid AND nspname = 'emaj' AND proname = v_rlbkFnctName; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_verify_all: Restore function % not found',v_rlbkFnctName; END IF; -- -> check the trigger exists PERFORM tgname FROM pg_trigger WHERE tgname = v_triggerName; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_verify_all: Trigger % not found',v_triggerName; END IF; -- -> check that the log tables structure is consistent with the application tables structure -- (same columns and same formats) PERFORM attname, atttypid, attlen, atttypmod FROM pg_attribute, pg_class, pg_namespace WHERE nspname = r_tblsq.group_schema AND relnamespace = pg_namespace.oid AND relname = r_tblsq.group_table AND attrelid = pg_class.oid AND attnum > 0 AND attisdropped = false EXCEPT SELECT attname, atttypid, attlen, atttypmod FROM pg_attribute, pg_class, pg_namespace WHERE nspname = v_emajSchema AND relnamespace = pg_namespace.oid AND relname = v_logTableName AND attrelid = pg_class.oid AND attnum > 0 AND attisdropped = false AND attname NOT LIKE 'emaj%'; IF FOUND THEN RAISE EXCEPTION 'emaj_verify_all: The structure of log table % is not coherent with %' ,v_logTableName,v_fullTableName; END IF; ELSEIF v_relkind = 'S' THEN -- if it is a sequence, nothing to do END IF; END LOOP; RETURN; END; $emaj_verify_all$ LANGUAGE plpgsql; ------------------------------------------------ ---- ---- ---- Functions to manage groups ---- ---- ---- ------------------------------------------------ CREATE or REPLACE FUNCTION emaj.emaj_lock_group(v_groupName TEXT) RETURNS void AS $emaj_lock_group$ -- This function locks all tables of a group -- Input: group name DECLARE v_relkind TEXT; v_fullTableName TEXT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('LOCK_GROUP BEGIN', v_groupName, ''); -- scan all classes of the group FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, lock the table v_fullTableName := quote_ident(r_tblsq.group_schema) || '.' || quote_ident(r_tblsq.group_table); EXECUTE 'LOCK TABLE ' || v_fullTableName; END IF; END LOOP; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('LOCK_GROUP END', v_groupName, ''); RETURN; END; $emaj_lock_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_create_group(v_groupName TEXT) RETURNS INT AS $emaj_create_group$ -- This function creates emaj objects for all tables of a group -- Input: group name -- Output: number of processed tables and sequences DECLARE v_pgversion TEXT := substring (version() from E'PostgreSQL\\s(\\d+\\.\\d+)\\.'); v_nb_tbl INT := 0; v_relkind TEXT; v_stmt TEXT; v_nb_trg INT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('CREATE_GROUP BEGIN', v_groupName, ''); -- check that no table of the group has an activated log trigger v_stmt = 'SELECT count(tgname) FROM pg_trigger, emaj.emaj_group ' || 'WHERE group_name = ''' || v_groupName || '''' || ' AND tgname = group_schema || ''_'' || group_table || ''_emaj_trg'' ' || ' AND tgenabled '; IF v_pgversion <= '8.2' THEN v_stmt = v_stmt || '= true'; ELSE v_stmt = v_stmt || '!= ''D'''; END IF; EXECUTE v_stmt INTO v_nb_trg; IF v_nb_trg > 0 THEN RAISE EXCEPTION 'emaj_create_group: At least one table of the group % has an enabled log trigger', v_groupName; END IF; -- OK, scan all classes of the group FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, create the related emaj objects PERFORM emaj.emaj_create_log (r_tblsq.group_schema, r_tblsq.group_table); ELSEIF v_relkind = 'S' THEN -- if it is a sequence, nothing to do END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_create_group: Group % is unknown', v_groupName; END IF; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('CREATE_GROUP END', v_groupName, v_nb_tbl || ' tables/sequences processed'); RETURN v_nb_tbl; END; $emaj_create_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_delete_group(v_groupName TEXT) RETURNS INT AS $emaj_delete_group$ -- This function deletes the emaj objects for all tables of a group -- Input: group name -- Output: number of processed tables and sequences DECLARE v_nb_tbl INT := 0; v_relkind TEXT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('DELETE_GROUP BEGIN', v_groupName, ''); -- check that the group is not in a LOGGING state PERFORM oper_datetime FROM emaj.emaj_hist WHERE oper_type = 'emaj_START' AND oper_datetime > (SELECT MAX(oper_datetime) FROM emaj.emaj_hist WHERE oper_type = 'emaj_STOP'); IF FOUND THEN RAISE EXCEPTION 'emaj_delete_group: The group % is in logging state', v_groupName; END IF; -- OK, delete the emaj objets for each table of the group FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, delete the related emaj objects PERFORM emaj.emaj_delete_log (r_tblsq.group_schema, r_tblsq.group_table); ELSEIF v_relkind = 'S' THEN -- if it is a sequence, delete all related data from emaj_sequence table PERFORM emaj.emaj_delete_seq (r_tblsq.group_schema, r_tblsq.group_table); END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_delete_group: Group % is unknown', v_groupName; END IF; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('DELETE_GROUP END', v_groupName, v_nb_tbl || ' tables/sequences processed'); RETURN v_nb_tbl; END; $emaj_delete_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_start_group(v_groupName TEXT, v_mark TEXT) RETURNS INT AS $emaj_start_group$ -- This function activates the log triggers of all the tables for a group and set a first mark -- Input: group name, name of the mark to set -- Output: number of processed tables DECLARE v_nb_tbl INT := 0; v_logTableName TEXT; v_fullTableName TEXT; v_triggerName TEXT; v_relkind TEXT; v_cpt BIGINT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('START_GROUP BEGIN', v_groupName, ''); -- check if the emaj environment is OK PERFORM emaj.emaj_verify_all(); -- check there is no remaining mark for the group PERFORM 1 FROM emaj.emaj_mark WHERE mark_group = v_groupName; IF found THEN RAISE EXCEPTION 'emaj_start_group: remaining rows in emaj.emaj_mark table for group %',v_groupName; END IF; -- check the log tables are empty FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, verify the log table is empty v_logTableName := quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log'); EXECUTE 'SELECT 1 FROM emaj.' || v_logTableName INTO v_cpt; IF v_cpt IS NOT NULL THEN RAISE EXCEPTION 'emaj_start_group: log table % is not empty', v_logTableName; END IF; ELSEIF v_relkind = 'S' THEN -- check emaj_sequence tables doesn't contain remaining rows for the sequence PERFORM 1 FROM emaj.emaj_sequence where sequ_schema = r_tblsq.group_schema AND sequ_name = r_tblsq.group_table; IF found THEN RAISE EXCEPTION 'emaj_start_group: remaining rows in emaj.emaj_sequence for %.%', r_tblsq.group_schema,r_tblsq.group_table; END IF; END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_start_group: Group % is unknown', v_groupName; END IF; v_nb_tbl = 0; -- OK, begin a sub-transaction, lock all tables to get a stable point ... BEGIN PERFORM emaj.emaj_lock_group(v_groupName); -- ... and enable all log triggers for the group FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, enable the emaj log trigger v_fullTableName := quote_ident(r_tblsq.group_schema) || '.' || quote_ident(r_tblsq.group_table); v_triggerName := quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_emaj_trg'); EXECUTE 'ALTER TABLE ' || v_fullTableName || ' ENABLE TRIGGER ' || v_triggerName; ELSEIF v_relkind = 'S' THEN -- if it is a sequence, nothing to do END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('START_GROUP END', v_groupName, v_nb_tbl || ' tables/sequences processed'); -- Set the first mark PERFORM emaj.emaj_set_mark_group(v_groupName, v_mark); -- ... and finaly commit the operation and release locks END; -- ... just before ending RETURN v_nb_tbl; END; $emaj_start_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_set_mark_group(v_groupName TEXT, v_mark TEXT) RETURNS void AS $emaj_set_mark_group$ -- This function inserts a mark in the emaj_mark table and takes an image of the sequences definitions for the group -- Input: group name, mark to set DECLARE v_pgversion TEXT := substring (version() from E'PostgreSQL\\s(\\d+\\.\\d+)\\.'); v_emajSchema TEXT := 'emaj'; v_flg INT := 0; v_nb_tbl INT := 0; v_relkind TEXT; v_fullSeqName TEXT; v_timestamp TIMESTAMPTZ; v_stmt TEXT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('SET_MARK_GROUP BEGIN', v_groupName, v_mark); -- check if the emaj environment is OK PERFORM emaj.emaj_verify_all(); -- if a mark with the same name already exists for the group, stop SELECT 1 INTO v_flg FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_mark LIMIT 1; IF v_flg > 0 THEN RAISE EXCEPTION 'emaj_set_mark_group: A mark has been already set with the same name (%) for this group (%).', v_mark, v_groupName; END IF; -- OK, begin a sub-transaction, lock all tables to get a stable point ... BEGIN PERFORM emaj.emaj_lock_group(v_groupName); -- ... look at the clock and insert the mark into the emaj_mark table v_timestamp = clock_timestamp(); INSERT INTO emaj.emaj_mark (mark_datetime, mark_group, mark_name) VALUES (v_timestamp, v_groupName, v_mark); -- then, examine the group's definition FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, record the emaj_id associated sequence parameters in the emaj sequence table v_fullSeqName := quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log_emaj_id_seq'); v_stmt = 'INSERT INTO emaj.emaj_sequence (' || 'sequ_schema, sequ_name, sequ_datetime, sequ_mark, sequ_last_val, sequ_start_val, ' || 'sequ_increment, sequ_max_val, sequ_min_val, sequ_cache_val, sequ_is_cycled, sequ_is_called ' || ') SELECT '''|| v_emajSchema || ''', ''' || v_fullSeqName || ''', ''' || v_timestamp || ''', ''' || v_mark || ''', ' || 'last_value, '; IF v_pgversion <= '8.3' THEN v_stmt = v_stmt || '0, '; ELSE v_stmt = v_stmt || 'start_value, '; END IF; v_stmt = v_stmt || 'increment_by, max_value, min_value, cache_value, is_cycled, is_called ' || 'FROM emaj.' || v_fullSeqName; EXECUTE v_stmt; ELSEIF v_relkind = 'S' THEN -- if it is a sequence, record the sequence parameters in the emaj sequence table v_fullSeqName := quote_ident(r_tblsq.group_schema) || '.' || quote_ident(r_tblsq.group_table); v_stmt = 'INSERT INTO emaj.emaj_sequence (' || 'sequ_schema, sequ_name, sequ_datetime, sequ_mark, sequ_last_val, sequ_start_val, ' || 'sequ_increment, sequ_max_val, sequ_min_val, sequ_cache_val, sequ_is_cycled, sequ_is_called ' || ') SELECT ''' || r_tblsq.group_schema || ''', ''' || r_tblsq.group_table || ''', ''' || v_timestamp || ''', ''' || v_mark || ''', ' || 'last_value, '; IF v_pgversion <= '8.3' THEN v_stmt = v_stmt || '0, '; ELSE v_stmt = v_stmt || 'start_value, '; END IF; v_stmt = v_stmt || 'increment_by, max_value, min_value, cache_value, is_cycled, is_called ' || 'FROM ' || v_fullSeqName; EXECUTE v_stmt; END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_set_mark_group: Group % is unknown', v_groupName; END IF; -- ... and finaly commit the operation and release locks END; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('SET_MARK_GROUP END', v_groupName, v_mark); RETURN; END; $emaj_set_mark_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_delete_mark_group(v_groupName TEXT, v_mark TEXT) RETURNS void AS $emaj_delete_mark_group$ -- This function deletes all traces from a previous set_mark_group function. -- Then, any rollback on the deleted mark will be possible. -- It affects emaj_mark and emaj_sequence tables. -- The initial mark set at start_group time cannot be deleted. -- Input: group name, mark to delete DECLARE v_emajSchema TEXT := 'emaj'; v_firstMark TEXT; v_timestamp TIMESTAMPTZ; v_nb_tbl INT := 0; v_relkind TEXT; v_fullSeqName TEXT; r_tblsq RECORD; BEGIN -- check if the mark is not the first mark for the group SELECT mark_name INTO v_firstMark FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_datetime = (SELECT min (mark_datetime) FROM emaj.emaj_mark WHERE mark_group = v_groupName); IF v_mark = v_firstMark THEN RAISE EXCEPTION 'emaj_delete_mark_group: The first mark (%) of the group cannot be deleted.', v_mark; END IF; -- OK BEGIN -- catch the mark's timestamp and then delete the mark from the emaj_mark table SELECT mark_datetime INTO v_timestamp FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_mark; DELETE FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_mark; -- then, examine the group's definition FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, record the emaj_id associated sequence parameters in the emaj sequence table v_fullSeqName := quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log_emaj_id_seq'); DELETE FROM emaj.emaj_sequence WHERE sequ_schema = 'emaj' AND sequ_name = v_fullSeqName AND sequ_datetime = v_timestamp; ELSEIF v_relkind = 'S' THEN -- if it is a sequence, record the sequence parameters in the emaj sequence table -- v_fullSeqName := quote_ident(r_tblsq.group_schema) || '.' || quote_ident(r_tblsq.group_table); DELETE FROM emaj.emaj_sequence WHERE sequ_schema = quote_ident(r_tblsq.group_schema) AND sequ_name = quote_ident(r_tblsq.group_table) AND sequ_datetime = v_timestamp; END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_delete_mark_group: Group % is unknown', v_groupName; END IF; END; -- insert in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('DELETE_MARK_GROUP', v_groupName, v_mark); RETURN; END; $emaj_delete_mark_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_stop_group(v_groupName TEXT) RETURNS INT AS $emaj_stop_group$ -- This function de-activates the log triggers of all the tables for a group -- Input: group name -- Output: number of processed tables and sequences DECLARE v_nb_tbl INT := 0; v_fullTableName TEXT; v_triggerName TEXT; v_relkind TEXT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('STOP_GROUP BEGIN', v_groupName, ''); FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, disable the emaj log trigger v_fullTableName := quote_ident(r_tblsq.group_schema) || '.' || quote_ident(r_tblsq.group_table); v_triggerName := quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_emaj_trg'); EXECUTE 'ALTER TABLE ' || v_fullTableName || ' DISABLE TRIGGER ' || v_triggerName; ELSEIF v_relkind = 'S' THEN -- if it is a sequence, nothing to do END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_stop_group: Group % is unknown', v_groupName; END IF; -- delete all marks for the group from the emaj_mark table to avoid any rollback DELETE FROM emaj.emaj_mark WHERE mark_group = v_groupName; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('STOP_GROUP END', v_groupName, v_nb_tbl || ' tables/sequences processed'); RETURN v_nb_tbl; END; $emaj_stop_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_rollback_group(v_groupName TEXT, v_mark TEXT) RETURNS INT AS $emaj_rollback_group$ -- The function rollbacks all tables ans sequences of a group up to a mark in the history -- Input: group name, mark in the history, as it is inserted by emaj.emaj_set_mark_group -- Output: number of processed tables and sequences DECLARE v_nb_tbl INT := 0; v_nb_tbl_in_group INT; v_relkind TEXT; v_timestampMark TIMESTAMPTZ; r_tblsq RECORD; BEGIN -- check that emaj environment is OK PERFORM emaj.emaj_verify_all(); -- check the requested group exists SELECT count(*) INTO v_nb_tbl_in_group FROM emaj.emaj_group WHERE group_name = v_groupName; IF v_nb_tbl_in_group = 0 THEN RAISE EXCEPTION 'emaj_rollback_group: Group % is unknown', v_groupName; RETURN 0; END IF; -- check the requested mark exists SELECT MAX(mark_datetime) INTO v_timestampMark FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_mark; IF v_timestampMark IS NULL THEN RAISE EXCEPTION 'emaj_rollback_group: No mark % for group % ', v_mark, v_groupName; RETURN 0; END IF; -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('ROLLBACK_GROUP BEGIN', v_groupName, 'Rollback to mark ' || v_mark || ' [' || v_timestampMark || ']'); -- OK, begin a sub-transaction, lock all tables. BEGIN PERFORM emaj.emaj_lock_group(v_groupName); -- and rollback each table and sequence FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, rollback for this table PERFORM emaj.emaj_rlbk_table(r_tblsq.group_schema, r_tblsq.group_table, v_timestampMark); ELSEIF v_relkind = 'S' THEN -- if it is a sequence, rollback for this sequence PERFORM emaj.emaj_rlbk_sequence(r_tblsq.group_schema, r_tblsq.group_table, v_timestampMark); END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; -- final check that we have the expected number of processed tables and sequences IF v_nb_tbl = 0 or v_nb_tbl <> v_nb_tbl_in_group THEN RAISE EXCEPTION 'emaj_rlbk_group: Internal error 1'; END IF; -- delete the now rollbacked marks DELETE FROM emaj.emaj_mark WHERE mark_datetime >= v_timestampMark; END; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('ROLLBACK_GROUP END', v_groupName, v_nb_tbl || ' tables/sequences processed'); RETURN v_nb_tbl; END; $emaj_rollback_group$ LANGUAGE plpgsql; CREATE or REPLACE FUNCTION emaj.emaj_reset_group(v_groupName TEXT) RETURNS INT AS $emaj_reset_group$ -- This function empties the log tables for all tables of a group, using a TRUNCATE, and deletes the sequences saves -- Input: group name -- Output: number of processed tables DECLARE v_nb_tbl INT := 0; v_emajSchema TEXT := 'emaj'; v_logTableName TEXT; v_fullSeqName TEXT; v_relkind TEXT; r_tblsq RECORD; BEGIN -- insert begin in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('RESET_GROUP BEGIN', v_groupName, ''); -- check the group is not in a logging state PERFORM oper_datetime FROM emaj.emaj_hist WHERE oper_type = 'START_GROUP' AND oper_datetime > (SELECT MAX(oper_datetime) FROM emaj.emaj_hist WHERE oper_type = 'STOP_GROUP'); IF FOUND THEN RAISE EXCEPTION 'emaj_reset_group: The groupe % is currently logging', v_groupName; END IF; -- delete all marks for the group from the emaj_mark table to avoid any rollback DELETE FROM emaj.emaj_mark WHERE mark_group = v_groupName; -- then, truncate log tables FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, -- truncate the related log table v_logTableName := quote_ident(v_emajSchema) || '.' || quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log'); EXECUTE 'TRUNCATE ' || v_logTableName; -- and delete rows from emaj_sequence related to the associated emaj_id sequence v_fullSeqName := quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log_emaj_id_seq'); DELETE FROM emaj.emaj_sequence WHERE sequ_name = v_fullSeqName; ELSEIF v_relkind = 'S' THEN -- if it is a sequence, delete all related data from emaj_sequence table PERFORM emaj.emaj_delete_seq (r_tblsq.group_schema, r_tblsq.group_table); END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_reset_group: Group % is unknown', v_groupName; END IF; -- insert end in the history INSERT INTO emaj.emaj_hist (oper_type, oper_object, oper_wording) VALUES ('RESET_GROUP END', v_groupName, v_nb_tbl || ' tables/sequences processed'); RETURN v_nb_tbl; END; $emaj_reset_group$ LANGUAGE plpgsql; CREATE TYPE emaj.emaj_stat_type AS ( stat_group TEXT, stat_schema TEXT, stat_table TEXT, stat_role VARCHAR(32), stat_verb VARCHAR(3), stat_rows BIGINT ); CREATE or REPLACE FUNCTION emaj.emaj_log_stat_group(v_groupName TEXT, v_firstMark TEXT, v_lastMark TEXT) RETURNS SETOF emaj.emaj_stat_type AS $emaj_log_stat_group$ -- This function returns statistics on row updates executed between 2 marks as viewed through the log tables -- Input: group name, the 2 marks names defining a range (either one or both marks can be NULL) -- Output: table of updates by user and table DECLARE v_nb_tbl INT := 0; v_emajSchema TEXT := 'emaj'; v_logTableName TEXT; v_fullSeqName TEXT; v_relkind TEXT; v_tsFirstMark TIMESTAMPTZ; v_tsLastMark TIMESTAMPTZ; v_stmt TEXT; r_tblsq RECORD; r_stat RECORD; BEGIN -- catch the timestamp of the first mark IF v_firstMark IS NOT NULL THEN SELECT mark_datetime INTO v_tsFirstMark FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_firstMark; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_log_stat_group: Start mark % is unknown for group %', v_firstMark, v_groupName; END IF; END IF; -- catch the timestamp of the last mark IF v_lastMark IS NOT NULL THEN SELECT mark_datetime INTO v_tsLastMark FROM emaj.emaj_mark WHERE mark_group = v_groupName AND mark_name = v_lastMark; IF NOT FOUND THEN RAISE EXCEPTION 'emaj_log_stat_group: End mark % is unknown for group %', v_lastMark, v_groupName; END IF; END IF; -- for each table of the emaj_group table FOR r_tblsq IN SELECT group_schema, group_table FROM emaj.emaj_group WHERE group_name = v_groupName LOOP -- check the class is valid v_relkind = emaj.emaj_check_class(r_tblsq.group_schema, r_tblsq.group_table); IF v_relkind = 'r' THEN -- if it is a table, count the number of operation per type (INSERT, UPDATE and DELETE) and role v_logTableName := quote_ident(v_emajSchema) || '.' || quote_ident(r_tblsq.group_schema || '_' || r_tblsq.group_table || '_log'); v_stmt= 'SELECT ''' || v_groupName || '''::TEXT as emaj_group, ''' || r_tblsq.group_schema ; v_stmt= v_stmt || '''::TEXT as emaj_schema, '''|| r_tblsq.group_table || '''::TEXT as emaj_table,'; v_stmt= v_stmt || ' emaj_user, emaj_verb, count(*) as emaj_rows FROM ' || v_logTableName ; v_stmt= v_stmt || ' WHERE NOT (emaj_verb = ''UPD'' AND emaj_tuple = ''OLD'')'; IF v_firstMark IS NOT NULL THEN v_stmt= v_stmt || ' AND emaj_changed >= timestamp '''|| v_tsFirstMark || ''''; END IF; IF v_lastMark IS NOT NULL THEN v_stmt= v_stmt || ' AND emaj_changed < timestamp '''|| v_tsLastMark || ''''; END IF; v_stmt= v_stmt || ' GROUP BY emaj_group, emaj_schema, emaj_table, emaj_user, emaj_verb'; v_stmt= v_stmt || ' ORDER BY emaj_user, emaj_verb'; FOR r_stat IN EXECUTE v_stmt LOOP RETURN NEXT r_stat; END LOOP; END IF; v_nb_tbl = v_nb_tbl + 1; END LOOP; IF v_nb_tbl = 0 THEN RAISE EXCEPTION 'emaj_log_stat_group: Group % is unknown', v_groupName; END IF; RETURN; END; $emaj_log_stat_group$ LANGUAGE plpgsql; -- and insert the init record in the operation history INSERT INTO emaj.emaj_hist (oper_type,oper_wording) VALUES ('EMAJ_INIT','E-maj initialisation completed'); COMMIT;