Re: [GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-08-24 Thread ivan_14_32

01.05.2011 12:58, Basil Bourque wrote:

Hoorah! I was able to complete my single PL/pgSQL function to create history records 
tracking individual field value changes generically for all my tables. Some developers 
call this an audit trail, though an accountant might say otherwise.

I made auditing based on triggers like aforementioned. And now I need 
fill audit table with already presented data. But there is a problem.


within trigger
EXECUTE 'SELECT ($1).name::text' INTO newVal USING NEW;
works fine

but function (table decor has field name)
CREATE OR REPLACE FUNCTION odb_InitLog()
  RETURNS void AS
DECLARE
  obj record;
BEGIN
  FOR obj IN (SELECT * FROM decor) LOOP
EXECUTE 'SELECT ($1).name::text' INTO newVal USING obj;
  END LOOP;
END;
doesn't work - ERROR: could not identify column name in record data type

Why?






--
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general


Re: [GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-08-24 Thread Merlin Moncure
On Wed, Aug 24, 2011 at 1:03 PM, ivan_14_32 ivan_14...@mail.ru wrote:
 01.05.2011 12:58, Basil Bourque wrote:

 Hoorah! I was able to complete my single PL/pgSQL function to create
 history records tracking individual field value changes generically for all
 my tables. Some developers call this an audit trail, though an accountant
 might say otherwise.

 I made auditing based on triggers like aforementioned. And now I need fill
 audit table with already presented data. But there is a problem.

 within trigger
 EXECUTE 'SELECT ($1).name::text' INTO newVal USING NEW;
 works fine

 but function (table decor has field name)
 CREATE OR REPLACE FUNCTION odb_InitLog()
  RETURNS void AS
 DECLARE
  obj record;
 BEGIN
  FOR obj IN (SELECT * FROM decor) LOOP
    EXECUTE 'SELECT ($1).name::text' INTO newVal USING obj;
  END LOOP;
 END;
 doesn't work - ERROR: could not identify column name in record data type

folks, this (dynamic field access of generic record in plpgsql) is the
number one FAQ on this list.  please check the archives before
searching (not picking on you specifically, it just gets asked in some
variant an awful lot).  First point:  hstore  execute.  if you _must_
use execute, you have to cast at some point.  when you pass a record
to something, it doesn't have the necessary context to know the field
names.   In your case, though, an explicit composite type is the way
to go:

DECLARE
 obj decor;
BEGIN
 FOR obj IN SELECT * FROM decor LOOP
   newVal := obj.name;
 END LOOP;
END;

merlin

-- 
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general


Re: [GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-05-01 Thread Sim Zacks

On 04/28/2011 10:46 PM, Basil Bourque wrote:


In PL/pgSQL, how does one generically access the fields of the OLD or NEW 
record?

I've tried code such as this:
   'NEW.' || quote_ident( myColumnNameVar ) || '::varchar'

But when run by an EXECUTE command, I get errors such as:
   ERROR:  missing FROM-clause entry for table old
   SQL state: 42P01

It seems that I cannot get PL/pgSQL to interpret the text of NEW. + column 
name as text.

My goal is to loop each field in a trigger, comparing the OLD.  NEW. 
values of each field. If different I want to log both values in a history/audit-trail table.

Is there some way to loop the fields of a trigger's Record? I've read other 
people's frustration at not being able to get an array of fields from the 
Record.

My approach is to fake it: Get the table's columns and data types by querying the 
meta-data tables (pg_attribute, pg_class, pg_type). But I cannot get NEW. || 
colNameVar to be interpreted. Perhaps there is a better approach.

If anyone is curious, my source code is pasted below.

--Basil Bourque

We use plpythonu for this as the new and old structures are dictionaries.

Sim

--
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general


Re: [GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-05-01 Thread Basil Bourque
Hoorah! I was able to complete my single PL/pgSQL function to create history 
records tracking individual field value changes generically for all my tables. 
Some developers call this an audit trail, though an accountant might say 
otherwise.

Thanks for all the replies. 

Special thanks to John DeSoi for pointing me to a crucial code example to make 
PL/pgSQL interpret:
OLD. || myColumnNameVar
as: 
OLD.first_name(for example)

The crucial line is:
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO t USING NEW;
found here:
http://wiki.postgresql.org/wiki/PL/pgSQL_Dynamic_Triggers
I'm new to SQL and Postgres, so I may be wrong but my interpretation of this is:
Since there appears no way to make the PL/pgSQL interpreter interpret our 
desired string, the trick is to look outside of PL/pgSQL and instead use the 
SQL interpreter. Brilliant! It works, and it seems to be fast enough, at least 
for my needs. 

I'll share my current code  table structure below. Caveat: This code has not 
yet been thoroughly tested, nor has it been deployed. I only finalized it 
today. 

--Basil Bourque

[A] I'm working on a greenfield project, where:
• I've built every table to have a primary key of type uuid named exactly 
pkey_.
• Every table has a TIMESTAMPTZ field named record_modified_.
My approach below hard-codes these assumptions. 

[B] I have this history_ table:

CREATE TABLE history_
(
  pkey_ uuid NOT NULL DEFAULT uuid_generate_v1mc(), -- The primary key for this 
table, though no primary key constraint was created (for the sake of 
performance and conservation). This column and timestamp_ column are the only 
two columns about this table itself. All other columns are about the 
inserted/modified/deleted record in some other table.
  table_name_ character varying(120) NOT NULL, -- Name of table whose row is 
being affected (inserted, deleted, or modified).
  column_name_ character varying(120) NOT NULL, -- Name of the column in some 
other table whose row value is being modified. This column's value is empty 
string if the operation was DELETE.
  timestamp_ timestamp with time zone NOT NULL DEFAULT clock_timestamp(), -- 
The moment this record was created. Using the clock_timestamp() function as a 
default, to capture the actual moment in time rather than moment when 
transaction began.
  db_user_name_ character varying(120) NOT NULL DEFAULT current_user(), -- 
The name of the Postgres user logged in to this database connection/session.
  app_name_ character varying(120) NOT NULL DEFAULT 
current_setting('application_name'::text), -- The name of the application 
connected to the database. May also include the version number of app, and the 
name of the human user authenticated within the app.
  old_value_ character varying(120) NOT NULL DEFAULT ''::character varying,
  new_value_ character varying(120) NOT NULL DEFAULT ''::character varying,
  uuid_ uuid NOT NULL, -- The UUID of the row being affected, the row being 
inserted, updated, or deleted. Assumes every table whose history is being 
recorded uses the 'uuid' data type as its primary key.
  operation_ character varying(120) NOT NULL, -- What database operation 
resulted in this trigger running: INSERT, UPDATE, DELETE, or TRUNCATE.
  table_oid_ oid NOT NULL, -- The oid of the table whose record is being 
modified. May be helpful if a table name changes over time.
  ordinal_position_of_column_ integer NOT NULL, -- The position of the affected 
column in its table. Every new column gets a number, incremented by one for 
each. This may be helpful when analyzing changes across a stretch of time 
during which a column's name was changed. Apparently columns have no oid, so we 
are recording this number instead.
  transaction_began_ timestamp with time zone NOT NULL DEFAULT 
transaction_timestamp() -- The time when the current transaction began. Can act 
like a transaction identifier, to group multiple history_ rows of the same 
transaction together. This is not foolproof, as multiple transaction could 
possibly start in the same split second moment. Assuming the computer's clock 
has a fine resolution, this chance of a coincidence should be quite miniscule.
)

I do not have a primary constraint for this table. The pkey_ column acts as a 
primary key, but there is no need for an index or uniqueness testing for this 
special table.

[C] For every table I want to track field-level value changes, I create a 
trigger like this:
CREATE TRIGGER XXX_trigger_history_
AFTER INSERT OR UPDATE OR DELETE ON XXX_
FOR EACH ROW EXECUTE PROCEDURE make_history_();
where 'XXX' is the name of the table.

[D] I created this function:

CREATE OR REPLACE FUNCTION make_history_()
RETURNS TRIGGER
LANGUAGE plpgsql
AS 
$BODY$ 

/*  Purpose: Make a history of changes to most fields in the table calling 
this trigger function.
This kind of history tracking is also known as an audit trail.
This function works by detecting each change in 

[GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-04-28 Thread Basil Bourque
In PL/pgSQL, how does one generically access the fields of the OLD or NEW 
record? 

I've tried code such as this:
  'NEW.' || quote_ident( myColumnNameVar ) || '::varchar'

But when run by an EXECUTE command, I get errors such as:
  ERROR:  missing FROM-clause entry for table old
  SQL state: 42P01

It seems that I cannot get PL/pgSQL to interpret the text of NEW. + column 
name as text.

My goal is to loop each field in a trigger, comparing the OLD.  NEW. 
values of each field. If different I want to log both values in a 
history/audit-trail table.

Is there some way to loop the fields of a trigger's Record? I've read other 
people's frustration at not being able to get an array of fields from the 
Record.

My approach is to fake it: Get the table's columns and data types by querying 
the meta-data tables (pg_attribute, pg_class, pg_type). But I cannot get NEW. 
|| colNameVar to be interpreted. Perhaps there is a better approach.

If anyone is curious, my source code is pasted below.

--Basil Bourque


CREATE OR REPLACE FUNCTION table_make_history_() RETURNS trigger AS 
$BODY$ 

DECLARE
metadata_record RECORD; 
/* 
http://www.postgresql.org/docs/current/static/plpgsql-declarations.html#PLPGSQL-DECLARATION-RECORDS
 */
table_dot_column VARCHAR;
my_sql VARCHAR;
column_is_loggable_var BOOLEAN;
edited_var BOOLEAN;

BEGIN

IF (TG_OP = 'INSERT') THEN
-- Ignore this case 

ELSIF (TG_OP = 'UPDATE') THEN

/* Get a list of column name, data type, and position with in table.
   attname = Name of column. 
   atttypid = Data type of column (as an oid from pg_type.oid)
*/
FOR metadata_record IN  
 SELECT attname::varchar AS nth_col_name, pg_type.typname::varchar as 
nth_col_type, pg_attribute.attnum
 FROM pg_attribute, pg_class, pg_type 
 WHERE attrelid = pg_class.oid 
 AND pg_attribute.attisdropped = False 
 AND relname = TG_TABLE_NAME 
 AND attnum  0 
 AND atttypid = pg_type.oid
LOOP -- For each table in the table calling this trigger.
-- Now metadata_record has one record from resultset of SELECT query 
above.
--table_dot_column := TG_TABLE_NAME::VARCHAR || '.' || 
metadata_record.nth_col_name ;
column_is_loggable_var := position( '_x_' in 
metadata_record.nth_col_name )  1 ;
IF column_is_loggable_var THEN 
-- The name of column in question does NOT contain _x_. So, 
proceed to possibly log modified data. 
-- See if the NEW-OLD values are different.
edited_var := true; -- Next line fails.
--EXECUTE 'edited_var := ( OLD.' || 
quote_ident(metadata_record.nth_col_name) || ' = NEW.' || 
quote_ident(metadata_record.nth_col_name) || ' );' ;
PERFORM 'edited_var := ( OLD.' || 
quote_ident(metadata_record.nth_col_name) || '  NEW.' || 
quote_ident(metadata_record.nth_col_name) || ' );' ;
IF edited_var THEN 
EXECUTE 'INSERT INTO history_ ( operation_, 
table_oid_, table_name_, uuid_, column_name_, old_value_, new_value_ ) '
|| 'VALUES ( ' || quote_literal(TG_OP) 
|| ', ' || TG_RELID || ', ' || quote_literal(TG_TABLE_NAME) || ', ' || 
quote_literal(OLD.pkey_) || '::uuid, ' 
|| 
quote_literal(metadata_record.nth_col_name)
|| ', OLD.' 
|| 
quote_ident(metadata_record.nth_col_name) || '::varchar'
|| ', NEW.' 
|| 
quote_ident(metadata_record.nth_col_name) || '::varchar'
|| ' ); ' ;
END IF;

END IF;
END LOOP;

RETURN NEW;

ELSIF (TG_OP = 'DELETE') THEN
-- ignore this case

END IF;

RETURN NULL; /* Should never reach this point. Perhaps we should raise an error 
here. */

END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
--

-- 
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general


Re: [GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-04-28 Thread hubert depesz lubaczewski
On Thu, Apr 28, 2011 at 12:46:50PM -0700, Basil Bourque wrote:
 In PL/pgSQL, how does one generically access the fields of the OLD or NEW 
 record? 
 
 I've tried code such as this:
   'NEW.' || quote_ident( myColumnNameVar ) || '::varchar'
 
 But when run by an EXECUTE command, I get errors such as:
   ERROR:  missing FROM-clause entry for table old
   SQL state: 42P01
 
 It seems that I cannot get PL/pgSQL to interpret the text of NEW. + column 
 name as text.
 
 My goal is to loop each field in a trigger, comparing the OLD.  NEW. 
 values of each field. If different I want to log both values in a 
 history/audit-trail table.
 
 Is there some way to loop the fields of a trigger's Record? I've read other 
 people's frustration at not being able to get an array of fields from the 
 Record.
 
 My approach is to fake it: Get the table's columns and data types by querying 
 the meta-data tables (pg_attribute, pg_class, pg_type). But I cannot get 
 NEW. || colNameVar to be interpreted. Perhaps there is a better approach.

long story short - it's not possible.

a bit longer story:
there are couple of workarounds.

1. you can use hstore datatype and it's casts from record to hstore
2. you can use another pl/* language - like pl/perl - which doesn't have
this problem
3. you can use ready made tool for auditing that does what you want, so
you don't have to worry ( http://pgfoundry.org/projects/tablelog/ )
4. 
http://www.depesz.com/index.php/2010/03/10/dynamic-updates-of-fields-in-new-in-plpgsql/
but really, read, and understand the warnings.

depesz

-- 
The best thing about modern society is how easy it is to avoid contact with it.
 http://depesz.com/

-- 
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general


Re: [GENERAL] OLD. || myColumnNameVar (How to generically access columns in a trigger's OLD or NEW records)

2011-04-28 Thread John DeSoi

On Apr 28, 2011, at 3:46 PM, Basil Bourque wrote:

 It seems that I cannot get PL/pgSQL to interpret the text of NEW. + column 
 name as text.
 
 My goal is to loop each field in a trigger, comparing the OLD.  NEW. 
 values of each field. If different I want to log both values in a 
 history/audit-trail table.
 
 Is there some way to loop the fields of a trigger's Record? I've read other 
 people's frustration at not being able to get an array of fields from the 
 Record.

This might help you:

http://wiki.postgresql.org/wiki/PL/pgSQL_Dynamic_Triggers




John DeSoi, Ph.D.





-- 
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general