sql-serveruser-defined-functionssql-viewtemporal-tables

How to use FOR SYSTEM_TIME clause on a view in a TVF?


I am trying to use the FOR SYSTEM_TIME clause on a view, which contains system-versioned tables, in a table-valued function.

Per Microsoft's documentation, using the FOR SYSTEM_TIME clause when querying a view should be possible: https://learn.microsoft.com/en-us/sql/relational-databases/tables/querying-data-in-a-system-versioned-temporal-table?view=sql-server-ver16

However, I am getting the following error:

Temporal FOR SYSTEM_TIME clause can only be used with system-versioned tables. 'dbo.AccountView' is not a system-versioned table.

Function, view, and table definitions:

--Account Table
CREATE TABLE [dbo].[Account](
    [Account No] [int] IDENTITY(1,1) NOT NULL,
    [Global ID]  AS ('A'+CONVERT([nvarchar](10),[Account No])) PERSISTED,
    [User] [varchar](50) NULL,
    [Version No] [tinyint] NULL,
    [Version Comment] [varchar](280) NULL,
    [Valid From] [datetime2](2) GENERATED ALWAYS AS ROW START NOT NULL,
    [Valid To] [datetime2](2) GENERATED ALWAYS AS ROW END NOT NULL,
 CONSTRAINT [PK_Account] PRIMARY KEY CLUSTERED 
(
    [Account No] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
    PERIOD FOR SYSTEM_TIME ([Valid From], [Valid To])
) ON [PRIMARY]
WITH
(
SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[hst_Account])
)
GO

ALTER TABLE [dbo].[Account] ADD  CONSTRAINT [DF_Account_ValidFrom]  DEFAULT (sysutcdatetime()) FOR [Valid From]
GO

ALTER TABLE [dbo].[Account] ADD  CONSTRAINT [DF_Account_ValidTo]  DEFAULT (CONVERT([datetime2](2),'9999-12-31 23:59:59.99')) FOR [Valid To]
GO

--Account_General Table
CREATE TABLE [dbo].[Account_General](
    [Account No] [int] NOT NULL,
    [Name] [varchar](100) NULL,
    [Main Phone] [varchar](12) NULL,
    [General Email] [varchar](100) NULL,
    [Website] [varchar](100) NULL,
    [Company] [varchar](50) NULL,
    [Valid From] [datetime2](2) GENERATED ALWAYS AS ROW START NOT NULL,
    [Valid To] [datetime2](2) GENERATED ALWAYS AS ROW END NOT NULL,
 CONSTRAINT [PK_Account_General] PRIMARY KEY CLUSTERED 
(
    [Account No] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY],
    PERIOD FOR SYSTEM_TIME ([Valid From], [Valid To])
) ON [PRIMARY]
WITH
(
SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[hst_Account_General])
)
GO

ALTER TABLE [dbo].[Account_General] ADD  CONSTRAINT [DF_Account_General_ValidFrom]  DEFAULT (sysutcdatetime()) FOR [Valid From]
GO

ALTER TABLE [dbo].[Account_General] ADD  CONSTRAINT [DF_Account_General_ValidTo]  DEFAULT (CONVERT([datetime2](2),'9999-12-31 23:59:59.99')) FOR [Valid To]
GO

ALTER TABLE [dbo].[Account_General]  WITH CHECK ADD  CONSTRAINT [FK_Account_General_AccountNo] FOREIGN KEY([Account No])
REFERENCES [dbo].[Account] ([Account No])
ON UPDATE CASCADE
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[Account_General] CHECK CONSTRAINT [FK_Account_General_AccountNo]
GO

--Account_Temp Table
CREATE TABLE [dbo].[Account_Temp](
    [Account No] [int] NOT NULL,
    [Status] [varchar](30) NULL,
    [Locked By] [varchar](50) NULL,
    [Time Locked] [datetime2](2) NULL,
 CONSTRAINT [PK_Account_Temp] PRIMARY KEY CLUSTERED 
(
    [Account No] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Account_Temp]  WITH CHECK ADD  CONSTRAINT [FK_Account_Temp_AccountNo] FOREIGN KEY([Account No])
REFERENCES [dbo].[Account] ([Account No])
ON UPDATE CASCADE
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[Account_Temp] CHECK CONSTRAINT [FK_Account_Temp_AccountNo]
GO

--AccountView
CREATE VIEW [dbo].[AccountView] AS
SELECT a.[Account No]
      ,a.[Global ID]
      ,atemp.[Status]
      ,agen.[Name]
      ,agen.[Main Phone]
      ,agen.[General Email]
      ,agen.[Website]
      ,agen.[Company]
      ,a.[Version No]
      ,atemp.[Locked by]
      ,atemp.[Time Locked]
      ,a.[User] 'Modified By'
      ,CONVERT(datetime2(2), a.[Valid From] AT TIME ZONE 'UTC' AT TIME ZONE 'Eastern Standard Time') 'Time Modified'
      ,ISNULL(ahist.[User], a.[User]) 'Created By'
      ,CONVERT(datetime2(2), ISNULL(ahist.[Valid From], a.[Valid From]) AT TIME ZONE 'UTC' AT TIME ZONE 'Eastern Standard Time') 'Time Created'
FROM [dbo].[Account] a
LEFT JOIN [dbo].[Account_Temp] atemp
ON a.[Account No] = atemp.[Account No]
LEFT JOIN [dbo].[Account_General] agen
ON a.[Account No] = agen.[Account No]
LEFT JOIN (
    SELECT ahist.*
          ,rn = ROW_NUMBER() OVER (PARTITION BY ahist.[Account No] ORDER BY ahist.[Valid From])
    FROM [dbo].hst_Account ahist) ahist
ON ahist.[Account No] = a.[Account No] AND ahist.rn = 1
GO

--Table-Valued Function
CREATE FUNCTION [dbo].[GetAsOfDate_AccountView] (@tblID int, @asOfDate datetime2(2))
RETURNS TABLE
AS RETURN
SELECT av.*
FROM [dbo].[AccountView] FOR SYSTEM_TIME AS OF @asOfDate AS av
WHERE av.[Account No] = @tblID

GO

*Edited to add script for view and tables.


Solution

  • As have been mentioned in the comments, and after much trial and error by others to create a minimal reproducible example, you can't use a variable/parameter in a (i)TVF in the FOR SYSTEM_TIME AS OF clause against a object that isn't itself isn't a temporal object. As such, even if the VIEW you are referencing does contain temporal objects you'll get a (some what misleading) error:

    Temporal FOR SYSTEM_TIME clause can only be used with system-versioned tables. 'Sandbox.dbo.PartiallyTemporalView' is not a system-versioned table.

    Why it works this way, honestly only someone from Microsoft could likely answer. You would probably be best opening a ticket (either via your support plan or via the Azure 365 Feedback SQL Community) with them to raise it as a "bug"; they may give some insight or they might even "fix" the problem (in a future version of SQL Server).

    As for a solution, that would be to repeat the definition of the VIEW in your FUNCTION instead, and use FOR SYSTEM_TIME AS OF against the relevant table(s). That appears to just be dbo.Account_General in this case:

    CREATE FUNCTION [dbo].[GetAsOfDate_AccountView] (@tblID int,
                                                     @asOfDate datetime2(2))
    RETURNS table
    AS
        RETURN SELECT a.[Account No],
                      a.[Global ID],
                      atemp.[Status],
                      agen.[Name],
                      agen.[Main Phone],
                      agen.[General Email],
                      agen.[Website],
                      agen.[Company],
                      a.[Version No],
                      atemp.[Locked by],
                      atemp.[Time Locked],
                      a.[User] AS [Modified By],
                      CONVERT(datetime2(2), a.[Valid From] AT TIME ZONE 'UTC' AT TIME ZONE 'Eastern Standard Time') AS [Time Modified],
                      ISNULL(ahist.[User], a.[User]) AS [Created By],
                      CONVERT(datetime2(2), ISNULL(ahist.[Valid From], a.[Valid From])AT TIME ZONE 'UTC' AT TIME ZONE 'Eastern Standard Time') AS [Time Created]
               FROM [dbo].[Account] a
                   LEFT JOIN [dbo].[Account_Temp] atemp ON a.[Account No] = atemp.[Account No]
                   LEFT JOIN [dbo].[Account_General] FOR SYSTEM_TIME AS OF @asOfDate agen ON a.[Account No] = agen.[Account No]
                   LEFT JOIN (SELECT ahist.*,
                                     ROW_NUMBER() OVER (PARTITION BY ahist.[Account No]
                                                        ORDER BY ahist.[Valid From]) AS rn
                              FROM [dbo].hst_Account ahist) ahist ON ahist.[Account No] = a.[Account No]
                                                                 AND ahist.rn = 1;
    
    GO