Postgres CHECK constraint executing before ON CONFLICT - sql

I have this historical stock movements table, that saves the stock losses per day:
CREATE TABLE stock (
item_id INTEGER NOT NULL,
date DATE NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity >= 0),
PRIMARY KEY (item_id, date)
);
And this INSERT/ON CONFLICT (To update instead, atomically):
INSERT INTO stock.loss (item_id, date, quantity)
VALUES(2, '2021-10-03', -1)
ON CONFLICT (item_id, date)
DO
UPDATE SET quantity = stock.quantity + excluded.quantity;
With this, I insert a movement for a day, or update it if it already exists.
The thing is, if "quantity" is negative (see query above), the CHECK constraint throws:
SQL Error [23514]: ERROR: new row for relation "stock" violates check constraint "stock_quantity_check"
Detail: Failing row contains (2, 2021-10-03, -1).
If I change the INSERT to an UPDATE, it works as expected. As I see, it's running the check on the temporary row instead of the final row, after the ON CONFLICT.
Is there any way to avoid this, and keep the CHECK? I would prefer to avoid a SELECT+INSERT/UPDATE, and make a direct "upsert".

Related

sql unique constraint with time window

I have a table where records have a (begin, end) time window of existence (for things like employement duration, birth and death, rent duration, ...)
begin IS NULL or end IS NULL if there is no bound.
CREATE TABLE mytable(
id int primary key,
value int, --UNIQUE at any point in time
begin datetime NULL,
end datetime NULL
);
I want column value to be unique at any point in time.
INSERT INTO mytable VALUES(1, 1, '2021-07-23', '2021-07-24'),(2, 1, '2021-07-25', NULL);
Is OK
Whereas
INSERT INTO mytable VALUES(1, 1, '2021-07-23', '2021-07-30'),(2, 1, '2021-07-25', NULL);
Is not OK, because both records have value=1 and overlapping time windows.
Is there a way to enforce such a constraint in SQL ?
You can't do this on the table, no, as there's nothing to make UNIQUE on.
What you could do, however, is use a VIEW to enforce it.
Firstly, let's create your table. I assume the columns datetime, should actually be begin and end; I recommend against these names as they are reserved keywords. As such I am calling them DateBegin and DateEnd. I am also assuming that they are date only (no time portion) values and so define them as a date not a datetime:
CREATE TABLE dbo.mytable(ID int primary key,
Value int,
[BeginDate] date NULL,
[EndEnd] date NULL);
And we'll INSERT your first 2 rows, as they are "ok":
INSERT INTO dbo.mytable (ID, Value, BeginDate, EndDate)
VALUES(1, 1, '20210723', '20210724'),
(2, 1, '20210725', NULL);
Now we need to make a VIEW, but we need one row per date. As such you'll want to create a Calendar Table. I'm not going to cover how to create one here, but there are literally 100's of articles, such as there on SQL Server Central: Bones of SQL - The Calendar Table, Calendar Tables in T-SQL.
Once you have your Calendar table, you can create the VIEW below, which JOINs the data in your table to the calendar table. We're going to make it so that the VIEW just returns the columns value and the date. WE're also going to schemabind it; this means we'll be able to add an UNIQUE INDEX to it:
CREATE VIEW dbo.MyView
WITH SCHEMABINDING
AS
SELECT MT.[Value],
CT.CalendarDate
FROM dbo.MyTable MT
JOIN dbo.CalendarTable CT ON MT.BeginDate <= CT.CalendarDate --I assume, despite your schema, MT.BeginDate can't be NULL
AND (MT.EndDate >= CT.CalendarDate OR MT.EndDate IS NULL);
Now we have a VIEW that has a row for each date, and for each value. This means we can now create our UNIQUE INDEX:
CREATE UNIQUE CLUSTERED INDEX MyIndex ON dbo.MyView ([Value], CalendarDate);
Now if we try to INSERT a row that is on the same date and value, we'll get an error:
INSERT INTO dbo.MyTable (ID, Value, BeginDate, EndDate)
VALUES(3, 1, '20210720', '20210723');
Cannot insert duplicate key row in object 'dbo.MyView' with unique index 'MyIndex'. The duplicate key value is (1, 2021-07-23).

Declarative approach to constrain data ranges in table

I would like to learn a declarative approach for a data constraint issue I have had from time to time related to exclusive date ranges.
Below is a simplified example. I have items and prices on those items. I want the effective date range of the prices to be mutually exclusive with no overlap.
As I understand things with Oracle, user-defined functions are not eligible for use in CONSTRAINT declarations - and I can't even imagine how poorly it would perform if it were allowed. So I require a procedural approach using triggers. Typical trigger source is also included below.
I am not interested in learning better procedural logic for use within the trigger (it is just a simple demonstrative example). I am interested in learning a more declarative solution to a relatively popular data constraint issue I (and likely others) face.
I want to eliminate trigger-based solutions whenever I can, as a practice. However I can't seem to find my way out of this requirement without a trigger.
create table item ( title varchar2(32) primary key );
create table price (
item varchar2(32) not null references item (title),
price number(9,2),
effective_from date not null,
effective_to date not null,
constraint price_from_to_ck check (effective_to > effective_from ));
[REDACTED]
*(A combination of row and statement level triggers inteneded to prevent logical chronological overlap)
insert into item values ('LETTUCE');
insert into item values ('WHISKY');
insert into price values ( 'LETTUCE', 1.05, date '2013-01-01', date '2013-03-31' );
insert into price values ( 'LETTUCE', 1.08, date '2013-04-01', date '2013-06-30' );
insert into price values ( 'WHISKY', 33.99, date '2013-01-01', date '2013-05-31' );
insert into price values ( 'WHISKY', 31.15, date '2013-06-01', date '2013-07-31' );
-- should fail
insert into price values ( 'WHISKY', 30.55, date '2013-05-15', date '2013-06-05' );
While waiting for next Oracle 12c version, which supports Temporal Validity, I still use next approach:
create table item ( title varchar2(32) primary key );
create table price (
price_id number primary key,
item varchar2(32) not null references item (title),
price number(9,2),
effective_from date not null,
effective_to date not null,
effective_prev_to date,
constraint price_from_to_ck check ( effective_to > effective_from ),
constraint price_to_prev_ck check ( effective_from = effective_prev_to + 1 ),
constraint price_from_uq unique ( item, effective_to ),
constraint price_dates_chain_fk foreign key ( item, effective_prev_to ) references price ( item, effective_to ) );
insert into item values ('LETTUCE');
insert into item values ('WHISKY');
insert into price values ( 1, 'LETTUCE', 1.05, date '2013-01-01', date '2013-03-31', null );
insert into price values ( 2, 'LETTUCE', 1.08, date '2013-04-01', date '2013-06-30', date '2013-03-31' );
insert into price values ( 3, 'WHISKY', 33.99, date '2013-01-01', date '2013-05-31', null );
insert into price values ( 4, 'WHISKY', 31.15, date '2013-06-01', date '2013-07-31', date '2013-05-31' );
Let's try:
insert into price values ( 5, 'WHISKY', 30.55, date '2013-05-15', date '2013-06-05', date '2013-05-14' );
ORA-02291: integrity constraint (USER_4_E7DF1.PRICE_DATES_CHAIN_FK) violated - parent key not found : insert into price values ( 'WHISKY', 30.55, date '2013-05-15', date '2013-06-05', date '2013-05-14' )
But now updating and deleting dates in the middle of the chain is pain in the ass. It needs to change preceding and following rows in one statement with MERGE. That's why I've added price_id column, because you can't update a key in MERGE -- so, you need another key instead of (item, effective_%).
You can do this declaritively with a materialized view, as first suggested by Brian Camire. Here's an example:
--Original tables (with an extra primary key on PRICE)
create table item ( title varchar2(32) primary key );
create table price (
id number primary key,
item varchar2(32) not null references item (title),
price number(9,2),
effective_from date not null,
effective_to date not null,
constraint price_from_to_ck check (effective_to > effective_from ));
create materialized view log on price with rowid;
--Items with overlapping dates
create materialized view price_no_overlap_mv
refresh fast on commit as
select 'overlapping row' as dummy, price1.rowid rowid1, price2.rowid rowid2
from price price1, price price2
where
--Same item
price1.item = price2.item
--Overlapping dates
and (price1.effective_from <= price2.effective_to and price1.effective_to >= price2.effective_from)
--Don't compare the same row
and price1.id <> price2.id
;
--Throw an error if any rows ever get created.
alter table price_no_overlap_mv
add constraint price_no_overlap_mv_ck check (dummy = 'no rows allowed');
insert into item values ('LETTUCE');
insert into item values ('WHISKY');
insert into price values (1, 'LETTUCE', 1.05, date '2013-01-01', date '2013-03-31' );
insert into price values (2, 'LETTUCE', 1.08, date '2013-04-01', date '2013-06-30' );
insert into price values (3, 'WHISKY', 33.99, date '2013-01-01', date '2013-05-31' );
insert into price values (4, 'WHISKY', 31.15, date '2013-06-01', date '2013-07-31' );
commit;
-- should fail
insert into price values (5, 'WHISKY', 30.55, date '2013-05-15', date '2013-06-05' );
commit;
ORA-12008: error in materialized view refresh path
ORA-02290: check constraint (JHELLER.PRICE_NO_OVERLAP_MV_CK) violated
This declarative approach is both concurrent and consistent. But there are a lot of draw backs:
Materialized view logs, which are required for a fast refresh, are only supported in Enterprise Edition.
Your table needs a primary key, although you probably already have one but just didn't include it in the example.
Although declarative, the solution is still not straight-forward. You have to declare the opposite condition, and then check that it never exists.
Getting FAST REFRESH to work can be a nightmare for more than the simplest of queries. Even for this simple example, I had to use the old-style joins and had to add useless ROWIDs.
The constraint is not enforced until a COMMIT. Although that could be a positive thing, as many types of changes would temporarily create overlapping results. If you never allow overlapping results, you have to modify the table in a specific order.

Retrieve values in same row of SQL table

I have a table that I use to calculate prices that looks up a table to get price per part and then multiplies that by number of parts ordered.
This number of parts ordered is in the same table however, and I can't seem to find a way to access values in the same row in a table when adding values.
Is this not possible, or are there better ways of doing this?
Here is the table this concerns:
CREATE TABLE PartOrder
(
OrderID INTEGER NOT NULL,
CustomerID INTEGER NOT NULL,
PartID INTEGER NOT NULL,
NumParts INTEGER NOT NULL,
Status CHAR(1) NOT NULL
CHECK (Status IN ('R', 'H',
'E', 'C')
OrderTime TIMESTAMP NOT NULL,
TotalCost DECIMAL,
CONSTRAINT partOrder_pk PRIMARY KEY (OrderID),
CONSTRAINT partOrder_fk1 FOREIGN KEY (CustomerID) REFERENCES Customer ON DELETE CASCADE,
CONSTRAINT partOrder_fk2 FOREIGN KEY (FlightID) REFERENCES Part ON DELETE CASCADE
);
I want it so that it will take the numParts value and multiply it by the price per part referenced in the parts table. however, I can't stick to hard values as the number ordered may change later, meaning that the totalPrice will change.
At the moment my insert statement is just:
INSERT INTO PartOrder VALUES (001, 001, 001, 4, 'R', NOW(), (4*(SELECT PricePerPart FROM Part WHERE PartID = 001)));
You might want to give a look at SELECT INTO as well
http://www.postgresql.org/docs/8.1/static/sql-selectinto.html
Or You can go with
INSERT INTO PartOrder
SELECT (1,1,1,4,'R',NOW, (4*Part.PricePerPart))
FROM Part
WHERE Part.PartId = 1;

Dataintegrity between tables in SQL Server

Is it possible to add data integrity between columns in different tables in SQL Server?
I have table Pay with column Date and table Orders with column DateofOrder. And I would like to add the data integrity so the Date cannot be earlier than the DateofOrder.
And when the user insert there the same date or even earlier database would show error.
I think you mean something like this, here done with a trigger;
CREATE TRIGGER trig_pay ON Pay
FOR INSERT, UPDATE
AS
IF EXISTS(SELECT *
FROM [Order] o
JOIN inserted i
ON o.id = i.payment_id
WHERE DateOfOrder>[date])
BEGIN
RAISERROR ('Sorry, Dave', 16, 1)
ROLLBACK;
RETURN;
END
INSERT INTO [Order] values (1, GETDATE()); -- Order today
INSERT INTO Pay values (1, DATEADD(dd, -1, getdate())); -- Pay yesterday
> Sorry, Dave
Yes, you can do it by using INSTEAD OF INSERT Trigger.
You would have to use an INSTEAD OF or AFTER trigger to enforce this, you can't do it declaratively. Well you could use a check constraint with a TVF or something but I've never tried that.
I'd show sample code but I'm not sure what payroll has to do with orders. If a new order comes in, what pay date must be later than or equal to the order date? Is there some other column that relates these two tables?
It is possible without resorting to triggers.
The idea is to add the DateofOrder in the Orders table to its existing key -- let's call it order_id -- to create a compound superkey, then reference this superkey (rather than the simple key solely order_id) in the Pay table.
Here are the bare bones:
CREATE TABLE Orders
(
order_id CHAR(10) NOT NULL,
DateofOrder DATE NOT NULL,
UNIQUE (order_id), -- simple candidate key
UNIQUE (DateofOrder, order_id) -- compund superkey
);
CREATE TABLE Pay
(
order_id CHAR(10) NOT NULL,
DateofOrder DATE NOT NULL,
FOREIGN KEY (DateofOrder, order_id)
REFERENCES Orders (DateofOrder, order_id),
DateOfPayment DATE NOT NULL,
CHECK (DateofOrder < DateOfPayment),
UNIQUE (order_id)
);

How not to insert specific value into database

I have MS SQL Server database and insert some values to one of the tables.
Let's say that the table contains columns ID, int Status and text Text.
If possible, I would like to create a trigger which prevents from writing specific incorrect status (say 1) to the table. Instead, 0 should be written. However, other columns should be preserved when inserting new values:
If the new row written is (1, 4, "some text"), it is written as is.
If the new row written is (1, 1, "another text"), it is written as (1, 0, "another text")
Is it possible to create such trigger? How?
EDIT: I need to allow writing such record even if status column is invalid, so foreign keys will not work for me.
I think you would need a foreign key to ensure data integrity even if you choose to use a trigger (though I would myself prefer a 'helper' stored proc -- triggers can cause debugging hell) e.g.
CREATE TABLE MyStuff
(
ID INTEGER NOT NULL UNIQUE,
Status INTEGER NOT NULL
CHECK (Status IN (0, 1)),
UNIQUE (Status, ID)
);
CREATE TABLE MyZeroStuff
(
ID INTEGER NOT NULL,
Status INTEGER NOT NULL
CHECK (Status = 0),
FOREIGN KEY (Status, ID)
REFERENCES MyStuff (Status, ID),
my_text VARCHAR(20) NOT NULL
);
CREATE TRIGGER tr__MyZeroStuff
ON MyZeroStuff
INSTEAD OF INSERT, UPDATE
AS
BEGIN;
INSERT INTO MyZeroStuff (ID, Status, my_text)
SELECT i.ID, 0, i.my_text
FROM inserted AS i;
END;
An insert trigger has been mentioned, but another way to achieve this is to have a foriegn key on your Status column which points back to a Status table - this will not allow the write and change the value, instead it will simply disallow the write if the foriegn key is not valid.
Check out referential integrity for more info on this option.