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


Включение истории 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.hist_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.hist_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 возвращает набор данных для показа изменений пользователю

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

Отображать пользователю идентификаторы и внешние ключи неинформативно, поэтому для каждой строки и значений полей выводится описание, которое можно переопределить через 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 'Сustomer';
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,'Pensil','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');

Изменения по опредленному условию можно получить используя 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;

-- Сombination 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
        );

-- Сomplex 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;

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


Таблица истории hist_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-схема таблицы (необязательный)
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_idbigint-ID 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(hist_columns_immutable, hist_columns_insert, hist_tables_detail, hist_tables_inherited)


Функция возвращает изменения в таблице (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-схема таблицы (необязательный)
table_namename-имя таблицы

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

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


pghist.hist_expression_row_desc(schema, table_name, expression)
pghist.hist_expression_value_desc(schema, table_name, column_name, expression)

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


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-имя столбца
expressionvarchar-выражение, 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_addrinet-IP адрес клиентского приложения
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.hist_data$[schema]_[table]


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

Столбцы:
hist_statement_idbigint-ID 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.hist_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_query-SQL-запросы
pghist.hist_statement-SQL-выражения
pghist.hist_table-таблицы с историей
pghist.hist_table_column-столбцы таблиц с историей
pghist.hist_column_custom_function-кастомизированные функции столбцов

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

Таблица истории pghist.hist_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;