Adding new rows to the end of the inventory trail
The simplest test case, is to INSERT new rows at the end of the inventory trail. First, let's add an initial inventory row for each of two items, as shown in Listing 9.
Our first real tests, shown in Listing 10, prove that we cannot save a row with incorrect CurrentQuantity, even if we also enter the wrong value for PreviousQuantity.
Also, we cannot withdraw more of an item than is currently in stock.
However, we can take out a valid quantity of stock.
So far our system has worked as expected. However, it still has loopholes. The command shown in Listing 7-32 succeeds in withdrawing more stock of a given item than is available, by failing to provide a value for PreviousChangeDate.
The fundamental problem is being caused by the need to allow NULL values in the PreviousChangeDate
column, to reflect the fact that we may be starting a brand new branch
of history entries for that item, in which case no previous change date
will exist. Our FOREIGN KEY constraint (FK_Inventory_Self) tries to match the value of PreviousQuantity in the row being modified to the CurrentQuantity value in the row that describes the previous modification of the same item, based on a composite key consisting of (ItemID, PreviousChangeDate, PreviousQuantity). Since PreviousChangeDate is NULL, no match can be made and so we can enter an incorrect value for PreviousQuantity (20 instead of 5).
In order to fix this obvious loophole in our logic, we should require the value entered for PreviousChangeDate to be NOT NULL if we are saving a NOT NULL value for PreviousQuantity, as shown in Listing 16.
If we rerun Listing 15 now, the INSERT
command fails. However, there is one more problem we still need to
address. Although we already have some history of changes for item #2,
we can start another trail of history for the same item by failing to
provide either a PreviousChangeDate or PreviousQuantity, as shown in Listing 17.
In order to solve this problem, we need to find a way to prevent two rows with the same ItemID having a NULL value for PreviousChangeDate. Listing 18 creates a UNIQUE constraint to solve this problem.
When we rerun Listing 17, the INSERT fails, as it should, and the problem has been fixed.
SQL Server UNIQUE constraints and the ANSI Standard
In SQL Server, a UNIQUE constraint disallows duplicate NULLs. However, according to the ANSI standard it should allow them. Therefore, the UNIQUE constraint in Listing 18 will work on SQL Server but will not port to a RDBMS that implements ANSI-compliant UNIQUE constraints.