0

I have two tables:

Book(BookID, Title, Author, Decision)

BookShipment(BookID, ShipmentID)

CREATE TABLE BookShipment(
BookID CHAR(4),
ShipmentID(7)
CONSTRAINT pk_BookShipment PRIMARY KEY (BookID, ShipmentID),
CONSTRAINT fk_BookShipment_Book FOREIGN KEY (BookID) REFERENCES Book(BookID));

The idea is that a Book needs to be "Approved" before it's added to a Shipment. If it's "Rejected" it won't be added.

Is there a way to add an additional constraint to BookShipment that, when a new BookID is added, would check that Decision under the Book table is equal to Approved (for that BookID)?

  • Does this answer your question? [Sub queries in check constraint](https://stackoverflow.com/questions/13000698/sub-queries-in-check-constraint) – astentx Jan 21 '21 at 10:57
  • @astentx It looks like it might. However it looks as though the op hasn't set a foreign key in his table, whereas I have - so unsure if I need to go as far as creating a subquery or function. I think using the `CHECK` functionality might work, but I'm unsure how to implement this as of yet. –  Jan 21 '21 at 11:09
  • 1
    Having a primary key on `BookID` in the table `BookShipment` looks wrong. That means you can only ship a book once; ever. – Thom A Jan 21 '21 at 11:16
  • @AaronWright No, [this is the limitation](https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-column-constraint-transact-sql?view=sql-server-ver15): *Is a logical expression used in a CHECK constraint and returns TRUE or FALSE. logical_expression used with CHECK constraints cannot reference another table but can reference other columns in the same table for the same row. The expression cannot reference an alias data type.* Very little description – astentx Jan 21 '21 at 11:21
  • @Larnu Think I made a mistake when typing it out there. Primary key should be `(BookID, ShipmentID)` –  Jan 21 '21 at 11:23
  • Can the decision change after a book has been added to "shipment"? If so, what happens? – Gordon Linoff Jan 21 '21 at 12:52

2 Answers2

2

If you have only a single thing to check, then astentex's answer will do you nicely. But if you have arbitrary constraints, especially spanning multiple tables, there is a different option which is more flexible.

It is based around a trick involving Indexed Views. I got this from an article by spaghettidba.

An indexed view is a view that is persisted to disk. We create it by creating a clustered index on the view. There are many limitations to it, crucially in our case that we can't use left/right/full join, only inner is allowed. It also must be schema-bound (you can't change the underlying columns), and must reference tables with two-part names.

Let us suppose that the opposite of your constraint is true: there are rows in BookShipment for which the relevant Book is not Approved. How can we see such Books in a view:

CREATE /* OR ALTER */ VIEW dbo.vwNonApprovedBooks
WITH SCHEMABINDING
AS
SELECT b.BookId
    FROM dbo.BookShipment AS bs
    JOIN dbo.Book AS b ON b.BookID = bs.BookID
    WHERE b.Decision <> 'Approved';
GO

We could index this by creating a clustered index, DO NOT do this yet:

CREATE UNIQUE CLUSTERED INDEX CX_vwNonApprovedBooks ON dbo.vwNonApprovedBooks (BookId);

Now we will pull a little trick. If we want to stop any rows existing in this view, we need to force every inserted row to multiply out so that it fails the unique constraint.

Let us create a table for this:

CREATE TABLE dbo.DummyTwoRows (x bit NOT NULL PRIMARY KEY);
GO
INSERT dbo.DummyTwoRows VALUES (0),(1);

Now we can redefine the view like this:

CREATE /* OR ALTER */ VIEW dbo.vwNonApprovedBooks
WITH SCHEMABINDING
AS
SELECT 1 AS DummyOne
    FROM dbo.BookShipment AS bs
    JOIN dbo.Book AS b ON b.BookID = bs.BookID
    CROSS JOIN dbo.DummyTwoRows
    WHERE b.Decision <> 'Approved';
GO
CREATE UNIQUE CLUSTERED INDEX CX_vwNonApprovedBooks ON dbo.vwNonApprovedBooks (DummyOne);

And on any insert into BookShipment with a Book that is not Approved, the unique constraint will fail.

SQL Server will maintain this view on inserts and updates, so that if a Book is changed to not Approved where it has BookShipment, the constraint will fail the update also.

Note that this index takes up no space as there are never any rows in it.

Charlieface
  • 52,284
  • 6
  • 19
  • 43
0

If you'll always have a single status to check, this can be done with little tricks on FK constraint:

  • Create dummy unuque index on Books(BookId, Decision).
  • Add calculated column to BookShipment with value Approved.
  • Reference the created unique index in FK constraint.

Defining UDF in CHECK constraint should be more flexible way for this.

create table book (
  BookID int identity(1,1) primary key,
  Title varchar(100),
  Author varchar(100),
  Decision varchar(100),
  
  --Dummy constraint for FK
  constraint u_book unique(bookid, decision)
);

CREATE TABLE BookShipment(
  BookID int,
  ShipmentID varchar(7),
  --Dummy column for FK
  approved as cast('Approved' as varchar(100)) persisted
  
  CONSTRAINT pk_BookShipment PRIMARY KEY (BookID),
  CONSTRAINT fk_BookShipment_Book_Approved
    FOREIGN KEY (BookID, approved)
    REFERENCES Book(BookID, decision)
);

insert into book (Title, Author, Decision)
select 'A', 'B', 'Approved' union all
select 'A', 'B', 'New'
;

--2 rows affected

insert into BookShipment values(1, 1);

--1 rows affected

insert into BookShipment values(2, 2);

/*

insert into BookShipment values(2, 2);


Msg 547 Level 16 State 0 Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "fk_BookShipment_Book_Approved". The conflict occurred in database "fiddle_ea408f09b06247a78b47ea9c353eda10", table "dbo.book".
Msg 3621 Level 0 State 0 Line 1
The statement has been terminated.
*/

db<>fiddle here

astentx
  • 6,393
  • 2
  • 16
  • 25
  • @astentx... Thanks for this. I'll give this a go. A lot of this seem a bit beyond me however. Is this just the kind of stuff you learn the longer you spend practicing SQL and database creation? –  Jan 21 '21 at 15:37
  • 1
    @AaronWright The most essential part, I think, is to know the functionality that your database can offer, how it works with data and why some limitations can exist (what is the functional reason of having them). Of course, practice has heavy impact since it creates a lot of cases to solve. But I do not know some formal way or to-do list to invent workarounds) – astentx Jan 21 '21 at 21:53