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


Включение истории 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-выражение. По умолчанию для строки возращается комментарий таблицы с первичным ключом, для столбца, являющегося внешним ключом, - первое текстовое поле связанной таблицы, дла остальных - значение. Таким образом, отображать пользователю можно не значения, а описания.

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

Пример:
-- 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  
select * from example.invoice_changes(insert_detail=>true, columns_immutable=>true) order by 1,2,3; 

-- Select partial changes by id
select * from example.invoice_changes('id=12') order by 1,2,3;

-- Select changes in two related tables, fast execution provides index invoice_product(invoice_id)
select * from example.invoice_changes('id=$1',12)
union all
select * from example.invoice_product_changes('invoice_id=$1', 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');

Для объединения истории по нескольким таблицам результаты функций можно объединять через оператор union all. Наиболее актуально это для detail-таблиц, поэтому при включении для них истории необходимо указать master-таблицу, что приведет к дополнительному хранению и индексированию полей внешнего ключа аналогично первичному.

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

Пример:
-- Create function that returns all columns, including unnecessary
create or replace function example.invoice_changes_ui_simple(id int) returns setof pghist.table_change language sql security definer as $$
  select * from example.invoice_changes('id=$1',id,insert_detail=>true)
  union all
  select * from example.invoice_product_changes('invoice_id=$1', 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.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.table_change_ui_cast(tc pghist.table_change) returns example.table_change_ui language plpgsql as $$
begin
  return (tc.timestamp,tc.operation_name,tc.db_user_name,tc.row_desc,tc.column_comment,tc.value_old_desc,tc.value_new_desc)::example.table_change_ui; 
end; $$;
create cast(pghist.table_change as example.table_change_ui) with function example.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.table_change_ui language sql security definer as $$
  select tc::pghist.table_change::example.table_change_ui from ( 
    select * from example.invoice_changes('id=$1',id,insert_detail=>true)
    union all
    select * from example.invoice_product_changes('invoice_id=$1', id)
    order by 1,2,3
  ) tc;  
$$;
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(where_clause, where_param, columns_immutable, insert_detail, cascade)


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

Параметры:
where_clausetext-условие на таблицу (необязательный)
where_paramanyelement-значение параметра в where_clause (необязательный)
columns_immutableboolean-возвращать значения неизменямых столбцов (необязательный, по умолчанию false)
insert_detailboolean-детализировать операцию INSERT по столбцам (необязательный, по умолчанию false)
cascadeboolean-учитывать унаследованные таблицы (необязательный, по умолчанию true)

Столбцы возвращаемого набора данных:
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(insert_detail=>true, columns_immutable=>true) order by 1,2,3; 

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

select * from example.invoice_changes('id=$1',12)
union all
select * from example.invoice_product_changes('invoice_id=$1', 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;