how can I insert data by declaring all fields (not *)? - postgresql

Here's my insert query:
INSERT INTO listing_replica_child (
(
SELECT rtz_comma_list(column_name)
FROM information_schema.columns
WHERE table_name = 'listing'
)
)
VALUES (
(
SELECT (
(
SELECT rtz_comma_list(column_name)
FROM information_schema.columns
WHERE table_name = 'listing'
)
FROM listing
WHERE listing_id = 9656
)
)
);
I'm using postgres.

do $$
DECLARE rec TEXT;
BEGIN
SELECT 'insert into listing_replica_child (' || t.col || ') select * from listing WHERE listing_id = 9656 '
INTO rec
FROM (
SELECT string_agg(column_name, ',') col
FROM information_schema.columns
WHERE table_name = 'listing'
) t;
EXECUTE rec;
END;$$;
You can wrap this dynamic query into a function like this
CREATE OR replace FUNCTION insert_listing_replica_child (_listing_id INT)
RETURNS void AS $$
DECLARE rec TEXT;
BEGIN
SELECT 'insert into listing_replica_child (' || t.col || ') select * from listing WHERE listing_id = ' || _listing_id || ' '
INTO rec
FROM (
SELECT string_agg(column_name, ',') col
FROM information_schema.columns
WHERE table_name = 'listing'
) t;
EXECUTE rec;
END $$
LANGUAGE plpgsql
So if want to insert values from listing table with lissting_id=9656 into table listing_replica_child
just Call
select insert_listing_replica_child (9656)

The way to specify all destination fields is to omit the column list entirely.
Your attempt then becomes:
INSERT INTO listing_replica_child
SELECT * FROM listing
WHERE listing_id = 9656
without loss of intention: If the number and type of the fields of the tables differs with your attempt, execution will explode (as will this query).
Even though you have asked that * not be used, it is the simplest and best way to achieve the task.

Related

Postgresql error: cannot open EXECUTE query as cursor

I have written a function to read certain columns from a table below using a dynamic query:
CREATE OR REPLACE FUNCTION select_cols ()
RETURNS SETOF mytable_name
LANGUAGE plpgsql
AS $$
DECLARE
list_of_columns text;
BEGIN
SELECT
string_agg(trim(cols::text, '()'), ', ') INTO list_of_columns
FROM (
SELECT
'mytable_name.' || column_name
FROM
information_schema.columns
WHERE
table_name = 'mytable_name'
AND column_name LIKE 'rm%_b'
OR column_name LIKE 'rm%_s') AS cols;
RETURN query EXECUTE concat(format('select %s from mytable_name', list_of_columns), ' RETURNING *');
END
$$;
Though when I run
select * from select_cols();
it gives me an error : "cannot open EXECUTE query as cursor".
I appreciate if someone can help with this issue
You are not returning a set, but you aggreagte the result set for only one table. So, for only one table you can use:
CREATE OR REPLACE FUNCTION select_colsx ()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
list_of_columns text;
BEGIN
SELECT
'select '||string_agg(trim(cols::text, '()'), ', ') || ' from pg_class RETURNING *'
INTO list_of_columns
FROM (
SELECT
'pg_class.' || column_name
FROM
information_schema.columns
WHERE
table_name = 'pg_class'
AND column_name LIKE 'oid'
OR column_name LIKE 'relacl') AS cols;
RETURN list_of_columns ;
END
$$;
select select_colsx();
DB Fiddle Example
RETURN QUERY EXECUTE was introduced in PostgreSQL 8.4. Upgrade to a less ancient version.

Use result of one query to query another in Postgres

I'm trying to get the column size for each row in a table. That's basically the combination of these two queries:
SELECT pg_size_pretty(sum(pg_column_size(COLUMN_NAME))) FROM TABLE_NAME;
And
SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'TABLE_NAME';
My first attempt was to do these two queries:
=> SELECT column_name, (SELECT pg_size_pretty(sum(pg_column_size(column_name))) FROM TABLE_NAME) FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'TABLE_NAME';
ERROR: column "columns.column_name" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT column_name, (SELECT pg_size_pretty(sum(pg_column_siz...
^
=> SELECT column_name, (SELECT pg_size_pretty(sum(pg_column_size(column_name))) FROM TABLE_NAME) FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'TABLE_NAME' GROUP BY column_name;
ERROR: more than one row returned by a subquery used as an expression
Tried the following too:
SELECT column_name, (SELECT pg_size_pretty(sum(pg_column_size(column_name))) FROM TABLE_NAME) FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'TABLE_NAME' GROUP BY 1;
Which returned:
ERROR: more than one row returned by a subquery used as an expression
When I add a LIMIT 1, the result is incorrect:
SELECT column_name,
(SELECT pg_size_pretty(sum(pg_column_size(column_name))) FROM main_apirequest LIMIT 1)
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'main_apirequest'
GROUP BY 1;
It looks something like this:
column_name | pg_size_pretty
------------------+----------------
api_key_id | 11 bytes
id | 3 bytes
...
When it should be something like this (which doesn't happen because of the limit 1)
=> SELECT pg_size_pretty(sum(pg_column_size(id))) FROM main_apirequest
;
pg_size_pretty
----------------
19 MB
Since you don't know the columns names in advance, but want to use the column name in the query, you'll have to use dynamic sql. Here's a quick example:
CREATE TABLE t1 (id INTEGER, txt TEXT);
INSERT INTO t1
SELECT g, random()::TEXT
FROM generate_series(1, 10) g;
Then the SQL to generate the query is:
DO $$
DECLARE
query TEXT;
BEGIN
SELECT 'SELECT ' || STRING_AGG(FORMAT('sum(pg_column_size(%1$I)) AS %1$s', column_name), ', ') || ' FROM t1'
INTO query
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 't1';
RAISE NOTICE '%', query;
END $$
The query created is SELECT pg_size_pretty(sum(pg_column_size(id))) AS id, pg_size_pretty(sum(pg_column_size(txt))) AS txt FROM t1
Would work the same if you had hundreds of columns.
Now to get it to generate and run the query and return you results, it really depends on what you want. If you're happy just having it print to the screen, then maybe you can format it like this instead:
DO $$
DECLARE
query TEXT;
result TEXT;
BEGIN
SELECT 'SELECT CONCAT_WS(E''\n'', ' || STRING_AGG(FORMAT('''%1$s: '' || pg_size_pretty(sum(pg_column_size(%1$I)))', column_name), ', ') || ') FROM t1'
INTO query
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 't1';
EXECUTE query
INTO result;
RAISE NOTICE '%', result;
END $$
That prints:
id: 40 bytes
txt: 181 bytes
If instead you want a record returned with multiple columns, I'm not too sure how you'd go about it because the number of columns and their names would be unknown. Best hack I can think around it would be to return it as JSON, then you return just one thing and there will be a variable number of fields in there with whatever column names:
CREATE OR REPLACE FUNCTION test1(_schema_name TEXT, _table_name TEXT)
RETURNS JSON AS
$$
DECLARE
query TEXT;
result JSON;
BEGIN
SELECT 'SELECT ROW_TO_JSON(cols) FROM (SELECT ' || STRING_AGG(FORMAT('pg_size_pretty(sum(pg_column_size(%1$I))) AS %1$s', column_name), ', ') || ' FROM t1) AS cols'
INTO query
FROM information_schema.columns
WHERE table_schema = _schema_name
AND table_name = _table_name;
EXECUTE query
INTO result;
RETURN result;
END
$$
LANGUAGE plpgsql;
Running it: SELECT test1('public', 't1')
Returns: {"id":"40 bytes","txt":"181 bytes"}

ERROR: missing FROM-clause entry for table "new"

I have a parent table layer_1_ and a number of child tables layer_1_points, layer_1_linestrings etc. which contain some geometry data. Each child table has its own geometry constraint. So, for example, layer_1_points has this constraint:
CONSTRAINT enforce_geotype_geom_geom CHECK (geometrytype(geom) = 'POINT'::text)
Whereas layer_1_linestrings table has this constraint:
CONSTRAINT enforce_geotype_geom_geom CHECK (geometrytype(geom) = 'LINESTRING'::text)
Many other layer tables have similar names: layer_2_, layer_3_, ..., layer_N_. And all of them have their own child tables. What I want to achive is that when a user inserts to a parent table (layer_N_), then this insert statement should be forwarded to a particular child table (layer_N_points etc.). So, for example, when I do:
INSERT INTO layer_1_ (geom) VALUES(ST_GeomFromText('POINT(0 0)', 3857))
I should actually insert to layer_1_points, because geom type is POINT. To achive all this I created this trigger function and the trigger itself:
CREATE OR REPLACE FUNCTION trigger_layer_insert()
RETURNS trigger AS
$$
DECLARE
var_geomtype text;
table_name text;
layer_id text := (TG_ARGV[0])::text;
BEGIN
var_geomtype := geometrytype(NEW.geom);
IF var_geomtype = 'POINT' THEN
table_name := (SELECT concat ('layer_', layer_id, '_points'));
ELSIF var_geomtype = 'MULTIPOINT' THEN
table_name := (SELECT concat ('layer_', layer_id, '_multipoints'));
ELSIF var_geomtype = 'LINESTRING' THEN
table_name := (SELECT concat ('layer_', layer_id, '_linestrings'));
ELSIF var_geomtype = 'MULTILINESTRING' THEN
table_name := (SELECT concat ('layer_', layer_id, '_multilinestrings'));
ELSIF var_geomtype = 'POLYGON' THEN
table_name := (SELECT concat ('layer_', layer_id, '_polygons'));
ELSIF var_geomtype = 'MULTIPOLYGON' THEN
table_name := (SELECT concat ('layer_', layer_id, '_multipolygons'));
END IF;
EXECUTE '
INSERT INTO ' || table_name || '
SELECT * FROM (SELECT NEW.*) AS t
';
RETURN NULL;
END;
$$
LANGUAGE 'plpgsql' VOLATILE;
CREATE TRIGGER trigger_layer_1_ BEFORE INSERT
ON layer_1_ FOR EACH ROW
EXECUTE PROCEDURE trigger_layer_insert(1);
However, when I do actual insert like:
INSERT INTO layer_1_ (geom) VALUES(ST_GeomFromText('POINT(0 0)', 3857))
I get an error message:
ERROR: missing FROM-clause entry for table "new"
LINE 3: SELECT * FROM (SELECT NEW.*) AS t
^
QUERY:
INSERT INTO layer_1_points
SELECT * FROM (SELECT NEW.*) AS t
So, what is wrong with SELECT NEW.* and how can I fix it? Thanks!
EDIT
I also tried this:
EXECUTE '
INSERT INTO ' || table_name || '
SELECT * FROM (SELECT NEW.*) AS t
' USING NEW;
But it has no effect.
When you execute something using PLPGSQL statement EXECUTE it runs in the different context so local variables is not visible there. To pass variable(s) the EXECUTE '<SQL script>' USING <variables list>; form is used:
EXECUTE 'insert into table(field1, field2) values ($1, $2)' USING var1, var2;
So the statement should be:
EXECUTE 'INSERT INTO ' || table_name || ' SELECT * FROM SELECT $1.*) AS t'
USING NEW;
But much more secure is using format function:
execute format('INSERT INTO %I SELECT * FROM SELECT $1.*) AS t', table_name)

Reusing a declared variable in postgres function

I am writing a postgresql function and my construct is as follows:
CREATE OR REPLACE FUNCTION function_name (argument_list) RETURNS INTEGER []
AS $$
DECLARE
--along with other declarations
_tablename text;
BEGIN
-- dynamically construct the intermediate _tablename which gets
-- populated
-- Now I want to use this _tablename in other queries like :
-- use it in the select from _tablename loop
-- construct array by selecting a column from this table
-- and return that array
END
How should I do this? I want to reuse the declared variable name in my further queries in the function.
My complete postgres function is as follows:
DROP FUNCTION get_value_histogram(BIGINT,BIGINT,BIGINT,INTEGER);
CREATE OR REPLACE FUNCTION get_value_histogram(customer_id BIGINT,
start_time BIGINT, end_time BIGINT, bucket_size INTEGER)
RETURNS INTEGER[] AS
$$
DECLARE
_tablename text;
_curr_timestamp BIGINT;
_var1 text;
_min_value INTEGER;
_max_value INTEGER;
_return_array INTEGER[];
BEGIN
-- create an intermediate table with the aggregation of the
-- required values. These values then will be passed to the
-- Histogram function.
_var1 := EXTRACT (EPOCH FROM now());
_var1 := replace(_var1, '.','_');
_tablename := 'thing_data_' || _var1;
EXECUTE 'CREATE TABLE ' || _tablename || ' (t_stamp BIGINT, sum_of_values INTEGER)';
--insert all the values in this intermediate table
EXECUTE ' INSERT INTO ' || _tablename || ' ( select t_stamp , sum(data) from thing_data td, collector_tb ct where td.thingname =
ct.collector_name and td.t_stamp BETWEEN ' || quote_literal(start_time) || ' AND ' || quote_literal(end_time) || ' and
ct.type like ' || quote_literal('%outlet%') ||' AND customer_id = ' || customer_id || ' GROUP BY t_stamp)' ;
EXECUTE 'select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from ' || _tablename || ' GROUP BY 1 ORDER BY 1' ;
_return_array := array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from _tablename GROUP BY 1 ORDER BY 1));
EXECUTE 'DROP TABLE ' || _tablename;
RETURN _return_array;
END $$ LANGUAGE plpgsql;
When I run this, I get an error saying relation "_tablename" does not exist
just replace :
_return_array := array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from _tablename GROUP BY 1 ORDER BY 1) a);
by :
EXECUTE 'select array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt from '|| _tablename ||' GROUP BY 1 ORDER BY 1) a)' into _return_array;
I assume the error is in the last part:
_return_array := array (select cnt from (select width_bucket(sum_of_values,500, 1000 , 100), count(*) as cnt
from _tablename GROUP BY 1 ORDER BY 1));
Here you're using _tablename as an actual literal table name and not as a variable.

Postgresql function return multiple select statements

Can any one of you tell me how to approach this:
CREATE OR REPLACE FUNCTION name()
RETURNS ????? AS
$func$
BEGIN
SELECT * FROM tbl_a a;
SELECT * FROM tbl_b b;
END
$func$ LANGUAGE plpgsql;
Both tables have different structures.
You can use cursors but I can hardly imagine why you need such a function.
CREATE OR REPLACE FUNCTION my_multiselect(refcursor, refcursor) RETURNS VOID AS
$func$
BEGIN
OPEN $1 FOR SELECT * FROM information_schema.routines;
OPEN $2 FOR SELECT * FROM information_schema.sequences;
END
$func$ LANGUAGE plpgsql;
BEGIN;
SELECT my_multiselect('first_cursor_to_routines', 'second_cursor_to_sequences');
FETCH ALL IN first_cursor_to_routines;
FETCH ALL IN second_cursor_to_sequences;
COMMIT;
I'm not really sure what you're doing with this, but it sounds like you just want to return a union of these distinct result sets. You can do this with a dynamic query. I'm using Postgres 9.4.
CREATE OR REPLACE FUNCTION make_query(IN p_tables text[])
RETURNS void AS
$BODY$
DECLARE
v_qry text;
v_cols text;
v_types text;
v_as text;
BEGIN
EXECUTE format('
WITH sub AS (
SELECT
table_name,
column_name,
data_type
FROM
information_schema.columns
WHERE
table_name = ANY(%L)
ORDER BY
table_name,
ordinal_position)
,sub2 AS(
SELECT
DISTINCT ON (column_name, data_type)
column_name || '' '' || data_type AS def
FROM
sub
)
SELECT
string_agg(def, '','')
FROM
sub2;
',
p_tables
) INTO v_types;
v_qry := '
CREATE OR REPLACE FUNCTION name()
RETURNS TABLE(' || v_types || ') AS
$func$';
FOR i IN 1..array_upper(p_tables, 1)
LOOP
v_as := 'tbl' || i;
EXECUTE format('
WITH sub AS (
SELECT
table_name,
column_name,
data_type
FROM
information_schema.columns
WHERE
table_name = ANY(%L)
ORDER BY
table_name,
ordinal_position)
,sub2 AS(
SELECT
DISTINCT ON (column_name, data_type)
CASE WHEN table_name = ''%I''
THEN %L || ''.'' || column_name
ELSE ''NULL::'' || data_type
END AS cols
FROM
sub
)
SELECT
string_agg(cols, '','')
FROM
sub2;
',
p_tables,
p_tables[i],
v_as
) INTO v_cols;
IF i > 1 THEN
v_qry := v_qry || '
UNION ALL';
END IF;
v_qry := v_qry || '
SELECT ' || v_cols || ' FROM ' || p_tables[i] || ' AS ' || v_as;
IF i = array_upper(p_tables, 1) THEN
v_qry := v_qry || ';';
END IF;
END LOOP;
v_qry := v_qry || '
$func$ LANGUAGE sql;
';
EXECUTE v_qry;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Sorry it looks ugly here, but this formatting helps the final product look nicer. If you're shy about executing a dynamic query like this off the bat, just replace EXECUTE v_qry; with RAISE INFO 'v_qry: %', v_qry; and it will simply print the dynamic query out in a message without executing it, so you can review what it will do once executed.
Then execute make_query() with a list of tables you want to display like this:
SELECT make_query(ARRAY['tbl_a', 'tbl_b']);
The result is that you will now have a function called name() which you can call in order to see the results of both tables at the same time, with all the union details already sorted out:
SELECT * FROM name();