Postgresql tuple constraints about NOT NULL - sql

I am building a database in POSTGRESQL, and I would like to create NOT NULL constraints for my columns, where one and only one column would be NOT NULL.
I have two columns in my table, site_id and buffer_result_id. Only one of these columns will have values.
alter table dt.analysis_result
add constraint ar_check check (site_id NOT NULL OR buffer_result_id NOT NULL);
The above code is just some pseudo-code to show my idea. How can I achieve this function?

You could use XOR expressed as:
alter table dt.analysis_result
add constraint ar_check check (
(site_id IS NOT NULL OR buffer_result_id IS NOT NULL)
AND NOT(site_id IS NOT NULL AND buffer_result_id IS NOT NULL)
);
db<>fiddle demo
More info: Exclusive OR - Equivalences
Demo:
CREATE TABLE analysis_result(site_id INT, buffer_result_id INT);
INSERT INTO analysis_result VALUES (NULL, NULL);
-- ERROR: new row for relation "analysis_result" violates check constraint "ar_check"
INSERT INTO analysis_result VALUES (1, 2);
-- ERROR: new row for relation "analysis_result" violates check constraint "ar_check"
INSERT INTO analysis_result VALUES (NULL, 2);
INSERT INTO analysis_result VALUES (1, NULL);
SELECT * FROM analysis_result

In Postgres, you can do this with a check constraint. I think the simplest method is to count the number of not null values:
alter table dt.analysis_result add constraint ar_check
check ( (site_id is not null)::int + (buffer_result_id is not null)::int = 1
);

Related

Setting value of row to NULL if another value is not present in Oracle

I am using an Oracle database and trying to add a constraint that sets the chemical_value to NULL if there is a chemical_outlier present vice versa. I have been searching the Oracle docs for a possible solution but stuck
Create table chemical
(
chemical_id char(3) not null,
chemical_name varchar2(50) not null,
chemical_value numeric,
checmical_outlier varchar(50),
constraint checkChemical <DONT KNOW HOW TO APPROACH THIS>
)
Just need some kind of direction is approaching this. I know NULL values can be bad but just want them there instead of an empty row
A check constraint won't set anything to null, but it will prevent invalid values being entered:
create table chemical
( chemical_id varchar2(3) not null
, chemical_name varchar2(50) not null
, chemical_value numeric
, chemical_outlier varchar(50)
, constraint chem_value_or_outlier_chk
check (not (chemical_value is not null and chemical_outlier is not null)
and (chemical_value is not null or chemical_outlier is not null))
);
Now these fail:
insert into chemical (chemical_id, chemical_name, chemical_value, chemical_outlier)
values ('x', 'xyz', 123, 'outlier');
ORA-02290: check constraint (XXX.CHEM_VALUE_OR_OUTLIER_CHK) violated
insert into chemical (chemical_id, chemical_name, chemical_value, chemical_outlier)
values ('x', 'xyz', null, null);
ORA-02290: check constraint (XXX.CHEM_VALUE_OR_OUTLIER_CHK) violated
But these succeed:
insert into chemical (chemical_id, chemical_name, chemical_value, chemical_outlier)
values ('x', 'xyz', null, 'outlier');
insert into chemical (chemical_id, chemical_name, chemical_value, chemical_outlier)
values ('x', 'xyz', 123, null);
If you call an INSERT without a column, that column is set with NULL by default. So, if you can control the INSERT's, just call them with 3 columns, the 2 mandatory and 1 of the other ones. In this case you need a check to guarante that anyone will call the INSERT with the 4 columns like that:
CONSTRAINT "CONSTRAINT_NAME"
CHECK ((chemical_value IS NULL AND checmical_outlier IS NOT NULL)
OR (chemical_value IS NOT NULL AND checmical_outlier IS NULL)) ENABLE
So, if someone try to define both columns, the constraint don't accept the INSERT.
But, if you have a form or something else that generate the INSERT query, and even when you don't fill a column, the INSERT is generated with that column with value "" (empty string), you need a trigger to convert this to a NULL value like that:
CREATE OR REPLACE TRIGGER "TRIGGER_NAME"
BEFORE INSERT OR UPDATE ON chemical
REFERENCING NEW AS NEW
FOR EACH ROW
BEGIN
IF :new.chemical_value = ''
THEN :new.chemical_value := NULL;
END IF;
IF :new.checmical_outlier = ''
THEN :new.checmical_outlier := NULL;
END IF;
END;
Please note that this trigger don't check the condition "one is filled and the other one isn't", the trigger just convert the empty string to NULL. You can use both, trigger and constraint, to guarante the condition and the conversion.

Interchangeable, composite primary keys

I have a table foo that has only two fields: fooIdA and fooIdB (both of the same type). Those are composite primary keys, so:
primary key (fooIdA, fooIdB)...
Considering this, how can I make all permutations of the keys to be the same?
That is, (fooIdA, fooIdB) = (fooIdB, fooIdA).
Depending on your DBMS, you can create a unique index on an expression that prevents inserting (1,2) and (2,1)
In Postgres and Oracle you can do this:
create unique index unique_combinations
on the_table (least(fooida, fooidb), greatest(fooida, fooidb));
You don't specify your RDBMS. You can also add calculated fields with MAX, MIN values and add UNIQUE constraint on these calculated fields. Here is the MSSQL example:
CREATE TABLE ATest (id1 int, id2 int);
ALTER TABLE ATest ADD idMax AS (CASE WHEN id1>=id2 THEN id1 ELSE id2 END);
ALTER TABLE ATest ADD idMin AS (CASE WHEN id1>=id2 THEN id2 ELSE id1 END);
ALTER TABLE ATest ADD CONSTRAINT UniqueConstCalc UNIQUE(idMax,idMin);
insert into ATest values (1,1);
insert into ATest values (1,2);
insert into ATest values (2,1);
(1 row(s) affected)
(1 row(s) affected)
Msg 2627, Level 14, State 1, Line 3
Violation of UNIQUE KEY constraint 'UniqueConstCalc'.
Cannot insert duplicate key in object 'dbo.ATest'.
The duplicate key value is (2, 1).
The statement has been terminated.

How to add unique constraint that depends of the foreign key values? [duplicate]

This question already has answers here:
Add unique constraint to combination of two columns
(4 answers)
Closed 7 years ago.
I have one table with contains foreign key "columnA" column and one more column "columnB". I want to prevent adding same values in "columnB" but only for same value in "columnA"...
columnA columnB
1 'a'
1 'a' - this is not allowed
2 'a' - this is allowed
From my perspective only way to do that is by using trigger, but i suppose that there is a better, more elegant way to make this constraint. Do you know best way to make this logic?
A Unique constraint would work.
alter table TableName add constraint UQ_consrtaint unique(columnA, columnB);
That should do it.
It looks like you need to create a primary key like this:
DECLARE #DataSource TABLE
(
[A] TINYINT
,[B] CHAR
,PRIMARY KEY([A], [B])
);
INSERT INTO #DataSource ([A], [B])
VALUES (1, 'a'); -- ok
INSERT INTO #DataSource ([A], [B])
VALUES (2, 'a'); -- ok
INSERT INTO #DataSource ([A], [B])
VALUES (1, 'a'); -- error
It will give you the following error:
Msg 2627, Level 14, State 1, Line 14 Violation of PRIMARY KEY
constraint 'PK__#B1CFBEC__D86D1834E734E52B'. Cannot insert duplicate
key in object 'dbo.#DataSource'. The duplicate key value is (1, a).
in the case above.
Or unique constrain on the two columns:
DECLARE #DataSource TABLE
(
[A] TINYINT
,[B] CHAR
,UNIQUE ([A], [B])
);
ALTER TABLE tablename ADD UNIQUE uniqueconstraintname(columnA, columnB);

how to create a Foreign-Key constraint to a subset of the rows of a table?

I have a reference table, say OrderType that collects different types of orders:
CREATE TABLE IF NOT EXISTS OrderType (name VARCHAR);
ALTER TABLE OrderType ADD PRIMARY KEY (name);
INSERT INTO OrderType(name) VALUES('sale-order-type-1');
INSERT INTO OrderType(name) VALUES('sale-order-type-2');
INSERT INTO OrderType(name) VALUES('buy-order-type-1');
INSERT INTO OrderType(name) VALUES('buy-order-type-2');
I wish to create a FK constraint from another table, say SaleInformation, pointing to that table (OrderType). However, I am trying to express that not all rows of OrderType are eligible for the purposes of that FK (it should only be sale-related order types).
I thought about creating a view of table OrderType with just the right kind of rows (view SaleOrderType) and adding a FK constraint to that view, but PostgreSQL balks at that with:
ERROR: referenced relation "SaleOrderType" is not a table
So it seems I am unable to create a FK constraint to a view (why?). Am I only left with the option of creating a redundant table to hold the sale-related order types? The alternative would be to simply allow the FK to point to the original table, but then I am not really expressing the constraint as strictly as I would like to.
I think your schema should be something like this
create table order_nature (
nature_id int primary key,
description text
);
insert into order_nature (nature_id, description)
values (1, 'sale'), (2, 'buy')
;
create table order_type (
type_id int primary key,
description text
);
insert into order_type (type_id, description)
values (1, 'type 1'), (2, 'type 2')
;
create table order_nature_type (
nature_id int references order_nature (nature_id),
type_id int references order_type (type_id),
primary key (nature_id, type_id)
);
insert into order_nature_type (nature_id, type_id)
values (1, 1), (1, 2), (2, 1), (2, 2)
;
create table sale_information (
nature_id int default 1 check (nature_id = 1),
type_id int,
foreign key (nature_id, type_id) references order_nature_type (nature_id, type_id)
);
If the foreign key clause would also accept an expression the sale information could omit the nature_id column
create table sale_information (
type_id int,
foreign key (1, type_id) references order_nature_type (nature_id, type_id)
);
Notice the 1 in the foreign key
You could use an FK to OrderType to ensure referential integrity and a separate CHECK constraint to limit the order types.
If your OrderType values really are that structured then a simple CHECK like this would suffice:
check (c ~ '^sale-order-type-')
where c is order type column in SaleInformation
If the types aren't structured that way in reality, then you could add some sort of type flag to OrderType (say a boolean is_sales column), write a function which uses that flag to determine if an order type is a sales order:
create or replace function is_sales_order_type(text ot) returns boolean as $$
select exists (select 1 from OrderType where name = ot and is_sales);
$$ language sql
and then use that in your CHECK:
check(is_sales_order_type(c))
You don't of course have to use a boolean is_sales flag, you could have more structure than that, is_sales is just for illustrative purposes.

Either OR non-null constraints in MySQL

What's the best way to create a non-NULL constraint in MySQL such that fieldA and fieldB can't both be NULL. I don't care if either one is NULL by itself, just as long as the other field has a non-NULL value. And if they both have non-NULL values, then it's even better.
This isn't an answer directly to your question, but some additional information.
When dealing with multiple columns and checking if all are null or one is not null, I typically use COALESCE() - it's brief, readable and easily maintainable if the list grows:
COALESCE(a, b, c, d) IS NULL -- True if all are NULL
COALESCE(a, b, c, d) IS NOT NULL -- True if any one is not null
This can be used in your trigger.
#Sklivvz: Testing with MySQL 5.0.51a, I find it parses a CHECK constraint, but does not enforce it. I can insert (NULL, NULL) with no error. Tested both MyISAM and InnoDB. Subsequently using SHOW CREATE TABLE shows that a CHECK constraint is not in the table definition, even though no error was given when I defined the table.
This matches the MySQL manual which says: "The CHECK clause is parsed but ignored by all storage engines."
So for MySQL, you would have to use a trigger to enforce this rule. The only problem is that MySQL triggers have no way of raising an error or aborting an INSERT operation. One thing you can do in the trigger to cause an error is to set a NOT NULL column to NULL.
CREATE TABLE foo (
FieldA INT,
FieldB INT,
FieldA_or_FieldB TINYINT NOT NULL;
);
DELIMITER //
CREATE TRIGGER FieldABNotNull BEFORE INSERT ON foo
FOR EACH ROW BEGIN
IF (NEW.FieldA IS NULL AND NEW.FieldB IS NULL) THEN
SET NEW.FieldA_or_FieldB = NULL;
ELSE
SET NEW.FieldA_or_FieldB = 1;
END IF;
END//
INSERT INTO foo (FieldA, FieldB) VALUES (NULL, 10); -- OK
INSERT INTO foo (FieldA, FieldB) VALUES (10, NULL); -- OK
INSERT INTO foo (FieldA, FieldB) VALUES (NULL, NULL); -- gives error
You also need a similar trigger BEFORE UPDATE.
MySQL 5.5 introduced SIGNAL, so we don't need the extra column in Bill Karwin's answer any more. Bill pointed out you also need a trigger for update so I've included that too.
CREATE TABLE foo (
FieldA INT,
FieldB INT
);
DELIMITER //
CREATE TRIGGER InsertFieldABNotNull BEFORE INSERT ON foo
FOR EACH ROW BEGIN
IF (NEW.FieldA IS NULL AND NEW.FieldB IS NULL) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '\'FieldA\' and \'FieldB\' cannot both be null';
END IF;
END//
CREATE TRIGGER UpdateFieldABNotNull BEFORE UPDATE ON foo
FOR EACH ROW BEGIN
IF (NEW.FieldA IS NULL AND NEW.FieldB IS NULL) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '\'FieldA\' and \'FieldB\' cannot both be null';
END IF;
END//
DELIMITER ;
INSERT INTO foo (FieldA, FieldB) VALUES (NULL, 10); -- OK
INSERT INTO foo (FieldA, FieldB) VALUES (10, NULL); -- OK
INSERT INTO foo (FieldA, FieldB) VALUES (NULL, NULL); -- gives error
UPDATE foo SET FieldA = NULL; -- gives error
This is the standard syntax for such a constraint, but MySQL blissfully ignores the constraint afterwards
ALTER TABLE `generic`
ADD CONSTRAINT myConstraint
CHECK (
`FieldA` IS NOT NULL OR
`FieldB` IS NOT NULL
)
I've done something similar in SQL Server, I'm not sure if it will work directly in MySQL, but:
ALTER TABLE tableName ADD CONSTRAINT constraintName CHECK ( (fieldA IS NOT NULL) OR (fieldB IS NOT NULL) );
At least I believe that's the syntax.
However, keep in mind that you cannot create check constraints across tables, you can only check the columns within one table.
I accomplished this using a GENERATED ALWAYS column with COALESCE ... NOT NULL:
DROP TABLE IF EXISTS `error`;
CREATE TABLE IF NOT EXISTS `error` (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
left_id BIGINT UNSIGNED NULL,
right_id BIGINT UNSIGNED NULL,
left_or_right_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(left_id, right_id)) NOT NULL,
when_occurred TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
message_text LONGTEXT NOT NULL,
INDEX id_index (id),
INDEX when_occurred_index (when_occurred),
INDEX left_id_index (left_id),
INDEX right_id_index (right_id)
);
INSERT INTO `error` (left_id, right_id, message_text) VALUES (1, 1, 'Some random text.'); -- Ok.
INSERT INTO `error` (left_id, right_id, message_text) VALUES (null, 1, 'Some random text.'); -- Ok.
INSERT INTO `error` (left_id, right_id, message_text) VALUES (1, null, 'Some random text.'); -- Ok.
INSERT INTO `error` (left_id, right_id, message_text) VALUES (null, null, 'Some random text.'); -- ER_BAD_NULL_ERROR: Column 'left_or_right_id' cannot be null
on MySQL version 8.0.22