Explicitly Set ValidFrom in Temporal Table - sql-server

I'm trying to set the ValidFrom range for the current record in a temporal table. I'm doing this because I'm rebuilding history from another system (non SQL) into a data warehouse so the current version of records may be "as of" a date that's in the past. If I can't get this to work, my fall back is to add a row in the history table that fills in the gap but I'm thinking there's a way to get this to work. Maybe there are some ways with alter columns?
/******** CURRENT TIME=2018-03-10 15:32:26 *****/
CREATE TABLE TestHist(
ID int NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) NOT NULL,
ValidTo datetime2(7) NOT NULL
)
GO
CREATE TABLE Test(
ID int IDENTITY(1,1) NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo datetime2(7) GENERATED ALWAYS AS ROW END NOT NULL,
PRIMARY KEY CLUSTERED (ID ASC) ,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.TestHist ) )
GO
ALTER TABLE Test SET (SYSTEM_VERSIONING = OFF)
go
--THIS WORKS BUT SETS THE VALIDFROM TO CURRENT TIME
insert into Test(name) values ('fred')
--AND BTW, THIS IS HOW I LOAD THE HISTORY (THIS WORKS TOO)
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'joe',null,'1/1/18','1/15/18')
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'steve','fred','2/1/18','3/1/18')
But the problem is that it sets the current ValidFrom time arbitrarily to when you do your insert statement:
select * from test
ID Name ParentName ValidFrom ValidTo
1 fred NULL 2018-03-10 15:32:26.4403041 9999-12-31 23:59:59.9999999
And here's what I wish I could do:
--THIS DOESN'T WORK
insert into Test(name,ValidFrom,ValidTo) values ('fred','2/1/18','9999-12-31 23:59:59.997')
I get this error:
Msg 13536, Level 16, State 1, Line 38
Cannot insert an explicit value into a GENERATED ALWAYS column in table 'CodeAnalytics.dbo.Test'. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column.

You cannot update ValidFrom on the temporal table. However, you can update ValidFrom on the history table that keeps track of changes. You create a record there by changing any value in the temporal table.
So you can do following steps:
Change anything in rows of your temporal table where you want to change the value of the ValidFrom column. This step creates a record in the history table for every changed record in the original table.
Set system versioning off for your temporal table.
Update ValidFrom in your history table.
Set system versioning back on for your temporal table.

Edit: oops. it's 2019 now. anyway, i needed to do this, so leaving here in case anyone else finds useful.
maybe something like this is what you're looking for?
CREATE TABLE TestHist(
ID int NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) NOT NULL,
ValidTo datetime2(7) NOT NULL
)
GO
CREATE TABLE Test(
ID int IDENTITY(1,1) NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo datetime2(7) GENERATED ALWAYS AS ROW END NOT NULL,
PRIMARY KEY CLUSTERED (ID ASC) ,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.TestHist ) )
GO
ALTER TABLE Test SET (SYSTEM_VERSIONING = OFF);
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'steve','2/1/18','3/1/18')
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'joe','1/1/18','1/15/18')
SET IDENTITY_INSERT Test ON;
insert into Test(id, name) values (1, 'fred')
--after dropping period one can update validfrom on temporal table from max history
alter table Test DROP PERIOD FOR SYSTEM_TIME;
GO
update test set validfrom =(select max(validto) from TestHist where id=test.ID);
--now add period and turn system versioning back on
alter table test ADD PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
GO
alter table test set (system_versioning = on (HISTORY_TABLE=dbo.TestHist));
GO
--think this gives what you want
select * from test for system_time all

Related

Can a temporal table historize insert?

I have created the following temportal table
/*
ALTER TABLE [dbo].________Test SET ( SYSTEM_VERSIONING = OFF)
GO
DROP TABLE dbo.________Test
GO
DROP TABLE dbo.________TestHistory
GO
*/
CREATE TABLE dbo.________Test
(
DeptID INT NOT NULL PRIMARY KEY CLUSTERED
, DeptName VARCHAR(50) NOT NULL
, ManagerID INT NULL
, ParentDeptID INT NULL
, SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL
, SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL
, PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)
-- ) WITH (SYSTEM_VERSIONING = ON);
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.________TestHistory));
Now I can insert, update and delete values.
But the values do only get historized on an update or a delete.
Is there a way to historize the insert-statements, too ?
I want to avoid having to write a trigger for that, if at all possible...
Having the data in two tables is extremely disadvantageous, because then i need to create a union first, and miss out on any field created afterwards, or I have potentially garbage columns if I use SELECT * and create a union-view, if the column-order gets altered, plus new columns would be missing, too.
Hmmm, those temporal tables seem to be really stupid(ly made)...
Apparently, they historize every potential change, even if there's no change in the data.
So I can do this:
CREATE TRIGGER TR_________Test_AfterInsert ON dbo.________Test
AFTER INSERT
AS
BEGIN
UPDATE dbo.________Test
SET DeptID = DeptID
WHERE DeptID IN
(
SELECT DeptID
FROM inserted
)
END
And the entries will swiftly be historized after insert.
On the other hand, this is nice, since then I don't have to manually synchronize the table-schema with the history-table-schema.

Capture SysEndTime on SQL Server temporal table delete

is it possible to capture the SysEndTime temporal timestamp with an OUTPUT keyword?
For example:
DECLARE #MyTableVar TABLE (sysendtime datetime2);
DELETE
FROM dbo.someTable
OUTPUT DELETED.sysendtime INTO #MyTableVar
WHERE
colA = 'something';
SELECT sysendtime FROM #MyTableVar;
The last SELECT is returning 9999-12-31 23:59:59.000 which is the value before the DELETE exec'd.
I read that SysEndTime is set at the end of the transaction so it wouldn't be visible until all the calls in that blocked completed but that'd mean I'd have to do a secondary query on the someTable table using the "for system_time all" syntax and retrieving the max(SysEndTime) column to capture the most recent change and with that I wouldn't be guaranteed that it's the delete that as an UPDATE would set SysEndTime too.
No this is not possible through the OUTPUT clause because the value does not exist in a column in the system versioned table.
SQL Server uses the transaction start time - not end time so logically it shouldn't be inherently impossible. The execution plan calls systrandatetime() in the compute scalar to get the value to use when inserting to the history table.
This is an internal function though and not exposed to us. The following query run in the same transaction should likely return something close but this is datetime datatype so will get the legacy rounding behaviour to 1/300 of a seconds. I doubt that you will find any guarantees that this even uses the exact same underlying data source either.
SELECT transaction_begin_time
FROM sys.dm_tran_active_transactions
WHERE transaction_id = (SELECT transaction_id
FROM sys.dm_tran_current_transaction);
I would not suggest using the transaction DMVs directly. However as the row already exists in the history table when the DELETE statement finishes you can (inside the same transaction) just query the history table to get the highest sysendtime for the primary key of one of the deleted rows (the PK can be captured with OUTPUT if needed).
e.g. Setup
CREATE TABLE dbo.someTable
(
PK INT PRIMARY KEY,
ColA VARCHAR(15),
[ValidFrom] datetime2 (2) GENERATED ALWAYS AS ROW START ,
[ValidTo] datetime2 (2) GENERATED ALWAYS AS ROW END ,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.someTableHistory))
INSERT INTO dbo.someTable
(PK,
ColA)
VALUES (1,'something'),
(2,'something');
Then you could achieve this as below
DECLARE #DeletedPks TABLE
(
PK INT
)
BEGIN TRAN
DELETE FROM dbo.someTable
OUTPUT DELETED.PK
INTO #DeletedPks
WHERE colA = 'something';
SELECT MAX(ValidTo)
FROM dbo.someTableHistory
WHERE PK = (SELECT TOP 1 PK /*Doesn't matter which PK we choose*/
FROM #DeletedPks)
COMMIT
You can get ROW START but as you say not ROW END
create table dbo.someTable
(
--sysendtime datetime2 ,
ColA varchar(15),
[ValidFrom] datetime2 (2) GENERATED ALWAYS AS ROW START
, [ValidTo] datetime2 (2) GENERATED ALWAYS AS ROW END
, PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
GO
✓
insert into someTable (ColA) values ( 'something')
GO
1 rows affected
DECLARE #MyTableVar TABLE (sysendtime datetime2);
DELETE FROM dbo.someTable
OUTPUT DELETED.ValidFrom INTO #MyTableVar
WHERE colA = 'something';
SELECT sysendtime FROM #MyTableVar;
GO
| sysendtime |
| :------------------ |
| 06/02/2019 20:52:54 |
DECLARE #MyTableVar TABLE (sysendtime datetime2);
DELETE FROM dbo.someTable
OUTPUT DELETED.ValidTo INTO #MyTableVar
WHERE colA = 'something';
SELECT sysendtime FROM #MyTableVar;
GO
| sysendtime |
| :------------------ |
| 31/12/9999 23:59:59 |
*db<>fiddle here**
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION;
CREATE TABLE #SYSUTCTRANSBEGINTIME (PK int, ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START, ValidTo datetime2(7) GENERATED ALWAYS AS ROW END, PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo));
WAITFOR DELAY '00:00:00.100';
SELECT SYSUTCDATETIME() AS SYSUTCDATETIME;
insert into #SYSUTCTRANSBEGINTIME (PK) values (0);
select ValidFrom as SYSUTCTRANSBEGINTIME from #SYSUTCTRANSBEGINTIME;
go
ROLLBACK;

ALTER table to increase column size does not reflect in History (versioned) table

I am currently using SQL Server 2016 and I have a table with SYSTEM VERSIONING TURNED ON. My alter command to increase the column size does not reflect in the history table (versioned table). Please can you advise?
I created the table using the below command
CREATE TABLE Report (
ReportId INTEGER NOT NULL,
ReportName VARCHAR(300) NULL,
CONSTRAINT PK_RPT PRIMARY KEY (ReportId)
)
ALTER TABLE Report
ADD ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL DEFAULT SYSUTCDATETIME(),
ValidTo DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
ALTER TABLE ICEBERG.Report
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ICEBERG.ReportHistory));
I am now trying to modify the column size ReportName in this table but the change in column size does not reflect in the ReportHistory table.
ALTER TABLE ICEBERG.Report SET (SYSTEM_VERSIONING = OFF); -- note comment below
ALTER TABLE ICEBERG.Report ALTER COLUMN ReportName VARCHAR(500) NULL;
ALTER TABLE ICEBERG.Report SET (SYSTEM_VERSIONING = ON); -- note comment below
Please note , I have tried to execute only the alter table command without turning SYSTEM_VERSIONING ON/OFF as above, even that does not help.

How to automatically create rows and pass values to other tables

There are three tables in database:
"BusinessEntity " which has the identity column "BusinessEntityID" as Primary Key (as well as rowguid and ModifiedDate columns).
"Firm" which has similarly the identity column "BusinessEntityID" as Primary Key, which is also a Foreign Key to BusinessEntity.BusinessEntityID (it has a 1-to-1 relationship with "BusinessEntity" table, FirmName, rowguid and ModifiedDate columns ).
"Customer" which has the identity column "CustomerID" as Primary Key and column "FirmID" as Foreign Key to Firm .BusinessEntityID (plus CustomerName, rowguid and ModifiedDate columns).
i.e. (also see image)
tables: BusinessEntity Firm Customer
columns: CustomerID (PK)
BusinessEntityID(PK) --> BusinessEntityID (PK/FK) --> FirmID (FK)
What I'm trying to do is whenever a new Customer row is to be created:
A new BusinessEntity row to be created automatically and then pass its BusinessEntityID value to an (automatically) newly created Firm row which it turn would pass its own BusinessEntityID to Customer table as FirmID column.
As you can see a BusinessEntity row was no meaning unless it corresponds to a Firm (or other entities) and a Customer must include a Firm.
I created a view containing all three tables along with a trigger to do the job without success. Any suggestions?
The tables:
BusinessEntity
CREATE TABLE [dbo ].[BusinessEntity](
[BusinessEntityID] [int] IDENTITY(1,1) NOT NULL,
[rowguid] [uniqueidentifier] NOT NULL,
[ModifiedDate] [datetime] NOT NULL,
CONSTRAINT [PK_BusinessEntity_BusinessEntityID] PRIMARY KEY CLUSTERED
(
[BusinessEntityID] ASC
)
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[BusinessEntity] ADD CONSTRAINT [DF_BusinessEntity_rowguid]
DEFAULT (newid()) FOR [rowguid]
GO
ALTER TABLE [dbo ].[BusinessEntity] ADD CONSTRAINT [DF_BusinessEntity_ModifiedDate]
DEFAULT (getdate()) FOR [ModifiedDate]
GO
Firm
CREATE TABLE [dbo].[Firm](
[BusinessEntityID] [int] IDENTITY(1,1) NOT NULL,
[FirmName] [nvarchar](30) NULL,
[rowguid] [uniqueidentifier] NOT NULL,
[ModifiedDate] [datetime] NOT NULL,
CONSTRAINT [PK_Firm_BusinessEntityID] PRIMARY KEY CLUSTERED
(
[BusinessEntityID] ASC
)
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Firm] ADD CONSTRAINT [DF_Firm_rowguid]
DEFAULT (newid()) FOR [rowguid]
GO
ALTER TABLE [dbo].[Firm] ADD CONSTRAINT [DF_Firm_ModifiedDate]
DEFAULT (getdate()) FOR [ModifiedDate]
GO
ALTER TABLE [dbo].[Firm] WITH CHECK ADD CONSTRAINT [FK_Firm_BusinessEntity_BusinessEntityID] FOREIGN KEY([BusinessEntityID])
REFERENCES [dbo].[BusinessEntity] ([BusinessEntityID])
GO
ALTER TABLE [dbo].[Firm] CHECK CONSTRAINT [FK_Firm_BusinessEntity_BusinessEntityID]
GO
Customer
CREATE TABLE [dbo].[Customer](
[CustomerID] [int] IDENTITY(1,1) NOT NULL,
[FirmID] [int] NULL,
[CustomerName] [nvarchar](28) NULL,
[rowguid] [uniqueidentifier] NOT NULL,
[ModifiedDate] [datetime] NOT NULL,
CONSTRAINT [PK_Customer_CustomerID] PRIMARY KEY CLUSTERED
(
[CustomerID] ASC
)
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Customer] ADD CONSTRAINT [DF_Customer_rowguid]
DEFAULT (newid()) FOR [rowguid]
GO
ALTER TABLE [dbo].[Customer] ADD CONSTRAINT [DF_Customer_ModifiedDate]
DEFAULT (getdate()) FOR [ModifiedDate]
GO
ALTER TABLE [dbo].[Customer] WITH CHECK ADD CONSTRAINT [FK_Customer_Firm_FirmID] FOREIGN KEY([FirmID])
REFERENCES [dbo].[Firm] ([BusinessEntityID])
GO
ALTER TABLE [dbo].[Customer] CHECK CONSTRAINT [FK_Customer_Firm_FirmID]
GO
Something weird happens here. I created this stored procedure:
CREATE PROCEDURE [dbo].[CreateFirmCustomer](#FirmName NVARCHAR(30), #CustomerName NVARCHAR(28)) AS
BEGIN;
SET NOCOUNT ON;
BEGIN TRANSACTION;
INSERT BusinessEntity DEFAULT VALUES;
DECLARE #BusinessEntityID INT = SCOPE_IDENTITY();
SET IDENTITY_INSERT [dbo].[Firm] ON
INSERT Firm(BusinessEntityID, FirmName)
VALUES (#BusinessEntityID, #FirmName);
SET IDENTITY_INSERT [dbo].[Firm] OFF
INSERT Customer(FirmID, CustomerName)
VALUES (#BusinessEntityID, #CustomerName);
DECLARE #CustomerID INT = SCOPE_IDENTITY();
SELECT #BusinessEntityID AS FirmID, #CustomerID AS CustomerID;
COMMIT;
END;
GO
When I run it sometimes the CustomerID column gets the value of BusinessEntityID column when it should really be independently auto-generated. Also the BusinessEntityID column auto-generates weird values e.g. jumped from value 7 to value 1002. (BusinessEntityID is BusinessEntity.BusinessEntityID ) Any clues? (see picture)
Now I created this view to insert Customers as Firms:
CREATE VIEW [dbo].[vBusEntityFirmCustomer]
AS
SELECT dbo.Firm.FirmName, dbo.Customer.CustomerName
FROM dbo.BusinessEntity INNER JOIN
dbo.Firm ON dbo.BusinessEntity.BusinessEntityID = dbo.Firm.BusinessEntityID INNER JOIN
dbo.Customer ON dbo.Firm.BusinessEntityID = dbo.Customer.FirmID
GO
And this trigger on the view:
CREATE TRIGGER [dbo].[trg_FirmCustomer]
ON [dbo].[vBusEntityFirmCustomer]
INSTEAD OF INSERT
AS
exec [dbo].[CreateFirmCustomer]
GO
But every time I enter a new FirmName CustomerName to insert a new row I get this message (see image):
Procedure or function 'CreateFirmCustomer' expects parameter '#FirmName', which was not supplied.
The fact is that I do supply FirmName.
Logically, as designed, you have to create a BusinessEntity first, then a Firm, then a Customer. Across all these tables, the only real information you're storing is the firm name and the customer name -- all the rest is derived and autogenerated by the database. We can encapsulate the operation CreateCustomer in a stored procedure:
CREATE PROCEDURE CreateCustomer(#FirmName NVARCHAR(30), #CustomerName NVARCHAR(28)) AS
BEGIN;
SET NOCOUNT ON;
BEGIN TRANSACTION;
INSERT BusinessEntity DEFAULT VALUES;
DECLARE #BusinessEntityID INT = SCOPE_IDENTITY();
INSERT Firm(BusinessEntityID, FirmName)
VALUES (#BusinessEntityID, #FirmName);
INSERT Customer(FirmID, CustomerName)
VALUES (#BusinessEntityID, #CustomerName);
DECLARE #CustomerID INT = SCOPE_IDENTITY();
-- Return IDs of the newly created rows as the result set
SELECT #BusinessEntityID AS FirmID, #CustomerID AS CustomerID;
COMMIT;
END;
Invoke as (for example) EXEC CreateCustomer 'Firm', 'Customer'. With the table definitions as given, this will fail because Firm.BusinessEntityID is an IDENTITY -- if it is to take its value from BusinessEntity, it shouldn't be. (You can work around this with IDENTITY_INSERT, but in a properly designed database this shouldn't be necessary.)
Another thing that's obviously weird is that we insert no business data at all in BusinessEntity (which is why we need the DEFAULT VALUES syntax) -- it's nothing but a super-general container of IDs, so it's of dubious value. Nevertheless, this demonstrates the general technique of inserting rows in multiple tables that have dependencies.
As written, this stored procedure always creates a new Firm and BusinessEntity to go along with the Customer. Logically, a Firm can have more than one Customer, so you probably want another stored procedure to create a Customer for an existing Firm. This is simpler, as it's just an INSERT in Customer with the appropriate FirmID. You may wish to have a separate CreateFirm stored procedure that you call first, followed by a CreateCustomer to add a customer for that firm.
According to me,
it all depend how and when those 3 tables are populated.
Suppose those three table are populated using single UI, then
I will write them in single proc within one transaction.
Suppose those 3 table will be will populated at diff stage i.e diff UI then i write them in diff proc as you have already define constraint.
BTW what is the purpose of rowguid in all 3 tables.

Calculate a column from a table depending on different column from another tablecomputed column based on different

I have tables:
create table Aprovizionari
(
ID_Aprovizionare int identity(1,1) primary key,
CodCarte char(3) foreign key references Carti(CodCarte),
CodLibrarie char(3) foreign key references Librarii(CodLibrarie),
DataAprovizionare date default getdate(),
Cantitate int default 1
)
create table Returnari
(
CodRet char(3) primary key,
CodCarte char(3) foreign key references Carti(CodCarte),
CodLibrarie char(3) foreign key references Librarii(CodLibrarie),
CodEditura char(4) foreign key references Edituri(CodEditura),
DataRet date,
Cantitate int
)
I have a trigger that decrement the column Cantitate(Quantity) from Aprovizionari(Supply) while I add in Cantitate from another table Facturi(Invoices).
In Returnari(Returns -of books) I should have:
DataRet date,--this should be =DataAprovizionare+3 mounths
Cantitate int--this should be=the remaining Cantitate(Quantity) from Aprovizionari at date=DataAprovizionare+3 mounts
To do this you're going to need to check every day for Aprovizionari records that are 3 months old. When you find one you'll INSERT the Returnari record.
To do this you've got to create a stored procedure that when executed will create the Returnari records and then schedule a SQL Server Agent job to execute the procedure every day.
The stored procedure will look something like:
CREATE PROCEDURE YourDailyProc ( #Date DATE )
AS
INSERT INTO Returnari (CodCarte, CodLibrarie, DataRet, Cantitate)
SELECT CodCarte, CobLibrarie, CAST(#Date AS DATE), Cantitate
FROM Aprovizionari
WHERE DataAprovizionare = CAST(DATEADD(MONTH, -3, #Date) AS DATE)
GO
It's not clear where you'd get CodEditura but presumably you just need to join to another table.
Now you need a job to execute EXEC YourDailyProc #Date=CURRENT_TIMESTAMP each day. This answer looks like it has all the information you'll need to do that - how to schedule a job for sql query to run daily?

Resources