Postgresql check constraint on all individual elements in array using function - arrays

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
);

Related

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: Custom Type + Arrays combination

I'm unable to find where the issue is for the below program. the values of the custom type are displaying without any errors when I use RAISE NOTICE statements at the end. When I run the final select statement, the error is Array value must start with "{" or dimension information. Please help me with the select statement on how to call the package/function.
create
or
replace TYPE t_col_foo as object
(
ID NUMBER
, CLUSTERNAME VARCHAR2(300)
, "1200AM" varchar2(10));
create
or
replace TYPE T_COL_R AS TABLE OF t_col_foo;
CREATE OR REPLACE PACKAGE foo_avail_pkg
IS
FUNCTION foo_slots
(
p_ref_data anyarray
)
RETURN t_col_r[];
END foo_avail_pkg;
CREATE OR REPLACE PACKAGE BODY foo_avail_pkg
IS
FUNCTION foo_slots
(
p_ref_data anyarray
)
RETURN t_col_r[]
IS
-- declare
r_target_data t_col_foo:=t_col_foo(null,null,null);
r_target_data_1 t_col_foo;
r_source_data text[];
t_return t_col_tab1;
BEGIN
t_return:=t_col_tab1();
select
array
(
select
unnest( p_ref_data )
)
into r_source_data
;
-- r_target_data = '{}';
for i in coalesce(array_lower(r_source_data,1),0) .. coalesce(array_upper(r_source_data,1),0)
LOOP
r_target_data.ID := substr(r_source_data[i],1,instr(r_source_data[i],',',1,1)-1);
r_target_data.CLUSTERNAME := substr(r_source_data[i],length(r_target_data.ID)+2,(instr(r_source_data[i],',',length(r_target_data.ID)+1,2) - instr(r_source_data[i],',',1,1))-1);
r_target_data."1200AM" := 3;
r_target_data_1 :=row(r_target_data.ID ,r_target_data.CLUSTERNAME,r_target_data."1200AM") :: t_col_foo;
END LOOP;
-- dbms_output.put_line(r_target_data_1);
RETURN r_target_data_1;
end;
END foo_avail_pkg;
This is how I have to call
select * from foo_avail_pkg.foo_SLOTS(array
(
select
ID
||','
||CLUSTER_NAME
||','
||LOB
from
y limit 1
));
And the error is
ERROR: malformed array literal: "(1398,Sanity20feb,3)"
DETAIL: Array value must start with "{" or dimension information.

How to CAST empty array value of an ANYARRAY-function to ANYARRAY?

I am using pgv10. The function that I need seems this wrong function:
CREATE FUNCTION array_coalesce(ANYARRAY) RETURNS ANYARRAY AS $f$
SELECT CASE WHEN $1 IS NULL THEN array[]::ANYARRAY ELSE $1 END;
$f$ language SQL IMMUTABLE;
Curiosity
... I started to simplify a complex problem, and arrives in the test select coalesce(null::text[], array[]::text[]) that not worked... So it was a good question, how to implement it? But sorry, I do something workng, COALESCE(array,array) is working fine (phew!).
So, "coalesce problem" is merely illustrative/didatic. What I really want to understand here is: How to use ANYARRAY?
PS: other curiosity, the string concat(), || and other concatenation operators in PostgreSQL do some "coalescing",
select concat(NULL::text, 'Hello', NULL::text); -- 'Hello'
select null::text[] || array[]::text[]; -- []
select array[]::text[] || null::text[]; -- []
How to use anyarray?
It's an interesting issue, in the context of the usage described in the question. The only way I know is to use an argument as a variable. It's possible in plpgsql (not in plain sql) function:
create or replace function array_coalesce(anyarray)
returns anyarray as $f$
begin
if $1 is null then
select '{}' into $1;
end if;
return $1;
end
$f$ language plpgsql immutable;
select array_coalesce(null::int[]);
array_coalesce
----------------
{}
(1 row)
By the way, you can simply use coalesce() for arrays:
select coalesce(null::text[], '{}'::text[]);
coalesce
----------
{}
(1 row)

Find different in two jsonb , PostgreSQL trigger function

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

Using return query in Postgres 9.0 function with composite type

I have a function in Postgres which return a setof composite type. When returning I am only able do it with return next; but not with return query command, why is that?
CREATE TYPE return_type AS
(paramname character varying,
value character varying);
CREATE OR REPLACE FUNCTION test(i_param1 character varying, i_param2 character varying)
RETURNS SETOF return_type AS
--this works just fine returning two rows
r.paramname:='row1';
r.value:='myvalue1';
return next r;
r.paramname:='row1';
r.value:='myvalue1';
return next r;
return;
-- with this command I do not get a single row attached in the resultset
return query
select 'row1' as paraName,'myvalue1' as value
UNION ALL
select 'row2' as paraName,'myvalue2' as value;
return;
This works for me, even in Postgres 8.4:
CREATE OR REPLACE FUNCTION test(i_param1 varchar, i_param2 varchar)
RETURNS SETOF return_type AS
$func$
BEGIN
RETURN QUERY
SELECT 'row1'::varchar, 'myvalue1'::varchar
UNION ALL
SELECT 'row2','myvalue2';
END
$func$ LANGUAGE plpgsql;
Your original should only return an exception, because of type mismatch. You need to cast the string literals to matching types. On the other hand, column aliases are irrelevant here: not visible outside the function body.
SQL Fiddle

Resources