Find different in two jsonb , PostgreSQL trigger function - database

user
CREATE TABLE IF NOT EXISTS "user"(
"id" SERIAL NOT NULL,
"create_date" timestamp without time zone NOT NULL,
"last_modified_date" timestamp without time zone,
"last_modified_by_user_id" integer,
"status" integer NOT NULL,
PRIMARY KEY ("id")
);
user_track
CREATE TABLE IF NOT EXISTS "user_track"(
"date" timestamp without time zone,
"by_user_id" integer,
"origin_value" jsonb,
"new_value" jsonb
);
function
CREATE OR REPLACE FUNCTION user_track_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_track(date, new_value)
VALUES(NEW.create_date, row_to_json(NEW)::jsonb);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION user_track_delete()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_track(date, by_user_id, origin_value)
VALUES(create_date, user_id, row_to_json(OLD)::jsonb);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION user_track_update()
RETURNS TRIGGER AS $$
DECLARE
js_new jsonb := row_to_json(NEW)::jsonb;
js_old jsonb := row_to_json(OLD)::jsonb;
BEGIN
INSERT INTO user_track(date, by_user_id, origin_value, new_value)
VALUES(NEW.create_date, OLD.id, js_old - js_new, js_new - js_old);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
trigger
CREATE TRIGGER user_track_insert_trigger AFTER INSERT ON "user"
FOR EACH ROW EXECUTE PROCEDURE user_track_insert();
CREATE TRIGGER user_track_delete_trigger AFTER DELETE ON "user"
FOR EACH ROW EXECUTE PROCEDURE user_track_delete();
CREATE TRIGGER user_track_update_trigger AFTER UPDATE ON "user"
FOR EACH ROW EXECUTE PROCEDURE user_track_update();
When I do update in pgadmin got error:
ERROR: operator does not exist: jsonb - jsonb
LINE 2: VALUES(NEW.create_date, OLD.id, js_old - js_new, js_n...
^
ERROR: operator does not exist: jsonb - jsonb
SQL state: 42883
Hint: No operator matches the given name and argument type(s). You might need to add explicit type casts.
Context: PL/pgSQL function user_track_update() line 6 at SQL statement
I only want to save json data present changed column/value not whole row all column.
I can't find jsonb - jsonb in document, so seems there is no such operator ...
How to pass parameter into user_track_delete function, for example if I want to some other use.id who execute this action ?
PostgreSQL 9.5.2

Related

Oracle: Insert only unique records into custom record in compound trigger

I am having a compound trigger in the following format. I need to ensure that my custom declaration ints_rows takes only "unique rows". Meaning if the ints_rows already has a record similar to what is being inserted, it should ignore it. More like a SET data structure. How do I do it in oracle? (I am pretty new to oracle, hence I am not great at syntax) I think I have to either change the BULK COLLECT statement or the declaration of int_records but I might be wrong. Any help/hints are much appreciated.
This is my compound trigger code.
CREATE OR REPLACE
TRIGGER MY_COMPOUND_TRIGGER
FOR UPDATE OF some_random_column ON some_random_table
COMPOUND TRIGGER
TYPE int_records IS RECORD (
column_one another_table.column_one%TYPE,
column_two another_table.column_two%TYPE
);
TYPE row_list IS TABLE OF int_records INDEX BY simple_integer;
ints_rows row_list;
BEFORE STATEMENT IS
BEGIN
ints_rows.delete;
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
SELECT column_one, column_two BULK COLLECT INTO ints_rows
FROM some_table_x WHERE some_col_id=:OLD.some_col_id;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN 1 .. ints_rows.COUNT LOOP
-- DO SOMETHING
END LOOP;
auctions_rows.delete;
END AFTER STATEMENT;
END;
This statement should not be getting duplicates at all.
SELECT column_one, column_two BULK COLLECT INTO ints_rows
FROM some_table_x WHERE some_col_id=:OLD.some_col_id;
In the AFTER EACH ROW part you overwrite entire ints_rows. In principle it could be this one:
CREATE TABLE SOME_RANDOM_TABLE (some_random_column NUMBER, some_col_id NUMBER);
CREATE TABLE ANOTHER_TABLE (column_one NUMBER, column_two NUMBER);
CREATE TABLE SOME_TABLE_X (some_col_id NUMBER, column_one NUMBER, column_two NUMBER);
CREATE OR REPLACE TYPE int_records AS OBJECT (
column_one NUMBER,
column_two NUMBER,
MAP MEMBER FUNCTION getId RETURN VARCHAR2
);
CREATE OR REPLACE TYPE BODY int_records AS
MAP MEMBER FUNCTION getId RETURN VARCHAR2 IS
BEGIN
RETURN column_one ||','|| column_two;
END getId;
END;
/
CREATE OR REPLACE TYPE row_list IS TABLE OF int_records;
CREATE OR REPLACE TRIGGER MY_COMPOUND_TRIGGER
FOR UPDATE OF some_random_column ON SOME_RANDOM_TABLE
COMPOUND TRIGGER
ints_rows row_list;
BEFORE STATEMENT IS
BEGIN
ints_rows := row_list();
END BEFORE STATEMENT;
AFTER EACH ROW IS
int_row row_list;
BEGIN
SELECT int_records(column_one, column_two)
BULK COLLECT INTO int_row
FROM SOME_TABLE_X x
WHERE x.some_col_id = :OLD.some_col_id;
ints_rows := ints_rows MULTISET UNION DISTINCT int_row;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN 1 .. ints_rows.COUNT LOOP
NULL;
END LOOP;
END AFTER STATEMENT;
END;
Please test and let us know if it does not work. You may make the records distinct manually with a loop or you need to implement a MAP MEMBER FUNCTION for the RECORD.

How do I dynamically pass a role name to IS_ROLE_IN_SESSION?

I'm setting up a masking policy that can be bypassed if the user's current role inherits from a specified role. This can be easily done with the function IS_ROLE_IN_SESSION. The challenge is I want to be able to change the specified role without having to modify the masking policy.
These examples assume the user is using a role other than ACCOUNTADMIN.
I got it to work with a session variable, but this is not secure since I can't control access to session variables:
create or replace table tab as select * from values('personal value') d (data);
set unmask_role = 'PUBLIC';
alter table tab modify column data unset masking policy;
create or replace masking policy hide as (d varchar) returns varchar ->
iff(is_role_in_session($unmask_role),d,replace(d,'personal value','hidden'));
alter table tab modify column data set masking policy hide;
set unmask_role = 'PUBLIC';
select * from tab;
-- Works as expected: shows personal value
set unmask_role = 'ACCOUNTADMIN';
select * from tab;
-- Works as expected: shows hidden
Ideally I would provide the role in a table since I can control access to the contents of a table but I can't get past these errors:
create or replace table unmask_role_tab as select 'PUBLIC' role;
alter table tab modify column data unset masking policy;
create or replace masking policy hide as (d varchar) returns varchar ->
iff(is_role_in_session((select role from unmask_role_tab)),d,replace(d,'personal value','hidden'));
alter table tab modify column data set masking policy hide;
select * from tab;
-- Fails with error:
-- SQL compilation error: error line Check Arg at position 0 invalid argument for function [IS_ROLE_IN_SESSION] unexpected argument [(SELECT UNMASK_ROLE_TAB.ROLE AS "ROLE" FROM UNMASK_ROLE_TAB AS UNMASK_ROLE_TAB)] at position 0,
alter table tab modify column data unset masking policy;
create or replace masking policy hide as (d varchar) returns varchar ->
(select iff(is_role_in_session(role),d,replace(d,'personal value','hidden')) from unmask_role_tab);
alter table tab modify column data set masking policy hide;
select * from tab;
-- Fails with error:
-- SQL compilation error: error line Check Arg at position 0 invalid argument for function [IS_ROLE_IN_SESSION] unexpected argument [UNMASK_ROLE_TAB.ROLE] at position 0,
It is an interesting question as it boils down to how to pass a "non-static" value to function that requires string_literal
IS_ROLE_IN_SESSION
is_role_in_session( '<string_literal>' )
Using view instead of table(if new entries has to be added then view defintion has to be updated, without changing masking policy definition):
create or replace table tab as select * from values('personal value') d (data);
CREATE OR REPLACE VIEW unmask_role_view
AS
SELECT 1 AS col WHERE IS_ROLE_IN_SESSION('PUBLIC')
-- UNION SELECT 1 AS col WHERE IS_ROLE_IN_SESSION('...') -- more entries
;
create or replace masking policy hide as (d varchar) returns varchar ->
case when exists(SELECT 1 FROM unmask_role_view) then d
else replace(d,'personal value','hidden')
end;
alter table tab modify column data set masking policy hide;
select * from tab;
A solution that requires defining all roles that should have access to data. It has one advantage though the roles are listed explicitly. One of the drawbacks is maintenance of this table.
create or replace table tab as select * from values('personal value') d (data);
create or replace table unmask_role_tab as select 'PUBLIC' role;
-- here we compare against CURRENT_ROLE
-- so we need all roles that have access to masked data
create or replace masking policy hide as (d varchar) returns varchar ->
case when exists(SELECT 1 FROM unmask_role_tab u WHERE u.role = CURRENT_ROLE()) then d
else replace(d,'personal value','hidden')
end;
alter table tab modify column data set masking policy hide;
select * from tab;
CREATE MASKING POLICY
CREATE [ OR REPLACE ] MASKING POLICY [ IF NOT EXISTS ] <name> AS
(VAL <data_type>) RETURNS <data_type> -> <expression_ON_VAL>
You can use:
Conditional Expression Functions
Context Functions,
and UDFs to write the SQL expression.
Attempt 1: Standard call
SELECT IS_ROLE_IN_SESSION(u.role) FROM unmask_role_tab u;
-- SQL compilation error: error line Check Arg at position 0 invalid argument
-- for function [IS_ROLE_IN_SESSION] unexpected argument [U.ROLE] at position 0
SELECT IS_ROLE_IN_SESSION(u.role::STRING) FROM unmask_role_tab u;
-- SQL compilation error: error line Check Arg at position 0 invalid argument
-- for function [IS_ROLE_IN_SESSION] unexpected argument [U.ROLE] at position 0
Attempt 2: Create UDF(executiing build SQL is not available)
CREATE OR REPLACE FUNCTION role_check(role_name STRING)
RETURNS boolean
LANGUAGE JAVASCRIPT
AS
$$
var res = snowflake.createStatement({sqlText: 'SELECT IS_ROLE_IN_SESSION(:1)'
, binds:[ROLE_NAME]}).execute()
res.next();
return res.getColumnValue(1);
$$;
SELECT role_check(u.role) FROM unmask_role_tab u;
-- JavaScript execution error: Uncaught ReferenceError:
-- snowflake is not defined in ROLE_CHECK
Attempt 3 SQL UDF(same error like with direct call
CREATE OR REPLACE FUNCTION role_check(role_name STRING)
RETURNS BOOLEAN
LANGUAGE SQL
AS $$
IS_ROLE_IN_SESSION(ROLE_NAME)
$$;
SELECT *, role_check(role) FROM unmask_role_tab;
-- SQL compilation error: error line Check Arg at position 0 invalid argument
-- for function [IS_ROLE_IN_SESSION] unexpected argument [UNMASK_ROLE_TAB.ROLE]
Attempt 4 User-Defined stored procedure:
CREATE OR REPLACE PROCEDURE role_check_proc(role_name STRING)
RETURNS boolean
LANGUAGE JAVASCRIPT
AS
$$
var res = snowflake.createStatement({sqlText: 'SELECT IS_ROLE_IN_SESSION(:1)'
,binds:[ROLE_NAME]}).execute()
res.next();
return res.getColumnValue(1);
$$;
CALL role_check_proc((SELECT role FROM unmask_role_tab));
-- TRUE
-- Works only if table contains single entry
It returns result but stored procedure call cannot be used in masking policy/SQL query call.
Wrapping them with function will not work as it is not possible to call SP from function.
CREATE OR REPLACE FUNCTION role_check(role_name STRING)
RETURNS BOOLEAN
LANGUAGE SQL
AS $$
CALL role_check_proc(ROLE_NAME::STRING)
$$;

Postgresql check constraint on all individual elements in array using function

If I have a table
create table foo ( bar text[] not null check ..... );
and a function
create function baz(text) returns boolean as $$ .....
How do I add a check constraint to the foo table such that every element in the bar field validates the baz function?
I'm thinking that I need to create a function
create function array_baz(arg text[]) returns boolean as $$
with x as ( select baz(unnest(arg)) as s_arg )
select not exists (select 1 from x where s_arg = false)
$$ language sql strict immutable;
create table foo (bar text[] not null check ( array_baz(bar) = true ) );
However, I'm sure that I'm reinventing the wheel here and there's a cuter way of doing this. What psql trick am I missing? A map function would be nice
create table foo (bar text[] not null check (true = all(map('baz', bar)));
but so far my search efforts are fruitless.
You can do what you want in more than one way. If you want to use the ALL(...) quantifiers, you need a suitable operator. For that, you first need a function to perform what you want:
Imagine you want to check that your texts don't have any uppercase letter in them. You'd define a function like:
CREATE FUNCTION doesnt_have_uppercase(b boolean, t text)
/* Compares b to the result of `t` not having any non-lowercase character */
RETURNS boolean
IMMUTABLE
STRICT
LANGUAGE SQL
AS
$$
SELECT (t = lower(t)) = b
$$ ;
Based on it, create an operator:
CREATE OPERATOR =%= (
PROCEDURE = doesnt_have_uppercase,
LEFTARG = boolean,
RIGHTARG = text
) ;
You need this operator because the ANY and ALL quantifiers need the following structure:
expression operator ALL(array)
At this point, you can define:
create table foo
(
bar text[] not null,
CONSTRAINT bar_texts_cant_have_uppercase CHECK(true =%= ALL(bar))
);
Which will lead you to the following behaviour:
INSERT INTO foo
(bar)
VALUES
(ARRAY['this will pass', 'this too']) ;
1 rows affected
INSERT INTO foo
(bar)
VALUES
(ARRAY['that would pass', 'BUT THIS WILL PREVENT IT']) ;
ERROR: new row for relation "foo" violates check constraint "bar_texts_cant_have_uppercase"
DETAIL: Failing row contains ({"that would pass","BUT THIS WILL PREVENT IT"}).
Check it all at
dbfiddle here
I would most probably seek a less tortuous route, however:
CREATE FUNCTION doesnt_have_uppercase(t text[])
/* Returns true if all elements of t don't have any uppercase letter */
RETURNS boolean
IMMUTABLE
STRICT
LANGUAGE SQL
AS
$$
SELECT (NOT EXISTS (SELECT 1 FROM unnest(t) q WHERE q <> lower(q)))
$$ ;
create table foo
(
bar text[] not null,
CONSTRAINT bar_texts_cant_have_uppercase CHECK(doesnt_have_uppercase(bar))
);
This behaves exactly like the previous example (except if some of the elements of the array are NULL).
dbfiddle here
Check constraints can be applied to individual items in an array by defining a domain type:
CREATE DOMAIN lower_text AS text
CHECK(
VALUE = lower(VALUE)
);
CREATE TABLE test (
name lower_text[] NOT NULL
);

Oracle => PostgreSQL: Array of %ROWTYPE?

Is there any way in PostgreSQL to declare local type "TABLE OF ..%ROWTYPE INDEX BY BINARY_INTEGER" inside a function like in Oracle?
CREATE OR REPLACE FUNCTION FNC
RETURN NUMBER
AS
TYPE TYPE_TB IS TABLE OF ADM_APPLICATIONS%ROWTYPE
INDEX BY BINARY_INTEGER;
TB_VAR TYPE_TB;
BEGIN
return 1;
END;
For every table there is also a corresponding type (with the same name) available.
So you can do the following:
CREATE OR REPLACE FUNCTION fnc()
RETURNs integer
AS
$$
declare
tb_var adm_applications[];
begin
return 1;
end;
$$
language plpgsql;

PostgreSQL full-text search with arrays

I would like to implement full-text search within my application but I'm running into some roadblocks associated with my Array-type columns. How would one implement a psql trigger so that the when my "object" table is updated, each element (which are strings) of its array column is added to the tsvector column of my "search" table?
In Postgres 9.6 array_to_tsvector was added.
If you are dealing with same table you can write it something like this.
CREATE FUNCTION tsv_trigger() RETURNS trigger AS $$
begin
IF (TG_OP = 'INSERT') OR old.array_column <> new.array_column THEN
new.tsv := array_to_tsvector( new.array_column);
END IF;
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON my_table FOR EACH ROW EXECUTE PROCEDURE tsv_trigger();
If you are dealing with two tables than you need to write update
CREATE FUNCTION cross_tables_tsv_trigger() RETURNS trigger AS $$
begin
IF (TG_OP = 'INSERT') OR old.array_column <> new.array_column THEN
UPDATE search_table st
SET tsv = array_to_tsvector( new.array_column )
WHERE st.id = new.searchable_record_id
END IF;
# you can't return NULL because you'll break the chain
return new;
end
$$ LANGUAGE plpgsql;
Pay attention that it will differ from default to_tsvector( array_to_string() ) combination.
It goes without position numbers, and lowercase normalization so you can get a unexpected results.

Resources