Can I call a user-defined function from a column CHECK constraint? - sql

I have a user-defined SQL function that returns 1 or 0 and I want to call it from a column CHECK constraint.

Yes. SQL Anywhere doesn't have a boolean data type so you have to code a predicate that yields TRUE, FALSE or UNKNOWN. In other words, if your function returns 1 or 0 for pass or fail, you have to code the constraint as CHECK ( f() = 1 ).
Note that TRUE and UNKNOWN both result in a "pass"; only a FALSE result causes the check to fail.
The following sample shows how to ALTER a table that already contains data, to add a column with such a CHECK constraint.
Breck
CREATE TABLE t (
pkey INTEGER NOT NULL PRIMARY KEY );
INSERT t VALUES ( 1 );
COMMIT;
CREATE FUNCTION is_filled_in (
IN #value VARCHAR ( 100 ) )
RETURNS TINYINT
BEGIN
IF COALESCE ( #value, '' ) <> '' THEN
RETURN 1;
ELSE
RETURN 0;
END IF;
END;
ALTER TABLE t ADD c VARCHAR ( 3 ) DEFAULT 'xxx'
CHECK ( is_filled_in ( c ) = 1 );
-- Works ok...
INSERT t VALUES ( 2, 'yyy' );
COMMIT;
-- Throws SQLCODE -209 Invalid value for column 'c' in table 't'...
INSERT t VALUES ( 3, '' );
COMMIT;
SELECT * FROM t;

Related

Postgres: reading the results of 'returning *' into a variable

All the examples I have found for the Postgres 'returning' functionality (https://www.postgresql.org/docs/current/dml-returning.html) return values for a single row.
How do I read multiple result rows into a variable?
Executing the following outside a function gives the desired results:
create sequence core.test_id_seq start with 10000;
create table core.test (
test_id integer not null default nextval('core.test_id_seq'),
field integer not null
);
insert into core.test ( field )
select unnest( array[1, 2] ) as id
returning *
;
test_id | field
---------+-------
10000 | 1
10001 | 2
(2 rows)
But I want to read the results into a variable or table to work with:
do $$
declare
recs ??;
begin
create sequence core.test_id_seq start with 10000;
create table core.test (
test_id integer not null default nextval('core.test_id_seq'),
field integer not null
);
insert into core.test ( field )
select unnest( array[1, 2] ) as id
returning * into recs
;
end $$;
Is this possible?
Thanks
You need to use an array of integers:
do $$
declare
new_ids int[];
begin
with new_rows as (
insert into core.test ( field )
select unnest( array[1, 2] ) as id
returning *
)
select array_agg(field)
into new_ids
from new_rows;
... work with the new_ids array ...
end
$$;

Checking whether a string contains any numeric values

I have the following table:
create table students
(
stuName varchar2(100),
cgpa number
);
My goal is to create a PL/SQL trigger that would fire if anyone tries to enter a name that contains any numeric values. My attempt:
create or replace trigger invalid_name
before insert
on students
for each row
declare
vName varchar2(100);
begin
vName := :new.stuName;
if upper(vName) like upper(vName) then
vName := initcap(vName);
end if;
exception
when value_error then
dbms_output.put_line('ERROR: Name contains numeric value(s).');
end;
I thought if the upper function were to act on a string containing any numeric value in it, it would throw an exception. But that's not happening and insert action is being executed.
I'd suggest using a constraint rather than a trigger.
create table foo (
name varchar2(100) NOT NULL
constraint name_non_numeric check ( not regexp_like( name, '[0-9]' ) )
);
Table created.
insert into foo ( name ) values ( 'Andy' );
1 row created.
> insert into foo ( name ) values ( 'Logan 5' );
insert into foo ( name ) values ( 'Logan 5' )
*
ERROR at line 1:
ORA-02290: check constraint (NAMESPACE.NAME_NON_NUMERIC) violated
If you don't want to replace, but check and raise an error, you can use this trick and raise an error if the result is not null:
SELECT LENGTH(TRIM(TRANSLATE('123b', ' +-.0123456789',' '))) FROM dual;
Result: 1
SELECT LENGTH(TRIM(TRANSLATE('a123b', ' +-.0123456789',' '))) FROM dual;
Result: 2
SELECT LENGTH(TRIM(TRANSLATE('1256.54', ' +-.0123456789',' '))) FROM dual;
Result: null
SELECT LENGTH(TRIM(TRANSLATE ('-56', ' +-.0123456789',' '))) FROM dual;
Result: null

How to check 'any or limited' in the WHERE clause of a query?

I have this plpgsql function:
CREATE OR REPLACE FUNCTION func_example(
IN check_ids INT [],
OUT recs REFCURSOR,
OUT po_result_code TEXT
) RETURNS RECORD LANGUAGE plpgsql AS $$
BEGIN
OPEN recs FOR
SELECT *
FROM my_table t
JOIN my_another_table tt on tt.tid = t.id
WHERE t.enabled = TRUE
AND tt.some = 1
AND (
check_ids IS NULL OR check_ids.count = 0 /* <-- problem here */
OR t.id = ANY (check_ids)
);
po_result_code := 0;
RETURN;
END;
$$;
Calling it results in the error message:
Error: [42P01] ERROR: missing FROM-clause entry for table "check_ids"
SQL state: 42P01
How to check 'argument is null or value in argument'?
Some sample data:
CREATE TABLE my_table (
id INT,
enabled BOOLEAN DEFAULT TRUE
);
CREATE TABLE my_another_table (
tid INT,
"some" INT DEFAULT 1,
CONSTRAINT t_another_to_my_fk FOREIGN KEY (tid) REFERENCES my_table (id)
);
INSERT INTO my_table (id, enabled) VALUES (1, TRUE);
INSERT INTO my_another_table (tid, "some") VALUES (1, 1);
To also let NULL and empty array pass, replace:
(t.id = ANY (check_ids) OR check_ids IS NULL OR check_ids.count = 0) -- illegal syntax
with the single expression:
(t.id = ANY (check_ids) OR (check_ids = '{}') IS NOT FALSE)
This would work, too, but a bit slower:
(t.id = ANY (check_ids) OR check_ids IS NULL OR check_ids = '{}')
Closely related answer for string types with detailed explanation:
Best way to check for "empty or null value"
However, your actual question asks for something different:
How to check 'argument is null or value in argument'?
That would burn down to simply:
(t.id = ANY (check_ids) OR check_ids IS NULL)
All of this is in the realm of SQL and unrelated to PL/pgSQL.
Replace check_ids.count with array_length(check_ids, 1).

Have SQL query fail if subquery returns null

If the below subquery finds no records it returns null and sets the action_batch_id as so. Is it possible to have the entire query fail/exit if the subquery returns no records?
UPDATE action_batch_items
SET action_batch_id = (SELECT id FROM action_batches WHERE id = '123'
AND batch_count < 1000 AND batch_size < 100000)
WHERE id = 1234567
UPDATE: Here's the structure (it's ActiveRecord)
CREATE TABLE "public"."action_batches" (
"id" int8 NOT NULL,
"action_batch_container_id" int8 NOT NULL,
"action_message" json,
"adaptor" json,
"batch_count" int4 DEFAULT 0,
"batch_size" int8 DEFAULT 0
)
CREATE TABLE "public"."action_batch_items" (
"id" int8 NOT NULL,
"action_batch_id" int8,
"config" json
)
create or replace function raise_error(text) returns int as $$
begin
raise exception '%', $1;
return -1;
end; $$ language plpgsql;
and then
action_batch_id = coalesce((select id ...), raise_error('No data'));
Try using COALESCE():
UPDATE action_batch_items
SET action_batch_id =
(
SELECT COALESCE(id, action_batch_id)
FROM action_batches
WHERE id = '123' AND batch_count < 1000 AND batch_size < 100000
)
WHERE id = 1234567
In the event that the subquery returns NULL the action_batch_id column will not be changed. In the case of the subquery returning one or more non NULL records, your UPDATE will behave as before.

Oracle 10g and data validation in a trigger before row update

I am using Oracle 10g and I have the following table:
create table DE_TRANSFORM_MAP
(
DE_TRANSFORM_MAP_ID NUMBER(10) not null,
CLIENT NUMBER(5) not null,
USE_CASE NUMBER(38) not null,
DE_TRANSFORM_NAME VARCHAR2(100) not null,
IS_ACTIVE NUMBER(1) not null
)
That maps to an entry in the following table:
create table DE_TRANSFORM
(
DE_TRANSFORM_ID NUMBER(10) not null,
NAME VARCHAR2(100) not null,
IS_ACTIVE NUMBER(1) not null
)
I would like to enforce the following rules:
Only one row in DE_TRANSFORM_MAP with the same CLIENT and USE_CASE can have IS_ACTIVE set to 1 at any time
Only one row in DE_TRANSFORM with the same NAME and IS_ACTIVE set to 1 at any time
A row in DE_TRANSFORM cannot have IS_ACTIVE changed from 1 to 0 if any rows in DE_TRANSFORM_MAP have DE_TRANSFORM_NAME equal to NAME and IS_ACTIVE set to 1
Does this make sense?
I have tried to write a stored proc that handles this:
create or replace trigger DETRANSFORMMAP_VALID_TRIG
after insert or update on SERAPH.DE_TRANSFORM_MAP
for each row
declare
active_rows_count NUMBER;
begin
select count(*) into active_rows_count from de_transform_map where client = :new.client and use_case = :new.use_case and is_active = 1;
if :new.is_active = 1 and active_rows_count > 0 then
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case, policy_id and policy_level may be active');
end if;
end;
When I do the following it works as expected, I get an error:
insert into de_transform_map (de_transform_map_id, client, use_case, de_transform_name, is_active) values (detransformmap_id_seq.nextval, 6, 0, 'TEST', 1);
insert into de_transform_map (de_transform_map_id, client, use_case, de_transform_name, is_active) values (detransformmap_id_seq.nextval, 6, 1, 'TEST', 1);
But if I then do this:
update de_transform_map set use_case = 0 where use_case = 1
I get the following:
ORA-04091: table DE_TRANSFORM_MAP is mutating, trigger/function may not see it
How can I accomplish my validation?
EDIT: I marked Rene's answer as correct because I think the most correct and elegant way to do this is with a compound trigger but our production DB is still just 10g, we are updating to 11g early next year and I will rewrite the trigger then. Until then, I have a blanket trigger that will assert that no rows are duplicated, here it is:
create or replace trigger DETRANSFORMMAP_VALID_TRIG
after insert or update on DE_TRANSFORM_MAP
declare
duplicate_rows_exist NUMBER;
begin
select 1 into duplicate_rows_exist from dual where exists (
select client, use_case, count(*) from de_transform_map where is_active = 1
group by client, use_case
having count(*) > 1
);
if duplicate_rows_exist = 1 then
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case may be active');
end if;
end;
The error you get means that you cannot query the table the trigger is on from within a row level trigger itself. One way to work around this problem is to use a combination of 3 triggers.
a) A before statement level trigger
b) A row level trigger
c) An after statement level trigger
Trigger A initializes a collection in a package
Trigger B adds every changed row to the collection
Trigger C performs the desired action for every entry in the collection.
More details here:
http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551198119097816936
One of the improvements in Oracle 11G is that you can do all these action in one compound trigger. More here:
http://www.oracle-base.com/articles/11g/trigger-enhancements-11gr1.php
You should perhaps consider doing a "before insert" sort of thing! I've only got an MSSQL engine to play with right now, but hopefully something below might help you on your way... I'm not sure what you mean with your example of an error that works, however, as it appears to be in contradiction to the first use case you've posted... Either way, triggers can be a real pain during concurrent writes so you'll want to be careful in doing this sort of business logic validation from only the back end.
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DE_TRANSFORM_MAP'
AND type = 'U' )
BEGIN
--DROP TABLE DE_TRANSFORM_MAP;
CREATE TABLE DE_TRANSFORM_MAP
(
DE_TRANSFORM_MAP_ID NUMERIC(10) NOT NULL,
PRIMARY KEY ( DE_TRANSFORM_MAP_ID ),
CLIENT NUMERIC( 5 ) NOT NULL,
USE_CASE NUMERIC( 38 ) NOT NULL,
DE_TRANSFORM_NAME NVARCHAR( 100 ) NOT NULL,
IS_ACTIVE TINYINT NOT NULL
);
END;
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DE_TRANSFORM'
AND type = 'U' )
BEGIN
--DROP TABLE DE_TRANSFORM;
CREATE TABLE DE_TRANSFORM
(
DE_TRANSFORM_ID NUMERIC( 10 ) NOT NULL,
PRIMARY KEY ( DE_TRANSFORM_ID ),
NAME NVARCHAR( 100 ) NOT NULL,
IS_ACTIVE TINYINT NOT NULL
);
END;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DETRANSFORMMAP_VALID_TRIG'
AND type = 'TR' )
BEGIN
--DROP TRIGGER DETRANSFORMMAP_VALID_TRIG;
EXEC( '
CREATE TRIGGER DETRANSFORMMAP_VALID_TRIG
ON DE_TRANSFORM_MAP INSTEAD OF INSERT, UPDATE
AS SET NOCOUNT OFF;' );
END;
GO
ALTER TRIGGER DETRANSFORMMAP_VALID_TRIG
ON DE_TRANSFORM_MAP INSTEAD OF INSERT, UPDATE
AS BEGIN
SET NOCOUNT ON;
IF ( ( SELECT MAX( IS_ACTIVE )
FROM ( SELECT IS_ACTIVE = SUM( IS_ACTIVE )
FROM ( SELECT CLIENT, USE_CASE, IS_ACTIVE
FROM DE_TRANSFORM_MAP
EXCEPT
SELECT CLIENT, USE_CASE, IS_ACTIVE
FROM DELETED
UNION ALL
SELECT CLIENT, USE_CASE, IS_ACTIVE
FROM INSERTED ) f
GROUP BY CLIENT, USE_CASE ) mf ) > 1 )
BEGIN
RAISERROR( 'DE_TRANSFORM_MAP: CLIENT & USE_CASE cannot have multiple actives', 16, 1 );
END ELSE BEGIN
DELETE DE_TRANSFORM_MAP
WHERE DE_TRANSFORM_MAP_ID IN ( SELECT DE_TRANSFORM_MAP_ID
FROM DELETED );
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
SELECT DE_TRANSFORM_MAP_ID, CLIENT, USE_CASE,
DE_TRANSFORM_NAME, IS_ACTIVE
FROM INSERTED;
END;
SET NOCOUNT OFF;
END;
GO
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
VALUES ( 1, 6, 0, 'TEST', 1 );
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
VALUES ( 2, 6, 1, 'TEST', 1 );
GO
SELECT *
FROM dbo.DE_TRANSFORM_MAP;
GO
TRUNCATE TABLE DE_TRANSFORM_MAP;
GO
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
SELECT 1, 6, 0, 'TEST', 1
UNION ALL SELECT 2, 6, 1, 'TEST', 1
UNION ALL SELECT 3, 6, 1, 'TEST2', 1;
GO
SELECT *
FROM dbo.DE_TRANSFORM_MAP;
GO
TRUNCATE TABLE DE_TRANSFORM_MAP;
GO
INSERT INTO DE_TRANSFORM_MAP ( DE_TRANSFORM_MAP_ID,
CLIENT, USE_CASE, DE_TRANSFORM_NAME, IS_ACTIVE )
SELECT 1, 6, 0, 'TEST', 1
UNION ALL SELECT 2, 6, 1, 'TEST', 0
UNION ALL SELECT 3, 6, 1, 'TEST2', 1;
GO
SELECT *
FROM dbo.DE_TRANSFORM_MAP;
GO
UPDATE dbo.DE_TRANSFORM_MAP
SET IS_ACTIVE = 1
WHERE DE_TRANSFORM_MAP_ID = 2;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DETRANSFORM_VALID_TRIG'
AND type = 'TR' )
BEGIN
--DROP TRIGGER DETRANSFORM_VALID_TRIG;
EXEC( '
CREATE TRIGGER DETRANSFORM_VALID_TRIG
ON DE_TRANSFORM INSTEAD OF INSERT, UPDATE
AS SET NOCOUNT OFF;' );
END;
GO
ALTER TRIGGER DETRANSFORM_VALID_TRIG
ON DE_TRANSFORM INSTEAD OF INSERT, UPDATE
AS BEGIN
SET NOCOUNT ON;
IF ( ( SELECT MAX( IS_ACTIVE )
FROM ( SELECT IS_ACTIVE = SUM( IS_ACTIVE )
FROM ( SELECT NAME, IS_ACTIVE
FROM DE_TRANSFORM
EXCEPT
SELECT NAME, IS_ACTIVE
FROM DELETED
UNION ALL
SELECT NAME, IS_ACTIVE
FROM INSERTED ) f
GROUP BY NAME ) mf ) > 1 )
BEGIN
RAISERROR( 'DE_TRANSFORM: NAME cannot have multiple actives', 16, 1 );
END ELSE IF EXISTS (SELECT 1
FROM DE_TRANSFORM_MAP
WHERE IS_ACTIVE = 1
AND DE_TRANSFORM_NAME IN ( SELECT NAME
FROM DELETED
UNION ALL
SELECT NAME
FROM INSERTED
WHERE IS_ACTIVE = 0 ) )
BEGIN
RAISERROR( 'DE_TRANSFORM: NAME is active in DE_TRANSFORM_MAP', 16, 1 );
END ELSE BEGIN
DELETE DE_TRANSFORM
WHERE DE_TRANSFORM_ID IN (SELECT DE_TRANSFORM_ID
FROM DELETED );
INSERT INTO DE_TRANSFORM ( DE_TRANSFORM_ID, NAME, IS_ACTIVE )
SELECT DE_TRANSFORM_ID, NAME, IS_ACTIVE
FROM INSERTED;
END;
SET NOCOUNT OFF;
END;
GO
INSERT INTO DE_TRANSFORM ( DE_TRANSFORM_ID, NAME, IS_ACTIVE )
VALUES( 1, 'TEST2', 0 );
GO
SELECT *
FROM DE_TRANSFORM;
GO
TRUNCATE TABLE DE_TRANSFORM;
GO
TRUNCATE TABLE DE_TRANSFORM_MAP;
GO
If the trigger condition is “always” verified in the table, and if DE_TRANSFORM_MAP is a small table or if the insert/update statement affects many rows in DE_TRANSFORM_MAP, then you can use a statement trigger like this:
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG
AFTER INSERT OR UPDATE ON DE_TRANSFORM_MAP
DECLARE
EXISTS_ROWS NUMBER;
BEGIN
SELECT 1 INTO EXISTS_ROWS FROM DUAL WHERE EXISTS(
SELECT CLIENT
FROM DE_TRANSFORM_MAP
WHERE IS_ACTIVE = 1
GROUP BY CLIENT, USE_CASE
HAVING COUNT(*) > 1
);
IF (EXISTS_ROW = 1) THEN
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case, policy_id and policy_level may be active');
END IF;
END;
/
If the trigger condition is “not always” verified in the table, and if DE_TRANSFORM_MAP is a big table or if the insert/update statement affects few rows in DE_TRANSFORM_MAP, then redesign your trigger following Rene's answer. Something like:
CREATE GLOBAL TEMPORARY TABLE DE_TRANSFORM_MAP_AUX AS
SELECT CLIENT, USE_CASE FROM DE_TRANSFORM_MAP WHERE 1 = 0;
/
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG1
BEFORE INSERT OR UPDATE ON SERAPH.DE_TRANSFORM_MAP
BEGIN
DELETE FROM DE_TRANSFORM_MAP_AUX;
END;
/
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG2
BEFORE INSERT OR UPDATE ON DE_TRANSFORM_MAP
FOR EACH ROW WHEN NEW.IS_ACTIVE = 1
BEGIN
INSERT INTO DE_TRANSFORM_MAP_AUX VALUES(:NEW.CLIENT, :NEW.USE_CASE);
END;
/
CREATE OR REPLACE TRIGGER DETRANSFORMMAP_VALID_TRIG3
AFTER INSERT OR UPDATE ON DE_TRANSFORM_MAP
DECLARE
EXISTS_ROW NUMBER;
BEGIN
SELECT 1 INTO EXISTS_ROWS FROM DUAL WHERE EXISTS(
SELECT CLIENT
FROM DE_TRANSFORM_MAP
WHERE IS_ACTIVE = 1 AND
(CLIENT, USE_CASE) IN (SELECT CLIENT, USE_CASE FROM DE_TRANSFORM_MAP_AUX)
GROUP BY CLIENT, USE_CASE
HAVING COUNT(*) > 1
);
DELETE FROM DE_TRANSFORM_MAP_AUX;
IF (EXISTS_ROW = 1) THEN
RAISE_APPLICATION_ERROR(-20000, 'Only one row with the specified client, use_case, policy_id and policy_level may be active');
END IF;
END;
/
You must consider to create an index on CLIENT and USE_CASE in DE_TRANSFORM_MAP if it does not exists.