I have mytable which has 3 integer fields: id, status, project_id.
I've told people that they should not progress status past 4 before assigning it a project_id value. Naturally people don't listen and then there are problems down the road.
Is there a way to return an error if someone tries to update from status 4 to 5 while project_id column is null? I still need people to be able to update status from 2 or 3 to status 4 regardless of it having a project_id.
You can use CHECK constraint as suggested by #stickbit if you need very simple checks.
If you need a more complicated logic, you can use TRIGGER functionality
CREATE FUNCTION check_status()
RETURNS trigger AS
$mytrigger$
BEGIN
IF OLD.status = 4 AND NEW.status >= 5 AND NEW.project_id IS NULL THEN
RAISE EXCEPTION 'Project ID must be assigned before progressing to status 5';
END IF;
RETURN NEW;
END
$mytrigger$
LANGUAGE plpgsql;
CREATE TRIGGER project_id_check
BEFORE UPDATE ON "MyTable"
FOR EACH ROW EXECUTE PROCEDURE check_status();
How about a check constraint on the table:
CHECK (project_id IS NOT NULL OR status < 5)
If you have data violating your desired rules, you can still use a CHECK constraint like demonstrated by sticky bit. Just make it NOT VALID:
ALTER TABLE mytable ADD CONSTRAINT project_id_required_for_status_5_or_higher
CHECK project_id IS NOT NULL OR status < 5) NOT VALID;
Then the constraint is only applied to subsequent inserts and updates. Existing rows are ignored. (But any new update must fix violating values or it will fail.)
You should also have a FOREIGN KEY constraint enforcing referential integrity for project_id, else the constraint can easily be circumvented with dummy values.
Fine point: a CHECK constraint not only prohibits the updates you mentioned, but inserts with a violating state as well.
Once all rows are fixed to comply with the new rule, you can VALIDATE the constraint:
ALTER TABLE mytable VALIDATE CONSTRAINT project_id_required_for_status_5_or_higher;
More:
Trigger vs. check constraint
Related
In my PostgreSQL 9.4 database, I have a table fields with a column name with unique values.
I'm creating a new table fields_new with a similar structure (not important here) and a column name as well. I need a way to constraint name values to be inserted to the fields_new not to be present in fields.name.
For example, if fields.name contains the values 'color' and 'length', I need to prevent fields_new.name from containing 'color' or 'length' values. So, in other words I need to provide that the name columns in both tables do not have any duplicate values between them. And the constraint should go both ways.
Only enforce constraint for new entries in fields_new
CHECK constraints are supposed to be immutable, which generally rules out any kind of reference to other tables, which are not immutable by nature.
To allow some leeway (especially with temporal functions) STABLE functions are tolerated. Obviously, this cannot be completely reliable in a database with concurrent write access. If rows in the referenced table change, they may be in violation of the constraint.
Declare the invalid nature of your constraint by making it NOT VALID (Postgres 9.1+). This way Postgres also won't try to enforce it during a restore (which might be bound to fail). Details here:
Disable all constraints and table checks while restoring a dump
The constraint is only enforced for new rows.
CREATE OR REPLACE FUNCTION f_fields_name_free(_name text)
RETURNS bool AS
$func$
SELECT NOT EXISTS (SELECT 1 FROM fields WHERE name = $1);
$func$ LANGUAGE sql STABLE;
ALTER TABLE fields_new ADD CONSTRAINT fields_new_name_not_in_fields
CHECK (f_fields_name_free(name)) NOT VALID;
Plus, of course, a UNIQUE or PRIMARY KEY constraint on fields_new(name) as well as on fields(name).
Related:
CONSTRAINT to check values from a remotely related table (via join etc.)
Function to update a status flag for validity of other column?
Trigger vs. check constraint
Enforce both ways
You could go one step further and mirror the above CHECK constraint on the 2nd table. Still no guarantees against nasty race conditions when two transactions write to both tables at the same time.
Or you could maintain a "materialized view" manually with triggers: a union of both name columns. Add a UNIQUE constraint there. Not as rock solid as the same constraint on a single table: there might be race conditions for writes to both tables at the same time. But the worst that can happen is a deadlock forcing transactions to be rolled back. No permanent violation can creep in if all write operations are cascaded to the "materialized view".
Similar to the "dark side" in this related answer:
Can PostgreSQL have a uniqueness constraint on array elements?
Just that you need triggers for INSERT / UPDATE / DELETE on both tables.
I had a similar problem where I wanted to maintain a list of items per-company, along with a global list for all companies. If the company number is 0, it is to be treated as global and a new item cannot be inserted for ANY company using that name. The following script (based on the above solution) seems to work:
drop table if exists blech;
CREATE TABLE blech (
company int,
name_key text,
unique (company, name_key)
);
create or replace function f_foobar(new_company int, new_name_key text) returns bool as
$func$
select not exists (
select 1 from blech b
where $1 <> 0
and b.company = 0
and b.name_key = $2);
$func$ language sql stable;
alter table blech add constraint global_unique_name_key
check (f_foobar(company, name_key)) not valid;
insert into blech values(0,'GLOB1');
insert into blech values(0,'GLOB2');
-- should succeed:
insert into blech values(1,'LOCAL1');
insert into blech values(2,'LOCAL1');
-- should fail:
insert into blech values(1,'GLOB1');
-- should fail:
insert into blech values(0,'GLOB1');
Im trying to add a constraint to a table so that there can only be one record in the table.
This is the code I already have:
ALTER TABLE CONFIG
ADD CONSTRAINT always_one
CHECK (count(*)= 1);
And this is the error I'm getting
ALTER TABLE CONFIG
ADD CONSTRAINT always_one CHECK (count(*)= 1)
Error report -
SQL Error: ORA-00934: group function is not allowed here
00934. 00000 - "group function is not allowed here"
*Cause:
*Action:
How does this work?
You can use already proposed solution with adding unique constraint on column
alter table config add constraint always_one check (pk_col=1);
this however allows inserting more than one row in case pk_col is null in second inserted row. So you need to handle this by adding a NOT NULL constraint as well
ALTER TABLE config
ADD CONSTRAINT notnulc CHECK (pk_col IS NOT NULL) ;
To prevent deleting this row, you should probably create before delete trigger as follows
create or replace trigger trg_ONLYONE before delete ON CONFIG
DECLARE
C NUMBER;
BEGIN
SELECT COUNT(*) INTO C FROM CONFIG;
if (C=1) THEN
RAISE_APPLICATION_ERROR (-20011, 'TOO FEW ROWS');
END IF;
END;
Futher options are: instead of check constraints mentioned above is CREATE BEFORE INSERT trigger, or instead of NOT NULL and UNIQUE CONSTRAINT make pk_col PRIMARY KEY
Just create a unique index on a column in the table, and add a constraint that the value of this column must be a certain value.
eg.
CREATE UNIQUE INDEX one_val ON config(pk_col);
ALTER TABLE CONFIG
ADD CONSTRAINT always_one
CHECK (pk_col = 1);
If all of your other columns could be any value, you may need to just add this additional column, and give it a default value.
Is it possible in Oracle SQL developer to do something like this
CREATE FUNCTION fnCheckValid(accountidd IN NUMBER)
RETURN NUMBER
IS retval NUMBER(4,0);
BEGIN
SELECT COUNT(accountid_fk)
INTO retval
FROM tbl_AccountAuthentications
WHERE accountid_fk = accountidd;
RETURN(retval);
END;
/
ALTER TABLE tbl_AccountAuthentications
ADD CONSTRAINT chkCheckvalid CHECK(fnCheckValid(accountid_fk) <= 1);
The error i keep getting is
Error starting at line 999 in command:
ALTER TABLE tbl_AccountAuthentications
ADD CONSTRAINT chkCheckvalid CHECK(fnCheckValid(accountid_fk) <= 1)
Error report:
SQL Error: ORA-00904: "FNCHECKVALID": invalid identifier
00904. 00000 - "%s: invalid identifier"
*Cause:
*Action:
The function is being created and i can find it but when i am trying to call it i keep getting that error
this is what i am trying to achieve
AccountID RegularID OpenID
1 5 null
1 null 10
1 null 11
1 6 <-- Forbidden
so that a user cannot make 2 regular accounts but can have as many OpenID accounts as he wants
The table is set up as follows
CREATE TABLE tbl_AccountAuthentications(
newAuthID NUMBER(4,0)
CONSTRAINT naid_pk PRIMARY KEY,
accountid_fk NUMBER(4,0)
CONSTRAINT accid_fk
REFERENCES tbl_UserAccounts(account_id),
regularid_fk NUMBER(4,0)
CONSTRAINT rgid_fk
REFERENCES tbl_StrongRoom(password_id),
openid_fk NUMBER(4,0)
CONSTRAINT opid_fk
REFERENCES tbl_OpenID(openid)
);
No you can't do that, see Restrictions on Check Constraints:
Calls to user-defined functions
But you can make a workaround using virtual columns
ALTER TABLE tbl_AccountAuthentications ADD (fnCheck NUMBER GENERATED ALWAYS AS (fnCheckValid(accountid_fk)) VIRTUAL);
ALTER TABLE tbl_AccountAuthentications
ADD CONSTRAINT chkCheckvalid CHECK(fnCheck <= 1);
Note, function has to be DETERMINISTIC, otherwise it does not work. Oracle does not verify whether your function is actually deterministic, it just checks for the keyword. This one is allowed (although it does not make any sense at all):
CREATE OR REPLACE FUNCTION DET_FUNCTION RETURN NUMBER DETERMINISTIC IS
BEGIN
RETURN DBMS_RANDOM.RANDOM();
END;
/
You want to ensure that the columns AccountID and RegularID, together, are unique, no matter how many values of OpenID there are.
The only way of doing this, as you've determined, is to constrain it. You note in the comments that you've experimented with triggers. This does not constrain the values within the database, it only ensures that it attempts to verify when the trigger is enabled. I expect that, when you attempted it, you got the error "ORA-04091: is mutating, trigger/function may not see it." as you're selecting from a table you're in the processes of changing (insert or update).
If you have to constrain this then that's what you should do; the problem with doing so is that your table is not normalised. So, normalise it. Use two tables instead of the one you have.
Firstly, you need your RegularID to be unique over AccountID this means it should be stored at this level. It appears as though tbl_UserAccounts is unique on this identifier so alter this table and store your RegularID there.
Next, you want a table that has as many OpenID s as a user might want. This means that you need a table unique on AccountID and OpenID 1.
create table openid_account_auth (
, account_id number(4,0)
, open_id number(4,0)
, constraint pk_openid_account_auth
primary key (account_id, open_id)
, constraint fk_openid_account_auth_accid
foreign key (account_id)
references tbl_UserAccounts(account_id)
, constraint fk_openid_account_auth_open
foreign key (open_id)
references tbl_OpenID (openid)
);
One point on this table (and your own), it means that multiple accounts can have the same OpenID. If you did not intend this then you should add AccountID as a foreign key in tbl_OpenID, which would be the only way to ensure that each OpenID is associated with one, and only one, AccountID.
You can then create a view in order to get the information in the same manner, if you really feel the need to use this. I'm not certain why you would.
create or replace view AccountAuthentications as
select account_id, regular_id, null
from user_accounts
union all
select account_id, null, open_id
from openid_account_auth;
Simply put, unless under severe constraints, you should always store data at it's natural level and use the database to ensure that integrity is maintained. If you then need to use the data slightly differently you can use views, or materialized views etc., to do so.
1. I'm sorry but I can't bring myself to prefix the name of every table with tbl_.
Hi I'm having trouble getting my sql syntax correct. I want to create a unique constraint that looks at the newly added foreign key, looks at some properties of the newly related entity to decided if the relationship is allowed.
CREATE or replace TRIGGER "New_Trigger"
AFTER INSERT OR UPDATE ON "Table_1"
FOR EACH ROW
BEGIN
Select "Table_2"."number"
(CASE "Table_2"."number" > 0
THEN RAISE_APPLICATION_ERROR(-20000, 'this is not allowed');
END)
from "Table_1"
WHERE "Table_2"."ID" = :new.FK_Table_2_ID
END;
Edit: APC answer is wonderfully comprehensive, however leads me to think im doing it in the wrong way.
The situation is I have a table of people with different privilege levels, and I want to check these privilege levels, e.g. A user, 'Bob', has low level privileges and he tries to become head of department which requires requires high privileges so the system prevents this happening.
There is a follow-up question which poses a related scenario but with a different data model. Find it here.
So the rule you want to enforce is that TABLE_1 can only reference TABLE_2 if some column in TABLE_2 is zero or less. Hmmm.... Let's sort out the trigger logic and then we'll discuss the rule.
The trigger should look like this:
CREATE or replace TRIGGER "New_Trigger"
AFTER INSERT OR UPDATE ON "Table_1"
FOR EACH ROW
declare
n "Table_2"."number".type%;
BEGIN
Select "Table_2"."number"
into n
from "Table_2"
WHERE "Table_2"."ID" = :new.FK_Table_2_ID;
if n > 0
THEN RAISE_APPLICATION_ERROR(-20000, 'this is not allowed');
end if;
END;
Note that your error message should include some helpful information such as the value of the TABLE_1 primary key, for when you are inserting or updating multiple rows on the table.
What you are trying to do here is to enforce a type of constraint known as an ASSERTION. Assertions are specified in the ANSI standard but Oracle has not implemented them. Nor has any other RDBMS, come to that.
Assertions are problematic because they are symmetrical. That is, the rule also needs to be enforced on TABLE_2. At the moment you check the rule when a record is created in TABLE_1. Suppose at some later time a user updates TABLE_2.NUMBER so it is greater than zero: your rule is now broken, but you won't know that it is broken until somebody issues a completely unrelated UPDATE on TABLE_1, which will then fail. Yuck.
So, what to do?
If the rule is actually
TABLE_1 can only reference TABLE_2 if
TABLE_2.NUMBER is zero
then you can enforce it without triggers.
Add a UNIQUE constraint on TABLE_2 for (ID, NUMBER); you need an additional constraint because ID remains the primary key for TABLE_2.
Add a dummy column on TABLE_1 called TABLE_2_NUMBER. Default it to zero and have a check constraint to ensure it is always zero. (If you are on 11g you should consider using a virtual column for this.)
Change the foreign key on TABLE_1 so (FK_Table_2_ID, TABLE_2_NUMBER) references the unique constraint rather than TABLE_2's primary key.
Drop the "New_Trigger" trigger; you don't need it anymore as the foreign key will prevent anybody updating TABLE_2.NUMBER to a value other than zero.
But if the rule is really as I formulated it at the top i.e.
TABLE_1 can only reference TABLE_2 if
TABLE_2.NUMBER is not greater than zero (i.e. negative values are okay)
then you need another trigger, this time on TABLE_2, to enforce it the other side of the rule.
CREATE or replace TRIGGER "Assertion_Trigger"
BEFORE UPDATE of "number" ON "Table_2"
FOR EACH ROW
declare
x pls_integer;
BEGIN
if :new."number" > 0
then
begin
Select 1
into x
from "Table_1"
WHERE "Table_1"."FK_Table_2_ID" = :new.ID
and rownum = 1;
RAISE_APPLICATION_ERROR(-20001, :new.ID
||' has dependent records in Table_1');
exception
when no_data_found then
null; -- this is what we want
end;
END;
This trigger will not allow you to update TABLE_2.NUMBER to a value greater than zero if it is referenced by records in TABLE_2. It only fires if the UPDATE statement touches TABLE_2.NUMBER to minimise the performance impact of executing the lookup.
Don't use a trigger to create a unique constraint or a foreign key constraint. Oracle has declarative support for unique and foreign keys, e.g.:
Add a unique constraint on a column:
ALTER TABLE "Table_1" ADD (
CONSTRAINT table_1_uk UNIQUE (column_name)
);
Add a foreign key relationship:
ALTER TABLE "ChildTable" ADD (
CONSTRAINT my_fk FOREIGN KEY (parent_id)
REFERENCES "ParentTable" (id)
);
I'm not clear on exactly what you're trying to achieve with your trigger - it's a bit of a mess of SQL and PL/SQL munged together which will not work, and seems to refer to a column on "Table_2" which is not actually queried.
A good rule of thumb is, if your trigger is querying the same table that the trigger is on, it's probably wrong.
I'm not sure, but are you after some kind of conditional foreign key relationship? i.e. "only allow child rows where the parent satisfies condition x"? If so, the problem is in the data model and should be fixed there. If you provide more explanation of what you're trying to achieve we should be able to help you.
I have created this trigger which should give a error, whenever the value of new rctmemenrolno of table-receipts1 is matched with the memenrolno of table- memmast, but it is giving error in both condition(it is matched or not matched).
kindly help me.
CREATE OR REPLACE TRIGGER HDD_CABLE.trg_rctenrolno
before insert ON HDD_CABLE.RECEIPTS1 for each row
declare
v_enrolno varchar2(9);
cursor c1 is select memenrolno from memmast;
begin
open c1;
fetch c1 into v_enrolno;
LOOP
If :new.rctmemenrolno<>v_enrolno
then
raise_application_error(-20186,'PLEASE ENTER CORRECT ENROLLMENT NO');
close c1;
end if;
END LOOP;
end;
You are validating whether the entered RECEIPTS1.rctmemenrolno matches a memenrolno in MEMAST, right? Well a trigger is the wrong way to do this.
The loop completely won't scale (the more rows in MEMAST the longer it will take to insert a record in RECEIPTS1). But even a direct lookup to check for the existence of the specified key will still suck. Also this approach fails to work safely in a multi-user environment because Oracle uses the READ COMMITTED isolation level. In other words while your transaction is making the check some other session is deleting the row you have just found. Which results in a corrupt database.
The one and only correct way to do this is with a foreign key constraint.
alter table receipt1
add constraint receipts1_memast_fk foreign key (rctmemenrolno)
references memast (memenrolno);
Of course, this presumes you have a primary key - or a unique constraint - on memast to enforce the uniquess of memenrolno. I fervently hope you are not trying to enforce its uniqueness through triggers.
edit
"already I have 1 primary key as MEMID on memmast table"
If there is a one-to-one relationship between MEMID and MEMENROLNO, which is common when we have a business key and a surrogate key, then the proper solution is to use the primary key on RECEIPTS1. That is, drop the column RCTMEMENROL and replace it with RCTMEMID. Then plug those column names into that alter table statement. The front end would be responsible for providing the users with a facility to lookup the MEMID for a given MEMENROLNO, such as a List Of Values widget.
A yuckier solution would be to build a unique constraint on MEMAST.MEMENROLNO. That is not advisable because using natural keys to enforce foreign keys is a bad idea, and utterly mad when the table in question already has a synthetic primary key.
If there isn't a one-to-one relationship between MEMID and MEMENROLNO then I don't know what the purpose of the check is. A query on MEMAST can assert the existence of a given MEMENROLNO now but without a foreign key it can say nothing about the state of the database in five minutes time. So why bother with the check at all?
It looks like you're looping through all possible values of memenrolno and making sure the new value matches every possible value, which will always be false when there is more than one possible memenrolno.
Try this instead.
CREATE OR REPLACE TRIGGER HDD_CABLE.trg_rctenrolno
before insert ON HDD_CABLE.RECEIPTS1 for each row
begin
If NOT EXISTS SELECT 1 FROM memast WHERE memenrolno = :new.rctmemenrolno
then
raise_application_error(-20186,'PLEASE ENTER CORRECT ENROLLMENT NO');
end if;
end;