I've looked a few methods of creating alphanumeric IDs on Stackoverflow, but they all had their weaknesses, some did not check for collision and others used sequences which are not a good option when using logical replication.
After some Googling I found this website that has the following script which checks for collisions and does not use sequences. However this is done as a trigger when a row is inserted into the table.
-- Create a trigger function that takes no arguments.
-- Trigger functions automatically have OLD, NEW records
-- and TG_TABLE_NAME as well as others.
CREATE OR REPLACE FUNCTION unique_short_id()
RETURNS TRIGGER AS $$
-- Declare the variables we'll be using.
DECLARE
key TEXT;
qry TEXT;
found TEXT;
BEGIN
-- generate the first part of a query as a string with safely
-- escaped table name, using || to concat the parts
qry := 'SELECT id FROM ' || quote_ident(TG_TABLE_NAME) || ' WHERE id=';
-- This loop will probably only run once per call until we've generated
-- millions of ids.
LOOP
-- Generate our string bytes and re-encode as a base64 string.
key := encode(gen_random_bytes(6), 'base64');
-- Base64 encoding contains 2 URL unsafe characters by default.
-- The URL-safe version has these replacements.
key := replace(key, '/', '_'); -- url safe replacement
key := replace(key, '+', '-'); -- url safe replacement
-- Concat the generated key (safely quoted) with the generated query
-- and run it.
-- SELECT id FROM "test" WHERE id='blahblah' INTO found
-- Now "found" will be the duplicated id or NULL.
EXECUTE qry || quote_literal(key) INTO found;
-- Check to see if found is NULL.
-- If we checked to see if found = NULL it would always be FALSE
-- because (NULL = NULL) is always FALSE.
IF found IS NULL THEN
-- If we didn't find a collision then leave the LOOP.
EXIT;
END IF;
-- We haven't EXITed yet, so return to the top of the LOOP
-- and try again.
END LOOP;
-- NEW and OLD are available in TRIGGER PROCEDURES.
-- NEW is the mutated row that will actually be INSERTed.
-- We're replacing id, regardless of what it was before
-- with our key variable.
NEW.id = key;
-- The RECORD returned here is what will actually be INSERTed,
-- or what the next trigger will get if there is one.
RETURN NEW;
END;
$$ language 'plpgsql';
I have have a table which already contains data, I have added a new column called pid would it be possible to modify this and use the function call as default so all my prior data gets a short id?
Suppose you have a table test:
DROP TABLE IF EXISTS test;
CREATE TABLE test (foo text, bar int);
INSERT INTO test (foo, bar) VALUES ('A', 1), ('B', 2);
You could add an id column to it:
ALTER TABLE test ADD COLUMN id text;
and attach the trigger:
DROP TRIGGER IF EXISTS unique_short_id_on_test ON test;
CREATE TRIGGER unique_short_id_on_test
BEFORE INSERT ON test
FOR EACH ROW EXECUTE PROCEDURE unique_short_id();
Now make a temporary table, temp, with the same structure as test (but with no data):
DROP TABLE IF EXISTS temp;
CREATE TABLE temp (LIKE test INCLUDING ALL);
CREATE TRIGGER unique_short_id_on_temp
BEFORE INSERT ON temp
FOR EACH ROW EXECUTE PROCEDURE unique_short_id();
Pouring test into temp:
INSERT INTO temp (foo, bar)
SELECT foo, bar
FROM test
RETURNING *
yields something like:
| foo | bar | id |
|------------+-----+----------|
| A | 1 | 9yt9XQwm |
| B | 2 | LCeiA-P8 |
If other tables have foreign key references on the test table or if test must remain online,
it may not be possible to drop test and rename temp to test.
Instead, it is safer to update test with the ids from temp.
Assuming test has a primary key (for concreteness, let's call it, testid), then
you could update test with the ids from temp using:
UPDATE test
SET id = temp.id
FROM temp
WHERE test.testid = temp.testid;
Then you could drop the temp table:
DROP TABLE temp;
Related
I would like to record the id of a user in the session/transaction, using SET, so I could be able to access it later in a trigger function, using current_setting. Basically, I'm trying option n2 from a very similar ticket posted previously, with the difference that I'm using PG 10.1 .
I've been trying 3 approaches to setting the variable:
SET local myvars.user_id = 4, thereby setting it locally in the transaction;
SET myvars.user_id = 4, thereby setting it in the session;
SELECT set_config('myvars.user_id', '4', false), which depending of the last argument, will be a shortcut for the previous 2 options.
None of them is usable in the trigger, which receives NULL when getting the variable through current_setting. Here is a script I've devised to troubleshoot it (can be easily used with the postgres docker image):
database=$POSTGRES_DB
user=$POSTGRES_USER
[ -z "$user" ] && user="postgres"
psql -v ON_ERROR_STOP=1 --username "$user" $database <<-EOSQL
DROP TRIGGER IF EXISTS add_transition1 ON houses;
CREATE TABLE IF NOT EXISTS houses (
id SERIAL NOT NULL,
name VARCHAR(80),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
PRIMARY KEY(id)
);
CREATE TABLE IF NOT EXISTS transitions1 (
id SERIAL NOT NULL,
house_id INTEGER,
user_id INTEGER,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
PRIMARY KEY(id),
FOREIGN KEY(house_id) REFERENCES houses (id) ON DELETE CASCADE
);
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS \$\$
DECLARE
user_id integer;
BEGIN
user_id := current_setting('myvars.user_id')::integer || NULL;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
\$\$ LANGUAGE plpgsql;
CREATE TRIGGER add_transition1 AFTER INSERT OR UPDATE ON houses FOR EACH ROW EXECUTE PROCEDURE add_transition1();
BEGIN;
%1% SELECT current_setting('myvars.user_id');
%2% SELECT set_config('myvars.user_id', '55', false);
%3% SELECT current_setting('myvars.user_id');
INSERT INTO houses (name) VALUES ('HOUSE PARTY') RETURNING houses.id;
SELECT * from houses;
SELECT * from transitions1;
COMMIT;
DROP TRIGGER IF EXISTS add_transition1 ON houses;
DROP FUNCTION IF EXISTS add_transition1;
DROP TABLE transitions1;
DROP TABLE houses;
EOSQL
The conclusion I came to was that the function is triggered in a different transaction and a different (?) session. Is this something that one can configure, so that all happens within the same context?
Handle all possible cases for the customized option properly:
option not set yet
All references to it raise an exception, including current_setting() unless called with the second parameter missing_ok. The manual:
If there is no setting named setting_name, current_setting throws an error unless missing_ok is supplied and is true.
option set to a valid integer literal
option set to an invalid integer literal
option reset (which burns down to a special case of 3.)
For instance, if you set a customized option with SET LOCAL or set_config('myvars.user_id3', '55', true), the option value is reset at the end of the transaction. It still exists, can be referenced, but it returns an empty string now ('') - which cannot be cast to integer.
Obvious mistakes in your demo aside, you need to prepare for all 4 cases. So:
CREATE OR REPLACE FUNCTION add_transition1()
RETURNS trigger AS
$func$
DECLARE
_user_id text := current_setting('myvars.user_id', true); -- see 1.
BEGIN
IF _user_id ~ '^\d+$' THEN -- one or more digits?
INSERT INTO transitions1 (user_id, house_id)
VALUES (_user_id::int, NEW.id); -- valid int, cast is safe
ELSE
INSERT INTO transitions1 (user_id, house_id)
VALUES (NULL, NEW.id); -- use NULL instead
RAISE WARNING 'Invalid user_id % for house_id % was reset to NULL!'
, quote_literal(_user_id), NEW.id; -- optional
END IF;
RETURN NULL; -- OK for AFTER trigger
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
Notes:
Avoid variable names that match column names. Very error prone. One popular naming convention is to prepend variable names with an underscore: _user_id.
Assign at declaration time to save one assignment. Note the data type text. We'll cast later, after sorting out invalid input.
Avoid raising / trapping an exception if possible. The manual:
A block containing an EXCEPTION clause is significantly more expensive
to enter and exit than a block without one. Therefore, don't use
EXCEPTION without need.
Test for valid integer strings. This simple regular expression allows only digits (no leading sign, no white space): _user_id ~ '^\d+$'. I reset to NULL for any invalid input. Adapt to your needs.
I added an optional WARNING for your debugging convenience.
Cases 3. and 4. only arise because customized options are string literals (type text), valid data types cannot be enforced automatically.
Related:
User defined variables in PostgreSQL
Is there a way to define a named constant in a PostgreSQL query?
All that aside, there may be more elegant solutions for what you are trying to do without customized options, depending on your exact requirements. Maybe this:
Fastest way to get current user's OID in Postgres?
It is not clear why you are trying to concat NULL to user_id but it is obviously the cause of the problem. Get rid of it:
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
DECLARE
user_id integer;
BEGIN
user_id := current_setting('myvars.user_id')::integer;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Note that
SELECT 55 || NULL
always gives NULL.
You can catch the exception when the value doesn't exist - here's the changes I made to get this to work:
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
DECLARE
user_id integer;
BEGIN
BEGIN
user_id := current_setting('myvars.user_id')::integer;
EXCEPTION WHEN OTHERS THEN
user_id := 0;
END;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION insert_house() RETURNS void as $$
DECLARE
user_id integer;
BEGIN
PERFORM set_config('myvars.user_id', '55', false);
INSERT INTO houses (name) VALUES ('HOUSE PARTY');
END; $$ LANGUAGE plpgsql;
Currently running version 9.5.3. Update planned, of course.
I have a PostgreSQL database whose schema pre-dates table row-level security (i.e. CREATE POLICY ...). Row-level security was implemented using views. The security is done in the view by selecting only rows that have the ownername matching CURRENT_USER.
I'm trying to build an upsert query using such a view. The problem comes when I try to name the conflict_target.
The problem with using ON CONFLICT UPDATE ... comes from naming what constraint has been violated.
Toy Example
CREATE TABLE foo (id serial, num int, word text, data text, ownername varchar(64));
For each user, the combinations of word and num must be unique.
CREATE UNIQUE INDEX foo_num_word_owner_idx ON foo (num, word, ownername);
The row-level security is implemented using a view based on the current user name. Permission is granted for the view, and removed for the underlying table for ordinary user. security_barrier was added after v 9.5. Note that users don't see ownername.
CREATE VIEW foo_user WITH (security_barrier = True) AS
SELECT id, num, word, data FROM foo
WHERE foo.ownername = CURRENT_USER;
Now auto-set ownername:
CREATE OR REPLACE FUNCTION trf_set_owner() RETURNS trigger AS
$$
BEGIN
IF (TG_OP = 'INSERT') THEN
NEW.ownername = CURRENT_USER::varchar(64);
END IF;
IF (TG_OP = 'UPDATE') THEN
NEW.ownername = CURRENT_USER::varchar(64);
END IF;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
CREATE TRIGGER foo_row_owner
BEFORE INSERT OR UPDATE ON foo FOR EACH ROW
EXECUTE PROCEDURE trf_set_owner();
Note that the ownername column is not displayed in the view; the row security is invisible to the user.
Now add some data:
INSERT INTO foo_user (num, word, data) VALUES (1, 'asdf', 'cat'), (2, 'qwer', 'dog');
SELECT * FROM foo;
-- normally, this would give an error related to privileges,
-- because we don't allow users to query the underlying table.
-- bypassed here for demo purposes.
id | num | word | data | ownername
----+-----+------+------+-----------
1 | 1 | asdf | cat | admin
2 | 2 | qwer | dog | admin
(2 rows)
SELECT * FROM foo_user;
id | num | word | data
----+-----+------+------
1 | 1 | asdf | cat
2 | 2 | qwer | dog
(2 rows)
So far, so good.
What I've Tried
As stated above, for each user, num and word must be unique. There is no problem with different owners having the same num and word (in fact, we expect it).
I'm trying to take advantage of the ON CONFLICT clause in INSERT to create some back-end UPSERT-ish functionality. And it's falling down.
Simple example of error
First, a simple failed insert:
INSERT INTO foo_user (num, word, data) VALUES (2, 'qwer', 'frog');
ERROR: duplicate key value violates unique constraint "foo_num_word_owner_idx"
DETAIL: Key (num, word, ownername)=(2, qwer, admin) already exists.
Entirely expected. Nothing wrong with that.
ON CONFLICT, first attempt
Now we try to make the client experience a bit smoother:
INSERT INTO foo_user (num, word, data) VALUES (2, 'qwer', 'frog')
ON CONFLICT DO UPDATE
SET data = 'frog'
WHERE num = 2 AND word = 'qwer';
ERROR: ON CONFLICT DO UPDATE requires inference specification or constraint name
LINE 2: ON CONFLICT DO UPDATE
^
HINT: For example, ON CONFLICT (column_name).
Yep, just like the documentation says. It needs to know what rule it broke. No problem:
ON CONFLICT, second attempt
INSERT INTO foo_user (num, word, data) VALUES (2, 'qwer', 'frog')
ON CONFLICT (num, word, ownername) DO UPDATE
SET data = 'frog'
WHERE num = 2 AND word = 'qwer';
ERROR: column "ownername" does not exist
LINE 2: ON CONFLICT (num, word, ownername) DO UPDATE
True. Ownername does not exist in the view. We can't drop ownername from the unique index, because we fully expect different owners to have identical num and word values.
ON CONFLICT, third attempt
So I tried converting the index to a constraint, and naming the constraint:
ALTER TABLE foo
ADD CONSTRAINT foo_num_word_owner_crt UNIQUE
USING INDEX foo_num_word_owner_idx;
NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index
"foo_num_word_owner_idx" to "foo_num_word_owner_crt"
Ok, now to test:
INSERT INTO foo_user (num, word, data) VALUES (2, 'qwer', 'frog')
ON CONFLICT ON CONSTRAINT foo_num_word_owner_crt DO UPDATE
SET data = 'frog'
WHERE num = 2 AND word = 'qwer';
ERROR: constraint "foo_num_word_owner_crt" for table "foo_user" does not exist
Well that makes sense: we're querying on the view but specifying a table constraint.
Conclusion
Now I'm out of ideas. How do we get ON CONFLICT to play nice with views like this? Or is it not possible?
I'm this close (holds up thumb and forefinger) to proposing we switch from views to tables with row-level security, but that's rather a lot of work (not necessarily an API-breaker, but still).
Any insights are much appreciated.
You can circumvent the issue by removing the ON CONFLICT clause and using an INSTEAD OF trigger that manually tests for any index conflict:
CREATE OR REPLACE FUNCTION trf_set_num_word() RETURNS trigger AS $$
BEGIN
-- Check if (num, word, ownername) exists by trying an UPDATE
UPDATE foo SET data = 'frog'
WHERE num = NEW.num AND word = NEW.word AND ownername = CURRENT_USER::varchar(64);
IF FOUND THEN
RETURN NULL; -- If so, don't INSERT/UPDATE
END IF;
RETURN NEW; -- If not, do the INSERT
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER foo_user_num_word
INSTEAD OF INSERT OR UPDATE ON foo_user FOR EACH ROW
EXECUTE PROCEDURE trf_set_num_word();
I have this function in my postgresql database that update row if exist or insert new one if it doesn't exist:
CREATE OR REPLACE FUNCTION insert_or_update(val1 integer, val2 integer) RETURNS VOID AS $$
DECLARE
BEGIN
UPDATE my_table SET col2 = val2 WHERE col1 = val1;
IF NOT FOUND THEN
INSERT INTO my_table (col2) values ( val2 );
END IF;
END;
$$ LANGUAGE 'plpgsql';
For now it's working perfect but I want to get the id of row if updated or inserted.
How can I do it?
Your function is declared as returns void so it can't return anything.
Assuming col1 is the primary key and is also defined as a serial, you can do something like this:
CREATE OR REPLACE FUNCTION insert_or_update(val1 integer, val2 integer)
RETURNS int
AS $$
DECLARE
l_id integer;
BEGIN
l_id := val1; -- initialize the local variable.
UPDATE my_table
SET col2 = val2
WHERE col1 = val1; -- !! IMPORTANT: this assumes col1 is unique !!
IF NOT FOUND THEN
INSERT INTO my_table (col2) values ( val2 )
RETURNING col1 -- this makes the generated value available
into l_id; -- and this stores it in the local variable
END IF;
return l_id; -- return whichever was used.
END;
$$ LANGUAGE plpgsql;
I changed four things compared to your function:
the function is declared as returns integer in order to be able to return something
you need a variable where you can store the returned value from the insert statement
and finally the generated value needs to be returned:
The language name is an identifier, so it must not be quoted using single quotes.
If you want to distinguish between an update or an insert from the caller, you could initialize l_id to null. In that case the function will return null if an update occurred and some value otherwise.
You can get the LastInsert ID using the method CURVAL(SEQUENCE_NAME_OF_TABLE).
But the best way is always to use the INSERT or UPDATE queries with RETURNING Clause.
CREATE OR REPLACE FUNCTION insert_or_update(val1 integer, val2 integer) RETURNS VOID AS $$
DECLARE
BEGIN
UPDATE my_table SET col2 = val2 WHERE col1 = val1 RETURNING col1;
IF NOT FOUND THEN
INSERT INTO my_table (col2) values ( val2 ) RETURNING col1;
END IF;
END;
$$ LANGUAGE 'plpgsql';
You can refer the following examples:
Insert Command - Last Example
Postgres with RETURNING clause
Note: In your UPDATE query, your WHERE clause is col1=val1. I assume that Val1 will be unique value, else multiple records will be updated. Hope you know that. And I assume col1 is your Primary Key like ID or so.
The PostgreSQL wiki's entry on UPSERT states that INSERT ... ON CONFLICT UPDATE will be added to PostgreSQL 9.5. This will allow you to more directly express the operation you desire without resorting to a stored procedure and/or introducing race conditions.
This operation is otherwise surprisingly tricky to express in earlier PostgreSQL versions without the risk of database corruption and/or a race condition. The code fragments posted so far all contain an error in that if two callers happen to want to upsert the same nonexistent row, the initial UPDATE will update zero rows and then they will both attempt an INSERT, one of which will fail. It should at least fail safe, aborting the query and any transaction in progress.
The PostgreSQL documentation on INSERT (search on that page for the text "Attempt to insert a new stock item along with the quantity of stock") shows how to do it safely and correctly on PostgreSQL 9.4 and earlier. Of particular note is that it tries the INSERT first to avoid any races on that front, and if that fails, does an UPDATE of the row it now knows exists. It uses a SAVEPOINT to ensure that a failed INSERT does not abort the transaction.
I am trying to create a trigger function in PostgreSQL that should check records with the same id (i.e. comparison by id with existing records) before inserting or updating the records. If the function finds records that have the same id, then that entry is set to be the time_dead. Let me explain with this example:
INSERT INTO persons (id, time_create, time_dead, name)
VALUES (1, 'now();', ' ', 'james');
I want to have a table like this:
id time_create time-dead name
1 06:12 henry
2 07:12 muka
id 1 had a time_create 06.12 but the time_dead was NULL. This is the same as id 2 but next time I try to run the insert query with same id but different names I should get a table like this:
id time_create time-dead name
1 06:12 14:35 henry
2 07:12 muka
1 14:35 waks
henry and waks share the same id 1. After running an insert query henry's time_dead is equal to waks' time_create. If another entry was to made with id 1, lets say for james, the time entry for james will be equal to the time_dead for waks. And so on.
So far my function looks like this. But it's not working:
CREATE FUNCTION tr_function() RETURNS trigger AS '
BEGIN
IF tg_op = ''UPDATE'' THEN
UPDATE persons
SET time_dead = NEW.time_create
Where
id = NEW.id
AND time_dead IS NULL
;
END IF;
RETURN new;
END
' LANGUAGE plpgsql;
CREATE TRIGGER sofgr BEFORE INSERT OR UPDATE
ON persons FOR each ROW
EXECUTE PROCEDURE tr_function();
When I run this its say time_dead is not supposed to be null. Is there a way I can write a trigger function that will automatically enter the time upon inserting or updating but give me results like the above tables when I run a select query?
What am I doing wrong?
My two tables:
CREATE TABLE temporary_object
(
id integer NOT NULL,
time_create timestamp without time zone NOT NULL,
time_dead timestamp without time zone,
PRIMARY KEY (id, time_create)
);
CREATE TABLE persons
(
name text
)
INHERITS (temporary_object);
Trigger function
CREATE FUNCTION tr_function()
RETURNS trigger AS
$func$
BEGIN
UPDATE persons p
SET time_dead = NEW.time_create
WHERE p.id = NEW.id
AND p.time_dead IS NULL
AND p.name <> NEW.name;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
You were missing the INSERT case in your trigger function (IF tg_op = ''UPDATE''). But there is no need for checking TG_OP to begin with, since the trigger only fires on INSERT OR UPDATE - assuming you don't use the same function in other triggers. So I removed the cruft.
Note that you don't have to escape single quotes inside a dollar-quoted string.
Also added:
AND p.name <> NEW.name
... to prevent INSERT's from terminating themselves instantly (and causing an infinite recursion). This assumes that a row can never succeed another row with the same name.
Aside: The setup is still not bullet-proof. UPDATEs could mess with your system. I could keep updating the id or a row, thereby terminating other rows but not leaving a successor. Consider disallowing updates on id. Of course, that would make the trigger ON UPDATE pointless. I doubt you need that to begin with.
now() as DEFAULT
If you want to use now() as default for time_create just make it so. Read the manual about setting a column DEFAULT. Then skip time_create in INSERTs and it is filled automatically.
If you want to force it (prevent everyone from entering a different value) create a trigger ON INSERT or add the following at the top of your trigger:
IF TG_OP = 'INSERT' THEN
NEW.time_create := now(); -- type timestamp or timestamptz!
RETURN NEW;
END IF;
Assuming your missleadingly named column "time_create" is actually a timestamp type.
That would force the current timestamp for new rows.
I have two tables. Lets say tblA and tblB.
I need to insert a row in tblA and use the returned id as a value to be inserted as one of the columns in tblB.
I tried finding out this in documentation but could not get it. Well, is it possible to write a statement (intended to be used in prepared) like
INSERT INTO tblB VALUES
(DEFAULT, (INSERT INTO tblA (DEFAULT, 'x') RETURNING id), 'y')
like we do for SELECT?
Or should I do this by creating a Stored Procedure?. I'm not sure if I can create a prepared statement out of a Stored Procedure.
Please advise.
Regards,
Mayank
You'll need to wait for PostgreSQL 9.1 for this:
with
ids as (
insert ...
returning id
)
insert ...
from ids;
In the meanwhile, you need to use plpgsql, a temporary table, or some extra logic in your app...
This is possible with 9.0 and the new DO for anonymous blocks:
do $$
declare
new_id integer;
begin
insert into foo1 (id) values (default) returning id into new_id;
insert into foo2 (id) values (new_id);
end$$;
This can be executed as a single statement. I haven't tried creating a PreparedStatement out of that though.
Edit
Another approach would be to simply do it in two steps, first run the insert into tableA using the returning clause, get the generated value through JDBC, then fire the second insert, something like this:
PreparedStatement stmt_1 = con.prepareStatement("INSERT INTO tblA VALUES (DEFAULT, ?) returning id");
stmt_1.setString(1, "x");
stmt_1.execute(); // important! Do not use executeUpdate()!
ResultSet rs = stmt_1.getResult();
long newId = -1;
if (rs.next()) {
newId = rs.getLong(1);
}
PreparedStatement stmt_2 = con.prepareStatement("INSERT INTO tblB VALUES (default,?,?)");
stmt_2.setLong(1, newId);
stmt_2.setString(2, "y");
stmt_2.executeUpdate();
You can do this in two inserts, using currval() to retrieve the foreign key (provided that key is serial):
create temporary table tb1a (id serial primary key, t text);
create temporary table tb1b (id serial primary key,
tb1a_id int references tb1a(id),
t text);
begin;
insert into tb1a values (DEFAULT, 'x');
insert into tb1b values (DEFAULT, currval('tb1a_id_seq'), 'y');
commit;
The result:
select * from tb1a;
id | t
----+---
3 | x
(1 row)
select * from tb1b;
id | tb1a_id | t
----+---------+---
2 | 3 | y
(1 row)
Using currval in this way is safe whether in or outside of a transaction. From the Postgresql 8.4 documentation:
currval
Return the value most recently
obtained by nextval for this sequence
in the current session. (An error is
reported if nextval has never been
called for this sequence in this
session.) Because this is returning a
session-local value, it gives a
predictable answer whether or not
other sessions have executed nextval
since the current session did.
You may want to use AFTER INSERT trigger for that. Something along the lines of:
create function dostuff() returns trigger as $$
begin
insert into table_b(field_1, field_2) values ('foo', NEW.id);
return new; --values returned by after triggers are ignored, anyway
end;
$$ language 'plpgsql';
create trigger trdostuff after insert on table_name for each row execute procedure dostuff();
after insert is needed because you need to have the id to reference it. Hope this helps.
Edit
A trigger will be called in the same "block" as the command that triggered it, even if not using transactions - in other words, it becomes somewhat part of that command.. Therefore, there is no risk of something changing the referenced id between inserts.