8

In Postgres 8.4 or higher, what is the most efficient way to get a row of data populated by defaults without actually creating the row. Eg, as a transaction (pseudocode):

create table "mytable"
(
  id serial PRIMARY KEY NOT NULL,
  parent_id integer NOT NULL DEFAULT 1,
  random_id integer NOT NULL DEFAULT random(),
)

begin transaction
  fake_row = insert into mytable (id) values (0) returning *;
  delete from mytable where id=0;
  return fake_row;
end transaction

Basically I'd expect a query with a single row where parent_id is 1 and random_id is a random number (or other function return value) but I don't want this record to persist in the table or impact on the primary key sequence serial_id_seq.

My options seem to be using a transaction like above or creating views which are copies of the table with the fake row added but I don't know all the pros and cons of each or whether a better way exists.

I'm looking for an answer that assumes no prior knowledge of the datatypes or default values of any column except id or the number or ordering of the columns. Only the table name will be known and that a record with id 0 should not exist in the table.

In the past I created the fake record 0 as a permanent record but I've come to consider this record a type of pollution (since I typically have to filter it out of future queries).

SpliFF
  • 38,186
  • 16
  • 91
  • 120
  • I want a row that is exactly what I'd get if I inserted it (letting the defaults populate in the DB) and selected it straight away but without the overhead/complexity and/or sideeffects of using a transaction (unless transactions really are the best way). – SpliFF Aug 01 '13 at 04:48
  • The query for the sequence is atomic and increments regardless of whether or not you roll back your transaction. This might not be a problem for you, but is worth mentioning. – bma Aug 01 '13 at 04:51
  • Yes the sequence might be an issue. I'd prefer not to create a lot of gaps in the sequence. It isn't critical but it does make debugging easier (knowing the difference between records that may have existed and been deleted and those that were never really created). – SpliFF Aug 01 '13 at 04:52

3 Answers3

7

You can copy the table definition and defaults to the temp table with:

CREATE TEMP TABLE table_name_rt (LIKE table_name INCLUDING DEFAULTS);

And use this temp table to generate dummy rows. Such table will be dropped at the end of the session (or transaction) and will only be visible to current session.

SpliFF
  • 38,186
  • 16
  • 91
  • 120
Ihor Romanchenko
  • 26,995
  • 8
  • 48
  • 44
  • 1
    Thanks. Your code was missing required parenthesis around the LIKE and INCLUDING clauses so I've updated your answer. – SpliFF Aug 02 '13 at 02:15
4

You can query the catalog and build a dynamic query

Say we have this table:

create table test10(
      id serial primary key,
      first_name varchar( 100 ),
      last_name  varchar( 100 ) default 'Tom',
      age int not null default 38,
      salary float  default 100.22
);

When you run following query:

SELECT string_agg( txt, ' ' order by id ) 
FROM (
select 1 id, 'SELECT ' txt
union all
select 2, -9999 || ' as id '
union all
select 3, ', '  
       || coalesce( column_default,  'null'||'::'||c.data_type ) 
       || ' as ' || c.column_name
from information_schema.columns c
where table_schema = 'public'
    and table_name = 'test10'
    and ordinal_position > 1
) xx
    ;

you will get this sting as a result:

"SELECT  -9999 as id  , null::character varying as first_name , 
'Tom'::character varying as last_name , 38 as age , 100.22 as salary"

then execute this query and you will get the "phantom row".

We can build a function that build and excecutes the query and return our row as a result:

CREATE OR REPLACE FUNCTION get_phantom_rec (p_i test10.id%type ) 
returns test10 as $$
DECLARE 
    v_sql text;
    myrow test10%rowtype;
begin
   SELECT string_agg( txt, ' ' order by id ) 
   INTO v_sql
   FROM (
    select 1 id, 'SELECT ' txt
    union all
    select 2, p_i || ' as id '
    union all
    select 3, ', '  
           || coalesce( column_default,  'null'||'::'||c.data_type ) 
           || ' as ' || c.column_name
    from information_schema.columns c
    where table_schema = 'public'
        and table_name = 'test10'
        and ordinal_position > 1
    ) xx
    ;
    EXECUTE v_sql INTO myrow;
    RETURN  myrow;
END$$ LANGUAGE plpgsql ;

and then this simple query gives you what you want:

select * from get_phantom_rec ( -9999 );

  id   | first_name | last_name | age | salary
-------+------------+-----------+-----+--------
 -9999 |            | Tom       |  38 | 100.22
krokodilko
  • 35,300
  • 7
  • 55
  • 79
  • Wow, how long did you spend on this? This answer deserves a bounty just for effort alone. It may not be the solution I go with because the temp table method posted by Igor looks so much easier but I suspect your answer might be faster in cases where performance is an issue (in my case it isn't). Either way your answer is educational. – SpliFF Aug 02 '13 at 01:35
2

I would just select the fake values as literals:

select 1 id, 1 parent_id, 1 user_id

The returned row will be (virtually) indistinguishable from a real row.


To get the values from the catalog:

select
  0 as id, -- special case for serial type, just return 0
  (select column_default::int -- Cast to int, because we know the column is int
   from INFORMATION_SCHEMA.COLUMNS
   where table_name = 'mytable'
   and column_name = 'parent_id') as parent_id,
  (select column_default::int -- Cast to int, because we know the column is int
   from INFORMATION_SCHEMA.COLUMNS
   where table_name = 'mytable'
   and column_name = 'user_id') as user_id;

Note that you must know what the columns are and their type, but this is reasonable. If you change the table schema (except default value), you would need to tweak the query.

See the above as a SQLFiddle.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • no. see last part of the question. I don't want to have to know the type or default value of somecol, that is what I'm trying to retreive. EDIT: I want to get all columns of the table without foreknowledge of the table structure. – SpliFF Aug 01 '13 at 04:35
  • Do you want the defaults as defined in the table for the columns returned? If so, you can query the catalog tables to construct a "default row", including returning zero when its a serial. I don't have an installation of postgres handy to get the query right, but I know I could be done. – Bohemian Aug 01 '13 at 04:36
  • yes I always want the defaults returned exactly as defined by the `DEFAULT` clause. – SpliFF Aug 01 '13 at 04:37
  • http://sqlfiddle.com/#!1/67ed4/3 is setup for you to refine your query if you like. – Shawn Balestracci Aug 01 '13 at 04:40
  • The trouble with the schema catalogue is that you get the literal string definition of the default value (eg, `random()`) but not the *calculated default value* (eg, 2348756) (see updated fiddle). – SpliFF Aug 01 '13 at 04:44
  • @ShawnBalestracci thx for the fiddle - I used it - see answer – Bohemian Aug 01 '13 at 05:37
  • "Note that you must know what the columns are and their type, but this is reasonable". I'm trying to let the schema drive my data format. It is my intention to discover columns, types and defaults from the table schema, not know them in advance no matter how unreasonable that sounds on the surface. This is part of a CMS system where the tables might be created on the fly or modified and I want my code to dynamically adapt (which it does, given access to calculated default values). – SpliFF Aug 01 '13 at 05:46
  • The code above does not work when the default value is a function. I need DEFAULT functions to run. – SpliFF Aug 01 '13 at 05:50
  • Then maybe you should put a default function in your example. – Bohemian Aug 01 '13 at 05:51
  • What you want is beyond what SQL can deliver. Do it in app code. Put an adapter layer between the rowset and what uses it that converts to a pojo - probably a HashMap for each row. Have your app code interrogate the catalog and deliver a HashMap into your business code. Then this problem will cease to exist. – Bohemian Aug 01 '13 at 05:54
  • SQL *can* do it using (INSERT / SELECT / DELETE in a transaction). The question was does a better method exist, such as a PostgreSQL feature I've overlooked? – SpliFF Aug 01 '13 at 05:59
  • I wouldn't even consider it. It muddies the database IMHO. Leave to app code. Easier to code and use. – Bohemian Aug 01 '13 at 06:03