Принцип использования


Включение истории pghist.hist_enable создает в схеме таблицы 3 объекта для работы с историей:

1. Представление [schema].[table]_hist

представляет собой лог изменений (аудит) и предназначено для анализа разработчиками проблем с данными

SQL-запрос и приложение позволяют определить местоположение в исходном коде, cсылка на общую транзацию - изменения в других таблицах и SQL-запросы.

Пример:
-- Create schema and table, enable history, change data 
drop schema if exists example cascade;
create schema example;

create table example.document(
  id int primary key,
  number varchar(10),
  amount numeric(10,2)
);

call pghist.hist_enable('example', 'document');

insert into example.document values (11, '#10', 100);
insert into example.document values (12, '#20', 200);
update example.document set number='#20/2',amount=210 where id=12;
update example.document set amount=220 where id=12;
delete from example.document where id=11;

-- Select full info
select * from example.document_hist;

-- Select partial info by id
select hist_timestamp,hist_operation,hist_db_user,amount_old 
  from example.document_hist
  where id=12 and (hist_operation!='UPDATE' or 'amount'=any(hist_update_columns))
  order by hist_statement_id;

-- Select partial info with current values
select * from example.document_hist
union all 
select null,null,null,'[CURRENT_VALUES]',null,null,null,null,null,null,* from example.document
order by id,1;

-- Create extended view and grant select on it to developer
-- (alternatively to 'grant select on all tables in schema pghist to developer');  
select *
  from pghist.data$example_document h  
  join pghist.hist_statement s on s.id = h.hist_statement_id
  join pghist.hist_query q on q.hash = s.query_hash
  join pghist.hist_transaction t on t.id = s.transaction_id
  order by h.hist_statement_id, h.id;
 
create or replace view example.document_hist_ext as  
  select h.id,t.timestamp_commit,t.db_client_addr,q.text query_text
    from pghist.data$example_document h  
    join pghist.hist_statement s on s.id = h.hist_statement_id
    join pghist.hist_query q on q.hash = s.query_hash
    join pghist.hist_transaction t on t.id = s.transaction_id
    order by h.hist_statement_id, h.id;
   
select * from example.document_hist_ext where id=12;   

grant select on example.document_hist_ext to developer_1;      


2. Функция [schema].[table]_changes

возвращает набор данных для показа изменений пользователю

Информация выводится по столбцам в формате старое-новое значение с описанием.

Отображать пользователю идентификаторы и внешние ключи неинформативно, поэтому для каждой строки и значений полей выводится описание, которое можно переопределить через SQL-выражение. По умолчанию для строки возвращается комментарий таблицы с первичным ключом, для столбца, являющегося внешним ключом, - первое текстовое поле связанной таблицы, для остальных - значение. Таким образом, отображать пользователю можно не значения, а описания.

При включении истории для detail-таблиц необходимо указать master-таблицу, это приведет к дополнительному хранению и индексированию полей внешнего ключа аналогично первичному. По умолчанию при получении изменений в master-таблице будут возвращаются и изменения в detail-таблицах.

Часть столбцов (например, имя пользователя) могу быть переопределены через функции кастомизации.

Пример:
-- Create schema and tables
drop schema if exists example cascade;
create schema example;

create table example.customer(
  id int primary key,
  name varchar(100) not null
);
insert into example.customer values (1,'Horns'),(2,'Hooves');

create table example.invoice(
  id int primary key,  
  number varchar(10),
  date date,  
  customer_id int references example.customer(id),
  amount numeric(20,2) 
);
comment on table example.invoice is 'Invoice';
comment on column example.invoice.id is 'Identifier';
comment on column example.invoice.number is 'Number';
comment on column example.invoice.date is 'Date';
comment on column example.invoice.customer_id is 'Customer';
comment on column example.invoice.amount is 'Amount';

create table example.product(
  id int primary key,
  name varchar(100) not null,
  code varchar(10) not null
);

create table example.invoice_product(
  id serial primary key,
  invoice_id int references example.invoice(id),   
  product_id int references example.product(id),  
  quantity int,
  color char(1) check (color in ('R','G','B'))  
);
comment on table example.invoice_product is 'Product of invoice';
comment on column example.invoice_product.id is 'Identifier';
comment on column example.invoice_product.invoice_id is 'Invoice';
comment on column example.invoice_product.product_id is 'Product';
comment on column example.invoice_product.quantity is 'Quantity';
comment on column example.invoice_product.color is 'Color';
create index on example.invoice_product(invoice_id);

-- Enable history
call pghist.hist_enable('example', 'invoice');
call pghist.hist_enable('example', 'invoice_product', 'example', 'invoice');

-- Change data
insert into example.invoice values (12,'#20', current_date, 1, 120.00);
update example.invoice set customer_id=2 where id=12;
insert into example.product(id,name,code) values (101,'Pencil','030'),(102,'Notebook','040');
insert into example.invoice_product(id, invoice_id, product_id, quantity, color) values (1,12,101,1000,'R'),(2,12,101,10,'G');

do $$
begin
  update example.invoice_product set quantity=quantity+1,color='B' where id=1;
  delete from example.invoice_product where id=2;
  update example.invoice set amount=150 where id=12;
end; 
$$;

-- Select all changes, first three columns provide chronological
select * from example.invoice_changes() order by 1,2,3;

-- Select all changes with column id (immutable), insert detail by columns, without detail table example.invoice_product
select * from example.invoice_changes(hist_columns_insert=>true, hist_columns_immutable=>true, hist_tables_detail=>false) order by 1,2,3; 

-- Select partial changes by id with detail table, autocreated indexes provide fast execution
select * from example.invoice_changes(12) order by 1,2,3;

-- Equivalent with using union all  
select * from example.invoice_changes(id=>12, hist_tables_detail=>false)
union all
select * from example.invoice_product_changes(invoice_id=>12)
order by 1,2,3;

-- Set description expression for columns row_desc and value_desc   
call pghist.hist_expression_row_desc('example', 'invoice', '''Invoice''');   
call pghist.hist_expression_row_desc('example', 'invoice_product', $$ 'Row #'||$1.id||' / '||(select name from example.product where id=$1.product_id) $$);
call pghist.hist_expression_value_desc('example', 'invoice_product', 'color', $$ case when $1='R' then 'Red' when $1='B' then 'Blue' when $1='G' then 'Green' else $1 end $$);

-- Replace function for column db_user_name
create or replace function example.db_user_name(db_user name) returns varchar language plpgsql as $$ 
begin 
  return '['||db_user||']';                                        
end; $$;
-- call pghist.hist_column_custom_function('db_user_name', 'example.db_user_name');
-- call pghist.hist_column_custom_function('db_user_name', 'pghist.hist_default_db_user_name');

Изменения по определенному условию можно получить используя cross join со списком первичных ключей. Для объединения истории по нескольким таблицам результаты функций можно объединять через оператор union all. Для подобных запросов рекомендуется создавать функции-обертки.

Для схем данных со сложной структурой рекомендуется создавать функции [schema].[table]_changes_ui(id), которые будут возвращать необходимый набор данных. Для приложения или проекта можно создать один единый тип данных hist_table_changes_ui и одну экранную форму (web-страницу) для отображения истории по любым таблицам и условиям.

Пример:
-- Create function that returns changes by date
create or replace function example.invoice_changes_ui_date(date_changes date) returns setof pghist.hist_table_change language sql security definer as $$
  select c.*
    from (select distinct id from example.invoice_hist where hist_timestamp::date=date_changes) h
    cross join example.invoice_changes(h.id) c
  order by 1,2,3;
$$;
select * from example.invoice_changes_ui_date(current_date);

-- Create function that returns changes to master and detail tables
create or replace function example.invoice_changes_ui_simple(id int) returns setof pghist.hist_table_change language sql security definer as $$
  select * from example.invoice_changes(id=>id,hist_tables_detail=>false,hist_columns_insert=>true)
  union all
  select * from example.invoice_product_changes(invoice_id=>id)
  order by 1,2,3;
$$;
select * from example.invoice_changes_ui_simple(12);

-- Create type with only necessary columns for the UI and cast function to it,
-- type is created only once (as a rule, there is only one history view form per project) 
create type example.hist_table_change_ui as (
  timestamp timestamptz,
  operation_name varchar,
  db_user_name varchar,
  row_desc text,  
  column_comment varchar,      
  value_old_desc text,      
  value_new_desc text
);
create or replace function example.hist_table_change_ui_cast(c pghist.hist_table_change) returns example.hist_table_change_ui language plpgsql as $$
begin
  return (c.timestamp,c.operation_name,c.db_user_name,c.row_desc,c.column_comment,c.value_old_desc,c.value_new_desc)::example.hist_table_change_ui; 
end; $$;
create cast(pghist.hist_table_change as example.hist_table_change_ui) with function example.hist_table_change_ui_cast as assignment;

-- Create function that returns only necessary columns
create or replace function example.invoice_changes_ui(id int) returns setof example.hist_table_change_ui language sql security definer as $$
  select c::pghist.hist_table_change::example.hist_table_change_ui from ( 
    select * from example.invoice_changes(id=>id,hist_tables_detail=>false,hist_columns_insert=>true)
    union all
    select * from example.invoice_product_changes(invoice_id=>id)
    order by 1,2,3
  ) c;  
$$;
select * from example.invoice_changes_ui(12);


3. Функция [schema].[table]_at_timestamp

создает копию таблицы (временную таблицу) на момент времени в прошлом и возвращает ee

Используется для восстановления данных и получения данных и результата SQL-запроса из нескольких таблиц на момент времени в прошлом, может быть соединена с представлением [schema].[table]_hist по первичному ключу.

Для оптимальной работы SQL-запросов созданная временная таблица имеет индексы аналогичные исходной, время восстановления можно задать один раз в параметре pghist.at_timestamp.

Пример:
--  Create schema and tables, enable history
drop schema if exists example cascade;
create schema example;

create table example.customer(
  id int primary key,
  name varchar(100) not null
);
insert into example.customer values (1,'Horns'),(2,'Hooves');

create table example.document(
  id int primary key,
  number varchar(10),
  date date
);

create table example.invoice(
  primary key (id),
  customer_id int references example.customer(id),
  amount numeric(20,2)
) inherits (example.document);
create index on example.invoice(customer_id);

call pghist.hist_enable('example', 'customer');
call pghist.hist_enable('example', 'document');
call pghist.hist_enable('example', 'invoice');

-- Enter data into tables   
insert into example.document values (11,'#10', current_date);
insert into example.invoice  values (12,'#20', current_date,   1, 120.00);
insert into example.invoice  values (13,'#30', current_date-1, 2, 130.00);

-- Select data in past
select * from example.document_at_timestamp(now()-interval '10 second');

-- Erroneous update and recovery
update example.invoice set customer_id=2,amount=300 where date=current_date;

update example.invoice i
    set amount = h.amount
  from example.invoice_at_timestamp('2024-04-06 10:00:00') h
  where i.id = h.id
    and i.date=current_date;

-- Combination log and versioning   
select * 
  from example.invoice_at_timestamp('2024-04-06 10:00:00')
  where id in (
          select id from example.invoice_hist
            where hist_timestamp>'2024-04-06 10:00:00'
              and hist_db_user=current_user
        );

-- Complex query in past        
select * 
  from example.invoice i
  join example.customer c on c.id=i.customer_id;
 
do $$
begin
  perform set_config('pghist.at_timestamp', '2024-04-06 10:00:00', true);    
  perform example.invoice_at_timestamp();    
  perform example.customer_at_timestamp();
end; 
$$;
 
select * 
  from example_invoice_at_timestamp i
  join example_customer_at_timestamp c on c.id=i.customer_id;

Безопасность


Таблица истории data$[schema]_[table] и триггерные функции создаются в схеме pghist, объекты для работы с историей - в схеме таблицы. Владельцем всех создаваемых объектов является суперпользователь, хранимые процедуры создаются с правами владельца (security definer). Владельцу основной таблицы выдаются права с опцией передачи (with grant option) на чтение представления [schema].[table]_hist и вызов функций [schema].[table]_changes, [schema].[table]_at_timestamp.

При необходимости доступ разработчику к общим таблицам суперпользователь выдает отдельно.

Доступ к данным показан на схеме.

Пример:
-- postgres grant privileges on pghist schema and pghist.hist_enable procedure to developer_1
-- all privileges in pghist_grants.sql file
grant usage on schema pghist to developer_1;
grant execute on procedure pghist.hist_enable(name) to developer_1;


-- developer_1 create table, enable history, grant privileges on example table and example_changes function to user_1
create table example(
  id int primary key,
  name varchar(20),
  number numeric(10,2),
  date date
);

call pghist.hist_enable('example');
  
grant select,insert,update on example to user_1;
grant execute on function example_changes to user_1;


-- user_1 change data and view changes 
insert into example values (1, 'Example', 10, current_date);
update example set number=20, date=date-1;

select * from example_changes() order by 1,2,3;

-- When trying to select log, user_1 receives an error
-- SQL Error [42501]: ERROR: permission denied for view example_hist 
select * from example_hist;

pghist.hist_enable

(schema, table_name, master_table_schema, master_table_name, columns_excluded)

Процедура включает ведение истории изменений для указанной таблицы, создает таблицу истории, триггеры и функции получения данных

Параметры
НаименованиеТипОписаниеОбязательныйПо умолчанию
schemanameсхема таблицыpublic
table_namenameимя таблицы
master_table_schemanameсхема мастер-таблицы
master_table_namenameимя мастер-таблицы
columns_excludedname[]исключенные столбцы

Примеры использования
call pghist.hist_enable('document');

call pghist.hist_enable('example', 'invoice');
call pghist.hist_enable('example', 'invoice_product', 'example', 'invoice');

call pghist.hist_enable('orm', 'myclass', columns_excluded => array['inserted_at','updated_at']);

[schema].[table]_hist



Представление содержит список изменений по строкам, создается при включении ведения истории.
Сортируется по первым двум столбцам, что обеспечивает хронологию по времени

Столбцы
НаименованиеТипОписаниеОбязательныйПо умолчанию
hist_statement_numbigintглобальный номер SQL-выражения
hist_row_numbigintномер строки в наборе данных, который изменяет SQL-выражения
hist_timestamptimestamptzдата-время начала выполнения SQL-выражения
hist_operationvarcharоперация: INSERT,UPDATE,DELETE,TRUNCATE
hist_update_columnsname[]список обновленных столбцов для операции UPDATE
hist_db_usernameпользователь базы данных
hist_app_uservarcharпользователь приложенияcurrent_setting('app.user')
hist_application_namevarcharимя приложения
hist_query_texttextтекст SQL-запроса
hist_statement_idbigintID SQL-выражения, ссылается на таблицу pghist.hist_statement
[primary key column(s)]значения первичного ключа
[column(s) of fkey on master table]значения внешнего ключа на мастер-таблицу
[column...]_oldстарые значения для каждого столбца таблицы [schema].[table]

Примеры использования
select * from example.document_hist;
select * from example.document_hist where id=2;
select * from example.document_hist where hist_operation='DELETE';

[schema].[table]_changes



Функция возвращает изменения в таблице (setof pghist.table_change), создается при включении ведения истории.
Сортировка по первым трем столбцам обеспечивает хронологию по времени, для получения связанных данных из нескольких таблиц рекомендуется использовать оператор union all

Параметры
НаименованиеТипОписаниеОбязательныйПо умолчанию
[primary key column(s)]значения первичного ключа
[column(s) of fkey on master table]значения внешнего ключа на мастер-таблицу
hist_columns_immutablebooleanвозвращать значения неизменяемых столбцовfalse
hist_columns_insertbooleanдетализировать операцию INSERT по столбцамfalse
hist_tables_detailbooleanвозвращать изменения в detail-таблицахtrue
hist_tables_inheritedbooleanучитывать унаследованные таблицыtrue

Столбцы возвращаемого набора данных (тип pghist.hist_table_change)
НаименованиеТипОписаниеОбязательныйПо умолчанию
statement_numbigintглобальный номер SQL-выражения
row_numintномер строки в наборе данных, который изменяет SQL-выражения
column_numintномер столбца в строке
timestamptimestamptzдата-время операции
operationvarcharоперация: INSERT,UPDATE,DELETE,TRUNCATE
operation_namevarcharнаименование операции,
задается переопределяемой функцией pghist.hist_custom_operation_name
column_namenameимя столбца
column_commentvarcharкомментарий столбца
value_oldtextстарое значение
value_old_desctextописание старого значения,
можно переопределить через процедуру pghist.hist_expression_value_desc
value_newtextновое значение
value_new_desctextописание нового значение,
можно переопределить через процедуру pghist.hist_expression_value_desc
row_desctextописание строки,
можно переопределить через процедуру pghist.hist_expression_row_desc
db_usernameпользователь базы данных
db_user_namevarcharимя пользователя базы данных,
задается переопределяемой функцией pghist.hist_custom_db_user_name
app_uservarcharпользователь приложения,
задается переопределяемой функцией pghist.hist_custom_app_user
app_user_namevarcharимя пользователя приложения,
задается переопределяемой функцией pghist.hist_custom_app_user_name
schemanameсхема
table_nametextимя таблицы
table_commenttextкомментарий таблицы

Примеры использования
select * from example.invoice_changes() order by 1,2,3;
select * from example.invoice_changes(hist_columns_immutable=>true) order by 1,2,3; 

select * from example.invoice_changes(12) order by 1,2,3;

select * from example.invoice_changes(id=>12, hist_columns_insert=>true, hist_tables_detail=>false)
union all
select * from example.invoice_product_changes(invoice_id=>12)
order by 1,2,3;

[schema].[table]_at_timestamp

(transaction_timestamp, cascade)

Функция создает временную таблицу [schema]_[table]_at_timestamp как копию исходной таблицы по состоянию на момент времени и возвращает ее

Параметры
НаименованиеТипОписаниеОбязательныйПо умолчанию
schemaсхема таблицы
table_nameимя таблицы
transaction_timestamptimestamptzдата-время завершения транзакцииcurrent_setting('pghist.at_timestamp')
cascadebooleanучитывать унаследованные таблицыtrue

Примеры использования
select * from example.document_at_timestamp(now()-interval '1 hour');

do $$
begin
  perform set_config('pghist.at_timestamp', '2024-04-06 10:00:00', true);    
  perform example.invoice_at_timestamp();    
  perform example.customer_at_timestamp();
end; 
$$;
select * 
  from example_invoice_at_timestamp i
  join example_customer_at_timestamp c on c.id=i.customer_id;

pghist.hist_disable

(schema, table_name)

Процедура выключает ведение истории изменений для указанной таблицы, удаляет необходимые объекты

Параметры
НаименованиеТипОписаниеОбязательныйПо умолчанию
schemanameсхема таблицыpublic
table_namenameимя таблицы

Примеры использования
call pghist.hist_disable('document');
call pghist.hist_disable('example', 'invoice');

SQL-выражения описаний

(schema, table, column_name, expression)

pghist.hist_expression_row_desc

(schema, table_name, expression)

pghist.hist_expression_value_desc

(schema, table_name, column_name, expression)

Процедуры устанавливают выражение для описания строки/значения поля, в выражении доступна переменная $1 с строкой/значением поля


pghist.hist_expression_row_desc_current

(schema, table_name)

pghist.hist_expression_value_desc_current

(schema, table_name, column_name)

Функции возвращают текущее выражение для описания строки/значения поля


pghist.hist_expression_row_desc_default

(schema, table_name)

pghist.hist_expression_value_desc_default

(schema, table_name, column_name)

Функции возвращают выражение по умолчанию для описания строки/значения поля. Для строки возвращается комментарий таблицы с первичным ключом, для столбца, являющегося внешним ключом, - первое текстовое поле связанной таблицы


Параметры
НаименованиеТипОписаниеОбязательныйПо умолчанию
schemanameсхема таблицы
tablenameимя таблицы
column_namenameимя столбцапри наличии
expressionvarcharSQL-выражение,
null отменяет сохраненное описание
при наличии

Примеры использования
call pghist.hist_expression_row_desc('example', 'invoice', '''Invoice''');   
call pghist.hist_expression_row_desc('example', 'invoice_product', $$ 'Row #'||$1.id||' / '||(select name from example.product where id=$1.product_id) $$);
call pghist.hist_expression_value_desc('example', 'invoice_product', 'color', $$ case when $1='R' then 'Red' when $1='B' then 'Blue' when $1='G' then 'Green' else $1 end $$);

select pghist.hist_expression_row_desc_current('example', 'invoice_product');
select pghist.hist_expression_value_desc_default('example', 'invoice_product', 'color');

Функции кастомизации



Для кастомизации и локализации полей конкретного приложения можно определить собственные функции.
Рекомендуется в SQL-менеджере (например, pgAdmin или DBeaver) взять за основу функцию по умолчанию, переопределить ее и сохранить в схеме приложения под другим именем

Столбцы, доступные для переопределения
НаименованиеТипОписаниеОбязательныйПо умолчанию
operation_namevarcharнаименование операции
db_user_namevarcharимя пользователя базы данных
app_uservarcharпользователь приложения
app_user_namevarcharимя пользователя приложения
app_client_addrinetIP-адрес клиентского приложения
app_client_hostnamevarcharимя хоста(компьютера) клиентского приложения


pghist.hist_column_custom_function

(column_name, custom_function)

Процедура устанавливает функцию для получения значения поля

Параметры
НаименованиеТипОписаниеОбязательныйПо умолчанию
column_namenameимя столбца
custom_functionnameимя функции без параметров


pghist.hist_column_custom_function

(column_name)

Функция возвращает текущую функцию для получения значения поля

Параметр
НаименованиеТипОписаниеОбязательныйПо умолчанию
column_namenameимя столбца


Пример
select pghist.hist_column_custom_function_current('db_user_name');

create or replace function myapp.user_name(db_user name) returns varchar language plpgsql as $$ 
begin 
  return (select name from myapp.users where login=db_user);                                        
end; $$;

call pghist.hist_column_custom_function('db_user_name', 'myapp.user_name');

Таблица истории pghist.data$[schema]_[table]



При включении истории дополнительно к основной таблице создается таблица истории, разработчику (владельцу основной таблицы) доступна через представление [schema].[table]_hist

Столбцы
НаименованиеТипОписаниеОбязательныйПо умолчанию
hist_statement_idbigintID SQL-выражения, ссылается на таблицу pghist.hist_statement
hist_row_numintномер строки в наборе данных, который изменяет SQL-выражения
hist_update_columnsname[]список обновленных столбцов для операции UPDATE
[primary key column(s)]значения первичного ключа
[column(s) of fkey on master table]значения внешнего ключа на мастер-таблицу
[column...]_oldстарые значения для каждого столбца таблицы [schema].[table]

Пример использования
select *
  from pghist.data$example_invoice h
  join pghist.hist_statement s on s.id=h.hist_statement_id
  join pghist.hist_query q on q.hash=s.query_hash
  where q.text like 'do%begin%';

Общие таблицы


pghist.hist_transactionтранзакции
pghist.hist_querySQL-запросы
pghist.hist_statementSQL-выражения
pghist.hist_tableтаблицы с историей
pghist.hist_table_columnстолбцы таблиц с историей
pghist.hist_column_custom_functionкастомизированные функции столбцов

При установке создаются таблицы, которые хранят общую информацию по изменениям (дата-время транзакции и SQL-выражения, операция, SQL-запрос, пользователь, сессия и т.д.), и таблицы с настройками. Изменение общих таблиц осуществляется через хранимые процедуры, допускается только чтение. По умолчанию разработчикам (владельцам основных таблиц) общие таблицы недоступны, при необходимости доступ выдается вручную.

Таблица истории pghist.data$[schema]_[table] по полю statement_id ссылается на pghist.hist_statement, которая в свою очередь ссылается на pghist.hist_transaction и pghist.hist_query.

Примеры использования:
select *
  from pghist.hist_statement s
  join pghist.hist_transaction t on t.id=s.transaction_id
  join pghist.hist_query q on q.hash=s.query_hash
  where t.timestamp_commit between '2024-04-06 10:00:00' and '2024-04-06 11:00:00';

select *
  from pghist.hist_table ht
  left join pghist.hist_table_column htc on htc.schema=ht.schema and htc.table_name=ht.name;

pghist.pghist_version



Функция возвращает версию инструмента

pghist.hist_sql_log



Все команды, выполняемые утилитой, записываются в лог-таблицу pghist.hist_sql_log

Примеры использования:
select * from pghist.hist_sql_log where schema='example' and table_name='document' order by id;
select * from pghist.hist_sql_log l
  join pghist.hist_transaction t on t.id=l.transaction_id
  where schema='example' and table_name='document'
  order by 1 desc;