PostgresQL allows you to INSERT with two lists, one of field names, the other of values.
INSERT INTO products (product_no, name, price) VALUES (1, 'Cheese', 9.99);
For long lists, it gets hard to figure out which list index you're on. Is there a way to insert by specifying the column name alongside the value, ie key-value pairs? Note: This is different than hstore.
ie.
INSERT INTO products (product_no => 1, name => 'Cheese', price => 9.99);
It is impossible for regular DML.
As an alternative:
Use list of values to make DML shorter:
INSERT INTO products (product_no, name, price) VALUES
(1, 'Cheese', 9.99),
(2, 'Sausages', 9.99),
...;
Or create function that you can execute with parameter specifying:
create or replace function insert_product(
in product_no products.product_no%type,
in name products.name%type,
in price products.price%type) returns products.product_no%type as $$
insert into products(product_no, name, price) values (product_no, name, price) returning product_no;
$$ language sql;
select insert_product(1, 'Mashrooms', 1.99); -- Parameters by order
select insert_product(product_no := 2, name := 'Cheese', price := 9.99); -- Parameters by name
select insert_product(product_no := 3, price := 19.99, name := 'Sosages'); -- Order does mot matter
I have one created for my use. The below takes json as input and creates dynamic SQL query and executes the same.
Sample Usage
create table employee(name character varying(20), address character varying(100), basic integer);
--sample call-1
call insert_into(true, 'employee', '{
"name" : "''Ravi Kumar''",
"address" : "''#1, 2nd Cross, Bangalore''",
"basic" : 35000
}',
'');
--sample call-2
call insert_into(true, 'employee', '{
"name" : "eo.name",
"address" : "eo.address",
"basic" : "eo.basic"
}',
'
from employee_old eo
');
Proceedure
CREATE or REPLACE PROCEDURE insert_into(
debug BOOLEAN,
tableName TEXT,
jsonTxt json,
fromWhere TEXT
)
LANGUAGE plpgsql
as $$
DECLARE
field TEXT;
fieldQuery TEXT;
valueQuery TEXT;
finalQuery TEXT;
noOfRecords INT;
BEGIN
IF debug THEN
raise notice 'preparing insert query';
END IF;
fieldQuery := CONCAT('INSERT INTO ', tableName, '(', E'\n');
valueQuery := CONCAT('SELECT ', E'\n');
FOR field IN SELECT * FROM json_object_keys(jsonTxt)
LOOP
fieldQuery := CONCAT(fieldQuery, field, E',\n');
valueQuery := CONCAT(valueQuery, json_extract_path_text(jsonTxt, field), E',\n');
END LOOP;
fieldQuery := RTRIM(fieldQuery, E',\n');
fieldQuery := CONCAT(fieldQuery, ')');
valueQuery := RTRIM(valueQuery, E',\n');
finalQuery := CONCAT(fieldQuery, E'\n', valueQuery, E'\n', fromWhere, ';');
IF debug THEN
RAISE NOTICE 'query:: %', finalQuery;
END IF;
EXECUTE finalQuery;
get diagnostics noOfRecords = row_count;
RAISE NOTICE 'Inserted:: %', noOfRecords;
END
$$;
Related
When performing a loop on a table in which a value has ", I get those quotes doubled such as:
initial input: 'test AND "test"'
output in the loop: 'test AND ""test""'
How to reproduce:
CREATE TABLE roles(
id int,
criteria VARCHAR (255)
);
INSERT INTO roles (id, criteria) VALUES (1, 'test AND "test"');
CREATE OR REPLACE FUNCTION test_quote()
RETURNS VARCHAR
LANGUAGE plpgsql
AS $$
DECLARE
sample varchar;
rec record;
BEGIN
FOR rec IN
SELECT * FROM roles
LOOP
sample:= rec;
END LOOP;
RETURN sample;
END
$$;
SELECT * FROM test_quote();
Expected result: (1,"test AND "test"")
Actual result: (1,"test AND ""test""")
Does anyone have an idea how to get the expected behaviour here?
Found it, indeed, the issue is from the conversion from record to varchar.
I can directly pass the column from the rec like:
sample:= rec.criteria;
(I didn't know that we could do that)
So, that gives:
CREATE TABLE roles(
id int,
criteria VARCHAR (255)
);
INSERT INTO roles (id, criteria) VALUES (1, 'test AND "test"');
CREATE OR REPLACE FUNCTION test_quote()
RETURNS VARCHAR
LANGUAGE plpgsql
AS $$
DECLARE
sample varchar;
rec record;
BEGIN
FOR rec IN
SELECT * FROM roles
LOOP
sample := rec.criteria;
END LOOP;
RETURN sample;
END
$$;
SELECT * FROM test_quote();
Thx guys!
I would like execute insert before I update some rows, insert would contain information about which table is updated, which column is updated, what query is executed etc..
And I wrote something like this:
DO $$
DECLARE _new_product_id bigint := 1;
_update_by_product_id bigint:= 2;
-- BEGIN TRANSACTION
BEGIN
DECLARE product_sql_query text := 'UPDATE products SET product_id =' + _new_product_id + 'WHERE product_id =' + _update_by_product_id + ';'
INSERT INTO products_audit (ordinal_number, table_name, column_name, action_applied, query_executed, value_before_update, value_after_update, created_date )
VALUES (1, 'products', 'product_id', 'UPDATE', users_query, 1, 1, NOW());
-- END TRANSACTION
COMMIT;
END;
$$
But I'm receiving an syntax error which says:
ERROR: syntax error at or near "INSERT" LINE 9: INSERT INTO
products_audit (ordinal_number, table...
What's wrong here, I guess product_sql_query value is not correct?
The DECLARE block needs to go before the first BEGIN of the PL/pgSQL - which is not the same as a BEGIN TRANSACTION. You can't control transactions in an anonymous PL/pgSQL block.
It's unclear to me what you intend with the product_sql_query variable, but removing all the syntax errors, the block should look like this:
DO $$
DECLARE
_new_product_id bigint := 1;
_update_by_product_id bigint:= 2;
product_sql_query text;
BEGIN
product_sql_query := 'UPDATE products SET product_id = ' || _new_product_id::text ||' WHERE product_id = ' || _update_by_product_id::text;
INSERT INTO products_audit
(ordinal_number, table_name, column_name, action_applied, query_executed, value_before_update, value_after_update, created_date)
VALUES
(1, 'products', 'product_id', 'UPDATE', users_query, 1, 1, NOW());
END;
$$
I am trying to create a function on Postgres 10 that would check whether all the given ids exist
in the target table.
CREATE OR REPLACE FUNCTION "public"."has_mismatching_ids"(
_ids TEXT[],
_column_name TEXT,
_table_name TEXT,
_schema_name TEXT DEFAULT 'public'
)
RETURNS BOOLEAN AS
$$
DECLARE
result TEXT[] := '{}';
_query TEXT := '';
BEGIN
IF _ids IS NULL THEN
RETURN false;
END IF;
_query := CONCAT('SELECT UNNEST($1) EXCEPT SELECT "', _column_name, '" FROM "', _schema_name ,'"."', _table_name ,'"');
EXECUTE _query INTO result USING $1;
RAISE NOTICE 'Executed';
IF array_length(result, 1) > 0 THEN
RETURN true;
END IF;
RETURN false;
END;
$$
LANGUAGE plpgsql
STABLE
SECURITY INVOKER;
This is my function that returns correctly true if all the ids exist, but it throws an exception if any of
the ids does not exist.
CREATE TABLE "public"."categories" ("category_id" TEXT NOT NULL PRIMARY KEY);
INSERT INTO "public"."categories" ("category_id") VALUES ('foo');
INSERT INTO "public"."categories" ("category_id") VALUES ('bar');
This works
SELECT FROM "public"."has_mismatching_ids"(ARRAY['foo'], 'category_id', 'categories', 'public');
NOTICE: Executed
has_mismatching_ids
---------------------
f
(1 row)
but this fails to even
SELECT FROM "public"."has_mismatching_ids"(ARRAY['foo2'], 'category_id', 'categories', 'public');
ERROR: malformed array literal: "foo2"
DETAIL: Array value must start with "{" or dimension information.
CONTEXT: PL/pgSQL function public.has_mismatching_ids(text[],text,text,text) line 14 at EXECUTE
and strangely this works also if I write a function that uses explicit names and without EXECUTE
SELECT UNNEST(ARRAY['foo', 'foo3']) EXCEPT SELECT "category_id" FROM "public"."categories";
unnest
--------
foo3
(1 row)
Why doesn't my dynamic function work when comparing to values that do not exist in the target table?
You may use a simple SQL to compare if all array element exists.
CREATE OR replace FUNCTION "public"."has_mismatching_ids"(
_ids text[],
_column_name text,
_table_name text,
_schema_name text DEFAULT 'public' )
returns boolean AS $$
DECLARE res BOOLEAN;
BEGIN
IF _ids IS NULL then
RETURN false;
END IF;
EXECUTE format ( 'SELECT COUNT(*) = cardinality($1)
FROM %I.%I WHERE %I = ANY($2)',
_schema_name,_table_name,_column_name )
INTO res using _ids, _ids;
RETURN res;
END;
$$ language plpgsql stable security invoker;
Demo
I would like to use a function/procedure to add to a 'template' table an additional column (e.g. period name) with multiple values, and do a cartesian product on the rows, so my 'template' is duplicated with the different values provided for the new column.
E.g. Add a period column with 2 values to my template_country_channel table:
SELECT *
FROM unnest(ARRAY['P1', 'P2']) AS prd(period)
, template_country_channel
ORDER BY period DESC
, sort_cnty
, sort_chan;
/*
-- this is equivalent to:
(
SELECT 'P2'::text AS period
, *
FROM template_country_channel
) UNION ALL (
SELECT 'P1'::text AS period
, *
FROM template_country_channel
)
--
*/
This query is working fine, but I was wondering if I could turn that into a PL/pgSQL function/procedure, providing the new column values to be added, the column to add the extra column to (and optionally specify the order by conditions).
What I would like to do is:
SELECT *
FROM template_with_periods(
'template_country_channel' -- table name
, ARRAY['P1', 'P2'] -- values for the new column to be added
, 'period DESC, sort_cnty, sort_chan' -- ORDER BY string (optional)
);
and have the same result as the 1st query.
So I created a function like:
CREATE OR REPLACE FUNCTION template_with_periods(template regclass, periods text[], order_by text)
RETURNS SETOF RECORD
AS $BODY$
BEGIN
RETURN QUERY EXECUTE 'SELECT * FROM unnest($2) AS prd(period), $1 ORDER BY $3' USING template, periods, order_by ;
END;
$BODY$
LANGUAGE 'plpgsql'
;
But when I run:
SELECT *
FROM template_with_periods('template_country_channel', ARRAY['P1', 'P2'], 'period DESC, sort_cnty, sort_chan');
I have the error ERROR: 42601: a column definition list is required for functions returning “record”
After some googling, it seems that I need to define the list of columns and types to perform the RETURN QUERY (as the error message precisely states).
Unfortunately, the whole idea is to use the function with many 'template' tables, so columns name & type lists is not fixed.
Is there any other approach to try?
Or is the only way to make it work is to have within the function, a way to get list of columns' names and types of the template table?
I did this with refcursor if You want output columns list completely dynamic:
CREATE OR REPLACE FUNCTION is_record_exists(tablename character varying, columns character varying[], keepcolumns character varying[] DEFAULT NULL::character varying[])
RETURNS SETOF refcursor AS
$BODY$
DECLARE
ref refcursor;
keepColumnsList text;
columnsList text;
valuesList text;
existQuery text;
keepQuery text;
BEGIN
IF keepcolumns IS NOT NULL AND array_length(keepColumns, 1) > 0 THEN
keepColumnsList := array_to_string(keepColumns, ', ');
ELSE
keepColumnsList := 'COUNT(*)';
END IF;
columnsList := (SELECT array_to_string(array_agg(name || ' = ' || value), ' OR ') FROM
(SELECT unnest(columns[1:1]) AS name, unnest(columns[2:2]) AS value) pair);
existQuery := 'SELECT ' || keepColumnsList || ' FROM ' || tableName || ' WHERE ' || columnsList;
RAISE NOTICE 'Exist query: %', existQuery;
OPEN ref FOR EXECUTE
existQuery;
RETURN next ref;
END;$BODY$
LANGUAGE plpgsql;
Then need to call FETCH ALL IN to get results. Detailed syntax here or there: https://stackoverflow.com/a/12483222/630169. Seems it is the only way for now. Hope something will be changed in PostgreSQL 11 with PROCEDURES.
With PostgreSQL(v9.5), the JSONB formats give awesome opportunities. But now I'm stuck with what seems like a relatively simple operation;
compare two jsonb objects; see what is different or missing in one document compared to the other.
What I have so far
WITH reports(id,DATA) AS (
VALUES (1,'{"a":"aaa", "b":"bbb", "c":"ccc"}'::jsonb),
(2,'{"a":"aaa", "b":"jjj", "d":"ddd"}'::jsonb) )
SELECT jsonb_object_agg(anon_1.key, anon_1.value)
FROM
(SELECT anon_2.key AS KEY,
reports.data -> anon_2.KEY AS value
FROM reports,
(SELECT DISTINCT jsonb_object_keys(reports.data) AS KEY
FROM reports) AS anon_2
ORDER BY reports.id DESC) AS anon_1
Should return the difference of row 1 compared to row 2:
'{"b":"bbb", "c":"ccc", "d":null}'
Instead it returns also duplicates ({"a": "aaa"}). Also; there might be a more elegant approach in general!
UPDATED
CREATE OR REPLACE FUNCTION jsonb_diff_val(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
result JSONB;
v RECORD;
BEGIN
result = val1;
FOR v IN SELECT * FROM jsonb_each(val2) LOOP
IF result #> jsonb_build_object(v.key,v.value)
THEN result = result - v.key;
ELSIF result ? v.key THEN CONTINUE;
ELSE
result = result || jsonb_build_object(v.key,'null');
END IF;
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql;
Query:
SELECT jsonb_diff_val(
'{"a":"aaa", "b":"bbb", "c":"ccc"}'::jsonb,
'{"a":"aaa", "b":"jjj", "d":"ddd"}'::jsonb
);
jsonb_diff_val
---------------------------------------
{"b": "bbb", "c": "ccc", "d": "null"}
(1 row)
I have created similar function that would scan the object recursively and will return the difference between new object and old object. I was not able to find a 'nicer' way to determine if jsonb object 'is empty' - so would be grateful for any suggestion how to simplify that. I plan to use it to keep track of updates made to the jsonb objects, so I store only what have changed.
Here is the function:
CREATE OR REPLACE FUNCTION jsonb_diff_val(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
result JSONB;
object_result JSONB;
i int;
v RECORD;
BEGIN
IF jsonb_typeof(val1) = 'null'
THEN
RETURN val2;
END IF;
result = val1;
FOR v IN SELECT * FROM jsonb_each(val1) LOOP
result = result || jsonb_build_object(v.key, null);
END LOOP;
FOR v IN SELECT * FROM jsonb_each(val2) LOOP
IF jsonb_typeof(val1->v.key) = 'object' AND jsonb_typeof(val2->v.key) = 'object'
THEN
object_result = jsonb_diff_val(val1->v.key, val2->v.key);
-- check if result is not empty
i := (SELECT count(*) FROM jsonb_each(object_result));
IF i = 0
THEN
result = result - v.key; --if empty remove
ELSE
result = result || jsonb_build_object(v.key,object_result);
END IF;
ELSIF val1->v.key = val2->v.key THEN
result = result - v.key;
ELSE
result = result || jsonb_build_object(v.key,v.value);
END IF;
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql;
Then simple query looks like this:
SELECT jsonb_diff_val(
'{"a":"aaa", "b":{"b1":"b","b2":"bb","b3":{"b3a":"aaa","b3c":"ccc"}}, "c":"ccc"}'::jsonb,
'{"a":"aaa", "b":{"b1":"b1","b3":{"b3a":"aaa","b3c":"cccc"}}, "d":"ddd"}'::jsonb
);
jsonb_diff_val
-------------------------------------------------------------------------------
{"b": {"b1": "b1", "b2": null, "b3": {"b3c": "cccc"}}, "c": null, "d": "ddd"}
(1 row)
Here is a solution without creating a new function;
SELECT
json_object_agg(COALESCE(old.key, new.key), old.value)
FROM json_each_text('{"a":"aaa", "b":"bbb", "c":"ccc"}') old
FULL OUTER JOIN json_each_text('{"a":"aaa", "b":"jjj", "d":"ddd"}') new ON new.key = old.key
WHERE
new.value IS DISTINCT FROM old.value
The result is;
{"b" : "bbb", "c" : "ccc", "d" : null}
This method only compares first level of json. It does NOT traverse the whole object tree.
My solution is not recursive but you can use it for detecting common key/values:
-- Diff two jsonb objects
CREATE TYPE jsonb_object_diff_result AS (
old jsonb,
new jsonb,
same jsonb
);
CREATE OR REPLACE FUNCTION jsonb_object_diff(in_old jsonb, in_new jsonb)
RETURNS jsonb_object_diff_result AS
$jsonb_object_diff$
DECLARE
_key text;
_value jsonb;
_old jsonb;
_new jsonb;
_same jsonb;
BEGIN
_old := in_old;
_new := in_new;
FOR _key, _value IN SELECT * FROM jsonb_each(_old) LOOP
IF (_new -> _key) = _value THEN
_old := _old - _key;
_new := _new - _key;
IF _same IS NULL THEN
_same := jsonb_build_object(_key, _value);
ELSE
_same := _same || jsonb_build_object(_key, _value);
END IF;
END IF;
END LOOP;
RETURN (_old, _new, _same);
END;
$jsonb_object_diff$
LANGUAGE plpgsql;
Result can look like this:
SELECT * FROM jsonb_object_diff(
'{"a": 1, "b": 5, "extra1": "woo", "old_null": null, "just_null": null}'::jsonb,
'{"a": 1, "b": 4, "extra2": "ahoj", "new_null": null, "just_null": null}'::jsonb);
-[ RECORD 1 ]--------------------------------------
old | {"b": 5, "extra1": "woo", "old_null": null}
new | {"b": 4, "extra2": "ahoj", "new_null": null}
same | {"a": 1, "just_null": null}
This does basically the same as what other folks have done, but reports changes in a right/left "delta" format.
Example:
SELECT jsonb_delta(
'{"a":"aaa", "b":"bbb", "c":"ccc"}'::jsonb,
'{"a":"aaa", "b":"jjj", "d":"ddd"}'::jsonb
);
Resolves to:
{"b": {"left": "bbb", "right": "jjj"}}
code:
CREATE OR REPLACE FUNCTION jsonb_delta(
IN json_left JSONB
, IN json_right JSONB
, OUT json_out JSONB
) AS
$$
BEGIN
IF json_left IS NULL OR json_right IS NULL THEN
RAISE EXCEPTION 'Non-null inputs required';
END IF
;
WITH
base as
(
SELECT
key
, CASE
WHEN a.value IS DISTINCT FROM b.value THEN jsonb_build_object('left', a.value, 'right', b.value)
ELSE NULL
END as changes
FROM jsonb_each_text(json_left) a
FULL OUTER JOIN jsonb_each_text(json_right) b using (key)
)
SELECT
jsonb_object_agg(key,changes)
INTO json_out
FROM base
WHERE
changes IS NOT NULL
;
json_out := coalesce(json_out, '{}');
END;
$$
LANGUAGE PLPGSQL
IMMUTABLE
PARALLEL SAFE
;
I achieved this by using the hstore extension.
CREATE EXTENSION hstore;
CREATE OR REPLACE FUNCTION log_history() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
hs_new hstore;
hs_old hstore;
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO history(item_id, old_values, new_values)
VALUES(OLD.id, row_to_json(OLD)::jsonb, NULL);
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO history(item_id, old_values, new_values)
VALUES(NEW.id, NULL, row_to_json(NEW)::jsonb);
ELSIF (TG_OP = 'UPDATE' AND NEW.* IS DISTINCT FROM OLD.*) THEN
hs_new := hstore(NEW);
hs_old := hstore(OLD);
INSERT INTO history(item_id, old_values, new_values)
VALUES(NEW.id, (hs_old - hs_new - 'updated_at'::text)::jsonb, (hs_new - hs_old - 'updated_at'::text)::jsonb);
END IF;
RETURN NULL;
END;
$$;
Notice that I used it to log history of any change happens on a specific table, and also I'm removing updated_at from the diff object.